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

в 17:01, , рубрики: android architecture, android architecture components, development, kotlin, андроид разработка приложений, Разработка под android

Статья рассказывает об использовании 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

Все в одном примере

Android Architecture Components samples

LiveData Overview

Async code using Kotlin Coroutines

Multithreading and Kotlin

Автор: app-z

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js