Android app development: design patterns for mobile architecture download






















Admittedly, this is a simplified overview, but you can read the Dagger documentation for more implementation details. You can also click the links above in the mentioned libraries for in-depth tutorials for each topic.

This pattern may seem complicated and magical at first, but it can help simplify your activities and fragments. The Singleton pattern specifies that only a single instance of a class should exist with a global access point.

This pattern works well when modeling real-world objects with only one instance. For example, if you have an object that makes network or database connections, having more than one instance of the project may cause problems and mix data. The Kotlin object keyword declares a singleton without needing to specify a static instance like in other languages:.

So, if you need to use a Kotlin object from Java code, you modify the call like this:. The Singleton is probably the most straightforward pattern to understand initially but can be dangerously easy to overuse and abuse. As the name suggests, Factory takes care of all the object creational logic. In this pattern, a factory class controls which object to instantiate. Factory pattern comes in handy when dealing with many common objects.

You can use it where you might not want to specify a concrete class. It helps to keep all object creation in one class. If used inappropriately, a Factory class can get bloated due to excessive objects.

Testing can also become difficult as the factory class itself is responsible for all the objects. Future You will undoubtedly appreciate the Structural Patterns you used to organize the guts of your classes and objects into familiar arrangements that perform typical tasks. Adapter and Facade are two commonly-seen patterns in Android. A famous scene in the movie Apollo 13 features a team of engineers tasked with fitting a square peg into a round hole.

This, metaphorically, is the role of an adapter. It might be a Product or a User or Tribble. Meanwhile, a RecyclerView is the same basic object across all Android apps. In this situation, you can use a subclass of RecyclerView.

Adapter and implement the required methods to make everything work:. The Facade pattern provides a higher-level interface that makes a set of other interfaces easier to use.

The following diagram illustrates this idea in more detail:. If your Activity needs a list of books, it should be able to ask a single object for that list without understanding the inner workings of your local storage, cache and API client.

Beyond keeping your Activities and Fragments code clean and concise, this lets Future You make any required changes to the API implementation without impacting the Activity. You create an interface to provide API data to client classes like so:. The client needs to call listBooks to receive a list of Book objects in the callback. For all it knows, you could have an army of Tribbles assembling the list and sending it back via transporter beam.

This lets you make all types of customizations underneath without affecting the client. The Decorator pattern dynamically attaches additional responsibilities to an object to extended its functionality at runtime. Take a look at the example below:. By using the SaladDecorator class, you can extend your salad easily without having to change PlainSalad.

You can also remove or add any salad decorator on runtime. You use the Composite pattern when you want to represent a tree-like structure consisting of uniform objects. A Composite pattern can have two types of objects: composite and leaf. A composite object can have further objects, whereas a leaf object is the last object.

Logically and technically the organization, in this case Raywenderlich , adds an Entity to the Team. Behavioral Patterns let you assign responsibility for different app functions. Often, developers use several behavioral patterns together in the same app. You just give your order to the waiter, who posts the order in the kitchen for the next available cook.

Similarly, the Command pattern lets you issue requests without knowing the receiver. You encapsulate a request as an object and send it off. Deciding how to complete the request is an entirely separate mechanism. You can create specific subclasses which carry data as well:. After defining your event, you obtain an instance of EventBus and register an object as a subscriber. Now that the object is a subscriber, tell it what type of event to subscribe to and what it should do when it receives one:.

Since so much of this pattern works its magic at run-time, Future You might have a little trouble tracing this pattern unless you have good test coverage. Still, a well-designed flow of commands balances out the readability and should be easy to follow later. The Observer pattern defines a one-to-many dependency between objects.

When one object changes state, its dependents get a notification and updates automatically. This pattern is versatile. You can use it for operations of indeterminate time, such as API calls. You can also use it to respond to user input.

It was originally popularized by the RxAndroid framework, also known as Reactive Android. This library lets you implement this pattern throughout your app:. In short, you define Observable objects that will emit values. These values can emit all at once, as a continuous stream or at any rate and duration. Subscriber objects will listen for these values and react to them as they arrive. More recently Android also introduced a native way to implement this pattern through LiveData.

