Skip to content

Commit

Permalink
Merge pull request #3 from wongcain/v0.2
Browse files Browse the repository at this point in the history
V0.2
  • Loading branch information
wongcain authored Mar 9, 2017
2 parents 17a5056 + f5f0a25 commit bb401ee
Show file tree
Hide file tree
Showing 49 changed files with 1,008 additions and 434 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Release version 0.1.2 (Feb 11, 2017)
* Bug fix for possible NPE in `Place.equals(obj)`
## Release version 0.2.0 (Mar 09, 2017)
* Toothpick integration refactoring
* Toothpick auto-scoping
* New Okuki-Android module
* Okuki-Android Parcelable Okuki state save/restore

## Release version 0.1.1 (Feb 03, 2017)
* Added generic typing to onPlace method to return Place without needing to cast.
Expand Down
177 changes: 85 additions & 92 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,64 @@
# Okuki
A simple, hierarchical navigation bus and back-stack for Android and Java, with optional Rx bindings,
and Toothpick integration for brainless dependency scope management.
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. Although Okuki is written in Java with no Android dependencies,
it was written with Android in mind. As such, both examples are Android projects:
* [Simple Example](https://github.com/wongcain/okuki/tree/master/okuki-sample): Demonstrates basic
usage in a simple Android project, without use of the optional Rx and Toothpick integrations.
* [MVP Example](https://github.com/wongcain/okuki/tree/master/okuki-toothpick-mvp-sample): Demonstrates
Okuki's full capabilities using both Rx and Toothpick integrations, and implements the MVP
(Model-View-Presenter) design pattern.
Okuki's full capabilities using Rx, Toothpick, and Parcelable-State save/restore. It also implements
an Okuki-centric approach to the MVP (Model-View-Presenter) design pattern.

## Setup
Gradle:
```
repositories {
jcenter()
maven {
url 'https://dl.bintray.com/wongcain/okuki'
}
}
...
dependencies {
compile 'com.cainwong.okuki:okuki:0.1.2'
compile 'com.cainwong.okuki:okuki:0.2.0'
// for RxBindings
compile 'io.reactivex:rxjava:1.2.5'
compile 'com.cainwong.okuki:okuki-rx:0.1.2'
compile 'com.cainwong.okuki:okuki-rx:0.2.0'
// for Android Parcelable State Save/Restore
compile 'com.cainwong.okuki:okuki-android:0.2.0'
// for Toothpick integration
compile 'com.github.stephanenicolas.toothpick:toothpick-runtime:1.0.5'
compile 'com.github.stephanenicolas.toothpick:toothpick-compiler:1.0.5'
compile 'com.cainwong.okuki:okuki-toothpick:0.1.2'
compile 'com.cainwong.okuki:okuki-toothpick:0.2.0'
}
```


## What is Okuki?
Okuki's purpose is to communicate and remember application UI navigation requests 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 the same general concept. However, where _Flow_ provides an
Android-specific mechanism for implementing UI changes (as well Resource management and lifecycle),
Okuki takes a simpler, less restrictive approach, functioning more like an _EventBus_ for
communicating UI state change requests without making requirements on how the requests are handled.
This means that choices such as whether or not to use multiple Activities or a single-Activity
architecture, or whether or not to use Fragments vs Custom Views is completely up to you. Okuki plays
nicely with all of these approaches.
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. Most likely, the payload
will of the type `String`or `Integer` representing the unique identifier of a model or record. But
Okuki makes no restriction on what may be sent as a payload. So a Place may carry a more complex
Object as a payload as well. Additionally, a Place is not required to carry a payload at all if it
is not needed. For convenience sake, Okuki provides `SimplePlace` which can be extended to create
no-payload Places.
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
Places are hierarchical. By default, each Place sits at the root-level of the hierarchy. However,
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.

Expand Down Expand Up @@ -144,12 +141,12 @@ okuki.removeGlobalListener(logListener);
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 propagation.
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 descibed by the various listener definitions above). So in the previous example, `logListener`
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
Expand All @@ -161,40 +158,39 @@ Issuing a place request is as simple as calling `okuki.gotoPlace(Place place)`.
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 backstack follows.
`HistoryAction` and the history back stack follows.

## Place History Backstack
## Place History Back Stack
In addition for providing a mechanism for requesting an receiving places, Okuki provides a history
backstack of the places requested. This backstack may be used to easily navigate to back to previously
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.
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 backstack as needed without triggering any Place requests.
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 backstack. But Okuki supports several
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 backstack.
* `REPLACE_TOP`: This will remove the most recent Place from the top of the history backstack, and
* `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 backstack. If not found, behaves the same as
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 backstack. If not found, behaves the same as `ADD`.
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 backstack.
including adding the requested Place to the back stack.

## Thread Safety
Okuki does not make use of concurrency, i.e. is _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 UI
Thread).
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
## Rx Bindings (RxJava 1.x)
RxBindings are provided in the optional package `okuki.rx`. 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:
Expand All @@ -207,59 +203,56 @@ subscription.unsubscribe(globalSub);
subscription.unsubscribe(branchSub);
subscription.unsubscribe(placeSub);
```
#### _RxJava 2.0 coming soon..._

## 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:
1. Add the dependency for Okuki-Android. (See _Setup_ above.)
2. Ensure that all of your `Place` classes either have `Void` payload types (which includes
`SimplePlace`), or payload types that implement `Parcelable` or `Serializable`.
3. 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._)
4. 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 to define dependency scopes. Using the staic methods provided
by `PlaceScoper` you can open and close scope whose hierachy matches the hierarcy of a given Place.
As you open each scope for a place, you can optionally provide any number of `Module` instances that
will be loaded into the scope. The dependencies loaded into the scope will be available to all scopes
opened for descendant Places until the scope is closed. Using this integration unifies the concepts
of "where are you" in an application with "what resources are available", and allows you to clean up
unused resouces (close scopes) more confidently knowing that you are not impacting the resources
depended on by other areas of your application.

