Architecture & Design

Trendyol Android App is being developed with reactive programming concepts and therefore Model-View-ViewModel(MVVM) is the most suitable pattern for the team. When we had started to implement MVVM properly, we also started to use recommended libraries like other developers who love to develop with clean coding. Then, let’s start!

Dagger2

I preferred to create a section for Dagger2; because it has a big role for whole layers in app’s structure and data flow. As you know, Dagger2 is dependency injection framework with no-reflection and it’s fully static. It saves our time and prevent boilerplate code and we’re using its power for testable, sustainable and reusable implementations.

Constructor injection is used at all layers except just one. At view layer in the app, we have to use field injection, because activities and fragments are created directly by the system and Dagger2 can’t reach their constructors to create. Because of this, we inject related activity or fragment in their lifecycle methods like below.

Repository

Repository is an abstraction of data flow in the app and this layer is responsible to provide/update related data for ViewModel with remote/local data sources. When we are implementing a new repository component for the app, we name and allocate it in a package considering related domain and API endpoint. Because we have a Domain Specific Language(DSL) in our company and Domain Driven Design(DDD) is important for us.

Almost all repositories in the app have remote and local data sources with its needs. At this layer, we also manage threading with RxJava2 and wrap provided data with a model named Resource that holds status of the flow as Success, Loading and Error. For example, when we call remote API:

thread is changed to IO thread from main thread to prevent blocking UI

remote data source is called

when response is provided, data is wrapped

Repository Diagram of Trendyol Android App

Simple Repository Implementation

Remote Data Source: This is the part where we communicate with REST API, so that the data source is used to fetch data remotely. When retrieving the data, we use type-safe HTTP client Retrofit.

To start data flow from web server, related parameters as ready to come from repository and request model creation isn’t made in here and then HTTP request is made with Retrofit. At this moment, RxJava2 comes to scene again for providing data flow and Retrofit returns data as Single or Completable. If the response isn’t important for the flow, return type of API service’s method is Completable, otherwise it is Single. After the successful response which is Single, the data is converted into an Observable in Remote Data Source to continue data flow.

Remote Data Source Diagram

Simple Remote Data Source Implementation

Local Data Source: At local data source, we’re using data storage with Room persistence library that is part of Android Architecture Components(AAC), and SharedPreferences API. Besides these, the data source is also used for caching data in memory. We decide the type of implementation related to our use cases. All data can be converted to Observable according to data flow.

While using Room , Data Access Object(DAO) provided from Room Database is used to persist changes and get Entities. At next step, related Entities start to hold data as wrapped with Observable or not in local data source.

, Data Access Object(DAO) provided from Room Database is used to persist changes and get Entities. At next step, related Entities start to hold data as wrapped with Observable or not in local data source. With SharedPreferences API , key-value datas are saved to private file in the system. SharedPreferences isn’t used to store only key-value data, it can also used to store any object as JSON string. When this type of storage is selected, to create object from JSON representation or convert the object to JSON, Gson is used.

, key-value datas are saved to private file in the system. SharedPreferences isn’t used to store only key-value data, it can also used to store any object as JSON string. When this type of storage is selected, to create object from JSON representation or convert the object to JSON, Gson is used. In some use cases like info texts, data is provided remotely at first and then, it is cached in memory as any types of data structure.

Local Data Source Diagram

Simple Local Data Source Implementations

UseCase

Connection between ViewModel and Repository for data flow is made in UseCase. It’s responsible from business logic, and it decides data source, converts/prepares/manipulates remote/local models for UI layer as UI models.

UseCase can have different and multiple UseCase classes and repositories to provide data that is ready.

UseCase use Mapper to convert remote/local models to UI models.

We try to create single responsible use case classes to develop more testable code like just fetching data from remote or just delete data from local, etc.

Mapper: Remote or local models are not used directly at UI layer in the app and at this point, Mapper converts and manipulates models that are gotten from data sources. Mapper is a functional interface that converts a value based on one or more input values, besides this it doesn’t contain any logic. It has only one responsibility in our data flow.

Decider: When we need to create data with condition or any fields provided from different sources in the mapper, we create a new class which we named Decider. Our motivation is that we move any logic to different classes and take out from encapsulation to create testable codes.

UseCase Diagram

Simple UseCase, Mapper and Decider Implementations

ViewModel

The objective of ViewModel is providing and keeping data in a lifecycle for UI controllers such as activity and fragment via LiveData. It also provides to survive data when re-creation(rotations, screen splitting, etc).

When we’re developing a ViewModel, we try to keep it dummy as much as possible using with UseCase classes and we don’t prefer using a repository class directly in a ViewModel. Because, at repository layer, we have raw data and this data can not emitted UI controllers without converting to UI models. We prefer to give responsibility of the converting process to UseCase. ViewModel has only one responsibility during the data flow, to create ViewState wrapped by LiveData.

ViewModel is mostly created for only one UI controllers such as activity or fragment, but some cases, SharedViewModel can be created to transfer data between more than one fragments.

We create lots of LiveData to be observed by UI controllers in ViewModel. For example, one LiveData is created to keep states(Loading, Error, Success) of the data; the other one is observed to display any message to user, etc.

LiveData observation is made in main thread at ViewModel.

UI controllers have ViewModel’s reference, and ViewModel shouldn’t know anything about the view.

LiveData: LiveData is a lifecycle-aware observable data holder class that can only observed by app’s components such as activity, fragment or services. In most cases, LiveData holds ViewState classes in the app, but in some cases, primitive types are also held by SingleLiveEvent that extended from MutableLiveData to display Snackbar, AlertDialog, etc. Any changes in LiveData’s values is updated in main thread during data flow to trigger observables in UI controller.

ViewModel Diagram

Simple ViewModel Implementation

UI/View

In the app, UI controller classes such as activities and fragments have ViewModel’s reference, observe LiveData in ViewModel to display data, trigger showing UI components like Snackbar and also update custom views. During data flow, observing LiveData that holds ViewState is the most important part of Data Binding. Every UI controllers and custom views in the app, have its own ViewState. For example, fragment has single ViewState to keep data status for Success, Loading and Error; to update custom views in the fragment, different ViewState classes have been used. Because, if fragment’s ViewState is used to update specific view, all parts of fragment will be updated unnecessarily.

Data Binding: Data Binding provides to bind observable data to UI components and prevent boilerplate codes at the UI objects. We use Data Binding at all UI development process to update related UI components.

ViewState: This class is created after data emission in ViewModel for UI updates, it contains all data and logics that is needed by UI controllers or view. We don’t prefer directly to use any types of classes that exists under android package such as Context in ViewModel, and ViewState provides a solution us when we need to follow this approach. Another usage of ViewState is that when we develop user interface for a fragment or a feature, we prefer to create custom views and ViewStates to update these views. Using ViewState classes not only creates more testable code, but also makes layout files in the app more readable.

UI Diagram

Simple ViewState Implementation