Android App Architecture Demo - MVVM with databinding (Java version) [Kotlin version] [中文版]
This is a sample project to demonstrate the Android application architecture with MVVM pattern, a simple client for The Movie DB web API.
Note: you will need an API key if you would like to run the application, see Get started.
- Why MVVM?
- Screenshots
- Application Introduction
- Application Architecture
- Get started
- Reusable Components
- External Libraries/Frameworks/Widgets
- Code Quality
- Notes
- License
Why MVVM?
For client application development, MVVM is better than other MV* patterns like MVC or MVP.
Why? Because a view model, as the extra abstraction of the view's data and behavior, has a higher abstraction level than other MV* patterns. Furthermore, view models are totally decoupled from views through databinding. The higher abstraction level and more complete decoupling lead to a cleaner architecture.
Any benefit comes with a cost, the abstraction and separation are not free. For projects that are complex enough, these initial cost will pay off soon. But for simple applications, it may be overkill.
For this demo project, using MVVM seems a little bit over engineering. But my purpose here is to demonstrate with a working example that is not too simple, and dealing with real problems in a real project. Furthermore, for a project whose goal is a full featured application like the client for "The Movie DB", this will be a good start.
The application has the following features:
- The main page has three tabs: Now Playing, Favorite and Settings.
- The Now Playing tab displays the movies now playing.
- Each movie is shown as a poster picture in the grid, with rating stars and favorite state icon on top of it.
- You can pull down the list to refresh and pull up at the end to load the next page.
- The Favorite tab shows the local favorite list and has the same functionality as the Now Playing tab.
- Clicking on the movie poster will navigate to the movie details page, in which additional details like backdrop image, tagline and similar movies will be shown.
- The float action button in the page shows a progress circle animation while loading and the favorite state icons will show after loading. You can click the favorite icon to add to or remove from local favorite movie list.
- You can pull up the similar movies list to load the next page.
- In settings page, you can clear the cached HTTP responses, images, as well as local favorites.
- Dependency
- Unidirectional dependency from top-down. Lower layers communicate with upper layers through various notification means:
- View Models notify UI through databinding.
- Models notify View Models through observable notification events.
- Repository layer just returns values through RxJava event sources to Models.
- Dependencies are injected by the DI (dependency injection) framework. View Models layer and Models layer components are all unit testable.
- Unidirectional dependency from top-down. Lower layers communicate with upper layers through various notification means:
- View models layer should have no dependency on UI components, and depend on Android as less as possible. (Current dependencies are the Android databinding framework, AndroidSchedulers from RxAndroid, and SparseArray.)
- Models layer is the domain model for the application and should be UI and platform independent, i.e. depend on neither the interaction/UI design nor the Android platform. (Currently it depends on SparseArray just for performance reason, and can be replaced if needed.)
- Repository layer is the abstraction for data access to local storage (Shared Preferences, SQLite databases) and external Web APIs.
Most of the classe names are obvious. For those not so obvious:
TmdbConfig
: the class used to get the TMDb configurations. See https://developers.themoviedb.org/3/configuration/get-api-configuration.IImageConfig
is the interface for View Models to get image path configurations.EntityStore
: the object store to hold the (weak) references of model layer entities.DataCleaner
: use to clear application cache for HTTP, image as well as local database.IConfigStore
: used to access configuration storage.IFavoriteStore
: used to access favorite movies.IMovieDbService
: used to access the Web API of "The Movie DB".
- Model-View-ViewModel (MVVM) architecture: which takes advantage of the native Android data-binding support.
- Decouple with Dependency Injection: using Dagger-2.
- Asynchrony: I/O operations should run in background with RxJava+RxAndroid.
- Activity navigation: URI based, decouple the activities. Implemented in NavigationHelper.
- Object Lifecycle
- Entities in model layer are saved in the EntityStore (as WeakReferences to prevent memory leaks) which ensures the uniqueness (one object instance for each movie) so that different views of the same entity can be kept in sync through change notifications.
- View models have the same lifetime as the corresponding views. They have one to one mapping to the views displayed. Otherwise the different lifetime will be very confusing and cause more problems than benefits (believe me, I tried that in the beginning).
As the development view of the application architecture, it is an essential part to have a clear separation into modules and directory structure (packages in Java).
- Modules division
- app: main application module.
- lib-common: common functionality
- lib-databinding: databinding support
- lib-widgets: reusable UI widgets
- Directory structure in App module
- di: dependency injection
- components: Component and Subcomponent for Dagger.
- modules: Module for Dagger.
- qualifiers: qualification specifiers for dependency injection.
- models: Model layer classes.
- repository: data access layer
- data: value objects used for JSON deserialization.
- local: local storage, including SharedPreferences and SQLite databases.
- util: utility classes.
- web: to access Web API of TMDb.
- ui: UI layer classes.
- activity: Activities in the application.
- databinding: BindingAdapters for databinding.
- fragment: Fragments in the application.
- nav: utility class NavigationHelper.
- view: application related UI controls.
- viewmodels: View Model layer classes.
- di: dependency injection
- Directory structure in Common module
- objstore: object store
- observable: observable pattern implementation for objects and collections.
- util: utility classes.
- Directory structure in Databinding module
- adapter: list adapter for RecyclerView, supporting binding to ObservableList.
- message: display notifications through databinding, so that view model can show notifications (Toast for now) without depending on UI controls.
- Directory structure in Widgets module
- behaviors: behaviors for CoordinatorLayout.
- utils: utility classes. ImageLoader for now.
- widgets: project independent, reusable UI controls.
Since the project has a submodule, you need to clone with --recurse-submodules
parameter, or run git submodule update --init --recursive
later to clone also the submodules.
Before you can run the application, you need to register a developer account following the TMDb introductions and get the API key. Then add the API key in the project's gradle.properties
file:
# API Key for the TMDb API
API_KEY="xxxxx"
Reference: https://developers.themoviedb.org/3/getting-started/authentication
Project independent reusable components are developed in separate modules.
- Observables: enable obsever registrations (with weak references) and event notifications.
- ObjectStore and ModelObjectStore: thread-safe object store which ensures that only one object will be associated with one key.
- RecyclerViewDatabindingAdapter: RecyclerView adapter which support data binding to an ObservableList.
- HeaderedRecyclerViewDatabindingAdapter: RecyclerView adapter which support data binding to an ObservableList, with a header binding to an object outside the list.
- DynamicGridView: RecyclerView based Grid view that can automatically adjust number of columns based on available width and specified cell width. Gaps between cells are carefully caculated so that they are spanned evenly, while the header can occupy the full width.
- FixedAspectRatioImage: AppCompatImageView with fixed aspect ratio (set through an attribut).
- ImageLoader: load image with Glide. Can load image after the ImageView is measured.
- AutoHideWhenScrollDownBehavior: A CoordinatorLayout behavior for the target view (e.g.
BottomNavigationView
) to auto hide when scroll down.
The usage of the following well know libraries/frameworks/widgets are demonstrated in this project:
- Dagger-2 for dependency injection, including its convenient AndroidInjector for Android components. It is based on code generation instead of reflection so it works like a charm with code obfuscations like ProGuard for Android.
- RFP (Reactive Functional Programming) and Asynchrony: RxJava with RxAndroid.
- Restful client: Retrofit + OkHttp + RxJava + GSON.
- ORM framework: Room
- Widgets:
- Image loading with Glide
- View binding: Butterknife, used to avoid
findViewById
. - Unit test frameworks:
- Mockito mock framework
- Robolectric unit test framework which run unit tests depending on Android SDK, locally.
- Browser SQLite databases and SharedPreferences: Android-Debug-Database
In order to ensure the code quality, the following project is use (as a submodule) in this application:
- Static code analysis using CheckStyle, FindBugs, Lint and PMD: Android-Quality-Essentials.
The signing configs comes from the project's gradle.properties
file. You should add the following if you would like to sign the APK with your own key:
# signingConfigs for release build
RELEASE_STORE_FILE=xxx.xxx
RELEASE_STORE_PASSWORD=xxx
RELEASE_KEY_ALIAS=xxx
RELEASE_KEY_PASSWORD=xxx
Copyright (C) 2018, Brian He
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.