Android LiveData на Kotlin с использованием Retrofit и coroutines

—

от автора

Статья рассказывает об использовании Android Components ViewModel, LifeCycle и LiveData. Эти компоненты позволяют не заботиться о жизненном цикле Activity.
Так же рассмотрен пример применения современного Coroutines в связке с репозитарием на Retrofit

fun main(args: Array<String>): Unit = runBlocking {     // Wait (suspend) for Result     val result: Result<User> = api.getUser("username").awaitResult()     // Check result type     when (result) {         //Successful HTTP result         is Result.Ok -> saveToDb(result.value)         // Any HTTP error         is Result.Error -> log("HTTP error with code ${result.error.code()}", result.error)         // Exception while request invocation         is Result.Exception -> log("Something broken", e)     } } 

Retrofit coroutines extension
kotlin-coroutines-retrofit
Расширение для Retrofit на Kotlin. Это всего два файла. Я просто добавил их в проект. Вы можете подключить их через Dependency в Gradle. На Github есть примеры использования.
Также подключаем Adapter addCallAdapterFactory(CoroutineCallAdapterFactory()).
ServerAPI и Repository находятся в одном файле

REST API
Реализацией REST API на Kotlin. Она не имеет каких либо специфичных изменений

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!!)      interface NetService {         @GET("api/stores")         fun getStoreAll(@Header("Authorization") bearer: String): Call<Array<Store>>     } } 

LiveData
Далее рассмотрим Repository. Это основной сервис получения LiveData. Инициализируем LiveData состоянием загрузки: Resource.loading(null). Далее ожидаем окончание запроса awaitResult() Этот вызов должен быть в блоке Coroutin async(UI)
По окончанию запроса мы можем хэндлить результат. Если все хорошо результат будет сохранен в mutableLiveData.value = Resource.success(result.value) Важный момент — это должена быть ссылка на новый экзэмпляр, иначе observer LiveData не отработает. see: new Resource<>(SUCCESS, data, null);

Repository

   class Repository {      fun getStores(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 type                 when (result) {                     //Successful HTTP result                     is Result.Ok -> {                         mutableLiveData.value = Resource.success(result.value)                     }                     // Any HTTP error                     is Result.Error -> {                         mutableLiveData.value  = Resource.error("Http Error!", null)                     }                     // Exception while request invocation                     is Result.Exception -> Log.d(TAG, result.exception.message)                 }             }         } catch (e: Exception) {             Log.d(TAG, e.toString())         }         return mutableLiveData     } }      

Wrapper data
Для обработки ошибок и передачи состояния в Fragment используется Wrapper — Resource<T>.
Он хранить три состояния:

public enum Status { SUCCESS, ERROR, LOADING }

Cами данные:

@Nullable public final 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. public class Resource<T> {     @NonNull public final Status status;     @Nullable public final T data;     @Nullable public final String message;     private Resource(@NonNull Status status, @Nullable T data,             @Nullable String message) {         this.status = status;         this.data = data;         this.message = message;     }      public static <T> Resource<T> success(@NonNull T data) {         return new Resource<>(Status.SUCCESS, data, null);     }      public static <T> Resource<T> error(String msg, @Nullable T data) {         return new Resource<>(Status.ERROR, data, msg);     }      public static <T> Resource<T> loading(@Nullable T data) {         return new Resource<>(Status.LOADING, data, null);     }      public enum Status { SUCCESS, ERROR, LOADING } }       

ViewModel
StoresViewModel запрашивает данные у репозитория и сохраняет во внутренней переменной stores

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

ViewModel

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

ViewModelProviders
Для передачи параметров в ViewModel расширим стандартную ViewModelProviders
Например для передачи в LoginViewModel надо два параметра (Login,Password). Для передачи токена в StoresViewModel используется один (Token)

AppViewModelFactory

class AppViewModelFactory(private val contect: Context, vararg params: Any) :     ViewModelProvider.NewInstanceFactory() {     private val mParams: Array<out Any>      init {         mParams = params     }      override fun <T : ViewModel> create(modelClass: Class<T>): T {         return if (modelClass == LoginViewModel::class.java) {             LoginViewModel(contect, mParams[0] as String, mParams[1] as String) as T         } else if (modelClass == StoresViewModel::class.java) {             StoresViewModel(contect, mParams[0] as String) as T         } else {             super.create(modelClass)         }     } }       

Fragment
Получение StoresViewModel:

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

Использование наблюдателя Observer за изменением данных:

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

Fragment

    override fun onCreateView(         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     }       override fun onActivityCreated(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)             }          })     }       

P.S.

Для хранения Token и использования его во всем приложении я применил библиотеку/расширение от Fabio Collini. Применение хорошо описано в его статье. Ссылка есть на странице в Github или ниже в этой статье.
prefs-delegates by Fabio Collini

class TokenHolder(prefs: SharedPreferences) {     var token by prefs.string()         private set      var count by prefs.int()         private set      fun saveToken(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.2 or 1.3     // compile 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.13.0'     // compile 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.13.0-eap13' 

Links
Все в одном примере
github.com/googlesamples/android-sunflower

Android Architecture Components samples
github.com/googlesamples/android-architecture-components

LiveData Overview
developer.android.com/topic/libraries/architecture/livedata#kotlin

Async code using Kotlin Coroutines
proandroiddev.com/async-code-using-kotlin-coroutines-233d201099ff

Multithreading and Kotlin
medium.com/@korhanbircan/multithreading-and-kotlin-ac28eed57fea


ссылка на оригинал статьи https://habr.com/post/427475/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *