kotlin-android-extensions 插件将被正式移除,如何无缝迁移?

语言: CN / TW / HK

theme: smartblue highlight: a11y-dark


前言

kotlin-android-extensions 插件早在 2020 年就已经被宣布废弃了,并且将在 Kotlin 1.8 中被正式移除:Discontinuing Kotlin synthetics for views

如上图示,移除 kotlin-android-extensions 的代码已经被 Merge 了,因此如果我们需要升级到 Kotlin 1.8,则必须要移除 KAE

那么移除 kotlin-android-extensions 后,我们该如何迁移呢?

迁移方案

官方的迁移方案如上所示,官方建议我们老项目迁移到 ViewBinding,新项目直接迁移到 Jetpack Compose

对于新代码我们当然可以这么做,但是对于大量存量代码,我们该如何迁移?由于 KAE 简单易用的特性,它在项目中经常被大量使用,要迁移如此多的存量代码,并不是一个简单的工作

存量代码迁移方案

KAE 存量代码主要有如图3种迁移方式

最简单也最直接的当然就是直接手动修改,这种方式的问题在于要迁移的代码数量庞大,迁移成本高。同时手动迁移容易出错,也不容易回测,测试不能覆盖到所有的页面,导致引入线上 bug

第二个方案,是把 KAE 直接从 Kotlin 源码中抽取出来单独维护,但是 KAE 中也大量依赖了 Kotlin 的源码,抽取成本较高。同时 KAE 中大量使用了 Kotlin 编译器插件的 API,而这部分 API 并没有稳定,当 K2 编译器正式发布的时候很可能还会有较大的改动,而这也带来较高的维护成本。

第三个方案就是本篇要重点介绍的 Kace

Kace 是什么?

Kace 即 kotlin-android-compatible-extensions,一个用于帮助从 kotlin-android-extensions 无缝迁移的框架

目前已经开源,开源地址可见:http://github.com/kanyun-inc/Kace

相比其它方案,Kace 主要有以下优点

  1. 接入方便,不需要手动修改旧代码,可以真正做到无缝迁移
  2. 与 KAE 表现一致(都支持 viewId 缓存,并在页面销毁时清除),不会引入预期外的 bug
  3. 统一迁移,回测方便,如果存在问题时,应该是批量存在的,避免手动修改可能引入线上 bug 的问题
  4. 通过生成源码的方式兼容 KAE,维护成本低

快速迁移

使用 Kace 完成迁移主要分为以下几步

1. 添加插件到 classpath

```kotlin // 方式 1 // 传统方式,在根目录的 build.gradle.kts 中添加以下代码 buildscript { repositories { mavenCentral() } dependencies { classpath("com.kanyun.kace:kace-gradle-plugin:1.0.0") } }

// 方式 2 // 引用插件新方式,在 settings.gradle.kts 中添加以下代码 pluginManagement { repositories { mavenCentral() } plugins { id("com.kanyun.kace") version "1.0.0" apply false } } ```

2. 应用插件

移除kotlin-android-extensions插件,并添加以下代码

kotlin plugins { id("com.kanyun.kace") id("kotlin-parcelize") // 可选,当使用了`@Parcelize`注解时需要添加 }

3. 配置插件(可选)

默认情况下 Kace 会解析模块内的每个 layout 并生成代码,用户也可以自定义需要解析的 layout

kotlin kace { whiteList = listOf() // 当 whiteList 不为空时,只有 whiteList 中的 layout 才会被解析 blackList = listOf("activity_main.xml") // 当 blackList 不为空时,blackList 中的 layout 不会被解析 }

经过以上几步,迁移就完全啦~

支持的类型

如上所示,Kace 目前支持了以上四种最常用的类型,其他 kotlin-android-extensions 支持的类型如 android.app.Fragment, android.app.Dialog, kotlinx.android.extensions.LayoutContainer 等,由于被废弃或者使用较少,Kace 目前没有做支持

版本兼容

| | Kotlin | AGP | Gradle | |--------|--------|-------|--------| | 最低支持版本 | 1.7.0 | 4.2.0 | 6.7.1 |

由于 Kace 的目标是帮助开发者更方便地迁移到 Kotlin 1.8,因此 Kotlin 最低支持版本比较高

原理解析:前置知识

编译器插件是什么?

Kotlin 的编译过程,简单来说就是将 Kotlin 源代码编译成目标产物的过程,具体步骤如下图所示:

Kotlin 编译器插件,通过利用编译过程中提供的各种Hook时机,让我们可以在编译过程中插入自己的逻辑,以达到修改编译产物的目的。比如我们可以通过 IrGenerationExtension 来修改 IR 的生成,可以通过 ClassBuilderInterceptorExtension 修改字节码生成逻辑

Kotlin 编译器插件可以分为 Gradle 插件,编译器插件,IDE 插件三部分,如下图所示

kotlin-android-extensions 是怎么实现的

我们知道,KAE 是一个 Kotlin 编译器插件,当然也可以分为 Gradle 插件,编译器插件,IDE 插件三部分。我们这里只分析 Gradle 插件与编译器插件的源码,它们的具体结构如下:

  1. AndroidExtensionsSubpluginIndicatorKAE插件的入口
  2. AndroidSubplugin用于配置传递给编译器插件的参数
  3. AndroidCommandLineProcessor用于接收编译器插件的参数
  4. AndroidComponentRegistrar用于注册如图的各种Extension

关于更细节的分析可以参阅:kotlin-android-extensions 插件到底是怎么实现的?

总的来说,其实 KAE 主要做了两件事

  1. KAE 会将 viewId 转化为 findViewByIdCached 方法调用
  2. KAE 会在页面关闭时清除 viewId cache

那么我们要无缝迁移,就也要实现相同的效果

Kace 原理解析

第一次尝试

我们首先想到的是解析 layout 自动生成扩展属性,如下图所示

```kotlin // 生成的代码 val AndroidExtensions.button1 get() = findViewByIdCached