Skip to content

Most visited

Recently visited

navigation

Guide to App Architecture

This guide is for developers who are past the basics of building an app, and now want to know the best practices and recommended architecture for building robust, production-quality apps.

Common problems faced by app developers

Unlike their traditional desktop counterparts which, in the majority of cases, have a single entry point from the launcher shortcut and run as a single monolithic process, Android apps have a much more complex structure. A typical Android app is constructed out of multiple app components, including activities, fragments, services, content providers and broadcast receivers.

Most of these app components are declared in the app manifest which is used by the Android OS to decide how to integrate your app into the overall user experience with their devices. While, as mentioned earlier, a desktop app is traditionally running as a monolithic process, a properly written Android app needs to be much more flexible as the user weaves their way through the different apps on their device, constantly switching flows and tasks.

For example, consider what happens when you share a photo in your favorite social network app. The app triggers a camera intent from which the Android OS launches a camera app to handle the request. At this point, the user leaves the social network app but their experience is seamless. The camera app, in turn, may trigger other intents, like launching the file chooser, which may launch another app. Eventually the user comes back to the social networking app and shares the photo. Also, the user could be interrupted by a phone call at any point in this process and come back to share the photo after finishing the phone call.

In Android, this app-hopping behavior is common, so your app must handle these flows correctly. Keep in mind that mobile devices are resource constrained, so at any time, the operating system may need to kill some apps to make room for new ones.

The point of all this is that your app components can be launched individually and out-of-order, and can be destroyed at anytime by the user or the system. Because app components are ephemeral and their lifecycle (when they are created and destroyed) are not under your control, you should not store any app data or state in your app components and your app components should not depend on each other.

Common architectural principles

If you can't use app components to store app data and state, how should apps be structured?

The most important thing you should focus on is the separation of concerns in your app. It is a common mistake to write all your code in an Activity or a Fragment. Any code that does not handle a UI or operating system interaction should not be in these classes. Keeping them as lean as possible will allow you to avoid many lifecycle related problems. Don't forget that you don't own those classes, they are just glue classes that embody the contract between the OS and your app. The Android OS may destroy them at any time based on user interactions or other factors like low memory. It is best to minimize your dependency on them to provide a solid user experience.

The second important principle is that you should drive your UI from a model, preferably a persistent model. Persistence is ideal for two reasons: your users won't lose data if OS destroys your app to free up resources and your app will continue to work even when a network connection is flaky or not connected. Models are components that are responsible for handling the data for the app. They are independent from the Views and app components in your app, hence they are isolated from the lifecycle issues of those components. Keeping UI code simple and free of app logic makes it easier to manage. Basing your app on model classes with well-defined responsibility of managing the data will make them testable and your app consistent.

In this section, we demonstrate how to structure an app using Architecture Components by working through a use-case.

Imagine we're building a UI that shows a user profile. This user profile will be fetched from our own private backend using a REST API.

Building the user interface

The UI will consist of a fragment UserProfileFragment.java and its corresponding layout file user_profile_layout.xml.

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

We will create a UserProfileViewModel based on the ViewModel class to keep this information.

A ViewModel provides the data for a specific UI component, such as a fragment or activity, and handles the communication with the business part of data handling, such as calling other components to load the data or forwarding user modifications. The ViewModel does not know about the View and is not affected by configuration changes such as recreating an activity due to rotation.

Now we have 3 files.

Below are our starting implementations (the layout file is left out for simplicity):

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
public class UserProfileFragment extends LifecycleFragment {
    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, container, false);
    }
}

Now, we have these three code modules, how do we connect them? After all, when the ViewModel's user field is set, we need a way to inform the UI. This is where the LiveData class comes in.

LiveData is an observable data holder. It lets the components in your app observe LiveData objects for changes without creating explicit and rigid dependency paths between them. LiveData also respects the lifecycle state of your app components (activities, fragments, services) and does the right thing to prevent object leaking so that your app does not consume more memory.

Now we replace the User field in the UserProfileViewModel with a LiveData<User> so that the fragment can be informed when the data is updated. The great thing about LiveData is that it is lifecycle aware and will automatically clean up references when they are no longer needed.

public class UserProfileViewModel extends ViewModel {
    ...
    private User user;
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}

Now we modify UserProfileFragment to observe the data and update the UI.

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}

Every time the user data is updated, the onChanged callback will be invoked and the UI will be refreshed.

If you are familiar with other libraries where observable callbacks are used, you might have realized that we didn't have to override the fragment's onStop() method to stop observing the data. This is not necessary with LiveData because it is lifecycle aware, which means it will not invoke the callback unless the fragment is in an active state (received onStart() but did not receive onStop()). LiveData will also automatically remove the observer when the fragment receives onDestroy().

