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

2,822 阅读7分钟

xwzz.jpg

前篇回顾

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

上篇文章,我们了解了:

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

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

Activity跳转

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

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

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

Activity跳转问题.png

我们知道,无论是哪个模块的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公共模块.png

/**
 * 路由管理类
 * 提供路由注册、查询等功能
 * */
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,别的模块再通过这些接口来调用对应的功能即可,具体的代码实现依旧保留业务线自身模块中,这样就可避免该问题,实现过程可以参考下方代码:

  • 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接口
    • 注册路由到RouterManager
object UserUtils {
    
    /**
     * 用户模块是否登录
     * 实际业务代码
     * */
    fun isLogin(): Boolean {
        // 校验Token之类的业务逻辑
        // ...
        // ...
        return true
    }
}
/**
 * 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