Data flow process in an application is the most important part to reflect how the application works. In this part there would be seen the coordination among class to handle all the required process starts from user request to the data to be shown in the presentation layer.
Nucleo offers the simplicity in implementing the data flow using the roboust software design pattern principles, clean architecture, reactive programming and its amazing implementation in Model View ViewModel (MVVM) patterns. They ensure that process will be running independently to get high cohesive in the dependency across components.
Our ultimate goal is making Nucleo as the reflections of culture and working framework across development team. It encapsulates the complexity to deliver the roboust, stable and high quality Android application.
The concrete implementation of Nucleo focus on how the main three things below working in harmony to support every business rules, expectation and all the components are independent, testable and easy to adapt with changes.
Three main parts that taken most of time our development can be solved with Nucleo with tons of functionalities, abstractions and functions to make developer's life easier. In Presentation architectural pattern Nucleo adapt the Model-View-ViewModel to handle data flow from the Presentation Layer to Data Layer.
To understand the nucleo framework in Android you need to understand the basic technology and principles beneath in it (at least) such as :
The data in flow will be encapsulated in RxJava and LiveData observable object that brings the flexibility to be manipulated in their stream before the data to be consumed by the presentation layer. All the communication are inward and independent means that the each layer can be stood independently to do their responsibility without knowing each other, will be delivering through abstraction and to be able to isolated testing. We keep the relations among objects separated as much as possible to keep the cleanliness of the architecture. We keep the S.O.L.I.D flag flying high in this codebase.
Sample implementation of Nucleo data flow in a sequence diagram as follow :
So the class diagrams design of each object above will be defined as follow :
They are working independently, so how they can do task in harmony? thanks to the power of Dependency Injection that able nailing the dependency process among application components so no more hard dependency in Nucleo.
So, let's get started to see how it works. We use the themoviedb API as the sample in this demo.
When you have a project that already set up and configured with the Nucleo, then the things you have to do is to change the default basic required data such as BASE URL, Api Keys, or BASE IMAGE URL at flavors part of app gradle file as follow :
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
buildConfigField("String", "BASE_URL", "\"https://api.themoviedb.org/3/\"")
buildConfigField("String", "BASE_IMAGE_URL", "\"https://image.tmdb.org/t/p/w500/\"")
buildConfigField("String", "API_KEY", "\"7012fc3c96c9f6f707e4edb9c9725718\"")
applicationIdSuffix ".dev"
}
staging {
dimension "env"
buildConfigField("String", "BASE_URL", "\"https://api.themoviedb.org/3/\"")
buildConfigField("String", "BASE_IMAGE_URL", "\"https://image.tmdb.org/t/p/w500/\"")
buildConfigField("String", "API_KEY", "\"7012fc3c96c9f6f707e4edb9c9725718\"")
applicationIdSuffix ".staging"
}
production {
dimension "env"
buildConfigField("String", "BASE_URL", "\"https://api.themoviedb.org/3/\"")
buildConfigField("String", "BASE_IMAGE_URL", "\"https://image.tmdb.org/t/p/w500/\"")
buildConfigField("String", "API_KEY", "\"7012fc3c96c9f6f707e4edb9c9725718\"")
applicationIdSuffix ""
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
Pro tips : You can have a properties file to manage this keys or base url like this
//env.properties ext{ BASE_URL = "https://api.themoviedb.org/3/" BASE_IMAGE_URL = "https://image.tmdb.org/t/p/w500/" API_KEY = "7012fc3c96c9f6f707e4edb9c9725718" }
The apply it on the top of app gradle file like this
... apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply from: "${rootProject.rootDir}/env.properties"
then the implementation will be
flavorDimensions "env" productFlavors { dev { dimension "env" buildConfigField("String", "BASE_URL", "\"${BASE_URL}\"") applicationIdSuffix ".dev" } staging { dimension "env" buildConfigField("String", "BASE_URL", "\"${BASE_URL}\"") applicationIdSuffix ".staging" } production { dimension "env" buildConfigField("String", "BASE_URL", "\"${BASE_URL}\"") applicationIdSuffix "" proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }
Don't forget to do Make Project by clicking build -> Make Project to ensure that the key and base url we have are updated in gradle system.
The format of get data in themoviedb is like this
https://api.themoviedb.org/3/movie/popular?api_key=7012fc3c96c9f6f707e4edb9c9725718
as you can see there's query param api_key that needs to be attached whenever we do the api request so after we have done with the gradle file then we need to modify a bit the ApiModule.kt
file, it belongs to package DI. In this file we update the base URL and parameter interceptor required like this:
const val BASE_URL: String = "baseUrl"
val apiModule = module {
single {
return@single OkHttpClientFactory.create(
interceptors = arrayOf(getHeaderInterceptor(get()), getParameterInterceptor()),
showDebugLog = BuildConfig.DEBUG,
authenticator = null
)
}
single(named(BASE_URL)) { BuildConfig.BASE_URL }
}
private fun getParameterInterceptor(): Interceptor {
val params = HashMap<String, String>()
params["api_key"] = BuildConfig.API_KEY
return ParameterInterceptor(params)
}
private fun getHeaderInterceptor(preferenceManager: PreferenceManager): Interceptor {
val headers = HashMap<String, String>()
//define default headers here
headers["Content-Type"] = "application/json"
return HeaderInterceptor(headers, preferenceManager)
}
Now we have a custom ApiModule that able to satisfy our request, next we need to create a GET call from this api url, copy paste it to your browser or postman then you will see the response from this api
https://api.themoviedb.org/3/movie/popular?api_key=7012fc3c96c9f6f707e4edb9c9725718
As you might seen that there are a lot of variables and objects in the respose so let's make an api call for this!
Dealing with the clean architecture will have a consequence, that is you need many file to keep the components or classes work independently. But don't worry, with the power of Nucleo Generator all the process done in only with few clicks! So let's do it.
Open your Android Studio terminal :
Type plop > press enter
You can choose nucleo-base-data-layer
The in the terminal prompt,
As you can see, there so much template and generator here
Click Finish to start generating the classeses.
You can enable the
Cache Strategy
that will be handle automatically persist the response data into local database using Room Framework.In this tutorial excludes the implementation of Room database
As you can see that the result is absolutely amazing. It generates all required file to make the api call with clean architecture implementation with ease.
To start our Journey in making the api call let's have a mindset that handle the data flow is starting from the data layer to outer layer so, we will do from transforming the api response to the data model. Open the MovieItem.kt
in data -> movie -> model -> response -> MovieItem . It has a default data class format that needs to adjust.
Copy the api response from the api url above and paste to the Kotlin data class from JSON (DTO Plugin, please ensure you have install this plugin in your Android Studio). Right click in the MovieItem class then select Generate
menu and select Kotlin data classes from JSON
.
Then in the dialog, paste the api response the fill up the field name with desired name, we will name it MovieItem
Then modify the file by removing the redudancy name so the final data model for this MovieItem
will be like this :
data class MovieItem(
@SerializedName("page")
val page: Int,
@SerializedName("results")
val results: List<Result>,
@SerializedName("total_pages")
val totalPages: Int,
@SerializedName("total_results")
val totalResults: Int
)
data class Result(
@SerializedName("adult")
val adult: Boolean,
@SerializedName("backdrop_path")
val backdropPath: String,
@SerializedName("genre_ids")
val genreIds: List<Int>,
@SerializedName("id")
val id: Int,
@SerializedName("original_language")
val originalLanguage: String,
@SerializedName("original_title")
val originalTitle: String,
@SerializedName("overview")
val overview: String,
@SerializedName("popularity")
val popularity: Double,
@SerializedName("poster_path")
val posterPath: String,
@SerializedName("release_date")
val releaseDate: String,
@SerializedName("title")
val title: String,
@SerializedName("video")
val video: Boolean,
@SerializedName("vote_average")
val voteAverage: Double,
@SerializedName("vote_count")
val voteCount: Int
)
But we need to adjust a bit about this generated code, so let's change all the attributes in MovieItem
class and subtitute it with all atributes in Result
class and the delete the Result
class, so the final class would be :
data class MovieItem(
@SerializedName("adult")
val adult: Boolean,
@SerializedName("backdrop_path")
val backdropPath: String,
@SerializedName("genre_ids")
val genreIds: List<Int>,
@SerializedName("id")
val id: Int,
@SerializedName("original_language")
val originalLanguage: String,
@SerializedName("original_title")
val originalTitle: String,
@SerializedName("overview")
val overview: String,
@SerializedName("popularity")
val popularity: Double,
@SerializedName("poster_path")
val posterPath: String,
@SerializedName("release_date")
val releaseDate: String,
@SerializedName("title")
val title: String,
@SerializedName("video")
val video: Boolean,
@SerializedName("vote_average")
val voteAverage: Double,
@SerializedName("vote_count")
val voteCount: Int
)
MovieApiClient.kt
file and we need to adjust with few things :Adjust the end point with
movie/popular
So the final GET method of MovieApiClient.kt
will be :
@GET("movie/popular")
Since the request url doesn't need the request param then we removed the request file due to the HTTP GET not accepting the BodyRequest
Since the basic response doesn't match with our ApiResponse's params such as code, status or message then we need create our own ApiResponse.kt
file in the package lib (data -> lib) as follow :
data class ApiResponse<T>(
@SerializedName("results")
val data: T
)
then change the return value with the ApiResponse.kt
that we created before
import com.nbs.nucleodocumentation.data.lib.ApiResponse
....
fun getMovie(): Single<Response<ApiResponse<List<MovieItem>>>>
The final interface of MovieApiClient.kt
will be like this :
import com.nbs.nucleodocumentation.data.lib.ApiResponse
import com.nbs.nucleodocumentation.data.movie.model.response.MovieItem
import io.reactivex.Single
import retrofit2.Response
import retrofit2.http.GET
interface MovieApiClient {
@GET("movie/popular")
fun getMovie(): Single<Response<ApiResponse<List<MovieItem>>>>
}
MovieApiClient.kt
will impact to the MovieApi
since MovieApi
is the concrete class of MovieApiClient
then we need to adjust it by doing :Removing the request param
Do alt
+return/enter
in the red line override method to update automatically the method implementation
then the final class will be like this :
import com.nbs.nucleo.data.WebApi
import com.nbs.nucleodocumentation.data.lib.ApiResponse
import com.nbs.nucleodocumentation.data.movie.model.response.MovieItem
import io.reactivex.Single
import retrofit2.Response
class MovieApi(private val apiClient: MovieApiClient) : WebApi, MovieApiClient {
override fun getMovie(): Single<Response<ApiResponse<List<MovieItem>>>> = apiClient.getMovie()
}
MovieRepository.kt
so it only takes the data only in a list of movie and remove the request paraminterface MovieRepository : BaseRepository {
fun getMovie(): Single<List<MovieItem>>
}
MovieRequest
is the basic form of the request class that can encapsulate to define the required parameters to satisfy the request. In Nucleo framework we use this to keep the separation of concerns works among layer with defining required objects among them. This approach is also valid for all http methods supported by OKhttpClient and Retrofit.MovieRepository.kt
also will impact to its concrete class MovieDataStore.kt
so then let's adjust the method implementation in MovieDataStore.kt
class to thisclass MovieDataStore(
api: MovieApi
) : MovieRepository {
override val webService = api
override val dbService = null
override fun getMovie(): Single<List<MovieItem>> {
return webService.getMovie()
.lift(singleApiError())
.map {
it.data
}
}
}
Nucleo Framework implements Repository Pattern that defines the access data to the source so the outer layer would not know the source of its data. The usage of repository pattern will make us easier to abstract the process returning data without knowing the detail.
MovieUseCase.kt
and remove the param so the final code would be like this :interface MovieUseCase {
fun getMovie(): Single<List<Movie>>
}
MovieUseCase.kt
so then it impacts to its concrete class, MovieInteractor.kt
so let's adjust it and the final code will be like this :class MovieInteractor(private val repository: MovieRepository) : MovieUseCase {
override fun getMovie(): Single<List<Movie>> {
return repository.getMovie().map {
it.map { movieItem ->
movieItem.toDomain()
}
}
}
}
MovieInteractor.kt
will have an error in toDomain()
method so it would be fixed by adding the function in MovieItem.kt
to map the object from data to the domain so the presentation layer will only access the domain or ui model data that already mapped. But first let's open the domain model object in Movie.kt
class and update with the required attribut that to be shown in presentation or UI layer. Let's have it like this :@Parcelize
data class Movie(
val id: Int,
val title: String,
val overview: String,
val poster: String
) : Par
Then open the MovieItem.kt
and add the function as follow :
fun toDomain(): Movie{
return Movie(
id = id,
title = originalTitle,
poster = BuildConfig.BASE_IMAGE_URL + posterPath,
overview = overview
)
}
So the final MovieItem.kt
class will be
package com.nbs.nucleodocumentation.data.movie.model.response
import com.google.gson.annotations.SerializedName
import com.nbs.nucleodocumentation.BuildConfig
import com.nbs.nucleodocumentation.domain.movie.model.Movie
data class MovieItem(
@SerializedName("adult")
val adult: Boolean,
@SerializedName("backdrop_path")
val backdropPath: String,
@SerializedName("genre_ids")
val genreIds: List<Int>,
@SerializedName("id")
val id: Int,
@SerializedName("original_language")
val originalLanguage: String,
@SerializedName("original_title")
val originalTitle: String,
@SerializedName("overview")
val overview: String,
@SerializedName("popularity")
val popularity: Double,
@SerializedName("poster_path")
val posterPath: String,
@SerializedName("release_date")
val releaseDate: String,
@SerializedName("title")
val title: String,
@SerializedName("video")
val video: Boolean,
@SerializedName("vote_average")
val voteAverage: Double,
@SerializedName("vote_count")
val voteCount: Int
){
fun toDomain(): Movie{
return Movie(
id = id,
title = originalTitle,
poster = BuildConfig.BASE_IMAGE_URL + posterPath,
overview = overview
)
}
}
Why do we need mapping? We keep the each layer independent and only access the data that they understand. It keeps the separation of concerns work among these layers and make us easier to adjust if there's changes from data source for the instance if the the data layer produced the value of birthdate in YYYY-MM-DD form then the presentaion layer requires only '10 Sep 1988' so the transformation of this will be doing in the mapper function only and not adding some dirty logics in presentation layer.
So, what is domain layer exactly used for? it is orchestrator to proceed the business process, manipulated the data and doing required algorithm to achieve certain goals. Some usual questions is how does the better use case implementation? the answer is relative based on the organization to define the behaviour of use case. But in Nucleo Framework we are very open to both type of usage :
a. Use Case is contain of functions in scope eg : it can be containing some functions in related scope like membership which can be login, register, verify otp, resetPassword, changePassword etc
b. Use Case is only doing one specific business use cases
Register the movieModule.kt
in DI -> featuremodule to list of Koin Module in Application class, in this case is HelloWorldApplication.kt
so the final code of it will be like this :
class HelloWorldApplication : BaseApplication() {
override fun getDefinedModules(): List<Module> {
return listOf(
apiModule,
dbModule,
preferenceModule,
rxModule,
utilityModule,
movieModule
)
}
override fun initApp() {
Timber.plant(Timber.DebugTree())
}
}
Nucleo framework handles the data in observables, data to domain will be in RxJava observables and it will be transformed to LiveData in ViewModel
Nucleo generator also generates the Koin DI module to simplify and avoid the manual writing and defining the dependency file, if you open the MovieModule.kt
in di -> featuremodules you will see the code like this :
val movieModule = module {
single { ApiService.createReactiveService(MovieApiClient::class.java, get(), get(named(BASE_URL))) }
single { MovieApi(get()) }
single<MovieRepository> { MovieDataStore(get()) }
single<MovieUseCase> { MovieInteractor(get()) }
viewModel { MovieViewModel(get(), get()) }
}
it defines very clear about the dependency for each components and its scope across the application and yes, we don't need to adjust or to change this class at this time. And may be you have to do it when you are working with projects. You need to learn about how the Koin DI works in Android and Kotlin projects first before you do writing the customization .
After we've done with the domain layer then we need to do some polishings in presentation layer, let's get started with MovieViewModel.kt
. As you know that Nucleo generator generates the basic templates to all required class and it is included the ViewModel.kt so then we just need to update it to comply with specification in domain layer, its use case. So let's adjust the code so it can match with the final code below :
class MovieViewModel(
private val movieUseCase: MovieUseCase,
private val disposable: CompositeDisposable
) : BaseViewModel(disposable) {
private val _movie = MutableLiveData<Result<List<Movie>>>()
val movie: LiveData<Result<List<Movie>>> get() = _movie
init {
_movie.value = Result.default()
}
fun getMovie() {
_movie.value = Result.loading()
movieUseCase.getMovie()
.compose(singleScheduler())
.subscribe({
_movie.value = Result.success(it)
}, { genericErrorHandler(it, _movie) })
.addTo(disposable)
}
}
The generated class will have only single LiveData object but as the good practice to use it and also if you understand how the MVVM works so then we need add another one to be exposed and immutable to its consumer (Activity) and listen to the every data changes in mutable one.
The final step in managing the end to end process from data to presentation layer, let's attached the MovieViewModel.kt
in MainActivity.kt
to see how it works. At this stage we just want to print out the Movie Title in log so open the MainActivity.kt
then adjust to match with the final code like this :
class MainActivity : BaseActivity() {
private val movieViewModel: MovieViewModel by viewModel()
override val layoutResource = R.layout.activity_main
override fun initIntent() {
}
override fun initUI() {
}
override fun initAction() {
}
override fun initProcess() {
movieViewModel.getMovie()
}
override fun initObservers() {
movieViewModel.movie.observeLiveData(this,
onLoading = {
showLoading()
},
onSuccess = {
hideLoading()
printTitles(it.data)
},
onFailure = { throwable, message ->
hideLoading()
showToast(it.message.toString())
})
}
private fun printTitles(data: List<Movie>) {
data.forEach {
debug {
it.title
}
}
}
}
Don't forget to remove all //todo comments
TheResult
class will be fromimport com.nbs.nucleo.data.Result
Last but not least, let's add the INTERNET permission in AndroidManifest.xml
. Then run the project into your favorite device or emulator.
<uses-permission android:name="android.permission.INTERNET"/>
and also add the Application class as the attribute name in AndroidManifest.xml
: android:name=".base.HelloWorldApplication"
so the final code will be like this :
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nbs.nucleodocumentation">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".base.HelloWorldApplication"
android:theme="@style/AppTheme">
<activity android:name=".presentation.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Run the application and you will see the result.