### PlaceScoper Methods
* `PlaceScoper.openRootScope(Module... modules)`
Open the root scope (application-level scope) and install provided modules.

* `PlaceScoper.closenRootScope()`
Close root scope.

* `PlaceScoper.openPlaceScope(Class<? extends Place> placeClass, Module... modules)`
Open scope for a Place (hierachical, extending from root scope) and install provided modules.

* `PlaceScoper.closenPlaceScope(Class<? extends Place> placeClass)`
Close the scope for a Place.

* `PlaceScoper.openParentScope(Class<? extends Place> placeClass)`
Open the scope of a Place's parent.

### PlaceModules
In addition to `PlaceScoper`, the class `PlaceModule` is also included. This class extends from
Toothpick's standard `Module` class, but adds the functionality of _injecting itself_ with dependencies
from the specified Place's _parent scope_. What this means is that a `PlaceModule` can reference other
injected dependencies specified in higher-level scopes in order to instantiate other resources that
it will be providing without needing to provide additional code for passing these dependencies into
the `Module` constructor, etc.
For example, the module below provies a Retrofit API implementation that is scoped to the class
`KittensPlace`. As you can see the `OkHttpClient` and `Gson` instance are injected, as they are bound
at a scope higher in the hierarchy:
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:
1. Add the dependencies for both Toothpick and Okuki-Toothpick. (See _Setup_ above.)
2. 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:
```
public class KittensModule extends PlaceModule<KittensPlace> {
@Inject
OkHttpClient client;
@Inject
Gson gson;
public KittensModule() {
bind(GiphyDataManager.class).toInstance(new GiphyDataManager(client, gson));
bind(KittensResultsList.class).singletonInScope();
placeScoper = new PlaceScoper.Builder().okuki(okuki).modules(new AppModule(), new NetworkModule()).build();
```
3. 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() {
}
}
```
4. 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](https://github.com/wongcain/okuki/tree/master/okuki-toothpick-mvp-sample).

Expand Down
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
}
}

Expand Down
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Fri Aug 19 13:58:16 PDT 2016
#Mon Mar 06 16:38:00 PST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
42 changes: 42 additions & 0 deletions install-aar.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
apply plugin: 'android-maven'

group = publishedGroupId

install {
repositories.mavenInstaller {
// This generates POM.xml with proper parameters
pom {
project {
packaging 'aar'
groupId publishedGroupId
artifactId artifact

// Add your description here
name libraryName
description libraryDescription
url siteUrl

// Set your license
licenses {
license {
name licenseName
url licenseUrl
}
}
developers {
developer {
id developerId
name developerName
email developerEmail
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl

}
}
}
}
}
2 changes: 1 addition & 1 deletion install.gradle → install-jar.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ install {
// This generates POM.xml with proper parameters
pom {
project {
packaging 'aar'
packaging 'jar'
groupId publishedGroupId
artifactId artifact

Expand Down
1 change: 1 addition & 0 deletions okuki-android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
Loading

0 comments on commit bb401ee

Please sign in to comment.