Skip to content

DataBinding 双向绑定

一、双向绑定概述

1.1 什么是双向绑定

双向绑定(Two-Way Data Binding)是 DataBinding 框架最强大的特性之一。它允许 View 的属性与数据模型自动同步,当数据改变时 UI 自动更新,当用户修改 UI 时数据自动更新,无需手动编写监听器和更新逻辑。

传统单向绑定的问题:

kotlin
// 单向绑定:数据 → View
class MyAdapter : RecyclerView.Adapter<ViewHolder>() {
    private var userName: String = ""
    private val editText: EditText = findViewById(R.id.edit_text)
    
    // 更新 UI
    fun updateName(name: String) {
        userName = name
        editText.text = text
    }
    
    // 获取用户输入
    fun getName(): String {
        return editText.text.toString()
    }
    
    // 问题:需要手动同步数据
    // 忘记同步会导致数据不一致
}

双向绑定的解决方案:

xml
<!-- 布局文件 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="user"
            type="com.example.User" />
    </data>
    
    <EditText
        android:id="@+id/edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@={user.name}" />
</layout>
kotlin
// 数据改变时,UI 自动更新
user.name = "John"  // editText 自动显示 "John"

// 用户输入时,数据自动更新
// 用户在 editText 中输入 "Jane" → user.name 自动变为 "Jane"

1.2 单向绑定 vs 双向绑定

特性单向绑定 @{}双向绑定 @={}
数据流向数据 → View数据 ↔ View
用户输入不会更新数据自动更新数据
程序修改自动更新 View自动更新 View
性能开销较高
使用场景只读展示编辑输入

1.3 双向绑定的应用场景

  1. 表单输入:登录表单、注册表单、设置页面
  2. 文本编辑:编辑器、搜索框
  3. 数值输入:金额、数量、年龄
  4. 复选框/开关:设置选项、多选列表
  5. 选择器:日期选择、时间选择

二、@={} 语法详解

2.1 基础语法

双向绑定符号

xml
<!-- 单向绑定 - 只读 -->
<TextView
    android:text="@{user.name}" />

<!-- 双向绑定 - 可读写 -->
<EditText
    android:text="@={user.name}" />

<!-- 区别:@{vs @={} -->

完整示例

xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.example.LoginViewModel" />
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <!-- 双向绑定:用户名 -->
        <EditText
            android:id="@+id/et_username"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="用户名"
            android:text="@={viewModel.username}" />

        <!-- 双向绑定:密码 -->
        <EditText
            android:id="@+id/et_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="密码"
            android:inputType="textPassword"
            android:text="@={viewModel.password}" />

        <!-- 单向绑定:显示当前用户名(只读) -->
        <TextView
            android:id="@+id/tv_display"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.username}" />

    </LinearLayout>
</layout>
kotlin
// ViewModel
class LoginViewModel : Observable() {
    var username: String = ""
    var password: String = ""
}

// 使用
val viewModel = LoginViewModel()
binding.viewModel = viewModel

// 双向绑定效果:
viewModel.username = "张三"  // et_username 自动显示 "张三"
// 用户在 et_username 输入 "李四" → viewModel.username 自动变为 "李四"

2.2 不同 View 的双向绑定

EditText

xml
<!-- 文本双向绑定 -->
<EditText
    android:id="@+id/edit_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={user.name}" />

<!-- 带默认值的双向绑定 -->
<EditText
    android:text="@={user.name ?: `默认值`}" />

CheckBox

xml
<!-- 复选框双向绑定 -->
<CheckBox
    android:id="@+id/cb_agree"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="同意协议"
    android:checked="@={user.agreed}" />

RadioButton

xml
<!-- 单选按钮双向绑定 -->
<RadioGroup
    android:id="@+id/rg_gender"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RadioButton
        android:id="@+id/rb_male"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="男"
        android:checked="@={user.gender == `male`}" />

    <RadioButton
        android:id="@+id/rb_female"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="女"
        android:checked="@={user.gender == `female`}" />

</RadioGroup>

SeekBar/ProgressBar

xml
<!-- 进度条双向绑定 -->
<SeekBar
    android:id="@+id/sb_volume"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:max="100"
    android:progress="@={user.volume}" />

Spinner

xml
<!-- Spinner 双向绑定(需要特殊处理) -->
<Spinner
    android:id="@+id/spinner_country"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onItemSelectedListener="@{user.countryListener}" />

2.3 复杂表达式的双向绑定

xml
<!-- 表达式双向绑定 -->
<EditText
    android:text="@={user.fullName ?: user.firstName + ` ` + user.lastName}" />

<!-- 计算属性双向绑定 -->
<EditText
    android:text="@={user.displayPrice ?: `$${user.price}`}" />

<!-- 带验证的双向绑定 -->
<EditText
    android:text="@={user.email}"
    app:error="@={user.emailError}" />

三、TwoWayBinding 机制

3.1 TwoWayBinding 接口

DataBinding 内部通过 TwoWayBinding 接口实现双向绑定机制:

java
// 内部接口(简化版)
public interface TwoWayBinding<O, T> {
    /**
     * 获取值 - 从数据源获取值设置到 View
     */
    O get(T source);
    
    /**
     * 设置值 - 从 View 获取值设置到数据源
     */
    void set(T source, O value);
}

3.2 自动生成的 TwoWayBinding 实现

对于 android:text="@={user.name}",DataBinding 会生成类似代码:

java
public class ActivityFormBinding extends ViewBinding {
    
    private static final TwoWayBinding sTextTwoWay = new TwoWayBinding() {
        @Override
        public CharSequence get(Object o) {
            return (CharSequence) o;
        }
        
        @Override
        public void set(Object obj, CharSequence value) {
            EditText editText = (EditText) obj;
            editText.setText(value);
            // 触发 TextWatcher 更新数据
        }
    };
    
    // 绑定方法
    public void setUserName(String userName) {
        this.userName.set(userName);
        TextView textView = (TextView) findViewById(R.id.edit_text);
        sTextTwoWay.set(textView, userName);
    }
}

3.3 绑定监听器注册

双向绑定会在内部注册监听器:

kotlin
// 生成的代码(简化)
fun bindEditText(editText: EditText, observable: ObservableField<String>) {
    // 1. 设置初始值
    editText.setText(observable.get())
    
    // 2. 注册 TextWatcher 监听输入
    editText.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            // 用户输入时更新数据
            observable.set(s.toString())
        }
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
    })
    
    // 3. 监听数据变化
    observable.addOnPropertyChangedListener { _, _, _, _ ->
        // 数据变化时更新 View(注意避免循环)
        val newValue = observable.get()
        if (editText.text != newValue) {
            editText.setText(newValue)
        }
    }
}

3.4 防止无限循环

DataBinding 内部有防止无限循环的机制:

java
// 内部实现(简化)
public void setViewValue(Object view, Object value) {
    // 检查是否需要更新
    if (currentValue.equals(value)) {
        return; // 值未变化,跳过
    }
    
    // 更新 View
    updateView(view, value);
    
    // 标记正在更新,防止循环
    isUpdatingView = true;
    
    // 监听器回调(可能会触发反向更新)
    // 但由于 isUpdatingView 为 true,会跳过
    
    isUpdatingView = false;
}

public void setDataValue(Object data, Object value) {
    if (isUpdatingView) {
        return; // View 正在更新中,跳过
    }
    
    // 更新数据
    updateData(data, value);
}

四、Braces 类详解

4.1 什么是 Braces

Braces 类是双向绑定中用于处理可空值和默认值的包装类。当双向绑定表达式可能返回 null 时,DataBinding 会自动使用 Braces 包装。

4.2 Braces 类的基本使用

java
// Braces 类(简化版)
public class Braces<T> {
    private T value;
    private boolean hasValue = false;
    
    public Braces() {}
    
    public Braces(T value) {
        this.value = value;
        this.hasValue = value != null;
    }
    
    public T get() {
        return value;
    }
    
    public void set(T value) {
        this.value = value;
        this.hasValue = value != null;
    }
    
    public boolean hasValue() {
        return hasValue;
    }
    
    // 双向绑定使用
    public Object getValue() {
        return value;
    }
    
    public void setValue(Object value) {
        this.value = (T) value;
        // 触发更新
    }
}

4.3 在双向绑定中的应用

xml
<!-- 可空属性的双向绑定 -->
<EditText
    android:id="@+id/et_nickname"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={user.nickname}" />
kotlin
// Data 类
data class User(
    var name: String = "",
    var nickname: String? = null  // 可空
)

// 生成的绑定代码
private Braces<String> _nickname = new Braces<>();

public String getUserNickname() {
    return _nickname.getValue(); // 可能是 null
}

public void setUserNickname(String nickname) {
    _nickname.setValue(nickname);
    // 更新 UI
}

4.4 Braces 与默认值

xml
<!-- 带默认值的双向绑定 -->
<EditText
    android:text="@={user.nickname ?: `默认昵称`}" />
kotlin
// 内部处理
private Braces<String> nickname = new Braces<>(
    user.getNickname() != null ? user.getNickname() : "默认昵称"
);

// 当用户修改时
public void setNickname(String value) {
    nickname.setValue(value);
    // 更新数据
    user.setNickname(value);
}

4.5 数值类型的 Braces

xml
<!-- 数值双向绑定 -->
<EditText
    android:id="@+id/et_age"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="number"
    android:text="@={user.age}" />
kotlin
// 使用 Braces 包装基本类型
private Braces<Integer> _age = new Braces<>(0);

public Integer getAge() {
    return _age.getValue() ?: 0;
}

public void setAge(Integer age) {
    _age.setValue(age);
    user.setAge(age);
}

4.6 布尔类型的 Braces

xml
<!-- 布尔双向绑定 -->
<CheckBox
    android:id="@+id/cb_news"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="订阅新闻"
    android:checked="@={user.subscribed}" />
kotlin
private Braces<Boolean> _subscribed = new Braces<>(false);

public Boolean getSubscribed() {
    return _subscribed.getValue() ?: false;
}

public void setSubscribed(Boolean subscribed) {
    _subscribed.setValue(subscribed);
    user.setSubscribed(subscribed);
}

五、性能影响

5.1 性能开销分析

双向绑定相比单向绑定有以下额外开销:

  1. 监听器注册:每个双向绑定都需要注册监听器
  2. 数据验证:每次更新都需要验证数据变化
  3. 循环保护:需要检查防止无限循环
  4. 对象创建:Braces 包装类创建

5.2 性能对比测试

kotlin
// 性能测试代码
@Test
fun testBindingPerformance() {
    val context = ContextWrapper(null)
    
    // 单向绑定
    val singleBindingTime = measureTimeMillis {
        for (i in 0..10000) {
            val binding = SingleBinding.inflate(layoutInflater)
            binding.data = TestData()
        }
    }
    
    // 双向绑定
    val twoWayBindingTime = measureTimeMillis {
        for (i in 0..10000) {
            val binding = TwoWayBinding.inflate(layoutInflater)
            binding.data = TestData()
        }
    }
    
    // 结果:双向绑定约慢 15-20%
    println("单向:$singleBindingTime ms")
    println("双向:$twoWayBindingTime ms")
    println("差异:${(twoWayBindingTime - singleBindingTime) * 100.0 / singleBindingTime} %")
}

5.3 内存开销

kotlin
// 内存分析
// 单向绑定:每个绑定约 100-200 字节
// 双向绑定:每个绑定约 300-500 字节(包含监听器和 Braces)

// 对于 100 个双向绑定的列表
// 额外内存:100 * (500 - 200) = 30 KB(可接受)

5.4 性能优化建议

1. 仅在需要时使用双向绑定

xml
<!-- ✅ 推荐:列表展示使用单向绑定 -->
<TextView
    android:text="@{item.name}" />  <!-- 单向,性能好 -->

<!-- ✅ 推荐:编辑输入使用双向绑定 -->
<EditText
    android:text="@={item.name}" />  <!-- 双向,需要编辑 -->

2. 使用 DiffUtil 优化列表

kotlin
// 列表更新优化
class MyDiffCallback : DiffUtil.Callback() {
    override fun areItemsTheSame(oldPosition: Int, newPosition: Int): Boolean {
        return oldList[oldPosition].id == newList[newPosition].id
    }
    
    override fun areContentsTheSame(oldPosition: Int, newPosition: Int): Boolean {
        return oldList[oldPosition] == newList[newPosition]
    }
}

// 使用
val diffResult = DiffUtil.calculateDiff(MyDiffCallback())
adapter.submitList(newList)
diffResult.dispatchUpdatesTo(adapter)

3. 避免过度更新

xml
<!-- ❌ 低效:每个字符都触发更新 -->
<EditText
    android:text="@={user.name}" />

<!-- ✅ 优化:使用防抖 -->
<EditText
    android:text="@={user.name}"
    android:afterTextChanged="@{user.nameWatcher}" />
kotlin
// 防抖实现
class NameWatcher(private val delay: Long = 300) : TextWatcher {
    private var handler = Handler(Looper.getMainLooper())
    private var runnable: Runnable? = null
    
    override fun afterTextChanged(s: Editable?) {
        runnable?.let { handler.removeCallbacks(it) }
        runnable = Runnable {
            // 防抖后的更新
            name.set(s.toString())
        }
        handler.postDelayed(runnable!!, delay)
    }
    
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}

5.5 列表双向绑定性能

kotlin
// ❌ 低效:整个列表使用双向绑定
class ListAdapter(data: List<Item>) {
    // 每个 item 都有双向绑定,1000 个 item 就是 1000 个监听器
}

// ✅ 高效:仅在编辑模式使用双向绑定
class ListAdapter(data: List<Item>, var editMode: Boolean = false) {
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        if (editMode) {
            // 编辑模式:使用双向绑定
            binding.editText.setText(@={item.name})
        } else {
            // 浏览模式:使用单向绑定
            binding.text.setText(@{item.name})
        }
    }
}

六、使用场景

6.1 表单输入

登录表单

xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="com.example.LoginViewModel" />
    </data>
    
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <!-- 用户名 -->
            <EditText
                android:id="@+id/et_username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="用户名"
                android:inputType="text"
                android:text="@={viewModel.username}" />

            <!-- 密码 -->
            <EditText
                android:id="@+id/et_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="密码"
                android:inputType="textPassword"
                android:text="@={viewModel.password}" />

            <!-- 记住我 -->
            <CheckBox
                android:id="@+id/cb_remember"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="记住我"
                android:checked="@={viewModel.rememberMe}" />

            <!-- 登录按钮 -->
            <Button
                android:id="@+id/btn_login"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="登录"
                android:onClick="@{viewModel::login}"
                android:enabled="@{!viewModel.username.isEmpty() &amp;&amp; !viewModel.password.isEmpty()}" />

        </LinearLayout>
    </ScrollView>
</layout>
kotlin
// ViewModel
class LoginViewModel {
    val username = ObservableField<String>("")
    val password = ObservableField<String>("")
    val rememberMe = ObservableField<Boolean>(false)
    
    fun login() {
        // 直接使用双向绑定的值
        if (!username.is_empty() && !password.is_empty()) {
            // 执行登录
        }
    }
}

注册表单

xml
<layout>
    <data>
        <variable
            name="user"
            type="com.example.RegisterUser" />
    </data>
    
    <LinearLayout
        android:orientation="vertical">
        
        <!-- 姓名 -->
        <EditText
            android:text="@={user.name}" />
        
        <!-- 邮箱 -->
        <EditText
            android:inputType="textEmailAddress"
            android:text="@={user.email}" />
        
        <!-- 手机号 -->
        <EditText
            android:inputType="phone"
            android:text="@={user.phone}" />
        
        <!-- 年龄 -->
        <EditText
            android:inputType="number"
            android:text="@={user.age}" />
        
        <!-- 性别 -->
        <RadioGroup android:orientation="horizontal">
            <RadioButton android:text="男"
                       android:checked="@={user.gender == `male`}" />
            <RadioButton android:text="女"
                       android:checked="@={user.gender == `female`}" />
        </RadioGroup>
        
        <!-- 兴趣选择 -->
        <CheckBox android:text="阅读"
                 android:checked="@={user.interests.reading}" />
        <CheckBox android:text="运动"
                 android:checked="@={user.interests.sports}" />
        
    </LinearLayout>
</layout>

6.2 设置页面

xml
<layout>
    <data>
        <variable
            name="settings"
            type="com.example.AppSettings" />
    </data>
    
    <LinearLayout
        android:orientation="vertical">
        
        <!-- 主题设置 -->
        <SeekBar
            android:max="3"
            android:progress="@={settings.themeMode}" />
        
        <!-- 字体大小 -->
        <SeekBar
            android:max="5"
            android:progress="@={settings.fontSize}" />
        
        <!-- 通知开关 -->
        <Switch
            android:text="开启通知"
            android:checked="@={settings.notificationsEnabled}" />
        
        <!-- 隐私设置 -->
        <CheckBox
            android:text="隐私模式"
            android:checked="@={settings.privacyMode}" />
        
    </LinearLayout>
</layout>

6.3 评论编辑

xml
<layout>
    <data>
        <variable
            name="comment"
            type="com.example.Comment" />
    </data>
    
    <LinearLayout
        android:orientation="vertical">
        
        <!-- 评论内容 -->
        <EditText
            android:id="@+id/et_comment"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:gravity="top"
            android:hint="写下你的评论..."
            android:inputType="textMultiLine"
            android:text="@={comment.content}" />
        
        <!-- 字符计数 -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`剩余 ${100 - comment.content.length()} 字`}" />
        
        <!-- 提交按钮 -->
        <Button
            android:text="提交"
            android:enabled="@{comment.content.length() > 0}" />
        
    </LinearLayout>
</layout>

6.4 搜索框

xml
<layout>
    <data>
        <variable
            name="searchViewModel"
            type="com.example.SearchViewModel" />
    </data>
    
    <EditText
        android:id="@+id/et_search"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="搜索..."
        android:inputType="text"
        android:text="@={searchViewModel.query}" />
</layout>
kotlin
// 搜索 ViewModel
class SearchViewModel {
    val query = ObservableField<String>("")
    
    init {
        // 监听搜索词变化,触发搜索
        query.addOnPropertyChangedListener { _, _, _, _ ->
            performSearch(query.get())
        }
    }
    
    private fun performSearch(query: String?) {
        // 执行搜索
    }
}

七、最佳实践

7.1 使用 ObservableField

kotlin
// ✅ 推荐:使用 ObservableField
class User {
    var name = ObservableField<String>("")
    var age = ObservableField<Int>(0)
}

// ❌ 不推荐:使用普通字段
class User {
    var name: String = ""  // 不会触发 UI 更新
    var age: Int = 0
}

7.2 使用 Observable 对象

kotlin
// ✅ 推荐:继承 Observable
class User : Observable() {
    var name: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.name)
        }
    
    var age: Int = 0
        set(value) {
            field = value
            notifyPropertyChanged(BR.age)
        }
}

7.3 防抖处理

kotlin
// 防抖的双向绑定
class DebounceTextWatcher(private val delay: Long = 500) : TextWatcher {
    private var handler = Handler(Looper.getMainLooper())
    private var runnable: Runnable? = null
    
    fun onTextChanged(text: String, callback: () -> Unit) {
        runnable?.let { handler.removeCallbacks(it) }
        runnable = Runnable {
            callback()
        }
        handler.postDelayed(runnable!!, delay)
    }
    
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
    override fun afterTextChanged(s: Editable?) {
        onTextChanged(s.toString()) {
            // 防抖后的处理
        }
    }
}

7.4 数据验证

kotlin
// 带验证的双向绑定
class ValidatedString : ObservableField<String>("") {
    private var validator: ((String) -> Boolean)? = null
    
    fun setValidator(validator: (String) -> Boolean) {
        this.validator = validator
    }
    
    override fun set(value: String?) {
        if (value != null && validator != null && !validator.invoke(value)) {
            // 验证失败
            return
        }
        super.set(value)
    }
}

