June 7, 2017 / Reading time: 10m

Android Architecture Components — now with 100% more MVVM

android

opensource

Room to LiveData to ViewModel to View. — How reactive ☢ are you?

Google announced some awesome tools during #io17. They’re called Architecture Components, I’m going to assume you already know the basics about each, if not you can find videos and official documentation at the end of this article.

After studying these new elements I was able to write up a simple demo project, where you can check (especially if you don’t have any prior experience) how awesome reactive programming can be. For that reason and regarding my previous MVP project, I decided to open source the code and write a blog post about my experience.


MVVM + Observables + Repositories + Architecture Components 😨?

Don’t be overwhelmed, it’s simpler than it first looks, this is my interpretation:

TL;DR: Data source → Repository → ViewModel → View. There’s a nice diagram at the end of this section with a simplified event/data flow. Also the project is available on GitHub.

The repository serves as the communication bridge between the data and the rest of the app. Building a Foo feature, the FooRepository abstracts the data source(s) from the FooViewModel, while providing the necessary Observables to stream data to the latter.

On the other hand, the ViewModel should bridge the Repository and the View, this is also where your Business Logic lives, much like a Presenter in MVP or a Controller in MVC, but with some key differences, we’re extending the framework’s own arch.lifecycle.ViewModel class, which has some internal lifecycle awareness, making our development a little less painful, but more on that later.

The FooView will hold a reference to the corresponding FooViewModel instance, getting this is as simple as calling:

viewModel = ViewModelProviders.of(this).get(FooViewModel.class);
Note: ViewModel objects are scoped to the Lifecycle passed to the ViewModelProvider when getting the ViewModel. The ViewModel stays in memory until the Lifecycle it’s scoped to go away permanently—in the case of an activity, when it finishes; in the case of a fragment, when it’s detached.

The next and final step is to subscribe to the ViewModel’s exposed data observables, display the data coming in, and report the user actions back to the ViewModel.

That’s it, no need to have a huge amounts of callbacks, keeping your views (as always) logic free, but now less verbose, therefore easier to maintain 👍.

Using this structure enables our code to be more reusable/modular, when compared to other patterns.

With the new android architecture components writing tests 👈 and separation of concerns is easy as 🍰.

Data and Events flowing seamlessly.

LiveData<T>: Is a data holder class that keeps a value and allows this value to be observed. Unlike a regular observable, LiveData respects the lifecycle of app components, such that the Observer can specify a Lifecycle in which it should observe.


Android’s ViewModel gives us a lifecycle aware component, without the need of holding a reference to a View (e.g Activity, Fragment), more susceptible to leaks. However it will take ownership of the data, therefore we must focus on exposing the right data and work on the Business Logic.

documentation
Note: Since the ViewModel outlives specific activity and fragment instantiations, it should never reference a View, or any class that may hold a reference to the activity context. If the ViewModel needs the Application context (for example, to find a system service), it can extend theAndroidViewModel class and have a constructor that receives the Application in the constructor (since Application class extends Context).

Bus ETA

Disclosure: At the moment my buddy Lucas Caixeta and I are building this project, so the full version won’t be open source, but everything discussed or related to this article is available on GitHub.


UPDATE 🔑 — New name: Transport ETA

This project will be redone in full on my coding stream

I decided to stream myself building an app with more complex tools and engineering paradigms from the ground up, this is the project I decided go with, not that makes any difference but I’ll make it an MVP, release but in the end will be an app built with: CLEAN architecture, Reactive and Tested app written in Kotlin only.

Uses SMS-Token service or the web to request a more precise ETA of a public-transport (Bus, Boat etc.) to a specific station, currently implementing for Lisbon 🇵🇹, but the idea is to scale up to any other cities around the world that provide the same kind of service(s). Most of the complex stuff will happen on the backend anyway — yay 🙌 Cloud Functions for Firebase. Writing a well cared reactive android client codebase should be enough to adapt.

Currently written in Java, but will most likely become Kotlin only on new classes/features.

Structure

The Data and Ui are in separated parts, imho it’s a crucial way to keep yourself form doing spaghetti code. Using the observer pattern has its advantages since we can simply open channels streaming data and then listen. Sounds awesome (and it is) but, if you’re not careful, you’ll start losing control of your own code 😣.

👌.

I’ve placed each ViewModel class inside of it’s ui/foo package, but it would be correct to have these on a separate viewmodel package at root-level along with repository and ui. I’m still experimenting to see if makes sense, if so, I’ll change it. Feel free to leave your opinion down below 👍.

