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

前言

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

關於MVVM的內容,能夠在我以前的文章中看到:數據結構

一點點入坑JetPack:ViewModel篇架構

一點點入坑JetPack:Lifecycle篇less

一點點入坑JetPack:LiveData篇ide

一點點入坑JetPack:實戰前戲NetworkBoundResource篇函數式編程

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

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

這篇系列融合了不少公司大佬們的架構分享,加上我本身思考總結的一篇文章。但願能夠給各位小夥伴們帶來收穫,固然也歡迎你們各抒己見,閉門造車就太不real了。源碼分析

正文

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

主要包括如下部分:

  • 一、notifyDataSetChanged()
  • 二、DiffUtil基本用法
  • 三、DiffUtil的思考
  • 番外篇:源碼分析

閒話就很少說了,我們搞快點、搞快點!

1、notifyDataSetChanged()

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

直到後來發現,數據量多了以後,notifyDataSetChanged()變得巨慢無比...此時的本身只能「不滿」的噴一下Google:你tm就不能優化一下麼?...

直到本身瞭解到了notifyItemChanged()notifyItemInserted()等方法的時候。才知道就算本身「編程時長兩年半」,該「蔡」仍是「蔡」...

剛剛提到的那些方法,其實做爲「職場老司機」,我猜你們應該很熟悉它們的用法。固然還有Payload機制下的局部bindData()

關於傳統RecyclerView的用法,就很少費口舌了,畢竟都是些基本操做。接下來就讓我們走進DiffUtil

2、DiffUtil基本用法

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

對於以前的咱們來講,diff的操做,都是咱們業務方本身去處理的問題。而後根據數據的變更,自行選擇使用什麼樣的notify方法。

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

2.一、繼承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.二、調用

咱們能夠在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的時候各類不生效。

3、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數據集合。

尾聲

不管是面向對象,仍是面向過程,亦或者函數式編程...自己都沒有特別明確的邊界。咱們要作的應該是讓優秀的思想爲咱們所用,提升咱們的生產力,最終實現「面向自由編程」~~~

最後,與君共勉!

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

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索