AndroidMvc Framework

Android Mvc framework helps Android developers implement Android projects simpler and cleaner with MVC/MVP/MVVM patterns and make them testable.

Features

Implement MVC/MVP/MVVM pattern

As we know MVP and MVVM patterns are just simply derivatives of MVC pattern. All of them are targeting the same goal - Separation.

Separating Views and Controllers allows moving more business logic away from views into controllers. This makes code cleaner and, more importantly, it makes code more testable since most logical components are not depending on specific views. In the sense of Android, it means more fuctions can be written without depending on Android API therefore not have to be tested on emulators.

Since MVC, MVP and MVVM are similar, in this framework we call both Prestener in MVP and ViewModel in MVVM just as traditionally Controller.

See the diagram illustrating the relation between components above

AndroidMvc Layers

Sample code to implement MVP

Sample of MVVM with Android Data Binding Library

Check the sample project here, in which Android Data Binding Library is used in master screen and eventbus is used in detail screen to demostrate different ways to implement MVVM.

Fragment Life cycles

Since AndroidMvc framwork is designed to implement apps with a single Activity in most cases. Fragments are playing an important role. In the framework, fragments can be used as a screen which is the what an activity does traditionally. Also it can be used as sub view as well.

Below are life cycle callback of MvcFragment provided by AndroidMvc framework

FragmentController Life cycles

All fragment life cycles are mapped into FragmentController. So fragments are further liberated from handling business logic. For example, if you need to do some stuff in Framgent.onViewReady, you can do it in FragmentController.onViewReady.

Here are the life cycles

Navigation

As metioned earlier, AndroidMvc framework uses single activity to create Android apps. Therefore navigation in AndroidMvc is to swap full screen fragments. Though fragment transactions involve complexity primarily because they may be committed asynchronously, AndroidMvc aims to wrap all the tricks up. This is another reason why onCreateView and onViewCreated life cycle call back are sealed and replaced by onViewReady() metioned in FragmentController Life cycles section.

The navigation functions are tested in instrumentation test cases. If you are interested you can check out the in instrumentation test project.

Mapping between MvcFragment and FragmentController

Routing

Routing rules can be defined in you main activity extending MvcActivity. Implement method MvcActivity#mapControllerFragment() to map which fragment will be launched as a full screen page for the corresponding controller class type.

A typical routing rule is as the code below.

@Override
protected Class<? extends MvcFragment> mapFragmentRouting(
        Class<? extends Controller> controllerClass) {
    if (controllerClass == CounterMasterController.class) {
        return CounterMasterScreen.class;
    } else if (controllerClass == CounterDetailController.class) {
        return CounterDetailScreen.class;
    } else {
        return null;
    }
}

If you want more automation, you can choose your own package structure file name pattern to apply a generic routing rule to locate concrete MvcFragment classes like below. See the code in the sample project

@Override
protected Class<? extends MvcFragment> mapFragmentRouting(
        Class<? extends FragmentController> controllerClass) {
    String controllerPackage = controllerClass.getPackage().getName();

    //Find the classes of fragment under package .view and named in form of xxxScreen
    //For example

    //a.b.c.CounterMasterController -> a.b.c.view.CounterMasterScreen

    String viewPkgName = controllerPackage.substring(0, controllerPackage.lastIndexOf(".")) + ".view";
    String fragmentClassName = viewPkgName + "."
            + controllerClass.getSimpleName().replace("Controller", "Screen");

    try {
        return (Class<? extends MvcFragment>) Class.forName(fragmentClassName);
    } catch (ClassNotFoundException e) {
        String msg = String.format("Fragment class(%s) for controller(%s) can not be found",
                fragmentClassName, controllerClass.getName());
        throw new RuntimeException(msg, e);
    }
}

Continuity between screens

AndroidMvc has 3 different ways to ensure continuity between two consequent screens on navigation transition

  1. Shared injectable instances will be retained through the navigation transition. For example, when 2 controllers have the same type of manager injected the same instance of the manager from the first screen's controller will be retained for the second screen's controller. You can check out the sample code, in the CounterMasterController there is an injected field called counterManager which is injected into CounterDetailController as well. So when master controller navigate to detail controller, the state of the manager retains.
  2. Just prepare the controller of the next screen just before navigation is taking place. In this case, the controller prepared will be injected into the next screen framgment.

    navigationManager.navigate(this).with(CounterDetailController.class, 
                new Preparer<CounterDetailController>() {
            @Override
            public void prepare(CounterDetailController detailController) {
                //Set the initial state for the controller of the next screen
                detailController.setCount(123);
            }
        }).to(CounterDetailController.class);
  3. Hold an injected instance of the manager depending on the next screen in the controller held by the delegateFragment. DelegateFragment is a long life fragment in the single activity containing all other fragments, so its controller will referenced during the entire lifespan of the app UI comopnent. So the inject managers held by the controller remain during the whole app session. For example, AndroidMvc has already had an internal controller for the delegateFragment holding NavigationManager, so the navigationManager is singleton globally and live through the entire app life span. Another example is, you can have an AccountManager held by delegateFragment's controller so accountManager will span the entire app session to manage the logged in user.

Navigation tool bar

The screen fragment doesn't have to take the entire screen. For example, all screens can share the same toolbar just like the tranditional ActionBar.

More details can be found in the Sample Code

Run AsyncTask in controller

When a http request need to be sent or other long running actions need to be performed, they need to be run off the UI thread. To run an asyncTask in controllers, simply call

//In any controller method you can use below code to run async task

Task.Monitor<Void> monitor = runTask(new Task<Void>() {
    @Override
    public Void execute(Monitor<Void> monitor) throws Exception {
        //Execute on Non-UI thread
        //When the view need to be updated here, you need use
        //uiThreadRunner to post it back to UI thread

        return null;
    }
}, new Task.Callback<Void>() {
    @Override
    public void onException(Exception e) throws Exception {
        //Handle exception
        //All callback methods are executed on UI thread
    }
});

//If you need to cancel unscheduled or executing task, call
//cancel against its monitor
boolean canInterrupt = true;
monitor.cancel(canInterrupt);

As you see, runTask will give you a monitor which can be used to query the state of the task or cancel it if it has not started or interupt the currently executing task for example a downloading task.

Tips:

Easy Unit Test

So far, you should have already got some ideas how much business logic can be written in controllers with AndroidMvc. See sample unit tests here

Enforce running controller tests

To enforce your controllers' unit tests to run successfull before assempling your Android app you can add a gradle task dependency of the controller's test task in your Android app module's build.gradle. So that your Android app won't compile until all your controllers' unit tests pass.

See the sample code's build.gradle

  //Enforce tests of core to be run before assembling each variant.
  android.applicationVariants.all {
  variant -> variant.assemble.dependsOn(":core:test")
  }

Mock dependencies

As most codes are wrapped in controllers without Android API dependencies. You can just simply test everything on JVM. Because in app, there are dependencies implemented with Android API which are lacking in the sole controller module, those dependencies' implementations need to be replaced by mocked instances. For example, every controller has an injected field - ExecutorService to run a aysncTask by controller.runTask(Task task). In unit test, this ExecutorService can be mocked and run task immediately on the same thread to mimic a http response with mocked data.

To override providers to replace injectable classes,

  1. create a MvcComponent say overridingComponent
  2. register your providers to provider mocking objects
  3. attach the overridingComponent to Mvc.graph().getRootComponent() with the

See the code sample below

//Mock executor service so that all async tasks run on non-UI thread in app will
//run on the testing thread (main thread for testing) to avoid multithreading headache
executorService = mock(ExecutorService.class);

doAnswer(new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        Callable runnable = (Callable) invocation.getArguments()[0];
        runnable.call();
        Future future = mock(Future.class);
        when(future.isDone()).thenReturn(true); //by default execute immediately succeed.
        when(future.isCancelled()).thenReturn(false);
        return future;
    }
}).when(executorService).submit(any(Callable.class));

overridingComponent = new MvcComponent("TestOverridingComponent");
overridingComponent.register(new Object(){
    @Provides
    public ExecutorService createExecutorService() {
        return executorService;
    }
});

//For base test class, allow sub test cases to register overriding providers
prepareGraph(overridingComponent);

Component rootComponent = Mvc.graph().getRootComponent();

//overriding indicates providers of this component attached to the root component will override 
//existing providers managing to provide instances with the same type and qualifier.
boolean overriding = true;
rootComponent.attach(overridingComponent, overriding);

Mock http response

Below are some code snippets of mocking http traffic. More code can be found in the sample in the project. The sample is using Retrofit for http resources.

Event Bus

There are event buses built in the framework can be used straight away.

Below is a code snippet. Note that the code is not complete to run but just for demostrating how to use event bus

public class OneView {
    @Inject
    private OneController onController;

