整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

「Medium 万赞好文」ViewModel 和 L

「Medium 万赞好文」ViewModel 和 LIveData:模式 + 反模式

文作者: https://medium.com/@JoseAlcerreca

原文地址: https://medium.com/androiddevelopers/viewmodels-and-livedata-patterns-antipatterns-21efaef74a54

译者:秉心说

View 和 ViewModel

分配责任

理想情况下,ViewModel 应该对 Android 世界一无所知。这提升了可测试性,内存泄漏安全性,并且便于模块化。通常的做法是保证你的 ViewModel 中没有导入任何 android.*,android.arch.* (译者注:现在应该再加一个 androidx.lifecycle)除外。这对 Presenter(MVP) 来说也一样。

? 不要让 ViewModel 和 Presenter 接触到 Android 框架中的类

条件语句,循环和通用逻辑应该放在应用的 ViewModel 或者其它层来执行,而不是在 Activity 和 Fragment 中。View 通常是不进行单元测试的,除非你使用了 http://robolectric.org/,所以其中的代码越少越好。View 只需要知道如何展示数据以及向 ViewModel/Presenter 发送用户事件。这叫做 https://martinfowler.com/eaaDev/PassiveScreen.html 模式。

? 让 Activity/Fragment 中的逻辑尽量精简

ViewModel 中的 View 引用

https://developer.android.com/topic/libraries/architecture/viewmodel.html 和 Activity/Fragment具有不同的作用域。当 Viewmodel 进入 alive 状态且在运行时,activity 可能位于 https://developer.android.com/guide/components/activities/activity-lifecycle.html 的任何状态。Activitie 和 Fragment 可以在 ViewModel 无感知的情况下被销毁和重新创建。

向 ViewModel 传递 View(Activity/Fragment) 的引用是一个很大的冒险。假设 ViewModel 请求网络,稍后返回数据。若此时 View 的引用已经被销毁,或者已经成为一个不可见的 Activity。这将导致内存泄漏,甚至 crash。

? 避免在 ViewModel 中持有 View 的引用

在 ViewModel 和 View 中通信的建议方式是观察者模式,使用 LiveData 或者其他类库中的可观察对象。

观察者模式

在 Android 中设计表示层的一种非常方便的方法是让 View 观察和订阅 ViewModel(中的变化)。由于 ViewModel 并不知道 Android 的任何东西,所以它也不知道 Android 是如何频繁的杀死 View 的。这有如下好处:

  1. ViewModel 在配置变化时保持不变,所以当设备旋转时不需要再重新请求资源(数据库或者网络)。
  2. 当耗时任务执行结束,ViewModel 中的可观察数据更新了。这个数据是否被观察并不重要,尝试更新一个
  3. 不存在的 View 并不会导致空指针异常。
  4. ViewModel 不持有 View 的引用,降低了内存泄漏的风险。
private void subscribeToModel() {
 // Observe product data
 viewModel.getObservableProduct().observe(this, new Observer<Product>() {
 @Override
 public void onChanged(@Nullable Product product) {
 mTitle.setText(product.title);
 }
 });
}

? 让 UI 观察数据的变化,而不是把数据推送给 UI

胖 ViewModel

无论是什么让你选择分层,这总是一个好主意。如果你的 ViewModel 拥有大量的代码,承担了过多的责任,那么:

  • 移除一部分逻辑到和 ViewModel 具有同样作用域的地方。这部分将和应用的其他部分进行通信并更新
  • ViewModel 持有的 LiveData。
  • 采用 https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html,添加一个 domain 层。这是一个可测试,易维护的架构。https://github.com/android/architecture-samples 中有 Clean Architecture 的示例。

? 分发责任,如果需要的话,添加 domain 层

使用数据仓库

如 https://developer.android.com/jetpack/docs/guide 中所说,大部分 App 有多个数据源:

  1. 远程:网络或者云端
  2. 本地:数据库或者文件
  3. 内存缓存

在你的应用中拥有一个数据层是一个好主意,它和你的视图层完全隔离。保持缓存和数据库与网络同步的算法并不简单。建议使用单独的 Repository 类作为处理这种复杂性的单一入口点.

如果你有多个不同的数据模型,考虑使用多个 Repository 仓库。

? 添加数据仓库作为你的数据的单一入口点。

处理数据状态

考虑下面这个场景:你正在观察 ViewModel 暴露出来的一个 LiveData,它包含了需要显示的列表项。那么 View 如何区分数据已经加载,网络错误和空集合?

  • 你可以通过 ViewModel 暴露出一个 LiveData ,MyDataState 可以包含数据正在加载,已经加载完成,发生错误等信息。
  • 你可以将数据包装在具有状态和其他元数据(如错误消息)的类中。查看示例中的 https://developer.android.com/jetpack/docs/guide#addendum 类。

? 使用包装类或者另一个 LiveData 来暴露数据的状态信息

保存 activity 状态

当 activity 被销毁或者进程被杀导致 activity 不可见时,重新创建屏幕所需要的信息被称为 activity 状态。屏幕旋转就是最明显的例子,如果状态保存在 ViewModel 中,它就是安全的。

但是,你可能需要在 ViewModel 也不存在的情况下恢复状态,例如当操作系统由于资源紧张杀掉你的进程时。

为了有效的保存和恢复 UI 状态,使用 onSaveInstanceState() 和 ViewModel 组合。

详见:[ViewModels: Persistence, onSaveInstanceState(), Restoring UIState and Loaders](https://medium.com/google-developers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090) 。

Event

Event 指只发生一次的事件。ViewModel 暴露出的是数据,那么 Event 呢?例如,导航事件或者展示 Snackbar 消息,都是应该只被执行一次的动作。

LiveData 保存和恢复数据,和 Event 的概念并不完全符合。看看具有下面字段的一个 ViewModel:

LiveData<String> snackbarMessage=new MutableLiveData<>();

Activity 开始观察它,当 ViewModel 结束一个操作时需要更新它的值:

snackbarMessage.setValue("Item saved!");

Activity 接收到了值并且显示了 SnackBar。显然就应该是这样的。

但是,如果用户旋转了手机,新的 Activity 被创建并且开始观察。当对 LiveData 的观察开始时,新的 Activity 会立即接收到旧的值,导致消息再次被显示。

与其使用架构组件的库或者扩展来解决这个问题,不如把它当做设计问题来看。我们建议你把事件当做状态的一部分。

把事件设计成状态的一部分。更多细节请阅读 https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

ViewModel 的泄露

得益于方便的连接 UI 层和应用的其他层,响应式编程在 Android 中工作的很高效。LiveData 是这个模式的关键组件,你的 Activity 和 Fragment 都会观察 LiveData 实例。

LiveData 如何与其他组件通信取决于你,要注意内存泄露和边界情况。如下图所示,视图层(Presentation Layer)使用观察者模式,数据层(Data Layer)使用回调。

当用户退出应用时,View 不可见了,所以 ViewModel 不需要再被观察。如果数据仓库 Repository 是单例模式并且和应用同作用域,那么直到应用进程被杀死,数据仓库 Repository 才会被销毁。 只有当系统资源不足或者用户手动杀掉应用这才会发生。如果数据仓库 Repository 持有 ViewModel 的回调的引用,那么 ViewModel 将会发生内存泄露。

如果 ViewModel 很轻量,或者保证操作很快就会结束,这种泄露也不是什么大问题。但是,事实并不总是这样。理想情况下,只要没有被 View 观察了,ViewModel 就应该被释放。

你可以选择下面几种方式来达成目的:

  • 通过 ViewModel.onCLeared() 通知数据仓库释放 ViewModel 的回调
  • 在数据仓库 Repository 中使用 弱引用 ,或者 Event Bu(两者都容易被误用,甚至被认为是有害的)。
  • 通过在 View 和 ViewModel 中使用 LiveData 的方式,在数据仓库和 ViewModel 之间进程通信

? 考虑边界情况,内存泄露和耗时任务会如何影响架构中的实例。

? 不要在 ViewModel 中进行保存状态或者数据相关的核心逻辑。 ViewModel 中的每一次调用都可能是最后一次操作。

数据仓库中的 LiveData

为了避免 ViewModel 泄露和回调地狱,数据仓库应该被这样观察:

当 ViewModel 被清除,或者 View 的生命周期结束,订阅也会被清除:

如果你尝试这种方式的话会遇到一个问题:如果不访问 LifeCycleOwner 对象的话,如果通过 ViewModel 订阅数据仓库?使用 https://developer.android.com/topic/libraries/architecture/livedata#transform_livedata 可以很方便的解决这个问题。Transformations.switchMap 可以让你根据一个 LiveData 实例的变化创建新的 LiveData。它还允许你通过调用链传递观察者的生命周期信息:

LiveData<Repo> repo=Transformations.switchMap(repoIdLiveData, repoId -> {
 if (repoId.isEmpty()) {
 return AbsentLiveData.create();
 }
 return repository.loadRepo(repoId);
 }
);

在这个例子中,当触发更新时,这个函数被调用并且结果被分发到下游。如果一个 Activity 观察了 repo,那么同样的 LifecycleOwner 将被应用在 repository.loadRepo(repoId) 的调用上。

无论什么时候你在 https://developer.android.com/reference/android/arch/lifecycle/ViewModel.html 内部需要一个 https://developer.android.com/reference/android/arch/lifecycle/Lifecycle.html 对象时,https://developer.android.com/topic/libraries/architecture/livedata#transform_livedata 都是一个好方案。

继承 LiveData

在 ViewModel 中使用 LiveData 最常用的就是 MutableLiveData,并且将其作为 LiveData 暴露给外部,以保证对观察者不可变。

如果你需要更多功能,继承 LiveData 会让你知道活跃的观察者。这对你监听位置或者传感器服务很有用。

public class MyLiveData extends LiveData<MyData> {
 public MyLiveData(Context context) {
 // Initialize service
 }
 @Override
 protected void onActive() {
 // Start listening
 }
 @Override
 protected void onInactive() {
 // Stop listening
 }
}

什么时候不要继承 LiveData

你也可以通过 onActive() 来开启服务加载数据。但是除非你有一个很好的理由来说明你不需要等待 LiveData 被观察。下面这些通用的设计模式:

  • 给 ViewModel 添加 start() 方法,并尽快调用它。https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragment.java#L64]
  • 设置一个触发加载的属性 https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/ui/repo/RepoFragment.kt]

你并不需要经常继承 LiveData 。让 Activity 和 Fragment 告诉 ViewModel 什么时候开始加载数据。

分割线

翻译就到这里了,其实这篇文章已经在我的收藏夹里躺了很久了。最近 Google 重写了 https://github.com/android/plaid 应用,用上了一系列最新技术栈, https://developer.android.com/topic/libraries/architecture/,MVVM, Kotlin,协程 等等。这也是我很喜欢的一套技术栈,之前基于此开源了 https://github.com/lulululbj/wanandroid 应用 ,详见 https://juejin.im/post/5cb473e66fb9a068af37a6ce 。

当时基于对 MVVM 的浅薄理解写了一套自认为是 MVVM 的 MVVM 架构,在阅读一些关于架构的文章,以及 Plaid 源码之后,发现了自己的 MVVM 的一些认知误区。后续会对 https://github.com/lulululbj/wanandroid 应用进行合理改造,并结合上面译文中提到的知识点作一定的说明。欢迎 Star !

网站开发过程中,需要从前端向后端传入数据,由后端对数据进行操作,比如计算、存入数据库等。

从前端向后端传输数据,一般使用form表单。在Django中,有三种方法:

  1. 手写前端html代码,使用form表单:
<form action=’’method=’post’> </form>
  1. Django提供了form类,生成form表单。
  2. Django提供了Modelform类,生成form表单。

Modelform做为Django中集成的组件,主要针对数据库中的某个表操作,通过models.py关联数据库。

本文着重讲modelform的使用,下面正式开始。

首先建立一个forms.py,用来写项目里的表单类。

首先引入几个类

from django import forms #引入forms表单类
from users.models import User #引入models里的User类
from django.core.exceptions import ValidationError #引入异常抛出类

创建User表单类,类继承了forms.ModelForm,password_confirm是密码确认,我们在进行注册的时候,往往会要求确认一次密码。

class Meta:是利用model创建表单的类。model=User,用来实例化models.py中的User类,fields是表单中的字段,也就是表单项目。widgets是一个字典,在这里定义password表单为密码输入格式。

class UserModelForm(forms.ModelForm):
    password_confirm=forms.CharField(label="确认密码",widget=forms.PasswordInput,min_length=6,max_length=20)
    class Meta:
        model=User
        fields=['username','password','password_confirm','gender','role']
        widgets={"password":forms.PasswordInput()}

下面在templates文件夹下建立一个user_add_form.html文件,用来展示表单。

在views.py中增加一个方法user_add_form()方法。该方法需要使用forms.py中的UserModelForm类和models中的User类,在头部引入这两个类,

from users.models import User   #引入models里的User类
from users.forms import UserModelForm #引入forms里的UserModelForm

增加一个user_add_form(request)方法,当前端的request是一个get方法时,实例化UserModelForm(),返回render方法,显示form表单,否则,将request.POST的数据传入UserModelForm类并实例化,

def user_add_form(request):
    if request.method=="GET":
        form=UserModelForm()
    return render(request,"user_add_form.html",{"form":form})

在urls.py中增加一个路由。

在user_add_form.html中写入{{form}},用来展示后端返回的form数据。

下面在浏览中测试一下。

输入127.0.0.1:8000/user_add_form/

右键检查页面源码,发现,字段及输入框已经在页面中。

本文结束。下一篇文章,将对user_add_form.html进行修改,实现表单的输入功能,并通过表单将数据传入后端,并插入数据库。

前面几期内容连续的介绍了Python的函数相关编程知识,是一个相对且完整的知识域,本文主要是对函数知识的一些有益拓展和补充。

本文简单扼要地说,辅以代码进一步地加深理解。我们继续——记得点赞+关注@传新视界

函数进阶与补充

递归函数

当函数调用自身而生成最终结果时,这样的函数称为递归。有时递归函数非常有用,因为它们使编写代码变得更容易——使用递归范式编写一些算法非常容易,而其他算法则不是这样。没有不能以迭代方式重写的递归函数,换句话说,所有递归函数都可以通过循环迭代的方式实现,因此通常由程序员根据手头的情况选择最佳方法。

递归函数主体通常有两个部分:一部分的返回值依赖于对自身的后续调用,另一部分的返回值不依赖于对自身的后续调用(称基本情况,或递归边界)。

作为理解的参考示例,我们看一个阶乘函数N!作为递归的两部分分别是:基本情况(边界,用来结束递归)是当N为0或1时,函数返回1,不需要进一步计算。另一方面,在一般情况下的自我调用,即N!返回的生成结果:

1 * 2 * ... * (N-1) * N

如果你仔细想想,N!可以写成这样:N!=(N - 1) !*N。作为一个实际的例子,请看如下的阶乘表示:

5!=1 * 2 * 3 * 4 * 5=(1 * 2 * 3 * 4) * 5=4! * 5

我们来转化成函数实现:

# 阶乘递归函数实现
def factorial(n):
    if n in (0, 1): # 递归边界
        return 1
    return factorial(n - 1) * n # 递归调用

高手大侠们在编写算法时经常使用递归函数,编写递归函数非常有趣。作为练习,尝试使用递归和迭代方法解决几个简单的问题。很好的练习对象可能是计算斐波那契数列,或其它诸如此类的东西。自己动手去试试吧。

提示:

在编写递归函数时,总是考虑要进行多少个嵌套调用,因为这是有限制的。有关这方面的更多信息,请查看sys.getrecursionlimit()和sys.setrecursionlimit()。

匿名函数

还有一种函数是匿名函数(Anonymous functions)。这些函数在Python中称为lambda(兰姆达),其通常在使用具有自己完整定义名称的函数有些多余时而使用,此时所需要的只是一个快速、简单的一行程序来完成这项工作。

假设我们想要一个列表,所有N的某个值,是5的倍数的数字。为此,我们可以使用filter()函数,它需要一个函数和一个可迭代对象作为输入。返回值是一个过滤器对象,当你遍历它时,会从输入可迭代对象中生成元素,所需的参数函数会为其返回True。如果不使用匿名函数,我们可能会这样做:

def isMultipleOfFive(n):
    return not n % 5

def getMultiplesOfFive(n):
    return list(filter(isMultipleOfFive, range(n)))

注意我们如何使用isMultipleOfFive()来过滤前n个自然数。这似乎有点过分——任务及其很简单,我们不需要为其他任何事情保留isMultipleOfFive()函数。此时,我们就可用lambda函数来重写它:

# lambda过滤
def getMultiplesOfFive(n):
     return list(filter(lambda k: not k % 5, range(n)))

逻辑是完全相同的,但是过滤函数现在是个lambda函数,显然,Lambda更简单。

定义Lambda函数非常简单,它遵循以下形式:

funcName=lambda [parameter_list]: expression

其返回的是一个函数对象,相当于:

def func_ name([parameter_list]):return expression

参数列表以逗号分隔。

注意,可选参数是方括号括起来的部分,是通用语法的表示形式,即文中的方括号部分是可选的,根据实际需要提供,

我们再来看另外两个等价函数的例子,以两种形式定义:

# lambda说明
# 示例 1: 两数相加
def adder(a, b):
    return a + b
# 等价于:
adder_lambda=lambda a, b: a + b

# 示例 2: 字符串转大写
def to_upper(s):
    return s.upper()
# 等价于:
to_upper_lambda=lambda s: s.upper()

前面的例子非常简单。第一个函数将两个数字相加,第二个函数生成字符串的大写版本。注意,我们将lambda表达式返回的内容赋值给一个名称(adder_lambda, to_upper_lambda),但是当按照filter()示例中的方式使用lambda时,就不需要这样做了——不需要把匿名函数赋给变量。

函数属性

Python中每个函数都是一个完整的对。因此,它有许多属性。其中一些是特殊的,可以以内省的方式在运行时检查函数对象。下面的示例,展示了它们的一部分以及如何为示例函数显示它们的值:

# 函数属性
def multiplication(a, b=1):
    """返回a乘以b的结构. """
    return a * b

if __name__=="__main__":
    special_attributes=[
    "__doc__", "__name__", "__qualname__", "__module__",
    "__defaults__", "__code__", "__globals__", "__dict__",
    "__closure__", "__annotations__", "__kwdefaults__",
    ]
    for attribute in special_attributes:
        print(attribute, '->', getattr(multiplication, attribute))

我们使用内置的getattr()函数来获取这些属性的值。getattr(obj, attribute)等价于obj.attribute,当我们需要在运行时动态地获取属性时,就从变量中获取属性的名称(如本例中所示),此时它就会派上用场。

运行这个脚本会得到类似如下输出:

__doc__ -> 返回a乘以b的结果.

__name__ -> multiplication

__qualname__ -> multiplication

