使用 LiveData 进行数据绑定

livedata-observe.png

 LiveData 是对可观察数据的封装。不像其他可观察对象(例如 ObservableField) , LiveData 可以感知到生命周期。这就意味着它可以关联到其他拥有生命周期的组件上,比如 Activity、Fragment 或者 Service。这种感知,可以确保 LinveData 的更新只发生在一个组件的活动状态上。如下图所示:

viewmodel_scope.png

 对于一个观察者类而言,所谓的激活状态就是 STARTED或者 RESUMED 状态。非激活状态并不更新。
 对于 Activity 来说,在 onStart 之后,到 onPause 之前,就是 STARTED;在 onResume 调用之后,就是 RESUMED 状态。
 通常,我们总是定义一个实现了 LifecyclerOwner 接口对象作为观察者。这种关系,会使得其在 DESTROY 状态时,自动移除对数的观察。

LiveData 的优势

使用 LiveData 有以下优势:

  • 确保 UI 和当前的数据状态匹配:LiveData 提供了一种观察者模式。当观察者的生命周期状态发生变化时,它会适时更新将数据更新到 UI 上。而并非是任何时候,都会对 UI 进行更新。
  • 避免内存泄漏:观察者是一个 Lifecycle 对象。当 LiveData 所关联的观察者被销毁时,LiveData 会自动清理自己。
  • 避免因 stop activity 造成的奔溃:当观察者对象处于非活动状态时,比如 activity 返回到回退栈中,此时,它将无法接收到 LiveData 的数据更新事件。
  • 不用手动处理生命周期:UI 组件观察相关的数据,但是并不会主动停止或者继续这种观察。当观察者生命周期发生变化时,LiveData 会自动管理自己。
  • 总是更新到最新的数据:当组件从 非活动 状态转换到 活动 状态时,他讲更新到最新的数据。
  • 正确的处理 configuration 的变化:当 activity 或者 fragment 由于 configuration(比如说屏幕旋转) 的变化而被创建时,它会自动接收到最新的可用数据。
  • 资源共享:我们可以使用单例模式继承一个 LiveData,当然将它绑定到一个系统服务中,这种这个 LiveData 就可以共享了。

LiveData 的使用

  1. 首先,创建一个持有数据的 LiveData 对象。这一步通常是在 ViewModel 中完成。
  2. 创建一个 Observer 对象,并定义其 onChange() 方法。该方法将控制在 LiveData 所持有的数据发生变化时,观察者将发生怎样的变化。我们通常创建在 UI controller 中创建 Observer。而这类 UI controller 诸如 activity 和 fragment。
  3. 通过 observe() 方法,将 Observer(观察者)和 LiveData(被观察者)绑定在一起。这样以来,当 LiveData 数据发生变化时,只要 Observer 处于 活动 状态,将自动通知 Observer 。

创建 LiveData 对象

 LiveData 可以包裹任何数据,包括集合类,比如 List。LiveData 通常存储在 ViewModel 中,通过 getter 方法提供给观察者。

public class UserViewModel extends ViewModel {

    MutableLiveData<String> userName;

    UserViewModel(){
        userName = new MutableLiveData<>();
    }

    public LiveData<String> getUserName(){
        return userName;
    }

    public void setUserName(String name){
        userName.setValue(name);
    }
}

 综上,我们看到 UI controller,比如 activity 或者 fragment 仅仅负责显示数据,而不再管理数据状态。如此一来,将大大避免了 UI controller 的臃肿。

订阅 LiveData 对象

 通常,组件的 onCreate() 方法,是个合适的地方以建立对 LiveData 的观察或者说是订阅,理由如下:

  • onCreate() 方法在创建的时候,只会调用一次。
  • 确保 UI controller 处于 活动 状态时,能够有数据显示。

 LiveData 只会在数据变化,同时观察者处于 活动 状态时,才会通知观察者更新。当然,第一次初始显示数据除外,数据被初始化,直接通知处于 活动状态的 UI controller 进行数据更新。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mainViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
    mainViewModel.getEncryptedFileNum().observe(this, num -> {
            encryptedFileNumText.setText(String.format("文件 %d 个", num));
    });
}

 当 observe() 方法调用后,onChange() 方法被立即调用,为 encyptedFileNumText 提供最新的值。随后,只有 mainViewModel 中的 encryptedFileNum 发生变化,且该 UI controller 处于 活动 状态,encyptedFileNumText 才会更新相应 UI。

