Application Architecture guide
Introduction – Mobile application user experience
In most cases, an App has only one entry point from the desktop or application launcher and then runs as a single overall process. Android applications, on the other hand, have a more complex structure. A typical Android application contains multiple application components, including Activities, fragments, Services, Content providers, and Broadcast Receivers
You declare most of these application components in app Manifest. The Android operating system then uses this file to determine how to integrate your application into the overall user experience of the device. Given that properly written Android applications contain multiple components and that users often interact with multiple applications in a short period of time, applications need to accommodate different types of user-driven workflows and tasks.
For example, what happens when you consider sharing photos in your favorite social networking app:
-
The application triggers camera intent. The Android operating system launches the camera application to process the request. At this point, the user has left the social networking application, but their experience is still seamless.
-
The camera application may trigger other intents, such as launching a file selector, which may launch another application.
-
Eventually, the user returns to the social networking application and shares photos.
At any time during this process, the user may be interrupted by a call or notification. After taking action on this interruption, the user wants to be able to go back and resume the photo sharing process. This application jumping behavior is common on mobile devices, so your application must handle it correctly.
Keep in mind that mobile devices are also resource-constrained, so at any time the operating system may kill some application processes to make room for new ones.
Given the conditions of this environment, your application components may start separately and out of order, and the operating system or user can destroy them at any time. Because these events are not under your control, you should not store any application data or state in your application components, and your application components should not depend on each other.
Common architectural principles
If you should not use application components to store application data and state, how should you design your application?
Separation of concerns
The most important principle is separation of concerns. It’s a common mistake to write all your code in one Activity or one Fragment. These UI-based classes should only contain logic for handling UI and APP interactions. By keeping these classes lean, you can avoid many life-cycle related problems.
Remember, you don’t have your own implementation of activities and fragments; Instead, these are just glue classes that represent the contract between the Android operating system and the application. The operating system can destroy them at any time based on system conditions such as user interaction or low memory. In order to provide a satisfying user experience and a more manageable application maintenance experience, it is best to rely on them as little as possible.
Drive the UI from the Model
Another important principle is that you should drive the UI from a model, preferably a persistent model. A model is the component responsible for processing application data. They are independent of the objects and application components in the View application, so they are not affected by the application life cycle and related issues.
Persistence is ideal for the following reasons:
- If the Android operating system destroys your application to free up resources, your users won’t lose data.
- If the network connection is unstable or unavailable, your application can still be used.
By basing your applications on model classes with clearly defined data management responsibilities, you make your applications more testable and consistent.
Google recommended application architectures
In this article, we’ll demonstrate how to build an application using Android Jetpack Components by using end-to-end use cases.
Note: It is not possible to write the best application for every situation. Having said that, this recommended architecture is a good starting point for most situations and workflows. If you already have a good way to write Android applications that follow common architectural principles, you don't need to change it.Copy the code
Imagine that we are building a UI that displays a user profile. We use private back-end and REST apis to get the data for a given configuration file.
An overview of the
First, consider the following diagram, which shows how all the modules should interact with each other after the application is designed:
Note that each component depends only on the component at the level below it. For example, activities and fragments depend only on the viewmodel. A repository is the only class that depends on multiple other classes; In this example, the repository relies on a persistent data model and remote back-end data sources.
This design creates a consistent and enjoyable user experience. Whether the user returns to the application a few minutes or a few days after they last closed the application, they will immediately see the user information that the application continues to have locally. If this data becomes obsolete, the application’s repository module will start updating the data from behind the scenes.
Building the User Interface
The UI consists of UserProfileFragment user_profile_layout.xml and the corresponding layout file.
To drive the UI, our data model needs to contain the following data elements:
- User ID: indicates the identifier of a User. It is best to pass this information to the Fragment using the Fragment parameter. If the Android operating system breaks our process, this information is retained, so the ID is available the next time the application is restarted.
- User Object: Data class that contains User details.
We use UserProfileViewModel to store this information in the viewModel-based architecture component.
A ViewModel object provides data for a particular UI component, such as a fragment or activity, and contains the business logic that processes the data 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 does not know about the UI components, so it is not affected by configuration changes, such as recreated activities, when the device is rotated.Copy the code
We now define the following files:
- User_profile.xml: UI layout definition for the screen.
- UserProfileFragment: UI controller that displays data.
- UserProfileViewModel: Class that prepares data for viewing userProfileFragments and responding to user interactions.
The following code snippet shows the starting contents of these files. (The layout file has been omitted for simplicity.)
UserProfileViewModel
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser(a) {
returnuser; }}Copy the code
UserProfileFragment
public class UserProfileFragment extends Fragment {
private static final String UID_KEY = "uid";
private UserProfileViewModel viewModel;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
String userId = getArguments().getString(UID_KEY);
viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
viewModel.init(userId);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.user_profile_layout, container, false); }}Copy the code
Now that we have these code modules, how do we connect them? After all, we need a way to notify the UI when a user sets a field in the UserProfileViewModel class. This is where the LiveData architecture component comes in.
LiveData is an observable data holder. Other components in the application can use this > holder to monitor changes to objects without creating explicit and strict dependency paths between them. The LiveData component also respects the lifecycle state of application components (such as Activities, Fragments, and Services) and includes cleanup logic to prevent object leaks and excessive memory consumption.
Note: If you already use libraries like RxJava or Agera, you can continue to use them instead of LiveData. However, when you use these libraries and methods, make sure you handle your application's life cycle correctly. In particular, make sure that the data flows are paused when the relevant LifecycleOwner content stops and destroyed when the relevant LifecycleOwner content is destroyed. You can also add android. Arch. Lifecycle: reactivestreams components to LiveData reaction with another flow library (such as RxJava2) are used together.Copy the code
To incorporate the LiveData component into our application, we change the field type in the UserProfileViewModel to LiveData. Now, the UserProfileFragment is notified when updating data. In addition, because this LiveData field recognizes the lifecycle, the reference is automatically cleared when it is no longer needed.
UserProfileViewModel
public class UserProfileViewModel extends ViewModel {...//private User user;
private LiveData<User> user;
public LiveData<User> getUser(a) {
returnuser; }}Copy the code
Now we modify the UserProfileFragment to view the data and update the UI:
UserProfileFragment
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// Update UI.
});
}
Copy the code
Each time the user profile data is updated, onChanged() invokes the callback and refreshes the UI.
If you’re familiar with other libraries that use observable callbacks, you may already be aware that we don’t have an onStop() method to override fragments to stop observing data. LiveData does not need this step because it is lifecycle aware, which means that onChanged() will not invoke the callback unless the fragment is active. That is, it has received onStart() but not onStop()). LiveData also automatically removes the observer when calling the Fragment’s onDestroy() method.
Nor did we add any logic to handle configuration changes, such as the user rotating the device’s screen. UserProfileViewModel automatically recovers when the configuration changes, so as soon as a new fragment is created, it receives the same ViewModel instance and invokes the callback immediately with the current data. Since the purpose of ViewModel objects is to override the corresponding view objects they update, you should not include direct references to view objects in your ViewModel implementation. For more information about the ViewModel life cycle that corresponds to the life cycle of UI components, see The ViewModel Life Cycle.
The request data
Now that we have used LiveData to connect the UserProfileViewModel to the UserProfileFragment, how do we get the user profile data?
For this example, let’s assume that our back end provides a REST API. We use the Retrofit library to access our back end, although you are free to use different libraries for the same purpose.
The following is the definition of communication between Webservice and the back end: Webservice
public 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}")
Call<User> getUser(@Path("user") String userId);
}
Copy the code
The first idea to implement the ViewModel might be to call Webservice directly to get the data and assign it to our LiveData object. This design works, but by using it, our application becomes harder and harder to maintain as it grows. It puts too much responsibility on the UserProfileViewModel class, which violates the separation of concerns principle. In addition, the scope of the ViewModel is tied to the Activity or Fragment life cycle, which means that data from the Webservice is lost when the associated UI object’s life cycle ends. This behavior creates a bad user experience.
Instead, our ViewModel delegates the data fetching process to a new module, a repository.
The repository module handles data operations. They provide a clean API so that the rest of the application can easily retrieve this data. They know where to get the data and the API calls to make when updating the data. You can think of repositories as mediators between different data sources, such as persistence Models, Web Services, and Caches.Copy the code
Our UserRepository class (shown in the following code snippet) uses an instance WebService to retrieve user data:
UserRepository
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// This is not an optimal implementation. We'll deal with that later.
final MutableLiveData<User> data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
data.setValue(response.body());
}
// Error cases are omitted for brevity.
});
returndata; }}Copy the code
Even if the repository module seems unnecessary, it serves an important purpose: it abstracts the data source from the rest of the application. Now, our UserProfileViewModel doesn’t know how to get data, so we can provide the viewmodel with data from several different data get implementations.
Note: The network error case has been omitted for simplicity. For alternative implementations of exposing errors and loading state, see appendix: Exposing Network State.Copy the code
Manages dependencies between components
The class above UserRepository requires an instance of a Webservice retrieving user data. It can simply create instances, but to do so it needs to know the dependencies of the Webservice class. Also, UserRepository may not be the only Webservice class that is required. This situation requires us to copy the code, because every class that needs to be referenced needs the Webservice to know how to construct it and its dependencies. If each class creates a new WebService, our application could become very resource-intensive.
You can use the following design patterns to solve this problem:
- Dependency Injection (DI) : Dependency injection allows classes to define their dependencies without constructing them. At run time, another class is responsible for providing these dependencies. We recommend using the Dagger 2 library to implement dependency injection in Android applications. Dagger 2 automatically constructs the object by traversing the dependency tree and provides compile-time guarantee for the dependency.
- Service locator: The service locator pattern provides a registry in which classes can obtain their dependencies rather than construct them.
Implementing a service registry is easier than using dependency injection, so if you are unfamiliar with DI, switch to the service locator pattern.
These patterns allow you to extend code because they provide a clear pattern for managing dependencies without duplicating code or adding complexity. In addition, these modes allow you to quickly switch between test and production data capture implementations.
Our sample application uses the Dagger 2 to manage the dependencies of the Webservice object.
Connect the ViewModel to the repository
Now, we modify our UserProfileViewModel using the UserRepository object:
UserProfileViewModel
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
The Dagger 2 command provides the UserRepository parameter.
@Inject
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(int userId) {
if (this.user ! =null) {
// ViewModel is created on a per-Fragment basis, so the userId
// doesn't change.
return;
}
user = userRepo.getUser(userId);
}
public LiveData<User> getUser(a) {
return this.user; }}Copy the code
Cache data
The UserRepository implementation abstracts calls to Webservice objects, but because it relies on only one data source, it is not very flexible.
The key problem with the UserRepository implementation is that after it gets the data from our back end, it doesn’t store it anywhere. Therefore, if the user leaves the UserProfileFragment and then returns to it, our application must retrieve the data again, even if it has not changed.
This design is not optimal for the following reasons:
- It wastes precious network bandwidth.
- It forces the user to wait for a new query to complete.
To address these shortcomings, we added a new data source to UserRepository that caches User objects in memory:
UserRepository
// Tell Dagger2 that this class should only be constructed once.
@Singleton
public class UserRepository {
private Webservice webservice;
// Simple memory cache. Details have been omitted for brevity.
private UserCache userCache;
public LiveData<User> getUser(int userId) {
LiveData<User> cached = userCache.get(userId);
if(cached ! =null) {
return cached;
}
final MutableLiveData<User> data = new MutableLiveData<>();
userCache.put(userId, data);
// This implementation is still suboptimal, but better than before.
// A complete implementation also requires a corresponding handling error case, which is ignored here
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) { data.setValue(response.body()); }});returndata; }}Copy the code
Continuous storage of data
With our current implementation, if the user rotates the device or leaves and immediately returns to the application, the existing UI is immediately visible because the repository retrieves data from the in-memory cache.
But what happens if the user leaves the application and comes back a few hours later after the Android operating system kills the process? With our current implementation in this case, we need to get the data from the network again. This re-acquisition process isn’t just a bad user experience; This is also wasteful, as it consumes valuable mobile data.
You can solve this problem by caching Web requests, but this creates a critical new problem: what happens if the same user data is displayed from other types of requests, such as getting a friend list? The application will display inconsistent data, which is confusing at best. For example, if a user makes a friend list request and a single user request at different times, our application might display two different versions of the same user’s data. Our application needs to figure out how to merge these inconsistent data.
The correct way to handle this situation is to use a persistence 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 corrupted SQL queries cause compile-time errors rather than run-time failures. The room abstracts out some of the underlying implementation details of using raw SQL tables and queries. It also allows you to observe changes to database data, including collectibles and join queries, and expose these changes using LiveData objects. It even explicitly defines execution constraints that deal with common threading problems, such as accessing storage on the main thread.
Note: If your application already uses another persistence solution, such as SQLite Object Relational Mapping (ORM), there is no need to replace the existing solution with Room. However, if you are writing a new application or refactoring an existing one, we recommend that you use Room to preserve the data for your application. In this way, you can take advantage of the library's abstraction and query validation capabilities.Copy the code
To use Room, we need to define the local schema. First, we add the @Entity annotation to the User data model class and the @PrimaryKey annotation to the ID field of the class. These comments mark User as the table in the database ID and the primary key of the table:
User
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// Getters and setters for fields.
}
Copy the code
We then create the database class by implementing RoomDatabase for our application:
UserDatabase
@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {}Copy the code
Note that UserDatabase is abstract. Room automatically provides its implementation. Refer to the Room documentation for more information.
We now need a way to insert user data into the database. For this task, we create a data access Object (DAO)
UserDao
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(int userId);
}
Copy the code
Notice that the load method returns an object of type, LiveData. Room knows when the database is being modified and automatically notifies all active observers when data changes. Since Room uses LiveData, this is very efficient; It only updates data if there is at least one active observer.
Note: Room fails to check for table modifications, which means it may send false positive notifications.Copy the code
After the UserDao defines our class, we reference the DAO from the database class: UserDatabase
@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {
public abstract UserDao userDao(a);
}
Copy the code
Now we can modify our UserRepository to merge the Room data source:
@Singleton
public class UserRepository {
private final Webservice webservice;
private final UserDao userDao;
private final Executor executor;
@Inject
public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
this.webservice = webservice;
this.userDao = userDao;
this.executor = executor;
}
public LiveData<User> getUser(String userId) {
refreshUser(userId);
Return the LiveData object directly from the database.
return userDao.load(userId);
}
private void refreshUser(final String userId) {
// Run in a background thread.
executor.execute(() -> {
// Check whether user data has been recently obtained.
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if(! userExists) {// Refresh data.
Response<User> response = webservice.getUser(userId).execute();
// Check for errors here.
// Update database. LiveData objects are automatic
// Refresh, so we don't need to do anything else.userDao.save(response.body()); }}); }}Copy the code
Note that even if we change the source of the data, UserRepository, we do not need to change our UserProfileViewModel or UserProfileFragment. This small update demonstrates the flexibility provided by our application architecture. It is also great for testing because we can provide a fake UserRepository of data and test our product UserProfileViewModel at the same time.
If users wait a few days before returning to applications using this architecture, they may see outdated information before the repository can get the updated information. Depending on your usage, you may not want to display this outdated information. Instead, you can display placeholder data that displays dummy values and indicates that your application is currently fetching and loading the latest information.
A single true source
It is common for different REST API endpoints to return the same data. For example, if our back end has another endpoint that returns a list of friends, the same user object might come from two different API endpoints, perhaps even with different levels of granularity. If UserRepository returns the response from the Webservice request as is, without checking for consistency, our UI may display confusing information because the version and format of the data in the repository will depend on the endpoint of the most recent call.
Therefore, our UserRepository implementation stores the Web service response in the database. Changes to the database trigger a callback to the LiveData object. With this model, the database can be used as a single real source that other parts of the application can access using UserRepository. Whether or not you use disk caching, we recommend that your repository specify a data source as the only true source for the rest of your application.
Displays ongoing operations
In some use cases, such as pull-to-refresh, it is important for the UI to show the user that the network operation is currently in progress. It’s good practice to separate UI operations from the actual data, which can be updated for a variety of reasons. For example, if we get a list of friends, we might programmatically get the same user again, triggering LiveData updates. From a UI perspective, the fact that there is a request running is just another data point, similar to any other data in the User object itself.
We can use one of the following strategies to display consistent data update status in the UI, regardless of where the request to update data comes from:
- Change getUser() to return an object of type LiveData. This object will contain the state of the network operation. For an example, see the implementation in the Android-Architecture-Components GitHub project in NetworkBoundResource.
- Provide another public function User in the UserRepository class that returns the refresh status. This option is better if you want to display the network state in the UI only when the data retrieval process is derived from an explicit user action (for example, a pull-to-refresh refresh).
Test each component
In the separation of concerns section, we mentioned that a key benefit of following this principle is testability.
The following list shows how to test each code module from the extended example:
- User interface and interaction: Test using the Android UI tool. The best way to create this test is to use the Espresso library. You can create a fragment and provide it with a mock UserProfileViewModel. Because fragments only communicate with fragments, the UserProfileViewModel emulating this class is sufficient to fully test the application’s UI.
- ViewModel: You can test this class using JUnit tests with UserProfileViewModel. You only need to emulate one class, UserRepository.
- UserRepository: You can also test UserRepository using JUnit Test. You need to emulate the Webservice and UserDao. In these tests, verify the following behavior:
- The repository makes the correct Web service call.
- The repository saves the results to the database.
- If the data is cached and up to date, the repository does not make unnecessary requests.
Because both Webservice and UserDao are interfaces, you can impersonate them or create implementations of fake data for more complex test cases.
- UserDao: Test the DAO class using a detection test. Because these detection tests do not require any UI components, they can run quickly. For each test, create an in-memory database to ensure that the test has no side effects, such as changing the database files on disk.
Note: Room allows you to specify a database implementation, so you can test the DAO by providing a JUnit implementation. However, this approach is not recommended because the version of SQLite running on the device may be different from the version running on the development machine. SupportSQLiteOpenHelper
- Web Services: Avoid network calls to the back end during these tests. Independence from the outside world is important for all testing, especially web-based testing. Several libraries, including MockWebServer, can help you create fake local servers for these tests.
- Test artifacts: Architecture Components provides a Maven artifact to control its background threads. The Android.arch. core:core-testing artifact contains the following JUnit rules:
- InstantTaskExecutorRule: Use this rule to perform any background operation on the calling thread immediately.
- CountingTaskExecutorRule: Use this rule to wait for background operations on architecture components. You can also associate this rule with Espresso as a free resource.
Best practices
Programming is a creative field, and building Android apps is no exception. There are many ways to solve the problem, whether it’s passing data between multiple activities or fragments, retrieving remote data and persisting it locally for use in offline mode, or any other common scenario encountered by very important applications.
While the following recommendations are not mandatory, our experience is that following them can make your code base more robust, testable, and maintainable in the long run:
- Avoid specifying application entry points (such as activities, services, and broadcast receivers) as data sources. Instead, they should only coordinate with other components to retrieve a subset of data relevant to that entry point. Each application component is fairly transient, depending on the user’s interaction with the device and the overall current health of the system.
- Create clearly defined areas of responsibility across application modules. For example, don’t load data from your code base into multiple classes or packages in your code base. Also, don’t define multiple unrelated responsibilities (such as data caching and data binding) into the same class.
- Expose as little as possible from each module. Don’t try to create “just that” shortcuts that expose internal implementation details from a module. You may gain some time in the short term, but as your code base grows, you take on technical debt multiple times.
- Consider how to make each module individually testable. For example, having well-defined apis for fetching data from the network makes it easier to test modules that keep that data in a local database. Conversely, if you mix the logic of the two modules in one place, or distribute network code across the entire code base, testing becomes more difficult – if not impossible.
- Focus on the unique core of your application to stand out from the rest. Don’t reinvent the wheel by writing the same boilerplate code over and over again. Instead, focus your time and effort on what makes your application unique, and let Android architecture components and other recommended libraries handle duplicate templates.
- Keep as much relevant and fresh data as possible. This way, users can enjoy the functionality of the app even when the device is in offline mode. Keep in mind that not all users enjoy a constant high-speed connection.
- Specify a data source as a single fact source. Whenever your application needs to access this data, it should always originate from this single fact source.
Appendix: Expose network status
In the recommended application architecture section above, we omitted network errors and load state to keep the code snippet simple.
This section demonstrates how to expose network state using a class called Resource that encapsulates data and its state.
The following code snippet provides the following sample implementation Resource:
// A generic class that contains data and status about loading this data.
public class Resource<T> {
@NonNull public final Status status;
@Nullable public final T data;
@Nullable public final String message;
private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
this.status = status;
this.data = data;
this.message = message;
}
public static <T> Resource<T> success(@NonNull T data) {
return new Resource<>(Status.SUCCESS, data, null);
}
public static <T> Resource<T> error(String msg, @Nullable T data) {
return new Resource<>(Status.ERROR, data, msg);
}
public static <T> Resource<T> loading(@Nullable T data) {
return new Resource<>(Status.LOADING, data, null);
}
public enum Status { SUCCESS, ERROR, LOADING }
}
Copy the code
Because it is common to load data from the network when displaying a disk copy of that data, it is good to create a helper class that can be reused in multiple places. In this case, we create a class called NetworkBoundResource.
The following figure shows the decision tree NetworkBoundResource:
It starts by looking at a database of resources. When an entry is first loaded from the database, NetworkBoundResource checks to see if the result is sufficient to dispatch or if it should be retrieved from the network. Note that both cases can happen at the same time, because you may want the cached data to be displayed when you update it from the network.
If the network call completes successfully, it saves the response to the database and reinitializes the flow. If the network request fails, NetworkBoundResource fails to send directly.
Note: After saving the new data to disk, we re-initialize the stream from the database. However, we usually don't need to do this because the database itself happens to send the changes. Keep in mind that relying on the database to dispatch changes involves relying on associated side effects, which is bad because undefined behavior of these side effects can occur if the database ends up not scheduling changes because the data has not changed. Also, do not send results arriving from the network, as this would violate the single truth principle. After all, a database might contain triggers that change data values during a "save" operation. Also, do not schedule without new SUCCESS data, because the client will receive the wrong version of the data at that time.Copy the code
The following code snippet shows the public API provided by the NetworkBoundResource class for its children:
// ResultType: Type for the Resource data.
// RequestType: Type for the API response.
public abstract class NetworkBoundResource<ResultType.RequestType> {
// Called to save the result of the API response into the database.
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
// Called with the data in the database to decide whether to fetch
// potentially updated data from the network.
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// Called to get the cached data from the database.
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb(a);
// Called to create the API call.
@NonNull @MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
// Called when the fetch fails. The child class may want to reset components
// like rate limiter.
@MainThread
protected void onFetchFailed(a);
// Returns a LiveData object that represents the resource that's implemented
// in the base class.
public final LiveData<Resource<ResultType>> getAsLiveData();
}
Copy the code
Note these important details about the class definition:
- It defines two types of parameters, ResultType and RequestType, since the data type returned from the API may not match the data type used locally.
- It uses a class that ApiResponse invokes for network requests. ApiResponse is a simple wrapper retroFIT2.Call class that converts the response into instance LiveData.
A complete implementation of the NetworkBoundResource class appears as part of the Android-Architecture-Components GitHub project.
After creating NetworkBoundResource, we can use it to write our disk and network to implement User’s UserRepository class:
UserRepository
class UserRepository {
Webservice webservice;
UserDao userDao;
public LiveData<Resource<User>> loadUser(final int userId) {
return new NetworkBoundResource<User,User>() {
@Override
protected void saveCallResult(@NonNull User item) {
userDao.insert(item);
}
@Override
protected boolean shouldFetch(@Nullable User data) {
return rateLimiter.canFetch(userId)
&& (data == null| |! isFresh(data)); }@NonNull @Override
protected LiveData<User> loadFromDb(a) {
return userDao.load(userId);
}
@NonNull @Override
protected LiveData<ApiResponse<User>> createCall() {
returnwebservice.getUser(userId); } }.getAsLiveData(); }}Copy the code
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 affiliates.
Last updated: September 25, 2018