i am trying to learn jetpack compose but i have some hard time working with navigation, specifically i cant understand why this code print "notwork" two time.
#Composable
fun NavigationController() {
var navController = rememberNavController()
NavHost(navController, startDestination = DummyRoutes.Dummy.route) {
composable(route = DummyRoutes.Dummy.route) {
Dummy(
openHome = { navController.navigate(SomeRoutes.SomeOther.route) },
)
}
}
}
#Composable
fun Dummy(
openHome: () -> Unit,
) {
Log.d("hello", "notwork: ")
Button(onClick = {openHome()}) {
}
NavHost recomposing its destinations because of transition animations.
With the initial route it only happens two times, but if you try to navigate to an other screen, there will be more recompositions of both appearing/disappearing screens. This is expected behaviour.
If you use your own animation, your view will be recomposed up to once a frame, and this is OK too.
There're times when you can reduce number of recompositions, e.g. when you use an often changing state, like lazy list state, check out this answer for more details.
But with animations and navigation you can't reduce this number, and you shouldn't, because screen is actually needs to be redrawn that often. Extra recompositions won't affect your app performance if you build it right - handle side effects correctly. See more details about recompositions and side effects in Thinking in Compose.
Composable component is recomposed every time when its parameters are changed. In your case, it is openHome. It might be that the lambda is changed in NavigationController.
Here is an interesting article about understanding recomposition with a case study
Related
I am learning State hosting in jetpack compose. I have created two separated function ContentStateful and ContentStateLess. In my ContentStateLess there is a lot of view inside them and I am checking some condition and change view accordingly. I am guessing that there is no condition/business logic inside Stateful compose. So what is the proper way of doing this kind of logic in here.
ContentStateful
#Composable
fun ContentStateful(
viewModel: PairViewModel = getViewModel()
) {
ContentStateLess(viewModel)
}
ContentStateLess
#Composable
fun ContentStateLess(
viewModel: PairViewModel
) {
Text()
Text()
Image()
if (viewModel.isTrue) {
Image()
// more item here
} else {
Text()
// more item here
}
Image()
}
So what is the best recommendation for this if - else logic in ContentStateLess(). Many Thanks
If you are building stateless Composables it's better not to pass anything like ViewModel. You can pass Boolean parameter instead. When you wish to move your custom Composable to another screen or another project you will need to move ViewModel either.
The reason Google recommends stateless Composables is it's difficult to test, you can easily test a Composable with inputs only.
Another thing you experience the more states inner composables have to more exposure you create for your composable being in a State that you might not anticipate.
When you build simple Composables with one, two, three layers might not be an issue but with more states and layers state management becomes a serious issue. And if you somehow forget or miss a state inside a Composable you might end up with a behavior that's not expected. So to minimize risks and make your Composables testable you should aim to manage your states in one place and possible in a state holder class that wraps multiple states.
#Composable
fun ContentStateLess(
firstOneTrue: Boolean
) {
Text()
Text()
Image()
if (firstOneTrue) {
Image()
// more item here
} else {
Text()
// more item here
}
Image()
}
Is there any way how to run animation once recomposition (of screen, or some composable) is done?
When animation is running and there is recomposition at the same time, animation has very bad performance (it is not smooth at all).
I tried delay to delay animation for a while, but I don't think that's a good idea. There must be better way of doing this.
Please provide producible example so you won't need to ask it again. You can run an animation with Animatable calling animatable.animateTo() inside LaunchedEffect after composition is completed. And instead of creating recomposition by using Modifier.offset(), Modifier.background() you can select Modifiers counterparts with lambda.
https://developer.android.com/jetpack/compose/performance/bestpractices#defer-reads
You can animate alpha for instance without triggering recomposition after composition is completed inside Draw phase of frame
val animatable = remember {
Animatable(0f)
}
LaunchedEffect(key1 = Unit) {
animatable.animateTo(1f)
}
Box(
Modifier
.size(100.dp)
.drawBehind {
drawRect(Color.Red.copy(alpha = animatable.value))
}
)
https://stackoverflow.com/a/73274631/5457853
I am still new to compose and I am curious how people treat this kind of thing.
Let's imagine that we have a screen that has two variants, one variant with some views, another variant with other views. That variant should be dictated by a persisted flag, which I have stored using DataStore (the new SharedPrefs). The only issues is that unlike SharedPrefs, DataStore is asynchronous and is made to work with coroutines. So here's what happens, the screen gets rendered in default state (variant A) for just a split second, atfer about 100-200ms the viewModel successfully reads the value from DataStore on a coroutine and posts it on a mutableStateOf(), which as a result triggers recomposition with the variant B of the screen that is saved in the prefs. This transition is visible and the entire behavior looks glitchy. How do you fix this? I don't want the screen to compose the default state before the viewModel has time to read the stored value, I want the screen to await those 100-200ms without doing anything and composing the views only after the reading from DataStore.
The code looks like:
#Composable
fun MyScreen(){
val viewModel = hiltViewModel<ScreenViewModel>()
val state = viewModel.uiState
if(state == MyScreenState.A){
[...] // some view here
} else {
[...] // other view here
}
}
#HiltViewModel
class ScreenViewModel #Inject constructor(
private val dataStoreService: DataStoreService
) : BaseViewModel() {
var uiState by mutableStateOf(MyScreenState.A)
init {
viewModelScope.launch {
dataStoreService.flag().collect { flag ->
uiState = if(flag) MyScreenState.A else MyScreenState.B
}
}
}
}
For simplicity, MyScreenState is just a simple enum in this case. One of the things I thought about is defaulting the uiState to null instead of variant A and in my screen check if the state is null and if so returning a Unit (basically rendering nothing). If the state is not null, render the screen accordingly. But the truth is that I don't feel like making that uiState nullable, I avoid working with nulls at all cost because they make the code just a little less readable and needs extra handling. What's your solution on this? Thanks.
Instead of creating nullable state create another state that represents nothing being happening. I generally use Idle state to set as initial or a UI state when nothing should happen. I also use this approach for fire once events after event is invoked and processed.
var uiState by mutableStateOf(MyScreenState.Idle)
it will be a loading or a blank screen depending on your UI
I have a top-level composable representing some app screen, which uses 2 view-models (for clarity). ViewModel_B starts some network activity once created:
Once network request finishes, it updates state_B: MutableStateFlow inside ViewModel_B which is observed from our composable. And that state is, for example, Loading | Error | Success enum, and let's say Success contains some payload:
When it's Success, I want my ViewModel_A to start another activity, which would in turn affect another state_A: MutableStateFlow, which we also observe and show UI based on it:
#Composable
fun MainScreen(
viewModel_A = viewModel { factory_A },
viewModel_B = viewModel { factory_B }.apply { networkRequest_B() }
) {
val state_B by viewModel_B.state_B.collectAsState()
when (val sB = state_B) {
Loading -> LoadingScreen() // Composable
Error -> ErrorScreen() // Composable
is Success -> {
val state_A by viewModel_A.state_A.collectAsState()
viewModel_A.doAction(sB.payload) // changes state_A internally
when (val sA = state_A) {
// other composables based on state_A values
}
}
}
}
When I run this code, the whole MainScreen() method gets called again and again in an infinite loop. Hence, two questions:
When should I start observing state_A - as close as possible to the place, where it is relevant? I mean, there is no sense to expect any values from it if state_B was Loading | Error, since there is no screen for state_A. Should I put viewModel_A.state_A.collectAsState() where it is now or I should hoist it top?
I believe, infinite loop is due to improper usage of Jetpack Compose here. It recomposes and runs viewModel_A.doAction() every recomposition, which will affect inner composables and everything would repeat. I believe, I should use rememberCoroutineScope() here OR LaunchedEffect {}, but I'm not sure how to do this properly.
What I see from LauchedEffect docs is that it's recomposed when key1 changes. I actually want to depend on sB.payload when running doAction(), but docs discourages me from passing sB.payload for key1.
For rememberCoroutineScope() - not sure, how to use it in my case, and also - where should I declare it - top or inside is Success -> {} block?
I am using a file picker inside a HorizontalPager in jetpack compose. When the corresponding screen is loaded while tapping the button, the launcher is triggered 2 times.
Code snippet
var openFileManager by remember {
mutableStateOf(false)
}
if (openFileManager) {
launcher.launch("*/*")
}
Button(text = "Upload",
onClick = {
openFileManager = true
})
Edited: First of all Ian's point is valid why not just launch it in the onClick directly? I also assumed that maybe you want to do something more with your true false value. If you want nothing but launch then all these are useless.
The screen can draw multiple times when you click and make openFileManager true so using only condition won't prevent it from calling multiple times.
You can wrap your code with LaunchedEffect with openFileManager as a key. The LaunchedEffect block will run only when your openFileManager change.
if (openFileManager) {
LaunchedEffect(openFileManager) {
launcher.launch("*/*")
}
}
You should NEVER store such important state inside a #Composable. Such important business logic is meant to be stored in a more robust holder like the ViewModel.
ViewModel{
var launch by mutableStateOf (false)
private set
fun updateLaunchValue(newValue: Boolean){
launch = newValue
}
}
Pass these to the Composable from the main activity
MyComposable(
launchValue = viewModel.launch
updateLaunchValue = viewModel::updateLaunchValue
)
Create the parameters in the Composable as necessary
#Comoosable
fun Uploader(launchValue: Boolean, onUpdateLaunchValue: (Boolean) -> Unit){
LaunchedEffect (launchValue){
if (launchValue)
launcher.launch(...)
}
Button { // This is onClick
onUpdateLaunchValue(true) // makes the value true in the vm, updating state
}
}
If you think it is overcomplicated, you're in the wrong paradigm. This is the recommended AND CORRECT way of handling state in Compose, or any declarative paradigm, really afaik. This keeps the code clean, while completely separating UI and data layers, allowing controlled interaction between UI and state to achieve just the perfect behaviour for the app.