升级目标API级别到35(三) —— kotlin-android-extensions从入门到废弃


什么是 kotlin-android-extensions

基本概念

kotlin-android-extensions 是 Kotlin 官方提供的一个 Android 插件,它允许开发者通过 kotlinx.android.synthetic 包直接访问 XML 布局文件中定义的 View,而无需使用 findViewById() 方法。

主要功能

  • 自动生成 View 绑定代码:根据 XML 布局文件自动生成对应的 View 引用
  • 类型安全:编译时检查 View 类型,避免运行时类型转换错误
  • 简化代码:减少样板代码,提高开发效率

kotlin-android-extensions 的工作原理

编译时处理

  1. 插件扫描:在编译时扫描 XML 布局文件
  2. 代码生成:为每个布局文件生成对应的 synthetic 导入
  3. 字节码注入:将生成的代码注入到 Kotlin 类中

生成的代码示例

// 原始代码
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 直接使用 View,无需 findViewById
        textView.text = "Hello World"
        button.setOnClickListener { /* ... */ }
    }
}

编译后生成的代码

// 编译器自动生成的代码
class MainActivity : AppCompatActivity() {
    private var _$_findViewCache: Map<Int, View>? = null

    private fun _$_findCachedViewById(id: Int): View? {
        if (_$_findViewCache == null) {
            _$_findViewCache = HashMap()
        }
        var view = _$_findViewCache!![id]
        if (view == null) {
            view = findViewById(id)
            _$_findViewCache!![id] = view
        }
        return view
    }

    // 为每个 View 生成属性
    val textView: TextView
        get() = _$_findCachedViewById(R.id.textView) as TextView

    val button: Button
        get() = _$_findCachedViewById(R.id.button) as Button
}

如何集成 kotlin-android-extensions

重要提示

从 Kotlin 1.4.20-M2 版本开始,kotlin-android-extensions 插件已被官方废弃。
如果您正在开始新项目,建议直接使用 View Binding。
以下内容仅供参考和维护旧项目使用。

版本要求

  • Kotlin 版本:< 1.8.0
  • Android Gradle Plugin 版本:≤ 4.2.2
  • Gradle 版本:≤ 6.7.1

1. 在项目级 build.gradle 中配置

buildscript {
    ext.kotlin_version = '1.7.10' // 必须使用 1.8.0 以下版本
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // 不需要单独添加 kotlin-android-extensions,它包含在 kotlin-gradle-plugin 中
    }
}

2. 在模块级 build.gradle 中启用插件

// 旧版 Groovy DSL
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

// 或者使用新版 Kotlin DSL
plugins {
    id("kotlin-android")
    id("kotlin-android-extensions")
}

3. 配置实验性功能(可选)

如果需要使用 @Parcelize 等实验性功能:

androidExtensions {
    experimental = true
}

4. 兼容性说明

  1. 版本限制

    • 不支持 Kotlin 1.8.0 及以上版本
    • 不建议在新项目中使用
    • 与某些新版本的 Jetpack 库可能存在兼容性问题
  2. 跨模块限制

    • synthetic 导入在跨模块场景下无法工作
    • 存在一个自2018年1月就未解决的相关问题
  3. IDE支持

    • Android Studio 最新版本可能无法很好地支持此插件
    • 建议使用与项目 Kotlin 版本相匹配的 IDE 版本

kotlin-android-extensions 的使用方法

1. 基本用法

// 导入布局文件的所有 View
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 直接使用 View
        titleTextView.text = "Welcome"
        loginButton.setOnClickListener {
            // 处理点击事件
        }
    }
}

2. 导入特定 View

// 只导入特定的 View
import kotlinx.android.synthetic.main.activity_main.titleTextView
import kotlinx.android.synthetic.main.activity_main.loginButton

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        titleTextView.text = "Welcome"
        loginButton.setOnClickListener { /* ... */ }
    }
}

kotlin-android-extensions 的优缺点

优点

  1. 简化代码:无需手动调用 findViewById()
  2. 类型安全:编译时检查 View 类型
  3. 性能优化:使用缓存机制,避免重复查找
  4. 开发效率:减少样板代码,提高开发速度
  5. IDE 支持:自动补全和重构支持

缺点

  1. 全局导入:可能导致命名冲突
  2. 编译时依赖:增加编译时间
  3. 调试困难:生成的代码难以调试
  4. 版本兼容性:与某些库可能存在兼容性问题
  5. 废弃风险:已被官方废弃,不再维护

为什么需要迁移

官方废弃声明

  • 废弃时间:2020年10月
  • 废弃原因:View Binding 提供了更好的替代方案
  • 维护状态:不再接收新功能和 bug 修复

技术债务

  1. 安全风险:使用废弃的插件存在安全风险
  2. 兼容性问题:与新版本 Android Gradle Plugin 可能存在兼容性问题
  3. 性能影响:可能影响编译性能和运行时性能

