Jetpack-Compose 學習筆記(一)—— Compose 初探

歷時兩年,Android 團隊推出了全新的原生 Android 界面 UI 庫——Compose。當然,Compose 也是屬于 Jetpack 工具庫中的一部分,官方宣稱可以簡化并加快 Android 上的界面開發(fā)行冰,可以用更少的代碼去快速打造生動而精彩的應用溺蕉。1.0 版本就在上個月底剛剛發(fā)布,而且可以在生產環(huán)境中使用悼做!不管咋樣疯特,先上手看一看!

1. 上手成本如何肛走?

個人感覺漓雅,還行,有一定的學習成本朽色。前提條件邻吞,對 Kotlin 語言熟悉,因為 Compose 都是用 Kotlin 語言開發(fā)實現的葫男,對其他的 Jetpack 庫熟悉就更好了抱冷。

Compose 可以和現有的工程項目進行互操作。比如梢褐,我們可以將 Compose UI 放到現有布局的 View 中旺遮,也可以將 View 放到 Compose UI 中。

作為 Jetpack 工具庫的一部分盈咳,Compose 當然也可以十分方便地與 LiveDada耿眉、ViewModel、Paging 等工具一起整合猪贪,從而提高編碼效率跷敬。

Compose 也提供了 Material Design 組件和主題的實現讯私,同時還有簡明的動畫 API 可以讓應用更加靈動热押,體驗更好。

2. 官方廣告概述

Google 畢竟憋了兩年斤寇,怎么說也得有兩把刷子的桶癣。Compose 是 Google 新推出的適用于 Android 的新式聲明性界面工具包。個人理解的聲明性的意思是:UI 的控件只需要我們一開始的時候聲明創(chuàng)建出來娘锁,綁定了數據就可以了牙寞,后續(xù)的更新可以全部交給 Compose 處理。

Google 是考慮到現在的應用展示的絕大多數不是靜態(tài)數據莫秆,更多的是會實時更新的间雀。而現有的 xml 界面,更新比較復雜繁瑣镊屎,很容易出現同步錯誤惹挟。并且軟件維護的復雜性還會隨著需要更新的視圖數量而增長,為了解決這一問題缝驳,Google 才想完全舍棄原有的用 xml 寫視圖的方案连锯,重新開發(fā)出 Compose 這一整套的解決方案归苍。

Compose 首先會生成整個屏幕,然后僅僅執(zhí)行必要的更改运怖。它是將 State 狀態(tài)轉化成 UI 界面拼弃,并且會智能地跳過那些數據沒有發(fā)生改變的控件,重新生成已經發(fā)生改變的控件摇展,這一過程稱之為重組(recomposition)吻氧。此外,Compose 布局模型不允許多次測量咏连,最多進行兩次測量就可算出各組件的尺寸医男。

3. 環(huán)境搭建

對 IDE 版本有要求,需要下載最新版的 Android Studio —— Android Studio Arctic Fox捻勉,目前是 2020 3.1 版本镀梭。這個版本在“新建項目”中支持選擇 Compose 模板,并且有即時預覽 Compose 界面等功能踱启。

一般情況下报账,對于這種新的技術,我們都會先在主項目中的非核心功能進行實踐埠偿,慢慢摸索透罢,等到坑踩得差不多了,才會考慮將之前老的工程代碼用新的方法重構冠蒋。所以羽圃,Compose 也支持添加到現有的項目中進行使用。

3.1 配置 Kotlin 和 Gradle

需要確保項目中使用的 Kotlin 版本在 1.5.10 及以上抖剿。

還需要將應用的最低 API 級別設置為 21 或更高朽寞,即 Android 5.0 版本及以上。另外還需將 app 目錄下的 gradle 文件中啟用 Jetpack Compose斩郎,并設置 Kotlin 編譯器插件的版本脑融。

android {
    defaultConfig {
        ...
        minSdkVersion 21    // SDK 版本最低為 21
    }

    buildFeatures {
        // Enables Jetpack Compose for this module
        compose true    // 開啟 Compose
    }
    ...

    // Set both the Java and Kotlin compilers to target Java 8.
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }

    composeOptions {
        // 編譯器插件版本設置
        kotlinCompilerExtensionVersion '1.0.0-rc02'
    }
}