    private Button refreshButton;

    public void onCreated() {
        eventBusV.register(this);

        refreshButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View refreshButton) {
                onController.refresh(refreshButton);
            }
        });
    }

    public void onDestroy() {
        eventBusV.unregister(this);
    }

    //Observe event OnListViewRefreshed from OneController
    //It runs on UI thread automatically
    private void onEvent(OneController.Event.OnListViewRefreshed event) {
        if (event.getSender() == refreshButton) {
            //refreshed by user interaction of pressing the refresh button
            //so if erred, it's better to show error message
        } else {
            //should by something else, e.g. controller wants to refresh for some 
            //reason
            //In this case, error message may not have to be shown
        }

        //refresh the list view
    }
}

public class OneController extends Controller{
    interface Event {
        public class OnListViewRefreshed {
            private final Object sender;
            public OnListViewRefreshed(Object sender) {
                this.sender = sender;
            }

            public Object getSender() {
                return sender;
            }
        }
    }

    @Inject
    private NavigationManager navigateManager;

    @Inject
    @EventBusV
    private EventBus eventBusV; //Framework guarantees the event will be posted to UI thread

    public void refresh(final Object sender) {
        runTask(new Task() {
            @Override
            public Object execute(Monitor monitor) throws Exception {
                //Pull data from http server
                //...
                //

                //successful and post event
                //Though execute method is run on non-Ui thread,
                //eventBusV will guarantee the observer will receive the event onto
                //UI thread
                eventBusV.post(new Event.OnListViewRefreshed(sender));
                return null;
            }
        });
    }

    //Observe logged on event from UserManager
    private void onEvent(UserManager.Event.OnLoggedOut event) {
        //...
        //User logged out

        navigateManager.navigate(this).to(LoginController.class, new Forwarder().clearAll());
    }
}

public class UserManager {
    interface Event {
        class OnLoggedOut{
            private final Object sender;
            public OnLoggedOut(Object sender) {
                this.sender = sender;
            }

            public Object getSender() {
                return sender;
            }
        }
    }

    @Inject
    @EventBusC
    private EventBus eventBusC; 

    public void setCurrentUser(Object sender, User user) {
        //... 

        //EventBusC post event to the thread that the invoker calling setCurrentUser 
        //is running on
        eventBusC.post(new Event.OnLoggedOut(sender));
    }
}

Instance State Management

Instance state management in standard Android SDK is painful. It requires defining countless key-value pairs. No mention to create Paracelable needs to write a lot of boilerplate code and it's error prone.

AndroidMvc framework manages the state automatically. Since a controller represents a view in abstraction so it has a property called model to contain the state of its view. AndroidMvc just getModel() and serialise it when the app is pushed to background and deserialise the model and bind it to the controller and view automatically. This is why you just need to bind the view in UiView.update() method and then no matter the view is newly created or restored, it's always reflecting the latest state of the model managed by the view's controller. Review Implement MVC/MVP/MVVM pattern to check the patterns.

Also not just controllers manage their state automatically, managers, services and any injected objects extending Bean(#https://github.com/kejunxia/AndroidMvc/blob/master/library/android-mvc-core/src/main/java/com/shipdream/lib/android/mvc/Bean.java) and returns non-null class type in its method modelType() will be automatically managed. See more details about Bean.java and injection in section Dependency injection

Dependency injection (Poke)

The framework has a built in dependency injection called Poke. Why reinvent the wheel?

To find the balance between simpler code and runtime performance, Poke can use naming convention to automatically locate implementations. So we don't need to repeatedly declare implementations by writing in real application with. However, Poke also allows registering implementation manually. This is helpful either for dynamical replacement of implementations or mocking injectable dependencies in unit tests.

Bean

Classes and interfaces can be injected with the their instances. There are some classes can be injected with some special behaviours. These classes are called beans. A Bean is an object 1. has life cycles. When a bean is created by the first injection, its method onCreated will be called. When a bean is released by the last object it was injected into, it method onDestroy will be called. 2. has a model. A model contains the state of the bean that can be serialised and deserialised. So the bean's state can be saved and restored during the Android's activity and fragment's life cycles.

In AndroidMvc framework there are a couple of pre-defined beans

If you want to use an interface as a bean, just extend bean class in its implementation.

Architecture

Below are the main parts to inject objects

How to inject

With poke and AndroidMvc, to inject an instance is easy. It doesn't need to declare what needs to don't need to Below we will explore how to inject in different scenarios.