There’s also a repository for each context, this will further help to archive the app’s complete abstraction from the data-source(s). And these can also be used by different ViewModels since the sole responsibility is to provide (specific) data.

At the moment the web-service responses are mocked, with MVVM or any other battle tested pattern, anything outside of the scope of this article is virtually meaningless, that’s the beauty of a good software architecture.

Note: As mentioned above, the open sourced code isn’t 100% implemented, there is a List and Detail screens (with a list of Stations belonging to the specific bus, demoing relation), this is purely to show Room’s implementation. I might update the project in the future, but I think there is enough there to understand the AACs.

Bye bye Presenter. Hello ViewModel.

Let’s start looking at our BusListViewModel


public class BusListViewModel extends ViewModel {

	private BusRepository busRepository;

	private MutableLiveData<BusEntity> selectedItem;
	private LiveData<List<BusEntity>> busList;

	public BusListViewModel(@Nullable BusRepository busRepository) {
		if (this.busRepository != null) {
			// ViewModel is created per Activity, so instantiate once
			// we know the userId won't change
			return;
		}
		if (busRepository != null) {
			this.busRepository = busRepository;
		}
	}

	public void onFabButtonClicked() {
		// As an example we'll create some data
		DatabaseMockUtils.populateMockDataAsync(DatabaseCreator.getInstance().getDatabase());
	}

	public void onPullRequested() {
		busRepository.loadListNewData();
	}

	public void onListItemClicked(BusEntity bus) {
		if (selectedItem.getValue() == bus) {
			return;
		}
		// At the moment this does nothing but would be a way to communicate changes to other
		// views (e.g. fragment) that subscribed to this listener.
		selectedItem.postValue(bus);
	}

	public LiveData<List<BusEntity>> getBusList() {
		if (busList == null) {
			busList = new MutableLiveData<>();
			loadBuses();
		}
		return busList;
	}

	public LiveData<BusEntity> getSelectedItem() {
		if (selectedItem == null) {
			selectedItem = new MutableLiveData<>();
		}
		return selectedItem;
	}

	private void loadBuses() {
		// Observe if/when database is created.
		busList = Transformations.switchMap(DatabaseCreator.getInstance().isDatabaseCreated(),
				new Function<Boolean, LiveData<List<BusEntity>>>() {
					@Override
					public LiveData<List<BusEntity>> apply(Boolean isDbCreated) {
						if (Boolean.TRUE.equals(isDbCreated)) {
							return busListRepository.loadList();
						}
						return null;
					}
				});
	}

	/**
	 * This creator is to showcase how to inject dependencies into ViewModels. It's not
	 * actually necessary in this case, as the BusRepository can be passed in a public method.
	 */
	static class Factory extends ViewModelProvider.NewInstanceFactory {

		private BusRepository mBusRepository;

		public Factory() {
		}

		public Factory(@NonNull BusRepository busRepository) {
			mBusRepository = busRepository;
		}

		@Override
		public <T extends ViewModel> T create(Class<T> modelClass) {
			//noinspection unchecked
			return (T) new BusListViewModel(mBusRepository);
		}
	}
}

As you can see the ViewModel has no idea to whom might subscribe to the emitted data. It is also completely abstracted from the data sources, it simply asks theBusRepository for the data and doesn’t really care of it’s internal implementations, it could come from a remote webservice or a simple local database (this is oddly satisfying to know we’re respecting the Single responsibility principle).

So now looking at our BusRepository


public class BusRepository {

	private static BusRepository sInstance;

	private final RemoteRepository remoteRepository;
	private final BusDao busDao;

	public static BusRepository getInstance(RemoteRepository remoteRepository, BusDao busDao) {
		if (sInstance == null) {
			sInstance = new BusRepository(remoteRepository, busDao);
		}
		return sInstance;
	}

	private BusRepository(RemoteRepository remoteRepository, BusDao busDao) {
		this.remoteRepository = remoteRepository;
		this.busDao = busDao;
	}

	public LiveData<BusEntity> load(long busId) {
		fetchBus(busId, new RemoteBusListener() {
			@Override
			public void onRemoteBusReceived(BusEntity busEntity) {
				insertTask(busEntity);
			}

			@Override
			public void onRemoteBusesReceived(List<BusEntity> busEntities) {
				// No-op
			}
		});
		return busDao.loadBus(busId);
	}

