https://kotlinlang.org logo
#decompose
Title
# decompose
n

Nacho Ruiz Martin

11/01/2023, 10:11 PM
Hey 👋 I finished part of the migration to Decompose of my nav-component App. More details here. I’m experiencing something that I don’t know if it’s expected. When I push a component on top of another and then go back, the Composable of the first screen gets recreated. This causes a moment of white screen that is not desirable. With the nav component, when going back the previous screen is untouched and immediately shows the previous state.
a

Arkadii Ivanov

11/01/2023, 10:25 PM
I believe with the classic
navigation-compose
, the previous composable leaves the composition, and then enters again. So all
remember
-ed objects are lost.
It would be good to see a video.
n

Nacho Ruiz Martin

11/02/2023, 11:48 AM
I’m quite sure this is something I did wrongly, tbh. The first video is of the navigation component and the second one is of Decompose. Thanks for the help, Arkadii.
a

Arkadii Ivanov

11/02/2023, 11:51 AM
Yeah, the issue seems with the state being updated asynchronously. Not sure how do you store the state in the original case. But with Decompose you can store your state in the Component, which is not destroyed in the back stack.
n

Nacho Ruiz Martin

11/02/2023, 11:51 AM
I’m using an MviKotlin Store to store the state.
a

Arkadii Ivanov

11/02/2023, 11:52 AM
Nice. Then the state should be there. It would be nice to see some code sample of the first screen.
The second screen is also blinking
n

Nacho Ruiz Martin

11/02/2023, 11:54 AM
Yes, but that’s expected because the data is fetched from the network every time.
a

Arkadii Ivanov

11/02/2023, 11:55 AM
In the original case, there is at least some data visible immediately.
n

Nacho Ruiz Martin

11/02/2023, 11:55 AM
Regarding the code, here’s how I instantiate my stores in my Blocs:
Copy code
abstract class CompleteBloc<Intent : Any, State : Any, Event : Any, Label : Any>(
    mainContext: CoroutineContext,
    componentContext: ComponentContext,
    storeFactory: () -> Store<Intent, State, Label>,
) : ComponentContext by componentContext,
    Bloc<Intent, State, Event> {

    protected val store = componentContext.instanceKeeper.getStore(storeFactory)
    override val states = store.stateFlow
    private val _events = MutableSharedFlow<Event>()
    override val events = _events
    private val scope = coroutineScope(mainContext + SupervisorJob())
The Store is a normal MviKotlin store and the Compose is:
Copy code
@Composable
fun FeedScreen(
    bloc: FeedBloc,
    showSnackbar: (String) -> Unit,
) {
    val state by bloc.states.collectAsStateWithLifecycle()
    val feedItems = bloc.feedFlow.collectAsLazyPagingItems()
Let me know if this is everything you need.
In the original case, there is at least some data visible immediately.
Oh, you are right. Same thing is happening there.
a

Arkadii Ivanov

11/02/2023, 11:56 AM
You can check https://github.com/IlyaGulya/TodoAppDecomposeMviKotlin This sample has Decompose + MVIKotlin + Reaktive. You can just ignore Reaktive being used instead of coroutines.
Ok, I suspect this is due to
bloc.states.collectAsStateWithLifecycle
being used.
Can you try just
collectAsState
?
n

Nacho Ruiz Martin

11/02/2023, 11:58 AM
Sure, let me try
a

Arkadii Ivanov

11/02/2023, 11:58 AM
Btw, it doesn't make any significant difference with MVIKotlin, because the Store is hot.
n

Nacho Ruiz Martin

11/02/2023, 11:58 AM
Oh, good to know!
Same behaviour with
collectAsState
. This is Compose Multiplatform, btw.
👍 1
a

Arkadii Ivanov

11/02/2023, 11:59 AM
Also, it's very important to collect Flows on Main.immediate dispatchers, when rendering with Compose
Here are a few things to check as well. 1. You can try adding
lifecycle.doOnDestroy { println("Destroyed: $this" }
and make sure that the component is not destroyed for any reason when moved to the back stack. 2. Try collecting the flow on
Main.immediate
dispatcher.
n

Nacho Ruiz Martin

11/02/2023, 12:00 PM
Oh, I thought that only mattered when using TextField
Same thing with
Main.immediate
👍 . Let me debug the destruction of the component.
👍 1
a

Arkadii Ivanov

11/02/2023, 12:03 PM
Btw, maybe the issue is with
bloc.feedFlow.collectAsLazyPagingItems()
? Not sure what is it.
It's not clear how exactly do you render the screen, what flows are being used.
n

Nacho Ruiz Martin

11/02/2023, 12:04 PM
That's for the paging3 library. The feedFlow is just a map over the states flow.
So everything is coming from the states flow.
👍 1
No, the Bloc/Component for the list screen is not being destroyed.
That’s weird… 😞.
a

Arkadii Ivanov

11/02/2023, 12:18 PM
Then maybe add logs right in the Composable function and see how the state comes?
n

Nacho Ruiz Martin

11/02/2023, 12:21 PM
Yep, let me try! 🙇
Something is happening there for sure. Each time I go back to the screen, the Composable is recomposed several times, the first time the state is sending 0 items.
And that causes the blink for sure.
a

Arkadii Ivanov

11/02/2023, 12:25 PM
Oh, in this case you should check what's going on in your Store. You try enabling logging, as described in the docs.
n

Nacho Ruiz Martin

11/02/2023, 12:26 PM
On it, thanks 🙇
Ok, found something that may light a bulb: The
states
flow being exposed from the
Bloc
is just:
store.stateFlow
(I’m not doing any mapping between store and component model) The state is storing some
PagingData<T>
in the state and the thing is that
collectAsLazyPagingItems
is an extension of
Flow<PagingData<T>>
. So I need to expose also the flow of paging data directly to the Composable. That’s why the Bloc has two flows exposed:
Copy code
val states = store.stateFlow //Flow<State>
val feedFlow = store.stateFlow.map { it.feed } // Flow<PagingData<T>>
The Composable is then collecting both (it’s the same flow but the second one is just a map):
Copy code
val state by bloc.states.collectAsState(Dispatchers.Main.immediate)
val feedItems = bloc.feedFlow.collectAsLazyPagingItems(Dispatchers.Main.immediate)
This was working perfectly when I wasn’t using Decompose components. But now this causes something weird with the recomposition because if I comment out the first line and I only observe the feed items it works as expected.
a

Arkadii Ivanov

11/02/2023, 12:40 PM
So this started to happen after converting to MVIKotlin. Indeed, the paging3 library doesn't work well with MVI. And actually I believe there is usually no need for the paging library. The following approach worked for me just fine multiple times.
Copy code
data class State(
  val items: List<Item>,
  val isLoadingMore: Boolean,
)

sealed class Intent {
  object LoadMore : Intent()
}
Then in your Executor, when you receive
LoadMore
, just load the next page something like this:
Copy code
if (state.isLoadingMore) {
  return
}

dispatch(Msg.LoadingMoreStarted)
val items = repository.loadMore(offset = state.items.size())
dispatch(Msg.LoadedMore(items))
Don't forget to handle Msgs and switch the
isLoadingMore
flag.
n

Nacho Ruiz Martin

11/02/2023, 12:40 PM
So this started to happen after converting to MVIKotlin.
Sorry for not being clear! I was using MVIKotlin with the nav component. I’m currently just migrating to Decompose.
a

Arkadii Ivanov

11/02/2023, 12:41 PM
Oh, then I don't see how Decompose could affect. It's just a holder for your Store, similarly to ViewModel.
There must be something else.
n

Nacho Ruiz Martin

11/02/2023, 12:42 PM
Yes sad panda . And the weird thing is that I was already using Essenty’s
InstanceKeeper
to keep my stores between config changes. I’m just adding the Decompose layer.
a

Arkadii Ivanov

11/02/2023, 12:43 PM
That's surprising, as I don't see how InstanceKeeper fits with navigation-compose. I would just put the Store in a ViewModel.
n

Nacho Ruiz Martin

11/02/2023, 12:45 PM
I couldn’t use ViewModel because I went for a multiplatform approach with only native views. Later on, I decided to also use Compose Multiplatform and hence the migration to Decompose. I was using a
Component
class that wasn’t Decompose, just to hold the
Store
instance.
That was a bad decision, now that I can look back. I should’ve used Decompose directly 🤷 .
InstanceKeeper fits with navigation-compose
I used Koin to help me with this. With the
instanceKeeper
extension to retrieve it from the `NavBackStackEntry`:
Copy code
@Composable
inline fun <reified T> getComponent(
    entry: NavBackStackEntry,
    vararg params: Any,
): T {
    return koinInject {
        parametersOf(
            entry.instanceKeeper(),
            *params,
        )
    }
}
And then
Component
is just:
Copy code
interface Component<Intent : Any, State : Any, Event : Any> {
    val states: StateFlow<State>
    val events: Flow<Event>
    fun accept(intent: Intent)
}

class DefaultComponent<Intent : Any, State : Any, Event : Any>(
    instanceKeeper: InstanceKeeper,
    storeFactory: () -> Store<Intent, State, Event>,
) : Component<Intent, State, Event> {
    private val store = instanceKeeper.getStore(storeFactory)
    override val states = store.stateFlow
    override val events = store.labels

    override fun accept(intent: Intent) {
        store.accept(intent)
    }
}
The Component gets recreated each time but the store doesn’t.
But then, this doesn’t explain why the flows are not working the same way in the new Decompose approach.
a

Arkadii Ivanov

11/02/2023, 12:54 PM
Yes, looks weird.
It's still not clear why there are 0 items in the state on first composition.
n

Nacho Ruiz Martin

11/02/2023, 1:02 PM
No, but it’s surely related to the fact that the same flow (store.states) is being collected twice in the same composition.
a

Arkadii Ivanov

11/02/2023, 1:03 PM
It should be remembered, and the actual collection should be performed only once
Can I see how you navigate your Composables?
You can add DisposableEffect(Unit) { onDispose {} } and log onDispose. It shouldn't be called when you go back. Only called once when you go forward.
n

Nacho Ruiz Martin

11/02/2023, 1:08 PM
It should be remembered, and the actual collection should be performed only once
Didn’t follow this.
Can I see how you navigate your Composables?
Surely! I think it’s the usual Decompose approach:
MainBloc
that holds everything looks like this:
Copy code
Children(mainBloc.childStack) {
                when (val child = it.instance) {
                    is MainBloc.Child.Home -> {
                        HomeContainer(child.bloc, snackbarShower::showSnackbar)
                    }

                    is MainBloc.Child.PostDetails ->
                        PostDetailsScreen(
                            child.bloc,
                            child.openCommentBox,
                            snackbarShower::showSnackbar
                        )
                }
            }
The
HomeBloc
is
Copy code
Scaffold(
        modifier = Modifier.fillMaxSize(),
        bottomBar = {
            MainBottomBar(
                currentDestination = state.currentDestination,
                destinations = state.destinations,
                onDestinationClick = {
                    bloc.accept(HomeContract.Intent.OnDestinationClick(it))
                }
            )
        }
    ) {
        Children(bloc.childStack) {
            when (val child = it.instance) {
                is HomeBloc.Child.Feed -> FeedScreen(child.bloc, showSnackBar)
                HomeBloc.Child.MyProfile -> TODO()
                HomeBloc.Child.Notification -> TODO()
                HomeBloc.Child.Progress -> TODO()
            }
        }
    }
The two Composables that are causing this are
FeedScreen
(the list) and
PostDetailsScreen
. Were you asking for this or the internal Component logic?
a

Arkadii Ivanov

11/02/2023, 1:09 PM
Yep, all looks good here
By remember, I mean it should be done automatically inside the collectAsState whatever functions.
No need to manually remember
n

Nacho Ruiz Martin

11/02/2023, 1:10 PM
Oh, OK, yes, I follow you.
a

Arkadii Ivanov

11/02/2023, 1:12 PM
Then it feels that the state somehow gets reset in the Store, assuming that the store doesn't get recreated for some reason.
n

Nacho Ruiz Martin

11/02/2023, 1:14 PM
I’ll try to pull the thread from there. Thank you 🙇
👍 1
It seems it’s a bug on the
paging3
library: https://stackoverflow.com/questions/76120368/in-compose-pagingdata-lazypagingitems-returns-0-items-and-loading-state-initia The reason why it worked with nav component it’s because the navigation was slower ( ) and only the second state with the items already sent was rendered. I checked and it was already getting 0 items and then the real amount, it just wasn’t rendered.
a

Arkadii Ivanov

11/02/2023, 1:47 PM
Thanks for the update! It seems like it was (partially?) fixed? https://issuetracker.google.com/issues/177245496#comment45
n

Nacho Ruiz Martin

11/02/2023, 1:49 PM
Yes, indeed, but only partially: https://issuetracker.google.com/issues/177245496#comment56 and https://issuetracker.google.com/issues/177245496#comment58 So I’ll try to make the “fix” work!
Anyway, Arkadii, thanks very much for the effort! And also for the magnificent libraries! 🙇
K 1
a

Arkadii Ivanov

11/02/2023, 1:58 PM
Yay!
4 Views