基于Kotlin的现代MVI架构



在过去的两年中,Badoo的Android开发人员经历了漫长而艰难的道路,从MVP到完全不同的应用程序体系结构方法。 想与ANublo分享我们的同事Zsolt Kocsi的文章翻译,以描述我们遇到的问题及其解决方案。

这是专门介绍在Kotlin上开发现代MVI架构的几篇文章的第一篇。

让我们从头开始:陈述问题


在任何时候,应用程序都具有确定其行为和用户所见内容的特定状态。 如果仅关注几个类,则此状态包括变量的所有值-从简单标志到单个对象。 这些变量中的每一个都有自己的生命,并由代码的不同部分控制。 您只能通过逐一检查所有应用程序来确定它们的当前状态。

通过编写代码,我们在脑海中创建了系统工作的现有模型。 当一切都按计划进行时,我们很容易实现理想情况,但完全无法计算出应用程序的所有可能问题和状况。 迟早,我们没有想到的条件之一将超越我们,并且我们将遇到错误。

最初,代码是根据我们对系统工作方式的想法编写的。 但是将来,在调试五个阶段​​中 ,有必要痛苦地重做所有事情,同时更改已经在我脑海中开发的已创建系统的模型。 仍然希望我们早晚了解问题出在哪里,并且该错误将得到修复。

但这远非总是幸运的。 系统越复杂,遇到意外状态的可能性就越大,调试该状态将是长期梦in以求的梦想。

在Badoo中,所有应用程序基本上都是异步的-不仅是因为用户可以通过UI使用广泛的功能,还因为服务器可以单向发送数据。 应用程序的状态和行为受到很大的影响-从更改付款状态到新的匹配项和验证请求。

结果,在我们的聊天模块中,我们遇到了几个奇怪且难以重现的错误,这些错误给每个人都充了很多血。 有时测试人员设法将它们写下来,但是在开发人员的设备上没有重复进行。 由于采用了异步代码,因此完全不可能重复发生一系列事件。 而且,由于该应用程序没有崩溃,我们甚至没有显示从何处开始搜索的堆栈跟踪。

清洁建筑也无法帮助我们。 甚至在我们重写了聊天模块之后,A / B测试也显示出使用新模块和旧模块的用户发出的消息数量略有差异,但差异很大。 我们认为,这是由于错误的难以复制和种族状态造成的。 检查所有其他因素后,差异仍然存在。 公司的利益受到损害,开发人员很难维护代码。

如果新组件的性能比现有组件差,则不能发布它,但是也不能发布它-因为它进行了更新,所以有原因。 因此,您需要了解为什么在看起来完全正常且不会崩溃的系统中,消息数量下降了。

从哪里开始搜索?

剧透:这不是“清洁建筑”的错-一如既往,人为的责任应归咎于人。 最后,我们当然修复了这些错误,但为此花费了很多时间和精力。 然后我们想到:有没有更简单的方法来避免这些问题?

隧道尽头的灯...


像Model-View-Intent和“单向数据流”这样的时尚术语对我们来说是熟悉的。 如果您的情况并非如此,我建议您谷歌搜索它们-Internet上有很多关于这些主题的文章。 Android开发人员尤其推荐Hannes Dorfman的八件套材料

从2017年初开始,我们就开始使用这些来自Web开发的想法。 事实证明,诸如Flux和Redux之类的方法非常有用-它们帮助我们解决了许多问题。

首先,将所有状态元素(影响UI并触发各种动作的变量)包含在一个对象State中非常有用。 当所有内容都存储在一个地方时,整个图片会更清晰可见。 例如,如果您想使用这种方法加载数据,则需要有效负载isLoading字段。 查看它们,您将看到何时接收数据( 有效负载 )以及是否向用户显示动画( isLoading )。

此外,如果我们不再使用回调执行并行代码,而是通过一系列事务将应用程序状态的变化表示为一系列事务,那么我们将获得一个入口点。 我们向您介绍Reducer ,它是从函数编程中找来的。 它获取当前状态和有关进一步操作( Intent )的数据,并根据它们创建一个新状态:

Reducer = (State, Intent) -> State

继续前面的示例中的数据加载,我们执行以下操作:

  • 开始加载
  • 成功完成


然后,您可以使用以下规则创建Reducer:

  1. 对于StartedLoading,通过复制旧对象创建一个新的State对象,并将isLoading设置为true。
  2. 对于FinishedWithSuccess,创建一个新的State对象,复制旧的State对象,其中isLoading值将设置为false, 有效负载值将为
    匹配上传。

