- Most recent branch: 1.0_dev3.x
- Most recent artifact: 1.0-dev3.8-SNAPSHOT (stable and used in production in several apps)
- Documentation is not up-to-date, you need to check the demo project in the 1.0_dev3.x branch
Mortar Architect helps building modern Android apps, implementing the MVP pattern with Mortar.
When working with Mortar and MVP pattern, you don't create Activities and Fragments anymore, but Android Views and ViewPresenters. Each ViewPresenter is associated to a MortarScope that holds and provide the ViewPresenter to its associated View.
If you use Dagger2, it would be the Dagger2 Component that holds and provides the ViewPresenter instance, and the MortarScope would hold and provide the Component instance.
Architect provides tools for navigating between Mortar scopes, and nesting Mortar scopes. It's more feature complete and ready to use than the "official" library Flow. It also requires much less code to write and it integrates seamlessly with Mortar.
Because Architect relies on Mortar scopes, it also require a class that setups those scopes. For each MortarScope that will be associated to a View and ViewPresenter, you have to provide a Stackable class that configures the Mortar scope.
The following Stackable class creates a Dagger2 component, and puts it inside the MortarScope. The Dagger2 component provides the HomePresenter, which is an instance of ViewPresenter.
@DaggerScope(Component.class)
public class HomeStackable implements Stackable {
private String name;
public HomeStackable(String name) {
this.name = name;
}
@Override
public void configureScope(MortarScope.Builder builder, MortarScope parentScope) {
builder.withService(DaggerService.SERVICE_NAME, DaggerHomePath_Component.builder()
.mainActivityComponent(parentScope.<MainActivityComponent>getService(DaggerService.SERVICE_NAME))
.module(new Module())
.build());
}
@dagger.Module
public class Module {
@Provides
@DaggerScope(Component.class)
public HomePresenter providesPresenter() {
return new HomePresenter(name);
}
}
@dagger.Component(dependencies = MainActivityComponent.class, modules = Module.class)
@DaggerScope(Component.class)
public interface Component {
void inject(HomeView view);
}
}With a Stackable, you can create new Context that contains the associated MortarScope. Architect takes care of the Mortar scope creation, and ensures the scope name is preserved during config changes.
public class HomeView extends FrameLayout {
protected HomePresenter presenter;
public HomeView(Context context, AttributeSet attrs) {
Context newContext = StackFactory.createContext(context, new HomeStackable());
View.inflate(newContext, R.layout.home_view, this);
DaggerService.<HomeStackable.Component>get(newContext).inject(this);
}
}Architect Navigator class allows you to navigate between Mortar scopes. It manages a history stack that preserves previous Mortar scope, allows you to provide custom transitions between views, and survives configuration changes and process kills.
Navigator lives inside its own Mortar scope, and you can retreive its instance through a child scope, from a View or a Context wrapped by Mortar.
Navigator.get(context).push(new ShowUserStackable("lukasz"));Stackable class does not specify which is the associated View to display, because you directly use the Stackable inside the View. For navigation, you need to implements the StackablePath interface, that extends from Stackable and declares one additional method:
public interface StackablePath extends Stackable {
// Return either a new MyView(context) directly
// Or inflate an xml: LayoutInflater.from(context).inflate(R.layout.my_view, parent, false)
View createView(Context context, ViewGroup parent);
}The following HomeStackable implements now StackablePath. Nothing else changed from the code above.
@DaggerScope(Component.class)
public class HomeStackable implements StackablePath {
View createView(Context context, ViewGroup parent) {
return new HomeView(context);
}
// ...
}Which is now compatible with Navigator:
Navigator.get(getView()).push(new HomeStackable("first home"));Navigator provides 6 navigation methods
The common navigation way, that push the new path in the navigation history. It will perform the view transition from the previous view to the new view. Once the transition is done, the previous view will be removed and destroyed. However, its Mortar scope won't be destroyed (and so neither its ViewPresenter).
The way when you want to show a "modal" view.
It works the same way as push(), but the difference is that the previous view won't be removed at the end of the view transition.
It's useful when you want to for instance to show a View on top of the previous one, while not taking the whole screen. So you would want that the previous view is not removed and still visible.
It replaces the current view by the new one.
It means that the previous view won't be in the history stack.
It goes back into the history stack.
It will perform the backward() view transition, and then remove the old view and destroy its Mortar scope.
Lets you execute several navigation events.
Set new paths stack by replacing the current one.
You can provide a TransitionsMapping to the Navigator that defines what view transition perform when navigating from one view to another.
navigator.transitions().register(TransitionsMapping()
.byDefault(new LateralViewTransition()) // default transition
.show(MyPopupView.class).withTransition(new FadeModalTransition(new Config().duration(250))) // by default, it's show().fromAny()
.show(MyOtherScreen.class).from(HomeView.class).withTransition(new BottomAppearTransition())); // you can also specify show().from() specific viewOnce the mapping is provided to the Navigator instance, it will apply the correct view transitions automatically.
You can also create and provide your custom view transitions. The following is the code of the LateralViewTransition which animates from left-to-right and reverse.
// LateralViewTransition.java
public class LateralViewTransition implements ViewTransition {
public LateralViewTransition() {
}
@Override
public void transition(View enterView, View exitView, ViewTransitionDirection direction, AnimatorSet set) {
if (direction == ViewTransitionDirection.FORWARD || direction == ViewTransitionDirection.REPLACE) {
set.play(ObjectAnimator.ofFloat(enterView, View.TRANSLATION_X, enterView.getWidth(), 0));
set.play(ObjectAnimator.ofFloat(exitView, View.TRANSLATION_X, 0, -exitView.getWidth()));
} else {
set.play(ObjectAnimator.ofFloat(enterView, View.TRANSLATION_X, -enterView.getWidth(), 0));
set.play(ObjectAnimator.ofFloat(exitView, View.TRANSLATION_X, 0, exitView.getWidth()));
}
}
}You can find more transitions in the sub-project commons.
A ViewPresenter can return a result to the previous ViewPresenter in the history. A kind of onActivityResult() between ViewPresenters.
Let's say you navigated from PresenterA to PresenterB, and now PresenterB wants to return a String result to PresenterA:
// PresenterB.java
Navigator.get(getView()).back("My result!");PresenterA must implement the ReceivesResult interface:
// PresenterA.java
public class PresenterA extends ViewPresenter<AView> implements ReceivesResult<String> {
private String result;
@Override
public void onReceivedResult(String result) {
this.result = result;
// beware that this is called before onLoad() and getView() returns null here
}
@Override
protected void onLoad(Bundle savedInstanceState) {
// onLoad() is called when we go back from PresenterB to PresenterA
if(result != null) {
getView().getTitleTextView().setText(result);
}
}
}You must also ensures that the View associated to the ViewPresenter that receives the result implements HasPresenter interface. It is already the case for all the base views of the architect-commons subproject.
public class AView extends LinearLayout implements HasPresenter<PresenterA> {
@Inject
protected PresenterA presenter;
@Override
public PresenterA getPresenter() {
return presenter;
}
}Before using Navigator, you need to configure and hook it to the root activity.
You need to call the Navigator.delegate() methods at the proper place.
You can find an example of configuration in the MainActivity class.
architect-commons subproject provides the ActivityArchitector class that takes care of some boilerplate required to setup Architect in the root activity.
In order to survive process kills, and restore the navigation stack, Navigator requires a StackableParceler that saves and restore the StackablePath from disk with the help of Android Parcelable.
Navigator navigator = Navigator.create(scope, new Parceler()); // Parceler is a class that implements StackableParcelerThe most performant solution is to make your StackablePath classes compatible with Parcelable. You have several options, like:
- Making your stackable paths implement
Parcelablewhich adds tons of boilerplate - Use a library that takes of the boilerplate for you, like Parceler
Below an example of the second solution:
// Some StackablePath
@Parcel(parcelsIndex = false)
public class HomeStackable implements StackablePath {
String name;
@ParcelConstructor
public HomeStackable(String name) {
this.name = name;
}
...
}
// StackableParceler
public class Parceler implements StackableParceler {
@Override
public Parcelable wrap(StackablePath path) {
return Parcels.wrap(path);
}
@Override
public StackablePath unwrap(Parcelable parcelable) {
return Parcels.unwrap(parcelable);
}
}That's it! Boilerplate to the minimum.
With Navigator, you can choose to not restore the navigation stack when the application process is killed. By default this option is not enabled.
The very big advantage of this option is enabled is that you won't have to bother with the savedInstanceState Bundle in the ViewPresenter onLoad(savedInstanceState) and onSave(Bundle outState).
Indeed, because ViewPresenter instances survive configuration changes, the only case where you would save and restore ViewPresenter instance from the Bundle class is when Android kills your application process. The next time you would open the app, Navigator would restore your navigation stack, and thus it would be your responsability to restore your ViewPresenter states.
In opposite, when the "don't restore navigation stack" option is enabled, Navigator will not restore the navigation stack if the process is killed, but will start the app from the initial state. So you would never use the savedInstanceState Bundle in your ViewPresenters.
To enable the option, provide a custom configuration when creating the Navigator instance:
// don't need to provide a parceler if dontRestoreStackAfterKill is true
Navigator navigator = Navigator.create(scope, null, new Navigator.Config().dontRestoreStackAfterKill(true));Architect is very flexible and you can use several Navigator instances at the same time. It allows to provide nested navigation in your app.
You can find an example of a sub navigator configured in a ViewPresenter in the SubnavPresenter class.
With Architect, you can easily nest several Stackables. You would for instance want to include a Stackable inside another one.
The following HomeMenuStackable is nested in the HomeStackable.
// HomeMenuPresenter.java
@AutoStackable(
component = @AutoComponent(dependencies = HomePresenter.class)
)
@DaggerScope(HomeMenuPresenter.class)
public class HomeMenuPresenter extends ViewPresenter<HomeMenuView> {
private final HomePresenter homePresenter;
@Inject
public HomeMenuPresenter(HomePresenter homePresenter) {
this.homePresenter = homePresenter;
}
@Override
protected void onLoad(Bundle savedInstanceState) {
}
}
// HomeMenuView.java
@AutoInjector(HomeMenuPresenter.class)
public class HomeMenuView extends FrameLayout {
@Inject
protected HomeMenuPresenter presenter;
public HomeMenuView(Context context, AttributeSet attrs) {
// create new Mortar wrapped context for the HomeMenuScope
Context newContext = StackFactory.createContext(context, new HomeMenuScope());
DaggerService.<HomeMenuScopeComponent>get(newContext).inject(this);
View view = View.inflate(newContext, R.layout.view_home_menu, this);
ButterKnife.inject(view);
}
// onAttachedToWindow()
// onDetachedFromWindow()
}You can then directly use the HomeMenuView in the HomeView layout:
<!-- view_home.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.mvp.presenter.HomeMenuView
android:id="@+id/menu_view"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="left|start"/>
</FrameLayout>Commons is a facultative sub project that provides some base class you can extend from, in order to save some boilerplate code.
ActivityArchitectorPresentedXXX, likePresentedFrameLayout,PresentedLinearLayout, etc. Base class for a View associated to a ViewPresenter.StackedXXX, likeStackedFrameLayout,StackedLinearLayout, etc. Base class for the a View associated to a ViewPresenter, and that will be included (stacked) in another one (like theHomeMenuView)StackablePagerAdapter, an implementation ofViewPagerthat manages a set ofStackablePath
The commons project is here both for easing the integration and providing an example of implementations that work well with Mortar and Architect. The code is very simple and straightforward.
Robot is a subproject that contains an annotation processor that generates Stackable and StackablePath classes for you. Robot is opiniated, it works only with Dagger2 and uses Auto Dagger2 to generate Dagger2 components.
To generate a Stackable from a ViewPresenter, use the @AutoStackable annotation:
@AutoStackable(
component = @AutoComponent(includes = StandardAutoComponent.class)
)
@DaggerScope(SlidesPresenter.class)
public class SlidesPresenter extends ViewPresenter<SlidesView> {
}And provide either pathWithView or pathWithLayout to generate a StackablePath instead:
@AutoStackable(
component = @AutoComponent(includes = StandardAutoComponent.class),
pathWithView = SlidesView.class
// OR
// pathWithLayout = R.layout.slides_view
)
@DaggerScope(SlidesPresenter.class)
public class SlidesPresenter extends ViewPresenter<SlidesView> {
}pathWithView will generate a StackablePath that instanciates the View directly, while pathWithLayout will generate a path that inflates the layout. You cannot use both at the same time.
By default, the generated StackablePath will have an empty constructor, and all the parameters of the ViewPresenter's constructor will be provided by Dagger2 in its module.
If you want some parameters to be provided by navigation, use the @FromPath annotation.
@AutoStackable(
component = @AutoComponent(dependencies = RootActivity.Component.class),
path = @AutoPath(withView = ShowUserView.class)
)
@DaggerScope(ShowUserPresenter.class)
public class ShowUserPresenter extends ViewPresenter<ShowUserView> {
// username is provided by the navigation
private final String username;
// some dependencies provided by dagger
private final RestClient restClient;
private final UserManager userManager;
// NOTE the @FromPath on the parameter provided by the navigation
public ShowUserPresenter(@FromPath String username, RestClient restClient, UserManager userManager) {
this.username = username;
this.restClient = restClient;
this.userManager = userManager;
}
}You can then navigate to the new generated StackablePath
Navigator.get(getView()).push(new ShowUserStackable("lukasz"));- The subproject app which showcases all the features offered by Architect
- Mortar architect map demo which showcase how to use MapView and DrawerLayout with Architect
You can also checkout the following example projects using Mortar and Flow. It may give you better understanding on how works Mortar and Flow together, and thus the purpose of Architect:
The motivation behind Architect is to provide a framework for building MVP apps with Mortar, with the minimum friction and boilerplate code.
While Flow can in theory work without Mortar, Architect relies heavely on Mortar and Mortar scopes. It allows to provide an API that integrates seamlessly with Mortar.
The goal is not to say that Architect is better than Flow, but that the 2 libraries handle things differently
-
Architect does not destroy the Mortar scopes in history. It means that the ViewPresenter of a previous View won't be destroyed, and a new View will be re-attached once navigation gets back to it.
-
Architect provides two different ways of navigation:
pushandshow. The latter allows to push a new View without removing the previous one (useful for showing partial views, like dialogs). Architect handles the view manipulation and restoration during config changes. -
Navigation events are applied directly on history, without waiting for the ViewTransition to finish. It means that if you rotate the screen during transition from A to B, the screen showed after rotation will be B. In opposite, Flow will start the view transition from A to B again.
-
Architect provides a ViewTransition mapping that let you define how to transition from a View to another, with very little code.
-
Architect allows to have nested navigation.
-
Architect provides convinient methods to nest scopes and views. Like including B into A directly in view's xml.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.1.3'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
}
}
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
repositories {
jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}
dependencies {
// local var convinience for architect version
def architect_version = '0.21'
// Core library
compile 'com.github.lukaspili.mortar-architect:architect:' + architect_version
// Commons
compile 'com.github.lukaspili.mortar-architect:commons:' + architect_version
// Robot
compile 'com.github.lukaspili.mortar-architect:robot:' + architect_version
apt 'com.github.lukaspili.mortar-architect:robot-compiler:' + architect_version
// Robot requires dagger2 and auto dagger2 deps
// Dagger2
compile 'com.google.dagger:dagger:2.0.1'
apt 'com.google.dagger:dagger-compiler:2.0.1'
provided 'javax.annotation:jsr250-api:1.0'
// Autodagger2
compile 'com.github.lukaspili.autodagger2:autodagger2:1.1'
apt 'com.github.lukaspili.autodagger2:autodagger2-compiler:1.1'
}The core API should be stable enough. Architect is implemented in several apps.
Because of the rapid development cycle, I'm currently only using SNAPSHOT versions (I don't want to wait for maven central propagation). Once Architect reaches the stable version 1.0, I will adopt proper versioning.
- Lukasz Piliszczuk (@lukaspili)
Mortar Architect is released under the MIT license. See the LICENSE file for details.