Android App Architecture – How to Do It Right?

0
205

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 Android app architecture with a more coherent approach. In this post, you can find how to develop a basic Android app. It will have several features like loading the news list, adding them to favorites, and deleting the news.

Also ReadUltimate Guide For Avoiding Mistakes During Android App Development

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, along with RxJava2, is 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.

tech stack for an android app

Also ReadAndroid VideoView Tutorial with Example Project in Android Studio

How to Begin the Project

The first step is to connect 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 there 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 ReadAndroid 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 of all, 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 components of the app, 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 that handles errors.
  • LiveData that processes the loading displaying the progress bar.
  • LiveData for processing the process of 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 the 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 ReadAndroid 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 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 both working with server responses and cooperation with databases.

In addition to the primary model, you can also 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, convert server responses and instantly save the necessary data to the database before displaying them on the UI.

Fragments are responsible for the UI, while ViewModels Fragments are in charge of 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, it’s necessary to transfer data for changing the UI. 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 the 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.

To make the implementation more manageable, we’ve chosen ‘Apple’ in the search parameters SEARCH_FOR. 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:

news app developed - android app architecture

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:

ui fragments of android app

Also ReadHow Long Does It Take For An App To Be Approved By Apple?

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 easy to understand for a developer, while the code is easily readable and scalable.

Also Read5 Best Android App Development Frameworks In 2020

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, describing programming trends and IoT innovations.