	public void loadNew(long busId) {
		fetchBus(busId, new RemoteBusListener() {
			@Override
			public void onRemoteBusReceived(final BusEntity busEntity) {
				insertTask(busEntity);
				// TODO: 07/06/2017 Remove all entities not in param busEntities
			}

			@Override
			public void onRemoteBusesReceived(final List<BusEntity> busEntities) {
				// No-op
			}
		});
	}

	/**
	 * Returns an observable for ALL the buses table changes
	 */
	public LiveData<List<BusEntity>> loadList() {
		fetchBusList(new RemoteBusListener() {
			@Override
			public void onRemoteBusReceived(BusEntity busEntity) {
				// No-op
			}

			@Override
			public void onRemoteBusesReceived(List<BusEntity> busEntities) {
				insertAllTask(busEntities);
			}
		});
		return busDao.loadAll();
	}

	public void loadListNew() {
		fetchBusList(new RemoteBusListener() {
			@Override
			public void onRemoteBusReceived(BusEntity busEntity) {
				// No-op
			}

			@Override
			public void onRemoteBusesReceived(final List<BusEntity> busEntities) {
				insertAllTask(busEntities);
				// TODO: 07/06/2017 Remove all entities not in param busEntities
			}
		});
	}

	/**
	 * Makes a call to the webservice. Keep it private since the view/viewModel should be 100% abstracted
	 * from the data source implementation.
	 */
	private void fetchBus(long busId, @NonNull final RemoteBusListener listener) {
		remoteRepository.getBus(busId, new RemoteCallback<BusEntity>() {
			@Override
			public void onSuccess(BusEntity response) {
				listener.onRemoteBusReceived(response);
			}

			@Override
			public void onUnauthorized() {
				// TODO: 25/05/2017 Report this to the view
			}

			@Override
			public void onFailed(Throwable throwable) {
				// TODO: 25/05/2017 Report this to the view
			}
		});
	}


	/**
	 * Makes a call to the webservice. Keep it private since the view/viewModel should be 100% abstracted
	 * from the data source implementation.
	 */
	private void fetchBusList(@NonNull final RemoteBusListener listener) {
		remoteRepository.getAllBuses(new RemoteCallback<List<BusEntity>>() {
			@Override
			public void onSuccess(List<BusEntity> response) {
				listener.onRemoteBusesReceived(response);
			}

			@Override
			public void onUnauthorized() {
				// TODO: 25/05/2017 Report this to the view
			}

			@Override
			public void onFailed(Throwable throwable) {
				// TODO: 25/05/2017 Report this to the view
			}
		});
	}

	private void insertTask(final BusEntity busEntity) {
		insertTask(busEntity, null);
	}

	private void insertTask(final BusEntity busEntity, @Nullable final TaskListener listener) {
		new AsyncTask<Context, Void, Void>() {
			@Override
			protected Void doInBackground(Context... params) {
				busDao.insert(busEntity);
				return null;
			}

			@Override
			protected void onPostExecute(Void aVoid) {
				super.onPostExecute(aVoid);
				if (listener != null) {
					listener.onTaskFinished();
				}
			}
		}.execute(App.getContext());
	}

	private void insertAllTask(final List<BusEntity> busEntity) {
		insertAllTask(busEntity, null);
	}

	private void insertAllTask(final List<BusEntity> busEntity,
	                           @Nullable final TaskListener listener) {
		new AsyncTask<Context, Void, Void>() {
			@Override
			protected Void doInBackground(Context... params) {
				busDao.insertAll(busEntity);
				return null;
			}

			@Override
			protected void onPostExecute(Void aVoid) {
				super.onPostExecute(aVoid);
				if (listener != null) {
					listener.onTaskFinished();
				}
			}
		}.execute(App.getContext());
	}

	private void deleteTask(final BusEntity busEntity) {
		deleteTask(busEntity, null);
	}

	private void deleteTask(final BusEntity busEntity, @Nullable final TaskListener listener) {
		new AsyncTask<Context, Void, Void>() {
			@Override
			protected Void doInBackground(Context... params) {
				busDao.delete(busEntity);
				return null;
			}

			@Override
			protected void onPostExecute(Void aVoid) {
				super.onPostExecute(aVoid);
				if (listener != null) {
					listener.onTaskFinished();
				}
			}
		}.execute(App.getContext());
	}

	private void deleteAllTask() {
		deleteAllTask(null);
	}

	private void deleteAllTask(@Nullable final TaskListener listener) {
		new AsyncTask<Context, Void, Void>() {
			@Override
			protected Void doInBackground(Context... params) {
				busDao.deleteAll();
				return null;
			}

			@Override
			protected void onPostExecute(Void aVoid) {
				super.onPostExecute(aVoid);
				if (listener != null) {
					listener.onTaskFinished();
				}
			}
		}.execute(App.getContext());
	}

	interface TaskListener {
		void onTaskFinished();
	}

	interface RemoteBusListener {
		void onRemoteBusReceived(BusEntity busEntity);

		void onRemoteBusesReceived(List<BusEntity> busEntities);
	}
}
Take this code with a grain of salt, it might not be 100% correct, and it’s quite complex, for example it could make sense to have a separate class to do Local Database (Room) operations.

The repository returns LiveData that is directly linked to the database, keeping it as the single source of truth. As mentioned before, we’re using the observable pattern so we’re sure every change gets propagated up the stream, preventing re-queries to the database.

Also notice how the responsibilities are, once again, delegated to the respective classes. The repository only gets the busDao, the only way to get database access, and the remote repository, the entity that deals with all the webservice request/response handling logic.

Finally our simple BusListActivity → the LifecycleOwner


public class BusListActivity extends AppCompatActivity implements LifecycleRegistryOwner,
		BusListAdapter.ClickListener {

	private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);

	private BusListViewModel viewModel;

	private BusListAdapter adapter;
	private ContentLoadingProgressBar progressDialogView;
	private SwipeRefreshLayout swipeToRefreshView;

	@Override
	public LifecycleRegistry getLifecycle() {
		return this.lifecycleRegistry;
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_bus_list);
		initViews();
		initViewModel();
	}

	private void initViews() {
		Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
		toolbar.setTitle(R.string.title_bus_list_activity);
		setSupportActionBar(toolbar);

		RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
		recyclerView.setHasFixedSize(true);
		recyclerView.setLayoutManager(new LinearLayoutManager(this));
		recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
		adapter = new BusListAdapter(this);
		recyclerView.setAdapter(adapter);

		progressDialogView = (ContentLoadingProgressBar) findViewById(R.id.progress_dialog);

		swipeToRefreshView = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh);
		swipeToRefreshView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
			@Override
			public void onRefresh() {
				viewModel.onPullRequested();
			}
		});

		FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
		fab.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				viewModel.onFabButtonClicked();
			}
		});
	}

	private void initViewModel() {
		viewModel = ViewModelProviders.of(this).get(BusListViewModel.class);
		subscribeDataStreams(viewModel);
	}

	private void subscribeDataStreams(BusListViewModel viewModel) {
		viewModel.getBusList().observe(this, new Observer<List<BusEntity>>() {
			@Override
			public void onChanged(@Nullable List<BusEntity> busEntities) {
				if (busEntities != null) {
					hideProgress();
					showBusListInUi(busEntities);
				} else {
					showProgress();
				}
			}
		});
	}

	private void showBusListInUi(List<BusEntity> busEntities) {
		adapter.setItems(busEntities);
	}

	private void showProgress() {
		if (adapter.isEmpty() && !progressDialogView.isShown()) {
			progressDialogView.setVisibility(View.VISIBLE);
		}
	}

	private void hideProgress() {
		if (swipeToRefreshView.isRefreshing()) {
			swipeToRefreshView.setRefreshing(false);
		}

		if (progressDialogView.isShown()) {
			progressDialogView.setVisibility(View.GONE);
		}
	}

	@Override
	public void onItemClicked(BusEntity bus) {
		viewModel.onListItemClicked(bus);
		openBusDetailActivity(bus);
	}

	private void openBusDetailActivity(BusEntity bus) {
		BusProfileActivity.start(this, bus.getId());
	}
}

Also notice I’m not extending the documentation referred LifecycleActivity. This was actually a test, and it works! Most on-going projects will probably have a big difficulty to extend a new classes (LifecycleActivity/LifecycleFragment), so this solution seems to work, just copy the LifecycleRegistry (gist’s line 5) and the getLifecycle() method.


For more examples look at the GitHub repository, there’s also Room code there, I didn’t dwell much on it here. Imo it is a whole other chapter on itself.

For any other details code related you can always ask here or hit me up on Twitter. I don’t think the focus of this article should be the LoC but the implementation of these new awesome tools, so I didn’t shown a lot of code, there are other articles here on medium who focused more on that.

I highly recommend you to watch the resources posted bellow 👇!

Resources

Share