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

Should Simple Stack be used in new projects #279

Closed
LeafyLappa opened this issue Aug 11, 2023 · 14 comments
Closed

Should Simple Stack be used in new projects #279

LeafyLappa opened this issue Aug 11, 2023 · 14 comments

Comments

@LeafyLappa
Copy link

LeafyLappa commented Aug 11, 2023

Thank you for your contribution to Android. After reading your numerous comments and posts on the state of Android development I came across this library.

However I'm sure if I should use it in my new project. Back in 2020 you mentioned somewhere that Simple Stack and Jetpack's navigation had become more or less equal feature-wise. It's been 3 years.

So my question is, would you advice using Simple Stack in 2023 over Jetpack, and of course why?

@Zhuinden
Copy link
Owner

Zhuinden commented Aug 12, 2023

Hello, Simple-Stack continues to be maintained, and we still use it on old and new projects because there is less "magic tooling" involved (read: there's no reflection, no code gen, no annotation processing, no gradle plugins, it's literally just code), other than state restoration the whole thing can be used as-is in unit tests, and it still allows easier control over your navigation history, and it still allows for "less friction" to share data between screens.

The one potential downside is that Google stopped creating frameworks with the intention of them being "Open for extension by third parties". So if you look at LifecycleOwner/SavedStateRegistryOwner, you can technically implement those.

However, the new Fragment behaviors for "in-app predictive back" is designed in such a way that it would only work with Fragments that are being used exactly as Jetpack Navigation does it.

If you're for some reason forced to use Hilt, then Hilt's scoping is much more rigid, and doesn't account for the ability to create scope inheritance. That, and Hilt's NavBackStackEntry-based integration only works with NavBackStackEntry, which is their own internal class. You can't just provide "something similar" unless you actually rewrite all relevant code.


Anyway, as far as I know, the people who use / have used Simple-Stack are satisfied with it. There's a much lower learning curve, and the features are more powerful. Even the Compose integration was more powerful than Navigation-Compose for years despite being what I think of as a proof-of-concept.

However, there is less reliance but also less out-of-the-box integration with certain Jetpack things. This is a double-edged sword; technically having no dependencies on Jetpack makes this library less fragile, but it also intimidates people because, well, there's just so much less stuff to work with that they think it "knows less". You have to be in a higher position to say, "yes, we will ditch the official ecosystem (that keeps getting either deprecated or rewritten every 3 years anyway) and instead rely on this guy's code that he uses in single-activity projects to fix issues and simplify development).

