Kotlin上的现代Android开发。 第二部分

哈Ha! 我向您介绍Mladen Rakonjac撰写的文章“ 用Kotlin进行现代Android开发(第2部分) ”的翻译。

注意事项 本文是Mladen Rakonjac撰写的系列文章的翻译,文章日期:09/23/2017。 Github 开始阅读SemperPeritus第一部分时,我发现该部分的其余部分由于某种原因未进行翻译。 因此,我提请您注意第二部分。 原来,这篇文章篇幅很大。

图片

“在Android Studio 3.0中很难找到一个涵盖Android开发中所有新事物的项目,所以我决定编写它。”

在本文中,我们将分析以下内容:

  1. Android Studio 3 beta 1 第1部分
  2. Kotlin编程语言第1部分
  3. 构建选项第1部分
  4. ConstraintLayout 第1部分
  5. 数据绑定库第1部分
  6. MVVM体系结构+存储库+ Android Manager Wrappers模式
  7. RxJava2及其在第3部分架构中如何帮助我们
  8. Dagger 2.11,什么是依赖注入,为什么要使用此第4部分
  9. 改造(使用Rx Java2)
  10. 会议室(带有Rx Java2)

MVVM体系结构+存储库+ Android Manager Wrappers模式


关于Android世界中的体系结构的几句话


相当长一段时间以来,Android开发人员在其项目中未使用任何架构。 在过去的三年中,她在Android开发人员社区中引起了很多炒作。 God Activity的时代已经过去,Google已发布了Android Architecture Blueprints存储库,其中包含有关各种体系结构方法的许多示例和说明。 最后,在Google IO '17上,他们引入了Android Architecture Components ,这是一个库集合,旨在帮助我们创建更简洁的代码并改进应用程序。 Component说您可以使用所有这些,也可以只使用其中之一。 但是,我发现它们都非常有用。 在正文和后续部分中,我们将继续使用它们。 首先,我将解决代码中的问题,然后使用这些组件和库进行重构,以查看它们应解决的问题。

有两种共享GUI代码的主要架构模式

  • 最有价值球员
  • MVVM

很难说哪个更好。 您必须同时尝试并决定。 我更喜欢使用生命周期感知组件的MVVM ,我将对此进行介绍。 如果您从未尝试使用MVP,那么在Medium上会有很多关于此的好文章。

什么是MVVM模式?


MVVM是一种可扩展为Model-View-ViewModel的体系结构模式 。 我认为这个名称使开发人员感到困惑。 如果我是提出他名字的人,我将其称为View-ViewModel-Model,因为ViewModel位于中间,将ViewModel连接起来。

视图ActivityFragment或任何其他自定义视图( Android自定义视图 )的抽象。 请注意,请勿将此视图与Android视图混淆。 视图应该是愚蠢的,我们不应该为其编写任何逻辑。 视图不应包含数据。 它应该存储对ViewModel实例的引用,并且View需要的所有数据都应来自该实例。 另外,当ViewModel中的数据更改时, View应该观察到此数据,并且布局也应更改。 总而言之, View负责以下工作:各种数据和状态的布局视图。

ViewModel是包含数据和逻辑的类的抽象名称,应在何时接收此数据以及何时显示该数据。 ViewModel存储当前状态ViewModel还存储一个或多个Model的链接,并从中接收所有数据。 例如,她不应该知道数据来自哪里,来自数据库还是来自服务器。 另外, ViewModel不需要了解有关View的任何信息。 而且, ViewModel应该完全不了解Android框架。

Model是为ViewModel准备数据的层的抽象名称。 这是我们将从服务器接收数据并将其缓存或存储在本地数据库中的类。 请注意,这些类别与User,Car,Square和其他仅存储数据的模型类别不同。 通常,这是存储库模板的实现,我们将在以后考虑。 模型不应该对ViewModel一无所知。

如果正确实施, MVVM是破坏代码并使其更具可测试性的好方法。 这有助于我们遵循SOLID原则,因此我们的代码更易于维护。

代码示例


现在,我将编写一个简单的示例来说明其工作原理。

