使用 LiveData 進(jìn)行數(shù)據(jù)綁定

livedata-observe.png

?LiveData 是對可觀察數(shù)據(jù)的封裝。不像其他可觀察對象(例如 ObservableField) , LiveData 可以感知到生命周期偿渡。這就意味著它可以關(guān)聯(lián)到其他擁有生命周期的組件上帆谍,比如 Activity、Fragment 或者 Service沥寥。這種感知碍舍,可以確保 LinveData 的更新只發(fā)生在一個組件的活動狀態(tài)上。如下圖所示:

viewmodel_scope.png

?對于一個觀察者類而言邑雅,所謂的激活狀態(tài)就是 STARTED或者 RESUMED 狀態(tài)片橡。非激活狀態(tài)并不更新。
?對于 Activity 來說淮野,在 onStart 之后捧书,到 onPause 之前,就是 STARTED骤星;在 onResume 調(diào)用之后经瓷,就是 RESUMED 狀態(tài)。
?通常洞难,我們總是定義一個實(shí)現(xiàn)了 LifecyclerOwner 接口對象作為觀察者舆吮。這種關(guān)系,會使得其在 DESTROY 狀態(tài)時队贱,自動移除對數(shù)的觀察色冀。

LiveData 的優(yōu)勢

使用 LiveData 有以下優(yōu)勢:

  • 確保 UI 和當(dāng)前的數(shù)據(jù)狀態(tài)匹配:LiveData 提供了一種觀察者模式。當(dāng)觀察者的生命周期狀態(tài)發(fā)生變化時柱嫌,它會適時更新將數(shù)據(jù)更新到 UI 上锋恬。而并非是任何時候,都會對 UI 進(jìn)行更新编丘。
  • 避免內(nèi)存泄漏:觀察者是一個 Lifecycle 對象与学。當(dāng) LiveData 所關(guān)聯(lián)的觀察者被銷毀時,LiveData 會自動清理自己嘉抓。
  • 避免因 stop activity 造成的奔潰:當(dāng)觀察者對象處于非活動狀態(tài)時索守,比如 activity 返回到回退棧中,此時抑片,它將無法接收到 LiveData 的數(shù)據(jù)更新事件蕾盯。
  • 不用手動處理生命周期:UI 組件觀察相關(guān)的數(shù)據(jù),但是并不會主動停止或者繼續(xù)這種觀察。當(dāng)觀察者生命周期發(fā)生變化時级遭,LiveData 會自動管理自己望拖。
  • 總是更新到最新的數(shù)據(jù):當(dāng)組件從 非活動 狀態(tài)轉(zhuǎn)換到 活動 狀態(tài)時,他講更新到最新的數(shù)據(jù)挫鸽。
  • 正確的處理 configuration 的變化:當(dāng) activity 或者 fragment 由于 configuration(比如說屏幕旋轉(zhuǎn)) 的變化而被創(chuàng)建時说敏,它會自動接收到最新的可用數(shù)據(jù)。
  • 資源共享:我們可以使用單例模式繼承一個 LiveData丢郊,當(dāng)然將它綁定到一個系統(tǒng)服務(wù)中盔沫,這種這個 LiveData 就可以共享了。

LiveData 的使用

  1. 首先枫匾,創(chuàng)建一個持有數(shù)據(jù)的 LiveData 對象架诞。這一步通常是在 ViewModel 中完成。
  2. 創(chuàng)建一個 Observer 對象干茉,并定義其 onChange() 方法谴忧。該方法將控制在 LiveData 所持有的數(shù)據(jù)發(fā)生變化時,觀察者將發(fā)生怎樣的變化角虫。我們通常創(chuàng)建在 UI controller 中創(chuàng)建 Observer沾谓。而這類 UI controller 諸如 activity 和 fragment。
  3. 通過 observe() 方法戳鹅,將 Observer(觀察者)和 LiveData(被觀察者)綁定在一起均驶。這樣以來,當(dāng) LiveData 數(shù)據(jù)發(fā)生變化時枫虏,只要 Observer 處于 活動 狀態(tài)妇穴,將自動通知 Observer 。

創(chuàng)建 LiveData 對象

?LiveData 可以包裹任何數(shù)據(jù)隶债,包括集合類腾它,比如 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 僅僅負(fù)責(zé)顯示數(shù)據(jù),而不再管理數(shù)據(jù)狀態(tài)稀并。如此一來仅颇,將大大避免了 UI controller 的臃腫。

訂閱 LiveData 對象

?通常碘举,組件的 onCreate() 方法忘瓦,是個合適的地方以建立對 LiveData 的觀察或者說是訂閱,理由如下:

  • onCreate() 方法在創(chuàng)建的時候引颈,只會調(diào)用一次耕皮。
  • 確保 UI controller 處于 活動 狀態(tài)時境蜕,能夠有數(shù)據(jù)顯示。

?LiveData 只會在數(shù)據(jù)變化凌停,同時觀察者處于 活動 狀態(tài)時粱年,才會通知觀察者更新。當(dāng)然罚拟,第一次初始顯示數(shù)據(jù)除外台诗,數(shù)據(jù)被初始化,直接通知處于 活動狀態(tài)的 UI controller 進(jìn)行數(shù)據(jù)更新赐俗。

@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));
    });
}

?當(dāng) observe() 方法調(diào)用后拉队,onChange() 方法被立即調(diào)用,為 encyptedFileNumText 提供最新的值阻逮。隨后粱快,只有 mainViewModel 中的 encryptedFileNum 發(fā)生變化,且該 UI controller 處于 活動 狀態(tài)夺鲜,encyptedFileNumText 才會更新相應(yīng) UI皆尔。

