Jetpack Compose(4)——重組

来源:https://www.cnblogs.com/joy99/p/18113680
-Advertisement-
Play Games

目錄一、狀態變化1.1 狀態變化是什麼1.2 mutableStateListOf 和 mutableStateMapOf二、重組的特性2.1 Composable 重組是智能的2.2 Composable 會以任意順序執行2.3 Composable 會併發執行2.4 Composable 會反覆 ...


目錄

上一篇文章講了 Compose 中狀態管理的基礎知識,本文講解 Compose 中狀重組的相關知識。

一、狀態變化

1.1 狀態變化是什麼

根據上篇文章的講解,在 Compose 我們使用 State 來聲明一個狀態,當狀態發生變化時,則會觸發重組。那麼狀態變化是指什麼呢?
下麵我們來看一個例子:

@Composable
fun NumList() {
    val num by remember {
        mutableStateOf(mutableListOf(1, 2, 3))
    }
    Column {
        Button(onClick = {
            num += (num.last() + 1)
            Log.d("sharpcj", "num: $num")
        }) {
            Text(text = "click to add one")
        }
        num.forEach {
            Text(text = "item --> $it")
        }
    }
}

這段代碼中,我們定義了一個 State ,其包裹的類型是 MutableList, 並且每次點擊,我們就給該 mutableList 增加一個元素。運行一下:

我們點擊了按鈕,界面並沒有發生變化,但是,從日誌看到,每次點擊後,list 中的元素的確增加了一個。

2024-03-18 20:51:41.472 12574-12574 sharpcj                 com.sharpcj.hellocompose             D  num: [1, 2, 3, 4]
2024-03-18 20:51:42.411 12574-12574 sharpcj                 com.sharpcj.hellocompose             D  num: [1, 2, 3, 4, 5]
2024-03-18 20:51:43.347 12574-12574 sharpcj                 com.sharpcj.hellocompose             D  num: [1, 2, 3, 4, 5, 6]

原因是什麼呢?其實狀態發生變化,實際上指的是 State 包裹的對象,進行 equals 比較,如果不相等,則認為狀態變化,否則認為沒有發生變化。所以這裡就解釋得通了,我們雖然在點擊按鈕後,給 mutableList 增加了元素,但是 mutableList 在進行前後比較時,比較的是其引用,對象的引用並沒有發生變化,所以沒有發生重組。【這裡結論並不准確,下麵穩定類型詳細解釋說】
那為了讓其發生重組,我們稍作修改,每次點擊按鈕時,創建一個新的 list,然後賦值,看看是不是我們所期待的結果。

@Composable
fun NumList() {
    var num by remember {
        mutableStateOf(mutableListOf(1, 2, 3))
    }
    Column {
        Button(onClick = {
            val num1 = num.toMutableList()
            num1 += (num1.last() + 1)
            num = num1
            Log.d("sharpcj", "num: $num")
        }) {
            Text(text = "click to add one")
        }
        num.forEach {
            Text(text = "item --> $it")
        }
    }
}

再次運行程式:

結果符合我們的預期。那對於 List 類型的數據對象,每次狀態發生變化,我們創建了一個新對象,這樣在進行 equals 比較時,必定不相等,則會觸發重組。

1.2 mutableStateListOf 和 mutableStateMapOf

上面的問題,我們雖然接解決了, 但是寫法不夠優雅,其實 Compose 給我們提供了一個函數 mutableStateListOf 來解決這類問題,我們看看這個函數怎麼用,改寫上面的例子

@Composable
fun NumList() {
    val num = remember {
        mutableStateListOf(1, 2, 3)
    }
    Column {
        Button(onClick = {
            num += (num.last() + 1)
            Log.d("sharpcj", "num: $num")
        }) {
            Text(text = "click to add one")
        }
        num.forEach {
            Text(text = "item --> $it")
        }
    }
}

這樣就可以滿足我們的需求。 mutableStateListOf 返回了一個可感知內部數據變化的 SnapshotStateList<T>, 它的內部的實現為了保證不變性,仍然是拷貝元素,只不過它用了更加高效的實現,比我們單純用toMutableList要高效得多。
由於 SnapshotStateList 繼承了 MutableList 介面,使得 MutableList 中定義的方法,依然可以使用。
同理,對於 Map 類型的對象, Compose 中提供了 mutableStateMapOf 方法,可以更優雅,更高效地進行處理。

