/okuki

Okuki is a simple, hierarchical navigation bus and back stack for Android, with optional Rx bindings, and Toothpick DI integration.

Primary LanguageJavaApache License 2.0Apache-2.0

Okuki

A simple, hierarchical navigation bus and back stack for Android, with optional Rx bindings, and Toothpick integration for automatic dependency-scope management.

Examples

2 sample projects are provided below:

  • Simple Example: Demonstrates basic usage in a simple Android project, without use of the optional Rx and Toothpick integrations.
  • MVP Example: Demonstrates Okuki's full capabilities using RxJava 1.x, Toothpick, and Parcelable-State save/restore. It also implements an Okuki-centric approach to the MVP (Model-View-Presenter) design pattern.
  • MVVM Example: Another full-featured demonstration, this time using RxJava 2.x and MVVM pattern with Android Data Binding.

Setup

Gradle:

repositories {
    jcenter()
}

...

dependencies {
    compile 'com.cainwong.okuki:okuki:0.3.1'
     
    // For RxJava 1.x Bindings
    compile 'io.reactivex:rxjava:1.2.5'
    compile 'com.cainwong.okuki:okuki-rx:0.3.1'
    
    // -OR- for RxJava 2.x Bindings
    compile 'io.reactivex.rxjava2:rxjava:2.0.2'
    compile 'com.cainwong.okuki:okuki-rx2:0.3.1'
 
    // For Android Parcelable State Save/Restore
    compile 'com.cainwong.okuki:okuki-android:0.3.1'
 
    // For Toothpick integration
    compile 'com.github.stephanenicolas.toothpick:toothpick-runtime:1.0.5'
    annotationProcessor 'com.github.stephanenicolas.toothpick:toothpick-compiler:1.0.5'
    compile 'com.cainwong.okuki:okuki-toothpick:0.3.0'
}

What is Okuki?

Okuki's purpose is to communicate and remember hierarchical application UI state changes in a consistent, abstracted way across an application. This is done by the creation of Place classes that represent unique UI states or destinations. Think of a Place as a URL or route that maps to a UI state. Square's Flow library is based on a similar concept. However, where Flow defines a specific mechanism for implementing UI changes (as well Resource management and lifecycle), Okuki makes no requirements on how UI state changes are implemented. For example Okuki can support single- or multi-Activity architectures, with or without using Fragments and/or custom views.

Places

Each UI state is presented by an instance of an Object extended from the class Place<T>. Each Place defines a type of payload T that will be carried by the instance. Okuki places no restriction on the Type of of payload that a Place may carry. However, if you wish to make use of OkukiParceler in the okuki-android extension (see Saving and Restoring State below), you will need to limit your payloads to Parcelable and/or Serializable Types. If you require no payload, you can use Type Void or extend SimplePlace which Okuki includes for convenience.

Place Hierachy

All Places are hierarchical. By default, each Place sits at the root-level of the hierarchy. However, Places can be nested in a hierarchy by annotating them with @PlaceConfig and setting the parent parameter.

@PlaceConfig(parent = ContactsPlace.class)
public class ContactDetailsPlace extends Place<Integer> {

    public ContactDetailsPlace(Integer data) {
        super(data);
    }

}

In the example above, ContactDetailsPlace is an immediate descendant of ContactsPlace. If another class is created that identifies ConactDetailsPlace as its parent, both this new class and ContactDetailsPlace would be descendants of ContactsPlace.

Listening for Place Requests

Okuki provies 3 types of Listeners. Each Listener registers to receive Place instances as they are requested. The difference between the listeners is what type of requested Places they receive.

PlaceListener

PlaceListeners receive only Places only of a specified type. The type is specified via the generic attribute P in PlaceListener<P extends Place>.

The following listener would receive only instances of ContactDetailsPlace:

PlaceListener<ContactDetailsPlace> contactDetailsPlaceListener = new PlaceListener<ContactDetailsPlace>{
    @Override
    public void onPlace(ContactDetailsPlace place) {
        int id = place.getData();
        // GET CONTACT DATA UPDATE UI, ETC.
    }
};
okuki.addPlaceListener(contactDetailsPlaceListener);

BranchListener

BranchListeners also receive Places of a specified type, but also receive any Place that is a descendant of the specified type. So given the classes defined in the Place Hierarchy section above, the following code would listen for Place requests for ContactsPlace and ContactDetailsPlace, as well as any other descendents of these classes:

BranchListener contactsBranchListener = new BranchListener<ContactsPlace>() {
   @Override
   public void onPlace(Place place) {
       // NAVIGATE TO "CONTACTS" SECTION OF APPLICATION, ETC.
   }
};
okuki.addBranchListener(contactsBranchListener);

GlobalListener

A registered GlobalListener will receive all Places that are requested, regardless of type or hierarchy. One simple use-case for such a listener would be a logging mechanism that reports all requested Places for the purpose of debugging.

GlobalListener logListener = new GlobalListener(){
   @Override
   public void onPlace(Place place) {
       log("Place requested: " + place);
   }
};
okuki.addGlobalListener(logListener);

Removing Listeners

Okuki keeps strong references to registered Listeners, so be sure the unregister them as appropriate for your application lifecyle.

okuki.removePlaceListener(contactDetailsPlaceListener);
...
okuki.removeBranchListener(contactsBranchListener);
...
okuki.removeGlobalListener(logListener);

Error Handling

In addition to onPlace(Place) each of the Listeners also implements an onError(Exception e) method. The default behavior of this method is to throw a Runtime exception. It is recommended that you override this behavior in your Listener implementations. If using RxOkuki, exceptions are delegated through the standard Rx error propagation.

Sticky Behavior

