Android開發(fā)Compose框架使用開篇
Compose的誕生
在2019年的谷歌IO大會(huì)上,Compose作為Android新一代UI開發(fā)亮相,因?yàn)槁暶魇介_發(fā)越來越流行了,對標(biāo)IOS開發(fā)SwiftUi,Compose的立項(xiàng)也為Android開發(fā)新加了聲明式ui的開發(fā)選項(xiàng),在2021年7月1.0正式版本的誕生,也意味著Compose即將進(jìn)入生產(chǎn)環(huán)節(jié),國際app巨頭Twitter就首當(dāng)其沖,在新頁面上用上了Compose
Compose好處
與傳統(tǒng)的xml相比,Compose不僅吸收了其優(yōu)點(diǎn),摒棄了糟粕,還具有以下幾個(gè)優(yōu)點(diǎn)
| 聲明式 | 兼容性 | 跨平臺(tái) | 布局效率 |
|---|---|---|---|
| 不同于傳統(tǒng)的命令式,ui的刷新需要調(diào)用者主動(dòng)調(diào)用刷新方法,比如TextView需要特定的setText進(jìn)行文本變化,而compose在定義好聲明狀態(tài)后,由框架自主調(diào)用刷新,減少狀態(tài)不一致 | compose最低兼容到android api 21,不但可以在原來View體系中嫁接使用,也可以在compose中使用原來View體系的xml | 跨平臺(tái),目前支持macos等多個(gè)平臺(tái),跨平臺(tái)由Jetbrain團(tuán)隊(duì)在做,compose未來會(huì)實(shí)現(xiàn)ui多跨平臺(tái),同時(shí)也搭配邏輯跨平臺(tái)KMM項(xiàng)目(有關(guān)kmm的以后有機(jī)會(huì)可以再說說,比起說跨平臺(tái),更不如說是多平臺(tái),因?yàn)榫幾g出來的代碼是直接符合原平臺(tái)開發(fā)規(guī)范的,比如ios編譯出來的就是framework),未來實(shí)現(xiàn)ui跟邏輯都跨平臺(tái)也不在遙遠(yuǎn) | compose 是嚴(yán)格遵循LayoutNode的單次測量,不會(huì)出現(xiàn)View的多次測量導(dǎo)致的問題,在ui卡頓或者ui規(guī)范上,是非常重要的改進(jìn) |
因?yàn)镃ompose是Android團(tuán)隊(duì)與JetBrain在推,國內(nèi)外的學(xué)習(xí)熱情都挺好,目前國內(nèi)也有不少大廠進(jìn)行了嘗試階段,比如字節(jié)。
Compose 架構(gòu)
說了這么多,那么Compose是怎么做到在原有View體系做到兼容并改善的呢?
我們從一個(gè)例子出發(fā):
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
xxx組件
}
}
}
我們可以看到,Compose在Activity中,用了setContent方法代替了原有的setContentView方法,那么setContent做了什么呢?
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
可以看到,android.R.id.content的第一個(gè)孩子被替換成了ComposeView,而這個(gè)ComposeView,就是Compose環(huán)境的提供者,在這里面,Compose將剔除原本View體系的測量邏輯,從而采用自己的測量架構(gòu)。如下圖包餃子架構(gòu)所示:

值得注意的是ComposeView是繼承于AbstractComposeView(他定義了compose環(huán)境的規(guī)范),還有就是android中是多window架構(gòu)的,所以針對Dialog這種,也有特別的環(huán)境實(shí)現(xiàn)類。針對PopupWindow這種共用window的組件,也同樣提供了compose環(huán)境實(shí)現(xiàn)類

