Desarrollo moderno de Android en Kotlin. Parte 2

Hola Habr! Les presento la traducción del artículo " Desarrollo moderno de Android con Kotlin (Parte 2) " de Mladen Rakonjac.

Nota Este artículo es una traducción de la serie de artículos de Mladen Rakonjac , fecha del artículo: 23/09/2017. Github Comenzando a leer la primera parte de SemperPeritus, descubrí que el resto de la parte no estaba traducida por alguna razón. Por lo tanto, traigo a su atención la segunda parte. El artículo resultó ser voluminoso.

imagen

"Es muy difícil encontrar un proyecto que cubra todo lo nuevo en desarrollo para Android en Android Studio 3.0, así que decidí escribirlo".

En este artículo, analizaremos lo siguiente:

  1. Android Studio 3 beta 1 Parte 1
  2. Lenguaje de programación Kotlin Parte 1
  3. Opciones de compilación Parte 1
  4. RestricciónDiseño Parte 1
  5. Biblioteca de enlace de datos Parte 1
  6. MVVM Architecture + Repository + Android Manager Wrappers Pattern
  7. RxJava2 y cómo nos ayuda en la arquitectura de la Parte 3
  8. Daga 2.11, qué es la inyección de dependencia, por qué debería usar esta Parte 4
  9. Retrofit (con Rx Java2)
  10. Habitación (con Rx Java2)

MVVM Architecture + Repository + Android Manager Wrappers Pattern


Algunas palabras sobre arquitectura en el mundo de Android


Durante bastante tiempo, los desarrolladores de Android no han utilizado ninguna arquitectura en sus proyectos. En los últimos tres años, ha surgido una gran expectación en la comunidad de desarrolladores de Android. La actividad del tiempo de Dios ha pasado y Google ha publicado el repositorio Blueprints de Arquitectura de Android , con muchos ejemplos e instrucciones sobre diversos enfoques arquitectónicos. Finalmente, en Google IO '17, presentaron Android Architecture Components , una colección de bibliotecas diseñadas para ayudarnos a crear un código más limpio y mejorar las aplicaciones. El componente dice que puede usarlos todos o solo uno de ellos. Sin embargo, los encontré todos realmente útiles. Más adelante en el texto y en las siguientes partes las usaremos. Primero, abordaré el problema en el código, y luego refactorizaré el uso de estos componentes y bibliotecas para ver qué problemas deben resolver.

Hay dos patrones arquitectónicos principales que comparten el código GUI:

  • MVP
  • MVVM

Es difícil decir cuál es mejor. Debes probar ambos y decidir. Prefiero MVVM usando componentes conscientes del ciclo de vida y escribiré sobre ello. Si nunca ha intentado usar MVP, hay toneladas de buenos artículos en Medium sobre esto.

¿Qué es un patrón MVVM?


MVVM es un patrón arquitectónico que se expande como Model-View-ViewModel. Creo que este nombre confunde a los desarrolladores. Si yo fuera a quien se le ocurrió su nombre, lo llamaría View-ViewModel-Model, porque ViewModel está en el medio, conectando Vista y Modelo .

La vista es una abstracción para Actividad , Fragmento o cualquier otra Vista personalizada (Vista personalizada de Android ). Tenga en cuenta que es importante no confundir esta vista con la vista de Android. La vista debe ser tonta, no debemos escribirle ninguna lógica. La vista no debe contener datos. Debe almacenar una referencia a la instancia de ViewModel y todos los datos que necesita la Vista deben provenir de allí. Además, la Vista debe observar estos datos y el diseño debe cambiar cuando cambien los datos del Modelo de vista . Para resumir, View es responsable de lo siguiente: vista de diseño para varios datos y estados.

ViewModel es un nombre abstracto para una clase que contiene datos y lógica, cuándo se deben recibir estos datos y cuándo se muestran. ViewModel almacena el estado actual. ViewModel también almacena un enlace a uno o más modelos y recibe todos los datos de ellos. Ella no debe saber, por ejemplo, de dónde provienen los datos, de la base de datos o del servidor. Además, ViewModel no necesita saber nada sobre View . Además, ViewModel no debería saber nada sobre el marco de Android.

Modelo es el nombre abstracto de la capa que prepara los datos para ViewModel . Esta es la clase en la que recibiremos datos del servidor y los almacenaremos en caché, o los almacenaremos en una base de datos local. Tenga en cuenta que estas no son las mismas clases que User, Car, Square, otras clases de modelos que simplemente almacenan datos. Por lo general, esta es una implementación de la plantilla del Repositorio, que discutiremos más adelante. El modelo no debe saber nada sobre ViewModel .

