ddd

Android architecture components are essentially a  collection of libraries that help us developers build testable,maintainable and robust native Android apps. This is one of the four components of Android Jetpack. Android Jetpack components bring together the existing Support Library and Architecture Components to bring exciting libraries which greatly reduce boilerplate code and increase consistency of the codebase across multiple Android versions and devices

Some of  the Android Architecture Components include:

  • Data Binding: It helps to bind your layout XML UI elements  to data sources of our app.
  • Lifecycles: It manages activity and fragment lifecycles of our app, survives configuration changes, avoids memory leaks and easily loads data into our UI.
  • LiveData: This is a lifecycle aware observable data holder.This lets the components in your app observe LiveData objects for changes
  • Navigation: It handles everything needed for in-app navigation in Android application.
  • Paging: It helps in loading data partially and in chunks to allow for efficient use of system resources.
  • Room: Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.
  • ViewModel: The ViewModel class is designed to hold and manage UI-related data in a life-cycle conscious way. This allows data to survive configuration changes such as screen rotations..
  • WorkManager: It manages every background jobs in Android with the circumstances we choose allowing for increased backward compatibility.

We will focus this article on DataBinding,ViewModel and LiveData.We will use MVVM(Model-View-ViewModel) architecture pattern with Kotlin as the programming language

CocktailsDB API

CocktailsDB is an open,crowd-sourced database consisting of hundreds of drinks and cocktails from all over the world.For the purpose of this article,we will use their excellent free JSON API.We will proceed to make a simple app that queries this API and gets a list of alcoholic cocktails and populate them in a RecyclerView on the app using as many Architecture components along the process.Consider upgrading to their premium service as well,if you enjoyed using the API,to unlock extra features

Analyzing the Recipe

Lets have a look at the API endpoint

https://www.thecocktaildb.com/api/json/v1/1/filter.php?a=Alcoholic

The "a" is a Query parameter.This is a key value pair to communicate how you want to proceed to filter the cocktails in the database.The "a" indicates you wish you filter by whether the cocktails are alcoholic or not.The value  is "Alcoholic" as our intended outcome of the app is to filter by alcoholic cocktails only

The response(truncated for display purposes)

{
    "drinks": [
        {
            "strDrink": "'57 Chevy with a White License Plate",
            "strDrinkThumb": "https://www.thecocktaildb.com/images/media/drink/qyyvtu1468878544.jpg",
            "idDrink": "14029"
        },
         {
            "strDrink": "Archbishop",
            "strDrinkThumb": "https://www.thecocktaildb.com/images/media/drink/4g6xds1582579703.jpg",
            "idDrink": "11052"
        },
        {
            "strDrink": "Arctic Fish",
            "strDrinkThumb": "https://www.thecocktaildb.com/images/media/drink/ttsvwy1472668781.jpg",
            "idDrink": "14622"
        },
        {
            "strDrink": "Arctic Mouthwash",
            "strDrinkThumb": "https://www.thecocktaildb.com/images/media/drink/wqstwv1478963735.jpg",
            "idDrink": "17118"
        },
        {
            "strDrink": "Arise My Love",
            "strDrinkThumb": "https://www.thecocktaildb.com/images/media/drink/wyrrwv1441207432.jpg",
            "idDrink": "11053"
        }
    ]
}

The response returns in essence a JSON array "drinks" with each drinks item having 3 properties; The cocktail name,the cocktail thumbnail picture and a unique identifier for the cocktail. This data will consequently be used in populating the RecyclerView item later

Prerequisites

To follow this tutorial, you should have basic knowledge of working with:

  • Kotlin
  • Retrofit(Networking Library)
  • Coroutines

Let's start mixing

Proceed to Android Studio and create a Kotlin Project

The Ingredients

Add the following dependencies in the root level build.gradle file

buildscript {
    ext.kotlin_version = '1.3.72'
    ext.lifecycle_version = '2.2.0'
    ext.retrofit_version = '2.7.1'
    ext.gson_version = '2.7.1'
    //...
    }

Then the following dependencies in app level build.gradle file

//retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
//Gson
    implementation "com.google.code.gson:gson:$gson_version"
//Glide,cardview,recyclerview
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'com.github.bumptech.glide:glide:4.9.0'

    //LifeCycle
    implementation "androidx.lifecycle:lifecycle-common:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"
    implementation "android.arch.lifecycle:extensions:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"


    //Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"

The Model

The model is responsible for providing data sources to the ViewModel, including entity classes, network requests, and local storage

As we can see, ViewModel knows Model but does not know View and View can know ViewModel but does not know Model.This is the abstraction that MVVM achieves,the separate layers of presentation layer,domain layer(business logic) and data layer(data access and storage).This separation of concerns allows for easier maintainability and testability of our Android apps.

data class Drinksitem (
    var idDrink: String,
    var strDrink: String,
    var strDrinkThumb: String)
data class DrinksList (
 var drinks:List<DrinksModel>
)

Create a package called model.In it,setup these two data classes; DrinkList references the first object in the json response,the drinks JSONArray with the cocktail items.Consequently DrinksItem is the model class for the individual cocktail item