可以看到架構(gòu)圖中,AbstractComposeView其實(shí)也是繼承于ViewGroup的。所以準(zhǔn)確來說,Compose并沒有完全脫離AndroidView體系,而是在這之上建立起了中間層,這就印證了一句老話,沒有什么架構(gòu)是不能解決的,如果有,那就加個(gè)中間層!而這個(gè)中間層,提供了全新的設(shè)計(jì),從而讓android得以脫胎換骨到一個(gè)新架構(gòu)!
@Composable的背后
說到Compose,那么肯定離不開Compsoable的介紹,我們肯定會(huì)有一個(gè)疑問,為什么一個(gè)函數(shù)加上了Composable注解,就變成了一個(gè)可見的視圖了呢?比如
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
我們進(jìn)行反編譯后
public static final void Greeting(String name, Composer $composer, int $changed) {
Intrinsics.checkNotNullParameter(name, "name");
Composer $composer2 = $composer.startRestartGroup(-154424256);
ComposerKt.sourceInformation($composer2, "C(Greeting)46@1589L27:MainActivity.kt#8m9ksz");
int $dirty = $changed;
if (($changed & 14) == 0) {
$dirty |= $composer2.changed(name) ? 4 : 2;
}
if ((($dirty & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4567String$0$str$arg0$callText$funGreeting() + name + LiveLiterals$MainActivityKt.INSTANCE.m4584String$2$str$arg0$callText$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
} else {
$composer2.skipToGroupEnd();
}
ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
if (endRestartGroup == null) {
return;
}
endRestartGroup.updateScope(new MainActivityKt$Greeting$1(name, $changed));
}
可以看到,實(shí)際上,編譯器為我們的帶有@Composable的Greeting方法添加了兩個(gè)參數(shù),
Composer $composer, int $changed
Composer就是我們真的compose執(zhí)行時(shí)的操作者,changed就是一個(gè)參與是否重組的標(biāo)識(shí)之一,這種通過注解在編譯時(shí)生成對應(yīng)參數(shù)的方案,在Coroutine里面也用到,當(dāng)然我們也可以通過ASM等方法去編譯時(shí)判斷注解并更改相應(yīng)的函數(shù)方法,只不過這部分由compose編譯器幫我們做了。
我們重點(diǎn)關(guān)注一下composer.startRestartGroup這個(gè)方法
override fun startRestartGroup(key: Int): Composer {
start(key, null, false, null)
addRecomposeScope()
return this
}
可以看到傳入了一個(gè)key為Int類型的標(biāo)識(shí),所有Composable函數(shù)區(qū)域會(huì)通過特定key去判斷重組的范圍以及當(dāng)前范圍是否進(jìn)行更新(范圍就是startRestartGroup - endRestartGroup 之間的狀態(tài)),當(dāng)然一個(gè)Composable函數(shù)里面可能存在多個(gè)重組范圍,我們還是拿上面的Greeting函數(shù)做個(gè)例子,不過這次有點(diǎn)變化
@Composable
fun Greeting(name: String) {
val state by remember {
mutableStateOf(true)
}
if (state){
Text(text = "true")
}else{
Text(text = "false")
}
}
此時(shí)反編譯后就會(huì)多一些代碼
public static final void Greeting(String name, Composer $composer, int $changed) {
Object value$iv$iv;
Intrinsics.checkNotNullParameter(name, "name");
Composer $composer2 = $composer.startRestartGroup(-154424404);
ComposerKt.sourceInformation($composer2, "C(Greeting)43@1455L45:MainActivity.kt#8m9ksz");
if (($changed & 1) == 0 && $composer2.getSkipping()) {
$composer2.skipToGroupEnd();
} else {
$composer2.startReplaceableGroup(-492369756);
ComposerKt.sourceInformation($composer2, "C(remember):Composables.kt#9igjgp");
Object it$iv$iv = $composer2.rememberedValue();
if (it$iv$iv == Composer.Companion.getEmpty()) {
value$iv$iv = SnapshotStateKt__SnapshotStateKt.mutableStateOf$default(Boolean.valueOf(LiveLiterals$MainActivityKt.INSTANCE.m4554x2b38c863()), null, 2, null);
$composer2.updateRememberedValue(value$iv$iv);
} else {
value$iv$iv = it$iv$iv;
}
$composer2.endReplaceableGroup();
MutableState state$delegate = (MutableState) value$iv$iv;
// 關(guān)注這里
if (m4607Greeting$lambda1(state$delegate)) {
$composer2.startReplaceableGroup(-154424297);
ComposerKt.sourceInformation($composer2, "47@1525L19");
TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4597String$arg0$callText$branch$if$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
$composer2.endReplaceableGroup();
} else {
$composer2.startReplaceableGroup(-154424258);
ComposerKt.sourceInformation($composer2, "49@1564L20");
TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4598String$arg0$callText$else$if$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
$composer2.endReplaceableGroup();
}
}
ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
if (endRestartGroup == null) {
return;
}
endRestartGroup.updateScope(new MainActivityKt$Greeting$1(name, $changed));
}
可以看到,我們狀態(tài)state變量改變的時(shí)候,compose會(huì)在if里面生成先startReplaceableGroup,代表著這個(gè)范圍是可以被替換的,也就是說當(dāng)if成立的時(shí)候會(huì)生成一個(gè)composition節(jié)點(diǎn),此時(shí)如果state變成了false,那么這個(gè)if里面的范圍group就會(huì)被移除,此時(shí)父group(即在之前調(diào)用startgroup的范圍)以替換的方式載入新的else,子group,這就是startReplaceableGroup的含義。
總之,我們可以注意到,key就是一個(gè)非常關(guān)鍵的點(diǎn),就是讓我們compose識(shí)別到哪些范圍能夠進(jìn)行重組,哪些不能!在if else語句中,如果我們依賴一個(gè)state,就會(huì)在相應(yīng)的if語句里面插入一個(gè)新的子范圍group,這就是compose架構(gòu)中的的智能重組!
智能重組真的那么智能嗎
上面我們提到了智能重組的這個(gè)概念,那么智能重組真的如字面所說,那么“智能”嗎?我們很容易想到,既然compose能在if else這種有作用域的關(guān)鍵字上加上子group,如果是list這種呢?存在著循環(huán)語句的條件呢?就算在循環(huán)語句里面加上子group,并不能滿足我們想要的需求呀!舉個(gè)??
@Composable
fun CustomList(list: List<CustromData>) {
Column {
for (item in list){
CustromView(item)
}
}
}
我們想要for循環(huán)里面的list,如果list改變了數(shù)據(jù),我們希望compose只重組改變部分的數(shù)據(jù),而不是全部list里面的CustromView,那么compose能做到嗎?compose:老子不干了! 是的!做不到!因?yàn)橐呀?jīng)生成的CustromView的key只能依靠著當(dāng)前的數(shù)據(jù)決定了,比如list的index,如果下次index更改,那么key就會(huì)因?yàn)閕ndex的不一致,導(dǎo)致了list中每一個(gè)生成的CustromView再次重組
CustomList部分反編譯代碼
ColumnScopeInstance columnScopeInstance = ColumnScopeInstance.INSTANCE;
int $changed2 = ((0 >> 6) & 112) | 6;
// 只加了一個(gè)startReplaceableGroup
$composer2.startReplaceableGroup(-1487261693);
ComposerKt.sourceInformation($composer2, "C*52@1739L17:MainActivity.kt#8m9ksz");
if ((($changed2 & 81) ^ 16) != 0 || !$composer2.getSkipping()) {
Iterator<CustromData> it = list.iterator();
// 循環(huán)沒辦法加入子group
while (it.hasNext()) {
Iterator<CustromData> it2 = it;
CustromData item = it.next();
CustromView(item, $composer2, 0);
$changed$iv = $changed$iv;
it = it2;
}
} else {
$composer2.skipToGroupEnd();
}
$composer2.endReplaceableGroup();
public static final void CustromView(CustromData data, Composer $composer, int $changed) {
Intrinsics.checkNotNullParameter(data, "data");
Composer $composer2 = $composer.startRestartGroup(-201540877);
ComposerKt.sourceInformation($composer2, "C(CustromView)59@1825L23:MainActivity.kt#8m9ksz");
int $dirty = $changed;
if (($changed & 14) == 0) {
$dirty |= $composer2.changed(data) ? 4 : 2;
}
if ((($dirty & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
TextKt.m1219TextfLXpl1I(data.getTest2(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
} else {
$composer2.skipToGroupEnd();
}
ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
if (endRestartGroup == null) {
return;
}
endRestartGroup.updateScope(new MainActivityKt$CustromView$1(data, $changed));
}
所以一旦出現(xiàn)增刪改查list的情況,那么不好意思,compose也就只能把group里面的list再重組一次,如圖:

這也是為什么list系列的compose函數(shù),比如LazyRow等會(huì)被詬病為有性能問題。那么compose就沒有相關(guān)解決方法嗎?嗯!官方肯定準(zhǔn)備了,就是我們可以主動(dòng)設(shè)置當(dāng)前composable函數(shù)的key!從而避免額外的重組!
@Composable
inline fun <T> key(
@Suppress("UNUSED_PARAMETER")
vararg keys: Any?,
block: @Composable () -> T
) = block()
回到我們自定義的list,就可以這樣使用
for (item in list){
key(keys = arrayOf(item.test) ) {
CustromView(item)
}
}
當(dāng)然,我們的LazyList系列也可以直接指定,在item函數(shù)及其他items函數(shù)中直接使用
interface LazyListScope {
fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
...
最后
好的!我們的神奇的compose第一篇架構(gòu)片就到此結(jié)束啦!接下來也會(huì)不定期更新其他的compose篇章,讓我們擁抱全新的compose吧!更多關(guān)于Android開發(fā)Compose框架的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Retrofit下載文件并實(shí)現(xiàn)進(jìn)度監(jiān)聽的示例
這篇文章主要介紹了使用Retrofit下載文件并實(shí)現(xiàn)進(jìn)度監(jiān)聽的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
Android中使用TextView實(shí)現(xiàn)圖文混排的方法
向TextView或EditText中添加圖像比直接添加文本復(fù)雜一點(diǎn)點(diǎn),需要用到<img>標(biāo)簽。接下來通過本文給大家介紹Android中使用TextView實(shí)現(xiàn)圖文混排的方法,希望對大家有所幫助2016-02-02
Android?NDK入門初識(shí)(組件結(jié)構(gòu)開發(fā)流程)
這篇文章主要為大家介紹了Android?NDK入門之初識(shí)組件結(jié)構(gòu)開發(fā)流程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
Android ListView實(shí)現(xiàn)上拉加載下拉刷新和滑動(dòng)刪除功能
這篇文章主要為大家詳細(xì)介紹了Android ListView實(shí)現(xiàn)上拉加載下拉刷新和滑動(dòng)刪除功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
Android開發(fā)之圖形圖像與動(dòng)畫(四)AnimationListener簡介
就像Button控件有監(jiān)聽器一樣,動(dòng)畫效果也有監(jiān)聽器,只需要實(shí)現(xiàn)AnimationListener就可以實(shí)現(xiàn)對動(dòng)畫效果的監(jiān)聽,感興趣的朋友可以了解下啊,希望本文對你有所幫助2013-01-01
Android Notification實(shí)現(xiàn)動(dòng)態(tài)顯示通話時(shí)間
這篇文章主要為大家詳細(xì)介紹了Android Notification實(shí)現(xiàn)動(dòng)態(tài)顯示通話時(shí)間,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
Android開發(fā)之創(chuàng)建可點(diǎn)擊的Button實(shí)現(xiàn)方法

