一. 背景
最近好多我们使用的SDK, 都要求Kotlin 1.8. 比如说我想升级Firebase RemoteConfig到v21+, 这样我就能使用Firebase RemoteConfig的real-time REmote Config的功能. 但一旦我在gradle里升了版本, 那就编译失败了, 具体出错如下:
Module was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 1.9.0,
// (1.9.0是新版本Firebase在用的版本)
expected version is 1.7.1
// (1.7.1是我们工程正在用的版本)

这不是唯一要求我们升级kotlin的SDK, 我们还有多个SDK也要求我们升级Kotlin 1.7. 这样一来, 我们就到了不得不升级的地步了.
1. 困难
升级kotlin听起来没这么麻烦, 那从kotlin 1.7 升级到 kotlin 1.8+是个特例. 这是因为kotlin 1.8正式移除了kotlin-android-extensions(简称为KAE), 这个给我们带了很多麻烦.
2. 什么是KAE
KAE是多个组件的组合. 其中常见的有:
findViewById的替代
<TextView android:id="@+id/tvAccountName"
那你在kotlin中就可以直接用viewId来代替view, 就省了你声明view变量, 以及用findViewById来赋值的过程了. 这个能力就来自于KAE
import kotlinx.android.synthetic.main.activity_main
tvAccountName.text = "hello world"
Parcelable的简写
以前写Parcelable好麻烦, 要写如何封装, 又要写如何解封, 并且顺序上还不能错. 所以KAE中还有一个便利的工具, 我们只要一个annotation就搞定了:
@Parcelize
data class Account(....)
这样一来, 再也不用写CREATOR, writeToParcel()等重复代码了, 就解放了我们程序员的双手.
3. 困局的破解之道
对于Parcelize
那其实就是去app模块的build.gradle里, 修改一下:
// 从这个:
apply plugin: "kotlin-android-extensions"
// 改为这个:
apply plugin "kotlin-parcelize"
我修改后的结果, 左侧为修改前, 右侧为修改后:

对于viewID
那没办法, 只好使用官方推荐的ViewBinding了.
开启ViewBinding很容易, 只要去app模块的build.gradle里开启:
android {
...
buildFeatures {
viewBinding true
}
}
[小技巧]
经过我实操, KAE可以与ViewBinding同时存在, 所以我就让二者共存, 每次提交PR就提交一些文件, 这样免得整个"remove KAE"的PR显得太大, 而无法review.
到了最后, 完全没有KAE代码了, 再去删除KAE的plugin:

二. ViewBinding的各种使用案例
这一块若是已经熟悉ViewBinding的同学可以跳过了, 不太熟练的同学可以看看如何在各种场景上使用ViewBinding.
Activity与Fragment
// Activity
class LauncherActivity : BaseActivity() {
private lateinit var vb: ActivityLauncherBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
vb = ActivityLauncherBinding.inflate(layoutInflater)
setContentView(vb.root)
vb.tvName = "hello"
Fragment则要复杂些. 因为Fragment可能被其它fragment给replace掉, 但Fragment不会被销毁(只是fragment的view被干掉了). 所以这时我们要注意清理掉vb, 免得内存泄露.
// Fragment
class CommonErrorFragment : BaseFragment() {
private var vb: FragmentCommonErrorBinding? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
vb = FragmentCommonErrorBinding.inflate(inflater)
return vb?.root
}
override fun onDestroyView() {
super.onDestroyView()
vb = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
vb!!.tvName.text = "hello"
自定义View
class UserCircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
private lateinit var vb: UserCircleViewBinding
init {
vb = UserProfileCircleViewBinding.inflate(LayoutInflater.from(context), this, true)
vb.btnAction.setOnClickListener {...}
}
RecyclerView的ViewHolder
因为ViewHolder的构造函数得有一个view来填充itemView, 所以我们一开始就得已经初始化好了ViewBinding. 说人话, 就是一般是Adapter中声明好了ViewBinding, 然后把view传给ViewHolder
public class BonusAdapter extends RecyclerView.Adapter<BonusViewHolder>{
@NonNull @Override
public BonusVH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
ItemBonusVhBinding binding = ItemBonusBinding.inflate(inflater, parent, false); // R.layout.item_bonus
return new BonusViewHolder(binding);
}
当然, 若你有多个itemType, 那你就可以有多个ViewBinding, 以及多个ViewHolder了.
三. ViewBinding的诸多坑
这第三部分就是主讲ViewBinding的诸多坑了. 无论你是ViewBinding新手, 还是也要和我一样从KAE转到ViewBinding来, 这些避坑经验应该都能帮到你.
坑1: ViewID的引用可能变化了
当你的layout xml长这样时:
<com.xx.MyTextView
android:id="@+id/tv_info" />
那在KAE时代里, 我们就可以直接用 tv_info_name.text = "hello"
但在ViewBinding时代, 这变了. 是的 , ViewBinding有一个隐藏的规则, 就是它会把snake case (如: tv_info_name) 转化为 camel case (如 : tvInfoName)
所以在转KAE为ViewBinding后, 正确的使用方法就成了:
vb.tvInfoName.text = "hello"
(备注: 我的ActivityMainBinding, FragmentMainBinding之类的变量, 我都声明成vb (ViewBinding的缩写). Android官方一般写成binding. 但我觉得binding名字太长, 不方便书写. 所以用vb就方便得多)
坑2: 不这么智能的Android Studio
我发现Android Studio (简称为AS)对ViewBinding的支持还是有些缺陷的.
以前我们修改一个layout xml的名字, AS会自动把用到这个layout xml的Activity也同样进行更改, 这样代码就不会编译失败.
但是在ViewBinding时代, 当我们修改一个layout xml名字时, 我发现Activity中的ViewBinding类型不会变化, 直接提示出错.
比如我把一个activity_main.xml改成了activity_home.xml, 结果AS就报错了:

解决方法自然是要我自己手动改. 但这明显是AS对这一块的支持还不够
坑3. <merge>引发的问题
若是你的一个自定义View, 它是这样写的:
class SomeView ... : FrameLayout(...) {
init {
LayoutInflater.from(context).inflate(R.layout.view_user_profile, this, true)
...
}
}
其实就是说去inflate那个layout/view_user_profile.xml文件, 然后再把这个文件的根view给加到SomeView里来, 相当于是:
val layoutRoot = inflate(R.layout.view_use_profile)
this.add(layoutRoot);
这个转成ViewBinding也容易, 我们一般使用下面的代码就行:
init {
vb = UserProfileViewBinding.inflate(LayoutInflater.from(context), this, true)
}
但是, 你可能会发现, 一些view是可行的. 但一些view, 会提示你说: 找不到inflate(inflater, parent, attachToParent) 方法!
这是什么原因呢?
<merge>的坑
仍以上面的SomeView为例, 若它的layout/view_user_profile.xml文件是这样的:
<ConstraintLayout >
<TextView .../>
<Button .../>
那没问题, 你的ViewBinding会有三参的inflate方法, 即会有inflate(inflater, parent, attachToParent) 方法.
但是, 要是layout/view_user_profile.xml文件是这样的:
<merge tools:parentTag="ConstraintLayout">
<TextView .../>
<Button .../>
那你就会"惊喜"地发现, 那个自动生成的ViewUserProfileBinding类, 竟然只有两参的inflate方法, 没有三参的inflater方法. 即:
// 有这方法
fun inflate(inflater, parent)
// 但没有下面的这个方法:
fun inflate(inflater, parent, attachToParent)
那这时我们要怎么办?
解决之法
其实去看下生成的ViewBinding源码就能很快知道解决之法了.
AS生成的ViewBinding源码在app模块的build目录里, 更具体地来说是在: $project/app/build/data_binding_base_class_source_out/$flavor/out/$package目录里:

它的二参inflate方法是:

而第65行的inflate方法是这样的:

所以只要我们的parent(也就是root参数不为空), 它其实就相当于inflate(layoutResId, parent, true)了.
总结下, 就是初始化一个ViewBinding类时:
-
普通的view可以用:
vb = UserProfileViewBinding.inflate(LayoutInflater.from(context), this, true) -
root是
<merge>的view可以用 :vb = AnotherViewBinding.inflate(LayoutInflater.from(context), this)
坑4. <include>的问题
当我的Activity的layout用了include:
// activity_main.xml里有:
<include layout="@layout/view_loading" ... />
// view_loading.xml里有:
<ProgressBar android:id="@+id/pbLoading" ... />
-
那在KAE时代, 我们可以直接用:
pbLoding.isVisible = false -
但在ViewBinding时代, 我们使用
vb.pbLoading.isVisible = false会报错, 说找不到pbLoading这样一个成员
解决之道
只要给<include>加一个id, 就可以用vb的链式调用来调用到被include的layou文件中的view了:
// activity_main.xml里有:
<include android:id="@+id/myLoading"
layout="@layout/view_loading" .../>
// view_loading.xml里有:
<ProgressBar android:id="@+id/pbLoading" .../>
// kotlin里就要吧用:
vb.myLoading.pbLoading.isVisible = false //pbLoading存在于另一xml里了
为何加个id就解决了
仍是去build目录里看源码,

从这就能看出来, 当给<include>加了id后, 这个id就会变成另一个ViewBinding类, 这样就解释了上面的做法为什么行得通了
坑5: 双layout的view
现在我有一个view, 要能适用grid mode与list mode.

这样一来, 这个View的构造函数就会成这样:
// List mode时
LayoutInflater.from(context).inflate(R.layout.card_list, this@MyCardView, true)
// Grid mode时
LayoutInflater.from(context).inflate(R.layout.card_grid, this@MyCardView, true)
那这种复杂的View, 能用ViewBinding吗?
: 我尝试了下, 能用. 就是要初始化两个ViewBinding, 分别对应上面两个不同的layout xml.
但总的来说, 使用起来有点太麻烦, 得用:
if(listMode) vb1.tvName.text = "hello"
else vb2.tvName.text = "hello"
经过实际操作后, 我发现这种多layout的view, 并不适合用ViewBinding. 还是老老实实用findViewById吧, 这样使用view时就方便得多:
val tvName = this.findViewById<TextView>(R.id.tv_name)
tvName.text = "hello"
四. 升级Kotlin的过程
4.1 升级kotlin
我现在是把kotlin 1.7.21升级到最新的kotlin 1.9.24, 想一步到位, 免得以后再升. 升级一下kotlin version是简单, 只要写成:
plugins {
id 'com.android.application' version '8.1.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.24' apply false
-
第一个plugin是AGP (Android-Gradle-Plugin)
-
第二个plugin就是Kotlin了. 我把版本从v1.7.21升级到了v1.9.24
4.2 加入ViewBinding
这个简单:
android {
...
buildFeatures {
viewBinding true
}
}
4.3 删除KAE
上面的代码经过编译后会报错:
以前其实也有这个提示, 不过级别是warning, 现在直接红色的error了. 不解决就不能编译的那种. 所以我们要把KAE(kotlin-android-extensions)插件给删除掉. 然后把KAE的使用全转为ViewBinding.
parcelize插件
备注: 若是还用了KAE中的parcelize功能, 那就还得再加上kotlin-parcelize插件

更新三方库
以前 Kotlin 1.7 + target33的情形下, 我们的引用里有:
// core-ktx 1.9.0与Android 13 (API 33)更兼容; 版本再高如1.10.0就编译出错, 因为它需要kotlin 1.8版本
implementation 'androidx.core:core-ktx:1.9.0'
//targetSDK = 33后, 可以从1.4.1升到最新的1.6.1
implementation 'androidx.appcompat:appcompat:1.6.1'
// targetSDK = 33后, 从1.6.0可升到最新的1.9.0. // 再也没这个编译错误了: "Can't determine type for tag '<macro name="m3_comp_assist_chip_container_shape">?attr/shapeAppearanceCornerSmall</macro>'"
implementation 'com.google.android.material:material:1.9.0'
// targetSDK = 33后, 版本不能是最新的1.7.1与1.6.0, 不然会因为kotlin-stdlib要使用1.8而编译不能
implementation "androidx.activity:activity-ktx:1.5.0"
implementation "androidx.fragment:fragment-ktx:1.5.0"
// targetSDK = 33后, 不能高到2.6去, 不然会因为kotlin-stdlib要使用1.8而编译不能
def lifecycle_version = "2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
现在kotlin 1.9 + target34 可以全部升级下这些jetpack库了:
//Jetpack and Support
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'com.google.android.material:material:1.12.0'
implementation "androidx.activity:activity-ktx:1.9.0"
implementation "androidx.fragment:fragment-ktx:1.7.1"
def lifecycle_version = "2.8.1"
....
解决其它Kotlin升级过程中的问题
升完后build就有多个类发生错误了. 不过仔细看一下, 就能发现其实各个类的报错类似, 主要就是下面红框标的两种错误

出错就是这种:

错误说明是:
Type argument is not within its bounds.
Expected: Any
Found: T
其实说白了, 这个错误就是以前<T>不加限制时就是表示T可以是任意对象.
但现在Kotlin 1.9里不是了, 你得明确指明这个T就是个Any的类型才可以.
所以正确的修复方法就是:

五. 总结
其实本文就是被迫升级kotlin 1.7到kotlin1.9过程中的诸多坑与解决方法. 包括泛型的修复, 包括删除KAE, 包括改用ViewBinding. 要是你也有如此需求, 希望本文对你有用.