主頁 > 移動端開發 > 再探 Compose 版本的玩安卓

再探 Compose 版本的玩安卓

2021-03-08 10:26:51 移動端開發

冒失的前言

之前寫了第一篇關于 Compose 初探的文章,大概說了下 Compose 的前世今生,本篇文章是基于上一篇文章寫的,閱讀之前最好先閱讀下:初探 Compose 版本的玩安卓,

上一篇文章由于篇幅的原因很多東西沒有介紹, Compose 非常大,也絕對不是一篇文章能寫完的,咱們慢慢來,這篇文章打算詳細介紹下 Compose 的導航—— Navigation ,還有 Compose 的狀態管理—— State ,然后是ComposeAndroid 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 可組合項相關聯,NavHostNavController 與導航圖相關聯, 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”
自定義 Parcelableapp:argType="",其中 是 Parcelable 的完全限定類名稱支持默認值“@null”,不支持其他默認值,
自定義 Serializableapp:argType="",其中 是 Serializable 的完全限定類名稱支持默認值“@null”,不支持其他默認值,
自定義 Enumapp: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 就差不多到一段落了,基本上也夠大多數的應用開發使用了,不要擔心沒有例子,下面都會寫實際應用例子的,因為還需要使用 ViewModelState 等技術,所以慢慢來,不要著急嘛!心急吃不了臭豆腐😂,

管理狀態——State

應用中的狀態是指可以隨時間變化的任何值,這是一個非常寬泛的定義,從 Room 資料庫到類的變數,再到咱們平時使用的 ViewModel、LiveData 等等,全部涵蓋在內,

先來看一張圖吧:
安卓核心界面更新回圈
如圖中所描述的那樣,這是所有 Android 應用都有核心界面更新回圈,

但是在 Jetpack Compose 中,狀態和事件是分開的,狀態表示可更改的值,而事件表示有情況發生的通知,通過將狀態與事件分開,可以將狀態的顯示與狀態的存盤和更改方式解耦,
Compose資料流
來看下官方給出的優勢說法吧:

通過遵循單向資料流,您可以將在界面中顯示狀態的可組合項與應用中存盤和更改狀態的部分解耦,

使用單向資料流的應用的界面更新回圈如下所示:

  • 事件:事件由界面的一部分生成并且向上傳遞,
  • 更新狀態:事件處理腳本可以更改狀態,
  • 顯示狀態:狀態會向下傳遞,界面會觀察新狀態并顯示該狀態,

使用 Jetpack Compose 時遵循此模式可帶來下面幾項優勢:

  • 可測驗性:通過將狀態與顯示狀態的界面解耦,更容易單獨測驗這兩者,
  • 狀態封裝:因為狀態只能在一個位置進行更新,所以不太可能創建不一致的狀態(或產生錯誤),
  • 界面一致性:通過使用可觀察的狀態存盤器,所有狀態更新都會立即反映在界面中,

在 Compose 應用中使用的常見可觀察型別包括 StateLiveDataStateFlowFlowObservable 平時咱們使用的 LiveDataStateFlowFlow 等等都可以直接轉成 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 該怎么辦呢?在 ActivityFragment 中咱們可以直接 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 小總結

說到這里,管理狀態的知識點就差不多了,接下來該看看 ComposeAndroid 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 中尚未提供的界面元素(比如 WebViewMapView)的話該咋辦?不知道了吧?

既然你發自內心得問了,那我就大發慈悲地告訴你!什么?你沒問?那我也要告訴你!就是下面這樣:

@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,可以結合使用 MutableStateonCommit(),使用 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 都可以共同使用,完全沒有代溝,,,

上面說的 ViewModelNavigation 之前已經說過使用方法了,這里就不贅述了,如果需要看的話可以看我之前的文章,

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 也進行了一些修改,之前 ViewModelLiveData 中的回傳值是 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 ,現在可以直接重復使用,比如上面代碼中使用到的 LoadingContentErrorContent 在其他地方也可以進行使用:

@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 ,我這塊給 HomeLoginPage 直接將 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

標籤:其他

上一篇:【Kotlin】let,also,run,apply與with的使用與區別

下一篇:2021蘋果CMSV10完美對接蘿卜影視(原生)藍色版

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more