We also didn't do anything special to handle configuration changes (for example, user rotating the screen). The ViewModel is automatically restored when the configuration changes, so as soon as the new fragment comes to life, it will receive the same instance of ViewModel and the callback will be called instantly with the current data. This is the reason why ViewModels should not reference Views directly; they can outlive the View's lifecycle. See The lifecycle of a ViewModel.

Fetching data

Now we have connected the ViewModel to the fragment, but how does the ViewModel fetch the user data? In this example, we assume that our backend provides a REST API. We will use the Retrofit library to access our backend though you are free to use a different library that serves the same purpose.

Here's our retrofit Webservice that communicates with our backend:

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);
}

A naive implementation of the ViewModel could directly call the Webservice to fetch the data and assign it back to the user object. Even though it works, your app will be difficult to maintain as it grows. It gives too much responsibility to the ViewModel class which goes against the principle of separation of concerns that we've mentioned earlier. Additionally, the scope of a ViewModel is tied to an Activity or Fragment lifecycle, so losing all of the data when its lifecycle is finished is a bad user experience. Instead, our ViewModel will delegate this work to a new Repository module.

Repository modules are responsible for handling data operations. They provide a clean API to the rest of the app. They know where to get the data from and what API calls to make when data is updated. You can consider them as mediators between different data sources (persistent model, web service, cache, etc.).

The UserRepository class below uses the WebService to fetch the user data item.

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        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 ViewModel does not know that the data is fetched by the Webservice, which means we can swap it for other implementations as necessary.

Managing dependencies between components:

The UserRepository class above needs an instance of the Webservice to do its work. It could simply create it but to do that, It would also need to know the dependencies of the Webservice class to construct it. This would significantly complicate and duplicate the code (e.g. each class that needs a Webservice instance would need to know how to construct it with its dependencies). Additionally, UserRepository is probably not the only class that needs a Webservice. If each class creates a new WebService, it would be very resource heavy.

There are 2 patterns you can use to tackle this problem:

These patterns allow you to scale your code because they provide clear patterns for managing dependencies without duplicating code or adding complexity. Both of them also allow swapping implementations for testing; which is one of the main benefits of using them.

In this example, we are going to use Dagger 2 for managing dependencies.

Connecting ViewModel and the repository

Now we modify our UserProfileViewModel to use the repository.

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}

Caching data

The repository implementation above is good for abstracting the call to the web service but because it relies on only one data source, it is not very functional.

The problem with the UserRepository implementation above is that after fetching the data, it does not keep it anywhere. If the user leaves the UserProfileFragment and comes back to it, the app re-fetches the data. This is bad for two reasons: it wastes valuable network bandwidth and forces the user to wait for the new query to complete. To address this, we will add a new data source to our UserRepository which will cache the User objects in memory.

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}

Persisting data

In our current implementation, if the user rotates the screen or leaves and returns to the app, the existing UI will be visible instantly because the repository retrieves data from the in-memory cache. But what happens if the user leaves the app and comes back hours later, after the Android OS has killed the process?

With the current implementation, we will need to fetch the data again from the network. This is not only a bad user experience, but also wasteful since it will use mobile data to re-fetch the same data. You could simply fix this by caching the web requests, but it creates new problems. What happens if the same user data shows up from another type of request (e.g., fetching a list of friends)? Then your app will possibly show inconsistent data, which is a confusing user experience at best. For instance, the same user's data may show up differently because the list-of-friends request and user request could be executed at different times. Your app needs to merge them to avoid showing inconsistent data.

The proper way to handle this 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 the schema, so that 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 observing changes to the database data (including collections and join queries), exposing such changes via LiveData objects. In addition, it explicitly defines thread constraints that address common issues such as accessing storage on the main thread.

To use Room, we need to define our local schema. First, annotate the User class with @Entity to mark it as a table in your database.

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}

Then, create a database class by extending RoomDatabase for your app:

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}

Notice that MyDatabase is abstract. Room automatically provides an implementation of it. See the Room documentation for details.

Now we need a way to insert the user data into the database. For this, we'll create a data access object (DAO).

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}

Then, reference the DAO from our database class.

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

Notice that the load method returns a LiveData<User>. Room knows when the database is modified and it will automatically notify all active observers when the data changes. Because it is using LiveData, this will be efficient because it will update the data only if there is at least one active observer.

Now we can modify our UserRepository to incorporate 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 a LiveData directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread
            // check if user was fetched recently
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database
                userDao.save(response.body());
            }
        });
    }
}

Notice that even though we changed where the data comes from in UserRepository, we didn't need to change our UserProfileViewModel or UserProfileFragment. This is the flexibility provided by the abstraction. This is also great for testing because you can provide a fake UserRepository while testing your UserProfileViewModel.

Now our code is complete. If the user comes back to the same UI days later, they will instantly see the user information because we've persisted it. Meanwhile, our repository will update the data in the background if the data is stale. Of course, depending on your use case, you may prefer not to show the persisted data if it is too old.