3.2 添加工具包依賴項

官方文檔上需要添加的依賴如下:

dependencies {
    implementation 'androidx.compose.ui:ui:1.0.0-rc02'
    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.0.0-rc02'
    // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
    implementation 'androidx.compose.foundation:foundation:1.0.0-rc02'
    // Material Design
    implementation 'androidx.compose.material:material:1.0.0-rc02'
    // Material design icons
    implementation 'androidx.compose.material:material-icons-core:1.0.0-rc02'
    implementation 'androidx.compose.material:material-icons-extended:1.0.0-rc02'
    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.3.0-rc02'
    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07'
    // Integration with observables
    implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-rc02'
    implementation 'androidx.compose.runtime:runtime-rxjava2:1.0.0-rc02'

    // UI Tests
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.0.0-rc02'
}

其實如果只是想上手看看效果,沒必要添加 Integration with activities缩宜、ViewModels肘迎、observables 這些庫。

4. 簡單上手

Compose 核心內容就是可組合的函數锻煌,如同它的英文名稱一樣妓布,將 UI 拆解成一個個可組合在一起的 Composable 函數,方便維護與復用宋梧。但是匣沼,可組合函數只能在其他的可組合函數的范圍內調用。要使函數成為可組合函數乃秀,只需在該函數上方添加 @Composable 注解即可肛著。其實可以直接把被 @Composable 注解的函數看成是一個 View圆兵。

@Composable 注解可告訴 Compose 編譯器:此函數旨在將數據轉換為界面。并且生成界面的 Compose 函數不需要返回任何內容枢贿,因為它們描述的是所需的屏幕狀態(tài)殉农,而不是構造界面的組件。

還有一個很強大的功能是局荚,Compose 是支持在 IDE 中預覽可組合函數的超凳,只需要在 Composable 函數上再添加一個 @Preview 注解就可以了,限制條件是 @Preview 注解只能修飾一個無參的函數耀态,所以轮傍,如果你要預覽,就得保證你預覽的函數無參首装,或者再用一個無參函數包起來:

// code 1
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview
@Composable
fun WrapperView() {
    Greeting("hahahaha")
}

就這樣子创夜,你就可以在 IDE 中看到預覽的效果了,甚至都沒有執(zhí)行入口仙逻!
Compose 的 Hello World 代碼也比較簡單驰吓,只需要在 setContent 方法里添加需要展示的 Composable 函數即可:

// code 2
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {    // 設置顯示內容,相當于 setContentView
            Greeting("Hello World!")
        }
    }
}

4.1 Compose 布局初探

如果寫過 Flutter系奉,那么你會發(fā)現檬贰,Compose 的布局與 Flutter 類似。Column 可以將元素從上到下進行排列缺亮,類似于 LinearLayout 布局的 oritation 設置為 vertical翁涤。Row 就是將元素從左到右進行排列,類似于 LinearLayout 布局的 oritation 設置為 horizonal萌踱。還有 Box 堆疊布局葵礼,類似于 FrameLayout 布局,這里不再展開虫蝶。下面是 Column 布局的一個簡單例子章咧,代碼顯示的效果如代碼下方的圖片所示:

// code 3
@Composable
fun NewStory() {
    Column(
        // 許多對象都有這個 Modifier 屬性,這個屬性非常重要能真,這里是設置了 padding
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.header),
            contentDescription = null
        )
        Text("今天天氣好")
        Text(text = "鄭州")
        Text(text = "July 2021")
    }
}
image.png

這里要說下 contentDescription 屬性,官方教學文檔的原文是:

Note: You also need to provide a contentDescription for the image. The description is used for accessibility. However, in a case like this where the image is purely decorative, it's appropriate to set the description to null, as we do here.

意思是:我們需要為 image 提供一個 contentDescription 屬性扰柠。這個屬性用于可訪問性(=粉铐。=?)卤档。然鵝蝙泼,如果這個 image 純粹只是裝飾作用,那么也可以像我們在這里設置的一樣劝枣,設為 null汤踏。

