-
Notifications
You must be signed in to change notification settings - Fork 0
Client Application
It is a common mistake to write all the code in an Activity
or a Fragment
. So the most important principle to follow is separation concerns
. These UI-based classes should only contain logic that handles with UI and operating system interactions. Keeping these classes as lean as possible, will avoid many lifecycle-related problems.
Another important principle is that the UI should be driven from a model
(preferably a persistent model). Models are components that are responsible for handling the data for an app and they are independent from de View
objects and app components, so they are unaffected by the app's lifecycle and the associated concerns. By basing an app on model classes with the well-defined responsibility of managing the data, the app is more testable and consistent.
Persistence is ideal for the following reasons:
- The users don't lose data if the Android OS destroys your app to free up resources.
- The app continues to work in cases when a network connection is flaky or not available.
To start, consider the following diagram, which shows how all the modules should interact with one another after designing the app:
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.
The Client can be divided in four blocks Activities/Fragments
, ViewModels
, Repository
and Remote Data Source
. This division was made following some principle like: separation of concerns, where each module has is own concern and objective and the principle where models are components that are responsible for handling the data for an app and they are independent from de View
objects and app components, so they are unaffected by the app's lifecycle and the associated concerns.
The Activities/Fragments
are responsible for the view in the client, where the user can interact with and make action.
The ViewModels
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 Repository
module 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. Can be considered repositories to be mediators between different data sources, such as persistent models, web services, and caches.
Remote Data Source
is the module that has the objective of communicating with the API, to obtain and update some data in the client.
The UI consists of multiple activities files like RouteCreationActivity
, RouteSearchActivity
and UserProfileActivity
, the corresponding layout files are activity_route_creation.xml
, activity_route_search.xml
(imports content_user_profile.xml
) and activity_user_profile.xml
.
To drive the UI, our data model needs to hold the following data elements:
- User Object : A data class that holds details about the user.
- User Identifier : The identifier for the user. Is unique per user, does not exists users with the same identifier.
- Route Object : A data class that hold details and information like location, name, description, classification, duration, dateCreated, points, personIdentifier (User Owner).
- Route Identifier : The Route identifier, which routes has its own id and its unique.
- Point Object : A data class to represent a Geo Coordinates like latitude and longitude, where a route has multiple Points, to represent the path.
The application is based in ViewModel architecture component, so was used diverse classes to follow this protocol like: RouteViewModel (related to all information about routes) and UserProfileViewModel (User that sign in information).
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.
After all, when the this fields is set in the ViewModel classes, we need a way to inform the UI. This is where the LiveData
architecture component comes in.
LiveData
is an observable data holder. Other components in 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 app's components—such as activities, fragments, and services—and includes clean-up logic to prevent object leaking and excessive memory consumption.
Every time the data is updated, the onChanged()
callback is invoked, and the UI is refreshed.
It was not added any logic to handle configuration changes, such as rotating the device's screen. The ViewModel classed are automatically restored when the configuration changes, so as soon as the new fragment/activity 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, shouldn't include direct references to View objects within ViewModel implementation.
After used the LiveData
to connect the ViewModel
with the Layouts
, is needed to fetch the data. The library used to it was Retrofit, that access the backend. Instead, of ViewModel delegates the data-fetching process to a new module, the repositories.
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. Can be considered repositories to be mediators between different data sources, such as persistent models, web services, and caches.
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 ViewModels
doesn't know how the data is fetched, so can provided the view model with data obtained from several different data-fetching implementations.
After the creation of all this components it is necessary to group them. The way to connect the ViewModel and the Repository is, which Repository has a ViewModel and the Activities only communicate with the Repositories and never with the ViewModel Classes. The responsability of the repositories is to return the wanted data, from REST Api
or saved data, in the shape of LiveData.
In the app architecture above, was omitted about network responses like network errors, successful responses and loading states. So was decided to create the class Resource
that helps exposing the network status. The Generic class encapsulate the data and its state from network.
The following code demonstrates the implementation of the class Resource
:
// A generic class that contains data and status about loading this data.
class Resource<T> private constructor(
val status: Status,
val data: T?,
val message: String?
) {
enum class Status {
SUCCESS, ERROR, LOADING
}
companion object {
fun <T> success(data: T): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(): Resource<T> {
return Resource(Status.LOADING, null, null)
}
}
}