Android LiveData on Kotlin using Retrofit and coroutines

  • Tutorial
This article is about using Android Components ViewModel, LifeCycle and LiveData. These components allow you to not care about the life cycle of the Activity.

Also considered an example of the use of modern Coroutines in conjunction with the repository on the Retrofit.

funmain(args: Array<String>): Unit = runBlocking {
    // Wait (suspend) for Resultval result: Result<User> = api.getUser("username").awaitResult()
    // Check result typewhen (result) {
        //Successful HTTP resultis Result.Ok -> saveToDb(result.value)
        // Any HTTP erroris Result.Error -> log("HTTP error with code ${result.error.code()}", result.error)
        // Exception while request invocationis Result.Exception -> log("Something broken", e)
    }
}

Retrofit coroutines extension

kotlin-coroutines-retrofit
Extension for Retrofit on Kotlin. These are just two files. I just added them to the project. You can connect them through Dependency in Gradle. There are usage examples on Github.
Also connect Adapter addCallAdapterFactory (CoroutineCallAdapterFactory ()) .
ServerAPI and Repository are in the same

REST API file by

implementing the REST API on Kotlin. It does not have any specific changes.

ServerAPI
import android.arch.lifecycle.MutableLiveData
import android.util.Log
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.experimental.CoroutineCallAdapterFactory
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.http.*
import ru.gildor.coroutines.retrofit.Result
import ru.gildor.coroutines.retrofit.awaitResult
object ServerAPI {
    var API_BASE_URL: String = getNetworkHost();
    var httpClient = OkHttpClient.Builder().addInterceptor(
        HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
        })
    var builder: Retrofit.Builder = Retrofit.Builder()
            .baseUrl(API_BASE_URL)
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .addConverterFactory(GsonConverterFactory.create())
    var retrofit = builder
            .client(httpClient.build())
            .build()
    var netService = retrofit.create<NetService>(
        NetService::class.java!!)interfaceNetService{
        @GET("api/stores")fungetStoreAll(@Header("Authorization") bearer: String): Call<Array<Store>>
    }
}


LiveData

Next, consider the Repository. This is the main service for getting livedata. Initialize LiveData with the load state: Resource.loading (null) . Next, we expect the end of the request. AwaitResult () This call should be in the Coroutin async (UI) block.

At the end of the request, we can handle the result. If all is well, the result will be saved in mutableLiveData.value = Resource.success (result.value) The important point is that the link to the new instance should be, otherwise observer LiveData will not work. see: new Resource <> (SUCCESS, data, null);

Repository
classRepository{
    fungetStores(token: String)  :  MutableLiveData<Resource<Array<Store>>>{
        val mutableLiveData = MutableLiveData<Resource<Array<Store>>>()
        mutableLiveData.value = Resource.loading(null)
        val req = PostsAPI.netService.getStoreAll(token)
        try {
            async(UI) {
                val result = req.awaitResult()
                // Check result typewhen (result) {
                    //Successful HTTP resultis Result.Ok -> {
                        mutableLiveData.value = Resource.success(result.value)
                    }
                    // Any HTTP erroris Result.Error -> {
                        mutableLiveData.value  = Resource.error("Http Error!", null)
                    }
                    // Exception while request invocationis Result.Exception -> Log.d(TAG, result.exception.message)
                }
            }
        } catch (e: Exception) {
            Log.d(TAG, e.toString())
        }
        return mutableLiveData
    }
}
     


Wrapper data

Wrapper - Resource <T> is used to handle errors and transfer status to the Fragment .

It store three states:

publicenumStatus{ SUCCESS, ERROR, LOADING }

The data itself:

@Nullablepublicfinal T data;

Resource <T>
A generic class that contains data and status about loading this data

