本文详细介绍了Meeting Room Helper移动应用程序的开发阶段:从构思到发布。 该应用程序是用Kotlin编写的,并基于简化的MVVM体系结构构建,无需使用数据绑定。 UI部分使用LiveData对象更新。 拒绝数据绑定的原因已详细说明。 该体系结构使用了许多有趣的解决方案,这些解决方案使将程序逻辑上拆分为小文件成为可能,从而最终简化了代码支持。


项目说明
3年前,我们公司提出了一个小项目,可即时预订会议室。 大多数人力资源经理和Arcadia都倾向于出于这种目的使用Outlook日历,但是其余的呢?
我将举两个开发者的例子
- 任何一支球队定期都有自发的愿望,要求他们快速集会5-10分钟。 这种愿望可以超越办公室任何角落的开发人员,并且为了不分散他们周围的同事的注意力,他们(不仅是开发人员)开始寻求免费的交谈。 同事从一个房间迁移到另一个房间(在我们的办公室中,会议室是连续排列的),同事“仔细检查”哪些房间目前是免费的。 结果,他们分散了内部同事的注意力。 即使要在公司章程中规定要执行死刑以中断集会,这类人过去也将永远如此。 谁了解,他就会了解。
- 这是另一种情况。 您刚刚离开饭厅,正走向自己,但是在这里,来自另一个部门的同事(或经理)会拦截您。 他想告诉您一些紧急情况,因此,您需要一间会议室。 根据规定,您必须先(通过电话或计算机)预订房间,然后才能入住。 如果您的手机带有移动Outlook,那就很好。 如果没有呢? 返回计算机,然后再次返回会议室? 要强迫每个员工将Outlook Express放在电话上并确保每个人都随身携带电话? 这些不是我们的方法。
因此,2.5年前,每个会议室都配备了自己的平板电脑:

对于这个项目,我的同事开发了该应用程序的第一个版本:Meeting Room Little Helper(
在这里您可以阅读有关内容 )。 MRLH允许预订,取消和续订预订,并显示剩余对话的状态。 识别员工的身份(使用Microsoft Face API云服务和我们的内部分析器)已成为一种创新的“技巧”。 事实证明该应用程序很可靠,并为公司忠实地服务了2.5年。
但是时间过去了……出现了新的想法。 我想要一些新鲜的东西,所以我们决定重写该应用程序。
职权范围
经常发生-但不幸的是,并非总是如此-开发始于技术规格的准备。 首先,我们打电话给最常使用平板电脑进行预订的人。 碰巧的是,他们中的大多数人都对以前专门使用Outlook的人力资源和经理沉迷。 从他们那里我们收到了以下反馈(从要求中可以立即清楚地看出人力资源要求和经理要求什么):
- 您必须添加通过任何平板电脑预订任何会议室的功能(以前,每个平板电脑仅允许您预订会议室);
- 查看全天会议的集会时间表很酷(理想情况下是任何一天);
- 整个开发周期必须在短时间内(6-7周)完成。
一切都符合客户的意愿,但是技术要求和未来呢? 从开发者协会添加对项目的一些要求:
- 该系统应既适用于现有的平板电脑,也适用于新的平板电脑。
- 系统的可扩展性-从50个对话以上(如果系统开始复制,这对于大多数客户来说应该足够了);
- 保持先前的功能(该应用程序的第一个版本使用Java API与Outlook服务进行通信,并且我们计划将其替换为专用的Microsoft Graph API,因此重要的是不要丢失功能);
- 最大限度地减少能耗(平板电脑由外部电池供电,因为商务中心不允许在墙上钻孔来铺设电线);
- 新的UX / UI设计,符合人体工程学,反映了所有创新。
总计8分。 要求是相当公平的。 此外,我们规定了一般的开发规则:
- 仅使用先进技术(这将使团队成为专家,而不是一站式发展,同时在可预见的将来简化项目支持);
- 遵循最佳做法,但不要盲目地将其视为理所当然,因为 任何专业人员(以及为此奋斗的开发人员)的主要原则是严格评估所有内容;
- 编写干净整洁的代码(当您尝试结合创新和紧迫的开发时间时,这可能是最困难的)。
已经开始。 一如既往的热情! 让我们看看接下来会发生什么。
设计方案
UX设计人员开发的应用程序设计:


这是主屏幕。 大多数时间都会显示。 所有必要的信息均符合人体工程学,位于此处:
- 房间名称及其编号;
- 当前状态;
- 直到下一次会议(或直到其结束)的时间;
- 屏幕底部的剩余房间状态。
请注意:转盘仅显示12小时,因为 系统已根据公司需求进行配置(Arcadia平板电脑的工作时间为上午8点至晚上8点,自动打开和关闭)


