冒失的前言
之前寫了第一篇關于 Compose 初探的文章,大概說了下 Compose 的前世今生,本篇文章是基于上一篇文章寫的,閱讀之前最好先閱讀下:初探 Compose 版本的玩安卓,
上一篇文章由于篇幅的原因很多東西沒有介紹, Compose 非常大,也絕對不是一篇文章能寫完的,咱們慢慢來,這篇文章打算詳細介紹下 Compose 的導航—— Navigation ,還有 Compose 的狀態管理—— State ,然后是Compose 和 Android View 的相互操作性,這些都是 Compose 的核心內容,然后文章最后還會寫一下玩安卓專案的一些實作,畢竟實戰還是很重要的,如果說這些知識點您都會,只是不知道如何真正使用的話,直接移步最下面的實戰就行,那么咱們現在就開始吧!
在寫之前還是放一下 Github 地址吧,別忘了是 main 分支哦:
Github 地址:github.com/zhujiang521…
Navigation
上一篇文章中本來也想簡單寫一下的,但是想了想 Navigation 內容很多,第一篇文章還是簡單一點好,要不閱讀起來就會有些困難了,所以放在了本篇中嘮叨,
添加依賴
上一篇中其實已經添加過了,但還是再寫一下吧:
dependencies {
implementation "androidx.navigation:navigation-compose:1.0.0-beta01"
}
使用入門
NavController 是 Navigation 組件的中心 API,此 API 是有狀態的,可以跟蹤組成應用螢屏的可組合項的回傳堆疊以及每個螢屏的狀態,
可以通過在可組合項中使用 rememberNavController() 方法來創建 NavController:
val navController = rememberNavController()
大家應該在可組合項層次結構中的適當位置創建 NavController,使所有需要參考它的可組合項都可以訪問它,
創建 NavHost
每個 NavController 都必須與一個 NavHost 可組合項相關聯,NavHost 將 NavController 與導航圖相關聯, NavController 用于指定你需要進行導航的頁面,當你在頁面之間進行導航時,NavHost 的內容會自動進行重組(大意就是重繪 UI),導航圖中的每個頁面都與一個路線相關聯,
NavHost(navController, startDestination = "one") {
composable("one") { Profile(...) }
composable("two") { FriendsList(...) }
...
}
我當時看這塊的時候就有點懵逼,這是啥???跳轉為什么要寫成這玩意?后來看懂了也還好,和路由是類似的,都是通過字串來定義指向可組合項的路徑,并且是唯一的,
跳轉
上面已經定義好了每個 Composable 的路徑,那么該怎么跳轉呢?很簡單,通過 NavController 就可以了:
fun One(navController: NavController) {
...
Button(onClick = { navController.navigate("two") }) {
Text(text = "One")
}
...
}
這里需要注意的是:應該只在回呼中呼叫 navigate(),盡量別在可組合項本身中呼叫它,以避免每次重組時都呼叫 navigate(),
默認情況下,navigate() 會將新頁面添加到回傳堆疊中,可以通過向 navigate() 呼叫附加其他導航選項來修改 navigate 的行為:
navController.navigate(“one”) {
popUpTo("home")
}
上面代碼的意思是將所有內容從后臺堆疊彈出到 home,并且導航到 one 頁面,
navController.navigate("one") {
popUpTo("home") { inclusive = true }
}
而這段代碼的意思是彈出所有包含 home 頁面的資訊,導航到 one 之前的后堆疊,
navController.navigate("search") {
launchSingleTop = true
}
最后這段代碼的意思是僅當我們沒有打開時 “search” 頁面時,才會導航到 “search” 頁面,避免在回傳堆疊,
傳遞引數的跳轉
Navigation Compose 還支持在可組合項頁面之間傳遞引數,為此,需要向路線中添加引數占位符:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
看著是不是似曾相識?哈哈哈,用過 Retrofit 吧?是不是很像?或者寫過后臺代碼嗎?和 SpringMVC 是不是寫法也有點類似?
默認情況下,所有引數都會被決議為字串,但可使用 arguments 引數來設定 type,以指定其他型別:
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
從 composable() 函式的 lambda 中提供的 NavBackStackEntry 中可以將 NavArguments 給提取出來:
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
如果要將引數傳遞到頁面,需要在 navigate 呼叫中添加路線值而不是占位符:
navController.navigate("profile/user1234")
這塊怎么理解呢,你都具體呼叫了跳轉了還傳什么占位符,直接傳你需要傳的引數啊,什么?不知道各種型別的引數該怎么傳?別著急,馬上給你說,
Navigation 庫支持以下引數型別:
| 型別 | app:argType 語法 | 是否支持默認值? | 是否支持 null 值? |
|---|---|---|---|
| 整數 | app:argType=“integer” | 是 | 否 |
| 浮點數 | app:argType=“float” | 是 | 否 |
| 長整數 | app:argType=“long” | 是 - 默認值必須始終以“L”后綴結尾(例如“123L”), | 否 |
| 布林值 | app:argType=“boolean” | 是 -“true”或“false” | 否 |
| 字串 | app:argType=“string” | 是 | 是 |
| 資源參考 | app:argType=“reference” | 是 - 默認值必須為“@resourceType/resourceName”格式(例如,“@style/myCustomStyle”)或“0” | 否 |
| 自定義 Parcelable | app:argType="",其中 是 Parcelable 的完全限定類名稱 | 支持默認值“@null”,不支持其他默認值, | 是 |
| 自定義 Serializable | app:argType="",其中 是 Serializable 的完全限定類名稱 | 支持默認值“@null”,不支持其他默認值, | 是 |
| 自定義 Enum | app:argType="",其中 是 Enum 的完全限定名稱 | 是 - 默認值必須與非限定名稱匹配(例如,“SUCCESS”匹配 MyEnum.SUCCESS), | 否 |
上面的表格是直接從官網 Navigation 中復制的,方便大家看,可以看到型別挺多,基本已經可以滿足絕大多數開發的需求了,
傳遞可選引數的跳轉
有的時候需要指定引數來傳,特別是 Kotlin 中甜甜的語法糖,用著非常爽,但是可選引數該怎么進行跳轉呢?可選引數與必需引數有以下兩點不同:
- 可選引數必須使用查詢引數語法 (
"?argName={argName}") 來添加 - 可選引數必須具有
defaultValue集或nullability = true(將默認值隱式設定為null)
所以,所有可選引數都必須以串列的形式顯式添加到 composable() 函式:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
即使沒有向目的地傳遞任何引數,系統也會使用“me”的 defaultValue,是不是 so easy?
深層鏈接
Navigation Compose 支持隱式深層鏈接,此類鏈接也可定義為 composable() 函式的一部分,使用 navDeepLink() 以串列的形式添加深層鏈接:
val uri = "https://example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
借助這些深層鏈接,就可以將特定的網址、操作和 / 或 MIME 型別與可組合項關聯起來,
默認情況下,這些深層鏈接不會向外部應用公開,如需向外部提供這些深層鏈接,必須向應用的 Androidmanifest.xml 檔案添加相應的 <intent-filter> 元素:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
當其他應用觸發該深層鏈接時,Navigation 會自動深層鏈接到相應的可組合項,
這些深層鏈接還可用于構建包含可組合項中的相關深層鏈接的 PendingIntent:
val id = "exampleId"
val context = AmbientContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://example.com/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
然后,就可以像使用任何其他 PendingIntent 一樣,使用 deepLinkPendingIntent 在相應深層鏈接打開應用,
Navigation 小總結
到這里為止 Navigation 就差不多到一段落了,基本上也夠大多數的應用開發使用了,不要擔心沒有例子,下面都會寫實際應用例子的,因為還需要使用 ViewModel、State 等技術,所以慢慢來,不要著急嘛!心急吃不了臭豆腐😂,
管理狀態——State
應用中的狀態是指可以隨時間變化的任何值,這是一個非常寬泛的定義,從 Room 資料庫到類的變數,再到咱們平時使用的 ViewModel、LiveData 等等,全部涵蓋在內,
先來看一張圖吧:

如圖中所描述的那樣,這是所有 Android 應用都有核心界面更新回圈,
但是在 Jetpack Compose 中,狀態和事件是分開的,狀態表示可更改的值,而事件表示有情況發生的通知,通過將狀態與事件分開,可以將狀態的顯示與狀態的存盤和更改方式解耦,

來看下官方給出的優勢說法吧:
通過遵循單向資料流,您可以將在界面中顯示狀態的可組合項與應用中存盤和更改狀態的部分解耦,
使用單向資料流的應用的界面更新回圈如下所示:
- 事件:事件由界面的一部分生成并且向上傳遞,
- 更新狀態:事件處理腳本可以更改狀態,
- 顯示狀態:狀態會向下傳遞,界面會觀察新狀態并顯示該狀態,
使用 Jetpack Compose 時遵循此模式可帶來下面幾項優勢:
- 可測驗性:通過將狀態與顯示狀態的界面解耦,更容易單獨測驗這兩者,
- 狀態封裝:因為狀態只能在一個位置進行更新,所以不太可能創建不一致的狀態(或產生錯誤),
- 界面一致性:通過使用可觀察的狀態存盤器,所有狀態更新都會立即反映在界面中,
在 Compose 應用中使用的常見可觀察型別包括 State、LiveData、StateFlow、Flow 和 Observable 平時咱們使用的 LiveData、StateFlow、Flow 等等都可以直接轉成 Compose 所支持的 State,并且可以進行觀察,,
ViewModel 使用
MVVM 現在在很多專案中都使用到了,好處也數不勝數,特別是搭配上 LiveData 更是絕配,那就來看看怎么在 Compose 中進行使用吧:
@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
val name: String by helloViewModel.name.observeAsState("")
Column {
Text(text = name)
TextField(
value = name,
onValueChange = { helloViewModel.onNameChanged(it) },
label = { Text("Name") }
)
}
}
沒錯,就是這么簡單,就可以直接獲取到 ViewModel ,然后該怎么做就怎么做!
但是,,,,不對啊,LiveData 該怎么辦呢?在 Activity 或 Fragment 中咱們可以直接 observe ,但是在 Compose 中不可以啊,因為需要一個 LifecycleOwner 的引數啊!
其實這個問題上面已經給出答案了,需要將 LiveData 轉為 Compose 中可以觀察的 State 就可以使用了,那,,,,怎么使用呢?
val position by viewModel.position.observeAsState()
ok了,這就可以了,這里使用了屬性委托語法 by 隱式地將 State<T> 視為 Jetpack Compose 中型別 T 的物件,是個甜甜的語法糖,當然,如果你覺得太甜了不好的話,可以
val position: State<Int> = helloViewModel.name.observeAsState(0)
這就可以進行使用了,該怎么用就怎么用,和之前的 LiveData 一樣,當資料改變的時候會幫你重繪,不用你自己操心,這也是現在一直說的資料驅動頁面重繪,
可組合項中的狀態
這個東西怎么說,可組合項記住單個物件,但是系統會在初始組合期間將由 remember 計算的值存盤在組合中,并在重組期間回傳存盤的值,
我不知道是不是我不會用,但是我覺得這個東西沒什么卵用,有用的資料還放到 ViewModel 中不得了,
但上一次還是用到了,因為第一篇文章中沒有使用 ViewModel ,為了實作功能才不得不用的!那也說一說吧:
var expanded by remember { mutableStateOf(false) }
就是 mutableStateOf() 這個東西會創建可觀察的 MutableState,所以可以驅動頁面進行重繪,
State 小總結
說到這里,管理狀態的知識點就差不多了,接下來該看看 Compose 和 Android View 的相互操作性了,
Compose 和 Android View 相互操作性
Android View 中的 Compose
來看看官方的描述吧:
Jetpack Compose 經過精心設計,可與基于視圖的既定界面方法配合使用,如果您要構建新應用,最好的選擇可能是使用 Compose 實作整個界面,但是,如果您要修改現有應用,您可能不希望遷移整個應用,而是可以將 Compose 與現有界面設計相結合,
您可以通過兩種主要方法將 Compose 與基于視圖的界面相結合:
- 您可以將 Compose 元素添加到現有界面中,具體方法是創建完全基于 Compose 的新螢屏,或者將 Compose 元素添加到現有 Fragment 或視圖布局中,
- 您可以將基于視圖的界面元素添加到可組合函式中,這樣做可讓您將非 Compose 微件添加到基于 Compose 的設計中,
在之前的第一篇文章中其實已經簡單描述了在 Android View 中如何添加 Compose ,這里就不再贅述這部分內容了,
Compose 中的 Android View
咱們可以在 Compose 界面中添加 Android View 層次結構,如果要使用 Compose 中尚未提供的界面元素(比如 WebView 或 MapView)的話該咋辦?不知道了吧?
既然你發自內心得問了,那我就大發慈悲地告訴你!什么?你沒問?那我也要告訴你!就是下面這樣:
@Composable
fun LoadingContent() {
val context = LocalContext.current
val progressBar = remember {
ProgressBar(context).apply {
id = R.id.progress_bar
}
}
progressBar.indeterminateDrawable =
AppCompatResources.getDrawable(LocalContext.current, R.drawable.loading_animation)
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
// Adds view to Compose
AndroidView(
{ progressBar }, modifier = Modifier
.width(200.dp)
.height(110.dp)
) {}
}
}
哈哈哈,是不是很簡單,有人可能就要問了,為什么要設定 id 呢?因為每個元素必須具有唯一的 ID 才能使 savedInstanceState 發揮作用,
上面代碼中還有一點需要注意,獲取 Context 的方法,之前還專門去網上搜索過,結果一無所獲,最后在 Google 官方 Demo 中找到了答案,,,千萬不要相信官方檔案中寫的,官方檔案中讓這樣進行獲取,但是根本沒法獲取,都找不到類用啥來獲取啊?
val context = AmbientContext.current // 錯誤寫法
val context = LocalContext.current // 正確寫法
上面只是簡單用法,來看下稍微復雜點的使用方法吧,來看看 WebView 怎么呼叫,正好為下面的文章詳情做準備:
@Composable
fun rememberX5WebViewWithLifecycle(): X5WebView {
val context = LocalContext.current
val x5WebView = remember {
X5WebView(context).apply {
id = R.id.web_view
}
}
// Makes MapView follow the lifecycle of this composable
val lifecycleObserver = rememberX5WebViewLifecycleObserver(x5WebView)
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) {
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
return x5WebView
}
@Composable
private fun rememberX5WebViewLifecycleObserver(x5WebView: X5WebView): LifecycleEventObserver =
remember(x5WebView) {
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> x5WebView.onCreate()
Lifecycle.Event.ON_START -> x5WebView.onStart()
Lifecycle.Event.ON_RESUME -> x5WebView.onResume()
Lifecycle.Event.ON_PAUSE -> x5WebView.onPause()
Lifecycle.Event.ON_STOP -> x5WebView.onStop()
Lifecycle.Event.ON_DESTROY -> x5WebView.destroy()
else -> throw IllegalStateException()
}
}
}
代碼很簡單,大家應該都能看懂,將控制元件和生命周期進行系結,避免記憶體泄漏,
如果需要添加視圖元素或層次結構,可以使用 AndroidView 可組合項,系統會向 AndroidView 傳遞一個回傳 View 的 lambda,AndroidView 還提供了在視圖膨脹時被呼叫的 update 回呼,每當在該回呼中讀取的 State 發生變化時,AndroidView 都會重組,
@Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
// Adds view to Compose
AndroidView(
modifier = Modifier.fillMaxSize(),
viewBlock = { context ->
CustomView(context).apply {
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
view.coordinator.selectedItem = selectedItem.value
}
)
}
@Composable
fun ContentExample() {
Column(Modifier.fillMaxSize()) {
Text("Look at this CustomView!")
CustomView()
}
}
這塊需要注意的是:盡量要在 AndroidView viewBlock 中構造視圖,別在 AndroidView 之外保存或 remember 對視圖的直接參考,
如果想嵌入 XML 布局,就需要使用 AndroidViewBinding 了,這應該不難,郭神之前寫過用法,我之前也寫過一篇關于 ViewBinding 的文章:ViewBingding?搞!,應該足夠使用了,
@Composable
fun AndroidViewBindingExample() {
AndroidViewBinding(ExampleLayoutBinding::inflate) {
exampleView.setBackgroundColor(Color.GRAY)
}
}
Compose 中的異步操作
Compose 提供了一些機制,可以從可組合項中執行異步操作,
對于基于回呼的 API,可以結合使用 MutableState 和 onCommit(),使用 MutableState 存盤回呼的結果,并在結果發生變化時重組受影響的界面,每當引數發生變化時,都使用 onCommit() 來執行操作,如果界面的組成在操作完成之前結束,也可以定義 onDispose() 方法以清除所有待處理的操作,以下示例展示了這些 API 如何協同作業,
@Composable
fun fetchImage(url: String): ImageBitmap? {
var image by remember(url) { mutableStateOf<ImageBitmap?>(null) }
onCommit(url) {
val listener = object : ExampleImageLoader.Listener() {
override fun onSuccess(bitmap: Bitmap) {
image = bitmap.asImageBitmap()
}
}
val imageLoader = ExampleImageLoader.get()
imageLoader.load(url).into(listener)
onDispose {
imageLoader.cancel(listener)
}
}
return image
}
如果異步操作是掛起函式,可以改用 LaunchedEffect:
suspend fun loadImage(url: String): ImageBitmap = TODO()
@Composable
fun fetchImage(url: String): ImageBitmap? {
var image by remember(url) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(url) {
image = loadImage(url)
}
return image
}
相互操作性小總結
相互操作不就是相互呼叫嘛,怎么說都行,這塊挺重要,,,再看看繞不開的庫吧,
Compose 和其他庫
這塊怎么說呢,Compose 不管怎么說也是親兒子,和 ViewModel Navigation Hilt Paging 都可以共同使用,完全沒有代溝,,,
上面說的 ViewModel 和 Navigation 之前已經說過使用方法了,這里就不贅述了,如果需要看的話可以看我之前的文章,
Hilt Paging這兩個庫就先不寫了,Paging 是因為我沒有用過,就不在這里現眼了,而 Hilt 我不僅是用過還寫過相關的文章,但為啥不寫呢?因為他還是 alpha 版本,還不穩定,,,
下面這個需要說一下了,
圖片加載框架
如果現在你問一個安卓開發:你用什么圖片加載框架?百分之九十以上的肯定會毫不猶豫地說:Glide !
但是,官方好像現在更傾向于 Coil,并且官方的 Demo 中也使用的是 Coil,并且可以直接在 Compose 中進行使用,不過需要先添加依賴:
implementation "dev.chrisbanes.accompanist:accompanist-coil:0.6.0"
接下來看下使用方法:
@Composable
fun MyExample() {
CoilImage(
data = "https://picsum.photos/300/300",
loading = {
Box(Modifier.fillMaxSize()) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
},
error = {
Image(painterResource(R.drawable.ic_error), contentDescription = "Error")
}
)
}
怎么樣,是不是很簡單,使用的時候直接呼叫即可,當然也可以自己實作,或者寫一個 Composable 呼叫 Glide 來實作也是可以的,
但是 Coil 更加輕一些,大家各憑喜好吧!
實戰演練
俗話說:養兵千日,用兵一時,學完了就應該用一用,哪怕寫個 Demo 也是好的嘛!最起碼比只看看不寫強的多,可能有人會說,這玩意兒根本不用看,用的時候一查不得了!這我得攔您一句,那是您,我這腦子不行,還是寫一寫加深一下印象的好!
那么咱們就先寫一下跳轉登錄頁面吧,
是不是忘記長什么樣了?再給大家看看吧:

怎么樣?是不是還挺好看,哈哈哈!
首頁 ViewModel 使用
上面 gif 中也有首頁的樣子,資料還是玩安卓提供的,由于之前已經有 ViewModel 了,所以直接使用就行:
val viewModel: HomePageViewModel = viewModel()
val result by viewModel.state.observeAsState(PlayLoading)
簡單吧,上面好像忘了說了,observeAsState 中的引數意思是默認值是什么,可以寫也可以不寫,寫的話默認值就是你寫的,不寫的話默認值就是 null,視情況而定,
這里其實我把我之前的 ViewModel 也進行了一些修改,之前 ViewModel 中 LiveData 中的回傳值是 Result ,但是,,,,,注意,這里有坑,也不知道是不是因為是 beta 版的問題,如果使用 Result 的話,會出現錯誤,調不好的那種,應該是原始碼中有問題,所以,盡量不要使用 Result 作為回傳值!!!
所以我把 Result 改為了自定義的一個密封類:
sealed class PlayState
object PlayLoading : PlayState()
data class PlaySuccess<T>(val data: T) : PlayState()
data class PlayError(val e: Throwable) : PlayState()
很簡單,三種狀態,加載中、加載成功、加載失敗,不同狀態顯示不同布局,
接下來需要呼叫下加載資料的方法:
viewModel.getArticleList(1, true)
暫時沒有做加載更多資料,之后有空再做吧,因為 Compose 中沒有現成的控制元件,需要自定義,或者直接使用之前的原生控制元件也可以,但總感覺已經使用 Compose 了再使用原生控制元件不太好,不到萬不得已我是不會使用原生控制元件的,但是下一篇文章中要說的文章詳情頁面就不得不使用原生的 WebView 了,因為這玩意實在沒有能力自定義啊!
再來看下 State 的實際使用吧:
Column(modifier = Modifier.fillMaxSize()) {
PlayAppBar(stringResource(id = R.string.home_page), false)
when (result) {
is PlayLoading -> {
LoadingContent()
}
is PlaySuccess<*> -> {
val data = result as PlaySuccess<List<Article>>
LazyColumn(modifier) {
itemsIndexed(data.data) { index, article ->
ArticleItem(
article,
index,
enterArticle = { urlArgs -> enterArticle(urlArgs) })
}
}
}
is PlayError -> {
loadState = true
viewModel.onRefreshChanged(REFRESH_STOP)
ErrorContent(enterArticle = { viewModel.getArticleList(1, true) })
}
}
}
其實很簡單,和之前使用基本一致,只是寫代碼的思想要變,
之前寫一個布局如果要想重用的話需要 include ,現在可以直接重復使用,比如上面代碼中使用到的 LoadingContent 和 ErrorContent 在其他地方也可以進行使用:
@Composable
fun ErrorContent(
enterArticle: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.padding(vertical = 50.dp),
painter = painterResource(id = R.drawable.bad_network_image),
contentDescription = "網路加載失敗"
)
Button(onClick = enterArticle) {
Text(text = stringResource(id = R.string.bad_network_view_tip))
}
}
}
這里就只放下 ErrorContent 吧,LoadingContent 和這個差不多,由于篇幅原因就不貼代碼了,需要的話可以直接從 Github 進行下載,
Navigation 使用
這塊使用的話就需要修改下之前的代碼了,上面也大概介紹了使用方法,但是如果真正讓你使用的話可能還是會有些懵逼,看懂是一回事,會寫是另一回事,
首先來定義下 Destinations:
object MainDestinations {
const val HOME_PAGE_ROUTE = "home_page_route"
const val ARTICLE_ROUTE = "article_route"
const val ARTICLE_ROUTE_URL = "article_route_url"
const val LOGIN_ROUTE = "login_route"
}
再來定義下我們需要使用到的 Action:
/**
* Models the navigation actions in the app.
*/
class MainActions(navController: NavHostController) {
val homePage: () -> Unit = {
navController.navigate(MainDestinations.HOME_PAGE_ROUTE)
}
val enterArticle: (String) -> Unit = { url ->
navController.navigate("${MainDestinations.ARTICLE_ROUTE}/$url")
}
val toLogin: () -> Unit = {
navController.navigate(MainDestinations.LOGIN_ROUTE)
}
val upPress: () -> Unit = {
navController.navigateUp()
}
}
上面寫的是不是已經用到了啊!
然后最后寫 NavHost:
@Composable
fun NavGraph(startDestination: String = MainDestinations.HOME_PAGE_ROUTE) {
val navController = rememberNavController()
val actions = remember(navController) { MainActions(navController) }
NavHost(
navController = navController,
startDestination = startDestination
) {
composable(MainDestinations.HOME_PAGE_ROUTE) {
Home(actions)
}
composable(MainDestinations.LOGIN_ROUTE) {
LoginPage(actions)
}
composable(
"${MainDestinations.ARTICLE_ROUTE}/{$ARTICLE_ROUTE_URL}",
arguments = listOf(navArgument(ARTICLE_ROUTE_URL) { type = NavType.StringType })
) { backStackEntry ->
val arguments = requireNotNull(backStackEntry.arguments)
ArticlePage(
url = arguments.getString(ARTICLE_ROUTE_URL) ?: "www.baidu.com",
onBack = actions.upPress
)
}
}
}
這就寫完了,先別懵,我給大家再念叨念叨,上面說過的就不啰嗦了,這里的一個 composable 相當于咱們之前的一個 Activity 或者 Fragment ,我這塊給 Home 和 LoginPage 直接將 navController 給傳進去了,navController 就是上面定義的 MainActions,頁面中想要進行跳轉動作的話直接呼叫即可,
來看看從我的頁面是怎樣跳轉到登錄頁面的吧:
@Composable
fun ProfilePage(onNavigationEvent: MainActions){
val scrollState = rememberScrollState()
Column(modifier = Modifier.fillMaxSize()) {
……
NameAndPosition(onNavigationEvent.toLogin) //將跳轉事件傳入
……
}
}
@Composable
private fun NameAndPosition(toLogin: () -> Unit) {
Column(modifier = if (Play.isLogin) {
Modifier.padding(horizontal = 16.dp)
} else {
Modifier
.padding(horizontal = 16.dp)
.clickable { toLogin() } // 進行跳轉
}) {
Name(
modifier = Modifier.baselineHeight(32.dp)
)
Position(
modifier = Modifier
.padding(bottom = 20.dp)
.baselineHeight(24.dp)
)
}
}
這下是不是有種恍然大明白的感覺了!哈哈哈😄
撰寫文章詳情頁面
其實在上面咱們已經把最麻煩的 WebView 給寫好了,直接進行呼叫就可以了:
@Composable
fun ArticleScreen(
url: String,
onBack: () -> Unit
) {
val x5WebView = rememberX5WebViewWithLifecycle()
Scaffold(
topBar = {
PlayAppBar("文章詳情", click = {
if (x5WebView.canGoBack()) {
//回傳上個頁面
x5WebView.goBack()
} else {
onBack.invoke()
}
})
},
content = {
AndroidView(
{ x5WebView },
modifier = Modifier
.fillMaxSize()
.padding(bottom = 56.dp),
) { x5WebView ->
x5WebView.loadUrl(url)
}
},
bottomBar = {
BottomBar(
post = url,
onUnimplementedAction = { showDialog = true }
)
}
)
}
是不是現在看起來這個頁面就很簡單了,還是使用的腳手架,直接插槽式,方便快捷,
上面的 PlayAppBar 我抽出來了一個控制元件,很簡單,這里就不貼代碼了,如果有需要的可以去 Github 查看,最后再看下文章詳情頁面撰寫好的樣子吧:

精致的結尾
以前一個新的包只需要一篇文章就能搞定,Compose 已經寫了兩篇文章了,但是感徑訓有很多內容,比如說控制元件、布局、主題、串列、影片等等,,,慢慢來吧,,
上面的代碼在我的 Github 中都有,記住是 main 分支,如果文章對你有幫助別忘了點贊和關注啊,感激不盡🙏,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/267399.html
標籤:其他