The Network

We will use Retrofit library and Coroutines for the networking calls.Coroutines is used in combination with Retrofit to make API calls in the separate asynchronous thread.

Let us start with the Retrofit Service class

interface DrinksApi {

    @GET("filter.php?")
    suspend fun filterbycategory(@Query("a") c: String?): DrinksModel
}

NB:We will revisit the suspend keyword later in the article

Retrofit Builder Class:

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitBuilder {

    private const val BASE_URL = "https://www.thecocktaildb.com/api/json/v1/1/"

    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build() //Doesn't require the adapter
    }

    val apiService: DrinksApi = getRetrofit().create(DrinksApi::class.java)
}

We are using the repository pattern. When the ViewModel requests the data that will be used for the View, the Repository will provide you this by choosing the appropriate data locally or on the network. At this point, the ViewModel doesn’t care if the requested data was retrieved locally or from the network. You can make it independent of a particular implementation by using Repository as an interface and creating an implementation.For this tutorial however,we are fetching the data from network so the repository will conform to that

class DrinksRepo(private val apiService: DrinksApi) {

    suspend fun getDrink(category: String) = apiService.filterbycategory(category)
}

The UI State

We need a utility class that will be responsible to communicate the current state of Network Call to the Presentation Layer.Create a package utils and add the following classes

enum class StateStatus {
    SUCCESS,
    ERROR,
    LOADING
}
data class Resource<out T>(val status: StateStatus, val data: T?, val message: String?) {
    companion object {
        fun <T> success(data: T): Resource<T> =
            Resource(status = StateStatus.SUCCESS, data = data, message = null)

        fun <T> error(data: T?, message: String): Resource<T> =
            Resource(status = StateStatus.ERROR, data = data, message = message)

        fun <T> loading(data: T?): Resource<T> =
            Resource(status = StateStatus.LOADING, data = data, message = null)
    }
}

The ViewModel

Now we are ready for the ViewModel.The ViewModel architecturally is responsible for the application logic.Our ViewModel will fetch the cocktails from the repository using a coroutine

package com.cocktails.mydrinksapp.viewmodels

import android.util.Log
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.cocktails.mydrinksapp.repo.DrinksRepo
import com.cocktails.mydrinksapp.utils.ResourceStatus
import kotlinx.coroutines.Dispatchers


class DrinksViewModel(private val drinksrepo: DrinksRepo) : ViewModel() {

    fun getCocktails(category: String) = liveData(Dispatchers.IO) {
        emit(ResourceStatus.loading(data = null))
        try {
            emit(ResourceStatus.success(data = drinksrepo.getDrink(category)))
        } catch (exception: Exception) {
            emit(ResourceStatus.error(data = null, message = exception.message ?: "Error Occurred!"))
        }
    }

    companion object {


        @BindingAdapter("imageUrl")
        @JvmStatic
        fun loadimage(imageView: ImageView, imageUrl: String?) {
            if (!imageUrl.isNullOrEmpty()) {

                Glide.with(imageView.context).load(imageUrl)
                    .apply(RequestOptions.centerCropTransform())
                    .into(imageView)

            } 
        }


    }
    }

We also create a BindingAdapter.A binding adapter is simply a static or instance method that is used to manipulate how some user defined attributes map data bound variables to views.We use Glide,an excellent image loading library to efficiently load an image into the ImageView of the recyclerview item we will define later.BindingAdapters allow for re-use of logic that apply to view properties throughout the codebase

To instantiate a ViewModel you need a ViewModelFactory: it’s a class that implements ViewModelProvider.Factory and it will create the ViewModel from a parameter .class

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.cocktails.mydrinksapp.network.DrinksApi
import com.cocktails.mydrinksapp.repo.DrinksRepo


class ViewModelFactory(private val apiService: DrinksApi) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(DrinksViewModel::class.java)) {
            return DrinksViewModel(DrinksRepo(apiService)) as T
        }
        throw IllegalArgumentException("Unknown class name")
    }

}

The View

Let us set up the MainActivity class below


package com.cocktails.mydrinksapp

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager

import com.cocktails.mydrinksapp.adapter.DrinksAdapter
import com.cocktails.mydrinksapp.model.DrinksItem
import com.cocktails.mydrinksapp.network.RetrofitBuilder
import com.cocktails.mydrinksapp.utils.StateStatus
import com.cocktails.mydrinksapp.viewmodels.DrinksViewModel
import com.cocktails.mydrinksapp.viewmodels.ViewModelFactory
import kotlinx.android.synthetic.main.activity_main.*