要预订房间,只需调用预订窗口并指明集会的持续时间即可。 其余房间的预订步骤相似,仅需单击房间图标即可。


如果要安排在特定时间召开会议,请转到下一个选项卡,转到今天将在会议室举行的会议列表,然后单击空闲时间。 此外,一切都与第一种情况相同。
完整的过渡树应如下所示:


让我们尝试有效地实施它。
技术栈
开发技术正在迅速发展并不断变化。 再过两年,Java是官方的Android开发语言。 每个人都用Java编写并使用了数据绑定。 在我看来,现在,我们正在向反应式编程和Kotlin迈进。 Java是一种很棒的语言,但是与Kotlin和AndroidX相比,它有一些缺陷。 Kotlin和AndroidX可以将数据绑定的使用减少到最低限度,即使不能完全排除它。 下面我将尝试解释我的观点。
科特林
我认为许多Android开发人员已经转向Kotlin,因此同意我的看法,即在2019年以Kotlin以外的任何其他语言编写新的Android项目就像在海里挣扎。 您当然可以争论,但是Flutter和Dart呢? C ++,C#甚至Cordova呢? 我将回答:选择永远是您的选择。
公元前480年 波斯国王谢尔克斯(Xerxes)下令其士兵出海航行,以惩处在暴风雨中摧毁部分军队的行为。五个世纪后,罗马皇帝卡利古拉(Caliguula)向波塞冬宣战。 口味问题。 十分之九的人认为Kotlin不错,但是十分糟糕。 这一切都取决于您,取决于您的愿望和抱负。
Kotlin是我的选择。 语言简单漂亮。 用它编写起来既轻松又愉快,最重要的是,不需要编写太多内容:数据类,对象,可选的setter和getter,简单的lambda表达式和扩展函数。 这只是该语言所提供内容的一小部分。 如果您尚未切换到Kotlin,请随时! 在练习部分中,我将演示该语言的一些优点(这不是广告)。
模型-视图-视图模型
当前,MVVM是Google推荐的应用程序体系结构。 在开发过程中,我们将坚持这种特定的模式,但是,由于MVVM建议使用数据绑定,因此我们不会完全遵守它,但是我们拒绝使用它。
MVVM的优点- 区分业务逻辑和UI。 在MVVM的正确实现中,除了来自AndroidX或Jetpack包的LiveData对象外,ViewModel中不应只有一个导入android。 正确使用会自动将所有UI工作留在片段和活动中。 那不是很好吗?
- 封装水平被泵送。 团队合作会更容易:现在,您可以在一个屏幕上一起工作,而不会互相干扰。 当一个开发人员在屏幕上工作时,另一个开发人员可以构建ViewModel,而第三个开发人员可以在存储库中编写查询。
- MVVM对编写单元测试有积极作用。 此项从上一项开始。 如果封装了所有类和方法以使其无法与UI一起使用,则可以轻松对其进行测试。
- 屏幕旋转的自然解决方案。 不管听起来有多奇怪,但此功能都会自动转换为MVVM(因为数据存储在ViewModel中)。 如果您检查了非常流行的应用程序(VK,Telegram,Sberbank-Online和Aviasales),事实证明其中只有一半无法旋转屏幕。 作为这些应用程序的用户,这使我有些惊讶和误解。
MVVM为什么很危险?- 内存泄漏。 如果您违反使用LiveData和Observer的法则,则会发生此危险错误。 我们将在练习部分详细检查此错误。
- 扩展ViewModel。 如果您尝试将所有业务逻辑都放入ViewModel中,则会得到不可读的代码。 解决这种情况的方法可能是将ViewModel分成一个层次结构,或使用Presenters。 那正是我所做的。
MVVM使用规则让我们从最大的错误开始,然后走到更少的错误:
- 请求正文不应在ViewModel中(仅在存储库中);
- LiveData对象是在ViewModel中定义的,它们不会将自己扔到存储库中,因为 使用Rx-Java(或协程)处理存储库中的请求;
- 所有处理功能都应移至第三方类和文件(“ Presenters”),以免使ViewModel混乱,也不会干扰本质。
实时数据
LiveData是可观察的数据持有者类。 与常规的可观察对象不同,LiveData具有生命周期感知功能,这意味着它尊重其他应用程序组件(例如活动,片段或服务)的生命周期。 这种意识确保LiveData仅更新处于活动生命周期状态的应用程序组件观察者。
资料来源: developer.android.com/topic/libraries/architecture/livedata从定义中可以得出一个简单的结论:LiveData是可靠的反应式编程工具。 我们将使用它来更新UI部件,而无需进行数据绑定。 为什么这样
XML文件的结构不允许简洁地分发从<data> ... </ data>获得的数据。 如果小文件一切都清楚了,那么大文件呢? 如何处理复杂的屏幕,多个包含和传递多个字段? 在各处使用模型? 获取硬性字段绑定? 而且,如果该字段应设置格式,则可以从Java包中调用方法? 这使代码毫无希望,而且完全是意大利面条。 完全不是MVVM所承诺的。
拒绝数据绑定将使对UI部件的更改透明。 所有更新将直接在观察器内部进行。 因为 由于Kolin代码简洁明了,我们不会因observer肿的观察者而遇到问题。 编写和维护代码将变得更加容易。 XML文件将仅用于设计-内部没有属性。
数据绑定是一个强大的工具。 它非常适合解决某些问题,并且与Java很好地协调,但是与Kotlin协调一致。对于Kotlin,在大多数情况下,数据绑定只是基本的要求。 数据绑定只会使代码复杂化,而不会带来任何竞争优势。
在Java中,您可以选择:要么使用数据绑定,要么编写很多难看的代码。 在Kotlin中,您可以绕过findViewById()及其属性直接访问视图元素。 例如:
出现一个逻辑问题:如果可以避免所有这些情况,为什么还要麻烦XML文件中的园艺模型,调用XML文件中的Java方法,重载XML部分的逻辑?
协程代替Thread()和Rx-Java
协程非常轻巧,易于使用。 它们非常适合大多数简单的异步任务:处理查询结果,更新UI等。
在不需要高性能的情况下,协程可以有效地替换Thread()和Rx-Java,因为 他们为速度的轻快付出了代价。 毫无疑问,Rx-Java具有更多功能,但是对于简单任务,并不需要其所有资产。
微软和其他
要使用Outlook服务,将使用Microsoft Graph API。 拥有适当的权限,您就可以通过它获得有关员工,会议室和活动(会议)的所有必要信息。 对于面部识别,将使用Microsoft Face API云服务。
展望未来,我会说,为了解决可扩展性问题,使用了Firebase云存储。 这将在下面讨论。
建筑学
可伸缩性问题
使系统完全或部分可伸缩非常困难。 如果该应用程序的第一个版本不可伸缩,而第二个版本应成为可伸缩版本,则这样做尤其困难。 应用程序v1一次将请求发送到所有房间。 每个平板电脑都会定期向服务器发送请求以更新所有数据。 同时,设备之间没有同步,因为 该项目根本没有自己的服务器。
当然,如果我们沿着相同的路径从N个平板电脑中的每个平板电脑发送N个请求,那么在某个时候,我们将推翻Microsoft Graph API或冻结系统。
使用客户端-服务器解决方案是合乎逻辑的,在该解决方案中,服务器轮询图形,累积数据并根据请求向平板电脑提供信息,但是在这里我们已被现实所满足。 该项目团队由2个人(Android开发人员和设计师)组成。 他们需要在7周的截止日期前完成,并且没有提供后端,因为 缩放是开发人员的要求。 但这并不意味着必须放弃这个主意吗?
在这种情况下,唯一正确的解决方案是使用云存储。 Firebase将替换服务器并充当缓冲区。 然后得出以下结果:
每个平板电脑仅从Microsoft Graph API轮询其地址,并且,如有必要,同步云存储中的数据,其他设备可以从中读取数据。此实现的优点是可以快速响应,因为 Firebase在实时模式下工作。 我们将减少发送给服务器的请求数量N次,这意味着设备将使用电池工作更长的时间。 从财务角度来看,该项目的价格并未上涨,因为 对于该项目,免费版本的Firebase具有足够的储备:1 GB的存储空间,每月10,000个授权和一次100个连接。 缺点可能包括依赖第三方框架,但Firebase激发了我们的信心,因为 它是Google维护和开发的稳定产品。
新系统的总体思路如下:N平板电脑和用于实时数据同步的云平台。 让我们开始设计应用程序本身。
存储库中的LiveData
看来我最近建立了良好形式的规则,并立即违反了其中一项规则。 与在ViewModel内部推荐使用LiveData不同,在此项目中,LiveData对象在存储库中初始化,并且所有存储库都声明为单例。 为什么这样
类似的解决方案与应用程序模式相关。 平板电脑的营业时间为上午8点至晚上8点。 在所有这些时间中,仅在其上启动了会议室助手。 结果,许多对象可以并且应该是长期存在的(这就是为什么所有存储库都设计为单例的原因)。
在工作过程中,会定期切换UI内容,这又需要创建和重新创建ViewModel对象。 事实证明,如果在ViewModel中使用LiveData,则将为每个创建的片段使用一组指定的LiveData对象创建自己的ViewModel。 如果在屏幕上同时显示2个类似的片段,并使用不同的ViewModel和一个公共的Base-ViewModel,则在初始化期间,将复制Base-ViewModel中的LiveData对象。 将来,这些重复项将占用内存空间,直到被“垃圾收集器”销毁为止。 因为 如果我们已经具有单例形式的存储库,并且希望将重新创建屏幕的成本降至最低,那么明智的做法是将LiveData对象传输到单例存储库中,从而简化ViewModel对象并加快应用程序的速度。
当然,这并不意味着您需要将所有LiveData从ViewModel传输到存储库,但是您应该更审慎地解决此问题并自觉地做出选择。 这种方法的缺点是长寿命对象的数量增加,因为 所有存储库都定义为单例,每个存储库都存储LiveData对象。 但在特定情况下,“会议室助手”不是负号,因为 该应用程序全天不间断运行,而无需将上下文切换到其他应用程序。
产生的架构

