Skip to content

Latest commit

 

History

History
569 lines (379 loc) · 21.8 KB

README.md

File metadata and controls

569 lines (379 loc) · 21.8 KB

Mortar Architect

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.

Stackable

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);
    }
}

Navigation

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

Navigator.push()

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).

Navigator.show()

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.

Navigator.replace()

It replaces the current view by the new one.
It means that the previous view won't be in the history stack.

Navigator.back()

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.

Navigator.chain()

Lets you execute several navigation events.

Navigator.set()

Set new paths stack by replacing the current one.

View Transitions

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 view

Once 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.

Return result

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;
    }
}

Architect and Navigator configuration

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.

StackableParceler

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 StackableParceler

The most performant solution is to make your StackablePath classes compatible with Parcelable. You have several options, like:

  • Making your stackable paths implement Parcelable which 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.

Don't restore navigation stack after process kill

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));

Nested navigator

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.

Nesting stackables

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>

Architect-commons

Commons is a facultative sub project that provides some base class you can extend from, in order to save some boilerplate code.

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.

Architect-Robot

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.

Navigation params

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"));

Demo projects

  • 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:

Motivation

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.

Key differences with Flow

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: push and show. 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.

Installation

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.15-SNAPSHOT'

    // 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'
}

Status

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.

Author

License

Mortar Architect is released under the MIT license. See the LICENSE file for details.