这个系列,拖了这么久,一个是搬砖时间不多,另外一个是想把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 相关的知识可以直接复盘总结查漏补缺等。等生态建立好了,感觉又是搬砖,学习不到任何东西。