How to avoid opening screen multiple times? - android

video with bug
My app architecture looks like: viewModel + Compose layout per screen. In my viewModels I have Channel() where I handle actions from screen:
init {
viewModelScope.launch {
try {
actions.receiveAsFlow().collect { action ->
when (action) {
//handle actions here
}
}
}
}
In Home screen I have list with post. The problem is when I select post and navigate user to PostDetails screen I can choose another post though list with post is underhood. I think it's related to Home viewModel and that action flow works in the background. Any solution what should I change?
#Composable
fun AppNavigationHost(appNavController: NavController) {
NavHost(
navController = appNavController as NavHostController,
startDestination = PostDetails.route
) {
composable(Default.route) {}
composable(PostDetails.route) { it ->
it.arguments?.getString("post").let {
val post = Gson().fromJson(it, Post::class.java)
PostDetailsScreen(post, appNavController)
}
}
}
}

I've had similar problems before, albeit not using compose. If you don't set a dialog to be clickable then you can click through it. I would suggest adding:
android:clickable="true"
To the UI you're using for the post details, or the compose equivalent

Related

Navigation is being called every time in Jetpack Compose

I'm implementing registration in my application and, after filling in the respective fields, I click on a button that will make a registration request to the API. Meanwhile, I place a Loading View and when I receive the successful response, I execute the navigation to the OnBoarding screen. The issue is that the navController is always running the navigation and doing the navigation and popUp several times, when it should only do it once. I always get this warning on logs: Ignoring popBackStack to destination 29021787 as it was not found on the current back stack and I am not able to do any click or focus in the OnBoardingScreen.
My code:
val uiState by registerViewModel.uiState.collectAsState()
when (uiState) {
is BaseViewState.Data -> {
navController.navigate(NavigationItem.OnBoarding.route) {
popUpTo(NavigationItem.Register.route) {
inclusive = true
}
}
}
is BaseViewState.Loading -> LoadingView()
is BaseViewState.Error -> BannerView()
else -> {}
}
On button click I call the viewModel like this:
registerViewModel.onTriggerEvent(
RegisterEvent.CreateUser(
usernameInputState.value.text,
emailInputState.value.text,
passwordInputState.value.text
)
)
And, in ViewModel, I do my request like this:
override fun onTriggerEvent(eventType: RegisterEvent) {
when (eventType) {
is RegisterEvent.CreateUser -> createUser(eventType.username, eventType.email, eventType.password)
}
}
private fun createUser(username: String, email: String, password: String) = safeLaunch {
setState(BaseViewState.Loading)
execute(createUser(CreateUser.Params(username, email, password))) {
setState(BaseViewState.Data(RegisterViewState(it)))
}
}
I guess it should be caused by recomposition, because I put a breakpoint on first when scenario and it stops here multiple times, but only one on ViewModel. How can I fix this?
This issue is here
is BaseViewState.Data -> {
navController.navigate(NavigationItem.OnBoarding.route) {
popUpTo(NavigationItem.Register.route) {
inclusive = true
}
}
}
Every time you call navController.navigate NavHost will keep on passing through this block, executing an endless loop.
I suggest having the navigate call from a LaunchedEffect with a key (like this),
LaunchedEffect(key1 = "some key") {
navController.navigate(…)
}
or creating a separate structure namely "Events" where they are emitted as SharedFlow and observed via a Unit keyed LaunchedEffect
LaunchedEffect(Unit) {
viewModel.event.collectLatest {
when (it) {
is UiEvent.Navigate -> {
navController.navigate(…)
}
}
}
}

How to check if a composable is on top of the back stack or not?

I have a navigation graph containing a HomeScreen and a MyBottomSheet. For bottom sheets I am using Accompanist Navigation.
Both of the destinations share a common ViewModel which is scoped to that navigation graph. From that ViewModel I am exposing a Flow<MyEvent> where MyEvent is:
sealed interface MyEvent
object MyEvent1: MyEvent
object MyEvent2: MyEvent
The two composables look like this:
#Composable
fun HomeScreen(viewModel: MyViewModel) {
LaunchedEffect(Unit) {
viewModel.eventsFlow.collect {
if(it is Event1) {
handleEvent1()
}
}
}
...
}
#Composable
fun MyBottomSheet(viewModel: MyViewModel) {
LaunchedEffect(Unit) {
viewModel.eventsFlow.collect {
if(it is Event2) {
handleEvent2()
}
}
}
...
}
Note that I want HomeScreen to handle Event1 and MyBottomSheet to handle Event2 (these events are basically navigation events).
The problem is that when MyBottomSheet is visible, both the composables are collecting the flow at the same time because of which Event2 also gets collected by HomeScreen. What I want is that when MyBottomSheet is the topmost destination in back stack, HomeScreen shouldn't be collecting flow. One of the possible solutions in my mind is:
#Composable
fun HomeScreen(viewModel: MyViewModel) {
LaunchedEffect(isHomeScreenOnTheTopOfBackStack) {
if(isHomeScreenOnTheTopOfBackStack) {
viewModel.eventsFlow.collect {
if(it is Event1) {
handleEvent1()
}
}
}
}
...
}
Now here, how can I check if HomeScreen is on top of the back stack or not?
Or is there a better way to approach this problem?
Turned out it was quite easy. We can use navController.currentBackStackEntryAsState() in the nav graph and pass the Boolean to the HomeScreen.
composable("home") {
val isOnTop = navController.currentBackStackEntryAsState().value?.destination?.route == "home"
HomeScreen(
viewModel = //,
isHomeScreenOnTheTopOfBackStack = isOnTop
)
}

Compose ViewModel lifecycle when working with live edit

I have following code for my main screen:
#Composable
fun MainScreen() {
val viewModel = getViewModel<MainViewModel>()
val tasks: List<Task> by viewModel.taskList.observeAsState(listOf())
LazyColumn(contentPadding = PaddingValues(bottom = 96.dp)) {
items(tasks) { task ->
if (task == tasks[0]) {
ListItemActive(task = task)
} else {
ListItemInactive(task = task)
}
}
}
val isInProgress: Boolean by viewModel.isInProgress.observeAsState(false)
if (isInProgress) {
Preloader()
}
}
Navigation is managed in main activity like this:
NavHost(navController = navController, startDestination = Screen.Main.route) {
composable(Screen.Login.route) { LoginScreen() }
composable(Screen.Main.route) { MainScreen() }
}
The problem is that whenever I adjust paddings or other sizes in android studio my Main screen composition is reevaluated. Which is what one would expect, but instead of taking existing view model, new one is created. And onCleared is not called for the old one. Is this an expected behavior, or am I missing something?
EDIT:
I'm also encountering a crash when any changes are made in code and live editor tries to update them on emulator.
Exception: reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
It happens in code below, when accessing SharedPreferences. This code is called from init block in view model which is initialised a second time during padding change in code.
httpClient.addInterceptor { chain ->
val request: Request = chain
.request()
.newBuilder()
.addHeader("Authorization", "Bearer ${get<PreferenceRepository>().accessToken}")
.build()
chain.proceed(request)
}
Though it does seem to be linked to viewModelScope I'm using. Replacing it with GlobalScope stops app from crashing.
Koin 3.1.2 doesn't work with Navigation Compose. The bug is tracked on GitHub: https://github.com/InsertKoinIO/koin/issues/1079.
As an alternative you can use cokoin library:
#Composable
fun App() {
val navController = rememberNavController()
KoinNavHost(navController, startDestination = "1") {
composable("1") {
val navViewModel = getNavViewModel<NavViewModel>()
//...
}
}
}

conditional navigation in compose, without click

I am working on a compose screen, where on application open, i redirect user to profile page. And if profile is complete, then redirect to user list page.
my code is like below
#Composable
fun UserProfile(navigateToProviderList: () -> Unit) {
val viewModel: MainActivityViewModel = viewModel()
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
return
}
else {
//compose elements here
}
}
but the app is blinking and when logged, i can see its calling the above redirect condition again and again. when going through doc, its mentioned that we should navigate only through callbacks. How do i handle this condition here? i don't have onCLick condition here.
Content of composable function can be called many times.
If you need to do some action inside composable, you need to use side effects
In this case LaunchedEffect should work:
LaunchedEffect(viewModel.userProfileComplete == true) {
if(viewModel.userProfileComplete == true) {
navigateToProviderList()
}
}
In the key(first argument of LaunchedEffect) you need to specify some key. Each time this key changes since the last recomposition, the inner code will be called. You may put Unit there, in this case it'll only be called once, when the view appears at the first place
The LaunchedEffect did not work for me since I wanted to use it in UI thread but it wasn't for some reason :/
However, I made this for my self:
#Composable
fun <T> SelfDestructEvent(liveData: LiveData<T>, onEvent: (argument: T) -> Unit) {
val previousState = remember { mutableStateOf(false) }
val state by liveData.observeAsState(null)
if (state != null && !previousState.value) {
previousState.value = true
onEvent.invoke(state!!)
}
}
and you use it like this in any other composables:
SingleEvent(viewModel.someLiveData) {
//your action with that data, whenever it was triggered, but only once
}

Is there a way to make a navigation work inside a handler using Jetpack Compose?

I am trying to create a splash screen using Jetpack Compose. I created my navigation and I have all my IDs to go to different screens, but I cannot make a screen navigate to another inside of a Hadler. How do you guys go about that?
#Composable
fun GoToMainScreen(navController: NavHostController){
Handler(Looper.getMainLooper()).postDelayed(object : Thread() {
override fun run() {
navController.navigate("main_screen")
Log.i("LOOPER", "It got here!")
}
}, 4000L)
}
I've tried this, and in my case it navigates fine with your composable:
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "profile") {
composable("profile") {
GoToMainScreen(navController)
}
composable("main_screen") { Text("main_screen") }
}
Not sure what's different in your case, but in compose we wouldn't usually need Handler
First of all, you need to wrap creation of the handler LaunchedEffect, otherwise your handler may be created many times in case of screen recomposition.
And inside LaunchedEffect we can use coroutine, so same with much less code looks like:
#Composable
fun GoToMainScreen(navController: NavHostController) {
LaunchedEffect(Unit) {
delay(2000L)
navController.navigate("main_screen")
}
}
If this still doesn't help make sure providing a minimal-reproducible-example, something like my first block of code.

Categories

Resources