更新 LiveData 对象

 LiveData 本身没有公开可用的方法用以更新数据。MultableLiveData 则暴露了 setValue(T) 和 postValue(T) 方法来更新 LiveData 中的数据。注意,setValue 方法用于在主线程中更新值,而 postValue 则用于在工作线程中更新值。

private MutableLiveData<String> addressName ;
public void setAddressName(String name) {
        addressName.setValue(name);
}

one-way data binding VS two-way data binding

 在单向绑定中,我们通过改变 LiveData 中的值,来更新 UI 。通常,我们还需要当用户对 UI 进行了操作之后,所带了的变化能反馈到 LiveData 的值上,即自动更新 LiveData 中的值。这一点,在 LiveData 中很容易做到。
单向绑定:

<CheckBox
    android:layout_width="18dp"
    android:layout_height="18dp"
    android:background="@drawable/checkbox_style"
    android:button="@null"
    android:checked="@{pickerBean.selected}"
    android:visibility="@{pickerBean.checkVisible? View.VISIBLE:View.GONE,default=visible}"/>

双向绑定:

<CheckBox
    android:layout_width="18dp"
    android:layout_height="18dp"
    android:background="@drawable/checkbox_style"
    android:button="@null"
    android:checked="@={pickerBean.selected}"
    android:visibility="@{pickerBean.checkVisible? View.VISIBLE:View.GONE,default=visible}"/>

 注意,单向绑定和双向绑定在 XML 中的唯一区别,就是 android:checked="@={pickerBean.selected}" 中 @ 后面是否有等号。

使用自定义属性进行双向绑定

 上个代码块中,我们对 checked 属性使用了双向绑定。那么,如果是我们自定义的属性该如何处理?
 为了达到这个目的,需要使用 @InverseBindingAdapter@InverseBindingMethod 注解。
 以为 MyView 绑定设置 时间 为例。首先,需要使用 @BindingAdapter

@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
    // Important to break potential infinite loops.
    if (view.time != newValue) {
        view.time = newValue;
    }
}

 然后,使用 @InverseBindingAdapter 注解,告诉它当 MyView 的属性发生变化时,该调用哪个方法:

@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
    return view.getTime();
}

 应当注意,当使用双向绑定时,不要发生的无限调用的陷阱。当用户改变了 View 的属性,@InverseBindingAdapter 被调用了。LiveData 中的值发生了变化,这将导致 @BindingAdapter 所注解的方法被调用。如此一来,可能会在 @InverseBindingAdapter@BindingAdapter 两个注解方法中无限循环下去。为了防止这种事情发生,可以参考上述 setTime 方法中的应用。

应用场景

 观察者模式的应用场景本身就很丰富。订阅-发布,通过消息或者说事件将组件之间,组件和数据之间关联起来,这种应用体验非常友好。业务逻辑将更加清楚;同时,将少大量的冗余代码,使开发者更加关注和处理业务逻辑。以下,记录一些实例,做一些展开说明。

在 Room 中使用

 Room 是 Google 提供的组件库之一,是对 SQLite 的封装。它对 LiveData 的支持,使得操作数据库的数据,可以直接反应到为用户提供的 UI 展示上。进一步说,它的查询方法可以返回一个 LiveData 对象,这个对象的泛型可以是基础类型的包装类,例如 Integer 、Boolean、String、Long 这些包装类,也可以是 List。

@Query(" SELECT  " +
        "              a.*    ," +
        "              b.transStatus ,       " +
        "              b.fileLength ,       " +
        "              b.progress ,       " +
        "              b.needDecrypted ,       " +
        "              b.id as transId, " +
        "              b.uuid as transUuid, " +
        "              b.localFilePath as transPath , " +
        "              MAX(b.date) as transDate " +
        "              FROM    FileShareEntity a  " +
        "              LEFT JOIN FileTransEntity b " +
        "              ON a.uuid = b.uuid  " +
        "              WHERE a.isRec == 1 AND a.gid=:gid" +
        "              group by a.uuid  order by a.date desc"
)
LiveData<List<FileShareSendItem>> getFileShareSendItems(String gid);

 通过查询,得到了一个 LiveData 对象,然后通过 ViewModel,将其和上层 UI 绑定在一起。

