和所有響應式UI框架一樣,Compose 也是使用State來更新UI的
我們通常都是用下面的結構來開發(fā):
class HelloCodelabActivity : AppCompatActivity() {
private lateinit var binding: ActivityHelloCodelabBinding
var name = ""
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.textInput.doAfterTextChanged {text ->
name = text.toString()
updateHello()
}
}
private fun updateHello() {
binding.helloText.text = "Hello, $name"
}
}
這種方式就是典型的命令式編程签孔,想要改變UI就必須得調(diào)用更新UI的方法叉讥,這種方式有以下缺點
- UI狀態(tài)和Views緊密結合窘行,導致難以進行單測
- 當有很多事件需要更新state時,可能會忘記更新state
- 當每個state變化時图仓,都要手動去更新UI罐盔,如果忘記了就會導致UI顯示異常
- 導致代碼邏輯復雜
單向數(shù)據(jù)流
為了解決這個問題,Android 推出了ViewModel 和LivaData
通過ViewModel 我們可以從UI中提取state救崔,也可以定義更新UI state的事件惶看。
看下面的例子
class HelloCodelabViewModel: ViewModel() {
// LiveData holds state which is observed by the UI
// (state flows down from ViewModel)
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
// onNameChanged is an event we're defining that the UI can invoke
// (events flow up from UI)
fun onNameChanged(newName: String) {
_name.value = newName
}
}
class HelloCodeLabActivityWithViewModel : AppCompatActivity() {
private val helloViewModel by viewModels<HelloCodelabViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
binding.textInput.doAfterTextChanged {
helloViewModel.onNameChanged(it.toString())
}
helloViewModel.name.observe(this) { name ->
binding.helloText.text = "Hello, $name"
}
}
}
在這個例子中我們把state從Activity中轉移到了ViewModel中。state代表一個抽象的概念六孵,在ViewModel中 state通過LiveData來表現(xiàn)纬黎,也可以說是一種數(shù)據(jù)模型,只不過這個數(shù)據(jù)模型可以當做UI的狀態(tài)狸臣,用來更新UI莹桅。
其實整體上和上面的代碼差別不大,只是中間多了個ViewModel來中轉數(shù)據(jù)烛亦,其實也可以是其他的observeable诈泼,只不過谷歌給大家封裝好了,就叫ViewModel煤禽。
這樣既達成了解耦的成就铐达,也實現(xiàn)了我們所說的單向數(shù)據(jù)流。
這樣做有以下幾點好處
- 可測試-UI和state分離檬果,容易分別測試ViewModel和Activity
- state封裝-只能通過ViewModel來更改state瓮孙,可以避免局部state更新造成的bug
- UI一致性-state改變之后,所有觀察該state的UI會馬上更新
單向數(shù)據(jù)流就是指 符合事件向上傳遞而狀態(tài)向下傳遞的設計模式选脊。
例如 在ViewModel中杭抠,事件通過UI的調(diào)用向上傳遞給ViewModel,而狀態(tài)通過LiveData 的 setValue 向下傳遞恳啥。
就像剛才說的偏灿,單向數(shù)據(jù)流不僅僅是描述ViewModel的術語,任何屬于這種設計的能被稱之為單向數(shù)據(jù)流钝的。
Compose 中的state
在前面我們了解了什么是單向數(shù)據(jù)流模型翁垂,Compose也是遵循這個模型的一個UI框架,在Compose中推薦用MutableState 來管理狀態(tài)硝桩,而不是LiveData沿猜。
在Compose中通常這樣聲明state
val name by mutableStateOf("Compose")
這里用到了Kotlin的by關鍵字,name的類型碗脊,取決于mutableStateOf方法傳進去的類型啼肩,在這里其實就是String類型,通過by引用的對象,在取值和賦值的時候均會調(diào)用 代理類的getValue 和 setValue方法方法疟游,這兩個方法分別在State接口和 MutableState 中聲明呼畸。
State 中的getValue
inline operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value
MutableState 中的setValue
inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}
注意 這兩個方法是通過擴展方法實現(xiàn)的,所以要進行導入
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue
在這兩個接口中只是簡單的實現(xiàn)了一下代理方法颁虐,看著沒有任何邏輯蛮原。
其實在setValue中賦值的時候,最終會調(diào)用到 this.value的set方法另绩,在SnapshotMutableStateImpl中有實現(xiàn)儒陨。
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.writable(this) { this.value = value }
}
}
仿照Flutter 寫個Counter
@Composable
fun Counter() {
var count by mutableStateOf(1)
Button(onClick = {
count ++
}) {
Text(text = count.toString())
}
}
在這段代碼中,用代理模式笋籽,將int類型的count代理給了mutableStateOf 返回的state蹦漠,然后對count進行set操作的時候就會觸發(fā) recompose,然后對Counter進行重新繪制车海。
上面的代碼是有問題的笛园,Compose 不像Flutter,每個Widget都是一個類侍芝,state可以作為一個類的屬性研铆,但是在Compose中,每個組件其實就是一個函數(shù)州叠,在這個函數(shù)里棵红,state只作為了局部變量,當state變化的時候咧栗,會重新調(diào)用函數(shù)逆甜,導致state重新初始化,就失去了state的意義致板。官方推薦的做法是:
@Composable
fun Counter() {
val count = remember {
mutableStateOf(0)
}
Button(onClick = { count.value ++ }) {
Text(text = count.value.toString())
}
}
state 使用remember包裹起來交煞,remember方法會對state實例進行保存,每次recompose 的時候會把暫存的state取出來斟或。由于remember的返回值只能試val類型错敢,下面又要對count更新,所以不能用by缕粹,只能用“=” 得到的是個 State,所以下面要調(diào)用.value 來更新纸淮。
其實很多時候數(shù)據(jù)的更新并不是那么簡單平斩,比如網(wǎng)絡請求,之前那一套MVVM 在Compose框架里也完全適用咽块。我們可以將這個簡單的Counter改為mvvm架構的绘面。
首先定義ViewModel
class HelloViewModel : ViewModel() {
var cout by mutableStateOf(0)
private set
fun plus() {
cout ++
}
}
由于是從ViewModel中取數(shù)據(jù),所以state就沒必要使用remember進行包裹
修改Counter揭璃,將viewModel作為入?yún)魅?/p>
@Composable
fun Counter(viewModel: HelloViewModel) {
Button(onClick = { viewModel.plus() }) {
Text(text = viewModel.cout.toString())
}
}
State改變之后是怎么recompose的
通過打斷點可以看到Compose重組時的調(diào)用棧
倒數(shù)第二行 出現(xiàn)了 Choreographer晚凿,這個類和屏幕的刷新機制息息相關,Compose其實就是在收到屏幕的刷新信號時做的重組瘦馍。
Compose 如何確定重組范圍
Compose 在編譯期分析出會受到某 state 變化影響的代碼塊歼秽,并記錄其引用,當此 state 變化時情组,會根據(jù)引用找到這些代碼塊并標記為 Invalid 燥筷。在下一渲染幀到來之前 Compose 會觸發(fā) recomposition,并在重組過程中執(zhí)行 invalid 代碼塊院崇。
Invalid 代碼塊即編譯器找出的下次重組范圍肆氓。能夠被標記為 Invalid 的代碼必須是非 inline 且無返回值的 @Composalbe function/lambda,必須遵循 重組范圍最小化 原則底瓣。
為何是 非 inline 且無返回值(返回 Unit)谢揪?
對于 inline 函數(shù),由于在編譯期會在調(diào)用處中展開捐凭,因此無法在下次重組時找到合適的調(diào)用入口拨扶,只能共享調(diào)用方的重組范圍。
而對于有返回值的函數(shù)柑营,由于返回值的變化會影響調(diào)用方屈雄,因此無法單獨重組,而必須連同調(diào)用方一同參與重組官套,因此它不能作為入口被標記為 invalid酒奶。
范圍最小化原則
只有會受到 state 變化影響的代碼塊才會參與到重組,不依賴 state 的代碼不參與重組奶赔。
在這個例子中惋嚎,重組的只是Text,Button并沒有重組站刑,因為重組只發(fā)生在 state read的函數(shù)中另伍,write的函數(shù)并不在重組范圍內(nèi)。真正重組的起始不是Text方法绞旅,而是Button 后面的lambda摆尝。
如果我們稍微改寫一下
@Composable
fun Counter(viewModel: HelloViewModel) {
Log.d("Counter", "recompose")
Button(
onClick = { viewModel.plus() })
{
Text(text = "按鈕")
}
Text(text = viewModel.cout.toString(), color = Color.Black)
}
再次斷點
就再次論證了剛才的觀點,重組源頭不是Text因悲,而是包裹著Text的方法堕汞。
當我們嘗試用Column包裹一下
再次斷點,發(fā)現(xiàn)重組的源頭還是Counter方法晃琳,而不是Column讯检,那是因為
Column琐鲁、Row、Box 乃至 Layout 這種容器類 Composable 都是 inline 函數(shù)人灼,所以在運行時就相當于沒有Column這一層围段,所以如果想通過縮小重組范圍提高性能的話可以通過自定義Composable
@Composable
fun Wrapper(content: @Composable () -> Unit) {
Log.d(TAG, "Wrapper recomposing")
Box {
Log.d(TAG, "Box")
content()
}
}
總結:
此文只是簡單介紹了Compose中的state是什么,為什么要設計state投放,以及簡單的介紹了一下recompose的過程奈泪,并未說明recompose到底是怎么觸發(fā)的,以及怎么確定的recompose的作用域跪呈。本文大量參考了Compose CodeLab段磨,和Compose 技術原理,在下不才耗绿,如有疑惑之處請移步這兩篇文章苹支。