- 所有请求都在存储库中执行。 所有存储库(在“会议室助手”中有11个)都设计为单例。 它们按返回对象的类型划分,并隐藏在外墙后面。
- 业务逻辑驻留在ViewModel中。 由于使用了“ Presenters”,因此所有ViewModel的总大小(项目中为6)少于120行。
- 活动和片段仅涉及使用UIModel和从ViewModel返回的LiveData更改UI部分。
- 用于处理和生成数据的功能存储在“ presenter”中。 Kotlin积极使用的权限功能进行数据处理。
后台逻辑已移至Intent-Service:
- 事件更新服务。 负责在Firebase和Graph API中同步当前房间数据的服务。
- 用户识别服务。 仅在主平板电脑上运行。 负责向系统中添加新人员。 使用Active Directory中的列表检查已受过培训的人员的列表。 如果出现新人,该服务会将他们添加到Face API并重新训练神经网络。 操作完成后,将其关闭。 它在应用程序启动时启动。
- Online-Notification-Service会通知其他平板电脑该平板电脑正在运行,即 外部电池未耗尽。 它通过Firebase起作用。
从职责分配的角度来看,结果是一个相当灵活和正确的体系结构,它满足了现代发展的所有要求。 如果将来我们放弃Microsoft Graph API,Firebase或任何其他模块,则可以轻松地用新的模块替换它们,而不会干扰应用程序的其余部分。 广泛的“演示者”系统的存在使将所有数据处理功能扩展到核心之外成为可能。 结果,该体系结构变得十分清晰,这是一大优势。 过度生长的ViewModel问题已完全消失。
下面,我将给出一个已开发应用程序中常用捆绑包的示例。
练习 观看更新
根据会议室的状态,表盘显示以下情况之一:


此外,沿表盘的轮廓定位有临时的集会弧,中心向下计数,直到会议结束或下一次集会开始。 所有这些都是由我们开发的画布库完成的。 如果会议网格已更改,我们必须更新库中的数据。
由于LiveData是在存储库中宣布的,因此从它们开始是最合乎逻辑的。
储存库
FirebaseRoomRepository-一个类,负责在Firebase中发送和处理与Room模型有关的请求。
为了演示,侦听器的Firebase初始化代码略有简化(删除了重新连接功能)。 让我们看一下这里发生的事情:
- 仓库被设计为单例(在Kotlin中,用object替换class关键字就足够了);
- LiveData对象的初始化;
- ValueEventListener声明为变量,以避免在重新连接的情况下重新创建匿名类(请记住,我通过在断开连接的情况下删除重新连接来简化初始化);
- ValueEventListener的初始化(如果Firebase中的数据发生更改,则侦听器将立即执行并更新LiveData对象中的数据);
- 更新LiveData对象。
函数本身将移至单独的FirebaseRoomRepositoryPresenter文件,并装饰为扩展函数。
fun MutableLiveData<List<Room>>.updateOtherRooms(rooms: MutableList<Room>) { this.postValue(rooms.filter { !it.isOwnRoom() }) }
FirebaseRoomRepositoryPresenter的扩展功能示例另外,为了大致了解图片,我将列出Room对象。
- 数据类。 该修饰符自动生成并覆盖toString(),HashCode()和equal()方法。 不再需要自己重新定义它们。
- 房间对象的事件列表。 更新拨号库中的数据需要此列表。
所有存储库类都隐藏在Facade类的后面。
object Repository {
- 在上方,您可以看到所有使用过的存储库类和二级外观的列表。 这简化了对代码的一般理解,并演示了所有连接的存储库类的列表。
- 从FirebaseRoomRepository返回对LiveData对象的引用的方法的列表。 Kotlin的setter和getter是可选的,因此您不需要不必要地编写它们。
这样的组织使您可以在一个根存储库中轻松容纳20至30个请求。 如果您的应用程序有更多请求,则必须将根外观分为2个或更多。
视图模型
BaseViewModel是从其继承所有ViewModel的基本ViewModel。 它包括一个通用的currentRoom对象。
- 开放标记意味着您可以从该类继承。 在Kotlin中,默认情况下,所有类和方法都是最终的,即 类不能被继承,方法也不能重新定义。 这是为了防止意外的不兼容版本更改。 我举一个例子。
您正在开发该库的新版本。 在某个时候,出于某种原因,您决定重命名该类或更改某些方法的签名。 通过更改它,您不小心创建了版本不兼容。 糟糕...如果您可能知道该方法可能会被某人覆盖并且该类已被继承,则您可能会更准确,并且几乎不会打断自己。 为此,在Kotlin中,默认情况下,所有内容都声明为final,并且要取消,有一个“ open”修饰符。
- getCurrentRoom()方法从存储库返回到当前房间的LiveData对象的链接,该链接又从FirebaseRoomRepository中获取。 调用此方法时,Room对象将返回包含有关该房间的所有信息的事件,包括事件列表。
为了将数据从一种格式转换为另一种格式,我们将使用转换。 为此,创建一个
MainFragmentViewModel并从
BaseViewModel继承它。
MainFragmentViewModel是从BaseViewModel
派生的类。 此ViewModel仅在MainFragment中使用。
- 请注意缺少打开修饰符。 这意味着没有人从该类继承。
- currentRoomEvents-使用转换获得的对象。 当前房间的对象更改后,将立即执行转换并更新currentRoomEvents对象。
- MediatorLiveData。 结果与转换相同(显示为参考)。
第一个选项用于将数据从一种类型转换为另一种类型,这正是我们所需要的,第二个选项用于执行一些业务逻辑。 但是,不会发生数据转换。 请记住,ViewModel中的android import无效。 因此,我从此处启动其他请求或根据需要重新启动服务。
重要提示! 为了使转换或中介起作用,必须从片段或活动中订阅某人。 否则,将不会执行该代码,因为 没有人会期望得到结果(这些是观察者对象)。
主片段
将数据转换为结果的最后一步。 MainFragment在屏幕底部包括一个拨号库和一个View-Pager。
class MainFragment : BaseFragment() {
- MainFragmentViewModel的初始化。 lateinit修饰符表示我们承诺在使用该对象之前先对其进行初始化。 Kotlin试图保护程序员免受错误代码编写的影响,因此我们必须立即说出对象可以为null或放置lateinit。 在这种情况下,ViewModel必须由该对象初始化。
- 观察者收听者更新拨号。
- 初始化ViewModel。 请注意,这是在片段附加到活动之后立即发生的。
- 创建活动后,我们订阅对currentRoomEvents对象的更改。 请注意,我不订阅片段生命周期(此),而是订阅viewLifecycleOwner对象。 事实是,在支持库28.0.0和AndroidX 1.0.0中,当观察者“取消订阅”时检测到错误。 为解决此问题,发布了ViewLifecycleOwner形式的补丁,Google建议您订阅该补丁。 当片段死亡并且观察者继续工作时,这解决了僵尸观察者的问题。 如果您仍在使用它,请确保将其替换为viewLifecycleOwner。
因此,我想展示不使用数据绑定的MVVM和LiveData的简单性和美观性。 请注意,在该项目中,由于项目的特殊性,我将LiveData放入存储库中违反了普遍接受的规则。 但是,如果我们将它们移至ViewModel,则整体图片将保持不变。
作为蛋糕上的樱桃,我为您准备了一个带演示的简短视频(为保证安全起见,姓名被涂了,我表示歉意):

总结
作为第一个月的应用程序的结果,在交叉集会的显示中发现了一些错误(Outlook允许您同时创建多个事件,而我们的系统则不能)。 现在该系统已经工作了3个月。 没有观察到错误或故障。
PS感谢
jericho_code的评论。 在Kotlin中,您可以并且应该使用emptyList()在模型中初始化List <>,然后不会创建额外的对象。
var events: List<Event.Short> = emptyList()