Giter Site home page Giter Site logo

Comments (20)

arkivanov avatar arkivanov commented on May 28, 2024 1

It is required by the nature of the library. Configurations are unique keys of components. Each component in a particular Router's stack must have unique configuration within the Router's stack. This way the Router can distinguish configurations and create/destroy components accordingly.

I will consider renaming Configuration to Key, perhaps it will make it more explicit. Filed #210.

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

Hello. The StateKeeper saves component state (aka business logic), the Children function is also responsible for saving the UI state. E.g. when you navigate from List to Details and then back to List, the scrolling position is automatically preservaled. This is a general approach, the Google`s compose-navigation should do the same.

It seems that your UI saves a lot of data. Could you describe your case in more detail?

from decompose.

avently avatar avently commented on May 28, 2024

E.g. when you navigate from List to Details and then back to List, the scrolling position is automatically preservaled

If so, maybe a good idea would be to use hashCode of child.configuration instead? If the data is the same, hashCode will be the same too but we won't save the data into bundle.
With hashCode we can get false positive since it can be the same for different data so probably we need another idea.

Could you describe your case in more detail?

I just have a list of objects I got from REST API and cached, pass them to a screen with Pager. After that the screen with the Pager loads more data from REST based on previous objects. So objects are highly coupled and for me it's much easier to pass them into configuration than storing elsewhere.

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

maybe a good idea would be to use hashCode of child.configuration instead? If the data is the same, hashCode will be the same too but we won't save the data into bundle

Sorry, but it's not clear what you mean. Maybe you could provide a code snippet? The Children function just preserves states of all Composable functions in the back stack.

Regarding your use case. If you pass objects via configuration then this data should be saved by StateKeeper, it's not clear how is it related to the UI state (the Children function). The latter only saves what is saved by the child Composable function.

from decompose.

avently avatar avently commented on May 28, 2024

This is a part of MainComponent:

val router: Router<ChildConf, Content> =
        appRouter(
            initialConfiguration = { ChildConf.Server },
            handleBackButton = true,
            childFactory = ::childContent,
            configurationClass = ChildConf::class
        )

private fun childContent(
        conf: ChildConf,
        context: AppContext
    ): Content =
        when (conf) {
            is ChildConf.Server -> ServerComponent(context, offsetChangedListener).asContent { it.ServerScreen(conf)
            is ChildConf.Login -> LoginComponent(context).asContent { it.LoginScreen() }
            is ChildConf.Plan -> PlanComponent(context).asContent { it.PlanScreen() }
        }


sealed class ChildConf : Parcelable {
        @Parcelize
        object Server : ChildConf()

        @Parcelize
        data class Plan(
            var plans: List<ApiPlan>,
            // maaaany other already cached values, no need to reload from internet
        ) : ChildConf()

        @Parcelize
        object Login : ChildConf()
}

This is MainScreen:

@Composable
fun MainScreen(mainComp: MainComponent) {
    Column {
        Children(routerState = mainComp.routerState, animation = mainComp.routerAnimation) { it: Child.Created<MainComponent.ChildConf, @Composable () -> Unit> ->
            it.instance()
        }
    }
}

The Children function just preserves states of all Composable functions in the back stack.

This line stores the whole configuration (like ChildConf.Plan in my example) in the bundle. If there will be heavy configurations we end up with TransactionTooLargeException.
https://github.com/arkivanov/Decompose/blob/master/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/Children.kt#L27

Let's say I open PlanScreen. I pass some data via ChildConf.Plan.
This is an output of the lib https://github.com/guardian/toolargetool (in order to see the output, I go to an Android launcher to trigger state saving into bundle and returning back to my app):

08-08 21:24:55.880 30512 30512 D TooLargeTool: ServerFragment.onSaveInstanceState wrote: Bundle200945447 contains 3 keys and measures 408.9 KB when serialized as a Parcel       
08-08 21:24:55.880 30512 30512 D TooLargeTool: * androidx.lifecycle.BundlableSavedStateRegistry.key = 203.4 KB                                                                   
08-08 21:24:55.880 30512 30512 D TooLargeTool: * activeFragment = 0.0 KB                                                                                                         
08-08 21:24:55.880 30512 30512 D TooLargeTool: * android:view_registry_state = 205.5 KB

Bundle with a key androidx.lifecycle.BundlableSavedStateRegistry.key will be cleared when I press back from the PlanScreen, so it will store ~0 KB,
Bundle with a key android:view_registry_state will never be cleared, it contains the keys you pass to rememberSaveable (ChildConf's).

Am I described the situation better?

For now I made this change to MainScreen:

@Composable
fun MainScreen(mainComp: MainComponent) {
    Column(modifier = Modifier) {
        val state = mainComp.routerState.subscribeAsState()
        val holder = rememberSaveableStateHolder()
        holder.retainStates(mainComp.routerState.value.asKeys())

        mainComp.routerAnimation(state.value) { child ->
            holder.SaveableStateProvider(child.configuration.confAsKey()) {
                child.instance()
            }
        }
    }
}

// THIS THING I TALKED ABOUT
private fun Any.confAsKey(): String = this::class.toString() + hashCode()

private fun <C : Any> RouterState<C, *>.asKeys(): Set<String> = 
    backStack.map { it.configuration.confAsKey() }.toSet() + activeChild.configuration.confAsKey()

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

Yes, now it's clear. So you are correct, each the configuration is stored twice. I will need to think if can improe the situation.

But the configuration should go away when you close the screen. It should be removed from SaveableStateHolder, which is done in the same file with Children function.

from decompose.

avently avatar avently commented on May 28, 2024

@arkivanov it should, yes, but it's not happening. So if visit multiple screens such screens are saved too.

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

Thanks, I will check it and provide updates here.

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

I checked the issue with non-removing configurations and could not reproduce. Configurations of popped screens are not saved into the Bundle. Both the StateKeeper and the Children function save configurations into androidx.lifecycle.BundlableSavedStateRegistry. And I don't see android:view_registry_state at all, which actually comes from FragmentStateManager. I think I will need a reproducer for this issue. Would you be able to provide one?

Regarding duplicated configurations (in StateKeeper and and in Children function) - I will try to fix it.

from decompose.

avently avatar avently commented on May 28, 2024

I don't see android:view_registry_state at all, which actually comes from FragmentStateManager

Did you setup your own fragment state listener instead of using the lib I suggested? If so, that's why you didn't see android:view_registry_state. I don't know why but I did the same thing and didn't see it too until I used the lib. Then I just copied the lib files into my project, added more logs into it to see the actual data inside bundle.

androidx.lifecycle.BundlableSavedStateRegistry is cleared ok, but not view_registry_key. Based on keys inside that bundle I found rememberSaveable:)

Anyway, I'll make a reproducer. Probably yesterday.

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

I checked using the TodoApp example with the following steps:

  1. Open the details screen and press back button
  2. Minimize the app
  3. Simulate app eviction using Logcat tab
  4. Put breakpoint in MainActivity.onCreate before super call
  5. Open the app with debugger attached
  6. Check the Bundle using object inspector

These are keys stored in the Bundle:

result = {MapCollections$KeySet@16386}  size = 4
 0 = "android:viewHierarchyState"
 1 = "androidx.lifecycle.BundlableSavedStateRegistry.key"
 2 = "android:lastAutofillId"
 3 = "android:fragments"

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

Tried to put everything into a Fragment and the key "android:view_registry_state" was in the Bundle. So it is only added by Fragments. However in my case it contained only one configuration, of the first screen.

from decompose.

avently avatar avently commented on May 28, 2024

In my case I saw the following when I opened one screen, then pressed back, then opened another screen, then pressed back, then went to a launcher:

[androidx.lifecycle.BundlableSavedStateRegistry.key = Bundle[{STATE_KEEPER_STATE=Bundle[{}]}], activ                          
eFragment = true, android:view_registry_state = Bundle[{androidx.lifecycle.BundlableSavedStateRegist                          
ry.key=Bundle[{SaveableStateRegistry:-1=Bundle[{wyp51p=[{Plan(someProperty="", ........... ))={}, Ac
count(account="someinfo")={-acf57h=[Closed], 2byr8d=[[0, 0]], dc
4esm=[{DefaultLazyKey(index=0)={}, DefaultLazyKey(index=1)={}, DefaultLazyKey(index=2)={}, DefaultLa
zyKey(index=3)={}}]}, com.myapp.ui.screens.main.MainComponent$ChildConf$Server@e564bfe={5fq
sbv=[Closed], 7wcquv=[113], es3y1y=[[0, 0], [0, 0]], -atccld=[{DefaultLazyKey(index=0)={}, DefaultLa
zyKey(index=1)={}, DefaultLazyKey(index=2)={}, DefaultLazyK.......

Your STATE_KEEPER_STATE is ok
android:view_registry_state has:

  • Plan conf for PlanScreen (with all the properties, I just deleted them because they so long). Notice that Plan conf is the key in map with an empty value (I did nothing with this blank value).
  • Account conf for AccountScreen. In this case a value for that key contains some other things I don't know about (like -acf57h=[Closed], 2byr8d=[[0, 0], dc4esm=[{DefaultLazyKey(index=0)={}).

Since you reproduced the problem do you still need a project-reproducer? Or it's enough info already? I think if you'll figure out a better replacement for the whole object it will be more than enough.

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

As I mentioned, I couldn't reproduce. The second screen's configuration is removed from all bundles if I do the following steps:

  1. Close the second screen via back button, so it is removed from stack
  2. Minimize the app

So a reproducer with exact steps is still would be useful.

from decompose.

avently avatar avently commented on May 28, 2024

Arkadii, I found why we have different results. In my case I modified the instance of Plan conf in a screen so it had new data in it after entering PlanScreen. This way the instance will be preserved in android:view_registry_state forever.

Example:

// <MainComponent.kt>:

private fun childContent(
        conf: ChildConf,
        context: AppContext
    ): Content =
when (conf) {
   is ChildConf.Test -> TestComponent(context).asContent { TestScreen(conf) }
}

@Parcelize
data class Test(
    var someList: List<String>
) : ChildConf()

// </MainComponent.kt>

// Do it from any screen of the app, for example, after clicking on a button:

val conf = ChildConf.Test(listOf("123"))
 // I have one router for all screens inside MainComponent, if it matters. Passing the router via custom component context
router.push(conf)


// <TestComponent.kt>
package com.example.screens.test

// AppContext is just custom context with public router
import com.example.reusable.AppContext

class TestComponent(
    private val context: AppContext
) : AppContext by context

// </TestComponent.kt>

// <TestScreen.kt>
package com.example.screens.test

import androidx.compose.runtime.*
import com.example.screens.main.MainComponent

@Composable
fun TestScreen(conf: MainComponent.ChildConf.Test) {
    LaunchedEffect(Unit) {
        conf.someList = listOf("777")
    }
}
// </TestScreen.kt>

Result:

[androidx.lifecycle.BundlableSavedStateRegistry.key = Bundle[{STATE_KEEPER_STATE=Bundle[{STATE_KEEPE
R_STATE=com.arkivanov.essenty.parcelable.AndroidParcelableContainer@127462f}]}], activeFragment = tr
ue, android:view_registry_state = Bundle[{androidx.lifecycle.BundlableSavedStateRegistry.key=Bundle[
{SaveableStateRegistry:-1=Bundle[{ursb73=[{Test(someList=[777])={}, com.example.screens.ma
in.MainComponent$ChildConf$Server@34b84b4={-ugkp4i=[Closed], -r890dq=[0], qcovrk=[[0, 0], [0, 0]], -
y5sdis=[{DefaultLazyKey(index=0)={}, DefaultLazyKey(index=1)={}, DefaultLazyKey(index=2)={}, Def

Are you able to reproduce?

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

Configurations must be immutable, as they are used as keys in things like Map, etc. If a configuration is mutated, it can not be longer located. I will add this disclaimer to the documentation.

from decompose.

avently avatar avently commented on May 28, 2024

@arkivanov I didn't expect that configuration will be used as a key somewhere. It's not recommended and a source of bugs as we see. Why not to drop usage as key? Is it required?

from decompose.

avently avatar avently commented on May 28, 2024

For me configuration sounds good but key isn't. Maybe adding a note about usage configuration as key would be enough?

from decompose.

arkivanov avatar arkivanov commented on May 28, 2024

Big thanks for you feedback and use cases @avently. I will think about it.

from decompose.

avently avatar avently commented on May 28, 2024

Thanks for you, not me. I would spend many weeks/months for creating something like Decompose.

from decompose.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.