Jetpack Compose中的副作用Api

news2025/1/14 0:46:02

Compose的生命周期

在这里插入图片描述
每个Composable函数最终会对应LayoutNode节点树中的一个LayoutNode节点,可简单的为其定义生命周期:

  • onActive: 进入重组作用域, Composable对应的LayoutNode节点被挂接到节点树上
  • onUpdate:触发重组, Composable对应的LayoutNode节点被更新(0次或者多次)
  • onDispose: 离开重组作用域, Composable对应的LayoutNode节点从节点树上移除

Compose的副作用

副作用是指发生在可组合函数作用域之外的应用状态的变化。由于可组合项的生命周期和属性(例如不可预测的重组、以不同顺序执行可组合项的重组或可以舍弃的重组),可组合项在理想情况下应该是无副作用的。

无副作用的函数也被称为纯函数: 唯一确定的输入决定唯一确定的输出,不会因为运行次数的增加导致输出结果的不同。这对于React、Compose这类的声明式UI框架至关重要,因为它们都是通过函数(组件)的反复执行来渲染UI的,函数执行的时机和次数都不可控,但是函数的执行结果必须可控,因此,我们要求这些函数组件必须用纯函数实现。

虽然副作用是不应该出现的,但是有时副作用是合理的,必要的,例如,IO操作、日志处理、弹出toast提醒、页面跳转等等,这些操作在能感知Composable生命周期的受控环境中执行,否则有可能打断Compose的施法。为此 Compose提供了很多副作用API,使用这些API可以保证对应的操作在Composable的生命周期的特定阶段被执行,确保行为的可预期性。

DisposableEffect

DisposableEffect可以感知ComposableonActiveonDispose,允许通过副作用完成一些预处理和收尾工作。

例如监听处理系统返回键的例子:

@Composable
fun BackPressHandler(enabled: Boolean = true, onBackPressed: () -> Unit) {
    val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
    val currentOnBack by rememberUpdatedState(onBackPressed)

    val backCallback = remember {
        object : OnBackPressedCallback(enabled) {
            override fun handleOnBackPressed() {
                currentOnBack()
            }
        }
    }

    // backDispatcher 发生变化时重新执行
    DisposableEffect(backDispatcher) {
        backDispatcher?.addCallback(backCallback) // onActive时添加回调
        // 当 Composable 进入 onDispose 时执行
        onDispose {
            backCallback.remove() // onDispose时移除回调 避免内存泄漏
        }
    }
}

Compose中自带的BackHandler组件内部就是基于DisposableEffect实现 的。

DisposableEffect的lambda中必须跟随一个onDispose{...}代码块的调用,否则会编译报错。onDispose一般常用于反注册接口回调,及一些资源清理工作,防止内存泄漏。当有新的副作用来临时,前一次的副作用就会执行onDispose{...}代码块中的代码。

DisposableEffect可以接受一个key作为参数,如果key是可变状态,当key发生变化时,会重新执行副作用中的代码块。如果keyUnittrue这样的常量,则副作用代码块只在onActive时执行一次。

SideEffect

SideEffect仅会在每次重组成功时执行,因此能正确的向外传递状态。其中不能用来处理耗时和异步任务。(注意Composable函数不一定每次都会执行重组也不一定每次重组都会执行成功)

@Composable
fun MyScreen(drawerTouchHandler: TouchHandler) {
    val drawerState = rememberDrawerState(DrawerValue.Closed)

    SideEffect { // 将 drawerState 通知外部
        drawerTouchHandler.enabled = drawerState.isOpen
    }
}
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

LaunchedEffect

当副作用中需要处理异步任务的需求时,可以使用 LaunchedEffect, 在 Composable 进入 onActive 时,LaunchedEffect 会启动协程执行 block 中的内容,一般用来启动子协程或者调用挂起函数。

Composable 进入 onDispose 时,LaunchedEffect启动的协程会自动取消,因此 LaunchedEffect 不需要实现onDispose{...}

LaunchedEffect 设置的 key 发生变化时,当前协程自动结束,同时开启新的协程。

