Android 구성 요소화 아이디어 인용

머리말

이 기사는 특정 구현 텍스트가 아니지만 주로 구성 요소화, 하위 모듈 및 모듈 통신에 대한 아이디어를 논의합니다. 그리고 간단한 라우팅 컴포넌트를 연습하여 페이지 점프와 모듈 간 통신의 두 가지 기능을 완성합니다.컴포넌트화를 처음 접하는 학생들에게 간단하고 복잡한 아이디어를 제공하고 컴포넌트화에 대한 지각적 이해를 돕도록 하는 데 목적이 있습니다.

구체적인 실제 기사를 보려면 다음을 읽을 수 있습니다. Android Componentization Best Practices - Nuggets(juejin.cn)

컴포넌트화란 무엇인가

구성 요소화는 본질적으로 코드를 구성하는 방법이지만 모듈에서 더 큰 세분성을 갖습니다. 컴포넌트화를 사용하지 않기 전에 모든 코드를 앱 모듈에 배치하고, 앱 모듈 내부에서 하도급으로 비즈니스 코드와 기능 코드를 나눕니다.

아래 그림과 같이,

세 가지 패키지는 비즈니스에 따라 구분됩니다.

  1. 찾기 찾기
  2. 쇼핑몰

기능에 따라 두 가지 패키지로 나뉩니다.

  1. http 네트워크 요청
  2. 유틸리티 도구 클래스

무제.png위의 경우는 componentization을 사용하지 않는 경우로 모든 코드가 하나의 모듈에 작성되어 있으므로 문제는 없으나 프로젝트 코드를 더 많이 사용하거나 프로젝트에 매개변수가 많을수록 큰 문제가 발생합니다. 다음과 같은 문제:

  1. 코드는 모두 하나의 모듈로 작성되며, 하도급이 아무리 상세해도 하나의 패키지에 10개 이상의 클래스가 있을 수 있습니다.
  2. 하도급 형태는 코드에 대한 제약이 거의 없음
  3. 많은 개발자가 있고 코드는 하나의 모듈에 작성되며 각 개발자는 파일을 읽고 쓸 수 있는 권한이 있어 코드 커버리지 충돌이 발생하기 쉽습니다.

간단히 말해서 컴포넌트화는 많은 코드, 많은 사람, 또는 많은 코드와 사람을 처리하기 위해 코드를 구성하는 방식으로, 하나의 모듈에 있는 코드가 여러 모듈에 흩어져 있습니다. 코드가 모듈에 없기 때문에 A 모듈이 B 모듈의 클래스를 참조할 수 없어 통신 문제가 발생합니다.

따라서 구성 요소화가 직면한 주요 문제는 크게 두 가지입니다.

  1. 하위 모듈
  2. 모듈 간 통신

하위 모듈

모듈은 무엇을 기반으로 합니까? 네 가지 큰 캐릭터: 단일 책임. 솔직히, 항상 단일 책임을 염두에 두고 코드를 작성하면 훌륭한 코드가 됩니다.

거대한 단일 모듈을 분할하는 아이디어는 거대한 단일 클래스를 분할하는 것과 같습니다. 사실, 그것들은 다른 책임을 가진 코드를 하나의 클래스/모듈에 넣는 같은 이유로 나타납니다. 따라서 분할 코드는 코드 분류로 이해할 수 있습니다.

코드는 크게 비즈니스 코드와 기능 코드로 나눌 수 있습니다.

  1. 홈 페이지는 비즈니스에 속하고 네트워크 요청은 기능에 속합니다.
  2. 쇼핑몰은 비즈니스에 속하고 데이터베이스는 기능에 속합니다

所以当你的项目计划进行模块化的时候,只需要根据项目实际情况划分即可,没有什么硬性规定。

拆分代码有两个好处:

  1. 高复用性
    1. 体现功能模块上,比如:网络请求,轮播图,播放器,支付,分享等功能,任何一个业务都能可能会使用。实现为一个单独的模块,哪里使用哪里引入。
  2. 代码隔离
    1. 体现在业务模块,比如:A模块实现商城,B模块实现文章论坛,两者绝大部分代码没有任何关联,独立存在。
    2. 假设有一天项目不做文章论坛了,业务直接砍掉。那么删除B模块即可,A模块不受任何影响
    3. 但A,B模块都有可能有到分享功能,所以分享作为功能模块出现,不包含任何业务,只提供分享功能。

经过划分模块的代码结构如图

무제 1.png

三个业务模块:

  1. module_find 发现
  2. module_home 首页
  3. module_shop 商城

两个功能模块:

  1. library_network 网络请求
  2. library_utils 工具类

总之代码模块的拆分是单一职责的体现,大概可以分为业务模块和功能模块两种,功能模块的粒度更小可复用性更高,比如:轮播图,播放器任何位置都可能使用。

业务模块的粒度更大,可以引用多个功能模块解决问题,大多数代码都是依据业务逻辑编写,与功能模块相比 除非是同一个公司有相同业务否则复用性没那么高,

通信分析

上面主要讲了拆分模块的思路,现在聊聊模块间通信。特意设计模块间通信方案,主要是用于业务模块间通信。

业务模块和功能模块之间是单向通信,业务模块直接引用功能模块,调用功能模块暴露的方法即可。

但业务模块不同,业务模块之间存在互相通信的情况,核心情况有两种:

  1. 页面跳转
    1. A模块跳转B模块的页面,B模块跳转A模块的页面
  2. 数据通信
    1. A模块获取B模块的数据,比如调用B模块的网络请求。
    2. 可能会有点疑问,直接在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模块通信。区别在于路由是否强大,支持多少功能。

무제 2.png

粗糙的路由实现

页面跳转

实现路由组件最基本的功能页面跳转,讨论具体技术方案之前,先理清思路。

Android原生跳转页面只有一种办法 startActivity(intent(context,class)) ,调用startActivity方法有三要素

  1. context 提供的 startActivity方法
  2. 构造intent 需要 context
  3. 构造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对象 或者 包名+类名,在需要跳转的时候取出不就可以了么。

大概步骤:

  1. 创建路由组件
  2. 模块向路由注册页面信息
  3. 从路由取出页面信息实现跳转

创建路由组件,只有一个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")
        }
    }
}
复制代码

把握住核心思想快速实现简单的模块间页面跳转还是非常简单的,在来回顾一下

  1. 代码隔离后 实现页面跳转最关键的问题是,无法直接获取目标类的Class引用
  2. 项目只是在编码期隔离,打包之后仍然是在一个虚拟机内,可以通过 Class.forName(包名+类名) 获取引用
  3. key-value的形式存储 需要模块间跳转类的Class信息,在需要的时候取出

看没什么用的效果图

QQ图片20220603100940.gif

上述代码肯定是可用的,但是实际运行并不是仅仅引入一个路由组件就可以了,还有很多项目配置细节,可以参考 开头推荐的文章

模块间通信

接口下沉方案,在Route组件中定义通讯接口,使所有模块都可以引用,具体实现只在某个业务模块中,在初始化时注册实现类,运行时通过反射动态创建实现类对象。

添加模块通信后,Route组件有两种逻辑要处理,页面跳转和模块通信。 保存的Class可能是Activity 或 某个接口实现类,业务操作也不同。

서로 다른 두 비즈니스를 구분하려면 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()
        }
    }
}
复制代码

여러 라우팅 최적화 아이디어

  1. 매번 라우팅 정보를 수동으로 등록해야 하는 번거로움
    1. APT 기술 최적화와 결합된 컴파일 타임 주석 사용
    2. 주석 사용자 정의, 점프 페이지 및 통신 클래스에 주석 추가
    3. 컴파일 타임에 주석을 읽을 주석 프로세서 정의
    4. Annotation에 의해 수행되는 정보 처리 비즈니스 로직에 따라 컴포넌트 등록 기능을 완료하기 위한 Java 클래스 생성
  2. 라우팅 구성 요소의 모든 라우팅 정보는 초기화 중에 한 번에 메모리에 로드되며 최적화해야 합니다.
    1. 그룹으로 저장, 지연 로드 정보
    2. 라우팅 정보는 경로에 따라 그룹화되어 저장되며,
      1. RootManager는 내부 홀딩 맵을 저장하고 모든 그룹 정보를 저장합니다.
      2. 그룹은 모든 노드 정보를 저장하기 위해 내부적으로 목록을 보유합니다.
    3. 단체 이용 시,
      1. 리플렉션을 통해 그룹을 인스턴스화하여 현재 그룹 아래의 노드 정보를 메모리에 로드합니다.
  3. 객체를 얻을 때마다 반사를 통해 새로운 객체가 생성되어 메모리를 소모합니다.
    1. 캐싱 메커니즘을 추가하고 처음에만 새 개체를 생성합니다.
    2. LruCache캐시 를 사용할 수 있습니다

위의 라우팅 구성 요소의 예는 매우 간단합니다.이 "단순한"라우팅 구성 요소를 아무 참조 없이 처음부터 생각해 내기 어렵습니다. 어쨌든 나는 이런 창의적 능력이 없습니다.

아직 성숙하고 완벽한 라우팅 컴포넌트를 만드는 것은 매우 어렵지만 처음에는 기본 기능부터 차근차근 반복해야 합니다. 당신이 큰 사람이 아닌 한 사용자 정의 라우팅 구성 요소는 권장되지 않습니다.

권장 오픈 소스

오픈 소스 모범 사례: Android 플랫폼용 페이지 라우팅 프레임워크인 ARouter - Alibaba Cloud Developer Community (aliyun.com)

WMRouter: Meituan Takeaway Android 오픈 소스 라우팅 프레임워크 - Meituan 기술 팀(meituan.com)

CC: 점진적인 구성 요소화 변환을 지원하는 업계 최초의 Android 구성 요소화 프레임워크(qibilly.com)

Didi 오픈 소스 DRouter: 효율적인 Android 라우팅 프레임워크 - Nuggets(juejin.cn)

추천

출처juejin.im/post/7105576036720443405