App Development Architecture Guide (Google official document translation)

App Development Architecture Guide (Google official document translation)

[[192223]]

This article is for readers who already have the basic knowledge of app development and want to know how to develop a robust app.

Note: This guide assumes that the reader is already familiar with the Android Framework. If you are new to app development, please check out the Getting Started series of tutorials, which covers the prerequisites for this guide.

Common problems faced by app developers

Unlike traditional desktop application development, the architecture of Android apps is much more complex. A typical Android app is composed of multiple app components, including activity, fragment, service, content provider, and broadcast receiver. Traditional desktop applications are often completed in a large single process.

Most app components are declared in the app manifest, which the Android OS uses to decide how to integrate your app with the device to form a unified user experience. Although, as just mentioned, desktop apps only run one process, an excellent Android app needs to be more flexible because users operate between different apps, constantly switching processes and tasks.

For example, imagine what happens when you share a photo in your favorite social networking app. The app triggers a camera intent, and the Android OS launches a camera app to handle the action. The user has left the social networking app, but the user experience is seamless. The camera app may in turn trigger another intent, such as launching a file picker, which may open another app. The user returns to the social networking app and shares the photo. At any time during this period, the user can be interrupted by a phone call and then return to share the photo.

In Android, this behavior of apps operating in parallel is very common, so your app must handle these processes correctly. Also remember that mobile devices have limited resources, so at any time the operating system may kill some apps to make room for new apps to run.

In general, your app components may be started separately and in disorder, and may be destroyed by the system or the user at any time. Because of the short life of app components and the uncontrollable life cycle, no data should be stored in app components, and app components should not depend on each other.

General architectural principles

If app components cannot store data and state, is the app still structurable?

One of the most important principles is to try to achieve separation of concerns in your app. A common mistake is to write all the code in an Activity or Fragment. Anything that is not related to the UI and system interaction should not be placed in these classes. Keeping them as simple and lightweight as possible can avoid many lifecycle issues. Don't forget that you don't own these classes, they are just a bridge between the app and the operating system. Depending on user operations and other factors, such as low memory, the Android OS may destroy them at any time. In order to provide a reliable user experience, it is best to minimize your dependence on them.

The second very important principle is persistence. Persistence is needed for two reasons: if the OS destroys the app to release resources, user data will not be lost; when the network is poor or disconnected, the app can continue to work. Models are components responsible for app data processing. They do not depend on Views or app components (Activities, Fragments, etc.), so they are not affected by the life cycle of those components. Keeping the UI code simple and separated from the business logic makes it easier to manage.

App architecture recommendation

In this section, we will use a use case to demonstrate how to use Architecture Component to build an app.

Note: There is no one-size-fits-all approach to writing apps. That said, the architectures recommended here are a good starting point for most use cases. But if you already have a good architecture, there’s no need to change it.

Suppose we are creating a UI that displays a user profile. The user information is retrieved from our own private backend REST API.

Creating the User Interface

The UI consists of UserProfileFragment.java and the corresponding layout file user_profile_layout.xml.

To drive the UI, our data model needs to hold two data elements.

User ID: User identification. *** Use fragment argument to pass this data. If the OS kills your process, this data can be saved, so the id is still available when the app is started again.

User object: A POJO object that holds user information data.

We will create a UserProfileViewModel that inherits the ViewModel class to save this information.

A ViewModel provides data for a specific UI component, such as a fragment or activity, and is responsible for communicating with the business logic that processes the data, such as calling other components to load data or forwarding user modifications. The ViewModel is unaware of the existence of the View and will not be affected by configuration changes.

Now we have three files.

user_profile.xml: defines the UI of the page

UserProfileViewModel.java: Class that prepares data for the UI

UserProfileFragment.java: Controller that displays data in the ViewModel and responds to user interactions

Let's start implementing it (for simplicity, the layout file is omitted):

  1. public class UserProfileViewModel extends ViewModel {
  2.  
  3. private String userId;
  4.  
  5. Private User   user ;
  6.  
  7.   
  8.  
  9. public void init(String userId) {
  10.  
  11. this.userId = userId;
  12.  
  13. }
  14.  
  15. public   User getUser() {
  16.  
  17. return   user ;
  18.  
  19. }
  20.  
  21. }
  1. public class UserProfileFragment extends LifecycleFragment {
  2.  
  3. private static final String UID_KEY = "uid" ;
  4.  
  5. private UserProfileViewModel viewModel;
  6.  
  7.   
  8.  
  9. @Override
  10.  
  11. public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  12.  
  13. super.onActivityCreated(savedInstanceState);
  14.  
  15. String userId = getArguments().getString(UID_KEY);
  16.  
  17. viewModel = ViewModelProviders. of (this).get(UserProfileViewModel.class);
  18.  
  19. viewModel.init(userId);
  20.  
  21. }
  22.  
  23.   
  24.  
  25. @Override
  26.  
  27. public   View onCreateView(LayoutInflater inflater,
  28.  
  29. @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  30.  
  31. return inflater.inflate(R.layout.user_profile, container, false );
  32.  
  33. }
  34.  
  35. }