@Composable
fun MyApp(
    state: UiState<List<Movie>>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {
    // 当 state 中包含错误时,显示一个 SnackBar,
    if (state.hasError) {
        // 显示一个 SnackBar 的显示需要一个协程环境,而 LaunchedEffect 会为其提供
        // 当 scaffoldState.snackbarHostState变化时,将启动一个新的协程, SnackBar重新显示一次
        // 当 state.hasError 变成 false 时,LaunchedEffect 进入 onDispose, 协程会被自动取消,SnackBar也会随之消失
        LaunchedEffect(scaffoldState.snackbarHostState) {
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }
}

rememberCoroutineScope

由于 LaunchedEffect 是可组合函数, 因此只能在Composable函数中调用,如果想在非Composable环境中使用协程,例如Button的onClick方法中, 可以使用 rememberCoroutineScope,它会返回一个CoroutineScope,可以用来启动新的协程。 当 Composable 进入 onDispose 时,启动的协程会自动取消。如果您需要手动控制一个或多个协程的生命周期,请使用 rememberCoroutineScope,例如在用户事件发生时取消动画。

@Composable
fun MyApp(scaffoldState: ScaffoldState = rememberScaffoldState()) {
    // 创建一个绑定 MyApp 生命周期的协程作用域
    val scope = rememberCoroutineScope()
    Scaffold(scaffoldState = scaffoldState) { padding ->
        Column(Modifier.padding(padding)) {
            Button(
                onClick = {
                    // 点击按钮时创建一个新的协程作用域,用于显示Snackbar
                    scope.launch {
                        scaffoldState.snackbarHostState.showSnackbar("Something happened!")
                    }
                }
            ) {
                Text(text = "Press me")
            }
        }
    }
}

rememberUpdatedState

rememberUpdatedState可以在不中断副作用的情况下感知外界的变化,一般用来获取观察状态的最新状态值。

@Composable
fun MyScreen(onTimeOut: () -> Unit) { 
    val currentOnTimeout by rememberUpdatedState(onTimeOut)
    // key为Unit时不会因为MyScreen的重组重新执行
    LaunchedEffect(Unit) {
        delay(300)
        currentOnTimeout() // 总是能获取到最新的 onTimeOut
    }
}

看下面的例子更加容易理解:

@Composable
private fun UpdatedRememberExample() {
    var myInput by remember { mutableStateOf(0) }

    Column(Modifier.height(100.dp)) {
        OutlinedButton(
            modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
            onClick = { myInput++ }
        ) {
            Text("Increase rememberInput: $myInput")
        }
        Calculation(input = myInput)
    }
}
@Composable
private fun Calculation(input: Int) {
    val rememberUpdatedStateInput by rememberUpdatedState(input)
    val rememberedInput by remember { mutableStateOf(input) }
    Text("updatedInput: $rememberUpdatedStateInput, rememberedInput: $rememberedInput")
}

在这里插入图片描述
可以看到在Calculation组件中,使用rememberUpdatedState方式的每次都能读取到外部更新后的最新的状态值,而普通方式则不行。当然这里也可以使用 remember(key) { } 的方式也能达到效果。

查看rememberUpdatedState可知它就是不断的将新值赋值给自身的value而已:

@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

snapshotFlow

snapshotFlow 可以将 ComposeState 转换为 Flow。每当State变化时,flow就会发送新数据(但是冷流,调用collect才会发) snapshotFlow 会在收集到块时运行该块,并发出从块中读取的 State 对象的结果。当在 snapshotFlow 块中读取的 State 对象之一发生变化时,如果新值与之前发出的值不相等,Flow 会向其收集器发出新值(此行为类似于 Flow.distinctUntilChanged 的行为)。

@Composable
fun MyScreen2() {
    val pagerState = rememberPagerState()
    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage } .collect { page ->
            // currentPage发生变化
        }
    }
    HorizontalPager(
        count = 10,
        state = pagerState,
    ) { page ->
        // ...
    }
}
@Composable
fun SnapshotFlowDemo() {
    val scaffoldState = rememberScaffoldState()
    LaunchedEffect(scaffoldState) {
        snapshotFlow { scaffoldState.snackbarHostState }
            .mapNotNull { it.currentSnackbarData?.message }
            .distinctUntilChanged()
            .collect { message ->
                println("A SnackBar with message $message was shown")
            }
    }
}