A very important aspect of Okuki's behavior is that the most recently requested Place is automatically provided to a Listener immediately when the listener is added (subject to the scope of the listener as described by the various listener definitions above). So in the previous example, logListener would automatically receive the most recently requested Place on okuki.addGlobalListener(logListener). contactsBranchListener would also receive the most recent place at okuki.addBranchListener(contactsBranchListener), but only if the most recent place was of type ContactsPlace or one of its descendants.

Requesting Places

Issuing a place request is as simple as calling okuki.gotoPlace(Place place). Okuki takes each received Place and broadcasts it to all registered listeners capable of receiving Places of the given type. Additionally, a method is provided for specifying a HistoryAction to perform when issuing the request: okuki.gotoPlace(Place place, HistoryAction historyAction). More about HistoryAction and the history back stack follows.

Place History Back Stack

In addition for providing a mechanism for requesting an receiving places, Okuki provides a history back stack of the places requested. This back stack may be used to easily navigate to back to previously requested Places. To go back to the previous Place, simply call okuki.goBack(). When doing so, the most recent Place is popped off the back stack and broadcast to configured listeners. Okuki also provides the method okuki.getHistory() that provides direct access to the history. This allows you to rewrite any portion of the back stack as needed without triggering any Place requests.

History Actions

By default each Place is added to the top of the history back stack. But Okuki supports several options for how a Place request affects the history. Use the following HistoryAction values as follows via the method okuki.gotoPlace(Place place, HistoryAction historyAction):

  • ADD: The default behavior. Adds the requested Place to the top of the history back stack.
  • REPLACE_TOP: This will remove the most recent Place from the top of the history back stack, and then place the provided Place at the top of the stack. If the stack is already empty, behaves the same as ADD.
  • TRY_BACK_TO_SAME: Searches backwards in the stack to find an equivalent Place (Object.equals(Object o)). If found, pops the stack back to the found Place (including popping the found Place), and then adds the requested Place to the back stack. If not found, behaves the same as ADD.
  • TRY_BACK_TO_SAME_TYPE: Searches backwards in the stack to find a Place of the same type (instance of the same Class). If found, pops the stack back to the found Place (including popping the found Place), and then adds the requested Place to the back stack. If not found, behaves the same as ADD.
  • NONE: Broadcasts the requested place to the Listeners without altering the history in any way, including adding the requested Place to the back stack.

Thread Safety

Okuki is single-threaded (i.e. NOT thread-safe). The reason for this is that it is designed for communicating UI changes and is intended to be run on a single thread (the Android Main/UI Thread).

Rx Bindings

RxBindings are provided in the optional package okuki.rx for RxJava 1.x, and okuki.rx2 for RxJava 2.x. Using these bindings simplifies use of Okuki as you no longer need to manage instances of the various Listener classes. Simply subscribe and unsubscribe like this:

Subscription globalSub = RxOkuki.onAnyPlace(okuki).subscribe(place -> logPlace(place));
Subscription branchSub = RxOkuki.onBranch(okuki, ContactsPlace.class).subscribe(place -> gotoContacts());
Subscription placeSub = RxOkuki.onPlace(okuki, ContactsPlace.class).subscribe(place -> dislayContact(place.getData()));
...
subscription.unsubscribe(globalSub);
subscription.unsubscribe(branchSub);
subscription.unsubscribe(placeSub);

Saving and Restoring State (Android Only)

The optional Okuki-Android package provides a mechanism for saving and restoring the state of an Okuki instance as a Parcelable. With it you can maintain Okuki's state (current Place and Place History Back Stack) across Android configuration changes and process death.

Usage

Do the following to enable Okuki State save/restore:

  • Add the dependency for Okuki-Android. (See Setup above.)
  • Ensure that all of your Place classes either have Void payload types (which includes SimplePlace), or payload types that implement Parcelable or Serializable.
  • Call OkukiParceler.extract(Okuki okuki) to get a Parcelable OkukiState object that can be written into a Bundle. (Note: the OkukiState may be null if no PlaceRequest has yet been made.)
  • Call OkukiParceler.apply(Okuki okuki, OkukiState okukiState) to restore the saved state to your Okuki instance.

Toothpick Dependency Injection Integration

Toothpick integration is provided via the optional package okuki.toothpick. This integration allows you to use Places and their hierarchy as your Toothpick Scope hierarchy, and to define modules to load at each level of the hierarchy. Once configured, a PlaceScoper listens for Place requests and automatically opens a Toothpick scope that reflects the respective Place hierarchy. Additionally, any scopes that is not part of the newly requested Place hierachy are automatically closed, freeing up resources not needed in the new Place hierarchy.

Usage

Do the following to enable the Toothpick integration:

  • Add the dependencies for both Toothpick and Okuki-Toothpick. (See Setup above.)
  • Create a PlaceScoper instance for your Okuki instance using its Builder, also specifying any Modules that you like to configure dependencies that should be available globally:
placeScoper = new PlaceScoper.Builder().okuki(okuki).modules(new AppModule(), new NetworkModule()).build();
  • Use the @ScopeConfig annotation on Place classes to define additional Modules that should apply only to the respective portion of the hierachy defined by the Place. (These modules will themselves automatically receive any injections available from their parent scope.):
@ScopeConfig(modules = KittensModule.class)
public class KittensPlace extends SimplePlace {
    public KittensPlace() {
    }
}
  • Use your instance of PlaceScoper to perform all of your injections across your application. (Do do so you'll need provide some kind of static accessor to your PlaceScoper instance.):
APP_INSTANCE.placeScoper.inject(obj);

To best understand how Okuki and Toothpick work together, see the MVP Example.

For more information about using the wonderful Toothpick library, please its Github repository and the thorough documentation on the wiki there.