思考如下問題:
假如我定義了一個類型:data class Hero(var name: String, var age: Int), 然後使用 mutableStateListOf 定義了狀態,其中的元素是自定義的類型 Hero, 當改變 Hero 的屬性時, 與該狀態相關的 Composable 是否會發生重組?

data class Hero(var name: String, var age: Int)

@Composable
fun HeroInfo() {
    val heroList = remember {
        mutableStateListOf(Hero(name = "安其拉", age = 18), Hero(name = "魯班", age = 19))
    }

    Column {
        Button(onClick = {
            heroList[0].name = "DaJi"
            heroList[0].age = 22
        }) {
            Text(text = "test click")
        }

        heroList.forEach {
            Text(text = "student, name: ${it.name}, age: ${it.age} ")
        }
    }
}

二、重組的特性

2.1 Composable 重組是智能的

傳統 View 體系通過修改 View 的私有屬性來改變 UI, Compose 則通過重組刷新 UI。 Compose 的重組非常“智能”,當重組發生時,只有狀態發生更新的 Composable 才會參與重組,沒有變化的 Composable 會跳過本次重組。

@Composable
fun KingHonour() {
    Column {
        var name by remember {
            mutableStateOf("周瑜")
        }
        Button(onClick = {
            name = "小喬"
        }) {
            Text(text = "改名")
        }
        Text(text = "魯班")
        Text(text = name)

    }
}

該例子中,點擊按鈕,改變了 name 的值,觸發重組,Button 和 Text(text = "魯班"),並不依賴該狀態,雖然在重組時被調用了,但是在運行時並不會真正的執行。因為其參數沒有變化,Compose 編譯器會在編譯器插入相關的比較代碼。只有最後一個 Text 依賴該狀態,會參與真正的重組。

2.2 Composable 會以任意順序執行

@Composable
fun Navi() {
    Box {
        FirstScreen()
        SecondScreen()
        ThirdScreen()
    }
}

在代碼中出現多個 Composable 函數時,它們並不一定按照在代碼中出現的順序執行,比如在一個 Box 中,處於前景的 UI 具有較高優先順序。所以不要試圖通過外部變數與其它 Composable 產生關聯。

2.3 Composable 會併發執行

重組中的 Composable 並不一定執行在 UI 線程,它們可以在後臺線程中併發執行,這樣利於發揮多喝處理器的優勢。正因為此,也需要考慮線程安全問題。

2.4 Composable 會反覆執行

除了重組會造成 Composable 的再次執行外,在動畫等場景中每一幀的變化都可能引起 Composable 的執行。因此 Composable 可能在短時間內多次執行。

2.5 Composable 的執行是“樂觀”的

所謂“樂觀”是指 Composable 最終會依據最新的狀態正確地完成重組。在某些場景下,狀態可能會連續變化,可能會導致中間態的重組在執行時被打斷,新的重組會插進來,對於被打斷的重組,Compose 不會將執行一半的結果反應到視圖樹上,因為最後一次的狀態總歸是正確的。

三、重組範圍

原則:重組範圍最小化。
只有受到了 State 變化影響的代碼塊,才會參與到重組,不依賴 State 變化的代碼則不參與重組。
如何確定重組範圍呢?修改上面的例子:

@Composable
fun RecompositionTest() {
    Column {
        Box {
            Log.i("sharpcj", "RecompositionTest - 1")
            Column {
                Log.i("sharpcj", "RecompositionTest - 2")
                var name by remember {
                    mutableStateOf("周瑜")
                }
                Button(onClick = {
                    name = "小喬"
                }) {
                    Log.i("sharpcj", "RecompositionTest - 3")
                    Text(text = "改名")
                }
                Text(text = "魯班")
                Text(text = name)
            }
        }
        Box {
            Log.i("sharpcj", "RecompositionTest - 4")
        }
        Card {
            Log.i("sharpcj", "RecompositionTest - 5")
        }
    }
}

運行,第一次我們看到,列印瞭如下日誌:

2024-03-22 15:36:15.303 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 1
2024-03-22 15:36:15.305 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 2
2024-03-22 15:36:15.326 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 3
2024-03-22 15:36:15.337 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 4
2024-03-22 15:36:15.344 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 5

這是正常的,每個控制項範圍內都執行了。我們點擊,button, 改變了 name 狀態。列印如下日誌:

2024-03-22 15:37:48.480 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 1
2024-03-22 15:37:48.480 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 2
2024-03-22 15:37:48.491 19870-19870 sharpcj                 com.sharpcj.hellocompose             I  RecompositionTest - 4

首先我們 name 這個狀態影響的組件時 Text,它所在的作用域應該是 Column 內部。列印 RecompositionTest - 2 好理解,可為什麼連 Column 的上一級作用域 Box 也被調用了,並且連該 Box 的統計 Box 也被調用了,但是 Card 卻又沒有被調用。這個好像與上面說的原則相悖。其實不然,我們看看 ColumnBoxCard 源碼就清楚了。

@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}
@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) {
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    Layout(
        content = { BoxScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}
@Composable
fun Card(
    modifier: Modifier = Modifier,
    shape: Shape = CardDefaults.shape,
    colors: CardColors = CardDefaults.cardColors(),
    elevation: CardElevation = CardDefaults.cardElevation(),
    border: BorderStroke? = null,
    content: @Composable ColumnScope.() -> Unit
) {
    Surface(
        modifier = modifier,
        shape = shape,
        color = colors.containerColor(enabled = true),
        contentColor = colors.contentColor(enabled = true),
        tonalElevation = elevation.tonalElevation(enabled = true),
        shadowElevation = elevation.shadowElevation(enabled = true, interactionSource = null).value,
        border = border,
    ) {
        Column(content = content)
    }
}

不難發現, Column 和 Box 都是使用 inline 修飾的。
最後簡單瞭解下 Compose 重組的底層原理。
經過 Compose 編譯器處理後的 Composable 代碼在對 State 進行讀取時,能夠自動建立關聯,在運行過程中,當 State 變化時, Compose 會找到關聯的代碼塊標記為 Invalid, 在下一渲染幀到來之前,Compose 觸發重組並執行 invalid 代碼塊, invalid 代碼塊即下一次重組的範圍。能夠被標記為 Invalid 的代碼必須是非 inline 且無返回值的 Composable 函數或 lambda。

需要註意的是,重組的範圍,與只能跳過並不衝突,確定了重組範圍,會調用對應的組件代碼,但是當參數沒有變化時,在運行時不會真正執行,會跳過本次重組。

四、參數類型的穩定性

4.1 穩定和不穩定

前面,Composable 狀態變化觸發重組,狀態變化基於 equals 比較結果,這是不准確的。準確地說:只有當比較的狀態對象,是穩定的,才能通過 equals 比較結果確定是否重組。什麼叫穩定的?還是看一個例子:

data class Hero(var name: String)

val shangDan = Hero("呂布")

@Composable
fun StableTest() {
    var greeting by remember {
        mutableStateOf("hello, 魯班")
    }

    Column {
        Log.i("sharpcj", "invoke --> 1")
        Text(text = greeting)
        Button(onClick = {
            greeting = "hello, 魯班大師"
        }) {
            Text(text = "搞錯了,是魯班大師")
        }
        ShangDan(shangDan)
    }
}

@Composable
fun ShangDan(hero: Hero) {
    Log.i("sharpcj", "invoke --> 2")
    Text(text = hero.name)
}

運行一下,列印

2024-03-22 17:07:50.248 26973-26973 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 1
2024-03-22 17:07:50.272 26973-26973 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 2

點擊 Button,再次看到列印:

2024-03-22 17:07:53.182 26973-26973 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 1
2024-03-22 17:07:53.191 26973-26973 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 2

問題來了, Shangdan 這個組件依賴的只依賴一個參數,並且參數也沒有改變,為什麼確在重組過程中被調用了呢?
接下來,我們將 Hero 這個類做點改變,將其屬性聲明由 var 變成 val

data class Hero(val name: String)

再次運行,

2024-03-22 17:35:41.435 28561-28561 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 1
2024-03-22 17:35:41.458 28561-28561 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 2

點擊button:

2024-03-22 17:35:47.790 28561-28561 sharpcj                 com.sharpcj.hellocompose             I  invoke --> 1

這次,Shangdan 這個 Composable 沒有參與重組了。為什麼會這樣呢?

其實是在因為此前,用 var 聲明 Hero 類的屬性時,Hero 類被 Compose 編譯器認為是不穩定類型。即有可能,我們傳入的參數引用沒有變化,但是屬性被修改過了,而 UI 又確實需要顯示修改後的最新值。而當用 val 聲明屬性了,Compose 編譯器認為該對象,只要對象引用不要變,那麼這個對象就不會發生變化,自然 UI 也就不會發生變化,所以就跳過了這次重組。
常用的基本數據類型以及函數類型(lambda)都可以稱得上是穩定類型,它們都不可變。反之,如果狀態是可變的,那麼比較 equals 結果將不再可信。在遇到不穩定類型時,Compose 的抉擇是寧願犧牲一些性能,也總好過顯示錯誤的 UI。

4.2 @Stable 和 @Immutable

上面講了穩定與不穩定的概念,然而實際開發中,我們經常會根據業務自定義 data class, 難道用了 Compose, 雖然 Kotlin 編碼規範,強調儘量使用 val, 但是還是要根據實際業務,使用 var 來定義可變屬性。對於這種類型,我們可以為其添加 @Stable 註解,讓編譯器將其視為穩定類型。從而發揮智能重組的作用,提升重組的性能。

@Stable
data class Hero(var name: String)

這樣,Hero 即便使用 var 聲明屬性,它作為參數傳入 Composable 中,只要對象引用沒變,都不會觸發重組。所以具體什麼時候使用該註解,還需要根據需求靈活使用。

除了 @Stable,Compose 還提供了另一個類似的註解 @Immutable,與 @Stable 不同的是,@Immutable 用來修飾的類型應該是完全不可變的。而 @Stable 可以用在函數、屬性等更多場景。使用起來更加方便,由於功能疊加,未來 @Immutable 有可能會被移除,建議優先使用 @Stable

最後總結一下:本文接著上篇文章的狀態,講解了重組的一些特性,如何確定重組的範圍,以及重組的中的類型穩定性概念,以及如何提升非穩定類型在重組過程中的性能。
下一篇文章將會講解 Composable 的生命周期以及重組的副作用函數。

作者:SharpCJ     作者博客:http://joy99.cnblogs.com/     本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • fdisk -l命令用於查看CentOS系統中所有硬碟及其分區的詳細信息。該命令的輸出會顯示硬碟的大小、分區表結構、分區類型以及每個分區的起始和結束扇區等信息。 ...
  • 一:ansible劇本 1:簡介 一系列ansible命令的集合,使用yaml語言進行編寫的,從上往下的執行,支持很多的特性,比如,將某個命令的狀態作為變數給其他的任務執行,變數,迴圈,判斷,錯誤糾正,可以是一個playbook或者是多個playbook執行 2:yaml基本語法 1、yaml約束 ...
  • 源碼分析丨MySQL的多層SP中Cursor相關BUG 一、問題發現 在一次開發中在sp中使用多層cursor的時候想知道每層的m_max_cursor_index值分別是多少,以用來做後續開發。於是做了以下的試驗,但是發現第一個level=2那層的m_max_cursor_index的值有點問題。 ...
  • 本文分享自華為雲社區《GaussDB DWS的SQL ON ANYWHERE技術解密》,作者:tooooooooooomy。 1. 前言 適用版本:【8.1.1(及以上)】 查詢分析是大數據要解決的核心問題之一,雖然大數據相關的處理引擎組件種類繁多,並提供了豐富的介面供用戶使用,但相對傳統資料庫用戶 ...
  • Apache DolphinScheduler已支持Apache SeaTunnel任務類型,本文介紹了SeaTunnel任務類型如何創建,任務參數,以及任務樣例。 一、Apache SeaTunnel SeaTunnel 任務類型,用於創建並執行 SeaTunnel 類型任務。worker 執行該 ...
  • 作者 | ALIREZA SADEGHI 翻譯 | Debra Chen 簡介 雖然生成式人工智慧和ChatGPT帶來的沸沸揚揚的炒作令科技界為之一振,但在數據工程領域,2023年仍然是一個令人振奮和充滿活力的一年,數據工程生態系統變得更加多樣化和複雜化,系統中的所有層面都在不斷創新和演進。 隨著各 ...
  • 本文介紹了SEQUENCE的使用場景和相關的函數的使用方法,並對使用SEQUENCE過程中遇到的常見問題及解決方法進行了彙總。 ...
  • 目錄一、 Composable 的生命周期二、 Composable 的副作用2.1 SideEffect2.2 DisposableEffect2.3 LaunchedEffect2.4 rememberCoroutineScope2.5 rememberUpdatedState2.6 deriv ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...