首先,让我们创建一个返回一行的简单模型

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


通常,接收数据是异步调用,因此我们必须等待它。 为了模拟这一点,我将类更改为以下内容:

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


我使用onDataReady方法创建了OnDataReadyCallback接口。 现在refreshData方法实现(实现) OnDataReadyCallback 。 为了模拟等待,我使用HandleronDataReady 2秒一次,将在实现OnDataReadyCallback接口的类上调用onDataReady方法。

让我们创建一个ViewModel

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


如您所见,有一个RepoModel实例, text实例(将显示)和一个isLoading变量来存储当前状态。 让我们创建一个负责检索数据的refresh方法:

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


refresh方法在refreshData上调用refreshData ,它在参数中采用OnDataReadyCallback实现。 好的,但是什么是object ? 每当您要实现接口或继承扩展类而不进行子类化时,都将使用对象声明 。 如果要将此作为匿名类使用? 在这种情况下,您正在使用对象表达式

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


调用refresh ,必须将视图更改为加载状态,并在数据到达时将isLoading设置为false

我们还必须将text替换为
 ObservableField<String> 
,并且isLoading
 ObservableField<Boolean> 
ObservableField是数据绑定库中的一个类,我们可以使用它代替创建Observable对象,它包装了我们要观察的对象。

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


请注意,我使用val而不是var ,因为我们将仅更改字段中的值,而不更改字段本身。 如果要初始化它,请使用以下命令:

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



让我们更改布局,使其可以观察文本isLoading 。 首先,绑定MainViewModel而不是Repository

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


然后:

  • 更改TextView以观察MainViewModel中的 文本
  • 添加一个ProgressBar,只有在isLoading为 true时才可见
  • 添加按钮,单击后将从MainViewModel调用refresh方法,并且仅当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}" /> ... 


如果现在运行, View.VISIBLE and View.GONE cannot be used if View is not imported ,将出现View.VISIBLE and View.GONE cannot be used if View is not imported 。 好吧,让我们导入:

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


好的,完成布局。 现在完成绑定。 就像我说的, View必须具有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() } } 


最后,我们可以运行


您可以看到旧数据已替换为新数据

这是一个简单的MVVM示例。

但是有一个问题,让我们打开屏幕


旧数据代替新数据 。 这怎么可能? 看一下Activity的生命周期:

活动生命周期
图片

当您打开手机时,将创建一个新的Activity实例,并调用onCreate()方法。 看一下我们的活动:

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


如您所见,创建Activity实例时,也会创建MainViewModel实例。 如果以某种方式我们为每个重新创建的MainActivity具有相同的MainViewModel实例,那会很好吗?

生命周期感知组件简介


因为 许多开发人员都面临这个问题,Android框架团队的开发人员决定创建一个旨在帮助解决此问题的库。 ViewModel类就是其中之一。 这是我们所有ViewModel都应继承的类。

让我们从支持生命周期的组件的ViewModel继承MainViewModel 。 首先,我们需要将生命周期感知组件库添加到我们的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" 


使MainViewModel成为 ViewModel 继承者:

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


我们的MainActivity的onCreate()方法将如下所示:

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


请注意,我们没有创建MainViewModel的新实例。 我们将使用ViewModelProviders获得它。 ViewModelProviders是一个Utility类,具有用于获取ViewModelProvider的方法。 这一切都与范围有关。 如果您在Activity中调用ViewModelProviders.of(此方法) ,则只要该Activity处于活动状态(直到它被销毁而不重新创建), ViewModel将一直存在。 因此,如果您在片段中调用此函数,则您的ViewModel将在Fragment处于活动状态时仍然存在,等等。 看一下该图:

范围生命周期
图片

ViewModelProvider负责在第一次调用中创建一个新实例,或者如果重新创建了Activity或Fragment则返回旧的实例。

不要混淆

 MainViewModel::class.java 

在科特林,如果您关注

 MainViewModel::class 

这将返回KClass ,这与Java中的Class不同。 因此,如果我们编写.java ,那么根据文档,它是:
将返回与此KClass实例相对应的Java 实例
让我们看看旋转屏幕时会发生什么


