Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor JerboaAppState into a viewmodel, instead of passing data between screens using SavedStateHandle #1728

Open
3 tasks done
dessalines opened this issue Nov 22, 2024 · 6 comments
Labels
enhancement New feature or request refactor Non-user-facing code improvements

Comments

@dessalines
Copy link
Member

dessalines commented Nov 22, 2024

Pre-Flight checklist

  • Did you check to see if this issue already exists?
  • This is a single feature request. (Do not put multiple feature requests in one issue)
  • This is not a question or discussion. (Use https://lemmy.ml/c/jerboa for that)

Describe The Feature Request Below

@MV-GH I wanted to get your input on this before I start it.

After reading this article on passing data between screens, it seems that the way we're passing data between composables (Using the SavedStateHandle), isn't as good as using a viewmodel (ignore their hilt recommendation, we can just use a regular jetpack one).

Cons

  • Only works using the navcontroller, which we can't use for things like embedded navhosts, or tablet UI's which have their own pane system.
  • Untyped
  • Requires parcelable and serialization.

It'd be much better if we turned JerboaAppState into a viewmodel, and used it as a persistent way to pass data between screens. Wouldn't rely on any navcontroller, and would be well typed.

@dessalines dessalines added enhancement New feature or request refactor Non-user-facing code improvements labels Nov 22, 2024
@MV-GH
Copy link
Collaborator

MV-GH commented Nov 23, 2024

Untyped

Since 2.8 navController they added type safety features SafeArgs
https://developer.android.com/guide/navigation/design/type-safety

Requires parcelable and serialization.

All Datatypes implement this. Its not that much of a drawback.

SavedStateHandle

SavedStateHandle is the only persistence that survives process-death.
https://developer.android.com/topic/libraries/architecture/saving-states#options

Currently we kinda use all three approaches.

  • Global Viewmodels like siteViewModel AccountViewModel

  • SavedStateHandle to pass data between Screens.

  • Route Arguments to get state from the route path.

I don't exactly see how you are going to replace SavedStateHandle with Viewmodel. Are you going to add a destination state variable per screen?

The advantage with SavedStateHandle is that the state bound to the route. Meaning if you go deeper and then pop back. The state is still there. And if you go open something again it doesn't retain the previous navigation state. And you can control this behaviour see https://developer.android.com/develop/ui/compose/navigation#bottom-nav

Currently I don't really know/experience about panes/embedded navhosts.

But if they all exist with the same destination then you don't need JerboaAppState to pass data between them? And can just use a normal viewmodel?

I would need to see much more concrete example but imo I don't see anything wrong with JerboaAppState other then we need to use type-safety. #1531

@dessalines
Copy link
Member Author

dessalines commented Nov 23, 2024

Fair enough w/ typing, but just as important IMO, is de-coupling shared data from the router / navcontroller.

For example, with this tablet UI, we now have 2 different nav routes for posts:

  • The main top level one (used for post links, and elsewhere)
  • As a pane in the tablet list-detail view, in an embedded navcontroller at /home (we could also even potentially add a /home?post_id={} , as a link).

Lets say you create a comment or post for the 2nd case: the embedded router has no idea about the action, because its tied to a navcontroller you might not be using.

  • Global Viewmodels like siteViewModel AccountViewModel
  • SavedStateHandle to pass data between Screens.
  • Route Arguments to get state from the route path.

We don't really need the 2nd of the third approach, with all this custom code around consuming saved state handles. We can refactor JerboaAppState into a global viewmodel as suggested here to store and read shared data that needs to be passed between screens AND panes, independent of any router.

The advantage with SavedStateHandle is that the state bound to the route. Meaning if you go deeper and then pop back. The state is still there. And if you go open something again it doesn't retain the previous navigation state. And you can control this behaviour see https://developer.android.com/develop/ui/compose/navigation#bottom-nav

A viewmodel should be able to do that also (the data will persist in the same way), and if we need to remove the data after its consumed, that should be possible also. My tablet PR removes that bottom nav in favor of their new adaptive navbar also.

The main difference between this new global shared viewmodel I'm proposing, that's different from our current viewmodels, is that the purpose of those is mainly to do network requests and persist that data for a specific screen. This new one will specifically be about sharing data between screens. We might as well refactor JerboaAppState into a viewmodel, since its currently serving that purpose.

@MV-GH
Copy link
Collaborator

MV-GH commented Nov 24, 2024

de-coupling shared data from the router / navcontroller.

This has advantages though

How will you replicate this behaviour?

-> home
-> open post
-> go to profile of a commenter
-> open a post
-> go back
-> go back

Needs to show the original post again?

Assuming you are suggesting

class RouterVM {
 var postState ...


  fun goToPost(id...) {
    postState = id...
    navController.navigate(Route.Post) // ?? still need navController 
  }
}

I just fear it will be very messy, and it still not clear to me, how exactly you are going to replace it?

It will need to support process-death so the RouterVM needs to use SaveStateHandle too. And it will need to support deeplinks.

@dessalines
Copy link
Member Author

dessalines commented Nov 24, 2024

-> home
-> open post
-> go to profile of a commenter
-> open a post
-> go back
-> go back

None of that actually needs a shared app state, all that's required there is to pass a navcontroller to the screen, and use onClickPost or onClickProfile events to navigate (or pop back, in the case of things like creating comments)

The problem I'm describing is about sharing data between screens.

  • You're viewing the post pane on a tablet UI : IE /home, OR the post route /post?id=
  • You click any reply to comment button
  • It takes you to the create comment screen.
  • After that completes, it pops back to the last screen
  • You need to share that newly created comment to your post pane or post page.
    • You can't really use the router, because each of those use two different nav hosts.
    • You can use a global viewmodel for both cases.

The process death is not important IMO because the shared data could get cleared out after its read / consumed anyway.

@MV-GH
Copy link
Collaborator

MV-GH commented Nov 26, 2024

Okay its bit more clear now but arguably my example still kind of applies.

Let me show you can example but its a bit of an edgecase but its similar to a bug we have had.

It is just for illustrative purposes but there are probably better cases. But its an edge case we have to support regardless imo

  • open profile
  • edit a post
  • body contains an @user
  • preview
  • click @user, see their profile
  • (come back crash, it was consumed (all though not a problem with your proposal we keep the state))
  • we are still on the profile (be a mod/ on your own profile)
  • edit a post under that profile
  • finish
  • go back to your first edit
  • It will show the passed postview of the second edit, but the edit of the first edit. As we use 'rememberSaveablewhich usesSaveStateHandle` so that state will be correctly scoped.

This also reminds me of a bug in the early days where we used to have only global viewmodels. When it failed to a load a posts comments it would show the comments of the previous post. Which was very confusing :D

The process death is not important IMO because the shared data could get cleared out after its read / consumed anyway.

Debatable, you don't always consume it, here an example

  • edit your post
  • swap to the browser to look/copy some information (in this very instance, jerboa can get killed, you come back, the edit is gone.)
  • Currently this would fully reload the app to previous state (due to StateHandle keeping the PostView in PostEditScreen, and the rest of the state uses rememberSaveable, where Saveable uses SaveStateHandle under the hood)

We can still use StateHandle with ViewModels though, just bit of manual work


Now back to

is de-coupling shared data from the router / navcontroller.

I didn't make my argument clear before.

We absolutely do want our state to be scoped to the "screen destination entry".
Even when its just passing data from screen to screen. We can have multiple of these entries in the stack.

(little example)

  • Route post/1/edit (was passed postView 1)
  • Route post/2/edit (was passed postView 2)

Each should hold its own scoped state and should be cleared when the entry is removed from the stack.

Using the navcontroller was/is ideal for this, we can access the stack and thus the SavedStateHandle

JerboaAppState wraps the navController as its unstable + now only exposes "typesafe" navigations + Framework to persist data in the SaveStateHandle

As for your suggested approach:

We can also access the SavedStateHandle in the Viewmodel. So no problem here.

But we need also need to scope this viewmodel to the "screen destination entry"/backstackEntry

But if you scope it to the screen you can't access it from another screen. Thus you can't pass the data through the viewmodel like this. :/

So you would have to make a "global" viewmodel i.e one bound to the activity. But then you will have the problems I just described here. And the ones we have before when we had global viewmodels everywhere.

But it ll be more complex as we will still have the main scoped viewmodel + now RouterViewmodel global state. (States no longer in sync)

I understand because of this it doesn't exactly work right now with panes. Atm I don't know a lot of panes but I think its possible that we can still apply the same trick with panes. As long as it has the same SaveStateHandle, we would then only need to adjust JerboaAppState to also work/navigate using PaneScaffoldNavigator for the those endpoints/destinations.

I used NowInAndroid as an inspiration originally and they are using adaptive now too. So we can do the same again.
see https://github.com/android/nowinandroid/blob/main/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/interests2pane/InterestsListDetailScreen.kt#L87

@dessalines
Copy link
Member Author

The link outlines the problem I'm running into: that panes and tablet UI's use their own navhost / navigator.

open profile
edit a post
body contains an @user
preview
click @user, see their profile
(come back crash, it was consumed (all though not a problem with your proposal we keep the state))
we are still on the profile (be a mod/ on your own profile)
edit a post under that profile
finish
go back to your first edit
It will show the passed postview of the second edit, but the edit of the first edit. As we use 'rememberSaveablewhich usesSaveStateHandle` so that state will be correctly scoped.

Normally UI's do this by "protecting / guarding" edit/create-type routes, so that when you navigate away from them, it warns you that the data will be lost. Its a bit overkill IMO to try to keep edit or create screen data on the stack also, it should probably popUpTo wherever it came from, rather than trying to keep all this shared data scoped to specific routes.

Also in the case above you could easily create a Map<PostId, EditData> in the global viewmodel, rather than trying to shove the shared data into the navcontroller which might not be the current one in use.

I greatly doubt most lemmy ui's do anything this complicated, and its not necessary. Most importantly though, its not flexible enough to use with tablet UI's and different navhosts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request refactor Non-user-facing code improvements
Projects
None yet
Development

No branches or pull requests

2 participants