- Easy to implement MVC/MVP/MVVM pattern for Android development
- Enhanced Android life cycles - e.g. a view needs to refresh when being brought back to foreground but not on rotation, onResume() is not specific enough to differentiate the two scenarios. Android mvc framework provides more granular life cycles
- All fragment life cycles are mapped into controllers thus logic in life cycles are testable on JVM
- Easy navigation between pages. Navigation is done in controllers instead of views so navigation can be unit tested on JVM
- Easy unit test on JVM since controllers don't depend on any Android APIs
- Built in event bus. Event bus also automatically guarantees post event view events on the UI thread
- Automatically save and restore instance state. You don't have to touch onSaveInstance and onCreate(savedInstanceState) with countless key-value pairs, it's all managed by the framework.
- Dependency injection with Poke to make mock easy
- Well tested - non-Android components are tested as the test coverage shown above (over 90%). For Android dependent module "android-mvc", it's tested by real emulator with this UI test module, even with "Don't Keep Activities" turned on in dev options to guarantee your app doesn't crash due to loss of instance state after it's killed by OS in the background!
Let's take a quick glance how to use the framework to navigate between screens first, more details will be discussed later.
The sample code can be found in Here. It is a simple counter app that has a master page and detail page. This 2 pages are represented by two fragments
- CounterMasterScreen paired with CounterMasterController
- CounterDetailScreen paired with CounterDetailController
In CounterMasterController, to navigate simply call
public void goToDetailView(Object sender) {
//Navigate to CounterDetailController which is paired by CounterDetailScreen
navigationManager.navigate(sender).to(CounterDetailController.class);
}
In CounterMasterScreen call the navigation method wrapped by the controller
buttonGoToDetailScreen.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
//Use counterController to manage navigation to make navigation testable
controller.goToDetailView(v);
}
});
In CounterDetailScreen
@Override
public void update() {
/**
* Controller will call update() whenever the controller thinks the state of the screen
* changes. So just bind the state of the controller to this screen then the screen is always
* reflecting the latest state/model of the controller
*/
display.setText(controller.getCount());
}
//Act: navigate to MasterScreen
navigationManager.navigate(this).to(CounterMasterController.class);
//Verify: location should be changed to MasterScreen
Assert.assertEquals(CounterMasterController.class.getName(),
navigationManager.getModel().getCurrentLocation().getLocationId());
//Act: navigate to DetailScreen
controller.goToDetailScreen(this);
//Verify: Current location should be at the view paired with CounterDetailController
Assert.assertEquals(CounterDetailController.class.getName(),
navigationManager.getModel().getCurrentLocation().getLocationId());
- View: Mainly fragments that bind user interactions to controllers such as tap, long press and etc. Views reflect the model managed by their controllers.
- Controller: Controllers expose methods to its view peer to capture user inputs. Once the state of the controller changes, the controller needs to notify its view the update. Usually by calling view.update() or post an event to its view.
- Model: Represents the state of view and managed by the controller. It can be accessed by controller.getModel(). But only read it to bind the model to views but don't modify the model from views. Modification of model should only be done by controller.
- Manager: What about controllers have shared logic? Break shared code out into managers. If managers need to access data. Inject services into managers. Managers can be thought as partial controllers serve multiple views through the controllers depending on them.
- Service: Services are below controller used to access data such as SharedPreferences, database, cloud API, files and etc. It provides abstraction for controllers or managers that can be easily mocked in unit tests for controllers. They the data access layer can be replaced quickly. For example, when some resources are removed from local data to remote data, just simply replace the services implementation to access web api instead of database or sharedPreferences.
See the illustration below
To enforce you don't write Android dependent functions into controllers to make unit tests harder, you can separate your Android project into 2 modules:
- app: View layer - a lean module depending on Android API only bind model to Android UI and
pass user interactions to core module. This module should include lib "android-mvc" explained
in download section below. This module includes:
- Activities, Fragments, Views, Android Services and anything as views depending on Android API
- Implementations of abstract contract defined in core module that depending on Android API. For example, a SharedPreferenceImpl that depends on Android context object.
- core: Controller layer - also includes model, managers and services. It's a module doesn't
have any Android dependency so can be tested straight away on JVM. This module should include lib
"android-mvc-core" explained in download section below. This module includes:
- Controllers
- Models
- Managers - shared by controllers
- Data services. When a service needs Android API it can be defined as an interface and implemented in app module. For example, define an interface SharedPreference to save and get data from Android preference. So in core module, the interface can be easily to be mocked for controllers or managers to provide mocked shared preference in unit tests.
However, separating the android project into two modules as above is not necessary. They for sure can be merged into one module and just depend on lib "android-mvc", which has already included "android-mvc-core". But in this way, you may accidentally write android dependent functions into controller to make mocking harder in controller unit tests.
See the chart below as an example of how to separate the modules. Also check out the Sample Code
Here is the the latest version number in jCenter
Maven:
-
lib android-mvc
<dependency> <groupId>com.shipdream</groupId> <artifactId>android-mvc</artifactId> <version>[LatestVersion]</version> </dependency>
-
lib android-mvc-core
<dependency> <groupId>com.shipdream</groupId> <artifactId>android-mvc-core</artifactId> <version>[LatestVersion]</version> </dependency>
Gradle:
-
lib android-mvc
compile "com.shipdream:android-mvc:[LatestVersion]"
-
lib android-mvc-core
compile "com.shipdream:android-mvc-core:[LatestVersion]"