https://github.com/wongcain/okuki

Places and Navigation

Okuki provides a way to define and navigate your application’s view hierarchy in an abstract way, decoupled from your various Activity/Fragment/View implementations.

With Okuki, you can organize your application around a hierarchy of named Place classes. Each Place may be annotated to define its parent, establishing the hierarchy. Additionally each Place defines a type argument for a data payload it may carry when being requested:

@PlaceConfig(parent = KittensPlace.class)

public class KittensDetailPlace extends Place<Integer> {



public KittensDetailPlace(Integer data) {

super(data);

}



}

In the above example, KittensDetailPlace is a descendant of KittensPlace , and carries an Integer payload. Now any component may request navigation to this place (with a payload of an Integer) via:

Okuki.getDefault().gotoPlace(new KittensDetailPlace(123));

In this way any component can make a navigation request to any other part of the application by broadcasting an instance of the respective Place without being concerned about sending Intents, using the FragmentManger , inflating custom views, etc.

Those details are only the concern of the receiving component that is responsible for displaying the respective UI. (In this case, some kind of “details” related to young cats.) The implementation may be that of loading an Activity, Fragment, or View, or just changing the value of text in a TextView. Okuki makes no requirements of how the request is fulfilled.

To respond to the requested place, the component responsible for displaying the UI needs only to listen for a request like this:

PlaceListener<KittensDetailPlace> placeListener = new PlaceListener<KittensDetailPlace>() {

@Override

public void onPlace(KittensDetailPlace place) {

int id = place.getData();

showDetails(id); // method that performs actual UI changes

}

};

Okuki.getDefault().addPlaceListener(placeListener);

Or using Rx and lambdas, like so:

RxOkuki.onPlace(Okuki.getDefault(), KittensDetailPlace.class)

.map(place -> place.getData())

.subscribe(this::showDetails);

Now consider that the UI for KittensDetailPlace is nested inside the UI for KittensPlace .

For example let’s say we have a MainActivity that loads Fragments. We also have KittensFragment that implements the UI for KittensPlace . And inside the content view for KittensFragment , there is a ViewGroup into which we load a custom view called KittenDetailView .

Before we can load KittenDetailView we need to make sure KittensFragment is loaded. This is where hierarchy of Places starts becoming useful. In addition to listening for a specific Place, you can also use a BranchListener to listen for a specific Place and all descendants of the specified Place. So in MainActivity we can listen for KittensPlace requests and KittenDetailPlace requests like this:

BranchListener<KittensPlace> branchListener = new BranchListener<KittensPlace>() {

@Override

public void onPlace(Place place) {

loadKittensFragment(); // use FragmentManager

}

};

Okuki.getDefault().addBranchListener(branchListener);

The BranchListener defined above will get called when we get requests for KittensPlace , KittensDetailPlace , and any other descendants of KittensPlace are requested.

We can also use Rx to do the same like this:

RxOkuki.onBranch(okuki, KittensPlace.class)

.subscribe(place -> loadKittensFragment());

Next we need KittensFragment to load KittensDetailView . To do this we use a PlaceListener as shown previously, but this time inside KittensFragment . (I’ll skip right to the Rx version this time):

RxOkuki.onPlace(Okuki.getDefault(), KittenDetailPlace.class)

.subscribe(place -> loadKittensDetailView());

Finally, when the KittenDetailView is loaded it also registers a listener for KittensDetailPlace as already shown.

But wait a minute… KittensFragment and KittensDetailView may not yet be loaded or have their listeners registered at the time KittensPlace is requested? How will they receive the request?

Good question, imaginary other person speaking in bold-italics. The reason that this works is because Okuki automatically fires a Listener’s onPlace event of the last Place requested immediately upon subscription (if the Listener is configured for a Place/Branch inclusive of the last requested Place). This behaviour is similar to the “StickyEvents” concept of GreenRobot’s EventBus, and that of Rx’s BehaviorSubject .

So, KittensFragment and KittensDetailView will each receive the requested KittensDetailPlace , complete with its payload, in succession when they initialize and register their listeners. And so when requesting a Place, the caller need not be concerned with the current state of the view hierarchy, despite how many levels of UI must be loaded to fulfill the request.

Bonus: Dependency Injection and Scoping

(Edit, March 19, 2017: The functionality and API for the Okuki-Toothpick integration has been changed (for the better) in version 0.2.0. Thus, the usage described below is deprecated. Please see the README.md in the Github repository for the latest usage information.)

If you are not yet familiar with the fantastic new DI library called Toothpick, please check it out on its Github. Toothpick’s primary advantage over other DI libraries such as Dagger is its ease of use for dependency scope management. Injections are performed always performed within a specified Scope . Also, scopes are hierarchical, so one scope inherits dependencies from a parent scope and so on. Sounds familiar?

Okuki provides a simple integration with Toothpick which allows you to use your Place hierarchy as your Toothpick scope definition. This makes it very easy to ensure that dependencies are available and shared with the parts of the application where they are needed, and released when they are no longer used. The way this works is that instead of opening scopes directly from Toothpick, you use Okuki’s PlaceScoper , passing in a Place class. PlaceScoper then opens a Scope hierarchy matching the hierarchy of the Place. So, you can do the following:

Scope scope = PlaceScoper.openPlaceScope(KittensDetailPlace.class, new KittensDetailModule());

Toothpick.inject(this, scope);

The above example would open a scope with with the following hierarchy:

Root -> KittensPlace -> KittensDetailPlace

Dependencies defined in Root and KittensPlace are inherited by KittensDetailPlace and available for injection, along with additional dependencies provided via KittensDetailModule . Then, when you dismiss the UI for displaying KittensDetailPlace you can call:

PlaceScoper.closePlaceScope(KittensDetailPlace.class);

Doing this closes the scope KittensDetailPlace and releases the KittensDetailModule dependencies, but the dependencies provided by the KittensPlace and Root Scopes remain available.

To see this all in action, please check out the MVP Example in Okuki’s repository. Or for a more basic example of using Okuki (without the Rx and Toothpick integrations) see the Simple Example.

Also, please see Okuki’s README file for more details on its features, including its History Backstack.