思想交融,Android中的函數式編程(1):DiffUtil體驗

前言

隨着業務的急劇擴張,一些架構上的調整也隨之破土動工。從最初的MVC,管他是唱、跳、Rap,還是打籃球。通通寫在Activity裏;再到MVP階段的業務與View分離;然後就是現在的MVVM。

關於MVVM的內容,可以在我之前的文章中看到:

一點點入坑JetPack:ViewModel篇

一點點入坑JetPack:Lifecycle篇

一點點入坑JetPack:LiveData篇

一點點入坑JetPack:實戰前戲NetworkBoundResource篇

一點點入坑JetPack(終章):實戰MVVM

我猜可能有小夥伴們會不解,上文一頓瞎BB,和題目中的函數式編程、DiffUtil又有啥關係呢。不要着急,這一整個系列將承接上一個MVVM系列,圍繞函數式編程徹底展開一個從業務層面思想層面理解的一個過程。

這篇系列融合了很多公司大佬們的架構分享,加上我自己思考總結的一篇文章。希望可以給各位小夥伴們帶來收穫,當然也歡迎大家各抒己見,閉門造車就太不real了。

正文

作爲系列開篇第一章,我打算搞點實戰意義比較強的。所以這篇文章不會上來就扯思想上的東西,而是主要以DiffUtil的用法爲主。

主要包括以下部分:

  • 1、notifyDataSetChanged()
  • 2、DiffUtil基本用法
  • 3、DiffUtil的思考
  • 番外篇:源碼分析

閒話就不多說了,咱們搞快點、搞快點!

一、notifyDataSetChanged()

我相信這個方法,咱們大家都不陌生吧。在那個“懵懂無知”的編程初期,不知道有多少小夥伴和我一樣,靠着notifyDataSetChanged(),一招鮮吃遍天下。

直到後來發現,數據量多了之後,notifyDataSetChanged()變得巨慢無比…此時的自己只能“不滿”的噴一下Google:你tm就不能優化一下麼?…

直到自己瞭解到了notifyItemChanged()notifyItemInserted()等方法的時候。才知道就算自己“編程時長兩年半”,該“蔡”還是“蔡”…

剛剛提到的那些方法,其實作爲“職場老司機”,我猜大家應該很熟悉它們的用法。當然還有Payload機制下的局部bindData()

關於傳統RecyclerView的用法,就不多費口舌了,畢竟都是些基本操作。接下來就讓咱們走進DiffUtil

二、DiffUtil基本用法

從名字中,我們很容易猜到它的作用:一個幫我們diff數據集的工具。

對於之前的我們來說,diff的操作,都是我們業務方自己去處理的問題。然後根據數據的變動,自行選擇使用什麼樣的notify方法。

而DiffUtil就是幫我們做這部分內容,然後根據我們的具體實現,自動幫我們去notify。直接裸上代碼,畢竟能點進來的小夥伴,技術實力都不會太差。想要使用DiffUtil,第一步是繼承特定的接口:

2.1、繼承DiffUtil.Callback

先定一個數據結構:

// 不要在意這些變量是啥意思,就是3個不同的變量
data class Book(val id: Long, val name: String, val version: Long)

然後就是DiffUtil.Callback的實現類:

class BooksDiffCallback : DiffUtil.Callback() {

    private var oldData = emptyList<Book>()
    private var data = oldData

    fun update(data: List<Book>) {
        oldData = this.data
        this.data = data
    }

    // 如果此方法返回true,說明來個數據集中同一個position位置的數據沒有變化,至於如何notify需要參考areContentsTheSame()的返回值
    // 如果此方法返回false,直接刷新item
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldData[oldItemPosition]
        val newItem = data[newItemPosition]

        return oldItem === newItem || oldItem.id == newItem.id
    }
    
    // 此方法會在areItemsTheSame()返回true的時候調用。
    // 如果返回true,則意味着數據一樣,Item也一樣,不需要刷新。(這裏屬於業務方自行實現,比如我的實現就是當Book的version相同時,業務上認爲數據相同不需要刷新)
    // 如果返回false,則意味着數據不同,需要刷新。不過這裏還有一個分支,那就是是否Payload。此時便會走到getChangePayload()中
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldData[oldItemPosition]
        val newItem = data[newItemPosition]
        return oldItem.version == newItem.version
    }
    // 這裏就是普通Payload的方法,當version不同且name不同時,我們就告訴DiffUtil使用PAYLOAD_NAME作爲Payload的表示
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val list = mutableListOf<Any>()
        val oldItem = oldData[oldItemPosition]
        val newItem = data[newItemPosition]
        if (oldItem.name != newItem.name) {
            list.add(PAYLOAD_NAME)
        return list
    }
    companion object {
        val PAYLOAD_NAME = Any()
    }

    override fun getOldListSize(): Int {
        return oldData.size
    }

    override fun getNewListSize(): Int {
        return data.size
    }
}