我们拥有与屏幕旋转之前相同的数据。

在上一篇文章中,我说过我们的应用程序将获取Github存储库的列表并显示它们。 为此,我们必须添加getRepositories函数,该函数将返回假的存储库列表:

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


我们还需要在MainViewModel中有一个从RepoModel调用getRepositories的方法:

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


最后,我们需要在RecyclerView中显示这些存储库。 为此,我们必须:

  • 创建布局rv_item_repository.xml
  • RecyclerView添加到布局activity_main.xml
  • 创建RepositoryRecyclerViewAdapter
  • 在recyclerview安装适配器

为了创建rv_item_repository.xml,我使用了CardView库,因此我们需要将其添加到build.gradle(应用程序)中:

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

看起来是这样的:

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> 


下一步是将RecyclerView添加到activity_main.xml中 。 在执行此操作之前,请确保添加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> 



请注意,我们删除了一些TextView元素,现在该按钮启动loadRepositories而不是refresh

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


让我们在不需要时从MainViewModel中删除refresh方法,从RepoModel中删除refreshData。

现在,您需要为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() } } } 


请注意,ViewHolder采用的是RvItemRepositoryBinding类型的实例,而不是View ,因此我们可以在ViewHolder中为每个元素实现数据绑定。 不要为单行功能(oneline)感到尴尬:

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

这只是以下内容的简短条目:

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

项目[position]是索引运算符的实现。 它类似于items.get(position)

另一行可能会使您感到困惑:

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

如果不使用_,则可以将其替换为_。 好吧

我们创建了适配器,但仍未将其应用于MainActivity中recyclerView

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


运行应用程序


真奇怪 怎么了

  • 活动已创建,因此还创建了一个新适配器,其中的存储库实际上是空的
  • 我们按下按钮
  • 称为loadRepositories,显示进度
  • 2秒钟后,我们得到了存储库,进度被隐藏了,但是没有出现。 这是因为适配器中未调用notifyDataSetChanged
  • 当我们旋转屏幕时,将创建一个新的Activity,因此将使用带有一些数据的repositories参数创建一个新的适配器

因此,由于MainViewModel应该通知MainActivity新项目,我们可以调用notifyDataSetChanged吗?

我们不能。

这真的很重要, MainViewModel根本不应该了解MainActivity

MainActivity是拥有MainViewModel实例的实例,因此它应该侦听更改并将更改通知Adapter

但是怎么做呢?

我们可以观察存储库 ,因此在更改数据之后,我们可以更改适配器。

这个决定有什么问题?

让我们看一下以下情况:

  • MainActivity中,我们观察存储库:发生更改时,我们执行notifyDataSetChanged
  • 我们按下按钮
  • 在我们等待数据更改时,由于配置更改 ,可能会重新创建MainActivity
  • 我们的MainViewModel仍然存在
  • 2秒后,“ 存储库”字段将接收新项目,并通知观察者数据已更改
  • 观察者尝试在不再存在的适配器上执行notifyDataSetChanged ,因为 MainActivity已重新创建

好吧,我们的决定还不够好。

LiveData简介


LiveData是另一个了解生命周期的组件,它基于可了解View生命周期的可观察对象。 因此,当Activity由于配置更改而被销毁时, LiveData会知道它,因此它也将从被销毁的Activity中删除观察者。

我们在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 } }) } } 


并开始观察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. } } 


这个词是什么意思? 如果一个函数只有一个参数,则可以使用it关键字访问此参数。 因此,假设我们有一个lambda表达式要乘以2:

 ((a) -> 2 * a) 

可以替换为:

 (it * 2) 

如果立即启动该应用程序,则可以确保一切正常


...

为什么比MVP更喜欢MVVM?



  • View没有无聊的界面,因为 ViewModel没有引用View
  • Presenter没有无聊的界面,这是没有必要的
  • 处理配置更改要容易得多
  • 使用MVVM,我们可以减少活动,片段等的代码。

...

储存库模式


方案
图片