Note: In the above example, LifecycleFragment is inherited instead of Fragment class. After the lifecycles API in Architecture Component is stabilized, the Fragment class in Android Support Library will also implement LifecycleOwner.

Now that we have these code modules, how do we connect them? After all, after the user member of the ViewModel is set, we still need to display it on the interface. This is where LiveData comes in.

LiveData is an observable data holder. You can observe the changes of LiveData objects without explicitly creating dependencies between it and app components. LiveData also takes into account the life cycle state of app components (activities, fragments, services) and does things to prevent object leakage.

Note: If you are already using libraries like RxJava or Agera, you can continue to use them instead of LiveData. But when using them, make sure to handle lifecycle issues correctly. The data stream should be stopped when the LifecycleOwner is stopped, and the data stream should be destroyed when the LifecycleOwner is destroyed. You can also use android.arch.lifecycle:reactivestreams to use LiveData with other responsive data stream libraries (for example, RxJava2).

Now we replace the User member in UserProfileViewModel with LiveData, so that the fragment will be notified when the data changes. The beauty of LiveData is that it is lifecycle aware and will automatically clean up the reference when it is no longer needed.

  1. public class UserProfileViewModel extends ViewModel {
  2.  
  3. ...
  4.  
  5. Private User   user ;
  6.  
  7. private LiveData <User> user ;
  8.  
  9. public LiveData<User> getUser () {
  10.  
  11. return   user ;
  12.  
  13. }
  14.  
  15. }

Now we modify the UserProfileFragment so that it observes the data and updates the UI.

  1. @Override
  2.  
  3. public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  4.  
  5. super.onActivityCreated(savedInstanceState);
  6.  
  7. viewModel.getUser().observe(this, user -> {
  8.  
  9. // update UI
  10.  
  11. });
  12.  
  13. }

Whenever the User data is updated, the onChanged callback will be triggered and the UI will be refreshed.

If you are familiar with the usage of observable callbacks in other libraries, you will realize that we do not need to override the fragment's onStop() method to stop observing the data. Because LiveData is lifecycle-aware, that is, the callback will not be triggered unless the fragment is active. LiveData can also automatically remove the observer when the fragment onDestroy() is called.

There is nothing special done for us to handle configuration changes (such as screen rotation). The ViewModel can be automatically saved when the configuration changes. Once the new fragment enters the lifecycle, it will receive the same ViewModel instance and the callback with the current data will be called immediately. This is why ViewModels should not directly reference any Views, they are independent of the View lifecycle. See ViewModel lifecycle.

Get data

Now we have linked the ViewModel to the fragment, but how does the ViewModel get the data? In our case, assuming that the backend provides a REST API, we use Retrofit to extract data from the backend. You can also use any other library to achieve the same purpose.

Here is the retrofit Webservice that interacts with the backend:

  1. public interface Webservice {
  2.  
  3. /**
  4.  
  5. * @GET declares an HTTP GET request
  6.  
  7. * @Path( "user" ) annotation on the userId parameter marks it as a
  8.  
  9. * replacement for the { user } placeholder in the @GET path
  10.  
  11. */
  12.  
  13. @GET( "/users/{user}" )
  14.  
  15. Call< User > getUser(@Path( "user" ) String userId);
  16.  
  17. }

A simple way to implement ViewModel is to directly call the Webservice to get data and then assign it to the User object. Although this is feasible, it will become difficult to maintain as the app grows. The excessive responsibilities of ViewModel also violate the separation of concerns principle mentioned above. In addition, the validity period of ViewModel is bound to the life cycle of Activity and Fragment, so losing all data when its life cycle ends is a bad user experience. Instead, our ViewModel will delegate this work to the Repository module.

Repository modules are responsible for handling data operations. They provide a simple API to the app. They know where to get the data and what API to call when the data is updated. You can think of them as intermediaries between different data sources (persistent models, web services, caches, etc.).

