从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 相关的知识可以直接复盘总结查漏补缺等。等生态建立好了,感觉又是搬砖,学习不到任何东西。

相关推荐
太空漫步112 小时前
android社畜模拟器
android
海绵宝宝_5 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
凯文的内存6 小时前
android 定制mtp连接外设的设备名称
android·media·mtp·mtpserver
天若子7 小时前
Android今日头条的屏幕适配方案
android
林的快手8 小时前
伪类选择器
android·前端·css·chrome·ajax·html·json
望佑8 小时前
Tmp detached view should be removed from RecyclerView before it can be recycled
android
xvch11 小时前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
人民的石头14 小时前
Android系统开发 给system/app传包报错
android
yujunlong391915 小时前
android,flutter 混合开发,通信,传参
android·flutter·混合开发·enginegroup
rkmhr_sef15 小时前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb