Android组件化开发实战

前言

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

目录

  • 前言
  • 组件化概述
  • 项目地址
  • 前置知识
  • 组件通信
  • 补充说明
  • 引入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单兵作战。如果你觉得这篇文章对你有帮助,欢迎点赞关注,你的支持便是对我最大的鼓励。

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

推荐阅读更多精彩内容