If I have my own `Presenter`like type ```interfac...
# compose
u
If I have my own `Presenter`like type
Copy code
interface Presented {
   fun onCreate()
   fun onDestroy()
}
that I'd like to create per
nav3
screen (
NavEntry
) How can I have the
onCreate/onDestroy
called automatically somehow? I'm reading up on
NavEntryDecorator
which sounds like what I want, but I'm not sure how it would access the given `presenter`instance of a given screen?
p
u
RetainObserver
ookay this is the missing piece thank you!
👍 1
a follow up ..
retain
is a top level function, how does it know know to scope the object to "this" nav entry?
p
Humm, I haven't read the implementation TBH. But I guess this is the answer: https://android-review.googlesource.com/c/platform/frameworks/support/+/3904490
u
Btw I'm trying it out and it doesnt seem to work
Copy code
00:30:26.141  D  AppRoot
00:30:26.178  D  home entry=sk.ursus.nav3demo.HomePresenter@691705a // now push profile
00:30:28.517  D  profile entry=sk.ursus.nav3demo.ProfilePresenter@7f9d399 // now orientation change
00:30:32.680  D  AppRoot
00:30:32.704  D  profile entry=sk.ursus.nav3demo.ProfilePresenter@7f9d399 // now back
00:30:36.313  D  home entry=sk.ursus.nav3demo.HomePresenter@42c3923 // observe different instance of home vm <-----------
Copy code
entryProvider {
    entry<Home> {
        val homePresenter = retain { HomePresenterl() }
        Log.d("Default", "home entry=$homePresenter")
        HomeScreen(
            onGoToProfile = { backStack += Profile }
        )
    }

    entry<Profile> {
        val profilePresenter = retain { ProfilePresenter() }
        Log.d("Default", "profile entry=$profilePresenter")
        ProfileScreen(
            onBack = { backStack.pop() }
        )
    }
}
nevermind I cant read, it says it needs the
RetainedValuesStoreNavEntryDecorator
which is not in the library yet
p
Right, the last section said that, is still under development 😢
u
btw where do you think is the correct place to call
onCreate
?
Copy code
@Composable  
fun <T : Presenter> retainPresenter(factory: () -> T): T {  
    return retain {  
        val presenter = factory()  
        presenter.onCreate()
        RetainedPresenterObserver(presenter)  
    }.presenter  
}
here?
p
I think in this case I would question myself what onCreate() means. Does it mean the class has been instantiated? - what not use
init {}
. Does it mean the first time it enters composition? - I would use one of the RetainCallback functions. But I guess is fine placing it where you have it, instead of using init{}
u
well I dislike google's pattern of running business logic in
init { .. }
, to me it's super sketchy, doing such stuff before the instance is even created so I delegate such logic to a standalone function once instance was created and yea semantically it means "presenter is ready".. im coming from fragments so there is was next line after instantiation in
onCreateView
p
In such a case I think is fine calling it from there, but I would also check if some function callback in the RetainObserver meet your needs, it is cleaner in my opinion using the RetainObserver
t
Why would we need
retain
if the UI runtime handles all configuration changes (all of them get added to
configChanges
in the manifest)
p
If you don't use a ViewModel you will lose your data when config changes. You could use rememberSavable but it impose restrictions that only serializable classes can be used. So if you have things like a VideoPlayer it won't work. So retain is the pure compose solution without imposing having to inherit from Android VM
t
"when config changes" is operative here. It's my understanding that the only reason you lose state is if you let the system handle config changes (by restarting your activity). Compose is supposed to handle all config changes, so
retain
would ordinarily do nothing more than
remember
.
p
When config changes, even though the Activity remains alive, the root Composable do a full recomposition, remember won't survive that
t
Got it! Something I was misinformed about.
👍 1
u
also rememberSaveable doesnt keep the instance across config changes right? It recreates evrrytime? If so, then with using retain, how can I pass the savedstate as a seed to the initial state of the presenter on process death/restore?
p
retain
is not meant to cover the process death scenario. Not sure how to do what you want. And yes you are right about the rememberSavable behavior
u
I know, but sometimes you do want to pass state from the previous process run to the new one, yes its a new vm instance but with previous state data the usual bundle stuff; nothing new; you've done this with Fragments all the time
p
Right, in that case you will have to use both I guess.
u
do you have pointers as to how? `rememberSaveable`needs to be setup ahead of time somehow and passed into the VM as parameter but what if my state is a plain data class? then at save time I need to serialize my state into the savable state - and is there such callback at all?
p
rememberSavable won't work with non-serializable classes. The implementation details you don't care. On process recreation I guess It will desirialize the saved data and you would pass it to retain. I guess you will have to call rememberSavable first. But you can encapsulate the two calls in one single function:
retainWithSaveable()
u
right, but doesnt that mean I need to hoist my presenter state out of presenter?
t
It doesn't make sense to do this. Typically (in the non-Compose world) you "retain" a ViewModel so it isn't recreated on config change. You "save" any UI state you need to recreate the ViewModel after restoring your process. You don't save the ViewModel itself, and you don't retain the ViewModel's state.
u
Of course I know, the use case is "payment options screen, on init options get fetched, and user can select one, selection just updates selection, only action button makes it continue" so I want to run the fetching in retained viewmodel scope, so the op doesnt die mid rotation - and I want to preserve selection index across config changes" so when the new vm instance fetches the options again, I'll just have the index ready
p
Right, perhaps there is no point in mixing both, remembersavable and retain. I am just wondering a possible use case. But yeah The VM is a powerful beast in this Android aspect .
u
okay maybe not the greates example but it's real from a project of mine
so retaining is the desired behavior of presenter, and in some screens I want to pass bits of state across config changes
i.e. refetching options on process restore is fine, but don't make the use reselect if already did
t
Well if the options change then you probably don't want to restore the selection index
u
it's a synthetic example, say I do, not preserving the selection index is not an option
do I really have to trade off preserving selection vs having coroutine scope die on orientation change?
it's a step back from fragments, if that's the case..
t
Of course not, but it's much easier with a real ViewModel because you wouldn't have to hoist out the persisted state
u
how? VM itself can have the state reinit magically across process deaths?
(im not seeing how is androidx.ViewModel different from a retained instance)
t
VM itself uses ViewModelStore which is retained automatically, without needing a
retain { }
in the composition
u
so? using retain you get the same lifecycle, with a 1st party api, not some bolted on lib
so..they're equal from this perspective, unless im missing something
t
I'll put aside
androidx-lifecycle
components being bolted-on libs... But the issue is that you have all this code in your composition to
rememberSaveable
this piece of state,
retain
that piece of state, etc, and VM just encapsulates it for you so you have one
viewModel()
call. I do it the way you want in our production app and it is a ton of boilerplate. Now that I'm clued in to this
retain
stuff I am starting to regret that choice, because we probably don't do things like network calls correctly across config changes, and it's not something that is easily testable so I have no idea until I make something break manually.
u
wdym?
viewModel()
has saving state across process death automatically?
t
Not automatically, via
SavedStateHandle
u
well, can't I use it in my type? does it require to be used in a viewmodel?
t
No, that is completely specific to the ViewModel machinery. Also why I ignored it, because CMP didn't support it at the time.
u
hmm okay, that changes things, thanks
it's a shame androidx viewmodel is such bad api .. the "run logic in init" is just nuts to me
but lets not get into that, .. I didnt know savedstatehandle cannot be used outside their vm, great info
Copy code
var savedState by rememberSaveable(stateSaver = PaymentSavedStateSaver) {
    mutableStateOf<PaymentSavedState?>(null)
}

val presenter = retain {
    PaymentPresenter(
        initialState = savedState?.toState() ?: State()
    )
}

LaunchedEffect(key1 = presenter) {
    presenter.state.collectLatest { latestState ->
        savedState = latestState.toSavedState()
    }
}
is this lame?
sounds kinda the same, it's two states that need being updated .. same in here, same in duplicating the vm state changes into the
SavedStateHandle
t
Pretty much exactly what I don't like about this
retain
business. I think a less-ugly option is to replace the
by
with
=
and pass the
MutableState
instance to your presenter. Then you have one source-of-truth.
u
then my presenter state needs to be compose State type, no?
t
Yes. If you want to completely divorce snapshot state from the presenter then obviously that won't work, but I generally encourage using it at the day job.
Otherwise you have two sources of truth you need to sync
u
yea i know bur isnr it what SavedStateHandle does? a 2nd sourfe of truth?
t
Yeah that's why it's bad too :) The VM has to push updates to it, when a better system would pull from the VM (the single source of truth)
u
which is what we had with fragments 😀
t
...yes.
p
You can encapsulate above code in a generic Composable and it will be 1 line of code.
Copy code
fun <T, P : Presenter> rememberRetainSavaablePresenter(
  presenterFactory: (T) -> P
) : P {}
Then
Copy code
val presenterX = 
rememberRetainSavaablePresenter {
  PresenterX(initialState = it)
}
t
Your T instance needs to come from somewhere
p
Missed that, I believe perhaps forcing the Presenter interface to provide an initial value. But let me read Vlas code again, I might have missed something.
t
Yeah you need at least the arguments to
rememberSaveable<T>
and
retain<P>
And that will not "compose" cleanly if you want the invalidation key stuff
p
I see
t
IDK why
rememberSaveable
shouldn't also retain the created instance; I am struggling to find a case where you would want to recreate on config change but not on process restart
p
I believe the idea is separating state in two types. One type that is serializable, and small, suited for process death. The other state, classes and instances
u
yea i think its just pragmatic, persisting avross config changes is the onRetainInstancr or whatever the activity api is, so it has nothing to do with saving state but yes they should align more with what the usual usecase is
... yea but api shape wise what would more make sense is for the RetainObserver to have onSaveState callback to implement and pull out the serualiAble state synchronously
not via compose magic
t
@Pablichjenkov the rememberSaveable API is exactly that: you pass in a Saver<P, T> that saves and restores a P by persisting a smaller T. You also pass in an initializer to create a P, which I am saying should be retained.
p
Right , same principle
I say it twice, the ViewModel is really convenient.
u
I mean you can easily stuff my sample inside a function and just reuse, same convenience
t
With the overhead of observing snapshot state as a Flow in order to update snapshot state :)
u
yea… yea.. it should be a callback on retain observer really thankfully this is a minority usecase, i think i can take the hit on the few screens