NOTE FROM THE FUTURE (2020–02–20): Service-Tree was deprecated a very long time ago. Scoping has become a first-party citizen in Simple-Stack 1.11.0 (2018–07–01) using ScopeKey , the latest version at time of this update is Simple-Stack 2.2.5.

Everything in this article is potentially outdated. For up-to-date and sane practices, refer to Step 7 and Step 8 in https://github.com/Zhuinden/simple-stack-tutorials/ .

If you’ve ever tried to work yourself through articles about Flow and Mortar (and in case you haven’t), you’ve probably seen that they try to solve two very specific problems:

Flow 0.8 handles navigation: determining where you are in the application, and therefore providing “history” for back button presses

handles navigation: determining where you are in the application, and therefore providing “history” for back button presses Mortar works as a hierarchical scoped service locator that allows finding a service identified by a String tag, inheriting from the parent, and makes them accessible to custom views via context.getSystemService()

And also,

Mortar provided a so-called BundleServiceRunner through the scoped service ViewPresenter that intended to allow persisting custom view state into the onSaveInstanceState(Bundle) bundle

— — — — — — — — — — — — — — — — — — — — — — — — — —

Considering there are barely any articles about it, you are more likely to not know, but:

Flow 1.0-alpha merges the two concepts into one: based on your current key (app state), it “automatically” creates the services that are bound to be created for that given key; and “torn down” when it’s out of scope.

— — — — — — — — — — — — — — — — — — — — — — — — — —

In order to truly understand why these problems needed to be solved, we must first look at a rather simple yet powerful “hack” that Square came up with:

Context.getSystemService() as a service locator

It’s not immediately obvious, but what context.getSystemService() does is that it attempts to find a service locally, and if it cannot find it, then it tries in the next context wrapper, then the next context wrapper, and so on.

Meaning, it is a hierarchical service locator. Leveraging this, it’s possible to share ANY service to ANYTHING that uses a given Activity’s (or Application’s) context, without explicitly giving it to them — that includes fragments and entire view hierarchies!

@Override

public Object getSystemService(String name) {

if(BooksPresenter.TAG.equals(name)) {

return booksPresenter;

}

return super.getSystemService(name);

} public static BooksPresenter get(Context context) {

//noinspection ResourceType

return (BooksPresenter) context.getSystemService(TAG);

} BooksPresenter booksPresenter = BooksPresenter.get(getContext());

As a result, Activity can be used as an implicit (sub)scope. People do it all the time, they create @ActivityScope components with Dagger — and possibly even share it using getSystemService() .

However, you cannot redefine getSystemService() inside a fragment or a view… so what can you do?

Providing parameters to views using custom ContextWrapper

In order to make something accessible to the whole view hierarchy, it’s apparently possible to create custom ContextWrappers. These wrappers do allow redefining getSystemService() , and look something like this:

public class KeyContextWrapper

extends ContextWrapper {

public static final String TAG = "Backstack.KEY";



private LayoutInflater layoutInflater;



private final Object key;



public KeyContextWrapper(Context base, @NonNull Object key) {

super(base);

if(key == null) {

throw new IllegalArgumentException("Key cannot be null!");

}

this.key = key;

}



@Override

public Object getSystemService(String name) {

if(Context.LAYOUT_INFLATER_SERVICE.equals(name)) {

if(layoutInflater == null) {

layoutInflater = LayoutInflater.from(getBaseContext())

.cloneInContext(this);

}

return layoutInflater;

} else if(TAG.equals(name)) {

return key;

}

return super.getSystemService(name);

}



public static <T> T getKey(Context context) {

// noinspection ResourceType

Object key = context.getSystemService(TAG);

// noinspection unchecked

return (T) key;

}

}

And then we can do:

View newView = LayoutInflater.from(

new KeyContextWrapper(getContext(), newKey)

)

.inflate(newKey.layout(), this, false); Key newKey = KeyContextWrapper.getKey(newView.getContext());

Neat! Based on this, we create the services and make them accessible through this custom context.

If you check, you can see that this is exactly how MortarScope s were given to custom viewgroups!

But we don’t really want Mortar (nobody likes Mortar), we just want services to be created when we’re at a given key, readily available.

Flow.Services to the rescue! …well, kind of?

If it weren’t for it being an unusable, limited, buggy mess! You think I’m being harsh, I’m not. I tried fixing it, using it, punching it to achieve what it was designed for, but it comes with limitations, severe ones.

Flow.Services provides two types of keys: MultiKey and TreeKey . This is essentially what determines that a key is actually a composite key; and that the key is the child of another key; respectively. However, MultiKeys are broken.

and . This is essentially what determines that a key is actually a composite key; and that the key is the child of another key; respectively. However, MultiKeys are broken. Flow’s provided key changer that manages services “sets up” the services each time even on configuration change, therefore after a single config change, services will never be torn down again.

The tree structure in Flow.Services works by “extending the parent”, and therefore obtaining a “child services” that points to its parent, and delegates getService() calls to it if a service is not found. However, the parents do not keep track of their children, and as a result, the tree cannot be traversed.

So while it’s great and all that child nodes are created and destroyed, but how are you going to persist the state of your services? Surely you don’t want to just let them die along with their state across process death?

Well with Flow.Services, you can’t do that. You cannot traverse the tree, you cannot manage the destruction of subtrees manually, and you cannot automate state persistence and state restoration.

— — — — — — — — — — — — — — — — — — — — — — — —

What we need is services that not only inherit from their parent, but also survive configuration change, are persistable/restorable across process death, and are accessible for their corresponding given key (and their children).

Solution: ‘Service-Tree’

Service-Tree is actually a very simple library (it’s just a tree in which parents actually know their children, after all) that I wrote because there were no other libraries of its kind, beyond of course Mortar and Flow.Services which brought their baggage along with them.

All it really does is that you can add and remove nodes for a given key; and these nodes store a Map<String, Object> into which services can be bound.

A node can be either a root (its only parent is the tree root), or a child (its parent is the specified node). Children inherit the services of their parent.

Of course, the ServiceTree can be treated as a singleton, and traversed to persist the states of all services found within its nodes.

Conclusion

People often say you shouldn’t reinvent the wheel. I think that applies only if that wheel actually works. :)

In this case, there was no “navigation + service locator” combo out there beyond Flow and Mortar (that I know of and could find).

The concept on its own is great; but in an Android context, ignoring process death and the necessity of persisting and restoring state is something that makes or breaks application stability.

As such, if you’re interested, check out service-tree and simple-stack. Maybe we’ll finally simplify Android development, and ditch fragments/activities after all!