// 使用
val email = ValidatedString()
email.setValidator { it.contains("@") }

7.5 空值处理

xml
<!-- 空值默认值 -->
<EditText
    android:text="@={user.nickname ?: `未设置`}" />

<!-- 空值显示为空字符串 -->
<EditText
    android:text="@={user.nickname ?: ``}" />

八、面试考点

8.1 基础概念

Q1: 什么是双向绑定?与单向绑定有什么区别?

A:

  • 双向绑定:数据与 UI 之间双向同步
  • 单向绑定:仅数据 → UI
  • 区别:双向绑定需要监听器,性能开销更大

Q2: @={} 和 @{的作用分别是什么?

A:

  • @{}:单向绑定,数据变化更新 UI
  • @={}:双向绑定,数据与 UI 互相同步

8.2 机制理解

Q3: 双向绑定的实现原理?

A:

  1. 为每个双向绑定创建 TwoWayBinding 实例
  2. 注册对应的监听器(TextWatcher、CheckedChangeListener 等)
  3. 监听器触发时更新数据源
  4. 数据源变化时更新 View
  5. 使用标志位防止无限循环

Q4: Braces 类的作用?

A:

  • 包装可空值和基本类型
  • 提供默认值处理
  • 支持双向绑定的空值安全

Q5: 如何防止双向绑定的无限循环?

A:

  • 使用标志位标记正在更新的状态
  • 检查新旧值是否相同
  • 仅在值真正变化时更新

8.3 场景题

Q6: 双向绑定在表单中的最佳实践?

A:

  1. 使用 ObservableField
  2. 添加防抖处理
  3. 数据验证
  4. 错误提示

Q7: 列表中使用双向绑定如何优化性能?

A:

  1. 仅编辑模式使用双向绑定
  2. 使用 DiffUtil 优化更新
  3. 减少不必要的绑定
  4. 考虑使用单项绑定 + 手动更新

8.4 手写代码

Q8: 实现一个简单的双向绑定

A:

kotlin
class SimpleTwoWayBinding(editText: EditText, observable: ObservableField<String>) {
    init {
        // 初始化
        editText.setText(observable.get())
        
        // 监听输入
        editText.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                if (s.toString() != observable.get()) {
                    observable.set(s.toString())
                }
            }
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
        })
        
        // 监听数据变化
        observable.addOnPropertyChangedListener { _, _, _, _ ->
            val newValue = observable.get()
            if (editText.text != newValue) {
                editText.setText(newValue)
            }
        }
    }
}

九、常见问题

9.1 绑定不生效

**问题:**双向绑定后,数据变化但 UI 未更新

原因:

  1. 未使用 ObservableField
  2. 未调用 notifyPropertyChanged
  3. 值变化被忽略(相同值)

解决:

kotlin
class User {
    var name = ObservableField<String>("")
    
    fun setName(newName: String) {
        name.set(newName)  // ✅ 正确
        // name = ObservableField(newName)  // ❌ 错误:替换了 ObservableField
    }
}

9.2 内存泄漏

**问题:**双向绑定的监听器导致内存泄漏

解决:

kotlin
class MyFragment : Fragment() {
    private var _binding: FragmentMyBinding? = null
    private val binding get() = _binding!!
    
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null  // 释放 Binding,监听器自动清理
    }
}

9.3 性能问题

**问题:**列表双向绑定导致卡顿

解决:

  1. 使用单向绑定
  2. 使用 DiffUtil
  3. 减少绑定数量

十、总结

10.1 核心要点

  1. 双向绑定语法@={}
  2. Observable 支持:ObservableField、ObservableObject
  3. 性能考虑:仅在需要时使用
  4. 内存管理:及时释放 Binding

10.2 使用建议

推荐场景:

  • 表单输入
  • 设置页面
  • 搜索框

不推荐场景:

  • 大量只读展示
  • 高性能要求的列表

10.3 学习路径

  1. 掌握基础语法 @={}
  2. 理解 Observable 机制
  3. 学会性能优化
  4. 掌握最佳实践

本文档最后更新:2024 年

双向绑定是 DataBinding 的精髓,合理使用可以大大简化代码!