// A generic class that contains data and status about loading this data.publicclassResource<T> {
    @NonNullpublicfinal Status status;
    @Nullablepublicfinal T data;
    @Nullablepublicfinal String message;
    privateResource(@NonNull Status status, @Nullable T data,
            @Nullable String message){
        this.status = status;
        this.data = data;
        this.message = message;
    }
    publicstatic <T> Resource<T> success(@NonNull T data){
        returnnew Resource<>(Status.SUCCESS, data, null);
    }
    publicstatic <T> Resource<T> error(String msg, @Nullable T data){
        returnnew Resource<>(Status.ERROR, data, msg);
    }
    publicstatic <T> Resource<T> loading(@Nullable T data){
        returnnew Resource<>(Status.LOADING, data, null);
    }
    publicenum Status { SUCCESS, ERROR, LOADING }
}
      


ViewModel

StoresViewModel requests data from the repository and stores in the internal variable stores

val api = Repository()
stores = api.getStores(token)

Viewmodel
classStoresViewModel (context: Context, token: String) : ViewModel() {
    val stores: MutableLiveData<Resource<Array<Store>>>
    init {
        val api = Repository()
        stores = api.getStores(token)
    }
}
      


ViewModelProviders

To transfer parameters to the ViewModel, we will extend the standard ViewModelProviders
For example, two parameters (Login, Password) are required to transfer to the LoginViewModel . To send a token to StoresViewModel, use one (Token)

AppViewModelFactory
classAppViewModelFactory(privateval contect: Context, vararg params: Any) :
    ViewModelProvider.NewInstanceFactory() {
    privateval mParams: Array<out Any>
    init {
        mParams = params
    }
    overridefun<T : ViewModel>create(modelClass: Class<T>): T {
        returnif (modelClass == LoginViewModel::class.java) {
            LoginViewModel(contect, mParams[0] as String, mParams[1] as String) as T
        } elseif (modelClass == StoresViewModel::class.java) {
            StoresViewModel(contect, mParams[0] as String) as T
        } else {
            super.create(modelClass)
        }
    }
}
      


Fragment

Getting StoresViewModel:

viewModel = ViewModelProviders.of(this, AppViewModelFactory(requireActivity(), tokenHolder.token)).get(StoresViewModel::class.java)

Using Observer for data changes:

// Observe data on the ViewModel, exposed as a LiveData
 viewModel.stores.observe(this, Observer<Resource<Array<Store>>> { storesResource ->

Fragment
overridefunonCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.stores_fragment, container, false)
        val tokenHolder = TokenHolder(PreferenceManager.getDefaultSharedPreferences(requireActivity()))
        viewModel = ViewModelProviders.of(this, AppViewModelFactory(requireActivity(), tokenHolder.token)).get(StoresViewModel::class.java)
        recyclerView = view.findViewById<RecyclerView>(R.id.store_list).apply {
            setHasFixedSize(true)
        }
        return view
    }
    overridefunonActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        // Observe data on the ViewModel, exposed as a LiveData
        viewModel.stores.observe(this, Observer<Resource<Array<Store>>> { storesResource ->
            val stores = storesResource?.data
            stores?.let {
                viewAdapter = StoresAdapter(stores!!)
                recyclerView.adapter = viewAdapter
            }
            if (storesResource?.status  == Resource.LOADING){
                log("Loading...")
            }
            if (storesResource?.status  == Resource.ERROR){
                log("Error : " + storesResource?.message)
            }
        })
    }
      


PS

To store the Token and use it throughout the application, I applied the library / extension from Fabio Collini. The application is well described in his article. The link is on the page in Github or lower in this article.

prefs-delegates by Fabio Collini

classTokenHolder(prefs: SharedPreferences) {
    var token by prefs.string()
        privatesetvar count by prefs.int()
        privatesetfunsaveToken(newToken: String) {
        token = newToken
        count++
    }
}

Gradle

    implementation 'android.arch.lifecycle:extensions:1.1.1'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.30.0"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.30.0"
    implementation "com.squareup.retrofit2:retrofit:2.4.0"
    implementation "com.squareup.retrofit2:converter-gson:2.4.0"
    implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:1.0.0"
    // If you use Kotlin 1.2or1.3
    // compile 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.13.0'
    // compile 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.13.0-eap13'

Links

All in one example

Android Architecture Components samples

LiveData Overview

Async code using Kotlin Coroutines

Multithreading and Kotlin

Only registered users can participate in the survey. Sign in , please.

What development tools and programming language for Android do you use?


Also popular now: