文作者: 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 的。这有如下好处:
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 拥有大量的代码,承担了过多的责任,那么:
? 分发责任,如果需要的话,添加 domain 层
使用数据仓库
如 https://developer.android.com/jetpack/docs/guide 中所说,大部分 App 有多个数据源:
在你的应用中拥有一个数据层是一个好主意,它和你的视图层完全隔离。保持缓存和数据库与网络同步的算法并不简单。建议使用单独的 Repository 类作为处理这种复杂性的单一入口点.
如果你有多个不同的数据模型,考虑使用多个 Repository 仓库。
? 添加数据仓库作为你的数据的单一入口点。
处理数据状态
考虑下面这个场景:你正在观察 ViewModel 暴露出来的一个 LiveData,它包含了需要显示的列表项。那么 View 如何区分数据已经加载,网络错误和空集合?
? 使用包装类或者另一个 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 中进行保存状态或者数据相关的核心逻辑。 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 被观察。下面这些通用的设计模式:
你并不需要经常继承 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中,有三种方法:
<form action=’’method=’post’> </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函数(简洁性和临时性)以及函数的属性以及如何实现函数的文档化描述等。
本文就写这些了,记得点赞 +关注@传新视界,转发分享给更多的朋友。再见^_^
*请认真填写需求信息,我们会在24小时内与您取得联系。