MVVM , si se implementa correctamente, es una excelente manera de romper su código y hacerlo más comprobable. Esto nos ayuda a seguir principios SÓLIDOS , por lo que nuestro código es más fácil de mantener.

Ejemplo de código


Ahora escribiré un ejemplo simple que muestra cómo funciona esto.

Para comenzar, creemos un Modelo simple que devuelva una línea:

RepoModel.kt
class RepoModel { fun refreshData() : String { return "Some new data" } } 


Por lo general, recibir datos es una llamada asincrónica , por lo que debemos esperar. Para simular esto, cambié la clase a la siguiente:

RepoModel.kt
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } 


OnDataReadyCallback interfaz OnDataReadyCallback con el método onDataReady . Y ahora el método refreshData implementa (implementa) OnDataReadyCallback . Para simular la espera, uso el Handler . Una vez onDataReady 2 segundos, se onDataReady método onDataReady en las clases que implementan la interfaz OnDataReadyCallback .

Creemos un ViewModel :

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false } 


Como puede ver, hay una instancia de RepoModel , text , que se mostrará, y la variable isLoading , que almacena el estado actual. Creemos un método de refresh que sea responsable de recuperar los datos:

MainViewModel.kt
 class MainViewModel { ... val onDataReadyCallback = object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } } fun refresh(){ isLoading.set(true) repoModel.refreshData(onDataReadyCallback) } } 


El método de refresh llama a refreshData en el RepoModel , que toma una implementación OnDataReadyCallback en argumentos. Ok, pero ¿qué es un object ? Siempre que desee implementar una interfaz o heredar una clase extendida sin subclasificar, utilizará una declaración de objeto . ¿Y si quieres usar esto como una clase anónima? En este caso, está utilizando la expresión de objeto :

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false fun refresh() { repoModel.refreshData( object : OnDataReadyCallback { override fun onDataReady(data: String) { text = data }) } } 


Cuando llamamos refresh , debemos cambiar la vista al estado de carga y cuando llegan los datos, establece isLoading en false .

También debemos reemplazar el text con
 ObservableField<String> 
y está isLoading
 ObservableField<Boolean> 
. ObservableField es una clase de la biblioteca de enlace de datos que podemos usar en lugar de crear un objeto Observable. Envuelve el objeto que queremos observar.

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() val text = ObservableField<String>() val isLoading = ObservableField<Boolean>() fun refresh(){ isLoading.set(true) repoModel.refreshData(object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } }) } } 


Tenga en cuenta que uso val en lugar de var , porque solo cambiaremos el valor en el campo, pero no el campo en sí. Y si desea inicializarlo, use lo siguiente:

initobserv.kt
 val text = ObservableField("old data") val isLoading = ObservableField(false) 



Cambiemos nuestro diseño para que pueda observar texto e isLoading . Para comenzar, enlace MainViewModel en lugar de Repository :

activity_main.xml
 <data> <variable name="viewModel" type="me.mladenrakonjac.modernandroidapp.MainViewModel" /> </data> 


Entonces:

  • Cambiar TextView para observar el texto de MainViewModel
  • Agregue una barra de progreso, que será visible solo si isLoading true
  • Botón Agregar, que, cuando se hace clic, llamará al método de actualización desde MainViewModel y se podrá hacer clic solo si isLoading false

main_activity.xml
 ... <TextView android:id="@+id/repository_name" android:text="@{viewModel.text}" ... /> ... <ProgressBar android:id="@+id/loading" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" ... /> <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.refresh()}" android:clickable="@{viewModel.isLoading ? false : true}" /> ... 


Si ejecuta ahora, obtendrá un View.VISIBLE and View.GONE cannot be used if View is not imported . Bueno, importemos:

main_activity.xml
 <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> 


Ok, hecho con el diseño. Ahora termina con la encuadernación. Como dije, View debe tener una instancia de ViewModel :

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } } 


Finalmente podemos correr


Puede ver que los datos antiguos se reemplazan por datos nuevos .

Este fue un simple ejemplo de MVVM.

Pero hay un problema, volteemos la pantalla


los datos antiguos reemplazaron los datos nuevos . ¿Cómo es esto posible? Eche un vistazo al ciclo de vida de la actividad:

