Android LiveData en Kotlin usando Retrofit y corutinas

Este artículo habla sobre el uso de componentes de Android ViewModel, LifeCycle y LiveData. Estos componentes le permiten no preocuparse por el ciclo de vida de la Actividad.

También se considera un ejemplo del uso de Coroutines modernas junto con el repositorio 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) } } 

Extensión de corutinas adaptadas

kotlin-coroutines-retrofit
Extensión para la modificación en Kotlin. Estos son solo dos archivos. Acabo de agregarlos al proyecto. Puede conectarlos a través de la dependencia en Gradle. Hay ejemplos de uso en Github.
También conectamos el adaptador addCallAdapterFactory (CoroutineCallAdapterFactory ()) .
ServerAPI y Repository están en el mismo archivo

API REST

Implementación de API REST en Kotlin. Ella no tiene ningún cambio específico.

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

A continuación, considere el repositorio. Este es el servicio principal para recibir LiveData. Inicializamos LiveData con el estado de carga: Resource.loading (nulo) . Luego, espere hasta el final de la solicitud awaitResult () Esta llamada debe estar en el bloque Coroutin async (UI)

Al final de la solicitud, podemos manejar el resultado. Si todo está bien, el resultado se almacenará en mutableLiveData.value = Resource.success (result.value). El punto importante es que debe haber un enlace a la nueva instancia; de lo contrario, el observador LiveData no funcionará. ver: nuevo Recurso <> (ÉXITO, datos, nulo);

Repositorio
  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 } } 


Datos del contenedor

Para el manejo de errores y la transferencia de estado a Fragment, se utiliza Wrapper - Resource <T> .

Almacena tres estados:

 public enum Status { SUCCESS, ERROR, LOADING } 

Los datos en sí:

 @Nullable public final T data; 

Recurso <T>
Una clase genérica que contiene datos y estado sobre la carga de estos datos.

 // 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 solicita datos del repositorio y los almacena en la variable de tiendas

 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

Para pasar parámetros a ViewModel, ampliamos los ViewModelProviders estándar
Por ejemplo, para pasar a LoginViewModel necesita dos parámetros (Inicio de sesión, Contraseña). Para transferir un token a StoresViewModel , se usa uno (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) } } } 


Fragmento

Obteniendo StoresViewModel:

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

Uso de Observer Observer para cambios de datos:

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

Fragmento
  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) } }) } 


PS

Para almacenar Token y usarlo en toda la aplicación, utilicé la biblioteca / extensión de Fabio Collini. La aplicación está bien descrita en su artículo. El enlace está en una página en Github o debajo de este artículo.

delegados de prefs por 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' 

Enlaces

Todo en un ejemplo.

Ejemplos de componentes de arquitectura de Android

Descripción general de LiveData

Código asíncrono usando Kotlin Coroutines

Multithreading y kotlin

Source: https://habr.com/ru/post/es427475/


All Articles