Android组件化架构 —— 基础(二) - 组件间通讯

xwzz.jpg

前篇回顾

链接:Android组件化 —— 基础(一) - 组件化与集成化

上篇文章,我们了解了:

  • 组件化与集成化的区别;
  • 通过gradle自动转换组件环境和集成环境;
  • 解决AndroidManifest.xml共用问题。

本篇,我们将探讨组件化架构中组件间通讯是如何完成的。

Activity跳转

我们知道,正常的Activity跳转代码大致如下:

     val intent = Intent(this, UserMainActivity::class.java)
     startActivity(intent)

然而,在组件环境下,各模块相互独立,它们之间不存在任何依赖关系,导致模块之间Activity互不可见的,代码在编译期无法识别到对方的Activty,页面的跳转就成了问题。

Activity跳转问题

其实我们知道,最终无论是哪个模块的Activity最终都会打包到同一个apk中,在代码文件层面上讲,这些Class文件是相互可见的,这就需要我们绕点弯路将这些Activity提供给对方。设想一下,能不能找一个中间人,把需要跳转的Activity都交给它来管理?

我大致想到一个方案:

  • 1、创建一个公共模块lib_comm,该模块用于存放各模块间公共代码或者说基础功能代码,并且其它模块都依赖于该Library;
  • 2、在lib_comm中创建一个“路由”容器管理类,该类向外提供路由的注册、查询等功能;
  • 3、各模块将需要外部跳转的Activity,注册到路由容器中;
  • 4、跳转时,由路由容器去查询路由,完成跳转。

什么是路由?前面提到,如果要跨模块跳转Activity,我们需要将这些Activity的Class对象提供到对方模块,为了方便管理和调用,我们用一个简化的字符串来标识对应的Activity.class对象,例如:"A" -> AActivty.class,这种通过字符串查询到指定页面的方案,可以称之为路由

按照上面思路,我把相关实现代码贴在了下方:

  • 创建lib_comm公共模块,以及路由管理类RouterManager
lib_comm公共模块
/**
 * 路由管理类
 * 提供路由注册、查询等功能
 * */
object RouterManager {

    const val TAG = "RouterManager"

    // 存储路由的的容器
    private val mRouterMap = HashMap<String, Class<*>>()

    /**
     * 添加路由
     * @param path 路由路径
     * @param clazz 路由目标
     * */
    fun addRouter(path: String, clazz: Class<*>) {
        mRouterMap[path] = clazz
    }

    /**
     * 开启Activity
     * */
    fun startActivity(context: Context, path: String) {
        val clazz = mRouterMap[path]
        if (clazz == null) {
            val log  = "not found router by path !"
            Log.e(TAG, log)
            showToast(context , log)
            return
        }
        // 判断是否是Activity的子类
        if (Activity::class.java.isAssignableFrom(clazz)) {
            val intent = Intent(context, clazz)
            context.startActivity(intent)
        } else {
            val log = "router's not Activity !"
            Log.e(TAG, log)
            showToast(context , log)
        }
    }

    private fun showToast(context:Context , log: String) {
        Toast.makeText(context , log , Toast.LENGTH_SHORT).show()
    }

}
  • 各模块依赖lib_comm,并在startup中完成路由注册
    // 各模块build.gradle中添加依赖
    implementation project(":lib_comm")
// 以user模块为例,通过startup完成路由注册
class UserInitializer : Initializer<UserInit> {

    override fun create(context: Context): UserInit {
        UserInit.init(context)
        return UserInit
    }

    override fun dependencies(): MutableList<Class<out Initializer<*>>> {
        return mutableListOf()
    }
}
// user模块初始化入口
object UserInit {

    fun init(context: Context) {
        initRouter()
    }

    private fun initRouter() {
        // 注册路由
        RouterManager.addRouter("user/UserMainActivity", UserMainActivity::class.java)
    }
}
  • 最终各模块使用路由完成跳转
class AppMainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.btn_user).setOnClickListener {
            // 通过路由跳转
            RouterManager.startActivity(this, "user/UserMainActivity")
        }
    }
}

跳转演示

(诺GIF图加载失败,可点击此处查看)

Fragment获取

实现方案与Activity类似,将Fragment对应的路由存储到路由容器中,再暴露一个获取Fragment API即可。

// 存储到路由容器中
RouterManager.addRouter("user/UserFragment", UserFragment::class.java)
// 暴露获取Fragment的API
object RouterManager {

    ...

    /**
     * 获取Fragment
     * */
    fun getFragment(context: Context, path: String): Fragment? {
        val clazz = mRouterMap[path]
        if (clazz == null) {
            val log = "not found router by path !"
            Log.e(TAG, log)
            showToast(context, log)
            return null
        }
        // 判断是否是Fragment的子类
        if (Fragment::class.java.isAssignableFrom(clazz)) {
            return clazz.newInstance() as Fragment
        } else {
            val log = "router's not Fragment !"
            Log.e(TAG, log)
            showToast(context, log)
        }
        return null
    }
  ...
}
// 获取Fragment ,并使用
RouterManager.getFragment(this, "user/UserFragment")?.apply {
    val beginTransaction = supportFragmentManager.beginTransaction()
    beginTransaction.replace(R.id.fl_fragment, this)
    beginTransaction.commit()
}

获取Fragment

(诺GIF图加载失败,可点击此处查看)

跳转携带参数

页面跳转过程中需要携带的参数,可以通过Bundle对象进行传递,代码实现如下,此处就不做过多阐述:

object RouterManager {
/**
     * 开启Activity
     * */
    fun startActivity(context: Context, path: String, bundle: Bundle? = null) {
        val clazz = mRouterMap[path]
        if (clazz == null) {
            val log = "not found router by path !"
            Log.e(TAG, log)
            showToast(context, log)
            return
        }
        // 判断是否是Activity的子类
        if (Activity::class.java.isAssignableFrom(clazz)) {
            val intent = Intent(context, clazz)
            // 添加参数
            if (bundle != null) {
                intent.putExtras(bundle)
            }
            context.startActivity(intent)
        } else {
            val log = "router's not Activity !"
            Log.e(TAG, log)
            showToast(context, log)
        }
    }

    /**
     * 获取Fragment
     * */
    fun getFragment(context: Context, path: String, bundle: Bundle? = null): Fragment? {
        val clazz = mRouterMap[path]
        if (clazz == null) {
            val log = "not found router by path !"
            Log.e(TAG, log)
            showToast(context, log)
            return null
        }
        // 判断是否是Fragment的子类
        if (Fragment::class.java.isAssignableFrom(clazz)) {
            val fragment = clazz.newInstance() as Fragment
            //添加参数
            if (bundle != null) {
                fragment.arguments = bundle
            }
            return fragment
        } else {
            val log = "router's not Fragment !"
            Log.e(TAG, log)
            showToast(context, log)
        }
        return null
    }
}

跨模块功能调用

实际开发中,部分模块的功能可能需要提供给别的模块使用。
例如:user模块在用户登录后会保存用户登录状态,其他模块部分业务可能需要校验该状态才可继续操作。
显然,是否登录功能需要暴露给其它模块使用,如果直接将用户是否登录的业务代码(校验token等)丢到lib_comm中,虽然可以解决问题,但我们知道这样的代码实际是属于用户业务线的,我们定义lib_comm的初衷是希望它存放公共代码,并且业务线尽量少甚至不去修改这部分代码,一旦用户业务线的校验规则发生改变,那么业务线去修改lib_comm就不可避免。

仔细想想,我们只需将业务线需要暴露的功能,以接口的形式提供给lib_comm,别的模块再通过这些接口来访问对应的功能即可,具体功能的实现还是保留在各自业务线模块里,这样就可避免业务线代码入侵问题,详细实现可以参考下方代码:

  • user模块的用户是否登录业务代码
object UserUtils {
    
    /**
     * 用户模块是否登录
     * 实际业务代码
     * */
    fun isLogin(): Boolean {
        // 校验Token之类的业务逻辑
        // ...
        // ...
        return true
    }
}
  • lib_comm模块
    • 定义表示功能标记接口
    • 以及user模块提供的功能接口
    • 并在RouterManager中提供对外获取接口实例的方法
/**
 * 功能性路由标记接口
 * */
interface IService
/**
 * User模块对外提供的功能接口
 * */
interface IUserService : IService {

    /**
     * 是否登录
     */
    fun isLogin(): Boolean
}
object RouterManager {
    ...

    /**
     * 获取用户模块提供的服务
     * */
    fun getUserService(context: Context): IUserService? {
        val clazz = mRouterMap["user/UserService"]
        if (clazz == null) {
            val log = "not found service router by path !"
            Log.e(TAG, log)
            showToast(context, log)
            return null
        }
        // 判断是否是Service & IUserService
        if (IService::class.java.isAssignableFrom(clazz)
            && IUserService::class.java.isAssignableFrom(clazz)
        ) {
            return clazz.newInstance() as IUserService
        } else {
            val log = "router's not IUserService !"
            Log.e(TAG, log)
            showToast(context, log)
        }
        return null
    }

    ...
}
  • user模块实现IUserService接口,并注册路由
/**
 * user模块实现对外暴露的功能
 * */
class UserServiceImpl : IUserService {

    override fun isLogin(): Boolean {
        return UserUtils.isLogin()
    }
}
// 注册功能到路由中
RouterManager.addRouter("user/UserService", UserServiceImpl::class.java)
  • app模块调用user模块暴露的isLogin()功能
RouterManager.getUserService(this)?.apply {
    Toast.makeText(
      this@AppMainActivity,
      "登录状态:${this.isLogin()}",
      Toast.LENGTH_SHORT
      ).show()
}

跨模块功能调用

(诺GIF图加载失败,可点击此处查看)

关于lib_comm的修改问题

虽然通过上面的方案,我们将业务模块对外提供的功能解耦到了自身模块里,但不得不在lib_comm中对外提供对应的IService子接口,一旦子业务线需要对外提供新的功能,或者删除旧的功能,那么在lib_comm修改IService子接口就在所难免。
显然IService子接口会随着业务线的变动发生修改,我们只是做到了尽量少的修改lib_comm代码;在后续篇章中,我会提供一种方案来解决该问题,该问题先暂时保留下来。

小结

本篇就先到这里,我们主要了解了组件化架构中Activity的跳转Fragment的获取、以及跨模块功能调用等开发中常会遇到的场景案例,并尝试手写代码来解决这些问题。如果你一步步完成了这些功能,恭喜你,你已经对“路由”的具体实现有了基本的认识。

我们编写的路由处理框架还存在很多问题,例如:

  • 我们在startup中注册路由,这就会导致在App启动时会将所有模块的路由全部注册到内存中,然而部分路由在用户的实际使用中可能未被使用,这就导致额外内存开销;
  • 对于功能性的Service,每次都重新创建新的对象给调用者,这块也可以进行缓存优化;
  • ...

路由中可能涉及到的其它功能,我们也没做具体的实现,但只要通过本篇对路由核心实现有了清晰认识也收获足以。市面上路由已经有了很多成熟框架,例如美团的WMRouter阿里的ARouter等,如果项目对代码的自研要求不高,使用这些框架来实现路由无论是在性能上,还是效率上都再好不过。

下篇,以ARouter为例继续探讨路由那些事。

Android组件化架构 —— 基础(三) - ARouter

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • Android 架构系列:Android 架构一:Android 架构浅析Android 架构二:纵向横向结合构建...
    IT前沿技术分享阅读 1,802评论 2 16
  • 前言说明 以下内容均为 Android 组件化架构知识点的总结归纳、修正错误和完善扩展,非系统知识集,个人笔记,仅...
    Parallel_Lines阅读 6,682评论 11 84
  • 现在规模比较大的app都实现了组件化方案,来解耦和方便协作。带来的问题时模块之间的相互通信比较麻烦。 一般App组...
    SimpleFunc阅读 2,055评论 0 3
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,503评论 28 53
  • 人工智能是什么?什么是人工智能?人工智能是未来发展的必然趋势吗?以后人工智能技术真的能达到电影里机器人的智能水平吗...
    ZLLZ阅读 3,754评论 0 5