Ciclo de vida de la actividad
imagen

Cuando encendió el teléfono, se creó una nueva instancia de Activity y se llamó al método onCreate() . Echa un vistazo a nuestra actividad:

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } } 


Como puede ver, cuando se creó una instancia de Activity, también se creó una instancia de MainViewModel . ¿Es bueno si de alguna manera tenemos la misma instancia de MainViewModel para cada MainActivity recreado?

Introducción a los componentes del ciclo de vida


Porque muchos desarrolladores se enfrentan a este problema, los desarrolladores del equipo de Android Framework decidieron crear una biblioteca diseñada para ayudar a resolver esto. La clase ViewModel es una de ellas. Esta es la clase de la que todos nuestros ViewModel deberían heredar.

Heredemos MainViewModel de ViewModel de los componentes que tienen en cuenta el ciclo de vida. Primero, debemos agregar la biblioteca de componentes con reconocimiento del ciclo de vida a nuestro archivo build.gradle :

build.gradle
 dependencies { ... implementation "android.arch.lifecycle:runtime:1.0.0-alpha9" implementation "android.arch.lifecycle:extensions:1.0.0-alpha9" kapt "android.arch.lifecycle:compiler:1.0.0-alpha9" 


Convierta MainViewModel en el heredero de ViewModel :

MainViewModel.kt
 package me.mladenrakonjac.modernandroidapp import android.arch.lifecycle.ViewModel class MainViewModel : ViewModel() { ... } 


El método onCreate () de nuestra MainActivity se verá así:

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.executePendingBindings() } } 


Tenga en cuenta que no creamos una nueva instancia de MainViewModel . Lo obtendremos usando ViewModelProviders . ViewModelProviders es una clase de utilidad que tiene un método para obtener ViewModelProvider . Se trata de alcance . Si llama a ViewModelProviders.of (this) en una Actividad, su ViewModel vivirá mientras esta Actividad esté activa (hasta que se destruya sin volver a crear). Por lo tanto, si llama a esto en un fragmento, su ViewModel vivirá mientras el Fragment esté vivo, etc. Echa un vistazo al diagrama:

Alcance del ciclo de vida
imagen

ViewModelProvider es responsable de crear una nueva instancia en la primera llamada o devolver la anterior si se recrea su Actividad o Fragmento.

No te confundas con

 MainViewModel::class.java 

En Kotlin, si sigues

 MainViewModel::class 

esto le devolverá KClass , que no es lo mismo que Class de Java. Entonces, si escribimos .java , de acuerdo con la documentación es:
Devolverá una instancia de Class Java correspondiente a esta instancia de KClass
Veamos qué sucede cuando giras la pantalla


Tenemos los mismos datos que antes de la rotación de la pantalla.

En el último artículo, dije que nuestra aplicación obtendrá una lista de repositorios de Github y los mostrará. Para hacer esto, debemos agregar la función getRepositories , que devolverá una lista falsa de repositorios:

RepoModel.kt
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100 , false)) arrayList.add(Repository("Second", "Owner 2", 30 , true)) arrayList.add(Repository("Third", "Owner 3", 430 , false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } interface OnRepositoryReadyCallback { fun onDataReady(data : ArrayList<Repository>) } 


También necesitamos tener un método en MainViewModel que llame a getRepositories desde RepoModel :

MainViewModel.kt
 class MainViewModel : ViewModel() { ... var repositories = ArrayList<Repository>() fun refresh(){ ... } fun loadRepositories(){ isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback{ override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories = data } }) } } 


Y finalmente, necesitamos mostrar estos repositorios en RecyclerView. Para hacer esto, debemos:

  • Crear diseño rv_item_repository.xml
  • Agregue RecyclerView al diseño activity_main.xml
  • Crear repositorioRecyclerViewAdapter
  • Instale el adaptador en la vista de reciclaje

Para crear rv_item_repository.xml, utilicé la biblioteca CardView, por lo que debemos agregarla a build.gradle (aplicación):

 implementation 'com.android.support:cardview-v7:26.0.1' 

Así es como se ve:

rv_item_repository.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="repository" type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" /> </data> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="96dp" android:layout_margin="8dp"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/repository_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryName}" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.083" tools:text="Modern Android App" /> <TextView android:id="@+id/repository_has_issues" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/has_issues" android:textStyle="bold" android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="@+id/repository_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toEndOf="@+id/repository_name" app:layout_constraintTop_toTopOf="@+id/repository_name" app:layout_constraintVertical_bias="1.0" /> <TextView android:id="@+id/repository_owner" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryOwner}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_name" app:layout_constraintVertical_bias="0.0" tools:text="Mladen Rakonjac" /> <TextView android:id="@+id/number_of_starts" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@{String.valueOf(repository.numberOfStars)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_owner" app:layout_constraintVertical_bias="0.0" tools:text="0 stars" /> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView> </layout> 


El siguiente paso es agregar RecyclerView a activity_main.xml . Antes de hacer esto, asegúrese de agregar la biblioteca RecyclerView:

 implementation 'com.android.support:recyclerview-v7:26.0.1' 

activity_main.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="me.fleka.modernandroidapp.MainActivity"> <ProgressBar android:id="@+id/loading" android:layout_width="48dp" android:layout_height="48dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.v7.widget.RecyclerView android:id="@+id/repository_rv" android:layout_width="0dp" android:layout_height="0dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/rv_item_repository" /> <Button android:id="@+id/refresh_button" android:layout_width="160dp" android:layout_height="40dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:onClick="@{() -> viewModel.loadRepositories()}" android:clickable="@{viewModel.isLoading ? false : true}" android:text="Refresh" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="1.0" /> </android.support.constraint.ConstraintLayout> </layout> 



Tenga en cuenta que eliminamos algunos elementos TextView y ahora el botón inicia loadRepositories en lugar de actualizar :

button.xml
 <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.loadRepositories()}" ... /> 


Eliminemos el método de actualización de MainViewModel y refreshData de RepoModel como innecesario.

Ahora necesita crear un adaptador para RecyclerView:

RepositoryRecyclerViewAdapter.kt
 class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>, private var listener: OnItemClickListener) : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { val layoutInflater = LayoutInflater.from(parent?.context) val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) override fun getItemCount(): Int = items.size interface OnItemClickListener { fun onItemClick(position: Int) } class ViewHolder(private var binding: RvItemRepositoryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(repo: Repository, listener: OnItemClickListener?) { binding.repository = repo if (listener != null) { binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) } binding.executePendingBindings() } } } 


Tenga en cuenta que ViewHolder toma una instancia de tipo RvItemRepositoryBinding , en lugar de View , para que podamos implementar Data Binding en ViewHolder para cada elemento. No se avergüence de la función de línea única (en línea):

 override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) 

Esta es solo una entrada corta para:

 override fun onBindViewHolder(holder: ViewHolder, position: Int){ return holder.bind(items[position], listener) } 

Y items [posición] es la implementación para el operador de índice. Es similar a items.get (posición) .

Otra línea que puede confundirte:

 binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) 

Puede reemplazar el parámetro con _ si no lo está usando. Bien, ¿eh?

Creamos el adaptador, pero aún no lo hemos aplicado al recyclerView en MainActivity :

MainActivity.kt
 class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this) } override fun onItemClick(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } 


Ejecuta la aplicación


Esto es raro Que paso

  • Se creó la actividad, por lo que también se creó un nuevo adaptador con repositorios que están prácticamente vacíos
  • Apretamos el boton
  • Se llama loadRepositories, que muestra el progreso
  • Después de 2 segundos, obtenemos los repositorios, el progreso está oculto, pero no aparecen. Esto se debe a que no se llama a notifyDataSetChanged en el adaptador
  • Cuando giramos la pantalla, se crea una nueva Actividad, por lo que se crea un nuevo adaptador con el parámetro de repositorios con algunos datos

Entonces, como MainViewModel debe notificar a MainActivity de los nuevos elementos, ¿podemos llamar a notifyDataSetChanged ?

No podemos

Esto es realmente importante, MainViewModel no debería saber nada sobre MainActivity .

MainActivity es el que tiene una instancia de MainViewModel , por lo que debe escuchar los cambios y notificar al Adaptador sobre los cambios.

¿Pero cómo hacerlo?

Podemos observar los repositorios , así que después de cambiar los datos, podemos cambiar nuestro adaptador.

¿Qué hay de malo en esta decisión?

Veamos el siguiente caso:

  • En MainActivity, observamos repositorios: cuando ocurre un cambio, ejecutamos notifyDataSetChanged
  • Apretamos el boton
  • Mientras esperamos los cambios de datos, MainActivity se puede volver a crear debido a cambios en la configuración.
  • Nuestro MainViewModel sigue vivo
  • Después de 2 segundos, el campo de repositorios recibe nuevos elementos y notifica al observador que los datos han cambiado.
  • El observador intenta ejecutar notifyDataSetChanged en el adaptador , que ya no existe, porque MainActivity ha sido recreada

