Good afternoon people. I got tired of trying to un...
# compose
c
Good afternoon people. I got tired of trying to understand how this does not work: It's navigation related, I wanna make a navigation router out of my splash screen and I keep getting into this infinite loop where the splash screen keeps getting triggered and every interaction the access to my second destination increases in one.
Copy code
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppTheme {
                MainNavigation()
            }
        }
    }
}

@Composable
fun MainNavigation() {
    val navHostController = rememberNavController()
    val mainActions = MainActions(
        navController = navHostController,
        destination = MainDestinations
    )
    AppTheme {
        NavHost(
            navController = navHostController,
            startDestination = SPLASH
        ) {
            composable(SPLASH) {
                SplashView(mainActions)
            }
            composable(LOGIN) {
                Login(mainActions)
            }
        }
    }
}

object MainDestinations {
    const val SPLASH = "SPLASH"
    const val LOGIN = "LOGIN"
    const val HOME = "HOME"
}

class MainActions(
    private val navController: NavHostController,
    private val destination: MainDestinations
) {
    fun navigateTo(route: String){
        navController.navigate(route){
            popUpTo(SPLASH) { inclusive = true }
        }
    }
    fun navigateHome(){
        navController.navigate(destination.HOME)
    }
}
Copy code
package de.cicerohellmann.jubilantOctoMemory.screens.splash

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import de.cicerohellmann.jubilantOctoMemory.MainActions
import org.koin.androidx.compose.getViewModel

@Composable
fun SplashView(
    action: MainActions,
    viewModel: SplashViewModel = getViewModel()
) {
    val destination = viewModel.destination.observeAsState()
    if(destination.value != null){
        action.navigateTo(destination.value!!)
    }
    SplashUI()
}

@Composable
fun SplashUI(){
    Column(modifier = Modifier.fillMaxSize()) {

    }
}

@Preview
@Composable
fun SplashUIPreview(){
    SplashUI()
}
Copy code
package de.cicerohellmann.jubilantOctoMemory.screens.splash

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.cicerohellmann.jubilantOctoMemory.extensions.immutable
import de.cicerohellmann.jubilantOctoMemory.usecase.GetStartingDestinationByUserIsLogged
import de.cicerohellmann.jubilantOctoMemory.usecase.ReadUserSortedByDateUseCase
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class SplashViewModel(private val readUserSortedByDateUseCase: ReadUserSortedByDateUseCase) :
    ViewModel() {
    private val _destination = MutableLiveData<String?>(null)
    val destination = _destination.immutable()

    init {
        viewModelScope.launch {
            _destination.value = GetStartingDestinationByUserIsLogged().execute(readUserSortedByDateUseCase.execute())
        }
    }
}
Copy code
class ReadUserSortedByDateUseCase(private val userDataSource: UserDataSource) {
    fun execute(): User? {
        return userDataSource.readUserFlow()
    }
}

class GetStartingDestinationByUserIsLogged {
    fun execute(user: User?): String {
        user ?: return MainDestinations.LOGIN
        return if (user.isLogged) {
            MainDestinations.HOME
        } else {
            MainDestinations.LOGIN
        }
    }
}
Copy code
if (destination.value != null) {
    LaunchedEffect(destination) {
        action.navigateTo(destination.value!!)
    }
}
Works but then the navigation method must be changed from popUpTo to simple navigation and the problem with this approach is that I will need to remove the Splash screen from the stack manually which I can't believe is the correct way.
Copy code
fun navigateTo(route: String){
    navController.navigate(route){
        popUpTo(route) { inclusive = false }
    }
}
to
Copy code
fun navigateTo(route: String){
    navController.navigate(route)
}
Even when I use something like:
Copy code
fun navigateTo(route: String){
    navController.navigate(route){
        popUpTo(route) { inclusive = true }
    }
}
When I finally in the login screen and press the back button, I still go back to the splash screen. I can't find the track of what is going wrong here.
Excuse me for tagging you here @Ian Lake but this rather simple approach already took me a whole week of trial and error and I can't find where to look so I can observe step by step why this is happening.
How's back press still taking me to Splash screen I can't understand.
I see that popUpTo pops up to a destination BEFORE navigating
Pop up to a given destination before navigating. This pops all non-matching destinations from the back stack until this destination is found.
c
@Cicero do you happen to have a repo available for this. I'd love to clone it and dig around.
c
Just adding a final line, even then the description of pupUpTo describes this behavior.,inclusive = true should override this description and remove the destination.
Damn it. I actually solved it a while ago but my tired brain jus ignored the most basic instinct which is to search for answers based on provided feedback. I had this: java.lang.ClassCastException: androidx.navigation.NavGraph cannot be cast to androidx.navigation.compose.ComposeNavigator$Destination from this:
Copy code
fun popUpToFromSplashTo(route: String){
        navController.navigate(route){
            popUpTo(SPLASH) { inclusive = true }
        }
    }
And it happened that I just needed to update my compose navigation version.
This solves the problem gracefully. So, launch effect and popping the splash destination.
I still don't understand why without launch effect this would go into an infinite loop
i
When you have animations, your destination gets recomposed on every frame, which means that your compositions must be side effect free - calling
navigate
as part of composition is absolutely a side effect that you would never want to run every frame of an animation
1
👍 1
c
I don't really have any animations. This is basic as this implementation can get. 2 empty composables, 1 navhost, splash observes destination as state and if different than null, which in this case always return LOGIN, returns a destination And when the navigation happens it gets stuck in a loop going into login, back to splash, on and on, increasing the amount of calls to login on every iteration. (Without using LaunchEffect) Intuitively as I've been using this library I would not only never expect this but I have no clue of what could be causing it. There is nothing fancy here, it's raw navigation based on observing a flow that doesn't get triggered more than once. This is the commit where tis can be reproduced: https://github.com/syntrofos-tech/jubilant-octo-memory/commit/0ddd808f6a2836f3c635f7f78adfb2b1455c829e
c
@Cicero if you use latest navigation there are fade anims by default.
c
Ok ok, but this really sounds and feels unintuitive. How can I observe recompositions triggered by animation? And even then this true, is there anywhere explaining this? Should it be explained? Is this supposed to be clear somewhere?) I found no reference and also there is no crash or concrete evidence that led me to any answers about this.
Using
observeAsState()
for events should have been your first red flag - state is safe to consume 100x over. Events are not
c
Now I feel like I'm doing something absolutely wrong. How would one go about feeding on a flow and reacting in its composable if not using MutableLiveData + LiveData & observeAsState()? And where can I read about it
Ok, so this could and should all be done using side effects?
c
@Cicero you might be interested in starring this https://issuetracker.google.com/issues/194911952 This conversation comes up a lot and I think it's mostly due to how some people used to work with "events" in a non-compose world.
c
Yes, absolutely I'm feeling my mind being shattered.
☝️ 1
c
We talked about this about 2 weeks ago in this huge thread that goes over possible solutions https://kotlinlang.slack.com/archives/CJLTWPH7S/p1627406028253800
🌟 1
c
Wow, just the first link to the tivi repo. Blip man, thank you all for the support. So I'm coming back to my original feelings about VM being unnecessary. That you could basically have a @Composable SplashViewModel and a @Composable SplashBody
This convo could be pinned because this must be a really recurrent question mark in peoples head
c
I think there is a general notion that we won't need AAC VMs in the future since VMs were build as a layer to paper over lifecycles.
i
That's not actually the most valuable part of AAC VMs in a Compose app and never has been (it is the hoisted state that survives cycles of disposal / recreation and gives you the only signal of final disposal of a screen)
c
You only get that if you use it with compose-nav though right? that = signal of final disposal of a screen
i
The same also applies to your containing activity/fragment
Compose doesn't have any signal for permanently destroyed (i.e., state being deleted): https://issuetracker.google.com/177562901
c
I feel a strong need to hear you talk about it 😂
I meant just the context of this conversation in general would be worth a video like that Google video about navigation
t
Going back to how we think about “events” in Compose, when coming from the imperative Android mindset, found that the docs for
SnackbarHostState
are helpful https://developer.android.com/reference/kotlin/androidx/compose/material/SnackbarHostState
👌🏽 1
c
Indeed helpful
👍🏼 1
c
@Tash oh sweet. I haven't had to use a snackbar yet, but that is indeed helpful!
👍🏼 1