A pragmatic Android application architecture

Daniel Novak
INLOOPX
Published in
6 min readNov 28, 2016

Remember the issues with AsyncTasks and orientation change? Not only would the AsyncTask leak the Activity context, it would also deliver the results to the previous (destroyed) activity. There were so many approaches which tried to solve it. Passing the AsyncTask through onRetainNonConfigurationInstance(), using WeakReferences, event bus to deliver results, checking isResumed()/isAdded()

The next step were Loaders and the LoaderManager which I still view as a failed abstraction. The behaviour can be unpredictable and it’s more of a black box for most developers (initLoader(), restartLoader() ?). Also retained and worker Fragments were sometimes viewed as a silver-bullet solution to retaining data and handling the lifecycle callbacks — only leading to more confusion and problems.

Some developers in their despair would then totally decouple every API / IO asynchronous task from Fragments/Activities and have Services or Singletons that would process and then message back (events?). Leading to complicated systems which were hard to debug and had unpredictable states. Then ContentProviders…

Around that time a lot of blogs about MVP, MVVM and similar patterns on Android started to appear. Some are nice, some have a lot of boiler-plate code, some don’t play well with the Activity/Fragment life-cycle, others force you to use frameworks (Dagger, RxJava…) or totally abandon Fragments. But generally this approach makes sense for Android.

What do we need?

We need an instance of something, let’s call it a ViewModel (not the same as in MVVM) that is attached to the Fragment or Activity and is not affected by orientation changes. Removing the fragment or activity should also remove the ViewModel. It also should play well with state restoration and process death.

The Fragment or Activity should just have the responsibility of displaying the data and capturing/forwarding the user input.

We don’t want to create a new ViewModel on every orientation change nor do we want to have only one instance for the whole application. We want to be able to start asynchronous tasks with callbacks without worrying about orientation change or memory leaks.

There should be almost no boiler-plate code or dependencies or the need to force this approach over the whole existing application (in case of a refactoring).

The AndroidViewModel

Two years ago at Inloop we decided to start working on something that would meet the mentioned goals. The result is an open-source Apache 2.0 library available at https://github.com/inloop/AndroidViewModel. Since then it has been tested on multiple projects and several forks on GitHub exist.

AVM is watching your Activity and Fragment lifecycle and creates and manages the ViewModels for you. It attaches a ViewModel once you enter the Fragment / Activity and discards it when you leave it.

Its’ targets are applications that use Activities and Fragments to create the UI. We still believe that Fragments are very effective and standard tools to build Android applications. AVM in that is trying to make your life easier and hide you from some problems that you shouldn’t be managing anyway.

Show me the code

Well, the point is that there shouldn’t be much code to show :-). It should be simple to use.

So first you create the ViewModel, let’s say you have a Fragment that will load and display a list of users. We can also use a simple caching mechanism and keep the loaded users in a field — since the ViewModel will not be destroyed upon screen orientation.

We then need to define an interface for our view. This makes coding easier — as there is a strict defined protocol that the Fragment/Activity needs to implement. And it will also make sure that we don’t drag some Android specific code into the ViewModel.

And the standard Fragment that implements the defined IUserList interface (notice the getViewModelClass method which specifies the ViewModel class):

And in case the user clicks on an item, you can call your ViewModel directly.

The library on GitHub contains a sample where you have more examples.

State Saving

State saving is supported — although generally not necessary. You can override onSaveInstanceState(Bundle bundle) in your ViewModel to store the state. You will get the bundle back in ViewModel’s onCreate() but only in case a state restore is actually needed, otherwise it’s null (so you don’t need to restore when not necessary). State restoration for ViewModels happens only in case of process death (if the app was in the background and killed) and in case your Fragments are used with our ViewModelStatePagerAdapter.

A special case of FragmentStatePagerAdapter

The FragmentStatePagerAdapter is a special adapter that is doing some “magic” and bypassing standard state-saving logic. It’s removing old fragments while you are moving to the next page. And it’s manually extracting their state. Once you go back to a previous page — it will recreate the Fragment from scratch and restore its’ instance by retrieving the state from its’ internal Bundle map.

In order to comply with this behaviour we have to manually remove the ViewModel from the Fragment that is being destroyed, because we can’t detect that a FragmentStatePagerAdapter is “messing” with the fragments.

We haven’t found a better way than extending FragmentStatePagerAdapter and creating a ViewModelStatePagerAdapter. There is not much code fortunately, it’s only overriding the destroyItem method.

The getView() method

The AbstractViewModel contains a getView() method which you call at any time and also in callbacks of your asynchronous activities. This method will always return the current View of the Activity or View that the Fragment is attached to. Starting an asynchronous operation (even the dreaded AsyncTask) and calling getView() in the result callback will make sure you won’t leak or manipulate an old or destroyed view.

getView() will return null in case there is no current View created, attached or is already in a state where you can’t manipulate it. There is no need to do any other checks besides a null check. You can then store the result of your asynchronous operation for example in a local field and wait until bindView() is called again on you Fragment/Activity and display the data (goodbye LoaderManager).

The onModelRemoved() method

Another helpful callback — this signalises that the ViewModel has been detached from the Fragment or Activity and won’t be re-attached anymore. This happens if the Activity finishes or the Fragment is removed. It’s a good place to cancel any pending work or API calls that depend on the UI.

Data binding supported

Android Data Binding is supported (recently added) — you are able to bind the View directly (have ObservableField directly in the ViewModel). You only need to override getViewModelBindingConfig() in your Fragment to supply the necessary data for the binding to work.

No Activity/Fragment specific classes in ViewModel

The ViewModel is using an interface to talk to the Activity or Fragment — you could even write a mock or prototype implementation of the interface and change it later without any problems. It’s also easier to split work between developers — one can implement the Fragment based on the defined interface (e.g. implement showUsers(List…) method) and the second developer can work on the ViewModel (loading and preparing the data, handling the UI events).

No need to extend base classes

There is a ViewModelBaseActivity and a ViewModelBaseFragment which you can extend to simplify the implementation (and ViewModelBaseBindingFragment if you need Data Binding support). But you can also choose to just use the helpers and copy the implementation from these classes if you don’t want to extend the base classes included in the library (composition vs. inheritance).

It works for us, it may or may not for you

There is no one single pattern or architecture on Android and probably never will be. We have seen a lot of improvement by using AndroidViewModel, less Fragment / Activity state headaches, less orientation change problems and more focus on the actual problem. Give it a try or look at the sample code. Let us know what you think or if you are missing something.

--

--