This post was last Updated on by Himanshu Tyagi to reflect the accuracy and up-to-date information on the page.
Creating complicated Android applications requires high expertise and experience. To make the app successful, it should be flexible and meet users’ expectations.
Android developers can use several different architectures:
- Stable samples — Java
- Stable samples — Kotlin
- External samples
- Deprecated samples
- Samples in progress
Of course, developers use each of these samples depending on the objectives, approaches, etc.
However, there is a way to create an Android app architecture with a more coherent approach. In this post, you can find out how to develop a basic Android app. It will have features like loading the news list, adding them to favorites, and deleting the news.
Tech Stack to Develop an Android App
We want to explain the Android app architecture and provide some code samples. So, during the app development, the following technologies were used:
- Kotlin. The programming language allows writing less code and is entirely compatible with Java.
- AndroidX. The main library used for this project.
- Room SQLite is the database.
- Stetho was used to create the ability to view data in the database.
- Retrofit2 and RxJava2 are used to implement requests to the server and receive responses.
- Glide. It’s an extensive library for image processing.
- Android Architecture Components (LiveData, ViewModel, and Room) and ReactiveX (RxJava2, RxKotlin, and RxAndroid) are taken to build dependencies, dynamically change the data and desynchronize processes.
Also Read: Android VideoView Tutorial with Example Project in Android Studio
How to Begin the Project
The first step is to connect to AndroidX. Then in gradle.properties at the application level, you should write:
android.enableJetifier=true android.useAndroidX=true
After that, replace dependencies from Android to AndroidX in the build.gradle at the app module level.
buildscript { ext.kotlin_version = '1.3.0' ext.gradle_version = '3.2.1' repositories { google() jcenter() maven { url 'https://jitpack.io' } mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:$gradle_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }
Make a separate extended file for the rest of the dependencies. Then, it’s better to put all the dependencies without exceptions, including SDK versions. As a result, you can detach versioning and make an array of dependencies.
The version name and the name of the array are executed randomly. As a result, the implementation of the dependencies in the build.gradle at the application level will be done as follows:
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion rootProject.ext.compileSdkVersion as Integer buildToolsVersion rootProject.ext.buildToolsVersion as String dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //Test testImplementation testDependencies.junit androidTestImplementation testDependencies.runner androidTestImplementation testDependencies.espressoCore //Support implementation supportDependencies.kotlin implementation supportDependencies.appCompat implementation supportDependencies.recyclerView implementation supportDependencies.design implementation supportDependencies.lifecycleExtension implementation supportDependencies.constraintlayout implementation supportDependencies.multiDex
It’s high time to indicate multiDexEnabled true in the default configs. The number of used methods usually reaches the allowed limit quite fast.
Also Read: Android ListView Tutorial With Example Project
How to Create Basic Components
The MVVM or Model-View-ViewModel pattern was taken as the basis during the development of the basic architecture.
First, You should create a class that will follow Application().
@SuppressWarnings("all") class App : Application() { companion object { lateinit var instance: App private set } override fun onCreate() { super.onCreate() instance = this Stetho.initializeWithDefaults(this) DatabaseCreator.createDatabase(this) } }
After that, create essential app components, starting with a ViewModel.
abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) { override fun onCleared() { super.onCleared() } }
The app has simple functionality, so there are three LiveDatas in the ViewModel
- LiveData handles errors.
- LiveData processes the loading by displaying the progress bar.
- LiveData for processing the receipt and availability of data in the adapter to display the placeholder in their absence. We need this LiveData because our application has lots of lists.
val errorLiveData = MediatorLiveData<String>() val isLoadingLiveData = MediatorLiveData<Boolean>() val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>()
You can use Consumer to transfer the results of functions to LiveData.
As a result, our BaseViewModel looks like this:
abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) { val errorLiveData = MediatorLiveData<String>() val isLoadingLiveData = MediatorLiveData<Boolean>() val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>() private var compositeDisposable: CompositeDisposable? = null protected open val onErrorConsumer = Consumer<Throwable> { errorLiveData.value = it.message } fun setLoadingLiveData(vararg mutableLiveData: MutableLiveData<*>) { mutableLiveData.forEach { liveData -> isLoadingLiveData.apply { this.removeSource(liveData) this.addSource(liveData) { this.value = false } } } } override fun onCleared() { isLoadingLiveData.value = false isEmptyDataPlaceholderLiveData.value = false clearSubscription() super.onCleared() } private fun clearSubscription() { compositeDisposable?.apply { if (!isDisposed) dispose() compositeDisposable = null } } }
Now it’s time to integrate an ability to display all possible errors in our future activity processes. Let’s utilize ordinary Toast.
protected open fun processError(error: String) = Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
Then let’s pass the error message to the display method:
protected open val errorObserver = Observer<String> { it?.let { processError(it) } }
One of the last steps is setting an ID for view in the onCreate method.
if (hasProgressBar()) { vProgress = findViewById(progressBarId()) vProgress?.setOnClickListener(null) } vPlaceholder = findViewById(placeholderId()) startObserveLiveData()
Now you can understand the principles to use while creating the primary fragments. Developing separate screens, you need to pay attention to the further scalability of the app. For example, you can create a particular component with its ViewModel.
Also Read: Android WebView Tutorial With An Example Project + Download Code
The Solution Architecture
Having all the essential components, it’s high time to move with the app architecture. It can be similar to Uncle Bob’s architecture. However, it’s possible to use RxJava2 to eliminate Boundaries interfaces in favor of the Observable and Subscriber.
Reactive Java tools allow implementing data conversion to work with them more conveniently. This change applies to both working with server responses and cooperation with databases.
In addition to the primary model, you can develop the server response model and a separate table model for Room. The data conversion between these models allows making changes to the conversion process, converting server responses and instantly saving the necessary data to the database before displaying them on the UI.
Fragments are responsible for the UI, while ViewModels Fragments is responsible for the business logic execution. ViewModels get information from the provider by initializing it through val … by lazy {} if an immutable object is needed.
After completing the business logic, transferring data for changing the UI is necessary. Let’s build a new MutableLiveData in the ViewModel to use in the observeLiveData() method.
interface BaseDataConverter<IN, OUT> { fun convertInToOut(inObject: IN): OUT fun convertOutToIn(outObject: OUT): IN fun convertListInToOut(inObjects: List<IN>?): List<OUT>? fun convertListOutToIn(outObjects: List<OUT>?): List<IN>? fun convertOUTtoINSingleTransformer(): SingleTransformer<IN?, OUT> fun convertListINtoOUTSingleTransformer(): SingleTransformer<List<OUT>, List<IN>> } abstract class BaseDataConverterImpl<IN, OUT> : BaseDataConverter<IN, OUT> { override fun convertInToOut(inObject: IN): OUT = processConvertInToOut(inObject) override fun convertOutToIn(outObject: OUT): IN = processConvertOutToIn(outObject) override fun convertListInToOut(inObjects: List<IN>?): List<OUT> = inObjects?.map { convertInToOut(it) } ?: listOf() override fun convertListOutToIn(outObjects: List<OUT>?): List<IN> = outObjects?.map { convertOutToIn(it) } ?: listOf() override fun convertOUTtoINSingleTransformer() = SingleTransformer<IN?, OUT> { it.map { convertInToOut(it) } } override fun convertListINtoOUTSingleTransformer() = SingleTransformer<List<OUT>, List<IN>> { it.map { convertListOutToIn(it) } } protected abstract fun processConvertInToOut(inObject: IN): OUT protected abstract fun processConvertOutToIn(outObject: OUT): IN }
By choosing third-party APIs, you can receive an empty response from the server, and there can be many reasons. It’s better to create NullOrEmptyConverterFactory to handle these situations.
class NullOrEmptyConverterFactory : Converter.Factory() { fun converterFactory() = this override fun responseBodyConverter(type: Type?, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, Any>? { return Converter { responseBody -> if (responseBody.contentLength() == 0L) { null } else { type?.let { retrofit.nextResponseBodyConverter<Any>(this, it, annotations)?.convert(responseBody) } } } } }
Having declared all the methods executing business logic in the ViewModel, it’s time to call the fragment method. After that, in the observeLiveData(), it’s necessary to process the results of each declared LiveData.
We’ve chosen ‘Apple’ in the search parameters SEARCH_FOR to make the implementation more manageable. After that, we’ll sort the items by popularity tag. If needed, you may add a minimum functionality to change these parameters.
Let’s call the NewsFragment method:
private fun loadLikedNews() { viewModel.loadLikedNews() }
Since our database is empty, the loadNews() method will start. As a result, in the viewModel.loadNewsSuccessLiveData.observe(..){news→ }, we’ll get the list of the news and transfer it to the adapter:
isEmptyDataPlaceholderLiveData.value = news.articles?.isEmpty() with(newsAdapter) { news.articles?.toMutableList()?.let { clear() addAll(it) } notifyDataSetChanged() } loadNewsSuccessLiveData.value = null
Having run the application, you have the following result:
The UI fragment of Favorites has only one screen to show liked news. Additionally, it has only one option to clear the database. If you press ‘Like,’ the screens will look as follows:
Why Should You Simplify the Clean Android Architecture?
As you can see, the discussed architecture follows the primary principles of Clean Android App Architecture. Developers tend to choose this architecture to build an Android app.
What is an Entity in the Clean Architecture? Simply saying, it’s everything that doesn’t depend on the particular application but, at the same time, is familiar to many apps. However, in mobile development, these are objects of a business application containing the most general and high-level rules (business logic).
In our solution, Gateways is the Repository for working with the database and the network’s module.
Technologies like RxJava2, KotlinRx, and Kotlin LiveData solve assigned tasks more structured and easy to understand for a developer, while the code is easily readable and scalable.
Author’s bio: Maria Diachenko is a tech writer at Cleveroad. It’s a web, iOS, and Android app development company in Ukraine. Maria enjoys making how-to tech guides and describing programming trends and IoT innovations.