现代化需求

  1. View Binding:官方推荐的现代化解决方案
  2. 类型安全:提供更好的类型安全保证
  3. 空安全:支持 Kotlin 空安全特性

kotlin-android-extensions 的完整功能范围

主要功能组成

  1. View 绑定功能 (kotlinx.android.synthetic)

    • 允许直接通过 ID 访问 View
    • 提供缓存机制优化性能
    • 支持在 Activity、Fragment、自定义 View 等组件中使用
  2. Parcelable 实现功能 (@Parcelize)

    • 提供 @Parcelize 注解,自动生成 Parcelable 实现
    • 大大简化了数据类的序列化过程
    @Parcelize
    data class User(
        val name: String,
        val age: Int
    ) : Parcelable
    
  3. 实验性功能

    • 容器化视图访问
    • 自定义视图解析器
    • 布局容器支持
  4. 编译时代码生成

    • 生成 View 缓存代码
    • 生成 Parcelable 实现代码
    • 生成类型安全的访问器
  5. IDE 支持

    • 代码补全
    • 导航支持
    • 重构工具

迁移影响

移除 kotlin-android-extensions 插件会带来以下影响:

  1. 需要替代 View 绑定

    • 使用 View Binding 或 Data Binding
    • 或回退到 findViewById
  2. 需要替代 Parcelable 实现

    • 手动实现 Parcelable 接口
    • 使用其他序列化库(如 Gson、Moshi)
    • 使用 kotlin-parcelize 插件(推荐,这是一个独立的插件)
  3. 构建配置变更

    // 移除旧插件
    // apply plugin: 'kotlin-android-extensions'
    
    // 添加新插件(如果需要 Parcelable 功能)
    apply plugin: 'kotlin-parcelize'
    
    // 启用 View Binding(替代 synthetic)
    android {
        buildFeatures {
            viewBinding true
        }
    }
    
  4. 代码迁移工作

    • 替换所有 kotlinx.android.synthetic 导入
    • 迁移所有 @Parcelize 注解(到新插件)
    • 更新所有使用实验性功能的代码
  5. 性能影响

    • 编译时间可能会改善(移除了额外的代码生成)
    • 运行时性能可能会略有变化(取决于替代方案)

@Parcelize 注解的迁移方案

@Parcelize 简介

@Parcelize 是 kotlin-android-extensions 插件提供的一个注解,用于自动生成 Parcelable 接口的实现代码。它极大地简化了数据类的序列化过程。

当前项目使用情况

项目中大量使用了 @Parcelize 注解,主要用于:

  1. 数据传输对象(DTO)
  2. UI 状态对象
  3. Bundle 传递的数据对象
  4. Intent 传递的数据对象

迁移方案

方案一:使用独立的 kotlin-parcelize 插件(推荐)
  1. 更新 build.gradle 配置

    // 移除旧插件
    // apply plugin: 'kotlin-android-extensions'
    
    // 添加新插件
    apply plugin: 'kotlin-parcelize'
    
  2. 更新导入语句

    // 旧导入
    // import kotlinx.android.parcel.Parcelize
    
    // 新导入
    import kotlinx.parcelize.Parcelize
    
  3. 代码无需修改

    // 原有代码可以保持不变
    @Parcelize
    data class User(
        val name: String,
        val age: Int
    ) : Parcelable
    
方案二:手动实现 Parcelable(不推荐)

如果因为某些原因无法使用 kotlin-parcelize 插件,可以手动实现 Parcelable 接口:

data class User(
    val name: String,
    val age: Int
) : Parcelable {
    constructor(parcel: Parcel) : this(
        parcel.readString() ?: "",
        parcel.readInt()
    )

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeInt(age)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<User> {
        override fun createFromParcel(parcel: Parcel): User {
            return User(parcel)
        }

        override fun newArray(size: Int): Array<User?> {
            return arrayOfNulls(size)
        }
    }
}

混淆配置

确保在 proguard-rules.pro 中添加以下规则:

-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

常见问题

  1. 编译错误

    Unresolved reference: Parcelize
    

    解决方案:检查是否正确添加了 kotlin-parcelize 插件并更新了导入语句

  2. 运行时错误

    ClassNotFoundException: Parcelable class not found
    

    解决方案:检查混淆规则是否正确配置

  3. 自定义类型序列化问题
    对于自定义类型,可能需要实现 Parcelize 的类型转换器:

    @TypeParceler<CustomType, CustomTypeParceler>()
    @Parcelize
    data class MyData(
        val customType: CustomType
    ) : Parcelable
    

LayoutContainer 实现原理及替代方案

LayoutContainer 简介

LayoutContainerkotlin-android-extensions 提供的一个接口,主要用于在 RecyclerView 的 ViewHolder 或自定义 View 中优化视图绑定性能。它通过提供一个 containerView 属性来缓存根视图,避免重复调用 findViewById

原始实现方式

import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_user.view.*

