Organización de una arquitectura simple en una aplicación de Android con un montón de ViewModel + LiveData, Retrofit + Coroutines

Sin largas presentaciones, le diré cómo organizar rápida y fácilmente una arquitectura conveniente de su aplicación. El material será útil para aquellos que no están muy familiarizados con el patrón mvvm y las rutinas de Kotlin.

Por lo tanto, tenemos una tarea simple: recibir y procesar una solicitud de red, mostrar el resultado en una vista.

Nuestras acciones: desde la actividad (fragmento), llamamos al método deseado ViewModel -> ViewModel accede al controlador de modificación, ejecutando la solicitud a través de las rutinas -> la respuesta se envía a los datos en vivo como un evento -> en la actividad que recibe el evento transferimos los datos a la vista.

Configuración del proyecto


Dependencias


//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" 

Manifiesto


 <manifest ...> <uses-permission android:name="android.permission.INTERNET" /> </manifest> 

Configuración de actualización


Cree un objeto Kotlinovsky NetworkService . Este será nuestro cliente de red - singleton
UPD singleton se utiliza para facilitar la comprensión. Los comentarios indicaron que es más apropiado usar la inversión de control, pero este es un tema separado.

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

Interfaz api


Utilizamos solicitudes bloqueadas para el servicio falso.

Pausa la diversión, aquí comienza la magia de la corutina.

Marcamos nuestras funciones con la palabra clave suspender diversión ...

La actualización aprendió a trabajar con las funciones de suspensión de Kotlin de la versión 2.6.0, ahora ejecuta directamente una solicitud de red y devuelve un objeto con datos:

 interface Api { @GET("5dcc12d554000064009c20fc") suspend fun getUsers( @Query("page") page: Int ): ResponseWrapper<Users> @GET("5dcc147154000059009c2104") suspend fun getUsersError( @Query("page") page: Int ): ResponseWrapper<Users> } 

ResponseWrapper es una clase de contenedor simple para nuestras solicitudes de red:

 class ResponseWrapper<T> : Serializable { @SerializedName("response") val data: T? = null @SerializedName("error") val error: Error? = null } 

Fecha clase Usuarios

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

ViewModel


Creamos una clase abstracta BaseViewModel de la que se heredará todo nuestro ViewModel. Aquí nos detenemos con más detalle:

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

Eventos


Una solución genial de Google es envolver las clases de fechas en una clase de envoltura de eventos, en la que podemos tener varios estados, generalmente CARGANDO, ÉXITO y ERROR.

 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 } 

Así es como funciona. Durante una solicitud de red, creamos un evento con el estado CARGANDO. Estamos esperando una respuesta del servidor y luego envolvemos los datos con el evento y los enviamos con el estado especificado más adelante. En la vista, verificamos el tipo de evento y, según el estado, establecemos diferentes estados para la vista. El patrón de arquitectura MVI se basa en la misma filosofía.

ActivityViewModel


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

Y finalmente

Mainactividad


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

Código fuente

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


All Articles