public class ShareSendModule extends AndroidViewModel {
...
LiveData<List<FileShareSendItem>> getFileShareSendItems(String gid) {
    return shareDao.getFileShareSendItems(gid);
}
...
}

 最后,在 Fragment 中完成绑定(订阅):

module.getFileShareSendItems(gid).observe(this, adapter::setData);

 此时,当 List 数据发生任何变化,如果 Fragment 处于活动状态,就会被更新。注意到这里的 setData 方法,将更改 adapter 中的数据,结合 DiffUtil.Callback ,RecyclerView 的使用将变得非常非常清爽。

在 RecyclerView 中使用

 其实上面已经提到了 Room 和 RecyclerView 的结合。我们可以做进一步的绑定。将 List 中的数据和每个 Item 绑定在一起。直接操作数据变化,不在单独处理 UI 展示。

public AddressAdapter(AppCompatActivity activity) {
    addressModel = new AddressModel();
    addressModel.getAddresses().observe(activity, addressEntities -> {
        if (mItems.size() != 0) {
            AddressDiffCallback postDiffCallback = new AddressDiffCallback(mItems, addressEntities);
            DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(postDiffCallback, true);
            transformEntities2Beans(addressEntities, mItems);
            diffResult.dispatchUpdatesTo(this);
            //  notifyDataSetChanged();
        } else {
            transformEntities2Beans(addressEntities, mItems);
            notifyDataSetChanged();
        }
    });

    setHasStableIds(true); // this is required for swiping feature.
    mItems = new ArrayList<>();
}

@NonNull
@Override
public AddressViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    if (viewType == ITEM_TYPE_NORMAL) {
        ActivityAddressItemBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.activity_address_item, parent, false);
        binding.setLifecycleOwner((LifecycleOwner) parent.getContext());
        return new AddressViewHolder(binding);
    } else {
        View header = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_address_item_add, null);
        return new AddressViewHolder(header);
    }
}

@Override
public void onBindViewHolder(@NonNull AddressViewHolder holder, int position) {
    AddressBean item = mItems.get(position);
    holder.bind(item);
}

@Override
public int getItemCount() {
    return mItems.size();
}

class AddressViewHolder extends RecyclerView.ViewHolder {

    ActivityAddressItemBinding binding;

    private boolean isHeader;

    AddressViewHolder(View root) {
        super(root);
        this.root = root;
        isHeader = true;
    }

    AddressViewHolder(ActivityAddressItemBinding binding) {
        super(binding.getRoot());
        this.binding = binding;
        isHeader = false;
    }

    void bind(AddressBean bean) {
        if (isHeader) {
            bindHeader();
        } else {
            bindItem(bean);
        }
    }

    void bindHeader() {
    .....
    }

    void bindItem(AddressBean bean) {
        binding.setAddressBean(bean);
        ......
    }
}

一些小技巧

 在使用过程中,还有一些小技巧,记录在此。

和方法的绑定
public class AddressBean extends ViewModel {
...
 public void onDelete(View view){
 ...
 }
...
}
// 在 xml 中
<TextView
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:background="#cfcfcf"
    android:text="删除"
    android:textSize="12sp"
    android:textColor="@color/white"
    android:gravity="center"
    android:onClick="@{addressBean::onDelete}"/>
View 可见性绑定
<data>
    <variable
        name="phoneBean"
        type="com.yuegs.AddressPhoneBean" />
    <import type="android.view.View" />
</data>

 <CheckBox
    android:layout_width="17dp"
    android:layout_height="17dp"
    android:layout_alignParentRight="true"
    android:layout_centerVertical="true"
    android:layout_marginRight="12dp"
    android:background="@drawable/checkbox_style"
    android:button="@null"
    android:checked="@={phoneBean.selected}"
    android:visibility="@{phoneBean.checkVisible? View.VISIBLE:View.GONE,default=visible}"/>

总结

 绑定的基础,是观察者模式。只不过,这种观察者模式的细节实现,由这类 LiveData 和 ViewModel 帮助我们实现了。

参考

LiveData Overview
LiveData beyond the ViewModel — Reactive patterns using Transformations and MediatorLiveData
Android Architecture Patterns Part 3:
Model-View-ViewModel

AndroidViewModel vs ViewModel
MediatorLiveData
Advanced Data Binding: Binding to LiveData (One- and Two-Way Binding)
Two-way data binding

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容