如前所述, Model只是我们准备数据的层的抽象名称。 它通常包含存储库和数据类。 每个实体(数据)类都有一个对应的存储库类。 例如,如果我们具有UserPost类,则还必须具有UserRepositoryPostRepository 。 所有数据都来自那里。 我们绝不应该从View或ViewModel调用共享首选项或数据库的实例。

因此,我们可以将RepoModel重命名为GitRepoRepository ,其中GitRepo将来自Github存储库,而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>) } 


好的, MainViewModelGitRepoRepsitories获取存储库的Github列表,但是从哪里获取GitRepoRepositories呢?

您可以从实例直接在存储库上调用客户端数据库 ,但这仍然不是最佳做法。 您的应用程序应尽可能模块化。 如果您决定使用其他客户用Retrofit代替Volley怎么办? 如果您内部有某种逻辑,将很难进行重构。 您的存储库不需要知道您要使用哪个客户端来检索远程数据。

  • 存储库唯一需要知道的是数据是远程还是本地到达。 无需知道我们如何获取此远程或本地数据。
  • 唯一需要的视图模型是数据
  • View唯一要做的就是显示此数据。

当我刚开始在Android上进行开发时,我想知道应用程序如何脱机工作以及数据同步如何工作。 好的应用程序体系结构使我们可以轻松做到这一点。 例如,如果存在Internet连接,则在ViewModel中调用loadRepositories时, GitRepoRepositories可以从远程数据源接收数据并将其保存到本地数据源。 当电话离线时, GitRepoRepository可以从本地存储接收数据。 因此, 存储库必须具有RemoteDataSourceLocalDataSource的实例以及该数据应来自的逻辑处理。

添加本地数据源

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


在这里,我们有两种方法:第一种返回虚假的本地数据,第二种用于虚拟数据存储。

添加远程数据源

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


只有一种方法可以返回伪造的远程数据。

现在我们可以向存储库添加一些逻辑:

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


因此,通过共享源,我们可以轻松地在本地保存数据。

如果您只需要网络数据,还需要使用存储库模板怎么办?是的 这使代码测试更加容易,其他开发人员可以更好地理解您的代码,并且可以更快地支持它!

...

Android Manager包装器


如果您想在GitRepoRepository中检查您的Internet连接以了解从何处请求数据,该怎么办?我们已经说过,我们不应在ViewModel和Model中放置任何与Android相关的代码,那么该如何处理呢?

让我们为互联网连接编写一个包装器:

NetManager.kt(类似的解决方案适用于其他管理器,例如,适用于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 } } 


仅当我们添加清单权限时,此代码才有效:

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

但是,如何在资源库中创建一个实例,如果我们没有上下文(上下文)?我们可以在构造函数中请求它:

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


我们在ViewModel中的新GitRepoRepository实例之前创建。正如我们现在可以NetManager在视图模型,当为NetManager必要的条件您可以从具有上下文的Lifecycle-aware组件库中使用AndroidViewModel 这是应用程序的上下文,而不是活动。

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


在这条线

 constructor(application: Application) : super(application) 

我们为MainViewModel定义了一个构造函数这是必需的,因为AndroidViewModel在其构造函数中请求应用程序的实例因此,在我们的构造函数中,我们调用super方法,该方法调用我们继承AndroidViewModel构造函数

注意:如果这样做,我们可以摆脱一行:

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

现在,我们在GitRepoRepository中有了NetManager实例,我们可以检查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>) } 


因此,如果我们有Internet连接,我们将收到已删除的数据并将其保存在本地。如果没有Internet连接,我们将获取本地数据。

注意在科特林:运营商为null检查并在返回值IT

在以下文章之一中,我将介绍依赖项注入,在ViewModel中创建存储库实例有多糟,以及如何避免使用AndroidViewModel。我还将写一些代码中现在存在的许多问题。我离开它们是有原因的...

我试图向您展示问题,以便您可以理解为什么所有这些库都很流行以及为什么要使用它们。

PS我改变了主意,映射器(映射器)。我决定在以下文章中对此进行介绍。

Source: https://habr.com/ru/post/zh-CN432826/


All Articles