更新 LiveData 對象

?LiveData 本身沒有公開可用的方法用以更新數(shù)據(jù)。MultableLiveData 則暴露了 setValue(T) 和 postValue(T) 方法來更新 LiveData 中的數(shù)據(jù)币励。注意慷蠕,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 仅胞。通常每辟,我們還需要當(dāng)用戶對 UI 進(jìn)行了操作之后,所帶了的變化能反饋到 LiveData 的值上干旧,即自動更新 LiveData 中的值渠欺。這一點(diǎn),在 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 中的唯一區(qū)別,就是 android:checked="@={pickerBean.selected}" 中 @ 后面是否有等號编整。

使用自定義屬性進(jìn)行雙向綁定

?上個代碼塊中舔稀,我們對 checked 屬性使用了雙向綁定。那么掌测,如果是我們自定義的屬性該如何處理内贮?
?為了達(dá)到這個目的,需要使用 @InverseBindingAdapter@InverseBindingMethod 注解。
?以為 MyView 綁定設(shè)置 時間 為例夜郁。首先什燕,需要使用 @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 注解拂酣,告訴它當(dāng) MyView 的屬性發(fā)生變化時秋冰,該調(diào)用哪個方法:

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

?應(yīng)當(dāng)注意,當(dāng)使用雙向綁定時婶熬,不要發(fā)生的無限調(diào)用的陷阱剑勾。當(dāng)用戶改變了 View 的屬性,@InverseBindingAdapter 被調(diào)用了赵颅。LiveData 中的值發(fā)生了變化虽另,這將導(dǎo)致 @BindingAdapter 所注解的方法被調(diào)用。如此一來饺谬,可能會在 @InverseBindingAdapter@BindingAdapter 兩個注解方法中無限循環(huán)下去捂刺。為了防止這種事情發(fā)生,可以參考上述 setTime 方法中的應(yīng)用募寨。

應(yīng)用場景

?觀察者模式的應(yīng)用場景本身就很豐富族展。訂閱-發(fā)布,通過消息或者說事件將組件之間拔鹰,組件和數(shù)據(jù)之間關(guān)聯(lián)起來仪缸,這種應(yīng)用體驗(yàn)非常友好。業(yè)務(wù)邏輯將更加清楚列肢;同時恰画,將少大量的冗余代碼,使開發(fā)者更加關(guān)注和處理業(yè)務(wù)邏輯瓷马。以下拴还,記錄一些實(shí)例,做一些展開說明欧聘。

在 Room 中使用

?Room 是 Google 提供的組件庫之一片林,是對 SQLite 的封裝。它對 LiveData 的支持怀骤,使得操作數(shù)據(jù)庫的數(shù)據(jù)费封,可以直接反應(yīng)到為用戶提供的 UI 展示上。進(jìn)一步說晒喷,它的查詢方法可以返回一個 LiveData 對象孝偎,這個對象的泛型可以是基礎(chǔ)類型的包裝類访敌,例如 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);

?此時虽抄,當(dāng) List 數(shù)據(jù)發(fā)生任何變化,如果 Fragment 處于活動狀態(tài)独柑,就會被更新迈窟。注意到這里的 setData 方法,將更改 adapter 中的數(shù)據(jù)忌栅,結(jié)合 DiffUtil.Callback 车酣,RecyclerView 的使用將變得非常非常清爽。

在 RecyclerView 中使用

?其實(shí)上面已經(jīng)提到了 Room 和 RecyclerView 的結(jié)合索绪。我們可以做進(jìn)一步的綁定湖员。將 List 中的數(shù)據(jù)和每個 Item 綁定在一起。直接操作數(shù)據(jù)變化瑞驱,不在單獨(dú)處理 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}"/>

總結(jié)

?綁定的基礎(chǔ),是觀察者模式拴袭。只不過读第,這種觀察者模式的細(xì)節(jié)實(shí)現(xiàn),由這類 LiveData 和 ViewModel 幫助我們實(shí)現(xiàn)了拥刻。

參考

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怜瞒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子般哼,更是在濱河造成了極大的恐慌吴汪,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒸眠,死亡現(xiàn)場離奇詭異漾橙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)楞卡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門霜运,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脾歇,“玉大人,你說我怎么就攤上這事淘捡∨焊鳎” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵焦除,是天一觀的道長激况。 經(jīng)常有香客問我,道長膘魄,這世上最難降的妖魔是什么乌逐? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮创葡,結(jié)果婚禮上黔帕,老公的妹妹穿的比我還像新娘。我一直安慰自己蹈丸,他們只是感情好成黄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著逻杖,像睡著了一般奋岁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荸百,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天闻伶,我揣著相機(jī)與錄音,去河邊找鬼够话。 笑死蓝翰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的女嘲。 我是一名探鬼主播畜份,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼欣尼!你這毒婦竟也來了爆雹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤愕鼓,失蹤者是張志新(化名)和其女友劉穎钙态,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體菇晃,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡册倒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了磺送。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驻子。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡屈尼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拴孤,到底是詐尸還是另有隱情,我是刑警寧澤甲捏,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布演熟,位于F島的核電站,受9級特大地震影響司顿,放射性物質(zhì)發(fā)生泄漏芒粹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一大溜、第九天 我趴在偏房一處隱蔽的房頂上張望化漆。 院中可真熱鬧,春花似錦钦奋、人聲如沸座云。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽朦拖。三九已至,卻和暖如春厌衔,著一層夾襖步出監(jiān)牢的瞬間璧帝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工富寿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留睬隶,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓页徐,卻偏偏與公主長得像苏潜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子变勇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容