The following UserRepository class uses a WebService to retrieve user data.

  1. public class UserRepository {
  2.  
  3. private Webservice webservice;
  4.  
  5. // ...
  6.  
  7. public LiveData< User > getUser( int userId) {
  8.  
  9. // This is   not an optimal implementation, we'll fix it below
  10.  
  11. final MutableLiveData< User > data = new MutableLiveData<>();
  12.  
  13. webservice.getUser(userId).enqueue(new Callback< User >() {
  14.  
  15. @Override
  16.  
  17. public void onResponse(Call< User > call, Response< User > response) {
  18.  
  19. // error case   is   left   out   for brevity
  20.  
  21. data.setValue(response.body());
  22.  
  23. }
  24.  
  25. });
  26.  
  27. return data;
  28.  
  29. }
  30.  
  31. }

Although the repository module may seem unnecessary, it actually plays an important role; it abstracts the data source from the app. Now our ViewModel does not know that the data is provided by the Web service, which means that it can be replaced with another implementation if necessary.

Note: For simplicity we have omitted the case where a network error occurs. See Addendum: exposing network status below for a version that exposes both network errors and loading status.

Managing dependencies between different components:

The UserRepository class above needs an instance of the Webservice to do its job. We could just create it, but to do that we would need to know what the Webservice depends on in order to build it. This significantly increases the complexity and coupling of the code (i.e., every class that needs an instance of the Webservice needs to know how to build it with its dependencies). In addition, UserRepository is likely not the only class that needs a Webservice. If every class creates a new WebService, it would become very heavy.

There are two modes to solve this problem:

Dependency injection: Dependency injection allows a class to define its own dependencies without constructing them. Another class is responsible for providing these dependencies at runtime. In Android apps, we recommend using Google's Dagger 2 to implement dependency injection. Dagger 2 automatically builds objects by traversing the dependency tree and provides compile-time dependencies.

Service Locator: Service Locator provides a registry where classes can get their dependencies instead of building them. It is simpler than dependency injection, so if you are not familiar with dependency injection, you can use Service Locator.

These patterns allow you to extend your own code because they provide clear patterns to manage dependencies instead of repeating code over and over again. Both support replacing mock dependencies for testing, which is one of the main advantages of using them.

In this example, we will use Dagger 2 to manage dependencies.

Connecting the ViewModel and the repository

Now we modify the UserProfileViewModel to use the repository.

  1. public class UserProfileViewModel extends ViewModel {
  2.  
  3. private LiveData <User> user ;
  4.  
  5. private UserRepository userRepo;
  6.  
  7.   
  8.  
  9. @Inject // UserRepository parameter is provided by Dagger 2
  10.  
  11. public UserProfileViewModel(UserRepository userRepo) {
  12.  
  13. this.userRepo = userRepo;
  14.  
  15. }
  16.  
  17.   
  18.  
  19. public void init(String userId) {
  20.  
  21. if (this. user != null ) {
  22.  
  23. // ViewModel is created per Fragment so
  24.  
  25. // we know the userId won't change
  26.  
  27. return ;
  28.  
  29. }
  30.  
  31. user = userRepo.getUser(userId);
  32.  
  33. }
  34.  
  35.   
  36.  
  37. public LiveData<User> getUser () {
  38.  
  39. return this.user ;
  40.  
  41. }
  42.  
  43. }

<<:  The use of OkHttp and simple encapsulation

>>:  How to write Android projects based on compile-time annotations

Recommend

How much does it cost to customize a driving school mini program in Bayannur?

In order to better penetrate into various industr...

Low cost and high activity, how to recall users?

What I will talk about today is mainly related to...

How much does it cost to develop a parent-child mini program in Dalian?

More and more businesses are paying attention to ...

How much does 100M dedicated IP bandwidth cost?

How much does 100M dedicated IP bandwidth cost? A...

Douyin live streaming: Ten rules for traffic

Ten military rules for traffic ecology: 1. Douyin...

How does Pinduoduo achieve user growth?

Recently, Pinduoduo has been caught up in a count...

Is there any free Baidu bidding and pricing software?

For bidding promotion personnel, in addition to d...

How to use human nature to attract traffic?

Today's traffic business has reached a stage ...

As early as 60,000 years ago, did Australian people achieve "nut freedom"?

Macadamia nuts are the only native Australian pla...

Some tips on server hosting?

The rapid development of the Internet will inevit...