produceState

produceStatederivedStateOf 都是状态创建的副作用API, 从本质上讲,remember 也是一种副作用API,只在组件OnActive时被创建一次,不跟随重组反复创建。

produceState可以将任意数据源转换成一个State供给Composable函数使用

produceState 会启动一个协程,该协程将作用域限定为可将值推送到返回的 State 的组合。使用此协程将非 Compose 状态转换为 Compose 状态,例如将外部订阅驱动的状态(如 Flow、LiveData 或 RxJava)引入组合。

该协程在 produceState 进入组合时启动,在其退出组合时取消。返回的 State 冲突;设置相同的值不会触发重组。

即使 produceState 创建了一个协程,它也可用于观察非挂起的数据源。

@Composable
fun loadNetWorkImage(
    url: String,
    imageRepository: ImageRepository
) : State<Result<Image>> {
    // produceState 观察 url 和 imageRepository 两个参数,当它们发生变化时,producer会重新执行
    // produceState的实现是通过 remember { mutableStateOf() } + LaunchedEffect (具有学习意义)
    // produceState 中的任务会随着 LaunchedEffect 的 onDispose 被自动停止。
    return produceState(initialValue = Result.Loading, url, imageRepository) {
        // 通过挂起函数请求图片
        val image = imageRepository.load(url)
        // 根据请求结果设置 Result
        // 当 Result 变化时,读取此 State 的 Composable 会触发重组
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

produceState 中使用 awaitDispose 清理资源避免内存泄漏:

val currentPerson by produceState(null, viewModel) {
    val disposeable = viewModel.registerPersonObserver { person ->
        value = person
    }
    awaitDispose {
        // 当 Composable 进入 onDispose时,进入此处
        disposeable.dispose()
    }
}

derivedStateOf

derivedStateOf 用来将一个或多个 State 转成另一个 StatederivedStateOf{...}block 中可以依赖其他 State 创建并返回一个 DerivedState, 当 block 中依赖的 State 变化时,会更新此 DerivedState,依赖 DerivedState 的所有 Composable 会随之重组。

以下示例展示了基本的“待办事项”列表,其中具有用户定义的高优先级关键字的任务将首先显示:

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {

    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or highPriorityKeywords
    // change, not on every recomposition
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

在以上代码中,derivedStateOf 保证每当 todoTasks 发生变化时,系统都会执行 highPriorityTasks 计算,并相应地更新界面。如果 highPriorityKeywords 发生变化,系统将执行 remember 代码块,并且会创建新的派生状态对象并记住该对象,以代替旧的对象。由于执行过滤以计算 highPriorityTasks 的成本很高,因此应仅在任何列表发生更改时才执行,而不是在每次重组时都执行。

此外,更新 derivedStateOf 生成的状态不会导致可组合项在声明它的位置重组,Compose 仅会对返回状态为已读的可组合项(在本例中,指 LazyColumn 中的可组合项)进行重组。

该代码还假设 highPriorityKeywords 的变化频率显著低于 todoTasks。否则,该代码会使用 remember(todoTasks, highPriorityKeywords) 而不是 derivedStateOf

@Composable
fun SearchScreen() {
    val postList = remember { mutableStateListOf<String>() }
    val keyWord by remember { mutableStateOf("") }

    // 这里 postList 和 keyWord任意一个变化时,会更新 result
    val result by remember {
        derivedStateOf { postList.filter { it.contains(keyWord) } }
    }

    Box(modifier = Modifier.fillMaxSize()) {
        LazyColumn {
            items(result) { item ->
                Text(item)
            }
        }
    }
}

也可以使用 remember 实现:

   val result2 = remember(postList, keyWord) {
        postList.filter { it.contains(keyWord) }
    }

但是这样写意味着postListkeyWord任意一个变化时,Composable会重组,与之相比 derivedStateOf 只有当输出的 DerivedState变化才会导致Composable重组,所以当一个计算结果依赖较多的 State 时,derivedStateOf 有助于减少重组次数,提高性能。

derivedStateOf 只能监听block内的 state,一个非State类型的数据变化则可以通过rememberkey进行监听。

副作用的观察参数

很多副作用Api都允许指定观察参数key,当key变化时,执行中的副作用会终止。 key的频繁变化会影响执行效率。因此关于key的使用应当遵循以下原则: 当一个状态的变化需要造成副作用终止时,才将其添加为观察参数key, 否则应该将其通过rememberUpdatedState包装后,在副作用中使用,以避免打断执行中的副作用。

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit,
    onStop: () -> Unit,
) {
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            // 回调 currentOnStart() 或 currentOnStop()
            when(event) {
                Lifecycle.Event.ON_START -> currentOnStart()
                Lifecycle.Event.ON_STOP -> currentOnStop()
                else -> { }
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

上面的示例中,当 lifecycleOwner 变化时,需要终止对当前 lifecycleOwner 的监听,并重新注册Observer, 因此这里才将其添加为key。而 onStart 和 onStop 只要保证在回调它们时,可以获取最新的值即可,所以应该使用rememberUpdatedState包装,不应该作为观察参数因为它们的变动终止副作用执行。

什么情况下该用 derivedStateOf { } Or remember(key) { }

记住以下原则即可:

在这里插入图片描述

在这里插入图片描述

比如下图示例中,submitEnabled在输入框文本每次发生变化时都会被更新一次,每次都会触发重组流程

在这里插入图片描述

而如果换成 derivedStateOf { } 则只会触发三次更新:

在这里插入图片描述

下面代码模拟购物车增加商品数量:

@Composable
private fun DerivedStateOfExample() {
    var numberOfItems by remember { mutableStateOf(0) }

    Column(modifier = Modifier.padding(horizontal = 8.dp).height(100.dp)) {
        Surface {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text(text = "Amount to buy: $numberOfItems", modifier = Modifier.weight(1f))
                IconButton(onClick = { numberOfItems++ }) {
                    Icon(imageVector = Icons.Default.Add, contentDescription = "add")
                }
                Spacer(modifier = Modifier.width(4.dp))
                IconButton(onClick = { if (numberOfItems > 0) numberOfItems-- }) {
                    Icon(imageVector = Icons.Default.Remove, contentDescription = "remove")
                }
            }
        }

        Surface {
            val derivedStateMax by remember {
                derivedStateOf { numberOfItems > 5 }
            }

            if (derivedStateMax) {
                println("🤔 COMPOSING...")
                Text("You cannot buy more than 5 items",
                    color = Color(0xffE53935),
                    modifier = Modifier.fillMaxWidth().background(getRandomColor())
                )
            }
        }
        Surface {
            val derivedStateMax2 by remember(numberOfItems) {
                mutableStateOf(numberOfItems > 5)
            }

            if (derivedStateMax2) {
                println("🤔 COMPOSING...2")
                Text("You cannot buy more than 5 items",
                    color = Color(0xffE53935),
                    modifier = Modifier.fillMaxWidth().background(getRandomColor())
                )
            }
        }
    }
}

在这里插入图片描述
可以看出第一个derivedStateMax使用 derivedStateOf { } 方式的状态,当数量超过5之后,继续增加也不会再更新状态触发重组,而第二个derivedStateMax2 使用 remember(key) { } 方式的状态值每次都会更新状态触发重组。这个例子能很好的理解二者的区别。

还有一个典型的例子就是在列表中显示返回顶部的按钮:

@Composable
private fun DerivedStateOfSample2(scrollState: LazyListState) {
    val coroutineScope = rememberCoroutineScope()

    val firstItemVisible by remember {
        derivedStateOf { scrollState.firstVisibleItemIndex != 0 }
    }

    Box {
        LazyRow(
            state = scrollState,
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            content = {
                items(places) { place: Place ->
                    PlacesToBookComponent(place = place)
                }
            }
        )

        if (firstItemVisible) {
            FloatingActionButton(
                onClick = { coroutineScope.launch { scrollState.animateScrollToItem(0) } },
                modifier = Modifier.align(Alignment.BottomEnd),
                backgroundColor = Color(0xffE53935)
            ) {
                Icon(Icons.Filled.ArrowBack, null, tint = Color.White)
            }
        }
    }
}

这里使用 derivedStateOf { } 方式可以确保firstItemVisible不会因为列表不停的滚动而不停的被更新状态,而是只有firstVisibleItemIndex由非0变成0或者由0变成非0的那一刻才会更新firstItemVisible状态值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/162700.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Dolphin scheduler在Windows环境下的部署与开发

这里写自定义目录标题环境介绍WSL2工程下载修改POM文件java版本mysql驱动修改mysql密码IDEA配置JDK8模块导出运行配置环境介绍 MySql&#xff1a;8.0.31 JDK&#xff1a;17 需要安装windows的wsl2 WSL2 首先安装好WSL2&#xff0c;并且通过 sudo apt-get install openjdk-17…

类模板与模板类

#include <stdio.h>#include <iostream>using namespace std;//注意必须将类的声明和定义写在同一个.h文件中 未来把它包含进来//写上关键字template 和模板参数列表template<typename T, int KSize, int KVal>class MyArray{public:MyArray();//当在类内定义…

正点原子STM32(基于HAL库)2

目录STM32 基础知识入门寄存器基础知识STM32F103 系统架构Cortex M3 内核& 芯片STM32 系统架构存储器映射寄存器映射新建寄存器版本MDK 工程STM32 基础知识入门 寄存器基础知识 寄存器&#xff08;Register&#xff09;是单片机内部一种特殊的内存&#xff0c;它可以实现…

【自学Docker】Docker HelloWorld

Docker HelloWorld Docker服务 查看Docker服务状态 使用 systemctl status docker 命令查看 Docker 服务的状态。 haicoder(www.haicoder.net)# systemctl status docker我们使用 systemctl status docker 命令查看 Docker 服务的状态&#xff0c;显示结果如下图所示&#…

HotPDF Delphi PDF编译器形成PDF文档

HotPDF Delphi PDF编译器形成PDF文档 HotPDF Delphi PDF编译器支持通过内部和外部链接完全形成PDF文档。计算机还完全支持Unicode。此外&#xff0c;在您的产品和软件中使用此计算机的最新功能&#xff0c;您可以指定加密、打印和编辑PDF文档的能力。当您加密PDF文档时&#xf…

Markdown总结

为什么要使用Markdowm 什么是Markdown?为什么需要使用Markdown&#xff1f; Markdown 是一种轻量级标记语言&#xff0c;它允许人们使用易读易写的纯文本格式编写文档。 Markdown 语言在 2004 由约翰格鲁伯&#xff08;英语&#xff1a;John Gruber&#xff09;创建。 Markdo…

openEuler 社区 2022 年 12 月运作报告

社区活跃度在社区所有开发者和用户的共同参与下&#xff0c;openEuler的3年持续迸发活力&#xff01;从0到超过1.27万名开发者&#xff0c;从0到超过100万的社区用户&#xff0c;从0到超过750家企业伙伴加入社区……截至目前&#xff0c;在大家的持续贡献下&#xff0c;openEul…

GemBox.Bundle 47.0.1012 VS Spire.Office Platinum 8.1.1

GemBox.Bundle 是一个 .NET 组件包&#xff0c;使您能够简单高效地处理办公文件&#xff08;电子表格、文档、演示文稿和电子邮件&#xff09;。 使用我们的组件&#xff0c;您可以以易于使用的形式快速获得可靠的结果。只需要 .NET&#xff0c;因此您可以轻松部署您的应用程序…

收官!OceanBase第五届技术征文大赛获奖名单公布!

OceanBase 一直在思考&#xff0c;什么样的数据库对用户而言更易用&#xff1f; 更易用&#xff0c;除了功能完善、性能优秀、运行稳定的数据库系统&#xff0c;丰富多样的生态工具也必不可少。 作为一款完全自主研发的原生分布式数据库&#xff0c;OceanBase 的生态工具经历…

基于Java SSM springboot+VUE+redis实现的前后端分类版网上商城项目

基于Java SSM springbootVUEredis实现的前后端分类版网上商城项目 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言…

路由 OSPF LSA介绍、1~7类LSA详细介绍

1.0.0 路由 OSPF LSA介绍、1~7类LSA详细介绍 OSPF LSA 链路状态通告( Link status announcement)&#xff0c;作用于 向其它邻接OSPF路由器 传递拓扑信息与路由信息。 LSA如何去描述拓扑信息与路由信息的呢&#xff1f; 其实是基于不同类型LSA进行描述&#xff0c;而常见的LS…

EquiBind模型源码分析

EquiBind模型源码分析 使用提供的模型权重来预测你自己的蛋白质配体对的结合结构 第 1 步:你需要什么作为输入 mol2或.sdf或.pdbqt或.pdb格式的配体文件&#xff0c;其名称包含字符串配体(配体文件应包含所有氢)。 .pdb格式的受体文件&#xff0c;其名称包含字符串protein。我…

leetcode.1819 序列中不同最大公约数的数目 - gcd + 枚举

1819. 序列中不同最大公约数的数目 目录 1、java版 2、c版 思路&#xff1a; 有n个元素的数组&#xff0c;则其子序列有 个&#xff0c;而1 ≤ n ≤ &#xff0c;则不可能枚举每一个子序列计算它的gcd&#xff0c;那样会tle我们可以逆转思路&#xff0c;因为1 ≤ nums[i]…

PhysioNet2017数据集介绍

一、数据集下载 PhysioNet2017为短单导联心电图记录的房颤分类数据集&#xff0c;下载地址如下&#xff1a;https://www.physionet.org/content/challenge-2017/1.0.0/ 二、数据集介绍 PhysioNet2017数据集主要用于对记录是否显示正常窦性心律、心房颤动&#xff08;AF&…

背包问题= =

一、01背包有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i件物品的体积是 vi&#xff0c;价值是 wi。求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不超过背包容量&#xff0c;且总价值最大。输出最大价值。&#xff08;下图是例子&#xff0c;一下…

14、ThingsBoard-自定义华为云SMS规则节点

1、概述 一个物联网平台承载着很多设备的连接,当设备出现异常的时候,能够快速的通知到运维管理员是非常重要的,thingsboard提供了自定义配置邮箱,但是它对支持发送短信的不是很友好,都是国外的sms服务商,我反正是不用那个,在国内常见就是阿里、腾讯、华为、七牛常用的s…

【Python百日进阶-数据分析】Day224 - plotly漏斗图px.funnel()

文章目录一、语法二、参数三、返回值四、实例4.1 使用 plotly.express 的漏斗图4.1.1 基本漏斗图4.1.2 堆积漏斗图4.1.3 基本区域漏斗图4.2 使用 plotly.graph_objects 的漏斗图4.2.1 基本漏斗图4.2.2 设置标记大小和颜色4.2.3 堆积漏斗图4.2.4 基本区域漏斗图4.2.5 在区域漏斗…

Postgresql遇到的问题

解决问题之前最好先下载个pgadmin可视化工具&#xff0c;下面可能要用到。先看图&#xff0c;了解基本的\n 代表是当前数据库下面schemas的文件\du 代表你有这写用户&#xff08;角色&#xff09;&#xff0c;后面代表的权限\l 代表的是你的数据库在敲命令之前你德先进去docker…

excel表格技巧:Ctrl+T在超级表中的妙用

在介绍超级表的其它奇妙功能之前&#xff0c;小编先给大家说一个在创建超级表时应该注意的小细节。在创建超级表的对话框里&#xff0c;有一个“表包含标题”的选项&#xff0c;大家一定要记得勾选哦&#xff0c;不然超级表就会变成下面这样&#xff1a;超级表会自作聪明的在表…

【ROS】package.xml文件解析

文章目录 文章目录 前言 一、基本格式 1.基本结构 2.必要标签 3.依赖关系 二、Metapackage包 总结 前言 ros每个功能包中都有一个包清单&#xff0c;它是一个名为package.xml的XML 文件&#xff0c;它必须包含在任何符合 catkin 的包的根文件夹中。此文件定义有关包的属…