Android LiveData sur Kotlin utilisant Retrofit et coroutines

Cet article traite de l'utilisation des composants Android ViewModel, LifeCycle et LiveData. Ces composants vous permettent de ne pas vous soucier du cycle de vie de l'activité.

Un exemple d'utilisation de Coroutines modernes conjointement avec le référentiel Retrofit est également considéré.

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

Rénovation de l'extension des coroutines

kotlin-coroutines-retrofit
Extension pour Retrofit sur Kotlin. Ce ne sont que deux fichiers. Je viens de les ajouter au projet. Vous pouvez les connecter via la dépendance dans Gradle. Il existe des exemples d'utilisation sur Github.
Nous connectons également l'adaptateur addCallAdapterFactory (CoroutineCallAdapterFactory ()) .
ServerAPI et Repository sont dans le même fichier

API REST

Implémentation de l'API REST sur Kotlin. Elle n'a pas de changements spécifiques.

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

Ensuite, considérez le référentiel. Il s'agit du principal service de réception de LiveData. Nous initialisons LiveData avec l'état de chargement: Resource.loading (null) . Ensuite, attendez la fin de la demande. WaitResult () Cet appel doit être dans le bloc asynchrone (UI) de Coroutin .

À la fin de la demande, nous pouvons gérer le résultat. Si tout va bien, le résultat sera stocké dans mutableLiveData.value = Resource.success (result.value) Le point important est qu'il doit y avoir un lien vers la nouvelle instance, sinon l'observateur LiveData ne fonctionnera pas. voir: nouvelle ressource <> (SUCCESS, data, null);

Dépôt
  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 } } 


Données de wrapper

Pour la gestion des erreurs et le transfert d'état vers Fragment, Wrapper - Resource <T> est utilisé .

Il stocke trois états:

 public enum Status { SUCCESS, ERROR, LOADING } 

Les données elles-mêmes:

 @Nullable public final T data; 

Ressource <T>
Une classe générique qui contient des données et un état sur le chargement de ces données

 // 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 demande des données au référentiel et les stocke dans la variable 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

Pour passer des paramètres à ViewModel, nous développons les ViewModelProviders standard
Par exemple, pour passer à LoginViewModel, vous avez besoin de deux paramètres (Login, Password). Pour transférer un jeton vers StoresViewModel , un (jeton) est utilisé

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

Obtenir des magasinsViewModel:

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

Utilisation de l'Observer Observer pour les modifications de données:

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


PS

Pour stocker Token et l'utiliser dans l'application, j'ai utilisé la bibliothèque / extension de Fabio Collini. L'application est bien décrite dans son article. Le lien se trouve sur une page sur Github ou ci-dessous dans cet article.

prefs-delegues par 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' 

Les liens

Tout dans un exemple.

Exemples de composants d'architecture Android

Présentation de LiveData

Code asynchrone à l'aide de Kotlin Coroutines

Multithreading et kotlin

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


All Articles