Статья рассказывает об использовании 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. Она не имеет каких либо специфичных изменений
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);
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;
// 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)
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)
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 ->
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
Async code using Kotlin Coroutines
Автор: app-z