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.
"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:
- Android Studio 3 beta 1 Parte 1
- Lenguaje de programación Kotlin Parte 1
- Opciones de compilación Parte 1
- RestricciónDiseño Parte 1
- Biblioteca de enlace de datos Parte 1
- MVVM Architecture + Repository + Android Manager Wrappers Pattern
- RxJava2 y cómo nos ayuda en la arquitectura de la Parte 3
- Daga 2.11, qué es la inyección de dependencia, por qué debería usar esta Parte 4
- Retrofit (con Rx Java2)
- 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:
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.ktclass 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 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 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")
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")
¿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
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>){
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.