Zhuinden / simple-stack

[ACTIVE] Simple Stack, a backstack library / navigation framework for simpler navigation and state management (for fragments, views, or whatevers).
Apache License 2.0
1.36k stars 76 forks source link

Getting arguments with getKey working first time but not the second #260

Closed MikaReesu closed 2 years ago

MikaReesu commented 2 years ago

Hello,

In a Splashcreen, I first use this to go to the HomeFragment, so the key is already created :

backstack.setHistory(History.of(HomeScreen(0, true), StateChange.FORWARD)

In a child view(a fragment) I use goTo with different data than the Splashscreen to pass to the HomeFragment. After this goTo I get the data of the Splashscreen in the HomeFragment, I guess cause the key is already created.

backstack.goTo(HomeScreen(2, true))

When I go back to the childview and when I use goTo again, this, time the data is passed to the HomeScreen. So the second time I use the goTo in the child is the moment when I finally send the good data to HomeFragment.

HomeFragment(to get the data) : val (indexPage, isRedirected) = getKey<HomeScreen>() if(isRedirected == true){ selectedIndex = indexPage }

HomeScreen :

` @Parcelize data class HomeScreen( val indexPage: Int, val isRedirected: Boolean? = false ): ScreenKey() { override fun instantiateFragment(): Fragment { return HomeFragment() }

override fun bindServices(serviceBinder: ServiceBinder) {
    with(serviceBinder){
        add(HomeViewModel(lookup(), backstack, lookup()))
    }
}

} `

I hope I was clear enough, and you need more code than this, I will add it.

Thank you for your help.

Zhuinden commented 2 years ago

Heyo,

Keys are passed to Fragments as arguments (via setArguments(Bundle().apply { putParcelable(DefaultFragmentArgs.ARGS_KEY, key))), and fragment arguments don't change over time once they are already set.

Also, by default, the getFragmentTag() returns getClass().getName(); as the "unique identifier" of a fragment, but if you are okay with destroying the previously existing HomeFragment and its scope, and replace it with a new scope, you can do return toString();, in which case the Fragment will be completely replaced with a new instance with the new arguments set.

Otherwise, normally I keep such selection state in a BehaviorRelay/MutableLiveData/MutableStateFlow etc and only use the key as the initial parameter for it. That way I can make changes to the selected state, keep it across process death, but don't try to override the initial arguments.

I hope that helps think of a solution, currently what happens in your case is "expected" (because of how fragment arguments are intended to work + what the default value of the fragment tag is) .

MikaReesu commented 2 years ago

Thank you for your fast reply!

Do you have a code example of this workaround? It seems to be quite interesting.

Thank you and have a great day!

On Wed, Jul 6, 2022, 00:25 Gabor Varadi @.***> wrote:

Heyo,

Keys are passed to Fragments as arguments (via setArguments(Bundle().apply { putParcelable(DefaultFragmentArgs.ARGS_KEY, key))), and fragment arguments don't change over time once they are already set.

Also, by default, the getFragmentTag() returns getClass().getName(); as the "unique identifier" of a fragment, but if you are okay with destroying the previously existing HomeFragment and its scope, and replace it with a new scope, you can do return toString();, in which case the Fragment will be completely replaced with a new instance with the new arguments set.

Otherwise, normally I keep such selection state in a BehaviorRelay/MutableLiveData/MutableStateFlow etc and only use the key as the initial parameter for it. That way I can make changes to the selected state, keep it across process death, but don't try to override the initial arguments.

I hope that helps think of a solution, currently what happens in your case is "expected" (because of how fragment arguments are intended to work + what the default value of the fragment tag is) .

— Reply to this email directly, view it on GitHub https://github.com/Zhuinden/simple-stack/issues/260#issuecomment-1175561908, or unsubscribe https://github.com/notifications/unsubscribe-auth/ATBTBWRPI4OCIHIEVPGQSZ3VSSY6XANCNFSM52W2WZWQ . You are receiving this because you authored the thread.Message ID: @.***>

Zhuinden commented 2 years ago

...that moment when you've been using this for ages but you don't have an open-source example 🤔

Anyhoo, the idea is that HomeViewModel can be passed the key as a constructor parameter, and in HomeViewModel you could do

private val selectedIndex = MutableLiveData(key.selectedIndex)

Once you have this, then you can persist the selected index via Bundleable

class HomeViewModel(...): Bundleable {
    ...

    override fun toBundle(): StateBundle = StateBundle().apply {
        putInt("selectedIndex", selectedIndex.value)
    }

    override fun fromBundle(bundle: StateBundle?) {
        bundle?.run {
            selectedIndex = getInt("selectedIndex", 0)
        }
    }
}

Once you have that, you can update the value in HomeViewModel easily

private val homeViewModel by lazy { lookup<HomeViewModel>() }

...

homeViewModel.updateSelectedIndex(newIndex)

And then you don't have to use the key to do it and you keep the ViewModel/Fragment instead of destroying them.


Not sure if overhead, I'm kinda used to it as this is how Fragment arguments worked in general as far as I'm aware 🤔 or at least in case of Activities, you were definitely not able to re-define the extras and actually preserve this change, which is why DefaultFragmentStateChanger doesn't try to invoke fragment.setArguments( on each state change, but only on creation.

Please notify if this solves your issue

MikaReesu commented 2 years ago

That's interesting, I think it will be much cleaner to use this method.

I still have a few questions related to that.

private val selectedIndex = MutableLiveData(key.selectedIndex) Where is the "key.selectedIndex" coming from?

So, the value is passing by the bundle of the ViewModel and not by the HomeScreen if I understand well.

And, finally the last piece of code will be used in the child Fragment to update the data before doing the goTo right?

private val homeViewModel by lazy { lookup<HomeViewModel>() }
homeViewModel.updateSelectedIndex(newIndex)

Thank you for your help and the time you take to answer.

Zhuinden commented 2 years ago

Where is the "key.selectedIndex" coming from?

That code is assuming that you keep it as initial param, but serviceBinder which creates the homeViewModel in add(HomeViewModel(lookup(), backstack, lookup())) actually has a getKey<T> function (and `this@HomeScreen is also the key to begin with) so the VM can be passed arguments during initialization.

So, the value is passing by the bundle of the ViewModel and not by the HomeScreen if I understand well.

That's how I generally do it, because I use args for initialization, but the rest happens via the "ViewModels" (scoped services).

And, finally the last piece of code will be used in the child Fragment to update the data before doing the goTo right?

Yes 👍

MikaReesu commented 2 years ago

So I need to add the getKey to HomeScreen :

add(HomeViewModel(**getKey()**, lookup(), backstack, lookup()))

In the HomeViewModel, Do I need to declare a key or no? The following code has errors, but is it suppose to be similar to this?

class HomeViewModel(
    private val key: <T>,
private val backstack: Backstack,
) : ScopedServices.Registered, Bundleable {
    private val selectedIndex = MutableLiveData(key.indexPage)

Sorry, to ask again, but I didn't think that I understood all correctly. Thank you, again!

Zhuinden commented 2 years ago

private val key: HomeScreen should work

MikaReesu commented 2 years ago

Oh I see, yeah working with the HomeScreen.

In the HomeViewModel I created the function updateindexPage. Is it gonna update it or I need to call toBundle in it? :

fun updateindexPage(index: Int){
        indexPage.value = index
    }

In the child fragment, is it correct?

homeViewModel.updateindexPage(2)
backstack.goTo(HomeScreen( 2, true))

So, after in the HomeFragment, to get the Value, I can"t do anymore, no? val (indexPage, isRedirected) = getKey<HomeScreen>()

Do I need to pass by HomeViewModel to get the data here ?

Zhuinden commented 2 years ago

In the HomeViewModel I created the function updateindexPage.

Is it gonna update it

It should, because of how LiveData works

or I need to call toBundle in it?

toBundle() is called by simple-stack, it's basically just like onSaveInstanceState.

So, after in the HomeFragment, to get the Value, I can"t do anymore, no?

Well, you should get the selected index from your ViewModel liveData with .observe now.

MikaReesu commented 2 years ago

Thanks a lot for your help!

It is working perfectly !