__module__ -> __main__

__defaults__ -> (1,)

__code__ -> <……>

__globals__ -> {…略…}

__dict__ -> {}

__closure__ -> None

__annotations__ -> {}

__kwdefaults__ -> None

这里省略了__globals__属性的值,内容太多。这个属性的含义可以在Python数据模型文档页面(或自带帮助文档中)的可调用类型部分找到:

https://docs.python.org/3/reference/datamodel.html#the-standard-typehierarchy

再次提醒:如果你想查看对象的所有属性,只需调用dir(object_name),将得到其所有属性的列表

内置函数

Python自带很多内置函数。它们可以在任何地方使用,你可以通过dir(__builtins__)来查看builtins模块,或通过访问官方Python文档来获得它们的列表。这里就不一一介绍了。在前面的学习过程中,我们已经见过其中的一些,如any、bin、bool、divmod、filter、float、getattr、id、int、len、list、min、print、set、tuple、type和zip等,但还有更多,建议你至少应该阅读一次。熟悉它们,尝试它们,为它们每个编写一小段代码,并确保您随时可以使用它们,以便在需要时使用它们。

可在官方文档中找到这个内置函数列表:https://docs.python.org/3/library/functions.html 。

文档化代码

我们非常喜欢不需要文档的代码。当我们正确地编程、选择正确的名称、并注意细节时,代码应该是不言自明的,几乎不需要文档。不过,有时注释非常有用,添加一些文档化描述也是如此。你可以在Python的PEP 257规范——文档字符串约定中找到Python的文档指南:

https://www.python.org/dev/peps/pep-0257/,

但在这里还是会向你展示基本原理。Python的文档中包含字符串,这些字符串被恰当地称为文档字符串(docstrings)。任何对象都可以被文档化来加以描述记录,可以使用单行或多行文档字符串。单行程序非常简单。不是为函数提供另外的签名,而应该声明或描述函数的目的。请看下面的示例:

# 简单的文档化代码
def square(n):
    """功能:返回数字n的平方。 """
    return n ** 2

def get_username(userid):
    """功能:返回给定id的用户名称。 """
    return db.get(user_id=userid).username
  • 使用三重双引号字符串可以在以后轻松展开或扩展文档内容。
  • 使用以句号结尾的句子,不要在前后留下空行。
  • 多行注释的结构与此类似。应该用一行代码简单地说明对象的主旨,然后是更详细的描述。

作为多行文档化的一个例子,我们在下面的例子中使用Sphinx表示法记录了一个虚构的connect()函数及文档化描述:

# 多行文档化代码
def connect(host, port, user, password):
    """功能:连接数据库并返回连接对象.
    使用如下参数直接连接 PostgreSQL数据库.
    :param host: 主机 IP.
    :param port: 端口.
    :param user: 连接用户名.
    :param password: 连接密码.
    :return: 连接对象.
    """
    # 函数主体...
    return connection

提示:

Sphinx是用于创建Python文档的最广泛使用的工具之一——事实上,官方Python文档就是用它编写的。绝对值得花点时间去看看。

内置函数help()用于即时交互使用的,它就使用对象的文档字符串为对象创建文档页面来展示对象的用法。基本用法如下:

def square(n):
    """功能:返回数字n的平方。 """
    return n ** 2

help(square)
Help on function square in module __main__:

square(n)
功能:返回数字n的平方。

首先明确或定义一个对象或函数(包括已有的对象或函数),然后使用内置help函数,并把对象或函数做help的参数,该函数就会返回相应对象的说明文档了。就这么简单。

本文小结

本文主要基于Python语言的一大特色——函数来拓展的一些相关编程知识,包括递归函数(重点是有限性和边界性)、lambda函数(简洁性和临时性)以及函数的属性以及如何实现函数的文档化描述等。

本文就写这些了,记得点赞 +关注@传新视界,转发分享给更多的朋友。再见^_^