再探 Compose 版本的玩安卓

简介: Compose 的核心内容

冒失的前言

之前写了第一篇关于 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 等等,全部涵盖在内。

先来看一张图吧:

20210307085329594.png

如图中所描述的那样,这是所有 Android 应用都有核心界面更新循环。

但是在 Jetpack Compose 中,状态和事件是分开的。状态表示可更改的值,而事件表示有情况发生的通知。通过将状态与事件分开,可以将状态的显示与状态的存储和更改方式解耦。

20210307085412581.png

来看下官方给出的优势说法吧:

通过遵循单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分解耦。

使用单向数据流的应用的界面更新循环如下所示:

  • 事件:事件由界面的一部分生成并且向上传递。
  • 更新状态:事件处理脚本可以更改状态。
  • 显示状态:状态会向下传递,界面会观察新状态并显示该状态。

使用 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") }
        )
    }
}
1

没错,就是这么简单,就可以直接获取到 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 查看,最后再看下文章详情页面编写好的样子吧:

b8fed2efed5ce950dbdee60858b3767.png

精致的结尾

以前一个新的包只需要一篇文章就能搞定,Compose 已经写了两篇文章了,但是感觉还有很多内容,比如说控件、布局、主题、列表、动画等等。。。慢慢来吧。。

上面的代码在我的 Github 中都有,记住是 main 分支。

目录
相关文章
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
54 4
|
3月前
|
开发工具 git 索引
repo sync 更新源码 android-12.0.0_r34, fatal: 不能重置索引文件至版本 ‘v2.27^0‘。
本文描述了在更新AOSP 12源码时遇到的repo同步错误,并提供了通过手动git pull更新repo工具来解决这一问题的方法。
118 1
|
3月前
|
IDE API 开发工具
与Android Gradle Plugin对应的Gradle版本和Android Studio版本
与Android Gradle Plugin对应的Gradle版本和Android Studio版本
351 0
|
4月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
5月前
|
Android开发
Android Studio(2022.3.1)设置阿里云源-新旧版本
Android Studio(2022.3.1)设置阿里云源-新旧版本
1017 1
|
4月前
|
Android开发
Android使用DrawerLayout仿qq6.6版本侧滑效果
Android使用DrawerLayout仿qq6.6版本侧滑效果
32 0
|
4月前
|
Android开发
【亲测,安卓版】快速将网页网址打包成安卓app,一键将网页打包成app,免安装纯绿色版本,快速将网页网址打包成安卓apk
【亲测,安卓版】快速将网页网址打包成安卓app,一键将网页打包成app,免安装纯绿色版本,快速将网页网址打包成安卓apk
122 0
|
5月前
|
存储 Android开发
详细解读Android获取已安装应用信息(图标,名称,版本号,包)
详细解读Android获取已安装应用信息(图标,名称,版本号,包)
70 0
|
5月前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
|
5月前
|
开发工具 Android开发
Android studio历史版本下载
Android studio历史版本下载
593 0