You can learn more about this topic here. You use a Strategy pattern when you have multiple objects of the same nature with different functionalities. For a better understanding, take a look at the following code:.

In the State pattern, the state of an object alters its behavior accordingly when the internal state of the object changes. Take a look at the following snippets:. So, you create an object of the class Printer to print. The Printer class handles all the states of the printer internally. App architectures play a vital part in structuring a loosely coupled codebase. You can use it anywhere, irrespective of the platform. App architectures help you write easily testable, extensible and decoupled code.

The App Architectures used to create solid and maintainable codebases are many, but in this article you will learn the most popular ones:. Model View Controller , or MVC , refers to one of the most popular architectural patterns and the one from which many others derive. Its name refers to the three ways to classify the classes in your code:.

Dividing your code between these three categories will go a long way toward making it decoupled and reusable. 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. By basing your app on model classes with the well-defined 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.

Imagine we're building a UI that shows a user profile. 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. A ViewModel object provides the data for a specific 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 configuration changes, such as recreating an activity when rotating the device. The following code snippets show the starting contents for these files. The layout file is omitted for simplicity. Now that we have these code modules, how do we connect them?

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:. 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.

Now, the UserProfileFragment is informed when the data is updated. Furthermore, because this LiveData field is lifecycle aware, it automatically cleans up references after they're no longer needed. Every time the user profile 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 configuration changes, such as the user rotating the device's screen. The UserProfileViewModel is automatically restored when the configuration 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.

We 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 definition of Webservice that communicates with our backend:. A first 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 difficult 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:.

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. 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.

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. We recommend following dependency injection patterns and using the Hilt library in Android apps.

Hilt automatically constructs objects by walking the dependency tree, provides compile-time guarantees on dependencies, and creates dependency containers for Android framework classes. Our example app uses Hilt to manage the Webservice object's dependencies. The UserRepository implementation abstracts the call to the Webservice object, but because it relies on only one data source, it's not very flexible.

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:. 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 fix 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 figure 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 defines execution constraints that address common threading issues, such as accessing storage on the main thread.

To use Room, we need to define our local schema. First, we add the Entity annotation to our User data model class and a PrimaryKey annotation to the class's id field. These annotations mark User as a table in our database and id as the table's primary key:. Then, we create a database class by implementing RoomDatabase for our app:. 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. Using Flow with Room allows you to get live updates.

This means that every time there's a change in the user table, a new User will be emitted. Now we can modify our UserRepository to incorporate the Room data source:. Notice that even though you changed where the data comes from in UserRepository , you didn't need to change UserProfileFragment. This small-scoped update demonstrates the flexibility that this app architecture provides. It's also great for testing, because you can provide a mock instance of UserRepository and test your 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 example values and indicates that your app is currently fetching and loading up-to-date information.

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.

From the UI's perspective, the fact that there's a request in flight 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:. In the separation of concerns section, we mentioned that one key benefit of following this principle is testability.

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 files on disk.

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. Programming is a creative field, 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 offline 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-defined 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 define 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-defined 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 difficult—if not impossible—to test. 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 offline 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. The following code snippet provides a sample implementation of Resource :.

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. The following diagram shows the decision tree for NetworkBoundResource :. It starts by observing the 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 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 undefined 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. 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:. Content and code samples on this page are subject to the licenses described in the Content License. App Basics. Build your first app. App resources. Resource types. App manifest file. Device compatibility.

Multiple APK support. Tablets, large screens, and foldables. Build responsive UIs. Build for foldables. Getting started. Handling data. User input. Watch Face Studio. Health services. Creating watch faces. Android TV. Build TV Apps. Build TV playback apps. Help users find content on TV. Recommend TV content. Watch Next. Build TV games. Build TV input services. TV Accessibility.

Android for Cars. Build media apps for cars. Build navigation, parking, and charging apps for cars. Android Things. Supported hardware. Advanced setup. Build apps. Create a Things app. Communicate with wireless devices. Configure devices. Interact with peripherals. Build user-space drivers. Manage devices. Create a build. Push an update.



0コメント

  • 1000 / 1000