Bueno, nuestra decisión no es lo suficientemente buena.

Introducción a LiveData


LiveData es otro componente del ciclo de vida basado en un observable que conoce el ciclo de vida de View. Entonces, cuando una Actividad se destruye debido a un cambio de configuración , LiveData lo sabe, por lo que también elimina al observador de la Actividad destruida.

Implementamos en MainViewModel :

MainViewModel.kt
 class MainViewModel : ViewModel() { var repoModel: RepoModel = RepoModel() val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } } 


y comience a observar MainActivity:

MainActivity.kt
 class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { private lateinit var binding: ActivityMainBinding private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = repositoryRecyclerViewAdapter viewModel.repositories.observe(this, Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} }) } override fun onItemClick(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } 


¿Qué significa la palabra que significa? Si una función tiene solo un parámetro, se puede obtener acceso a este parámetro utilizando la palabra clave it. Entonces, supongamos que tenemos una expresión lambda para multiplicar por 2:

 ((a) -> 2 * a) 

Se puede reemplazar de la siguiente manera:

 (it * 2) 

Si inicia la aplicación ahora, puede asegurarse de que todo funcione


...

¿Por qué prefiero MVVM sobre MVP?



  • No hay una interfaz aburrida para View, ya que ViewModel no tiene referencia a View
  • No hay una interfaz aburrida para Presenter, y esto no es necesario
  • Mucho más fácil de manejar los cambios de configuración
  • Usando MVVM, tenemos menos código para Actividad, Fragmentos, etc.

...

Patrón de repositorio


Esquema
imagen

Como dije anteriormente, Modelo es solo un nombre abstracto para la capa donde estamos preparando los datos. Suele contener repositorios y clases de datos. Cada clase de entidad (datos) tiene una clase de repositorio correspondiente. Por ejemplo, si tenemos las clases User y Post , también debemos tener UserRepository y PostRepository . Todos los datos provienen de allí. Nunca debemos llamar a una instancia de Preferencias compartidas o DB desde View o ViewModel.

Por lo tanto, podemos cambiar el nombre de nuestro RepoModel a GitRepoRepository , donde GitRepo vendrá del repositorio de Github y Repository vendrá del patrón Repository.

