序文
この記事は特定の実装テキストではありませんが、主にコンポーネント化、サブモジュール、およびモジュール通信の概念について説明しています。また、簡単なルーティングコンポーネントを練習して、ページジャンプとモジュール間の通信の2つの機能を完了します。目的は、コンポーネント化に不慣れな学生が、シンプルで複雑でないアイデアを提供し、コンポーネント化を知覚的に理解できるようにすることです。クイックスタート
特定の実用的な記事については、これを読むことができます:Androidコンポーネント化のベストプラクティス-ナゲッツ(juejin.cn)
コンポーネント化とは
コンポーネント化は本質的にコードを整理する方法ですが、モジュール内でより細かくなります。コンポーネント化を使用しない前は、すべてのコードがアプリモジュールに配置され、ビジネスコードと機能コードはアプリモジュール内で外注によって分割されます。
以下に示すように、
3つのパッケージはビジネスに応じて分けられます:
- 検索検索
- 家
- ショップモール
機能に基づいて2つのパッケージに分けられます。
- httpネットワークリクエスト
- utilsツールクラス
上記はコンポーネント化を使用しない場合です。すべてのコードが1つのモジュールに記述されているため、問題はありませんが、使用するプロジェクトコードが増えるか、プロジェクトにパラメータが増えると、大きな問題になります。次のような問題:
- コードはすべてモジュールで記述されています。下請けがどれほど詳細であっても、必然的に1つのパッケージに10を超えるクラスまたはそれ以上のクラスが含まれることになります。
- 下請けの形式は、コードにほとんど制約がありません
- 多くの開発者がいて、コードは1つのモジュールで記述されており、各開発者はファイルを読み書きする権利を持っているため、コードカバレッジの競合が発生しやすくなります。
要するに、コンポーネント化とは、多くのコード、多くの人、または多くのコードと人を処理するためにコードを編成する方法です。1つのモジュール内のコードは複数のモジュールに分散しています。コードがモジュール内にないため、AモジュールがBモジュール内のクラスを参照できないように見え、通信の問題が発生します。
したがって、コンポーネント化が直面する主な問題は主に2つです。
- サブモジュール
- モジュール間の通信
サブモジュール
モジュールは何に基づいていますか?4つの大きなキャラクター:単一責任。正直なところ、常に単一責任を念頭に置いてコードを書くことは、優れたコードになります。
巨大な単一モジュールを分割するという考え方は、巨大な単一クラスを分割することと同じです。実際、それらは同じ理由で表示され、異なる責任を持つコードを1つのクラス/モジュールに入れます。したがって、コードの分割はコード分類として理解できます
コードは、次のようにビジネスコードと機能コードに大別できます。
- ホームページはビジネスに属し、ネットワークリクエストは機能に属します
- モールはビジネスに属し、データベースは機能に属します
所以当你的项目计划进行模块化的时候,只需要根据项目实际情况划分即可,没有什么硬性规定。
拆分代码有两个好处:
- 高复用性
- 体现功能模块上,比如:网络请求,轮播图,播放器,支付,分享等功能,任何一个业务都能可能会使用。实现为一个单独的模块,哪里使用哪里引入。
- 代码隔离
- 体现在业务模块,比如:A模块实现商城,B模块实现文章论坛,两者绝大部分代码没有任何关联,独立存在。
- 假设有一天项目不做文章论坛了,业务直接砍掉。那么删除B模块即可,A模块不受任何影响
- 但A,B模块都有可能有到分享功能,所以分享作为功能模块出现,不包含任何业务,只提供分享功能。
经过划分模块的代码结构如图
三个业务模块:
- module_find 发现
- module_home 首页
- module_shop 商城
两个功能模块:
- library_network 网络请求
- library_utils 工具类
总之代码模块的拆分是单一职责的体现,大概可以分为业务模块和功能模块两种,功能模块的粒度更小可复用性更高,比如:轮播图,播放器任何位置都可能使用。
业务模块的粒度更大,可以引用多个功能模块解决问题,大多数代码都是依据业务逻辑编写,与功能模块相比 除非是同一个公司有相同业务否则复用性没那么高,
通信分析
上面主要讲了拆分模块的思路,现在聊聊模块间通信。特意设计模块间通信方案,主要是用于业务模块间通信。
业务模块和功能模块之间是单向通信,业务模块直接引用功能模块,调用功能模块暴露的方法即可。
但业务模块不同,业务模块之间存在互相通信的情况,核心情况有两种:
- 页面跳转
- A模块跳转B模块的页面,B模块跳转A模块的页面
- 数据通信
- A模块获取B模块的数据,比如调用B模块的网络请求。
- 可能会有点疑问,直接在A模块写要调用的接口不就好了,为什么要费劲巴拉的进行模块间通信,可以是可以。组件化就是为了隔离,解耦,复用。如果A模块直接实现了要用的网络请求,还要组件化干嘛呢,出现类似情况都这么干,项目内就会出现很多重复代码,除了图方便 没有别好处
单一模块开发时所有的类都能直接访问,上述的问题简直不是问题,从MainActivity 跳转到 TestActivity ,可以直接获取TestActivity的class对象完成跳转
val intent = Intent(this@MainActivity,TestActivity::class.java)
startActivity(intent)
复制代码
但是分开多模块就是问题了,MainActivity 和 TestActivity 分别在A,B两个模块中,两个业务模块之间没有直接引用代码隔离,所以不能直接调用到想使用的类。
这种情况就需要一个中间人,帮助A,B模块通信。
(需求简单,实现简单)中间人好像邮局,两人住在同一个村甚至对门,想要唠嗑,送点东西,因为距离近走着就去了。如果两人相隔千里不能见面,想要唠嗑需要写信,标记地址交给邮局,让邮局转发。
(需求复杂,实现复杂)信件好保存一般不会损坏,运送比较方便。如果想要快点到,加钱用更快的运送工具。 如果想要送一块家乡的红烧肉,为了保鲜原汁原味,可能要加更多的钱用飞机+各种保险措施送过去
模块间通信也是类似,A,B模块通过中间人,也就是路由组件通信。页面跳转是最简单的通信需求实现简单,如果想要访问数据,获取对象应用等更复杂的需求,可能需要更加复杂的设计和其他技术手段才实现目标。
但总之A,B模块代码隔离之后不会无缘无故就实现了通信,一定会存在路由角色帮助A,B模块通信。区别在于路由是否强大,支持多少功能。
粗糙的路由实现
页面跳转
实现路由组件最基本的功能页面跳转,讨论具体技术方案之前,先理清思路。
Android原生跳转页面只有一种办法 startActivity(intent(context,class))
,调用startActivity方法有三要素
- context 提供的 startActivity方法
- 构造intent 需要 context
- 构造intent 需要 目标类的class对象
世面上所有的路由组件封装跳转页面功能,就算他封装出花来,也是基于AndroidSDK,无法脱离原生提供的方法。
所以我们现在需要想办法调用完整的startActivity(intent(context,class))
关键点在于,由于代码隔离,我们无法直接获取目标activity的class,直白点说无法 直接**“.”**出class。那么怎么可以在代码隔离的情况下拿到目标类的class呢
有个小技巧,先要说明一个事,模块A,模块B仅仅在编码的时候处于代码隔离的状态,但是打包之后它们还是一个应用,代码在一个虚拟机中。所以可以使用 Class.forName(包名+类名)
运行时获取class对象,完成跳转
val clazz = Class.forName("com.xxx.TestActivity")
val intent = Intent(this,clazz);
startActivity(intent)
复制代码
这种方式可以帮助我们实现页面跳转的逻辑,但是非常粗糙,总不能需要模块间页面跳转,就硬编码包名+类名 获取class,太麻烦了,太容易出错了,代码散落在程序各处。
但是这种粗糙的方式也为我们提供了一点思想火花
如果我们能通过一种方式收集到 有模块间跳转需求的页面class对象 或者 包名+类名,在需要跳转的时候取出不就可以了么。
大概步骤:
- 创建路由组件
- 模块向路由注册页面信息
- 从路由取出页面信息实现跳转
创建路由组件,只有一个Route类
object Route {
private val routeMap = ArrayMap<String, Class<*>>()
fun register(path: String, clazz: Class<*>) {
routeMap[path] = clazz
}
fun navigation(context: Context, path: String) {
val clazz = routeMap[path]
val intent = Intent(context, clazz)
context.startActivity(intent)
}
}
复制代码
其他组件在初始化时注册路由
Route.register("home/HomeActivity", HomeActivity::class.java)
复制代码
模块间跳转页面
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
val button: Button = findViewById(R.id.button)
button.setOnClickListener {
Route.navigation(this, "home/HomeActivity")
}
}
}
复制代码
把握住核心思想快速实现简单的模块间页面跳转还是非常简单的,在来回顾一下
- 代码隔离后 实现页面跳转最关键的问题是,无法直接获取目标类的Class引用
- 项目只是在编码期隔离,打包之后仍然是在一个虚拟机内,可以通过
Class.forName(包名+类名)
获取引用 - key-value的形式存储 需要模块间跳转类的Class信息,在需要的时候取出
看没什么用的效果图
上述代码肯定是可用的,但是实际运行并不是仅仅引入一个路由组件就可以了,还有很多项目配置细节,可以参考 开头推荐的文章
模块间通信
接口下沉方案,在Route组件中定义通讯接口,使所有模块都可以引用,具体实现只在某个业务模块中,在初始化时注册实现类,运行时通过反射动态创建实现类对象。
添加模块通信后,Route组件有两种逻辑要处理,页面跳转和模块通信。 保存的Class可能是Activity 或 某个接口实现类,业务操作也不同。
2つの異なるビジネスを区別するには、Routeコンポーネントに少し変更を加え、RouteEntityを追加してデータを保存し、次のようにRouteTypeルートタイプを使用して区別します。
object Route {
private val routeMap = ArrayMap<String, RouteEntity>()
/**
* 注册信息
*/
fun register(route: RouteEntity) {
routeMap[route.path] = route
}
/**
* 页面导航
*/
fun navigation(context: Context, path: String) {
val routeEntity = routeMap[path] ?: throw RuntimeException("path错误 找不到类")
val intent = Intent(context, routeEntity.clazz)
context.startActivity(intent)
}
/**
* 获取通信实例
*/
fun getService(path: String): Any {
val routeEntity = routeMap[path] ?: throw RuntimeException("path错误 找不到类")
return routeEntity.clazz.newInstance()
}
}
/**
* 保存路由信息
* @param path 路径 用于查找class
* @param type 类型 区分 页面跳转 和 通信
* @param clazz 类信息
*/
data class RouteEntity(val path: String,@RouteType val type:Int,val clazz: Class<*>)
/**
* 路由类型
*/
@IntDef(RouteType.ACTIVITY, RouteType.SERVICE)
annotation class RouteType() {
companion object {
const val ACTIVITY = 0
const val SERVICE = 1
}
}
复制代码
次のように使用します。
//在 Route组件中 定义接口
interface IShopService {
fun getPrice(): Int
}
//业务模块中实现接口
class ShopServiceImpl :IShopService {
override fun getPrice(): Int {
return 12
}
}
//模块初始化时注册
override fun create(context: Context) {
Route.register(RouteEntity("shop/ShopActivity",RouteType.ACTIVITY,ShopActivity::class.java))
Route.register(RouteEntity("shop/ShopService",RouteType.SERVICE,ShopServiceImpl::class.java))
}
//其他模块中使用
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity_home)
val btnGoShop = findViewById<Button>(R.id.btn_go_shop)
val btnGetPrice = findViewById<Button>(R.id.btn_get_price)
btnGoShop.setOnClickListener {
//跳转页面
Route.navigation(this, "shop/ShopActivity")
}
btnGetPrice.setOnClickListener {
//模块通信
val shopService: IShopService = Route.getService("shop/ShopService") as IShopService
Toast.makeText(this, "价格:${shopService.getPrice()}", Toast.LENGTH_SHORT).show()
}
}
}
复制代码
いくつかのルーティング最適化のアイデア
- 毎回手動でルーティング情報を登録するのは面倒です
- APTテクノロジーの最適化と組み合わせたコンパイル時の注釈を使用する
- 注釈をカスタマイズし、ジャンプしたページやコミュニケーションクラスに注釈を追加します
- コンパイル時に注釈を読み取るための注釈プロセッサを定義する
- アノテーションによって運ばれる情報処理ビジネスロジックに従って、コンポーネント登録機能を完了するためのJavaクラスを生成します
- ルーティングコンポーネントのすべてのルーティング情報は、初期化中に一度にメモリにロードされます。これは最適化する必要があります
- グループで保存、遅延読み込み情報
- ルーティング情報は、パスに従ってグループ化されて保存されます。
- RootManagerは内部保持マップを保存し、すべてのグループ情報を保存します
- グループは、すべてのノード情報を保存するために内部的にリストを保持します
- グループを使用する場合、
- リフレクションを介してグループをインスタンス化することにより、現在のグループの下のノード情報をメモリにロードします
- オブジェクトが取得されるたびに、リフレクションによって新しいオブジェクトが作成され、メモリを消費します
- キャッシュメカニズムを追加し、初めて新しいオブジェクトを作成するだけです
LruCache
キャッシュを使用できます
上記のルーティングコンポーネントの例は非常に単純です。難しいのは、この「単純な」ルーティングコンポーネントを参照なしで最初から作成することです。とにかく、私にはこの創造的な能力はありません。
成熟した完璧なルーティングコンポーネントを作成することはまだ非常に困難ですが、最初は基本的な機能から少しずつ繰り返す必要があります。あなたが大物でない限り、カスタムルーティングコンポーネントはお勧めしません
推奨されるオープンソース
WMRouter:Meituan Takeaway Androidオープンソースルーティングフレームワーク-Meituanテクニカルチーム(meituan.com)
CC:プログレッシブコンポーネント化変換をサポートする業界初のAndroidコンポーネント化フレームワーク(qibilly.com)