Без долгих вступлений расскажу, как можно быстро и просто организовать удобную архитекруту вашего приложения. Материал будет полезен тем, кто не очень хорошо знаком с mvvm-паттерном и котлиновскими корутинами.
Итак, у нас стоит простая задача: получить и обработать сетевой запрос, вывести результат во вью.
Наши действия: из активити (фрагмента) вызываем нужный метод ViewModel -> ViewModel обращается к ретрофитовской ручке, выполняя запрос через корутины -> ответ сетится в лайвдату в виде ивента -> в активити получая ивент передаём данные во вью.
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.1'
//Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
//ViewModel lifecycle
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc01"
<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
object NetworkService {
private const val BASE_URL = " http://www.mocky.io/v2/"
// HttpLoggingInterceptor выводит подробности сетевого запроса в логи
private val loggingInterceptor = run {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.apply {
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
}
}
private val baseInterceptor: Interceptor = invoke { chain ->
val newUrl = chain
.request()
.url
.newBuilder()
.build()
val request = chain
.request()
.newBuilder()
.url(newUrl)
.build()
return@invoke chain.proceed(request)
}
private val client: OkHttpClient = OkHttpClient
.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(baseInterceptor)
.build()
fun retrofitService(): Api {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
.create(Api::class.java)
}
}
interface Api {
@GET("5dcc12d554000064009c20fc")
suspend fun getUsers(
@Query("page") page: Int
): ResponseWrapper<Users>
@GET("5dcc147154000059009c2104")
suspend fun getUsersError(
@Query("page") page: Int
): ResponseWrapper<Users>
}
class ResponseWrapper<T> : Serializable {
@SerializedName("response")
val data: T? = null
@SerializedName("error")
val error: Error? = null
}
data class Users(
@SerializedName("count")
var count: Int?,
@SerializedName("items")
var items: List<Item?>?
) {
data class Item(
@SerializedName("first_name")
var firstName: String?,
@SerializedName("last_name")
var lastName: String?
)
}
abstract class BaseViewModel : ViewModel() {
var api: Api = NetworkService.retrofitService()
// У нас будут две базовые функции requestWithLiveData и
// requestWithCallback, в зависимости от ситуации мы будем
// передавать в них лайвдату или колбек вместе с параметрами сетевого
// запроса. Функция принимает в виде параметра ретрофитовский suspend запрос,
// проверяет на наличие ошибок и сетит данные в виде ивента либо в
// лайвдату либо в колбек. Про ивент будет написано ниже
fun <T> requestWithLiveData(
liveData: MutableLiveData<Event<T>>,
request: suspend () -> ResponseWrapper<T>) {
// В начале запроса сразу отправляем ивент загрузки
liveData.postValue(Event.loading())
// Привязываемся к жизненному циклу ViewModel, используя viewModelScope.
// После ее уничтожения все выполняющиеся длинные запросы
// будут остановлены за ненадобностью.
// Переходим в IO поток и стартуем запрос
this.viewModelScope.launch(Dispatchers.IO) {
try {
val response = request.invoke()
if (response.data != null) {
// Сетим в лайвдату командой postValue в IO потоке
liveData.postValue(Event.success(response.data))
} else if (response.error != null) {
liveData.postValue(Event.error(response.error))
}
} catch (e: Exception) {
e.printStackTrace()
liveData.postValue(Event.error(null))
}
}
}
fun <T> requestWithCallback(
request: suspend () -> ResponseWrapper<T>,
response: (Event<T>) -> Unit) {
response(Event.loading())
this.viewModelScope.launch(Dispatchers.IO) {
try {
val res = request.invoke()
// здесь все аналогично, но полученные данные
// сетим в колбек уже в главном потоке, чтобы
// избежать конфликтов с
// последующим использованием данных
// в context классах
launch(Dispatchers.Main) {
if (res.data != null) {
response(Event.success(res.data))
} else if (res.error != null) {
response(Event.error(res.error))
}
}
} catch (e: Exception) {
e.printStackTrace()
// UPD (подсказали в комментариях) В блоке catch ивент передаем тоже в Main потоке
launch(Dispatchers.Main) {
response(Event.error(null))
}
}
}
}
}
data class Event<out T>(val status: Status, val data: T?, val error: Error?) {
companion object {
fun <T> loading(): Event<T> {
return Event(Status.LOADING, null, null)
}
fun <T> success(data: T?): Event<T> {
return Event(Status.SUCCESS, data, null)
}
fun <T> error(error: Error?): Event<T> {
return Event(Status.ERROR, null, error)
}
}
}
enum class Status {
SUCCESS,
ERROR,
LOADING
}
class ActivityViewModel : BaseViewModel() {
// Создаем лайвдату для нашего списка юзеров
val simpleLiveData = MutableLiveData<Event<Users>>()
// Получение юзеров. Обращаемся к функции requestWithLiveData
// из BaseViewModel передаем нашу лайвдату и говорим,
// какой сетевой запрос нужно выполнить и с какими параметрами
// В данном случае это api.getUsers
// Теперь функция сама выполнит запрос и засетит нужные
// данные в лайвдату
fun getUsers(page: Int) {
requestWithLiveData(simpleLiveData) {
api.getUsers(
page = page
)
}
}
// Здесь аналогично, но вместо лайвдаты используем котлиновский колбек
// UPD Полученный результат мы можем обработать здесь перед отправкой во вью
fun getUsersError(page: Int, callback: (data: Event<Users>) -> Unit) {
requestWithCallback({
api.getUsersError(
page = page
)
}) {
callback(it)
}
}
}
class MainActivity : AppCompatActivity() {
private lateinit var activityViewModel: ActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
activityViewModel = ViewModelProviders.of(this).get(ActivityViewModel::class.java)
observeGetPosts()
buttonOneClickListener()
buttonTwoClickListener()
}
// Наблюдаем за нашей лайвдатой
// В зависимости от Ивента устанавливаем нужное состояние вью
private fun observeGetPosts() {
activityViewModel.simpleLiveData.observe(this, Observer {
when (it.status) {
Status.LOADING -> viewOneLoading()
Status.SUCCESS -> viewOneSuccess(it.data)
Status.ERROR -> viewOneError(it.error)
}
})
}
private fun buttonOneClickListener() {
btn_test_one.setOnClickListener {
activityViewModel.getUsers(page = 1)
}
}
// Здесь так же наблюдаем за Ивентом, используя колбек
private fun buttonTwoClickListener() {
btn_test_two.setOnClickListener {
activityViewModel.getUsersError(page = 2) {
when (it.status) {
Status.LOADING -> viewTwoLoading()
Status.SUCCESS -> viewTwoSuccess(it.data)
Status.ERROR -> viewTwoError(it.error)
}
}
}
}
private fun viewOneLoading() {
// Пошла загрузка, меняем состояние вьюх
}
private fun viewOneSuccess(data: Users?) {
val usersList: MutableList<Users.Item>? = data?.items as MutableList<Users.Item>?
usersList?.shuffle()
usersList?.let {
Toast.makeText(applicationContext, "${it}", Toast.LENGTH_SHORT).show()
}
}
private fun viewOneError(error: Error?) {
// Показываем ошибку
}
private fun viewTwoLoading() {}
private fun viewTwoSuccess(data: Users?) {}
private fun viewTwoError(error: Error?) {
error?.let {
Toast.makeText(applicationContext, error.errorMsg, Toast.LENGTH_SHORT).show()
}
}
}
К сожалению, не доступен сервер mySQL