从0开始搭建一个APP:(4) BaseUI层MVVM和compose的接入

这个系列,拖了这么久,一个是搬砖时间不多,另外一个是想把compose 一起整进去,但是又不能上班整,之前创建项目的时候的gradle 版本过低,然后又是Java 1.8和Java 17的plugin 的函数冲突问题,反正各种打脑壳。拖延症拖到了今天,终于鼓起勇气来先解决这个问题了。TODO 列多了

骚年,你看起来很焦虑啊!

正文

环境配置

在这篇 从0开始搭建一个APP:(1)项目的配置笔记中,我们对gradle 版本和DRouter版本进行了限制。所以,开局没有开好,后续的问题非常多。

gradle 版本

如果尝试过compose 开发的同学,大概知道,compose用最新的AS 进行创建预览是没有任何的问题的,而默认创建的gradle 版本号就是8+,但是8+版本下,Java 版本得Java 17,这就导致了我们在buildSrc 里面使用的Java 版本得改,同时项目的Java 版本也得改,Drouter plugin 的版本 还得改。

至于8以下7.5以上的gradle 编译compose 是没有多大问题的,但是预览一直报错,当然预览报错产生的原因很多,大多数和plugin版本与maven 的版本导入有关。因为时间不是太多,就没有一一的去尝试验证问题的细节了,所以我这里就粗暴的将gradle 版本给升级到8+了。

  • 第一步修改 distributionUrl=services.gradle.org/distributio...
  • 第二步:修改Android的plugin版本号:8.1.1
  • 第三步:修改kotlin plugin 的版本号:1.9.10,我感觉可以不升级这个玩意,但是他报黄了,还是升级下吧。
  • 第四步:修改Drouter 的plugin的版本号:1.4.0
  • 第五步:修改buildSrc 中的JAVA 版本:JavaVersion.VERSION_17
  • 第六步:修改项目工程的JAVA版本:JavaVersion.VERSION_17,因为我单独提了一个common.gradle 所以字需要改一个地方。

执行上面几个步骤之后,逻辑意义上,原工程是可以编译成功的,如果JAVA home 不是17,那么需要在 project structure 中的 sdk location 板块中 更改 jdk location 相关配置,把选中改到17,当然最新几个版本的AS默认就是JAVA 17,这也就导致了一些老工程的plugin使用的是JAVA 8的项目需要手动切换这里到JAVA 1.8或者JAVA 10。

compose的接入

当我们把gradle 版本升级上去之后,最简单的方法便是,在现有的工程中,新增一个application,选中compose 即可。这个使用预览是可以成功的,但是编译是过不了的,他会提示一个JAVA 版本冲突了,这个问题是因为:

ini 复制代码
composeOptions {
    kotlinCompilerExtensionVersion = "1.5.3"
}

as默认创建的时候,指定的JAVA 版本就是 JAVA 1.8,对应的kotlinCompilerExtensionVersion是1.4.3。我们把它改到1.5.3即可,当然JAVA 版本也得改。

这个值,有点迷,因为我们在Google官网上看到的版本也是1.4.3,我as创建了好几个compose 项目也是1.4.3,但是我在学习compose的过程中,发现playAndroid 这个项目的JAVA 版本上17,可以预览也可以编译,所以,copy了一波。 2023年10月29日 更新,找到问题了:developer.android.google.cn/jetpack/and... 官网英文版本和中文版本的文档没有同步,需要看英文文档。

当compose 的application可以编译预览后,我们直接把plugin 换成 id("com.android.library") 即可,当然需要删除写代码和配置。

dependencies

这个讲道理,不应该单独分一个板块的,但是,上面说的无法预览,除了是plugin 有一定因素以外,最大的问题是 dependencies的导入的版本。

scss 复制代码
debugApi("androidx.compose.ui:ui-tooling")
debugApi("androidx.compose.ui:ui-test-manifest")

在8+ 中,这两个缺少了就会无法预览,所以我们设置成 debugApi。

scss 复制代码
api("androidx.activity:activity-compose:1.7.0")

这个则是compose activity的父类,也可以不导入,但是写起来就比较复杂了,因为compose是UI框架,最终都会走到activity上面去,而activity是设置view的,所以compose 必定会生成一个view,而这个库非常便捷了帮我们把一一系列操作整好了,所以直接用是最好的,当然了这个库还有一些起来能力。下面贴上完整的dependencies:

scss 复制代码
api("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
api("androidx.activity:activity-compose:1.7.0")
api(platform("androidx.compose:compose-bom:2023.03.00"))
api("androidx.compose.ui:ui")
api("androidx.compose.ui:ui-graphics")
api("androidx.compose.ui:ui-tooling-preview")
api("androidx.compose.material3:material3")
api("com.google.android.material:material:1.8.0")
implementation("androidx.appcompat:appcompat:1.6.1")
debugApi("androidx.compose.ui:ui-tooling")
debugApi("androidx.compose.ui:ui-test-manifest")

baseUI 层

因为是compose和原生VIew的混合开发,所以我们逃避不了的一个问题就是,一个APP 不可能是一个单界面application。当然,这么说是不绝对的,其实蛮多APP 是单activity多fragment的。

这种在安全方面还是蛮香的。个人见解,4大组件通常来说无法混淆类,在反编译的过程中,一个类名无法被混淆的APP,可以减少很多反编译的工作量,所以就出现了很多混淆的思路,比如大规模的单fragment界面,编译成其他冷门语言啥的。

所以,我们baseUI 包含两个部分,activity 与fragment,因为我们这里使用的是viewbinding和viewModel,通常来说,一个界面通常只需要强制绑定一个viewbinding和viewModel,结合反射的相关思想。我们可以直接通过反射实现这块对象的创建。

反射

反射viewbinding
kotlin 复制代码
private val parameterizedType = javaClass.genericSuperclass as ParameterizedType
private val vbClass = parameterizedType.actualTypeArguments[0] as Class<VB>
private val inflate = vbClass.getDeclaredMethod("inflate", LayoutInflater::class.java)
protected lateinit var binding: VB
activity
csharp 复制代码
binding = inflate.invoke(null, layoutInflater) as VB
fragment

可以看到,activity和fragment 获取inflate的入参是不一致的。所以需要单独处理。

kotlin 复制代码
private val inflate = vbClass.getDeclaredMethod(
    "inflate",
    LayoutInflater::class.java,
    ViewGroup::class.java,
    Boolean::class.java
)
csharp 复制代码
binding = inflate.invoke(null, layoutInflater, container, false) as VB
反射viewModel
kotlin 复制代码
    private val vmClass = parameterizedType.actualTypeArguments[1] as Class<VM>
    protected lateinit var viewModel: VM

通过by lazy 这种方式:

kotlin 复制代码
private val vm by lazy {
    getVM(vmClass,getVMFactory())
}

在onCreated 中调用函数也行。

kotlin 复制代码
fun getVM(vmClass: Class<VM>, vmFactory: ViewModelProvider.Factory): VM {
    return ViewModelProvider(this,vmFactory)[vmClass] as VM
}
fun getVMFactory(): ViewModelProvider.Factory{
    return defaultViewModelProviderFactory
}

至于为啥要自己写,而不是直接用 viewModels() 这种扩展函数,可能在我的角度里面,viewModel 的参数传递是可以通过构造函数传递的,所以直接自己整了,更重要的一点是:

kotlin 复制代码
public inline fun <reified VM : ViewModel> Fragment.viewModels()

这个玩意是内联函数,没有搞懂怎么设置activity或fragment类上的泛型。

compose

既然,compose是UI框架,所以这个里面,我们viewBinding 就没有,就只有一个viewModel 需要反射。而且compose是完全的面向函数编程。

compose base activity
kotlin 复制代码
abstract class ComposeUiActivity<VM: BaseViewModel>: ComponentActivity() {
    private val parameterizedType = javaClass.genericSuperclass as ParameterizedType
    private val vmClass = parameterizedType.actualTypeArguments[0] as Class<VM>
    protected lateinit var viewModel: VM
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        onDispense(savedInstanceState)
        setContent{
            DemoTheme { pageContent() }
        }
    }

    @Composable
    abstract fun pageContent()
    }

这个很单纯啊,定义了一个pageContent,同时使用了DemoTheme。DemoTheme 的作用主要是设置颜色字体和window 相关操作。目前直接用的创建自带的,到时候直接魔改即可。

compose fragment

通过阅读setContent{}的代码就可以发现,这个代码设置view的关键点在于:

scss 复制代码
ComposeView(this).apply {
    setParentCompositionContext(parent)
    setContent(content)
    setOwners()
    setContentView(this, DefaultActivityContentLayoutParams)

所以,我们写的所有composeUI 都会转换为为ComposeView 对象,通过外部设置ComposeView的作用域函数结合编译时技术,实现UI的处理。所以,我们在fragment中处理就很简单了。

kotlin 复制代码
override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View  {
   return ComposeView(requireContext()).apply {
        setContent { demoTheme {
            pageContent()
        }}
    }
}

    @Composable
    abstract fun pageContent()

至于其他的view层相关的,比如说dialog 相关的,既然都可以直接创建ComposeView 对象,那么把composeUI添加到原生View 中就不存在问题了,当然,手势冲突还是需要处理一下,这个后续再说。

结束

其实,整个baseUI 的东西还是比较简单的,主要是反射viewbinding 和viewModel,当然compose的踩坑也比较耗时,也有一些其他细节没有处理,这只是处理了大的UI框架,对大的方向进行了约束。

  • val fontMedium=FontFamily(Font(resId = R.font.xxxx, weight = FontWeight.Normal)) 这种设置自定义字体。
  • 设置状态栏为白底黑字。因为compose感觉抛弃了原来的那种通过style 设置主题的思路,所主题和style 得通过代码再整一遍。

composes是UI框架,所以导航我们还是采用DRouter这一套吧。

个人还是看好这种声明式UI的,起码写起来,很多Android 相关的知识可以直接复盘总结查漏补缺等。等生态建立好了,感觉又是搬砖,学习不到任何东西。

相关推荐
服装学院的IT男4 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms2064 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男4 小时前
【Android 源码分析】Activity生命周期之onStop-1
android
ChinaDragonDreamer6 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
网络研究院8 小时前
Android 安卓内存安全漏洞数量大幅下降的原因
android·安全·编程·安卓·内存·漏洞·技术
凉亭下9 小时前
android navigation 用法详细使用
android
小比卡丘11 小时前
C语言进阶版第17课—自定义类型:联合和枚举
android·java·c语言
前行的小黑炭12 小时前
一篇搞定Android 实现扫码支付:如何对接海外的第三方支付;项目中的真实经验分享;如何高效对接,高效开发
android
落落落sss14 小时前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.14 小时前
数据库语句优化
android·数据库·adb