In some use cases, such as pull-to-refresh, it is important for the UI to show the user if there is currently a network operation in progress. It is good practice to separate the UI action from the actual data since it might be updated for various reasons (for example, if we fetch a list of friends, the same user might be fetched again triggering a LiveData<User> update). From the UI's perspective, the fact that there is a request in flight is just another data point, similar to any other piece data (like the User object).

There are 2 common solutions for this use case:

Single source of truth

It is common for different REST API endpoints to return the same data. For instance, if our backend had another endpoint that returns a list of friends, the same user object could come from two different API endpoints, maybe in different granularity. If the UserRepository were to return the response from the Webservice request as-is, our UIs could potentially show inconsistent data since the data might change on the server side between these requests. This is why in the UserRepository implementation, the web service callback just saves the data into the database. Then, changes to the database will trigger callbacks on active LiveData objects.

In this model, the database serves as the single source of truth, and other parts of the app access it via the repository. Regardless of whether you use a disk cache, we recommend that your repository designate a data source as the single source of truth to the rest of your app.

Testing

We've mentioned that one of the benefits of separation is testability. Lets see how we can test each code module.

The final architecture

The following diagram shows all the modules in our recommended architecture and how they interact with one another:

Guiding principles

Programming is a creative field, and building Android apps is not 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 offline mode, or any number of other common scenarios that non-trivial apps encounter.

While the following recommendations are not mandatory, it has been our experience that following them will make your code base more robust, testable and maintainable in the long run.

Addendum: exposing network status

In the recommended app architecture section above, we intentionally omitted network error and loading states to keep the samples simple. In this section, we demonstrate a way to expose network status using a Resource class to encapsulate both the data and its state.

Below is a sample implementation:

//a generic class that describes a data with a status
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<>(SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String msg, @Nullable T data) {
        return new Resource<>(ERROR, data, msg);
    }

    public static <T> Resource<T> loading(@Nullable T data) {
        return new Resource<>(LOADING, data, null);
    }
}

Because loading data from network while showing it from the disk is a common use case, we are going to create a helper class NetworkBoundResource that can be reused in multiple places. Below is the decision tree for NetworkBoundResource:

It starts by observing database for the resource. When the entry is loaded from the database for the first time, NetworkBoundResource checks whether the result is good enough to be dispatched and/or it should be fetched from network. Note that both of these can happen at the same time since you probably want to show the 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, we dispatch a failure directly.

Below is the public API provided by 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 it should be
    // fetched 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();

    // 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() {
    }

    // returns a LiveData that represents the resource
    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

Notice that the class above defines two type parameters (ResultType, RequestType) since the data type returned from the API may not match the data type used locally.

Also notice that the code above uses ApiResponse for network request. ApiResponse is a simple wrapper around Retrofit2.Call class to convert its response into a LiveData.

Below is the rest of the implementation for the NetworkBoundResource class:

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        LiveData<ApiResponse<RequestType>> apiResponse = createCall();
        // we re-attach dbSource as a new source,
        // it will dispatch its latest value quickly
        result.addSource(dbSource,
                newData -> result.setValue(Resource.loading(newData)));
        result.addSource(apiResponse, response -> {
            result.removeSource(apiResponse);
            result.removeSource(dbSource);
            //noinspection ConstantConditions
            if (response.isSuccessful()) {
                saveResultAndReInit(response);
            } else {
                onFetchFailed();
                result.addSource(dbSource,
                        newData -> result.setValue(
                                Resource.error(response.errorMessage, newData)));
            }
        });
    }

    @MainThread
    private void saveResultAndReInit(ApiResponse<RequestType> response) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                saveCallResult(response.body);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                // we specially request a new live data,
                // otherwise we will get immediately last cached value,
                // which may not be updated with latest results received from network.
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }
}

Now, we can use use NetworkBoundResource to write our disk and network bound User implementation in the repository.

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final String 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() {
                return userDao.load(userId);
            }

            @NonNull @Override
            protected LiveData<ApiResponse<User>> createCall() {
                return webservice.getUser(userId);
            }
        }.getAsLiveData();
    }
}
This site uses cookies to store your preferences for site-specific language and display options.

Get the latest Android developer news and tips that will help you find success on Google Play.

* Required Fields

Hooray!

Browse this site in ?

You requested a page in , but your language preference for this site is .

Would you like to change your language preference and browse this site in ? If you want to change your language preference later, use the language menu at the bottom of each page.

This class requires API level or higher

This doc is hidden because your selected API level for the documentation is . You can change the documentation API level with the selector above the left navigation.

For more information about specifying the API level your app requires, read Supporting Different Platform Versions.

Take a one-minute survey?
Help us improve Android tools and documentation.