• AndroidBaseFrameMVVM 框架


    AndroidBaseFrameMVVM 是一个Android工程框架,所使用技术栈为:组件化、Kotlin、MVVM、Jetpack、Repository、Kotlin-Coroutine-Flow,本框架既是一个可以开箱即用的工程框架基础层,也是一个很好的学习资源,文档下面会对框架中所使用的一些核心技术进行阐述。该框架作为个人技术积累的产物,会一直更新维护,如果有技术方面的谈论或者框架中的错误点,可以在 GitHub 上提 Issues,我会及时进行回应。希望这个框架项目能给大家带来帮助。
    项目地址:AndroidBaseFrameMVVM

    Demo

    ​ 以鸿洋大神的玩安卓开放Api做的一款极简风格的玩安卓,仓库地址:WanAndroidMVVM,该项目正在开发中,由于工作较忙需要一段时间才能完成。如果想看旧版本的demo,可以直接打开demo分支看。

    框架图示

    谷歌 Android 团队 Jetpack 视图模型:

    模块

    • app:

    app壳 工程,是依赖所有组件的壳,该模块不应该包含任何代码,它只作为一个空壳存在,由于项目中使用了EventBusAPT技术,需要索引到各业务组件的对应的APT生成类,所以在 app壳 内有这一部分的代码。

    • buildSrc:

    这是一个特殊的文件夹,负责项目的构建,里面存放着一些项目构建时用到的东西,比如项目配置,依赖。这里面还是存放 Gradle 插件的地方,一些自定义的 Gradle 的插件都需要放在此处。

    • lib_base:

    项目的基础公共模块,存放着各种基类封装、对远程库的依赖、以及工具类、三方库封装,该组件是和项目业务无关的,和项目业务相关的公共部分需要放在 lib_common 中。

    • lib_common:

    项目的业务公共模块,这里面存放着项目里各个业务组件的公共部分,还有一些项目特定需要的一些文件等,该组件是和项目业务有关系的。

    组件化相关

    组件初始化

    为了更好的代码隔离与解耦,在特定组件内使用的SDK及三方库,应该只在该组件内依赖,不应该让该组件的特定SDK及三方库的API暴露给其他不需要用的组件。有一个问题就出现了,SDK及三方库常常需要手动去初始化,而且一般都需要在项目一启动(即 Application 中)初始化,但是一个项目肯定只能有一个自定义的 Application,该项目中的自定义 Application 在 lib_base 模块中,并且也是在 lib_base 模块中的清单文件中声明的,那其他组件该如何初始化呢?带着这个问题我们一起来深入研究下。

    常见的组件初始化解决方案:

    在我的了解范围内,目前有两种最为常见的解决方案:

    • 面向接口编程 + 反射扫描实现类:

    该方案是基于接口编程,自定义 Application 去实现一个自定义的接口(interface),这个接口中定一些和 Application 生命周期相对应的抽象方法及其他自定义的抽象方法,每个组件去编写一个实现类,该实现类就类似于一个假的自定义 Application,然后在真正的自定义 Application 中去通过反射去动态查找当前运行时环境中所有该接口的实现类,并且去进行实例化,然后将这些实现类收集到一个集合中,在 Application 的对应声明周期方法中去逐一调用对应方法,以实现各实现类能够和 Application 生命周期相同步,并且持有 Application 的引用及 context 上下文对象,这样我们就可以在组件内模拟 Application 的生命周期并初始化SDK和三方库。使用反射还需要做一些异常的处理。该方案是我见过的最常见的方案,在一些商业项目中也见到过。

    • 面向接口编程 + meta-data + 反射:

    该方案的后半部分也是和第一种方法一样,通过接口编程实现 Application 的生命周期同步,其实这一步是避免不了的,在我的方案中,后半部分也是这样实现的。不同的是前半部分,也就是如何找到接口的实现类,该方案使用的是 AndroidManifest 的 meta-data 标签,通过每个组件内的 AndroidManifest 内去声明一个 meta-data 标签,包含该组件实现类的信息,然后在 Application 中去找到这些配置信息,然后通过反射去创建这些实现类的实例,再将它们收集到一个集合中,剩下的操作基本相同了。该方案和第一种方案一样都需要处理很多的异常。这种方案我在一些开源项目中见到过,个人认为过于繁琐,还要处理很多的异常。

    本项目中所使用的方案:

    • 面向接口编程 + Java的SPI机制(ServiceLoader)+AutoService:

    先来认识下 Java 的 SPI 机制:面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。JavaSPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似 IOC 的思想,将装配的控制权移到了程序之外。这段话也是我复制的别人的,听起来很懵逼,大致意思就是我们可以通过 SPI 机制将实现类暴露出去。关于如何使用 SPI,这里不在陈述,总之是我们在各组件内通过 SPI 去将实现类暴露出去,在 Application 中我们通过 Java 提供的 SPI API 去获取这些暴露的服务,这样我们就拿到了这些类的实例,剩下的步骤就和上面的方案一样了,通过一个集合遍历实现类调用其相应的方法完成初始化的工作。由于使用 SPI 需要在每个模块创建对应的文件配置,这比较麻烦,所以我们使用 Google 的 AutoService 库来帮助我们自动创建这些配置文件,使用方式也非常的简单,就是在实现类添加一个 AutoService 注解。本框架中的核心类是这几个:lib_base-LoadModuleProxylib_base-ApplicationLifecycle。这种方案是我请教的一个米哈游的大佬,这位大佬告诉我在组件化中组件的初始化可以使用 ServiceLoader 来做,于是我就去研究了下,最后发现这种方案还不错,比前面提到的两种方案都要简单、安全。

    资源命名冲突

    在组件化方案中,资源命名冲突是一个比较严重的问题,由于在打包时会进行资源的合并,如果两个模块中有两个相同名字的文件,那么最后只会保留一份,如果不知道这个问题的小伙伴,在遇到这个问题时肯定是一脸懵逼的状态。问题既然已经出现,那我们就要去解决,解决办法就是每个组件都用固定的命名前缀,这样就不会出现两个相同的文件的现象了,我们可以在 build.gradle 配置文件中去配置前缀限定,如果不按该前缀进行命名,AS 就会进行警告提示,配置如下:

    source-groovy
    android {
        resourcePrefix "前缀_"
    }
    
    • 1
    • 2
    • 3
    • 4

    组件划分

    其实组件的划分一直是一个比较难的部分,这里其实也给不到一些非常适合的建议,看是看具体项目而定。

    关于基础组件通常要以独立可直接复用的角度出现,比如网络模块、二维码识别模块等。

    关于业务组件,业务组件一般可以进行单独调试,也就是可以作为 app 运行,这样才能发挥组件化的一大用处,当项目越来越大,业务组件越来越多时,编译耗时将会是一个非常棘手的问题,但是如果每个业务模块都可以进行的单独调试,那就大大减少了编译时间,同时,开发人员也不需要关注其他组件。

    关于公共模块,lib_base 放一些基础性代码,属于框架基础层,不应该和项目业务有牵扯,而和项目业务相关的公共部分则应该放在 lib_common 中,不要污染 lib_base

    依赖版本控制

    组件化常见的一个问题就是依赖版本,每个组件都有可能自己的依赖库,那我们应该统一管理各种依赖库及其版本,使项目所有使用的依赖都是同一个版本,而不是不同版本。本项目中使用 buildSrc 中的几个kt文件进行依赖版本统一性的管理,及其项目的一些配置。

    MVVM相关

    • MVVM 采用 Jetpack 组件 + Repository 设计模式 实现,所使用的 Jetpack 并不是很多,像 DataBindingPaging 3Room 等并没有使用,如果需要可以添加。采用架构模式目的就是为了解偶代码,对代码进行分层,各模块各司其职,所以既然使用了架构模式那就要遵守好规范。
    • Repository 仓库层负责数据的提供,ViewModel 无需关心数据的来源,Repository 内避免使用 LiveData,框架里使用了 Kotlin 协程的 Flow 进行处理请求或访问数据库,Repository 的函数会返回一个 Flow 给 ViewModel 的调用函数,Flow 上游负责提供数据,下游也就是 ViewModel 获取到数据使用 LiveData 进行存储,View 层订阅 LiveData,实现数据驱动视图
    • 三者的依赖都是单向依赖,View -> ViewModel -> Repository

    项目使用的三方库及其简单示例

    Kotlin协程

    关于 Kotlin 协程,是真的香,具体教程可以看我的一篇文章:

    Flow 类似于 RxJava,它也有一系列的操作符,资料:

    PermissionX

    PermissionX 是郭霖的一个权限申请框架 使用方式:

    PermissionX.init(this)
         .permissions("需要申请的权限")
         .request { allGranted, grantedList, deniedList -> }
    
    • 1
    • 2
    • 3

    EventBus APT

    事件总线这里选择的还是 EventBus,也有很多比较新的事件总线框架,还是选择了这个直接上手的 在框架内我对 EventBus 进行了基类封装,自动注册和解除注册,在需要注册的类上添加 @EventBusRegister 注解即可,无需关心内存泄漏及没及时解除注册的情况,基类里已经做了处理

    source-kotlin
    @EventBusRegister
    class MainActivity : AppCompatActivity() {}
    
    • 1
    • 2
    • 3

    很多使用 EventBus 的开发者其实都没有发现 APT 的功能,这是 EventBus3.0 的重大更新,使用 EventBus APT 可以在编译期生成订阅类,这样就可以避免使用低效率的反射,很多人不知道这个更新,用着3.0的版本,实际上却是2.0的效率。 项目中已经在各模块中开启了 EventBus APTEventBus 会在编译器对各模块生成订阅类,需要我们手动编写代码去注册这些订阅类:

    source-kotlin
    // 在APP壳的AppApplication类中
    EventBus
         .builder()
    	 .addIndex("各模块生成的订阅类的实例 类名在base_module.gradle脚本中进行了设置 比如 module_home 生成的订阅类就是 module_homeIndex")
         .installDefaultEventBus()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    屏幕适配 AndroidAutoSize

    屏幕适配使用的是 JessYan 大佬的 今日头条屏幕适配方案终极版

    GitHub: https://github.com/JessYanCoding/AndroidAutoSize

    使用方式:

    // 在清单文件中声明
    
         
        // 主单位使用dp 没设置副单位
            
                       
                    
    
    
    // 默认是以竖屏的宽度为基准进行适配
    // 如果是横屏项目要适配Pad(Pad适配尽量使用两套布局 因为手机和Pad屏幕宽比差距很大 无法完美适配)
    
                    
        // 以高度为基准进行适配 (还需要手动代码设置以高度为基准进行适配) 目前以高度适配比宽度为基准适配 效果要好
                       
                    
    
    
    // 在Application 中设置
    // 屏幕适配 AndroidAutoSize 以横屏高度为基准进行适配
    AutoSizeConfig.getInstance().isBaseOnWidth = false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    ARoute

    ARoute 是阿里巴巴的一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

    使用方式:

    // 1.在需要进行路由跳转的Activity或Fragment上添加 @Route 注解
    @Route(path = "/test/activity")
    public class YourActivity extend Activity {
        ...
    }
    
    // 2.发起路由跳转
    ARouter.getInstance()
        .build("目标路由地址")
        .navigation()
    
    // 3.携带参数跳转
    ARouter.getInstance()
    	.build("目标路由地址")
        .withLong("key1", 666L)
        .withString("key3", "888")
        .withObject("key4", new Test("Jack", "Rose"))
        .navigation()
    
    // 4.接收参数
    @Route(path = RouteUrl.MainActivity2)
    class MainActivity : AppCompatActivity() {
    
        // 通过name来映射URL中的不同参数
        @Autowired(name = "key")
        lateinit var name: String
    
    	override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(mBinding.root)
            // ARouter 依赖注入 ARouter会自动对字段进行赋值,无需主动获取
            ARouter.getInstance().inject(this)
        }
    }
    
    // 5.获取Fragment
    Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    ViewBinding

    通过视图绑定功能,可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。 在大多数情况下,视图绑定会替代 findViewById

    使用方式:

    按模块启用ViewBinding

    source-groovy
    // 模块下的build.gradle文件
    android {
    	// 开启ViewBinding
        // 高版本AS
        buildFeatures {
            viewBinding = true
        }
        // 低版本AS 最低3.6
        viewBinding {
            enabled = true
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Activity 中 ViewBinding 的使用

    source-kotlin
    // 之前设置视图的方法
    setContentView(R.layout.activity_main)
    
    // 使用ViewBinding后的方法
    val mBinding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(mBinding.root)
    
    // ActivityMainBinding类是根据布局自动生成的 如果没有请先build一下项目
    // ViewBinding会将控件id转换为小驼峰命名法,所以为了保持一致规范,在xml里声明id时也请使用小驼峰命名法
    // 比如你有一个id为mText的控件,可以这样使用
    mBinding.mText.text = "ViewBinding"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Fragment 中 ViewBinding 的使用

    // 原来的写法
    return inflater.inflate(R.layout.fragment_blank, container, false)
    
    // 使用ViewBinding的写法
    mBinding = FragmentBlankBinding.inflate(inflater)
    return mBinding.root
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    官方文档: https://developer.android.com/topic/libraries/view-binding

    ViewModel

    ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。

    使用方式:

    source-kotlin
    class MainViewModel : ViewModel(){}
    
    class MainActivity : AppCompatActivity() {
    		// 获取无参构造的ViewModel实例
        val mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    官方文档: https://developer.android.com/topic/libraries/architecture/viewmodel

    LiveData

    LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 ActivityFragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者

    LiveData 分为可变值的 MutableLiveData 和不可变值的 LiveData

    常用方法:

    source-kotlin
    fun test() {
            val liveData = MutableLiveData()
            // 设置更新数据源
            liveData.value = "LiveData"
            // 将任务发布到主线程以设置给定值
            liveData.postValue("LiveData")
            // 获取值
            val value = liveData.value
            // 观察数据源更改(第一个参数应是owner:LifecycleOwner 比如实现了LifecycleOwner接口的Activity)
            liveData.observe(this, {
                // 数据源更改后触发的逻辑
            })
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    官方文档: https://developer.android.com/topic/libraries/architecture/livedata

    Lifecycle

    Lifecycle 是一个类,用于存储有关组件(如 Activity 或 Fragment)的生命周期状态的信息,并允许其他对象观察此状态。LifecycleOwner 是单一方法接口,表示类具有 Lifecycle。它具有一种方法(即 getLifecycle()),该方法必须由类实现。实现 LifecycleObserver 的组件可与实现 LifecycleOwner 的组件无缝协同工作,因为所有者可以提供生命周期,而观察者可以注册以观察生命周期。

    官方文档: https://developer.android.com/topic/libraries/architecture/lifecycle

    Hilt

    Hilt 是 Android 的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。执行手动依赖项注入要求您手动构造每个类及其依赖项,并借助容器重复使用和管理依赖项。

    Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 **DI(依赖项注入)**的标准方法。Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。

    目前官方文档还没有更新正式版的,还是 alpha 版本的文档:使用 Hilt 实现依赖项注入

    Dagger 的 Hilt 文档目前是最新的:Dagger-Hilt

    Coil

    Coil 是一个 Android 图片加载库,通过 Kotlin 协程的方式加载图片。特点如下:

    • 更快: Coil 在性能上有很多优化,包括内存缓存和磁盘缓存,把缩略图存保存在内存中,循环利用 bitmap,自动暂停和取消图片网络请求等。
    • 更轻量级: Coil 只有2000个方法(前提是你的 APP 里面集成了 OkHttp 和 Coroutines),Coil 和 Picasso 的方法数差不多,相比 Glide 和 Fresco 要轻量很多。
    • 更容易使用: Coil 的 API 充分利用了 Kotlin 语言的新特性,简化和减少了很多样板代码。
    • 更流行: Coil 首选 Kotlin 语言开发并且使用包含 Coroutines, OkHttp, Okio 和 AndroidX Lifecycles 在内最流行的开源库。

    Coil 名字的由来:取 Coroutine Image Loader 首字母得来。

    官方文档: https://coil-kt.github.io/coil/

    在学习架构这方面知识点时,做了一些相关的学习笔记,将自己在每个环节遇到的问题及解决方法都有记录,相信分享这份学习相关笔记能够帮助到大家,有需要参考学习的直接 点击这里查看获取方式传送门直达!

  • 相关阅读:
    计算机视觉学习记录(六):深度学习基础
    实战使用Airtest与mitmdump爬取app数据
    Photoshop中图层的应用
    POJ3104Drying题解
    获取店铺的所有商品API 返回值说明
    python图片预标注
    企业级GitLab在Docker部署使用
    单例模式(Singleton)
    ChatGPT的前世今生:从概念到现实的AI之旅
    GAMES104-如何构建游戏世界
  • 原文地址:https://blog.csdn.net/m0_71263309/article/details/126232202