懵逼臉织鲸。然后去源碼看看這個到底是個啥?

*@param contentDescription text used by accessibility services to describe what this image
*represents. This should always be provided unless this image is used for decorative purposes,
*and does not represent a meaningful action that a user can take. This text should be
*localized, such as by using [androidx.compose.ui.res.stringResource] or similar

意思是:" 這個屬性是可訪問性服務用于描述此圖片代表的是什么溪胶。這個屬性的信息應該都要提供搂擦,除非此圖只是用于裝飾的目的,或者并沒有表示用戶有特殊意義的操作哗脖。此外瀑踢,屬性的信息文本應該存放在本地資源中,如 res 目錄下的 string 或類似的地方才避。"

額橱夭。。桑逝。還是有點懵棘劣,去網上看了下 ImageView 中的 contentDescription 屬性,好像是為了方便視力有障礙的人群所設置的楞遏。反正絕大多數情況下可以忽略呈础,如有實際用途,歡迎交流討論橱健。

此外而钞,Compose 的布局還有很靈活的,還記得在 LinearLayout 布局中可以設置 weight 來控制填充父布局嗎拘荡?在 Compose 也有類似的用法臼节,直接上代碼吧~

// code 4
@Composable
fun MyScreenContent(names: List<String> = listOf("Android","there")) {
    Column(modifier = Modifier.fillMaxHeight()) {    // 類似于 match_parent
        Column(modifier = Modifier.weight(1f)) {    // 占滿父布局剩余的高度空間
            for (name in names) {
                Text(text = name)
                Divider(color = Color.Black)
            }
        }
        Button(onClick = { }) {
            Text(text = "這是第二個 Button")
        }
    }
}

這個布局就是可以將 Button 放在父布局的底部位置,然后父布局剩余空間都會被內層的 Column 布局占滿珊皿。

當然网缝,Compose 可以輕松地遵循 Material Design 原則,因為可以直接在任何 Composable 函數外部用 MaterialTheme {} 包裹起來蟋定,這就可以使用 MaterialTheme 的屬性了粉臊。包括字體樣式、色值等驶兜。這里代碼都比較簡單扼仲,不再贅述。代碼示例:https://gitee.com/xiuzhizhu/ComposeDemo.git

4.2 Compose 可構建容器函數

Compose 支持構建容器函數抄淑,容器函數類似于 Theme 主題屠凶,可以將一些基礎的設置信息放在容器函數中,這樣放入這個容器函數中的 Composable 函數就會根據設置的信息進行繪制肆资、渲染矗愧。舉個簡單的栗子:

// code 5
// 聲明一個容器函數
@Composable
fun MyApp(content: @Composable () -> Unit) {
    MaterialTheme() {
        Surface(color = Color.Yellow, modifier = Modifier.padding(10.dp)) {
            content()
        }
    }
}

// 實際運用
MyApp {
    Text(text = "被容器函數所修飾的 Text")
}

所有放入 MyApp 容器中的 Composable 函數都會帶上容器函數中設置的屬性。這樣可提高代碼復用性和可讀性郑原。

4.3 Compose 狀態(tài)初探

Compose 的核心內容就是響應 state 狀態(tài)的改變唉韭。Compose 通過調用 Composable 函數可以將 data 數據展示在 UI 上夜涕,Compose 本身也提供了工具去觀察 data 數據的變化,從而可以自動地回調展示 UI属愤,這一過程官方稱為重組女器,前面也有說到〈核可以理解為更新 UI晓避。

在 Composable 函數內部我們可以使用 mutableStateOf 方法去添加一個可變的 state,為了避免每次重組都會出現不同的狀態(tài)只壳,所以可以用 remember 記住這個可變狀態(tài)俏拱。

// code 6
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0)}  // 初始化為 0
    Button(onClick = { count.value++ }) {
        Text(text = "已點擊了 ${count.value} 次!")
    }
}

這樣每次點擊 Button吼句,都會更新點擊的次數值锅必。

4.4 Compose 列表初探