RepoRepositories.kt
 class GitRepoRepository { fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100, false)) arrayList.add(Repository("Second", "Owner 2", 30, true)) arrayList.add(Repository("Third", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


Ok, MainViewModel obtiene la lista de repositorios Github de GitRepoRepsitories , pero ¿de dónde obtener GitRepoRepositories ?

Puede llamar al cliente o DB directamente en el repositorio desde la instancia, pero todavía no es la mejor práctica. Su aplicación debe ser modular tanto como pueda. ¿Qué sucede si decide usar diferentes clientes para reemplazar Volley con Retrofit? Si tiene algún tipo de lógica dentro, será difícil refactorizar. Su repositorio no necesita saber qué cliente está utilizando para recuperar los datos remotos.

  • Lo único que el repositorio necesita saber es que los datos llegan de forma remota o local. No es necesario saber cómo obtenemos estos datos remotos o locales.
  • El único modelo de vista requerido son los datos.
  • Lo único que debe hacer la Vista es mostrar estos datos.

Cuando recién comencé a desarrollar en Android, me preguntaba cómo funcionan las aplicaciones sin conexión y cómo funciona la sincronización de datos. Una buena arquitectura de aplicaciones nos permite hacer esto con facilidad. Por ejemplo, cuando se llama a loadRepositories en ViewModel si hay una conexión a Internet, GitRepoRepositories puede recibir datos de una fuente de datos remota y guardarlos en una fuente de datos local. Cuando el teléfono está desconectado, GitRepoRepository puede recibir datos del almacenamiento local. Por lo tanto, los repositorios deben tener instancias de RemoteDataSource y LocalDataSource y el procesamiento lógico de donde deberían venir estos datos.

Agregue una fuente de datos local :

GitRepoLocalDataSource.kt
 class GitRepoLocalDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First From Local", "Owner 1", 100, false)) arrayList.add(Repository("Second From Local", "Owner 2", 30, true)) arrayList.add(Repository("Third From Local", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000) } fun saveRepositories(arrayList: ArrayList<Repository>){ //todo save repositories in DB } } interface OnRepoLocalReadyCallback { fun onLocalDataReady(data: ArrayList<Repository>) } 


Aquí tenemos dos métodos: el primero, que devuelve datos locales falsos, y el segundo, para el almacenamiento de datos ficticios.

Agregar una fuente de datos remota :

GitRepoRemoteDataSource.kt
 class GitRepoRemoteDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First from remote", "Owner 1", 100, false)) arrayList.add(Repository("Second from remote", "Owner 2", 30, true)) arrayList.add(Repository("Third from remote", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000) } } interface OnRepoRemoteReadyCallback { fun onRemoteDataReady(data: ArrayList<Repository>) } 


Solo hay un método que devuelve datos remotos falsos .

Ahora podemos agregar algo de lógica a nuestro repositorio:

GitRepoRepository.kt
 class GitRepoRepository { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


Por lo tanto, al compartir fuentes, fácilmente guardamos datos localmente.

¿Qué sucede si solo necesita datos de la red y aún necesita usar la plantilla de repositorio? Si Esto hace que las pruebas de código sean más fáciles, otros desarrolladores pueden entender mejor su código y usted puede admitirlo más rápido

...

Contenedores de Android Manager


¿Qué sucede si desea verificar su conexión a Internet en GitRepoRepository para saber dónde solicitar datos? Ya dijimos que no deberíamos colocar ningún código relacionado con Android en ViewModel and Model , entonces, ¿cómo lidiar con este problema?

Escribamos un contenedor para una conexión a internet:

NetManager.kt (Una solución similar se aplica a otros gerentes, por ejemplo, a NfcManager)
 class NetManager(private var applicationContext: Context) { private var status: Boolean? = false val isConnectedToInternet: Boolean? get() { val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val ni = conManager.activeNetworkInfo return ni != null && ni.isConnected } } 


Este código solo funcionará si agregamos permiso para manifestar:

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

Pero, ¿cómo crear una instancia en el repositorio, si no tenemos el contexto ( context La )? Podemos solicitarlo en el constructor:

GitRepoRepository.kt
 class GitRepoRepository (context: Context){ val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() val netManager = NetManager(context) fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


Creamos antes de la nueva instancia de GitRepoRepository en ViewModel. ¿Cómo podemos ahora tener NetManager en ViewModel cuando necesitamos contexto para NetManager ? Puede usar AndroidViewModel desde la biblioteca de componentes de Lifecycle-aware, que tiene un contexto . Este es el contexto de la aplicación, no la Actividad.

MainViewModel.kt
 class MainViewModel : AndroidViewModel { constructor(application: Application) : super(application) var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication())) val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } } 


En esta linea

 constructor(application: Application) : super(application) 

Definimos un constructor para MainViewModel . Esto es necesario porque AndroidViewModel solicita una instancia de la aplicación en su constructor. Entonces, en nuestro constructor, llamamos al súper método que llama al constructor AndroidViewModel , del cual heredamos.

Nota: podemos deshacernos de una línea si lo hacemos:

 class MainViewModel(application: Application) : AndroidViewModel(application) { ... } 

Y ahora que tenemos la instancia de NetManager en el GitRepoRepository , podemos verificar la conexión a Internet:

GitRepoRepository.kt
 class GitRepoRepository(val netManager: NetManager) { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { netManager.isConnectedToInternet?.let { if (it) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onRemoteDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } else { localDataSource.getRepositories(object : OnRepoLocalReadyCallback { override fun onLocalDataReady(data: ArrayList<Repository>) { onRepositoryReadyCallback.onDataReady(data) } }) } } } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


Por lo tanto, si tenemos una conexión a Internet, recibiremos los datos eliminados y los guardaremos localmente. Si no tenemos una conexión a Internet, obtendremos datos locales.

Nota de Kotlin : la instrucción let comprueba si es nula y devuelve un valor dentro de ella .

En uno de los siguientes artículos escribiré sobre la inyección de dependencias, lo malo que es crear instancias de repositorio en ViewModel y cómo evitar usar AndroidViewModel. También escribiré sobre una gran cantidad de problemas que ahora están en nuestro código. Los dejé por una razón ...

Estoy tratando de mostrarle los problemas para que pueda entender por qué todas estas bibliotecas son populares y por qué debería usarlas.

PD Me he cambiado de opinión sobre el asignador ( cartógrafos ). Decidí cubrir esto en los siguientes artículos.

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


All Articles