完成這一步,我們就可以set我們的數據集了。

2.2、調用

我們可以在Adapter中簡單的封裝一個方法:

class BooksAdapter :RecyclerView.Adapter...{
    private val diffCallback = BooksDiffCallback()
    // ...省略部分代碼
    
    // BookViewHolder就是一個普通的ViewHolder
    override fun onBindViewHolder(viewHolder: BookViewHolder, book: Book) {
        viewHolder.bindData(book, viewHolder.adapterPosition)
    }

    override fun onBindViewHolder(holder: BookViewHolder, item: Book, payloads:MutableList<Any>) {
        if (payloads.isNullOrEmpty()) {
            onBindViewHolder(holder, item)
            return
        }

        if(payloads.contains(PAYLOAD_NAME)){
            // 調用BookViewHolder中業務方自己的局部刷新View的方法
            viewHolder.bindData(book.name, viewHolder.adapterPosition)
        }
    }
    
    // 對外暴露
    fun updateData(items: List<Books>) {
        this.items = items
        diffCallback.update(items)
        // 第二個參數false是啥意思呢?簡單來說到Adapter被調用了notifyItemMoved()時,不使用動畫。
        DiffUtil.calculateDiff(diffCallback, false).dispatchUpdatesTo(this)
    }
}

使用的時候,直接調用updateData(),傳入我們的新數據集合,無需任何其他操作。

到這我們的DiffUtil用法就結束了。我相信已經開始用DiffUtil的小夥伴一定會遇到下面這個問題:

數據集合都是同一個,因爲都是直接操作同一個集合的引用,因此導致DiffUtil的時候各種不生效。

三、DiffUtil的思考

上述的問題,我猜很多小夥伴都遇到過。因爲一些模式或者架構的原因。導致我們很多邏輯操作,都是使用同一個集合的引用,因此改變也是同一個集合元素。那麼這種情況下對於DiffUtil來說,至始至終oldData和newData都是同一個集合,那就不存在diff這一說了。

如果大家能感受到這其中的彆扭之處,那麼離理解,我想要聊的函數式編程就不遠。

因爲DiffUtil設計本身就是對不同的集合對象進行diff。因此我們在update的時候,就必須要輸入倆個不同的集合實例。

而這恰恰滿足了函數式編程(Functional Programming)所強調的倆點中的一點:不可變(immutable)。注意這個英文單詞immutable,以及於之對立的mutable。不知道大家有沒有留意到Kotlin中,大量的使用了這類單詞。簡單舉幾個例子:
MutableListMutableMap…等等

函數式編程強調的另一點是:無狀態(stateless)

大家再思考一個問題:RecycleView是啥?不就是一個UI控件麼。我們要做的是啥?不就是給RecycleView一個數據集,然後讓它展示出來。

那麼我們簡化一下RecycleView的這個模型,是不是RecycleView的這一系列操作就像一個函數/公式?給定一個輸入,必定有一個輸出。

嘚吧嘚,扯了這麼多“玄之又玄”的東西,想表達啥意思呢。UI操作本身就像函數表達式一樣,至於一切的數據變化,狀態改變,那是輸入給UI前的變換(transform)。

還記不記得咱們在上一個系列聊MVVM的時候,提到了數據驅動。Google抽象出了ViewModel就是讓我們去做數據的變換(transform)。變換完畢之後再輸入給UI模塊。對於咱們的例子來說,在Viewmodel之中變換完數據,把變化後的新數據集合,丟給DiffUtil,這纔是正確的使用方式。

而這次整個系列所聊的內容就基本發生在ViewModel這一層。我們應該使用函數編程的思想去transform數據集合。

尾聲

無論是面向對象,還是面向過程,亦或者函數式編程…本身都沒有特別明確的邊界。我們要做的應該是讓優秀的思想爲我們所用,提高我們的生產力,最終實現“面向自由編程”~~~

最後,與君共勉!

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,以及我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~

個人公衆號:鹹魚正翻身

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章