For whatever reason, even though the Google teams are also around 2-3 people on a given package (except Compose), seeing "google" there instead of "zhuinden" somehow makes people trust it more (even though that just means it's more likely to get dropped, as Google keeps dropping support for their things once they get tired of it).


Personally, I would say, check the feature set and see if it suits your needs. There are teams that use simple-stack as implementation detail (https://github.com/inovait/kotlinova) and they are also satisfied with it afaik.

@Zhuinden
Copy link
Owner

Zhuinden commented Aug 12, 2023

TL;DR is that we still use Simple-Stack and it is still simpler to manage state with it, than when in Jetpack Navigation you're defining actions, routes, all are always available but most of them are invalid states.


Hypothetically it would be possible to restrict Simple-Stack's feature set by making it an implementation detail of a Jetpack Navigation navigator, however I don't see why you'd want to just make things harder for yourself.

The one thing that Navigation does have over Simple-Stack is out of the box "<include-dynamic" support for dynamic feature modules, But that stuff is also technically "just code", and their implementation is open-source.

@LeafyLappa
Copy link
Author

Thank you, very detailed and kotlinova seems very interesting

@Zhuinden Zhuinden added the done label Aug 13, 2023
@Zhuinden
Copy link
Owner

Yes, kotlinova does look interesting, and the 2.8.0 feature update was added pretty much because kotlinova requested it, although I personally don't use kotlinova.

We use a setup very similar to what you see in the FTUE samples.

@LeafyLappa
Copy link
Author

I've spent some time going through the samples (very clean code once you start to understand albeit very different from what I'm used to) and I have a few more questions now, mostly regarding migrating from Jetpack in an existing project.

  1. How easy should it be to migrate a Jetpack application to Simple Stack?
  2. Would you recommend using customs views instead of fragments? (I don't entirely understand this. I've always used fragments and activities for UI)
  3. MVVM sample uses an explicit service locator, and all services seem to be tied to the application class, while view models seem to be tied to fragment lifecycles. Would it be possible to use Hilt or somehow simply the boilerplate codes?
  4. My application has multiple fragment activities. Somehow I found it easier to split the application into parts where each part is a separate activity. For example login / registration flow is contained in its own activity. How can I use Simple Stack in this case?

@Zhuinden
Copy link
Owner

Zhuinden commented Aug 13, 2023

How easy should it be to migrate a Jetpack application to Simple Stack?

That's a question of how much you rely on Jetpack Navigation's feature set. For example, their bottom navigation works differently than how we do it (nested fragments), their deep-linking API is different, and they have dialog destinations which are not part of the StateChanger implementation by default.

The Navigation dialog destination support used to have edge-cases that were tricky to resolve on their side, now their code seems to have improved in that regard (because they made Navigation be async / state-based in order to support Compose transitions).

Honestly, Simple-Stack does "less things". This makes it more robust, but it also means sometimes you have to do something they have already written, and vice versa. For example, it's easier to just say backstack.setHistory() with the keys than it is to make that string in Jetpack Navigation and hope for the best.

I don't tend to migrate tooling that "works". I have migrated custom navigators to be replaced with Simple-Stack however to fix bugs.

Having multiple Activities within the accessible app flow is the most intrusive architectural decision that is very hard to undo the effects of.

Would you recommend using customs views instead of fragments? (I don't entirely understand this. I've always used fragments and activities for UI)

I've been using Fragments in projects lately. Custom views are still nicer for when you need to support complex transitions.

MVVM sample uses an explicit service locator, and all services seem to be tied to the application class, while view models seem to be tied to fragment lifecycles. Would it be possible to use Hilt or somehow simply the boilerplate codes?

You're also tied to an "explicit service locator" by using @AndroidEntryPoint. Only Hilt would ever support that annotation. The generated code actually does a look-up to Application, and grabs the FragmentComponent/ActivityComponent's factory from there.

There is fundamentally no difference between how the two work. In fact, having to use lateinit var for your @Inject fields is actually more error-prone.

You could theoretically create something like a custom component like ViewModelComponent for scoped services, or set up the scoped services to hold onto a ViewModelStore/ViewModelStoreOwner and make Hilt work with that. Another option would be to use Hilt to create various scoped services then also map them over to ServiceBinder via set multibinding, or manually retrieving them from the EntryPoints.get().

Personally I find all of these options to be much more work than it's worth. The boilerplate would be to interop with Hilt's extremely rigid (and limited) APIs, especially knowing that it wouldn't be able to support scope inheritance, it's not part of their design.

This is why Anvil exists in the first place. I don't use Anvil, but it would be easier to support Anvil as its scoping is not rigid (see kotlinova which uses it).

TL;DR you can use Hilt but it is kinda difficult, and it is because of Hilt's limitations. Also, you don't pass ServiceBinder to scoped services, so that's not actually "explicit service location" it's manual IoC. The constructor gets the arguments it needs.

This is something I've run into criticisms of, but I find it's because people just want to see code generation here whether they need it or not. I have no intention to include code generation (#223).

My application has multiple fragment activities. Somehow I found it easier to split the application into parts where each part is a separate activity. For example login / registration flow is contained in its own activity. How can I use Simple Stack in this case?

Simple-Stack works within an Activity. Personally I believe that creating "loginActivity/registrationActivity" breaks deep-linking potential and control over your task stack, but you can still somewhat work around it as long as you only have 1 Activity on your task stack at a given time.

Honestly, sure, AuthenticationActivity/LoggedInActivity could work, but Login/Register? At that point, you don't need a single-Activity navigation lib, neither Jetpack Navigation nor this one. Even in Jetpack Nav, you're breaking the internal state representation by having more than 1 NavHosts, they expect you to use activity destinations which honestly nobody does (citation needed)

But I have used simple-stack to manage fragments in a multi-activity app within various activities, the option is there. The only thing you might need to guard against is that globalServices would try to restore themselves multiple times if they implement Bundleable. In a multi-activity app, you are probably not using that anyway.

@Zhuinden
Copy link
Owner

If one were to want to use code-gen-based DI by any means, then Anvil is much less restrictive/restricted than Hilt.

@LeafyLappa
Copy link
Author

I only use Hilt because I'm a simple man and almost always all I need to do is write @HiltViewModel and @Inject constructor, very rarely inject into lateinit fields. I use Koin in some of my projects but somehow it's quite a bit slower internally and requires boilerplate code. ServiceBinder reminded me of Koin.

I also had to work with Kodein once and it seemed worse than Dagger, and I've never tried Anvil.

Regarding multiple activities in the project, I think it ended up this way after I had issues with Jetpack navigation and it turned out easier / cheaper to implement different flows as different activities, ha. The project is not even MVP yet and we are redesigning most screens so I want to try something else to speed up further development.

@Zhuinden
Copy link
Owner

Zhuinden commented Aug 13, 2023

Well technically you can still use Hilt+ViewModel but you will lose the scope inheritance and "advanced" lifecycle management that ScopedServices give you.

And the the cost of creation of a ScopedService compared to the @Inject variant is to "invoke the constructor once", which is actually insignificant compared to the rest of Dagger/Hilt boilerplate, I find.

It is theoretically possible to create subcomponents and whatnot per each scope node and then get those scoped parameters into the ServiceBinder but it's so much more work than just using it like how it's done in the samples that I've removed Dagger from all samples since. It's somewhere in the commit history, not really worth finding it. I used to put the services and the Dagger component in the ServiceBinder as a service for "lookup" below, wasn't really worth it in the end.

@LeafyLappa
Copy link
Author

Hmm, to be honest I simply don't understand. I don't really use any advanced Dagger features in my projects, haven't been for years with Hilt. I'm a simple man 😁 Not having to manually add services to GlobalServices and then also write lookups is so convenient, but then again it's also a matter of a few lines in pretty much any app.

@Zhuinden
Copy link
Owner

Zhuinden commented Aug 14, 2023

It seems convenient and then you get hit by the quirky build errors in kapt, to replace 2 lines of code with a @Inject, and now you can't use scope inheritance 😅

https://youtu.be/5ACcin1Z2HQ?si=xvqbBSCHEVtR7KD2&t=1449

Anyway, ScopedServices have always been an optional aspect of Simple-Stack. I believe them to greatly simplify things because of onServiceRegistered/onServiceUnregistered˛and onServiceActive/onServiceInactive, toBundle/fromBundle, technically also back support although that is a bit trickier now with the ahead-of-time model, and primarily that you can look-up implicit parents registered as previous screens. While this is a feature where you want to be vigilant, as you set up a dependency on the screen history (you can undo that by using shared explicit parent scope though), it's also why I can just set up a filter screen for a list screen and directly talk to the previous screen's viewmodel rather than, I dunno, FragmentResultListener?

If you wanted to use Hilt with GlobalServices, you'd need to get a reference to each app-scoped instance and .add() them via EntryPoints accessors.

But the navigation APIs alone are already powerful and versatile enough that it's easier to use than NavGraphs, NavGraphs are very limiting because once you have a subgraph, you can only access its start destination with 1 action.

@LeafyLappa
Copy link
Author

Very informative. I have perhaps one last question, is there a tutorial on how to handle dialogs? I can find discussions here and I think there was something in the samples, but not too specific. And dialogs are such a nuisance.

@Zhuinden
Copy link
Owner

Zhuinden commented Aug 15, 2023

I've been using either AlertDialog or DialogFragment depending on whether something should survive config changes or not; there's nothing specific to dialogs in Simple-Stack out of the box. I don't tend to put it on the backstack because then you start needing to handle multiple types, which I know jetpack navigation does internally, but dialog destinations really are a trick in regards that their parent also needs to be created and shown anyway - it is a special hierarchical state if extracted.

My favorite way to do Dialogs was DialogFragment + setTargetFragment, was not happy about that deprecation.

But a fun thing about DialogFragments is that they have Context, so they can access the Backstack, so they can access implicit/explicit scoped services using by lazy { lookup<T>() }. So it's possible to talk to the caller's "ViewModel" (scoped service) that way. I do that sometimes.

@LeafyLappa
Copy link
Author

LeafyLappa commented Aug 17, 2023

So it's possible to talk to the caller's "ViewModel" (scoped service) that way. I do that sometimes.

Nice, makes things easier. I'll make use of that.

Thank you for all your replies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants