Guide To App Architecture - Android Developers
Guide To App Architecture - Android Developers
This page assumes a basic familiarity with the Android Framework. If you are new to Android app development, check Cache data
out our Developer guides to get started and learn more about the concepts mentioned in this guide. Persist data
Show in-progress operations
If you're interested in app architecture, and would like to see the material in this guide from a Kotlin programming Test each component
perspective, check out the Udacity course Developing Android Apps with Kotlin. Best practices
Addendum: exposing network status
In the majority of cases, desktop apps have a single entry point from a desktop or program launcher, then run as a single,
monolithic process. Android apps, on the other hand, have a much more complex structure. A typical Android app
contains multiple app components, including activities, fragments, services, content providers, and broadcast receivers.
You declare most of these app components in your app manifest. The Android OS then uses this le to decide how to
integrate your app into the device's overall user experience. Given that a properly-written Android app contains multiple
components and that users often interact with multiple apps in a short period of time, apps need to adapt to different
kinds of user-driven work ows and tasks.
For example, consider what happens when you share a photo in your favorite social networking app:
1. The app triggers a camera intent. The Android OS then launches a camera app to handle the request. At this point,
the user has left the social networking app, but their experience is still seamless.
2. The camera app might trigger other intents, like launching the le chooser, which may launch yet another app.
3. Eventually, the user returns to the social networking app and shares the photo.
At any point during the process, the user could be interrupted by a phone call or noti cation. After acting upon this
interruption, the user expects to be able to return to, and resume, this photo-sharing process. This app-hopping behavior
is common on mobile devices, so your app must handle these ows correctly.
Keep in mind that mobile devices are also resource-constrained, so at any time, the operating system might kill some app
processes to make room for new ones.
Given the conditions of this environment, it's possible for your app components to be launched individually and out-of-
order, and the operating system or user can destroy them at any time. Because these events aren't under your control,
you shouldn't store any app data or state in your app components, and your app components shouldn't depend on each
other.
If you shouldn't use app components to store app data and state, how should you design your app?
Separation of concerns
The most important principle to follow is separation of concerns . It's a common mistake to write all your code in an
Activity or a Fragment . These UI-based classes should only contain logic that handles UI and operating system
interactions. By keeping these classes as lean as possible, you can avoid many lifecycle-related problems.
Keep in mind that you don't own implementations of Activity and Fragment ; rather, these are just glue classes that
represent the contract between the Android OS and your app. The OS can destroy them at any time based on user
interactions or because of system conditions like low memory. To provide a satisfactory user experience and a more
manageable app maintenance experience, it's best to minimize your dependency on them.
Another important principle is that you should drive your UI from a model, preferably a persistent model. Models are
components that are responsible for handling the data for an app. They're independent from the View objects and app
components in your app, so they're unaffected by the app's lifecycle and the associated concerns.
Your users don't lose data if the Android OS destroys your app to free up resources.
Your app continues to work in cases when a network connection is aky or not available.
By basing your app on model classes with the well-de ned responsibility of managing the data, your app is more testable
and consistent.
In this section, we demonstrate how to structure an app using Architecture Components by working through an end-to-
end use case.
Note: It's impossible to have one way of writing apps that works best for every scenario. That being said, this recommended
architecture is a good starting point for most situations and work ows. If you already have a good way of writing Android
apps that follows the common architectural principles, you don't need to change it.
Imagine we're building a UI that shows a user pro le. We use a private backend and a REST API to fetch the data for a
given pro le.
Overview
To start, consider the following diagram, which shows how all the modules should interact with one another after
designing the app:
Notice that each component depends only on the component one level below it. For example, activities and fragments
depend only on a view model. The repository is the only class that depends on multiple other classes; in this example,
the repository depends on a persistent data model and a remote backend data source.
This design creates a consistent and pleasant user experience. Regardless of whether the user comes back to the app
several minutes after they've last closed it or several days later, they instantly see a user's information that the app
persists locally. If this data is stale, the app's repository module starts updating the data in the background.
The UI consists of a fragment, UserProfileFragment , and its corresponding layout le, user_profile_layout.xml .
To drive the UI, our data model needs to hold the following data elements:
User ID: The identi er for the user. It's best to pass this information into the fragment using the fragment
arguments. If the Android OS destroys our process, this information is preserved, so the ID is available the next
time our app is restarted.
User object: A data class that holds details about the user.
We use a UserProfileViewModel , based on the ViewModel architecture component, to keep this information.
A ViewModel object provides the data for a speci c UI component, such as a fragment or activity, and contains data-
handling business logic to communicate with the model. For example, the ViewModel can call other components to
load the data, and it can forward user requests to modify the data. The ViewModel doesn't know about UI
components, so it isn't affected by con guration changes, such as recreating an activity when rotating the device.
UserProfileViewModel : The class that prepares the data for viewing in the UserProfileFragment and reacts to
user interactions.
The following code snippets show the starting contents for these les. (The layout le is omitted for simplicity.)
UserProfileViewModel
class UserProfileViewModel : ViewModel() {
val userId : String = TODO()
val user : User = TODO()
}
UserProfileFragment
class UserProfileFragment : Fragment() {
// To use the viewModels() extension function, include
// "androidx.fragment:fragment-ktx:latest-version" in your app
// module's build.gradle file.
private val viewModel: UserProfileViewModel by viewModels()
Now that we have these code modules, how do we connect them? After all, when the user eld is set in the
UserProfileViewModel class, we need a way to inform the UI.
To obtain the user , our ViewModel needs to access the Fragment arguments. We can either pass them from the
Fragment, or better, using the SavedState module, we can make our ViewModel read the argument directly:
Note: SavedStateHandle allows ViewModel to access the saved state and arguments of the associated Fragment or Activity.
// UserProfileViewModel
class UserProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId : String = savedStateHandle["uid"] ?:
throw IllegalArgumentException("missing user id")
val user : User = TODO()
}
// UserProfileFragment
private val viewModel: UserProfileViewModel by viewModels(
factoryProducer = { SavedStateVMFactory(this) }
...
)
Now we need to inform our Fragment when the user object is obtained. This is where the LiveData architecture
component comes in.
LiveData is an observable data holder. Other components in your app can monitor changes to objects using this holder
without creating explicit and rigid dependency paths between them. The LiveData component also respects the lifecycle
state of your app's components—such as activities, fragments, and services—and includes cleanup logic to prevent
object leaking and excessive memory consumption.
Note: If you're already using a library like RxJava, you can continue using them instead of LiveData. When you use libraries
and approaches like these, however, make sure you handle your app's lifecycle properly. In particular, make sure to pause
your data streams when the related LifecycleOwner is stopped and to destroy these streams when the related
LifecycleOwner is destroyed. You can also add the android.arch.lifecycle:reactivestreams artifact to use LiveData
with another reactive streams library, such as RxJava2.
To incorporate the LiveData component into our app, we change the eld type in the UserProfileViewModel to
LiveData<User> . Now, the UserProfileFragment is informed when the data is updated. Furthermore, because this
LiveData eld is lifecycle aware, it automatically cleans up references after they're no longer needed.
UserProfileViewModel
class UserProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId : String = savedStateHandle["uid"] ?:
throw IllegalArgumentException("missing user id")
val user : LiveData<User> = TODO()
}
Now we modify UserProfileFragment to observe the data and update the UI:
UserProfileFragment
Every time the user pro le data is updated, the onChanged() callback is invoked, and the UI is refreshed.
If you're familiar with other libraries where observable callbacks are used, you might have realized that we didn't override
the fragment's onStop() method to stop observing the data. This step isn't necessary with LiveData because it's
lifecycle aware, which means it doesn't invoke the onChanged() callback unless the fragment is in an active state; that
is, it has received onStart() but hasn't yet received onStop() ). LiveData also automatically removes the observer
when the fragment's onDestroy() method is called.
We also didn't add any logic to handle con guration changes, such as the user rotating the device's screen. The
UserProfileViewModel is automatically restored when the con guration changes, so as soon as the new fragment is
created, it receives the same instance of ViewModel , and the callback is invoked immediately using the current data.
Given that ViewModel objects are intended to outlast the corresponding View objects that they update, you shouldn't
include direct references to View objects within your implementation of ViewModel . For more information about the
lifetime of a ViewModel corresponds to the lifecycle of UI components, see The lifecycle of a ViewModel.
Fetch data
Now that we've used LiveData to connect the UserProfileViewModel to the UserProfileFragment , how can we fetch
the user pro le data?
For this example, we assume that our backend provides a REST API. We use the Retro t library to access our backend,
though you are free to use a different library that serves the same purpose.
Webservice
interface Webservice {
/**
* @GET declares an HTTP GET request
* @Path("user") annotation on the userId parameter marks it as a
* replacement for the {user} placeholder in the @GET path
*/
@GET("/users/{user}")
fun getUser(@Path("user") userId: String): Call<User>
}
A rst idea for implementing the ViewModel might involve directly calling the Webservice to fetch the data and assign
this data to our LiveData object. This design works, but by using it, our app becomes more and more di cult to
maintain as it grows. It gives too much responsibility to the UserProfileViewModel class, which violates the
separation of concerns principle. Additionally, the scope of a ViewModel is tied to an Activity or Fragment lifecycle,
which means that the data from the Webservice is lost when the associated UI object's lifecycle ends. This behavior
creates an undesirable user experience.
Instead, our ViewModel delegates the data-fetching process to a new module, a repository.
Repository modules handle data operations. They provide a clean API so that the rest of the app can retrieve this data
easily. They know where to get the data from and what API calls to make when data is updated. You can consider
repositories to be mediators between different data sources, such as persistent models, web services, and caches.
Our UserRepository class, shown in the following code snippet, uses an instance of WebService to fetch a user's
data:
UserRepository
class UserRepository {
private val webservice: Webservice = TODO()
// ...
fun getUser(userId: String): LiveData<User> {
// This isn't an optimal implementation. We'll fix it later.
val data = MutableLiveData<User>()
webservice.getUser(userId).enqueue(object : Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
data.value = response.body()
}
// Error case is left out for brevity.
override fun onFailure(call: Call<User>, t: Throwable) {
TODO()
}
})
return data
}
}
Even though the repository module looks unnecessary, it serves an important purpose: it abstracts the data sources from
the rest of the app. Now, our UserProfileViewModel doesn't know how the data is fetched, so we can provide the view
model with data obtained from several different data-fetching implementations.
Note: We've left out the network error case for the sake for simplicity. For an alternative implementation that exposes errors
and loading status, see Addendum: exposing network status.
The UserRepository class above needs an instance of Webservice to fetch the user's data. It could simply create the
instance, but to do that, it also needs to know the dependencies of the Webservice class.
Additionally, UserRepository is probably not the only class that needs a Webservice . This situation requires us to
duplicate code, as each class that needs a reference to Webservice needs to know how to construct it and its
dependencies. If each class creates a new WebService , our app could become very resource heavy.
You can use the following design patterns to address this problem:
Dependency injection (DI) : Dependency injection allows classes to de ne their dependencies without
constructing them. At runtime, another class is responsible for providing these dependencies. We recommend the
Dagger 2 library for implementing dependency injection in Android apps. Dagger 2 automatically constructs objects
by walking the dependency tree, and it provides compile-time guarantees on dependencies.
Service locator : The service locator pattern provides a registry where classes can obtain their dependencies
instead of constructing them.
It's easier to implement a service registry than use DI, so if you aren't familiar with DI, use the service locator pattern
instead.
These patterns allow you to scale your code because they provide clear patterns for managing dependencies without
duplicating code or adding complexity. Furthermore, these patterns allow you to quickly switch between test and
production data-fetching implementations.
Our example app uses Dagger 2 to manage the Webservice object's dependencies.
UserProfileViewModel
Cache data
The UserRepository implementation abstracts the call to the Webservice object, but because it relies on only one
data source, it's not very exible.
The key problem with the UserRepository implementation is that after it fetches data from our backend, it doesn't
store that data anywhere. Therefore, if the user leaves the UserProfileFragment , then returns to it, our app must re-
fetch the data, even if it hasn't changed.
To address these shortcomings, we add a new data source to our UserRepository , which caches the User objects in
memory:
UserRepository
Persist data
Using our current implementation, if the user rotates the device or leaves and immediately returns to the app, the existing
UI becomes visible instantly because the repository retrieves data from our in-memory cache.
However, what happens if the user leaves the app and comes back hours later, after the Android OS has killed the
process? By relying on our current implementation in this situation, we need to fetch the data again from the network.
This refetching process isn't just a bad user experience; it's also wasteful because it consumes valuable mobile data.
You could x this issue by caching the web requests, but that creates a key new problem: What happens if the same user
data shows up from another type of request, such as fetching a list of friends? The app would show inconsistent data,
which is confusing at best. For example, our app might show two different versions of the same user's data if the user
made the list-of-friends request and the single-user request at different times. Our app would need to gure out how to
merge this inconsistent data.
The proper way to handle this situation is to use a persistent model. This is where the Room persistence library comes to
the rescue.
Room is an object-mapping library that provides local data persistence with minimal boilerplate code. At compile time, it
validates each query against your data schema, so broken SQL queries result in compile-time errors instead of runtime
failures. Room abstracts away some of the underlying implementation details of working with raw SQL tables and
queries. It also allows you to observe changes to the database's data, including collections and join queries, exposing
such changes using LiveData objects. It even explicitly de nes execution constraints that address common threading
issues, such as accessing storage on the main thread.
Note: If your app already uses another persistence solution, such as a SQLite object-relational mapping (ORM), you don't
need to replace your existing solution with Room. However, if you're writing a new app or refactoring an existing app, we
recommend using Room to persist your app's data. That way, you can take advantage of the library's abstraction and query
validation capabilities.
To use Room, we need to de ne our local schema. First, we add the @Entity annotation to our User data model class
and a @PrimaryKey annotation to the class's id eld. These annotations mark User as a table in our database and
id as the table's primary key:
User
@Entity
data class User(
@PrimaryKey private val id: String,
private val name: String,
private val lastName: String
)
UserDatabase
Notice that UserDatabase is abstract. Room automatically provides an implementation of it. For details, see the Room
documentation.
We now need a way to insert user data into the database. For this task, we create a data access object (DAO).
UserDao
@Dao
interface UserDao {
@Insert(onConflict = REPLACE)
fun save(user: User)
Notice that the load method returns an object of type LiveData<User> . Room knows when the database is modi ed
and automatically noti es all active observers when the data changes. Because Room uses LiveData, this operation is
e cient; it updates the data only when there is at least one active observer.
Note: Room checks invalidations based on table modi cations, which means it may dispatch false positive noti cations.
With our UserDao class de ned, we then reference the DAO from our database class:
UserDatabase
Now we can modify our UserRepository to incorporate the Room data source:
companion object {
val FRESH_TIMEOUT = TimeUnit.DAYS.toMillis(1)
}
}
Notice that even though we changed where the data comes from in UserRepository , we didn't need to change our
UserProfileViewModel or UserProfileFragment . This small-scoped update demonstrates the exibility that our
app's architecture provides. It's also great for testing, because we can provide a fake UserRepository and test our
production UserProfileViewModel at the same time.
If users wait a few days before returning to an app that uses this architecture, it's likely that they'll see out-of-date
information until the repository can fetch updated information. Depending on your use case, you may not want to show
this out-of-date information. Instead, you can display placeholder data, which shows dummy values and indicates that
your app is currently fetching and loading up-to-date information.
It's common for different REST API endpoints to return the same data. For example, if our backend has another endpoint
that returns a list of friends, the same user object could come from two different API endpoints, maybe even using
different levels of granularity. If the UserRepository were to return the response from the Webservice request as-is,
without checking for consistency, our UIs could show confusing information because the version and format of data
from the repository would depend on the endpoint most recently called.
For this reason, our UserRepository implementation saves web service responses into the database. Changes to the
database then trigger callbacks on active LiveData objects. Using this model, the database serves as the single source
of truth, and other parts of the app access it using our UserRepository . Regardless of whether you use a disk cache,
we recommend that your repository designate a data source as the single source of truth for the rest of your app.
In some use cases, such as pull-to-refresh, it's important for the UI to show the user that there's currently a network
operation in progress. It's good practice to separate the UI action from the actual data because the data might be
updated for various reasons. For example, if we fetched a list of friends, the same user might be fetched again
programmatically, triggering a LiveData<User> update. From the UI's perspective, the fact that there's a request in ight
is just another data point, similar to any other piece of data in the User object itself.
We can use one of the following strategies to display a consistent data-updating status in the UI, regardless of where the
request to update the data came from:
Change getUser() to return an object of type LiveData . This object would include the status of the network
operation.
For an example, see the NetworkBoundResource implementation in the android-architecture-components GitHub
project.
Provide another public function in the UserRepository class that can return the refresh status of the User . This
option is better if you want to show the network status in your UI only when the data-fetching process originated
from an explicit user action, such as pull-to-refresh.
In the separation of concerns section, we mentioned that one key bene t of following this principle is testability.
The following list shows how to test each code module from our extended example:
User interface and interactions: Use an Android UI instrumentation test. The best way to create this test is to use
the Espresso library. You can create the fragment and provide it a mock UserProfileViewModel . Because the
fragment communicates only with the UserProfileViewModel , mocking this one class is su cient to fully test
your app's UI.
ViewModel: You can test the UserProfileViewModel class using a JUnit test. You only need to mock one class,
UserRepository .
UserRepository: You can test the UserRepository using a JUnit test, as well. You need to mock the
Webservice and the UserDao . In these tests, verify the following behavior:
The repository doesn't make unnecessary requests if the data is cached and up to date.
Because both Webservice and UserDao are interfaces, you can mock them or create fake implementations for
more complex test cases.
UserDao: Test DAO classes using instrumentation tests. Because these instrumentation tests don't require any UI
components, they run quickly. For each test, create an in-memory database to ensure that the test doesn't have any
side effects, such as changing the database les on disk.
Caution:Room allows specifying the database implementation, so it's possible to test your DAO by providing the JUnit
implementation of SupportSQLiteOpenHelper. This approach isn't recommended, however, because the SQLite
version running on the device might differ from the SQLite version on your development machine.
Webservice: In these tests, avoid making network calls to your backend. It's important for all tests, especially web-
based ones, to be independent from the outside world. Several libraries, including MockWebServer, can help you
create a fake local server for these tests.
Testing Artifacts: Architecture Components provides a maven artifact to control its background threads. The
androidx.arch.core:core-testing artifact contains the following JUnit rules:
InstantTaskExecutorRule : Use this rule to instantly execute any background operation on the calling
thread.
Best practices
Programming is a creative eld, and building Android apps isn't an exception. There are many ways to solve a problem,
be it communicating data between multiple activities or fragments, retrieving remote data and persisting it locally for
o ine mode, or any number of other common scenarios that nontrivial apps encounter.
Although the following recommendations aren't mandatory, it has been our experience that following them makes your
code base more robust, testable, and maintainable in the long run:
Avoid designating your app's entry points—such as activities, services, and broadcast receivers—as sources of data.
Instead, they should only coordinate with other components to retrieve the subset of data that is relevant to that entry
point. Each app component is rather short-lived, depending on the user's interaction with their device and the overall
current health of the system.
Create well-de ned boundaries of responsibility between various modules of your app.
For example, don't spread the code that loads data from the network across multiple classes or packages in your code
base. Similarly, don't de ne multiple unrelated responsibilities—such as data caching and data binding—into the same
class.
Don't be tempted to create "just that one" shortcut that exposes an internal implementation detail from one module. You
might gain a bit of time in the short term, but you then incur technical debt many times over as your codebase evolves.
For example, having a well-de ned API for fetching data from the network makes it easier to test the module that
persists that data in a local database. If, instead, you mix the logic from these two modules in one place, or distribute
your networking code across your entire code base, it becomes much more di cult—if not impossible—to test.
Focus on the unique core of your app so it stands out from other apps.
Don't reinvent the wheel by writing the same boilerplate code again and again. Instead, focus your time and energy on
what makes your app unique, and let the Android Architecture Components and other recommended libraries handle the
repetitive boilerplate.
That way, users can enjoy your app's functionality even when their device is in o ine mode. Remember that not all of
your users enjoy constant, high-speed connectivity.
Whenever your app needs to access this piece of data, it should always originate from this single source of truth.
In the recommended app architecture section above, we omitted network error and loading states to keep the code
snippets simple.
This section demonstrates how to expose network status using a Resource class that encapsulate both the data and
its state.
Resource
// A generic class that contains data and status about loading this data.
sealed class Resource<T>(
val data: T? = null,
val message: String? = null
) {
class Success<T>(data: T) : Resource<T>(data)
class Loading<T>(data: T? = null) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
}
Because it's common to load data from the network while showing the disk copy of that data, it's good to create a helper
class that you can reuse in multiple places. For this example, we create a class called NetworkBoundResource .
It starts by observing the database for the resource. When the entry is loaded from the database for the rst time,
NetworkBoundResource checks whether the result is good enough to be dispatched or that it should be re-fetched from
the network. Note that both of these situations can happen at the same time, given that you probably want to show
cached data while updating it from the network.
If the network call completes successfully, it saves the response into the database and re-initializes the stream. If
network request fails, the NetworkBoundResource dispatches a failure directly.
Note: After saving new data to disk, we re-initialize the stream from the database. We usually don't need to do that, however,
because the database itself happens to dispatch the change.
Keep in mind that relying on the database to dispatch the change involves relying on the associated side effects, which isn't
good because unde ned behavior from these side effects could occur if the database ends up not dispatching changes
because the data hasn't changed.
Also, don't dispatch the result that arrived from the network because that would violate the single source of truth principle.
After all, maybe the database includes triggers that change data values during a "save" operation. Similarly, don't dispatch
`SUCCESS` without the new data, because then the client receives the wrong version of the data.
The following code snippet shows the public API provided by NetworkBoundResource class for its subclasses:
NetworkBoundResource.kt
// Called when the fetch fails. The child class may want to reset components
// like rate limiter.
protected open fun onFetchFailed() {}
It de nes two type parameters, ResultType and RequestType , because the data type returned from the API
might not match the data type used locally.
It uses a class called ApiResponse for network requests. ApiResponse is a simple wrapper around the
Retrofit2.Call class that convert responses to instances of LiveData .
The full implementation of the NetworkBoundResource class appears as part of the android-architecture-components
GitHub project .
After creating the NetworkBoundResource , we can use it to write our disk- and network-bound implementations of
User in the UserRepository class:
UserRepository
Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its a liates.
Twitter YouTube
Follow @AndroidDev on Check out Android Developers
Twitter on YouTube
Android Studio
Privacy | License | Brand guidelines Get news and tips by email SUBSCRIBE ENGLISH