class UserViewHolder(
    override val containerView: View
) : RecyclerView.ViewHolder(containerView), LayoutContainer {

    fun bind(user: User) {
        // 直接使用 ID 访问视图,无需 itemView 前缀
        tvUserName.text = user.name
        ivUserAvatar.load(user.avatar)
    }
}

实现原理

  1. 缓存机制

    interface LayoutContainer {
        val containerView: View?
    }
    
    • containerView 作为缓存的根视图
    • 所有子视图查找都基于这个根视图
    • 避免了重复的 findViewById 调用
  2. 编译时处理

    // 编译器生成的代码
    class UserViewHolder : RecyclerView.ViewHolder, LayoutContainer {
        override val containerView: View
    
        private fun findView(id: Int): View {
            return containerView.findViewById(id)
        }
    
        val tvUserName: TextView
            get() = findView(R.id.tvUserName) as TextView
    }
    

替代方案

  1. 使用 View Binding

    class UserViewHolder(
        private val binding: ItemUserBinding
    ) : RecyclerView.ViewHolder(binding.root) {
    
        fun bind(user: User) {
            binding.tvUserName.text = user.name
            binding.ivUserAvatar.load(user.avatar)
        }
    }
    
  2. 使用属性委托

    class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val tvUserName by lazy { itemView.findViewById<TextView>(R.id.tvUserName) }
        private val ivUserAvatar by lazy { itemView.findViewById<ImageView>(R.id.ivUserAvatar) }
    
        fun bind(user: User) {
            tvUserName.text = user.name
            ivUserAvatar.load(user.avatar)
        }
    }
    
  3. 自定义 ViewHolder 基类

    abstract class BaseViewHolder<T>(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val viewCache = mutableMapOf<Int, View>()
    
        protected fun <V : View> findViewById(id: Int): V {
            return viewCache.getOrPut(id) { itemView.findViewById(id) } as V
        }
    }
    
    class UserViewHolder(itemView: View) : BaseViewHolder<User>(itemView) {
        private val tvUserName: TextView = findViewById(R.id.tvUserName)
        private val ivUserAvatar: ImageView = findViewById(R.id.ivUserAvatar)
    
        fun bind(user: User) {
            tvUserName.text = user.name
            ivUserAvatar.load(user.avatar)
        }
    }
    
  4. 自定义 LayoutContainer

    /**
     * A base interface for all view holders supporting Android Extensions-style view access.
     */
    interface LayoutContainer {
       /** Returns the root holder view. */
       val containerView: View?
    }
    
    

Gradle 构建问题及解决方案

MissingMethodException 错误

当移除 kotlin-android-extensions 插件后,可能会遇到以下错误:

Caused by: groovy.lang.MissingMethodException: No signature of method: build_f60ids97qe6e542qgq7ean07.android() is applicable for argument types: (build_f60ids97qe6e542qgq7ean07$_run_closure1) values: [build_f60ids97qe6e542qgq7ean07$_run_closure1@355b8b3b]

这个错误通常有以下几个原因:

  1. 插件顺序问题

    // 正确的顺序
    apply plugin: 'com.android.application'  
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-kapt'
    // apply plugin: 'kotlin-android-extensions'  // 移除此行
    
  2. 版本兼容性问题

    • Android Gradle Plugin 版本
    • Kotlin 版本
    • Gradle 版本
      这三者之间必须保持兼容
  3. 构建配置问题

    android {
        // 移除 androidExtensions 块
        // androidExtensions {
        //     experimental = true
        // }
    }
    

解决方案

  1. 检查插件声明顺序

    • 确保 com.android.application 插件在最前面
    • 其他 Kotlin 相关插件紧随其后
  2. 更新版本组合

    // 项目级 build.gradle
    buildscript {
        ext.kotlin_version = '1.7.10'
        dependencies {
            classpath 'com.android.tools.build:gradle:4.2.2'
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        }
    }
    
  3. 检查 Gradle 版本

    // gradle-wrapper.properties
     // distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
    distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
    
  4. 清理项目

    # 执行清理命令
    ./gradlew clean
    # 删除 .gradle 文件夹
    rm -rf .gradle
    # 删除 build 文件夹
    rm -rf build/
    

版本兼容性表

Kotlin 版本AGP 版本Gradle 版本
1.7.x4.2.26.7.1
1.6.x4.2.06.7.1
1.5.x4.1.36.5

其他可能的解决方案

  1. 重新导入项目

    • 关闭 Android Studio
    • 删除 .idea 文件夹
    • 重新打开项目
  2. 更新 IDE 缓存

    • File -> Invalidate Caches / Restart
    • 选择 Invalidate and Restart
  3. 检查 Gradle JDK 设置

    • File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle
    • 确保使用正确的 JDK 版本

注意事项

  1. 不要直接删除配置

    • 先注释掉相关配置
    • 确认构建正常后再删除
  2. 保持版本一致性

    • 所有模块使用相同的 Kotlin 版本
    • 确保依赖库版本兼容
  3. 迁移步骤

    • 先在测试分支进行迁移
    • 确认所有功能正常后再合并到主分支
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值