列表布局使用頻率還是比較高的,像 ListView 和 RecyclerView 都是耳熟能詳的用于展示列表的 View 控件惕艳。那么 LazyColumn 就相當于 Compose 中的 RecyclerView搞隐,用于展示可滑動的長列表。它提供了 items API 用于展示簡單的列表布局远搪。

// code 7
@Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(items = names) { name ->
            Greeting(name = name)
            Divider(color = Color.Black)    // 分割線類對象
        }
    }
}

然而劣纲,LazyColumn 不會像 RecyclerView 一樣緩存列表中的布局,而是在滾動瀏覽它時谁鳍,它會渲染新的列表 View癞季,并沒有回收機制,但是相比于實例化 Android View倘潜,渲染 Composable UI 組件效率更高绷柒。

4.5 Compose 自定義主題

Compose 中有自帶的一些主題,比如 MaterialTheme涮因,被這些 Theme 包裹废睦,就可以呈現出這些 Theme 所設置的屬性了。當然也可以單獨將這些 Theme 中某些屬性拿出來养泡,比如字體嗜湃。然后就可以使用該主題下設置的各種字體樣式了,同樣的還有色值:

// code 8
@Composable
fun Greeting(name: String) {
    val greetingTypography = MaterialTheme.typography // 獲取 MaterialTheme 字體樣式
    val greetingColors = MaterialTheme.colors // 獲取 MaterialTheme 色值
    Text(text = "Hello $name",
        color = greetingColors.onBackground,    // 使用 MaterialTheme 的 onBackground 色值
        style = greetingTypography.body2)
}

還可以調用 copy 方法復制某主題的樣式瓤荔,然后在此基礎上改寫自己的一些樣式屬性:

// code 9
@Composable
fun Greeting(name: String) {
    val customStyle = MaterialTheme.typography.h5.copy(color = Color.Green)
    Text(text = "Hello $name",
        style = customStyle)
}

如何自定義一個自己的 Theme净蚤?其實也很簡單,下面是一個例子:

// code 10
// 主要方法输硝,被此方法包裹的 Composable 函數都會被設置為自定義主題
@Composable
fun CustomTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),  // 默認根據系統(tǒng)來設置是否為暗夜模式
    content: @Composable () -> Unit  // 被傳入的 Composable 函數
){
    val colors = if (darkTheme) {
        DarkColors
    } else {
        LightColors
    }

    MaterialTheme(colors = colors) { // 將設置好的色值傳入
        content()
    }
}

private val DarkColors = darkColors( // 暗夜模式下的色值
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

private val LightColors = lightColors( // 白天模式下的色值
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

是不是覺得簡單?是的程梦,在 Compose 中自定義一個主題就是這么簡單点把。

5. 編程思想

再來說一說官方文檔里提到的 Compose 的編程思想吧橘荠。它采用的是聲明性界面模型,該模型工作原理是先從開始生成整個屏幕郎逃,然后僅執(zhí)行必要的更改哥童。重組就是使用新數據再次調用 Composable 函數,從而進行更新的褒翰。當然重組過程僅調用可能已更改的函數或 lambda贮懈,而跳過其余函數或 lambda,所以 Compose 可以高效地重組优训。

其中朵你,官方建議在更新時,不要依賴于執(zhí)行 Composable 函數所產生的附帶效應揣非,因為可能會跳過函數的重組抡医。附帶效應指的是對應用的其余可見部分的任何更改。危險的附帶效應有1)寫入共享對象的屬性(這個應該是怕有其他的邏輯正在讀取共享對象屬性來更新 UI 等早敬,使得 UI 變化不準確忌傻。);2)更新 ViewModel 中的可觀察項(原理同1))搞监;3)更新 SharedPreference(原理同1))水孩。(不是很理解,可能日后真正使用后會更有體會吧~歡迎一起討論)

Composable 函數可能會像每一幀一樣頻繁地重新執(zhí)行琐驴,例如在呈現動畫時俘种。Composable 函數應快速執(zhí)行,避免在播放動畫期間出現卡頓棍矛。如果需要執(zhí)行耗時操作安疗,如從 SharedPreference 中讀取數據,那么建議在后臺協(xié)程中處理够委,然后使用回調傳遞當前值來觸發(fā)更新荐类。還有幾個值得注意的 Tips:

1、Composable 函數可以按任何順序執(zhí)行
如果某個 Composable 函數中包含有幾個 Composable 函數茁帽,那么這些 Composable 函數可能按任何順序運行玉罐,Compose 會識別出哪些界面元素的優(yōu)先級高于其他的界面元素,從而優(yōu)先繪制這些元素潘拨。

2吊输、 Composable 函數可以并行運行
Compose 可以通過并行運行 Composable 函數來優(yōu)化重組。所以铁追,Compose 可以利用多個核心季蚂,并以較低的優(yōu)先級運行 Composable 函數。因此,Composable 函數可能會在后臺線程池中執(zhí)行扭屁。調用某個 Composable 函數時算谈,調用可能發(fā)生在與調用方不同的線程中。

3料滥、重組會跳過盡可能多的內容
Compose 會盡力只重組需要更新的部分然眼,每個 Composable 函數和 lambda 又可以自行重組更新。Compose 若在一次重組時發(fā)現參數又更新了葵腹,則會取消當前的重組高每,并用新參數重新開始。

官方推薦將 Composable 函數寫在頂級函數践宴,方便以后復用。

Compose 博大精深浴井,許多概念性的東西并沒有理解得很透徹晒骇,還需慢慢實踐才能得出真知啊!歡迎留言交流瘤缩,互相學習剥啤!

ps. 簡單的 Demo:https://gitee.com/xiuzhizhu/ComposeDemo.git
官方教程網站:https://developer.android.google.cn/courses/pathways/compose

參考文獻

  1. Jetpack Compose 1.0 正式發(fā)布!打造原生 UI 的 Android 現代工具包
  2. Jetpack Compose 基礎知識
  3. Compose 編程思想

尾巴:這是 Compose 系列筆記的首篇不脯,相信細心的同學也發(fā)現了府怯,這篇筆記是根據官方教程網站上的學習路線進行記錄學習的。自己在學習的過程中防楷,遇到不明白的地方也會查閱大量的資料進行補充牺丙,喜歡的話,歡迎分享轉發(fā)加關注~

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末复局,一起剝皮案震驚了整個濱河市冲簿,隨后出現的幾起案子,更是在濱河造成了極大的恐慌亿昏,老刑警劉巖峦剔,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異角钩,居然都是意外死亡吝沫,警方通過查閱死者的電腦和手機呻澜,發(fā)現死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來野舶,“玉大人易迹,你說我怎么就攤上這事宰衙∑降溃” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵供炼,是天一觀的道長一屋。 經常有香客問我,道長袋哼,這世上最難降的妖魔是什么冀墨? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮涛贯,結果婚禮上诽嘉,老公的妹妹穿的比我還像新娘。我一直安慰自己弟翘,他們只是感情好虫腋,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著稀余,像睡著了一般悦冀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上睛琳,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天盒蟆,我揣著相機與錄音,去河邊找鬼师骗。 笑死历等,一個胖子當著我的面吹牛,可吹牛的內容都是我干的辟癌。 我是一名探鬼主播寒屯,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼愿待!你這毒婦竟也來了浩螺?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤仍侥,失蹤者是張志新(化名)和其女友劉穎要出,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體农渊,經...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡患蹂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年或颊,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片传于。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡囱挑,死狀恐怖,靈堂內的尸體忽然破棺而出沼溜,到底是詐尸還是另有隱情平挑,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布系草,位于F島的核電站通熄,受9級特大地震影響,放射性物質發(fā)生泄漏找都。R本人自食惡果不足惜唇辨,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望能耻。 院中可真熱鬧赏枚,春花似錦、人聲如沸晓猛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鞍帝。三九已至诫睬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間帕涌,已是汗流浹背摄凡。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蚓曼,地道東北人亲澡。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像纫版,于是被迫代替她去往敵國和親晓殊。 傳聞我的和親對象是個殘疾皇子韧衣,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內容