Organisation einer einfachen Architektur in einer Android-Anwendung mit einer Reihe von ViewModel + LiveData, Retrofit + Coroutines

Ohne lange Einführungen erkläre ich Ihnen, wie Sie eine praktische Architektur Ihrer Anwendung schnell und einfach organisieren können. Das Material wird für diejenigen nützlich sein, die mit dem MVVM-Muster und den Kotlin-Coroutinen nicht sehr vertraut sind.

Wir haben also eine einfache Aufgabe: Eine Netzwerkanfrage zu empfangen und zu verarbeiten, das Ergebnis in einer Ansicht anzuzeigen.

Unsere Aktionen: Ausgehend von der Aktivität (Fragment) rufen wir die gewünschte Methode ViewModel auf -> ViewModel greift auf das Retrofit-Handle zu, führt die Anforderung über die Coroutinen aus -> die Antwort wird als Ereignis an die Live-Daten gesendet -> in der Aktivität, die das Ereignis empfängt, übertragen wir die Daten an die Ansicht.

Projekteinrichtung


Abhängigkeiten


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


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

Nachrüstsetup


Erstellen Sie ein Kotlinovsky NetworkService- Objekt. Dies wird unser Netzwerk-Client sein - Singleton
Der Einfachheit halber wird UPD-Singleton verwendet. In den Kommentaren wurde darauf hingewiesen, dass die Verwendung der Steuerelementumkehrung besser geeignet ist, dies ist jedoch ein separates Thema.

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

API-Schnittstelle


Wir verwenden gesperrte Anfragen an den Fake-Service.

Unterbrich den Spaß, hier beginnt die Magie von Corutin.

Wir kennzeichnen unsere Funktionen mit dem Schlüsselwort suspend fun ....

Das Retrofit hat gelernt, mit den Kotlin-Suspend-Funktionen ab Version 2.6.0 zu arbeiten. Jetzt führt es direkt eine Netzwerkanforderung aus und gibt ein Objekt mit Daten zurück:

 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 ist eine einfache Wrapper-Klasse für unsere Netzwerkanforderungen:

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

Datumsklasse Benutzer

 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


Wir erstellen eine abstrakte BaseViewModel- Klasse, von der unser gesamtes ViewModel geerbt wird. Hier verweilen wir im Detail:

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

Ereignis


Eine coole Lösung von Google besteht darin, Datumsklassen in eine Event-Wrapper-Klasse zu packen, in der wir mehrere Status haben können, normalerweise LOADING, SUCCESS und 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 } 

So funktioniert es Während einer Netzwerkanfrage erstellen wir ein Ereignis mit dem Status LOADING. Wir warten auf eine Antwort vom Server und verpacken dann die Daten mit dem Ereignis und senden es mit dem angegebenen Status weiter. In der Ansicht überprüfen wir den Ereignistyp und legen je nach Status unterschiedliche Status für die Ansicht fest. Das Architekturmuster MVI basiert auf derselben Philosophie.

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

Und endlich

Hauptaktivität


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

Quellcode

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


All Articles