如果将结果状态序列输出到日志,将看到以下内容:

  1. 状态( 有效负载 = null, isLoading = false)-初始状态。
  2. 状态( 有效负载 = null, isLoading = true)-StartedLoading之后。
  3. 状态( 有效负载 =数据, isLoading = -FinishedWithSuccess之后。

通过将这些状态连接到UI,您将看到该过程的所有阶段:首先是空白屏幕,然后是加载屏幕,最后是必要的数据。

这种方法有很多优点。

  • 首先,通过使用一系列事务来集中更改状态,我们不允许种族状态和许多看不见的烦人的bug。
  • 其次,研究了一系列事务后,我们可以了解发生了什么,发生了什么原因以及它如何影响应用程序的状态。 此外,使用Reducer,可以更轻松地想象在设备上首次启动应用程序之前的所有状态更改。
  • 最后,我们能够创建一个简单的界面。 由于所有状态都存储在一个位置(Store)中,该位置考虑了意图(Intents),使用Reducer进行更改并演示了状态链,因此您可以将所有业务逻辑放入Store中并使用该界面启动意图和显示状态。


还是不行

...也许火车冲你


仅仅使用减速器显然是不够的。 异步任务结果如何呢? 如何响应来自服务器的推送? 状态更改后如何启动其他任务(例如,清除缓存或从本地数据库加载数据)呢? 事实证明,要么我们没有将所有这些逻辑都包含在Reducer中(也就是说,业务逻辑的一半不会被覆盖,并且决定使用我们组件的人员将不得不照顾它),要么我们迫使Reducer立即执行所有操作。

MVI框架要求


当然,我们希望将单个功能的整个业务逻辑封装在一个独立的组件中,其他团队的开发人员可以通过简单地创建一个实例并订阅其状态来轻松地进行工作。

另外:

  • 它应该轻松地与系统的其他组件进行交互;
  • 在其内部结构中,应明确划分职责;
  • 组件的所有内部部分必须完全确定;
  • 仅当需要其他元素时,此类组件的基本实现才应该简单而复杂。

我们没有立即从Reducer过渡到我们今天使用的解决方案。 每个团队使用不同的方法都面临问题,开发出适合所有人的通用解决方案似乎不太可能。

但是,当前的情况适合每个人。 我们很高兴为您介绍MVICore! 该库的源代码是开放的,可以在GitHub上获得

什么是MVICore好


  • 一种使用单向数据流实现反应式编程业务功能的简便方法。
  • 缩放:基本实现仅包括Reducer,在更复杂的情况下,您可以使用其他组件。
  • 一种用于处理不想包含在状态中的事件的解决方案( SingleLiveEvent问题 )。
  • 一个简单的API,用于将功能(和系统的其他反应组件)与UI以及彼此绑定,并支持Android生命周期(不仅如此)。
  • 系统每个组件的中间件支持(请参见下文)。
  • 现成的记录器以及对每个组件进行时间调试的功能。


功能简介


由于分步说明已经发布在GitHub上,因此我将省略详细的示例,而将重点放在框架的主要组件上。

功能 -框架的中心元素,包含组件的所有业务逻辑。 功能由三个参数定义: 接口功能<愿望,状态,新闻>

Wish对应于Model-View-Intent的Intent-这些是我们希望在模型中看到的更改(由于Intent在Android开发人员的环境中具有其含义,因此我们必须找到其他名称)。 愿望是功能的切入点。

正如您已经了解的那样,状态是组件的状态。 国家不是一成不变的:我们不能改变其内部价值,但是我们可以创造新的国家。 这是输出:每次创建新状态时,都会将其传递给Rx流。

新闻 -处理不应处于状态的信号的组件; 新闻在创建期间使用一次( SingleLiveEvent问题 )。 使用新闻是可选的(您可以在功能签名中使用Kotlin的Nothing)。

还必须在Feature中包含Reducer

功能可能包含以下组件:

  • Actor-根据当前状态执行异步任务和/或条件状态修改(例如,表单验证)。 Actor将“愿望”绑定到特定的“效果”编号,然后将其传递给Reducer(在没有Actor的情况下,Reducer直接接收“愿望”)。
  • 新闻发布者-当“愿望”变为任何产生结果的效果(作为新状态)时调用。 根据这些数据,他决定是否创建新闻。
  • PostProcessor-在创建新状态后也被调用,并且还知道导致其创建的原因。 它启动某些附加动作(动作)。 动作-这些是无法从外部启动的“内部愿望”(例如,清除缓存)。 它们在演员中执行,从而产生了新的效果和状态链。
  • 引导程序是可以自行运行操作的组件。 它的主要功能是初始化功能和/或将外部源与操作相关联。 这些外部来源可能是来自其他功能的新闻,也可能是服务器数据,应在无需用户干预的情况下修改状态。


该图看起来很简单:


或包括上述所有其他组件:


功能本身包含所有业务逻辑并且可以立即使用,看起来再简单不过了:



还有什么


功能是框架的基石,它在概念上起作用。 但是图书馆有更多的提供。

  • 由于Feature的所有组件都是确定性的(Actor除外,Actor并不是完全确定性的,因为它与外部数据源进行交互,但是即使如此,它执行的分支也由输入数据而不是由外部条件确定),因此它们中的每一个都可以包装在中间件中。 同时,该库已经包含了用于日志记录和时间旅行调试的现成解决方案。
  • 中间件不仅适用于Feature,而且还适用于实现Consumer <T>接口的任何其他对象,这使其成为必不可少的调试工具。
  • 当使用调试器进行相反方向的调试时,可以实现DebugDrawer模块。
  • 该库包含一个IDEA插件,可用于为Feature的最常见实现添加模板,从而节省大量时间。
  • 有一些支持Android的帮助程序类,但是库本身并不与Android绑定。
  • 有一种现成的解决方案,用于通过基本API将组件绑定到UI以及彼此绑定(将在下一篇文章中讨论)。

我们希望您能尝试使用我们的库,并且它的使用将为您带来与我们一样多的欢乐-它的创建!

在11月24日至25日,您可以尝试加入我们! 我们将举办一次移动招聘活动:有一天,有可能会经历所有选择阶段并收到报价。 来自iOS和Android团队的同事将与莫斯科的候选人进行交流。 如果您来自其他城市,Badoo会产生旅行费用。 要获得邀请,请通过链接进行筛选测试。 祝你好运

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


All Articles