Mortar Architect


Source link: https://github.com/lukaspili/Mortar-architect

DISCONTINUED

  • 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

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

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.

Resources

An image with a close button like Google Messenger attachments.

A parallax listview, scrollview and gridview with zoom support.

RefreshableView allows you to refresh ViewGroup, ListView using pull-to-refresh principle.

Variety of tools for Android development: working with thread pool, logging, crashes capturing, etc.

DraggableFlagView allows you to drag the red dot on the certain distance and run "disappear"-callback.

Use annotations to simplify android development.

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