Simplified Android Development Using Simple-Stack

Gabor Varadi
The Startup
Published in
7 min readNov 14, 2020

It’s been a long time since I’ve wanted to write this article. Time to go at it, shall we? :)

People always seem to be surprised and say, “wait, you DON’T use Jetpack ViewModel, Jetpack Navigation, and Dagger-Hilt? Then what DO you use?”

This article intends to answer that. We use Simple-Stack.

(If you prefer watching talks, then a talk on the same subject is available at https://www.youtube.com/watch?v=5ACcin1Z2HQ)

What is Simple-Stack?

Simple-Stack is a navigation framework that I’ve been working on since the beginning of 2017, although its roots go back to 2015 — when we were still experimenting with multiple versions of Mortar & Flow. (The 90s kids might still remember).

Following up on the original proposition by Square, “Advocating Against Android Fragments” and “Simpler Android Apps with Flow and Mortar”, we’ve been using Single-Activity approach in our apps since 2015.

Now, the history of Flow and Mortar isn’t all roses. I’ve heard people say “we tried it and it didn’t work for us”. I can attest to that: we had to manage our own fork, but even with that, we hit some of its limitations over time.

We needed something reliable and versioned, so we rewrote Flow’s original behavior, and intended to improve upon its limitations. This rewrite is what resulted in the creation of Simple-Stack.

Why Simple-Stack?

Simple-Stack is based on the idea that you intend to track your navigation state as a list of immutable and Parcelable classes — screen identifiers (which the library calls “keys”).

As long as the underlying navigation implementation allows for “navigation from any state to any state”, it is possible to wrap that implementation using Simple-Stack.

For example, something I hear often is that “navigation with Fragments is hard”.

I always point out that in our code, it’s been like this since 2016:

backstack.goTo(new SecondScreen(detailId)); // java

No explicit fragment transaction, no Bundle, no newInstance(), just calling a method. How is this possible? Isn’t Fragment navigation hard? Well, it can be, but Fragments are powerful, and they can be abstracted away.

Going back is very similar:

backstack.goBack(); // java

No finish(), no magic.

Setting up Simple-Stack with Fragments

Of course, over the years, the API of Simple-Stack has improved. This is something you’d expect from a library that started with 0.1.0 and is now 2.5.0.

Originally, Simple-Stack set out to make working with compound viewgroups as our core UI easy, but this article focuses on its usage with Fragments.

In a few simple steps, we can achieve simple, type-safe navigation between Fragments.

As it says in the readme, the initial setup is to add dependencies:

Dependencies

and add the integration point into the Activity:

Adding Simple-Stack to Activity

With that, we’ve installed Simple-Stack’s Navigator, which will make the backstack available to us, pretty much anywhere (and handles state restoration, lifecycle events, and back presses — along with fragment navigation in general).

If we define FirstKey, this will show our first Fragment on our screen, and it looks like this:

First screen identifier for FirstFragment

By default, DefaultFragmentKey will use javaClass.name as the fragment tag, but this can be customized.

To make the FirstKey accessible via getKey(), we can make our Fragment extend from KeyedFragment:

FirstFragment

Of course, we don’t need to extend KeyedFragment, as getKey() is the same as requireArguments().getParcelable(DefaultFragmentKey.ARGS_KEY), hiding it is for convenience.

How would adding a new screen work?

To add a new fragment, what we do is:

Adding SecondFragment and SecondKey

Create a key, create a fragment, and we can navigate to it via backstack.goTo(SecondKey()). Fairly formulaic.

Arguments can be passed to the key directly. As long as they are parcelable, @Parcelize will handle it for us.

Second Key with arguments

Navigating there is a simple method call.

Finding the current fragment, handling back in Fragments

There are times when we need to pass back events to a Fragment.

My recipe for that so far has been to define an interface, like so:

BackHandler

Implement BackHandler on the Fragment, if it intends to handle back presses:

Handling back in a Fragment

And we can now dispatch back to our current shown Fragment, even without the use of a BackPressDispatcher:

Handling Fragment Back in Activity

In this case, we could access the current fragment, ask it whether it intends to handle back, and if yes, then don’t pass it to the Activity — in a fairly transparent and easy-to-understand manner.

Showing a title for a given screen

You might find yourself trying to configure a global view, one that belongs to the Activity, as you navigate between your Fragments.

In this case, some people might choose to override Fragment.onStart() and tinker with the Activity right there and then.

However, should a Fragment really know about the Activity’s views? Wouldn’t it be great if we could handle this in the Activity directly?

Well, this is easy with Simple-Stack as well. We just need to put the title resource ID in the key, and handle it along with the navigation state change we’re already handling.

Sharing data between screens

Here’s where things get a little more interesting. While I’ve already mentioned this in two of my previous articles:

It’s worth iterating over it once more, as this is where the replacement for Jetpack ViewModel (and in simple cases, even of Dagger/Koin) comes in.

Scoped services, global services

Simple-Stack defines the concept of “scopes”.

The idea is that as you navigate forward and back, your navigation state can declare what scopes each screen expect to exist as you navigate there — and if it doesn’t exist yet, the Backstack will create it.

Each scope can hold “scoped services”, which have a lifecycle automatically managed based on the navigation state (and can also support saved state persistence and restoration).

For the simpler cases, we can use the DefaultServiceProvider, and implement DefaultServiceProvider.HasServices on our key. This will allow us to lookup() services from the Backstack that are registered in any of the previous scopes.

Any service registered in the GlobalServices will be available too, living in the parent scope of all scopes. When constructing scoped services bound to screens, we can use serviceBinder.lookup() to inherit services from previously bound scopes.

With that, we have a FirstScopedModel that inherits the someDao from global services, is able to navigate, gets arguments, and could even handle lifecycle or state restoration if it implemented Bundleable and ScopedServices.Registered respectively.

If we need to bind additional scopes that are shared between screens that are not directly a result of the screen history, we can use ScopeKey.Child. It’s similar in concept to NavGraph-scoped ViewModels.

Other recipes to consider

Clearing the task stack (jumping to first element)

If we want to set a completely new history (for example, for deep-linking), we can use:

backstack.setHistory(
History.of(SomeScreen(), OtherScreen()),
StateChange.FORWARD
)

If we just want to jump to the first element, we can use:

backstack.jumpToRoot()

Result passing

We can use the inheritance of scoped services, as the previous screen’s services are guaranteed to exist.

Therefore, if we implement an interface on a previous, expected service, we can grab that and pass results to it directly.

Unit testing

As the Backstack class does not rely on Android in any way, and keys don’t depend on Android in any way, it’s easy to test for interactions with the backstack and assert the result.

This is why the library itself is heavily tested via automated tests, too.

Scoping in a multi-module setup

In that case, we created an implementation of ScopedServices that would delegate over to Dagger’s provider map multibinding to create the scoped services for a given scope.

But that was mostly necessary because the keys were global, even though that’s most likely not the best way to structure navigation code across modules (as it becomes a global dependency of all modules, while the keys become separate from the feature they belong to).

After all, if you need to know about or make assumptions about the existence of other modules you don’t depend on, then you’re violating the isolation of modules (either via reflection or other heinous means), which isn’t a good approach.

Next time, I’d rather expose an interface from the feature module, and handle navigation in the app module. That way, no such tricks are needed.

Is it ready for Compose?

Technically yes, although I still can’t figure out composable animations. I’ll update this gist when I figure it out.

Using Simple-Stack with Compose

This actually gives us all of the above. Navigation between screens, scoping support (via backstack.lookup()), and it goes without saying — state restoration across process death.

Conclusion

I hope this article helped clarify some mysteries regarding how we currently use Simple-Stack.

The default settings for Fragments work well for simple animations (the default is horizontal slide, although it’s customizable) — and animations generally aren’t that flashy in business apps. Shared element transitions would, unfortunately, need a bit more tricks (due to FragmentTransaction’s design), although there’s a sample for it in the repo.

When we needed more complex animations, we actually opted to use compound viewgroups instead, as they’re more versatile in that regard.

Still, the end result is that we can access the backstack throughout the project (activity, fragments, views, scoped services) as needed, and its lifecycle is predictable (it lives in the Activity retained scope, and handles process death).

Using Simple-Stack allows us to abstract away the Android lifecycle to such degree, that navigation actions become a simple method call. Result passing becomes a method call. Parameter passing between screens becomes a constructor parameter of a class. Type-safe and customizable navigation, with lifecycle-aware state management.

You can see the gist of how to use Simple-Stack in this sample project, although it’ll be eerily similar to this article.

Hope that helped explain how it works, why we’re using it, and maybe even how it compares against the current alternate approaches. If you have any further questions, just comment below :)

You can also check out the discussion threads, here for /r/androiddev, and here for /r/android_devs.

--

--

Gabor Varadi
The Startup

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.