Okuki


Source link: https://github.com/wongcain/okuki

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.

Resources

Scoop is a micro framework for building view based modular Android applications.

Android view centring RecyclerView.

Rx based generic RecyclerView adapter library.

Screen adaptation library for Android.

An implementation of a floating search box with search suggestions.

Let

Annotation based simple API flavoured with AOP to handle new Android runtime permission model.

Topics


2D Engines   3D Engines   9-Patch   Action Bars   Activities   ADB   Advertisements   Analytics   Animations   ANR   AOP   API   APK   APT   Architecture   Audio   Autocomplete   Background Processing   Backward Compatibility   Badges   Bar Codes   Benchmarking   Bitmaps   Bluetooth   Blur Effects   Bread Crumbs   BRMS   Browser Extensions   Build Systems   Bundles   Buttons   Caching   Camera   Canvas   Cards   Carousels   Changelog   Checkboxes   Cloud Storages   Color Analysis   Color Pickers   Colors   Comet/Push   Compass Sensors   Conferences   Content Providers   Continuous Integration   Crash Reports   Credit Cards   Credits   CSV   Curl/Flip   Data Binding   Data Generators   Data Structures   Database   Database Browsers   Date &   Debugging   Decompilers   Deep Links   Dependency Injections   Design   Design Patterns   Dex   Dialogs   Distributed Computing   Distribution Platforms   Download Managers   Drawables   Emoji   Emulators   EPUB   Equalizers &   Event Buses   Exception Handling   Face Recognition   Feedback &   File System   File/Directory   Fingerprint   Floating Action   Fonts   Forms   Fragments   FRP   FSM   Functional Programming   Gamepads   Games   Geocaching   Gestures   GIF   Glow Pad   Gradle Plugins   Graphics   Grid Views   Highlighting   HTML   HTTP Mocking   Icons   IDE   IDE Plugins   Image Croppers   Image Loaders   Image Pickers   Image Processing   Image Views   Instrumentation   Intents   Job Schedulers   JSON   Keyboard   Kotlin   Layouts   Library Demos   List View   List Views   Localization   Location   Lock Patterns   Logcat   Logging   Mails   Maps   Markdown   Mathematics   Maven Plugins   MBaaS   Media   Menus   Messaging   MIME   Mobile Web   Native Image   Navigation   NDK   Networking   NFC   NoSQL   Number Pickers   OAuth   Object Mocking   OCR Engines   OpenGL   ORM   Other Pickers   Parallax List   Parcelables   Particle Systems   Password Inputs   PDF   Permissions   Physics Engines   Platforms   Plugin Frameworks   Preferences   Progress Indicators   ProGuard   Properties   Protocol Buffer   Pull To   Purchases   Push/Pull   QR Codes   Quick Return   Radio Buttons   Range Bars   Ratings   Recycler Views   Resources   REST   Ripple Effects   RSS   Screenshots   Scripting   Scroll Views   SDK   Search Inputs   Security   Sensors   Services   Showcase Views   Signatures   Sliding Panels   Snackbars   SOAP   Social Networks   Spannable   Spinners   Splash Screens   SSH   Static Analysis   Status Bars   Styling   SVG   System   Tags   Task Managers   TDD &   Template Engines   Testing   Testing Tools   Text Formatting   Text Views   Text Watchers   Text-to   Toasts   Toolkits For   Tools   Tooltips   Trainings   TV   Twitter   Updaters   USB   User Stories   Utils   Validation   Video   View Adapters   View Pagers   Views   Watch Face   Wearable Data   Wearables   Weather   Web Tools   Web Views   WebRTC   WebSockets   Wheel Widgets   Wi-Fi   Widgets   Windows   Wizards   XML   XMPP   YAML   ZIP Codes