class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: DrinksViewModel
    private lateinit var adapter: DrinksAdapter
    private val  category = "Alcoholic"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProviders.of(
            this,
            ViewModelFactory(RetrofitBuilder.apiService)
        ).get(DrinksViewModel::class.java)
       
        recyclerView.layoutManager = LinearLayoutManager(this)
        setupObservers()
    }








    private fun setupObservers() {
        viewModel.getCocktails(category).observe(this, Observer {
            it?.let { resource ->
                when (resource.status) {
                    StateStatus.SUCCESS -> {
                        recyclerView.visibility = View.VISIBLE
                        progressBar.visibility = View.GONE
                        resource.data?.let { lstdrinks

                            -> setList(lstdrinks.drinks)

                        }
                    }
                    StateStatus.ERROR -> {
                        recyclerView.visibility = View.VISIBLE
                        progressBar.visibility = View.GONE
                        Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()

                    }
                    StateStatus.LOADING -> {
                        progressBar.visibility = View.VISIBLE
                        recyclerView.visibility = View.GONE
                    }
                }
            }
        })
    }

    private fun setList(listdr: List<DrinksItem>) {
        adapter = DrinksAdapter(listdr)
        recyclerView.adapter = adapter

    }
}

The respective layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

So what is happening here? We start by initialising the ViewModel with the required dependency.We then proceed by setting appropiate layout manager for the recyclerview. The method setupobservers() is responsible for calling the ViewModel method that contains our logic.Dependent on the 3 states of the networking call,each state is handled appropiately.On successful response,the JSONArray is parsed to extract the list of cocktails which is subsequently passed to the recyclerview adapter.

The Adapter

Let's have a look at the Adapter


class DrinksAdapter(private  var drinkslist:List<DrinksItem>): RecyclerView.Adapter<DrinksAdapter.ViewHolder>() {



    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DrinksAdapter.ViewHolder {
        val binding: RowItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.row_item, parent, false)
        return ViewHolder(binding)


    }

    override fun onBindViewHolder(holder: DrinksAdapter.ViewHolder, position: Int) {
        holder.bind(drinkslist[position])
        holder.itemView.tag = position





    }



    override fun getItemCount(): Int {
        return drinkslist.size
    }


    class ViewHolder(private val binding: RowItemBinding): RecyclerView.ViewHolder(binding.root){
        private val viewModel = RowItemViewModel()


        fun bind(imgpojo: DrinksItem){
            viewModel.bind(imgpojo)
            binding.viewModel = viewModel
   }


    }

The respective layout

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"

    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="com.cocktails.mydrinksapp.viewmodels.RowItemViewModel" />

    </data>
    <LinearLayout
        android:orientation="vertical"
        android:layout_marginTop="10dp"
        android:layout_marginLeft="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"

            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:paddingRight="15dp">



            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:padding="5dp">
                <androidx.cardview.widget.CardView
                    android:layout_width="120dp"
android:background="@android:color/white"
                    android:layout_height="wrap_content"


                    card_view:cardCornerRadius="7dp"
                    card_view:cardElevation="2dp"
                    >
                    <ImageView
                        android:id="@+id/thumbnail"
                        android:layout_width="120dp"
                        app:imageUrl="@{viewModel.thumbnail}"
                        android:layout_height="100dp"/>
                </androidx.cardview.widget.CardView>

                <TextView
                    android:id="@+id/drinkname"
                    android:layout_width="120dp"
                    android:layout_height="wrap_content"
                    android:gravity="center_vertical"
android:textColor="@color/colorPrimaryDark"
android:layout_marginStart="20dp"
                    android:layout_marginTop="5dp"
                    android:text="@{viewModel.title}"
                    android:textSize="12sp" />

            </LinearLayout>

        </LinearLayout>

    </LinearLayout>
</layout>

Data Binding

Data Binding uses declarative layouts and minimises the glue code between programming code and XML.Let us enable this by editing the app level build.gradle file

android {
    
    dataBinding {
        enabled = true
    }
    
    // ...
    }

The Row Item ViewModel

package com.cocktails.mydrinksapp.viewmodels

import android.R
import android.content.Context
import android.content.res.ColorStateList
import android.util.Log
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.cocktails.mydrinksapp.model.DrinksItem


class RowItemViewModel: ViewModel() {
    private val thumbnail = MutableLiveData<String>()
    private val title = MutableLiveData<String>()


    fun bind(child: DrinksItem){
        thumbnail.value = child.strDrinkThumb
        title.value = child.strDrink

    }

    fun getThumbnail():MutableLiveData<String>{
        return thumbnail
    }

    fun getTitle():MutableLiveData<String>{
        return title
    }

}

app:imageUrl="@{viewModel.thumbnail}" and android:text="@{viewModel.title}" attributes are used to display the data in the imageview and Textview respectively from the LiveData elements in RowItemViewModel.

The bound data(thumbnail and title) are LiveData objects.Remember LiveData is lifecycle-aware, meaning it respects the lifecycle state of the app components  and ensures that LiveData only updates the component (the observer) when it’s in an active lifecycle state. This behavior prevents object leaking and ensures the app doesn’t do more work than it should.

Run your app now and you should see something like this

The Garnish

This tutorial was meant to be a very basic and brief introduction to Android Architecture Components showcasing how to consume the CocktailsDB api.Here are tips on how to expand this app using Architecture Components

  1. Dependency Injection using Hilt
  2. Storing the data from remote sources locally for offline usage using Room
  3. Efficiently load and display huge chunks of data using Paging

Find the full source code in this Github link.Find as well a production app I built using this API here.

"Good source code is magic,but with words"

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.