I am just starting with Compose. For the first look, for me, it all appears like a copy of SwiftUI, which I love. But when I started to really use it, I quickly ran into many issues. Apparently, I need to find the proper way how to use it to benefit from it...
Here is one of my issues.
package org.test.android.kotlin.compose.ui
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.test.android.kotlin.compose.ui.theme.MbiKtTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MbiKtTheme {
val navController = rememberNavController()
// <Edit #1>
// Navigator.route.collectAsState("").value.takeIf { it.isNotEmpty() }?.also { navController.navigate(it) }
// Navigator.route.observe(this, { route -> navController.navigate(route) })
// </Edit #1>
// <Edit #2>
Navigator.route.collectAsState("").value.takeIf { it.isNotEmpty() }?.also {
navController.popBackStack()
navController.navigate(it)
}
// </Edit #2>
Surface(color = MaterialTheme.colors.background) {
NavHost(
navController = navController,
startDestination = "setup"
) {
composable(route = "setup") {
SetupScreen()
}
composable(route = "progress") {
ProgressScreen()
}
}
}
}
}
}
}
// This is unnecessary here in this simple code fragment, but a MUST for large modular projects
object Navigator {
// <Edit #1>
val route = MutableSharedFlow<String>(0, 1, BufferOverflow.DROP_OLDEST)
//val route: MutableLiveData<String> = MutableLiveData()
// </Edit #1>
}
class SetupViewModel : ViewModel() {
init {
Log.d(toString(), "Create")
}
override fun onCleared() {
Log.d(toString(), "Destroy")
}
override fun toString(): String {
return "SetupViewModel"
}
}
@Composable
fun SetupScreen(model: SetupViewModel = viewModel()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = Dp(8f))
) {
Text(text = "Setup")
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { Navigator.route.tryEmit("progress") }, modifier = Modifier.fillMaxWidth()) { Text(text = "Register") }
}
}
class ProgressViewModel : ViewModel() {
init {
Log.d(toString(), "Created")
}
override fun onCleared() {
Log.d(toString(), "Cleared")
}
override fun toString(): String {
return "ProgressViewModel"
}
}
@Composable
fun ProgressScreen(model: ProgressViewModel = viewModel()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = Dp(8f))
) {
Text(text = "Progress")
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { Navigator.route.tryEmit("setup") }, modifier = Modifier.fillMaxWidth()) { Text(text = "Abort") }
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MbiKtTheme {
SetupScreen()
}
}
My reality is, of course, much more complex, I did my best to simplify it all as much as possible, but this already demonstrates the problem I have:
- Navigate between the two screens (composables) and rotate the screen
- And observe the Created/Destroyed messages from both view models in the LogCat
- In the first place: Destroyed is never called when navigating from one screen to another (clearly because the activity stays alive), which is totally not acceptable in large projects
- Subsequently, as soon as you navigate at least once to the other screen (just tap the button), the view models start to be recreated with every screen rotation, which is also totally not acceptable
I know that compose is not mature yet (I have seen some components still in "alpha" release). So it may be a bug in compose itself.
Or it may be just my missunderstanding of how to use Compose in large scale and modular projects...
Any ideas?
(Just for completeness, I double checked I am using the latest currently available versions of everything.)
Edit #1 (2021/09/05)
Thanks to the article which deals with one of my issues (link in comment below), I fixed one of the problems: The view models are NOT being recreated when rorating screen any more (still no clue, why).
So the remaining issue is that the view models do not follow the expected lifecycle.
Edit #2 (2021/09/13)
Thanks to the answer below (unfortunately I did not find any way how to make it accepted answer - SF UI is still a bit unclear to me), I was able to really make the view models lifecycle to work as expected.
I just disabled the back-stack, which is anyway unwanted (creates a lot of mess between the UI and the underlying model) feature in my kind of app...