Android组件化开发实战

作者: 辉涛 | 来源:发表于2021-05-26 21:44 被阅读0次

    前言

    本文只是我在开发过程中一步一步总结的实战经验,若有疑问,欢迎私信,留言讨论。你的支持是对我最大的鼓励。

    目录

    • 前言
    • 组件化概述
    • 项目地址
    • 前置知识
    • 组件通信
    • 补充说明
    • 引入kapt插件
    • 引入AutoService服务
    • 搭建组件化框架
    • 01Activity中调用Fragment
    • 02 自定义注解ITabPage
    • 03使用ITabPage注解
    • 04获取对象及注解
    • 05Fragment切换
    • ①添加扩展函数witchContent()
    • ②重写底部BottomNavigationView Click事件
    • 06组件传递数据
    • ①新建IMainAppService接口
    • ②实现IMainAppService接口
    • ③使用AutoService完成跳转
    • ④取值
    • 总结

    组件化概述

    组件化不同于模块化,是可独立运行的module,module可以独立打包测试运行,业务解耦,解决65536问题,可拆卸,便于协同开发而不受其它业务模块影响。如果还不了解组件化是什么,我为你贴心准备了以下连接,请前往下方连接观看了解
    https://www.zhihu.com/question/29735633/answer/90873592

    项目地址

    https://gitee.com/zhuhuitao/componentization

    前置知识

    这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~
    自定义plugin+including构建工程
    注解(Annotation)详细讲解
    AutoService官方示例及API文档
    组件化之AutoService使用与源码解析

    组件通信

    当我们在实现组件化工程的首要问题是如何解决组件通信问题,在业界比较熟悉的如阿里的ARouter,不得不说使用ARouter还是稍微繁琐。在后来的项目开发过程中我更多的采用了谷歌为我们提供的Autoservice,来完成组件通信,如果不了解AutoService的使用及原理,可以前往我为你准备的前置知识查看了解。

    补充说明

    由于采用自定义plugin+including方式构建项目,所以与以往的传统方式在编写gradle有稍许差别,这里我还是想说明工程是在什么地方引入kapt插件以及AutoService服务,避免不了解的小伙伴,不知如何下手,以免浪费小伙伴的宝贵时间。
    在version module中有VersionConfigPlugin文件,这里对工程做了许多配置项,包括引入kapt插件和AutoService服务,如下所示:

    引入kapt插件

       ///kotlin插件
        private fun Project.configCommonPlugin() {
            //添加android  或者 kotlin插件
            plugins.apply("kotlin-android")
            //已启用 使用viewBinding
            //plugins.apply("kotlin-android-extensions")
            plugins.apply("kotlin-kapt")
        }
    

    引入AutoService服务

        ///library module 公共依赖
        private fun Project.configLibraryDependencies() {
            dependencies.apply {
                add(api, fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
                add(implementation, GradlePlugins.kotlinStdlib)
                //如果module 的 name 不是common则引入common模块,不加次判断,活报错,提示我们common
                //module自己引入自己,相当于递归了
                if (name != "common") {
                    add(api, project(":common"))
                }
                //引入autoService服务,主要为后续的组件化开发做准备,Kotlin中要使用kapt方式
                add(kapt, ThirdParty.autoService)
                add(compileOnly, ThirdParty.autoService)
                configTestDependencies()
            }
        }
    

    搭建组件化框架

    在开发中,我们应用主页通常都为一个Activity+多个Fragment完成,这时我们的MainActivity和多个Fragment并不在一个module中,我们该怎样在MainActivity中使用其他module中的Fragment呢。又或者我们的module A 怎么 把数据传递给module B呢,在接下来的篇幅中,我将详细介绍如何使用AutoService结合自定义注解完成这些功能。

    01Activity中调用Fragment

    正如我们前面提到,module A 如果不和module B做任何关联依赖,A中是无法使用B中的任何方法或者资源的,我们通过使用AutoService,然后通过ServiceLoader获取具体的对象,最后通过获取的对象获取我们自定义的注解,完成其它module Fragment调用。

    02 自定义注解ITabPage

    自定义注解ITabPage,在前面我们提到可以使用AutoService+ServiceLoader获取具体的对象,有了具体的对象,我们就可以获取类的任何属性信息,于是我们通过自定义注解,以注解的方式传入我们的TabName,以及IconName,然后在Activity中获取注解信息,然后运用,自定义注解详细代码如下:

    /**
     *author  : huitao
     *e-mail  : pig.huitao@gmail.com
     *date    : 2021/5/25 11:26
     *desc    : tabName 为底部菜单Fragment对应的名字,IconName为相对应的图标
     *version :
     */
    @Retention(AnnotationRetention.RUNTIME)
    @Target(AnnotationTarget.CLASS)
    annotation class ITabPage(val tabName: String, val iconName: String)
    
    

    03使用ITabPage注解

    假如我们现在有一个首页module,在module中有HomeFragment,代码如下:

    /**
     *author  : huitao
     *e-mail  : pig.huitao@gmail.com
     *date    : 2021/5/25 11:26
     *desc    : 首页
     *version :
     */
    @AutoService(Fragment::class)
    @ITabPage(tabName = "首页", iconName = "tab_home")
    class HomeFragment : BaseFragment<FragmentHomeBinding>() {
        override fun getDataBindingConfig(): DataBindingConfig {
            return DataBindingConfig(R.layout.fragment_home)
        }
    
    }
    

    关于其他module:project,square等当中的Fragment与HomeFragment差不多,就不一一贴出详细代码,这里为了后续工作,我将Fragment做了抽取,建了一个BaseFragmeng,其中我们使用了ViewDataBinding,详细代码如下:

    /**
     *author  : huitao
     *e-mail  : pig.huitao@gmail.com
     *date    : 2021/5/24 15:42
     *desc    : Fragment基类
     *version :
     */
    abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
        private lateinit var mBinding: T
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            val datingConfig = getDataBindingConfig()
            val array = datingConfig.bindingParams
            val bind: T =
                DataBindingUtil.inflate(inflater, datingConfig.layout, container, false)
            bind.lifecycleOwner = this
            for (i in 0 until array.size()) {
                bind.setVariable(array.keyAt(i), array.valueAt(i))
            }
            mBinding = bind
            return mBinding.root
        }
    
        fun getBind(): ViewDataBinding {
            return mBinding
        }
    
        abstract fun getDataBindingConfig(): DataBindingConfig
    }
    

    04获取对象及注解

    当我们的自定义注解ITabPage等一切基础设施完成之后,我们通过ServiceLoader获取我们具体的Fragment对象,代码如下:

         private fun loadFragments() {
            //获取所有使用@Autoservice注解对象 (也就是Fragment)
            val iterator = ServiceLoader.load(Fragment::class.java)
            //获取布局中的BottomNavigationView
            val menu = getBinding().bottomMenu.menu
            //遍历被我们使用@Autoservice注解的Fragment
            iterator.forEach { fragment ->
                //获取我们自定义的注解
                val property = fragment.javaClass.getAnnotation(ITabPage::class.java) ?: return@forEach
                //加入tab名称
                val menuItem = menu.add(property.tabName)
                //设置图标
                menuItem.setIcon(resources.getIdentifier(property.iconName, "mipmap", packageName))
                //将fragment加入集合中,便于后续操作fragment
                mFragmentList.add(fragment)
                //这里为了给提交事务时加上tag标签
                mFragmentTags.add(property.tabName)
                //将指针移动下一个位置
                iterator.iterator()
            }
            //如果有fragment对象,则将当前的第一个fragment加入到布局中fragment container中并提交
            if (mFragmentList.isNotEmpty()) {
                mCurrentFragment = mFragmentList.first()
                supportFragmentManager.beginTransaction()
                    .add(R.id.fragment_container, mCurrentFragment, mFragmentTags.first()).commit()
            }
        }
       
    

    以上的几步操作完成了获取Fragmeng对象,以及fragemnt中我们自定义的注解属性,并提交了我们当前的第一个fragment。

    05Fragment切换

    ①添加扩展函数witchContent()

    为了便于做首页Fragmen之间的来回t的切换,我们为AppCompatActivity添加扩展函数AppCompatActivity.witchContent,这样我们可以在项目的很多地方使用此扩展函数,详细代码如下:

    fun AppCompatActivity.witchContent(from: Fragment, to: Fragment, tag: String, id: Int) {
        val ft = supportFragmentManager.beginTransaction()
        when (to.isAdded) {
            true -> {
                ft.hide(from).show(to).commit()
            }
            else -> {
                ft.hide(from).add(id, to, tag).commit()
            }
        }
    }
    
    ②重写底部BottomNavigationView Click事件

    这一部分相对简单,直接贴代码:

        private fun initBottomEvent() {
            val menu = getBinding().bottomMenu
            menu.setOnNavigationItemSelectedListener { item ->
                val size: Int = menu.menu.size()
                var index = 0
                //找到我们选中的position
                for (i in 0 until size) {
                    if (menu.menu.getItem(i) === item) {
                        index = i
                        break
                    }
                }
                witchContent(
                    mCurrentFragment,
                    mFragmentList[index],
                    mFragmentTags[index],
                    R.id.fragment_container
                )
                mCurrentFragment = mFragmentList[index]
                //注意这里一定要返回true
                true
            }
        }
    
    

    以上基本上对Activity中获取Fragment做了详细描述,也详细讲解了如何自定义注解,使用对象获取注解属性。

    06组件传递数据

    在前面的Fragment使用中我们通过自定义注解,然后通过具体对象获取注解属性的值,如果设计少量的数据传递,无疑是完全没有问题,如果数据大量,或者组件间要传递的数据涉及到非常多的地方,这种方式无疑是痛苦的,接下来我们使用AutoService完成另外一种组件通信。

    ①新建IMainAppService接口

    由于我们的组件模块不止一个,这里我通过一个组件来举例说明,其它组件原理一样,我们在Common层新建接口IMainAppService,代码也非常简单如下:

    /**
     *author  : huitao
     *e-mail  : pig.huitao@gmail.com
     *date    : 2021/5/26 13:48
     *desc    :
     *version :
     */
    interface IMainAppService {
        //跳转到首页,并将电话号码传递到首页
        fun <T : BaseActivity<*>> startActivity(c: T, mobile: String)
    }
    
    ②实现IMainAppService接口

    在组件module app中实现IMainAppService接口,在module中新建MainAppService,代码如下:

    /**
     *author  : huitao
     *e-mail  : pig.huitao@gmail.com
     *date    : 2021/5/26 13:49
     *desc    :
     *version :
     */
    @AutoService(IMainAppService::class)
    class MainAppService : IMainAppService {
        override fun <T : BaseActivity<*>> startActivity(c: T, mobile: String) {
            val bundle = Bundle()
            bundle.putString("mobile", mobile)
            c.startActivity(MainActivity::class.java, bundle)
        }
    }
    
    ③使用AutoService完成跳转
        inner class ClickProxy {
            fun login() {
                when {
                    mViewModel.mobile.get().isNullOrEmpty() -> showToast(
                        getString(R.string.enter_mobile),
                        this@LoginActivity
                    )
                    mViewModel.password.get().isNullOrEmpty() -> showToast(
                        getString(R.string.enter_password),
                        this@LoginActivity
                    )
    
                    else -> {
                        loadService(IMainAppService::class.java)?.startActivity(
                            this@LoginActivity,
                            "${mViewModel.mobile.get()}\n${mViewModel.password.get()}"
                        )
                    }
                }
    
            }
        }
    }
    
    ④取值
      override fun initData() {
            intent.extras.let {
                val mobile = it?.getString("mobile")
                showToast(mobile!!,this)
           }
    
        }
    

    以下为MainActivity 和 LoginActivity详细代码:

    class MainActivity : BaseActivity<ActivityMainBinding>() {
        private val mFragmentList = ArrayList<Fragment>()
        private val mFragmentTags = ArrayList<String>()
        private lateinit var mCurrentFragment: Fragment
        override fun initViews() {
            loadFragments()
            initBottomEvent()
        }
    
        private fun initBottomEvent() {
            val menu = getBinding().bottomMenu
            menu.setOnNavigationItemSelectedListener { item ->
                val size: Int = menu.menu.size()
                var index = 0
                //找到我们选中的position
                for (i in 0 until size) {
                    if (menu.menu.getItem(i) === item) {
                        index = i
                        break
                    }
                }
                witchContent(
                    mCurrentFragment,
                    mFragmentList[index],
                    mFragmentTags[index],
                    R.id.fragment_container
                )
                mCurrentFragment = mFragmentList[index]
                //注意这里一定要返回true
                true
            }
        }
    
        private fun loadFragments() {
            //获取所有使用@Autoservice注解对象 (也就是Fragment)
            val iterator = ServiceLoader.load(Fragment::class.java)
            //获取布局中的BottomNavigationView
            val menu = getBinding().bottomMenu.menu
            //遍历被我们使用@Autoservice注解的Fragment
            iterator.forEach { fragment ->
                //获取我们自定义的注解
                val property = fragment.javaClass.getAnnotation(ITabPage::class.java) ?: return@forEach
                //加入tab名称
                val menuItem = menu.add(property.tabName)
                //设置图标
                menuItem.setIcon(resources.getIdentifier(property.iconName, "mipmap", packageName))
                //将fragment加入集合中,便于后续操作fragment
                mFragmentList.add(fragment)
                //这里为了给提交事务时加上tag标签
                mFragmentTags.add(property.tabName)
                //将指针移动下一个位置
                iterator.iterator()
            }
            //如果有fragment对象,则将当前的第一个fragment加入到布局中fragment container中并提交
            if (mFragmentList.isNotEmpty()) {
                mCurrentFragment = mFragmentList.first()
                supportFragmentManager.beginTransaction()
                    .add(R.id.fragment_container, mCurrentFragment, mFragmentTags.first()).commit()
            }
        }
    
        override fun initData() {
            intent.extras.let {
                val mobile = it?.getString("mobile")
                showToast(mobile!!,this)
           }
    
        }
    
        override fun getDataBindingConfig(): DataBindingConfig {
            return DataBindingConfig(R.layout.activity_main)
        }
    
    }
    
    class LoginActivity : BaseActivity<ActivityLoginBinding>() {
        private val mViewModel = LoginViewModel()
        override fun initViews() {
        }
    
        override fun initData() {
        }
    
        override fun getDataBindingConfig(): DataBindingConfig {
            return DataBindingConfig(R.layout.activity_login).addBindParams(BR.clickProxy, ClickProxy())
                .addBindParams(BR.vm, mViewModel)
        }
    
    
        inner class ClickProxy {
            fun login() {
                when {
                    mViewModel.mobile.get().isNullOrEmpty() -> showToast(
                        getString(R.string.enter_mobile),
                        this@LoginActivity
                    )
                    mViewModel.password.get().isNullOrEmpty() -> showToast(
                        getString(R.string.enter_password),
                        this@LoginActivity
                    )
    
                    else -> {
                        loadService(IMainAppService::class.java)?.startActivity(
                            this@LoginActivity,
                            "${mViewModel.mobile.get()}\n${mViewModel.password.get()}"
                        )
                    }
                }
    
            }
        }
    }
    

    总结

    使用谷歌为我们提供的AutoService无疑是轻量级的,易于理解的,并不需要我们做过多复杂配置以及些过多代码,就可以轻而易举完成我们组件开发框架的搭建,在整个架构的搭建中,我们最能感受到的是业务被具体划分,也不在是一个module单兵作战。如果你觉得这篇文章对你有帮助,欢迎点赞关注,你的支持便是对我最大的鼓励。

    相关文章

      网友评论

        本文标题:Android组件化开发实战

        本文链接:https://www.haomeiwen.com/subject/woslsltx.html