I've read the article about handling deeplinks with Compose,
but I didn't find anything for my use case - for navigation within LazyColumn.
I have the following main Composable which I want to make the root of the graph (I want it to be host for NavHost):
#Composable
private fun MainScreenContainer(
mainState: state,
...
) {
val navController = rememberNavController()
MainTheme {
Box(
modifier = Modifier.fillMaxSize()
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
mainScreenContent(
state = mainState,
...
)
}
StickyToolbar(
...
)
}
}
}
and here is the remaining code:
private fun LazyListScope.mainScreenContent(
mainState: MainState,
...
) {
header(...)
overview(...)
myCard(...)
footer()
}
private fun LazyListScope.myCard(
...
) {
item {
ContentContainer(
modifier = Modifier.padding(vertical = MainTheme.spacing.medium)
) {
MyCardStateless(
onCardClick = myCardViewModel::onCardClick,
...
)
}
}
}
#Composable
fun MyCardStateless(
onCardClick: () -> Unit,
...
) {
Column(modifier = modifier) {
Card(
modifier = Modifier.fillMaxWidth()
.clickable(onClick = onCardClick)
) { ... }
}
}
And within MyCardViewModel I was using navigation to another fragment via Navigation Component:
fun onCardClick() {
navigate(
MainFragmentDirections.actionMainFragmentToDescriptionFragment()
)
}
#AndroidEntryPoint
class DescriptionFragment : ...() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
setContent {
DescriptionStateful(...)
}
}
...
}
But now I want to add 2 deeplinks ("myapp://mycard" - to MyCardStateless and "myapp://description" - to DescriptionFragment or to DescriptionStateful) and handle navigation using navigation-compose. Is it possible at all?
Related
I have a problem with navigation in my Compose app. So starting from the beginning I want to have two navigation graphs: one for authentication related things and second for main functionalities, which should be accessible only after login. So typical case.
I want to use Splash Screen API from Android 12, so in my main activity I could do something like this:
class AppMainActivity : ComponentActivity() {
private val viewModel by viewModels<SplashViewModel>()
private var userAuthState: UserAuthState = UserAuthState.UNKNOWN
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onEvent(SplashEvent.CheckAuthentication)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isAuthenticated.collect {
userAuthState = it
}
}
}
setContent {
CarsLocalizerTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
val splash = installSplashScreen()
splash.setKeepOnScreenCondition {
userAuthState != UserAuthState.UNKNOWN
}
when (userAuthState) {
UserAuthState.UNAUTHENTICATED -> {
AuthNavigation(
navController = navController,
scaffoldState = scaffoldState
)
}
UserAuthState.AUTHENTICATED -> {
HomeScreen(
viewModel = hiltViewModel(),
onLogout = { navController.popBackStack() }
)
}
UserAuthState.UNKNOWN -> {}
}
}
}
}
}
}
}
Here I am collecting StateFlow from view model, which describes if user is authenticated already or no. If authenticated successfully then go to HomeScreen, which has HomeNavigation inside. If not authenticated, go to Authentication nav graph. Problem is that this approach would not work, since activity is created only once, so if I user will login, this when
when (userAuthState) {
UserAuthState.UNAUTHENTICATED -> {
AuthNavigation(
navController = navController,
scaffoldState = scaffoldState
)
}
UserAuthState.AUTHENTICATED -> {
HomeScreen(
viewModel = hiltViewModel(),
onLogout = { navController.popBackStack() }
)
}
UserAuthState.UNKNOWN -> {}
}
Won’t be called again.
I was trying to find some solution for my problem but, can’t find anything helpful. Maybe somebody had such issue before, or saw something useful? Will be very glad for any help.
Rest of my code:
SplashViewModel
#HiltViewModel
class SplashViewModel #Inject constructor(
private val authenticateUseCase: AuthenticateUseCase
): ViewModel() {
private val _isAuthenticated = MutableStateFlow<UserAuthState>(value = UserAuthState.UNKNOWN)
val isAuthenticated: StateFlow<UserAuthState> = _isAuthenticated.asStateFlow()
fun onEvent(event: SplashEvent) {
when (event) {
is SplashEvent.CheckAuthentication -> {
viewModelScope.launch {
val result = authenticateUseCase()
when (result) {
true -> {
_isAuthenticated.emit(UserAuthState.AUTHENTICATED)
}
false -> {
_isAuthenticated.emit(UserAuthState.UNAUTHENTICATED)
}
}
}
}
}
}
}
AuthNavigation
#Composable
fun AuthNavigation(
navController: NavHostController,
scaffoldState: ScaffoldState
) {
NavHost(
navController = navController,
startDestination = Screen.Login.route,
modifier = Modifier.fillMaxSize()
) {
composable(Screen.Login.route) {
LoginScreen(
onNavigate = { navController.navigate(it) } ,
onLogin = {
navController.popBackStack()
},
scaffoldState = scaffoldState,
viewModel = hiltViewModel()
)
}
composable(Screen.Register.route) {
RegisterScreen(
onPopBackstack = { navController.popBackStack() },
scaffoldState = scaffoldState,
viewModel = hiltViewModel()
)
}
composable(Screen.Onboarding.route) {
OnboardingScreen(
onCompleted = { navController.popBackStack() },
viewModel = hiltViewModel()
)
}
}
}
HomeScreen
#Composable
fun HomeScreen(
viewModel: HomeViewModel,
onLogout: () -> Unit
) {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
var appBarTitle by remember {
mutableStateOf("")
}
LaunchedEffect(key1 = true) {
viewModel.userName.collectLatest {
appBarTitle = "Hello $it"
}
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
if (appBarTitle.isEmpty()) {
AppBarWithName("Hello")
} else {
AppBarWithName(appBarTitle)
}
},
bottomBar = { BottomNavigationBar(navController) },
content = { padding ->
Box(modifier = Modifier.padding(padding)) {
HomeNavigation(
navController = navController,
scaffoldState = scaffoldState,
onLogout = { onLogout() }
)
}
}
)
}
HomeNavigation
#Composable
fun HomeNavigation(
navController: NavHostController,
scaffoldState: ScaffoldState,
onLogout: () -> Unit
) {
NavHost(
navController = navController,
startDestination = Screen.Map.route
) {
composable(Screen.Map.route) {
MapScreen(viewModel = hiltViewModel())
}
composable(Screen.ManageCars.route) {
ManageCarsScreen(
viewModel = hiltViewModel(),
scaffoldState = scaffoldState,
onAddCar = {
navController.navigate(Screen.AddCar.route)
}
)
}
composable(Screen.AddCar.route) {
AddCarScreen(
viewModel = hiltViewModel(),
onPopBackstack = {
navController.popBackStack()
}
)
}
composable(Screen.Logout.route) {
LogoutDialogScreen(
viewModel = hiltViewModel(),
onLogout = {
navController.popBackStack()
onLogout()
},
onCancel = {
navController.popBackStack()
}
)
}
}
}
You need to store the UserAuthState as MutableState, this way, when the value is updated, setContent will automatically re-compose:
private var userAuthState = mutableStateOf(UserAuthState.UNKNOWN)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isAuthenticated.collect { userAuthState.value = it }
}
}
setContent {
when (userAuthState.value) {
etc....
}
}
From the docs:
mutableStateOf creates an observable MutableState, which is an
observable type integrated with the compose runtime.
Any changes to value schedules recomposition of any composable
functions that read value.
I am try to learning android jetpack compose, and I have simple app. In ScreenA I have a text field and when I click the button, I am save this data to firestore, and when I come in ScreenB, I want to save city name also in firebase, but I am using one viewmodel, so how can save both text field data in the same place in firestore, I did not find any solution.
ScreenA:
class ScreenAActivity : ComponentActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this)[MyViewModel::class.java]
setContent {
ScreenA(viewModel)
}
}
}
#Composable
fun ScreenA(
viewModel : MyViewModel
) {
val name = remember { mutableStateOf(TextFieldValue()) }
OutlinedTextField(
value = name.value,
onValueChange = { name.value = it },
label = { Text(text = "name") },
)
Button(
modifier = Modifier
.width(40.dp)
.height(20.dp),
onClick = {
focus.clearFocus(force = true)
viewModel.onSignUp(
name.value.text,
)
context.startActivity(
Intent(
context,
ScreenB::class.java
)
)
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Red
),
shape = RoundedCornerShape(60)
) {
Text(
text = "OK"
)
)
}
}
ScreenB:
class ScreenBActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ScreenB()
}
}
}
#Composable
fun ScreenB(
) {
val city = remember { mutableStateOf(TextFieldValue()) }
OutlinedTextField(
value = city.value,
onValueChange = { city.value = it },
label = { Text(text = "city") },
)
Button(
modifier = Modifier
.width(40.dp)
.height(20.dp),
onClick = {
focus.clearFocus(force = true)
viewModel.onSignUp(
city.value.text,
)
},
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Red
),
shape = RoundedCornerShape(60)
) {
Text(
text = "OK"
)
)
}
}
The recommendation is use a single activity and navigate through screens using Navigation library.
After you've refactored your code to use the Navigation library, you can pass the previous back stack entry in order to get the same instance of the View Model.
val navController = rememberNavController()
NavHost(
navController = navController,
...
) {
composable("ScreenA") { backStackEntry ->
val viewModel: MyViewModel = viewModel(backStackEntry)
ScreenA(viewModel)
}
composable("ScreenB") { backStackEntry ->
val viewModel: MyViewModel = viewModel(navController.previousBackStackEntry!!)
ScreenB(viewModel)
}
}
But if you really need to do this using activity, my suggestion is define a shared object between the view models. Something like:
object SharedSignInObject {
fun signUp(name: String) {
// do something
}
// other things you need to share...
}
and in your viewmodels you can use this object...
class MyViewModel: ViewModel() {
fun signUp(name: String) {
SharedSignInObject.signUp(name)
}
}
I'm using accompanist library for swipe to refresh.
And I adopt it sample code for testing, however, it didn't work.
I search for adopt it, but I couldn't find.
Is there anything wrong in my code?
I want to swipe when user needs to refresh
class MyViewModel : ViewModel() {
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean>
get() = _isRefreshing.asStateFlow()
fun refresh() {
// This doesn't handle multiple 'refreshing' tasks, don't use this
viewModelScope.launch {
// A fake 2 second 'refresh'
_isRefreshing.emit(true)
delay(2000)
_isRefreshing.emit(false)
}
}
}
#Composable
fun SwipeRefreshSample() {
val viewModel: MyViewModel = viewModel()
val isRefreshing by viewModel.isRefreshing.collectAsState()
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() },
) {
LazyColumn {
items(30) { index ->
// TODO: list items
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
setContent {
TestTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
}
}
}
}
}
Your list doesn't take up the full screen width and you should include the state parameter:
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() },
) {
LazyColumn(state = rememberLazyListState(), modifier = Modifier.fillMaxSize()) {
items(100) { index ->
Text(index.toString())
}
}
}
or with Column:
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())) {
repeat(100) { index ->
Text(index.toString())
}
}
My app has a main screen with a Scaffold and a BottomNavigation bar:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
MyApplicationTheme {
Scaffold(
bottomBar = {
BottomBar(navController = navController)
}
) {
NavigationGraph(navController = navController)
}
}
}
}
...
#Composable
fun NavigationGraph(navController: NavHostController){
NavHost(navController = navController, startDestination = BottomMenuOption.Home.route) {
composable(route = BottomMenuOption.Home.route) {
HomeScreen(navController = navController)
}
composable(route = BottomMenuOption.Settings.settings) {
SettingsScreen()
}
composable(route = BottomMenuOption.Profile.route) {
ProfileScreen()
}
composable(route = "feature") {
FeatureScreen()
}
}
}
FeatureScreen has it's own Scaffold with a topBar instead of a bottomBar and when I navigate to it from HomeScreen, I want to replace the previous one from the main screen and just see a topBar but instead, I'm seeing the two bars in the screen.
#Composable
fun FeatureScreen() {
Scaffold (
topBar = {
TopBar(" Feature Screen")
}
) {
}
}
Is it possible to accomplish this? I'm thinking it could be done by just using a new Activity but ideally, I would like to keep the Single Activity approach.
I would suggest creating a new function like this:
#Composable
fun MainScaffold(
topBar: #Composable (() -> Unit) = {},
bottomBar: #Composable (() -> Unit) = {},
content: #Composable (PaddingValues) -> Unit){
Scaffold(
bottomBar = bottomBar,
topBar = topBar,
content = content
)
}
then, use this main scaffold in your screens:
#Composable
fun HomeScreen(navController: NavHostController) {
MainScaffold(
bottomBar = { BottomBar(navController = navController) },
content = {
// content
})
}
and in your feature screen:
#Composable
fun FeatureScreen() {
MainScaffold (
topBar = {
TopBar(" Feature Screen")
}
) {
//content
}
}
and in setContent
setContent {
val navController = rememberNavController()
VoiceAssistantJetpackComposeTheme {
NavigationGraph(navController = navController)
}}
I'm doing my test project using ViewModel and ComposeView.
My architecture include: one Activity and multi ComposeView, using navigation like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainView()
}
}
}
#Composable
fun MainView() {
TestComposeTheme {
val navController = rememberNavController()
Surface(color = MaterialTheme.colors.background) {
NavHost(
navController = navController,
startDestination = KYCScreen.getName(),
) {
navigation(startDestination = KYCScreen.Preload.name, route = KYCScreen.getName()) {
composable(KYCScreen.Preload.name) { PreloadView(navHostController = navController) }
composable(KYCScreen.StartKYC.name) { StartView(navHostController = navController) }
composable(KYCScreen.Login.name) { LoginView(navHostController = navController) }
}
navigation(startDestination = HomeScreen.Home.name, route = HomeScreen.getName()) {
composable(HomeScreen.Home.name) { HomeView(navHostController = navController) }
}
}
}
}
}
The problem happens when I include logic that executes when a Composable function is executed, my logic code was looped many times. Here my code:
#Composable
fun PreloadView(navHostController: NavHostController) {
val preloadViewModel: PreloadViewModel = viewModel()
preloadViewModel.getSyncData()
val syncState by preloadViewModel.uiStateSync.observeAsState()
syncState?.let { PreloadContent(navHostController = navHostController, it) }
}
#Composable
fun PreloadContent(navHostController: NavHostController, uiState: UiState<SyncResponse>) {
when (uiState.state) {
RequestState.SUCCESS -> {
navHostController.navigate(KYCScreen.StartKYC.name)
}
RequestState.FAIL -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
Text(text = "Error", color = Color.Black)
}
}
RequestState.NON -> {
}
}
}
Anyone have a solution to help me with that architecture?
Your composable function should be side-effects free.
If you want to run something just once. You can do something like the following
LaunchedEffect(Unit){
preloadViewModel.getSyncData()
}
or in your case, if it is mandatory to Sync Data when ViewModel initializes you can call this function in the init block inside of your ViewModel.
Check the official doc https://developer.android.com/jetpack/compose/side-effects