如何从一个申请两个申请。 Tinkoff少年体验

嗨,我叫Andrey,我正在为Android平台开发Tinkoff和Tinkoff Junior应用程序。 我想谈一谈我们如何从一个代码库中收集两个类似的应用程序。


— , ̆ 14 . , (, ), , , (, ).



在项目开始时,我们考虑了各种实施方案,并做出了许多决定。 显而易见,这两个应用程序(Tinkoff和Tinkoff Junior)将在公共代码中占很大一部分。 我们不想从旧的应用程序中派生出来,然后复制错误修复程序和新的常用功能。 为了一次处理两个应用程序,我们考虑了三个选项:Gradle Flavors,Git子模块,Gradle模块。


摇篮口味


我们的许多开发人员已经尝试使用调味剂,而且我们可以将多维调味剂与现有调味剂一起使用。
但是,风味有一个致命的缺陷。 Android Studio仅将代码视为活动风味的代码-即位于主文件夹和风味文件夹中的代码。 代码的其余部分与注释一起被视为文本。 这对某些工作室工具施加了限制:代码使用情况搜索,重构等。


Git子模块


实现我们想法的另一个选择是使用githa的子模块:将通用代码转移到一个单独的存储库,并将其作为子模块连接到两个具有特定应用程序代码的存储库。


这种方法增加了使用项目源代码的复杂性。 同样,在更改公共模块的API时,开发人员仍然必须使用所有三个存储库进行编辑。


多模块架构


最后的选择是切换到多模块架构。 这种方法没有其他两个缺点。 但是,向多模块体系结构的过渡需要耗时的重构。


当我们开始研究Tinkoff Junior时,我们有两个模块:一个用于描述如何与服务器一起工作的小型API模块,以及一个用于处理大部分项目代码的大型整体应用程序模块。


画图画图
结果,我们希望获得两个应用程序模块: 成人初级以及一些通用的核心模块。 我们确定了两种选择:


  • 将通用代码放入通用公共模块中。 这种方法“更正确”,但是需要更多时间。 我们估计代码重用量约为80%。
    画图
  • 将应用程序模块转换为库,然后将此库连接到瘦的成人初级模块。 此选项速度更快,但是它将为Tinkoff Junior带来永远不会执行的代码。
    画图

我们有足够的时间,我们决定根据第一个选项( 通用模块)开始开发,条件是在重构时间不足时切换到快速选项。
最后,这发生了:我们将项目的一部分转移到通用模块,然后将剩余的应用程序模块转换为库。 结果,现在我们具有以下项目结构:


画图

我们具有具有功能的模块,这使我们能够区分“成人”,“普通”或“儿童”代码。 但是, 应用程序模块仍然足够大,现在大约有一半的项目存储在此处。


将应用程序转换为库


该文档包含有关将应用程序转换为库的简单说明 。 它包含四个简单点,似乎应该没有困难:


  1. 打开模块build.gradle文件
  2. 从模块配置中删除applicationId
  3. 在文件的开头,将apply plugin: 'com.android.application'替换为apply plugin: 'com.android.application' apply plugin: 'com.android.library'
  4. 保存更改并在Android Studio中同步项目 (“ 文件”>“使用Gradle文件同步项目”

但是,转换花费了几天的时间,结果差异如下所示:


  • 183个文件已更改
  • 1601次插入(+)
  • 1920次删除(-)

怎么了?

首先,在库中,资源标识符不是常量 。 在库中,如在应用程序中一样,将生成带有资源标识符列表的R.java文件。 并且在库中,标识符值不是恒定的。 Java不允许您打开非恒定值,并且所有开关都必须替换为if-else。


 // Application int id = view.getId(); switch(id) { case R.id.button1: action1(); break; case R.id.button2: action2(); break; } // Library int id = view.getId(); if (id == R.id.button1) { action1(); } else if (id == R.id.button2) { action2(); } 

接下来,我们遇到了包装冲突。
假设您有一个具有package = com.example的库,并且带有package = com.example.app的应用程序取决于此库。 然后,将在库中分别生成com.example.R类,并在应用程序中分别生成com.example.app.R 。 现在,让我们在应用程序中创建com.example.MainActivity活动 ,我们将在其中尝试访问R类。 如果没有显式导入,将使用库的R类,其中不指定应用程序资源,而仅指定库资源。 但是,Android Studio不会突出显示该错误,并且当您尝试从代码切换到资源时,一切都会好起来的。


匕首


我们使用Dagger作为依赖项注入的框架。
在每个包含活动,片段和服务的模块中,我们都有描述这些实体的注入方法的常用接口。 在应用程序模块( Adultjunor )中, 匕首组件接口从这些接口继承。 在模块中,我们将组件带到该模块所需的接口。


多重绑定


通过使用多绑定,大大简化了我们项目的开发。
在一个通用模块中,我们定义一个接口。 在每个应用程序模块( Adultjunior )中,我们描述此接口的实现。 使用@Binds批注,我们@Binds匕首,每次使用它而不是接口时,都必须为儿童或成人应用程序注入其特定的实现。 我们还经常收集接口实现(集合或映射)的集合,并且这些实现在不同的应用程序模块中描述。


香精


为了不同的目的,我们收集了几个应用程序选项。 基本模块中描述的风味也必须在从属模块中描述。 另外,要使Android Studio正常运行,必须在所有项目模块中选择兼容的装配选项。


结论


在短时间内,我们实现了一个新的应用程序。 现在,我们将新功能交付到两个应用程序中,只需编写一次。


同时,我们花了一些时间进行重构,同时减少了技术负担,并切换到了多模块架构。 在此过程中,我们遇到了来自我们成功管理的Android SDK和Android Studio的限制。

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


All Articles