在Compose中父组件如何调用子组件的函数?

原文地址:# 在Compose中父组件如何调用子组件的函数?

咋一看标题你可能会觉得这有什么好研究的,请仔细看我的描述:在父组件 中调用子组件的函数!

众所周知,如果我们希望让子组件 调用父组件的函数,可以如下方式:

那么问题来了,父组件如何调用子组件的函数呢?

Compose 不同于传统的 View 体系,每一个组件都是@Composable注解的函数,没有实例对象,无法对外暴露函数。

你可能会说,父组件调用子组件函数有必要么?

试想这样一个场景,我们有一个复杂的展示页面,里面有 banner轮播图、用户信息、资讯List 等等,他们被放在一个大的可滑动组件中,现在我们要实现这个可滑动组件下拉刷新。

这时事件不再是有子组件发出(不同于"状态向下、事件向上"),而是父组件发出的,此时应该如何处理?

一个简单的例子:

kotlin 复制代码
@Composable
fun Container() {
    Column {
        TButton("refresh"){
            TODO() //如何实现
        }
        (0..10).map {
            SubComponent(index = it)
        }
    }
}

@Composable
fun SubComponent(index:Int) {
    val (state, setState) = useState(0.0)
    //刷新函数在子组件
    fun refresh() {
        setState(Random.nextDouble())
    }
    Column {
        Row {
            Text(text = "index $index: $state")
            TButton(text = "refresh") {
                // 子组件可以轻松的刷新自己
                refresh()
            }
        }
        Divider(modifier = Modifier.fillMaxWidth())
    }
}

方法1:继续状态提升↑!!

我们可以继续贯彻状态提升的思想,父组件的刷新只改变顶层状态,由子组件监听状态变化,实现刷新:

kotlin 复制代码
@Composable
fun Container() {
    // 状态提升到父组件
    val (isRefresh, _, setIsRefresh) = useBoolean(false)
    val scope = rememberCoroutineScope()
    Column {
        TButton("refresh") {
            scope.launch {
                setIsRefresh(true)
                delay(1.seconds)
                setIsRefresh(false)
            }
        }
		//.............
    }
}

@Composable
fun SubComponent(index: Int, refreshState: Boolean) {
    val (state, setState) = useState(0.0)
    fun refresh() {
        setState(Random.nextDouble())
    }
    // 子组件监听状态
    useEffect(refreshState) {
        if (refreshState) {
            refresh()
        }
    }
    //.............
}

这样做当然是OK的,代码也可以完美运行,但是我们需要给所有组件加上新的参数,以及 useEffect 监听状态变化;

原始代码同样存在我在上一篇文章中提到的问题,一旦层级过多,就会存在大量中间传递,如果修改就牵一发动全身。

如果你想用这种方法,我建议使用 useContext 进行优化:

kotlin 复制代码
val RefreshContext = createContext(false)

@Composable
fun Container() {
    //.............
    Column {
        TButton("refresh") {
           //.............
        }
        // 通过 Provider 向子组件暴露状态
        RefreshContext.Provider(isRefresh){
            (0..10).map {
                SubComponent(index = it)
            }
        }
    }
}

@Composable
fun SubComponent(index: Int) {
    //.............
    // 刷新状态通过 useContext获取
    val refreshState =useContext(context = RefreshContext)
    useEffect(refreshState) {
        if (refreshState) {
            refresh()
        }
    }
    //.............
}

方法2:使用 useEvent

为什么叫这个名字是因为它的使用有一点类似 EventBus 的订阅发布模式,我们先来看改造后的代码

kotlin 复制代码
@Composable
fun Container() {
    val post = useEventPublish<Unit>()
    Column {
        TButton("refresh") {
            post(Unit)
        }
        (0..10).map {
            SubComponent(index = it)
        }
    }
}

@Composable
fun SubComponent(index: Int) {
    val (state, setState) = useState(0.0)
    fun refresh() {
        setState(Random.nextDouble())
    }
    useEventSubscribe { _: Unit ->
        refresh()
    }
    //.............
}

使用方法大致如下:

  1. 使用 useEventPublish<Unit>() 拿到一个 post 发布函数,

    尖括号中的是我们需要传递的Event参数类型,由于这里只是简单演示,我直接使用了 Unit

  2. 在需要订阅的事件的组件中使用useEventSubscribe注册一个函数

    注意在闭包中声明正确的类型!

  3. 发布者调用 post 函数,发布事件

与使用 useContext 相似,我们可以用它来解耦父子组件之间的关系,无需担心层级嵌套的问题。

但是同时也要注意,useEventSubscribe 在组件不可见时是自动反注册的,所以需要在可见范围内才能正确的响应。

现在我们可以回答标题的问题了,父组件如何调用子组件函数:

  1. 无法直接调用,因为组件是函数
  2. 通过状态提升,父组件修改状态。子组件监听状态变化间接调用
  3. 使用useEvent相关钩子,通过订阅发布模式,解耦组件关系、实现跨组件通信

Tips

Tips: useEvent 更多是为了用于跨组件通信,并不只是用来解决父组件调用子组件函数哦,post 函数可以传递任意实例。它和 EventBus 是几乎相同的。相同事件类型的订阅者,会接收到相同的事件发布。

探索更多

项目开源地址:junerver/ComposeHooks

MavenCentral:hooks

kotlin 复制代码
implementation("xyz.junerver.compose:hooks:1.0.5")

欢迎使用、勘误、pr、star。

相关推荐
阿巴斯甜19 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker20 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952721 小时前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android