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.
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.
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);
Wrapper data
Wrapper - Resource <T> is used to handle errors and transfer status to the Fragment .
It store three states:
The data itself:
ViewModel
StoresViewModel requests data from the repository and stores in the internal variable stores
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)
Fragment
Getting StoresViewModel:
Using Observer for data changes:
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
Gradle
Links
All in one example
Android Architecture Components samples
LiveData Overview
Async code using Kotlin Coroutines
Multithreading and Kotlin
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.