Android Compose入门和基本使用
文章目录
- 一、Jetpack Compose 介绍
- Jetpack Compose是什么
- Composable 函数
- 命令式和声明式UI
- 组合和继承
- 二、状态管理
- 什么是状态
- State
- remember
- 状态提升
- 三、自定义布局
- Layout Modifier
- Layout Composable
- 固有特性测量
- 使用内置组件固有特性测量
- 自定义固有特性测量
- 四、项目中使用Jetpack Compose
- Compose与View相互操作
- View中使用Compose
- Compose中使用View
- Compose和View的关系
- Jetpack Compose使用对比
- 相关资料链接:
- 五、总结
一、Jetpack Compose 介绍
Jetpack Compose是什么
Jetpack Compose是用于构建原生Android界面的新工具包,是一种声明式UI框架。
Composable 函数
Compose声明式UI的基础是Composable函数,通过定义一组接收数据而渲染界面元素的可组合函数来构建界面。换句话说,加上了Composeable函数,我们的函数就会变成一个UI。
举个例子:
Geeting函数接收一个String,并渲染出一个显示问候信息的Text
@Composable
fun Greeting(name: String) {Text("Hello, $name")
}
运行效果:
对于需要渲染成界面的函数,称之为可组合函数,有几个特点
- 此函数带有@Composable注解,表面他是一个可组合函数,所有组合函数都必须要带上这个注解
- 可组合函数需要在其他可组合函数的作用域内被调用
- 为了与普通函数区分,要求组合函数首字母大写
- 带上@Preview注解的Composeable函数,点击desgin选项,可以实时预览样式,不过函数不能带参数,不然预览不了
命令式和声明式UI
命令式UI:通过一系列明确的指令来控制用户界面的行为。需要手动指定每一步操作,以确保用户界面达到预期的状态。View视图体系就属于命令式编程,比如先获取到一个View的对象引用,再通过它的setXXX()方法去改变这个View的属性,以此来更改UI状态。手动操控视图比较容易遗漏,比如一条数据再多个地方呈现,很有可能会忘记去更新这个视图。
声明式UI:只需要负责声明描述UI,而不需要手动更新。我们只需要在描述一个页面的时候附带各个控件的状态,然后当有任何状态发生改变时,页面会自动刷新。可能会有疑问,那每次更新一个控件,都会刷新整个页面吗,其实不是,Compose采用了相似的优化策略,也就是重组。刷新页面时,只会更新那些状态发生改变的控件,那些状态没有发生改变的会跳过执行。
举个例子
当布局里面的文字发生变化的时候,怎么更新
左边命令式的写法:
右边声明式写法:不需要手动更新,页面会跟随数据的变化自动更新,只需要在初始化的时候加上by mutableStateOf,当text的值发生改变的时候,会自动更新页面。(属性委托)
这个时候会发现,数据和界面进行关联,界面跟着数据自动更新,不就是DataBinding吗,但是DataBinding只能更新界面上元素的值,而Compose不仅可以更新界面上的任何元素,不只是元素的值,还可以更新界面元素结构。
showImage发生变化时,界面自动改变,而在View中通常是用visibility进行视觉的隐藏,而这对UI的性能是有提升的,因为对visibility的设置会重新渲染布局,requestLayout
组合和继承
组合优先于继承,这是面向对象编程中的设计原则,比如:在View的体系中,View.java有超过3万行代码是比较臃肿的,比较臃肿的父类,会造成子类视图功能的不合理。举个例子,在Android View中常用的Button控件,为了让Button具备显示文字的功能,继承了TextView,但是也有很多不适合按钮的功能也被继承下来,后续随着TextView自身功能的迭代,Button可能引入更多的不必要功能。
而Jetpack Compose则是函数的方式,相互之间没有继承关系,Compose中的Button
Button(onClick = { /* 处理点击事件 */ },modifier = Modifier.wrapContentSize()
) {Text("I'm a button")
}
这样就将一个 Button 组件和一个 Text 组件组合起来,Button 本身的作用就是提供点击事件,Text 提供文本作用的,形成了一个带有文本的按钮。设计模式的角度来讲,各个组件职责更单一。
二、状态管理
什么是状态
状态是可以变化的任何值。在Compose函数中,如果数据状态没有发生改变,则UI永远不会自行改变。在Compose中,每个组件都是一个被@Composable修饰的函数,其状态就是函数的参数。因为Composeable是个函数,只能通过传参,当参数不变,则函数的输出就不会变,唯一的参数决定唯一输出。反言之,如果要让界面发生变化,则需要改变界面的状态,然后 Composable 响应这种变化。
举个例子:
一个简单的计数器,有个显示计数的空间,一个增加的按钮,每点击一次,则计数器加1,一个减少的按钮,每点击一次,计数器减一
View中状态
假设我们按照之前View的体系,可以这样写
class MainActivity : AppCompatActivity() {// ...override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// ...binding.incrementBtn.setOnClickListener {binding.tvCounter.text = "${Integer.valueOf(binding.tvCounter.text.toString()) + 1 }"}binding.decrementBtn.setOnClickListener {binding.tvCounter.text = "${Integer.valueOf(binding.tvCounter.text.toString()) - 1 }"}}
}
但是上面的计算逻辑和UI的耦合度比较高,可以再优化一下,就变成
class MainActivity : AppCompatActivity() {// ...private var counter: Int = 0override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// ...binding.incrementBtn.setOnClickListener {counter++updateCounter()}binding.decrementBtn.setOnClickListener {counter--updateCounter()}}private fun updateCounter() {binding.tvCounter.text = "$counter"}
}
这个主要改动在于,新增了counter变量用于计数,其实属于一种"状态提升",原本TextView的内部状态,上升到了Activity中,这样假设更好了UI,但是计数的逻辑还是可以复用。
还可以再优化,比如现在的计算逻辑在Activity中,无法在其他页面进行复用,进一步使用MVVM结构进行改造。引入ViewModel,将状态从Activity中提升到了ViewModel
class CounterViewModel: ViewModel() {private var _counter = MutableLiveData(0)val counter: LiveData<Int> =_counterfun incrementCounter() {_counter.value = _counter.value!! + 1}fun decrementCounter() {_counter.value = _counter.value!! - 1}
}class MainActivity : AppCompatActivity() {private val viewModel: CounterViewModel by viewModels<CounterViewModel>()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding.incrementBtn.setOnClickListener {viewModel.incrementCounter()}binding.decrementBtn.setOnClickListener {viewModel.decrementCounter()}viewModel.counter.observe(this){ count->binding.tvCounter.text = count}}
}
有Jetpack库使用经验还是比较熟悉上面的代码的,将状态提升到了ViewModel,使用LiveData包装起来,在Activity中监听状态的变化,从而自动刷新UI。
而在Compose中实现这样一个计数器。
@Composable
fun CounterPage() {Column(horizontalAlignment = Alignment.CenterHorizontally) {var counter = 0Text(text = "$counter")Button(onClick = { counter++ }) {Text(text = "increment")}Button(onClick = { counter-- }) {Text(text = "decrement")}}
}
这个时候无论怎么点击,Text显示的值还是为0,我们只是单纯的计数count,但是函数并没有被重新调用,刷新UI,所以Text显示的值没有发生变化。
在上面传统View中,我们改变状态,需要主动调用updateCounter方法去刷新UI,后面经过改造,我们把状态提升到了ViewModel,这样使用LiveData包装后,需要在Activity中监听状态的变化,才能对状态的变化进行响应。
Compose的刷新UI逻辑是:状态发生变化,触发了重组,函数被重新调用,由于参数发生了变化,函数输出改变了,最终渲染出的画面才发生变化。
State
在传统View中,需要使用LiveData将状态变量包装成一个可观察类型的对象。Compose也提供了可观察的状态类型,可变状态类型MutableState和不可变状态类型State。我们只需要使用State/MutableState将状态变量包装起来,这样就可以触发重组,方便的是在声明式UI框架中,不需要我们显示注册监听状态变化,框架自动实现了订阅关系。
@Composable
fun CounterPage() {Column(horizontalAlignment = Alignment.CenterHorizontally) {val counter: MutableState<Int> = mutableStateOf(0)Log.d("cyr", "counter text --> ${counter.value}")Text(text = "${counter.value}")Button(onClick = {Log.d("cyr", "increment button click ")counter.value++}) {Text(text = "increment")}Button(onClick = {Log.d("cyr", "decrement button click ")counter.value--}) {Text(text = "decrement")}}
}
打印日志
D counter text --> 0
D increment button click
D counter text --> 0
D decrement button click
D counter text --> 0
D increment button click
D counter text --> 0
D increment button click
D counter text --> 0
D decrement button click
D counter text --> 0
D decrement button click
D counter text --> 0
这里使用了mutableStateOf()方法初始化了一个MutableState类型的状态变量,并传入默认值0,使用的时候通过调用counter.value。
通过日志发现,点击按钮Text(text = “${counter.value}”)会重新执行,也就是发生了重组,但是执行的时候参数没有改变,依然是0,要解决这个问题,则需要使用Compose的一个函数,remember
remember
remember函数的源码
/*** Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.* Recomposition will always return the value produced by composition.*/
@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T = currentComposer.cache(false, calculation)
可以将remember看作是为函数提供单个对象的存储空间,其作用就是对其包裹起来的变量值进行缓存,后续发生重组过程中,不会重新初始化,而是直接从缓存中读取。
使用remember向自身添加内存,然后在内存中存储MutableStateOf,以创建MutableState< String >,对Value进行任何更改都会自动重组用于读取此状态的所有可组合函数。
@Composable
fun CounterPage() {Column(horizontalAlignment = Alignment.CenterHorizontally) {val counter: MutableState<Int> = remember { mutableStateOf(0) }Text(text = "${counter.value}")Button(onClick = {counter.value++}) {Text(text = "increment")}Button(onClick = {counter.value--}) {Text(text = "decrement")}}
}
再次运行,正常加减
状态提升
我们希望事件始终向上流动,而状态始终向下流动,状态提升,做法就是内部状态移除,以参数的形式传入。以及需要回调给调用方的事件,也以参数形式传入。
改造一下:
@Composable
fun CounterPage(counter: Int, onIncrement: () -> Unit, onDecrement: () -> Unit) {Column(horizontalAlignment = Alignment.CenterHorizontally) {Text(text = "$counter")Button(onClick = {onIncrement()}) {Text(text = "increment")}Button(onClick = {onDecrement()}) {Text(text = "decrement")}}
}
这样CounterPage函数变成一个无状态函数,有助于单向数据流打造(确保数据只能从一个方向流动:从父组件流向子组件)
三、自定义布局
布局阶段用来对视图树中每个LayoutNode进行宽高尺寸测量并完成位置摆放。
在Compose中,每个LayoutNode都会根据来自父LayoutNode的布局约束进行自我测量(类似传统View的MeasureSpec)。布局约束中包含了父LayoutNode允许子LayoutNode的最大宽高和最小宽高,当父LayoutNode希望子LayoutNode测量的宽高为某个具体值时,约束中的最大宽高和最小宽高就是相同的。LayoutNode不允许被多次测量,在Compose中多次测量会抛异常,有些需求场景需要多次测量LayoutNode,Compose提供了固有特性测量。
Layout Modifier
使用Modifier.layout()手动控制元素的测量和布局。通常layout修饰符的使用方法
fun Modifier.customLayoutModifier(...)
= Modifier.layout { measurable, constraints ->...
})
使用layout修饰符的时候,传入的回调Lambda包含两个参数:measurable,constraints
measureable:提供api完成测量与布局的过程
constraints:子元素的测量约束,包裹宽度和高度的最大值和最小值
例子:
fun Modifier.firstBaselineToTop(firstBaselineToTop: Dp
) = layout { measurable, constraints ->//测量元素val placeable = measurable.measure(constraints)//测量之后,获取元素的基线值val firstBaseLine = placeable[FirstBaseline]//元素新的Y坐标 = 新基线值 - 旧基线值val placeableY = firstBaselineToTop.roundToPx() - firstBaseLineval height = placeable.height + placeableYlayout(placeable.width, height) {//设置元素的位置placeable.placeRelative(0, placeableY)}
}@Composable
fun TextWithPaddingToBaseLine(){Test2Theme {Text(text = "Hi there!",Modifier.firstBaselineToTop(24.dp).background(Color.Red))}
}
Layout Composable
LayoutModifier会将当前元素的所有子元素视为整体进行统一的测量和布局,更多适用于统一处理的场景。当我们需要测量布局中每一个子组件,类似于自定义ViewGroup,需要使用Layout Composable。
@Composable
fun CustomLayout(modifier: Modifier = Modifier,// custom layout attributes content: @Composable () -> Unit
) {Layout(modifier = modifier,content = content) { measurables, constraints ->// measure and position children given constraints logic here}
}
modifier:由外部传入的修饰符,会决定该UI元素的constraints
content:在content中声明所有子元素信息
例子:
@Composable
fun MyOwnColumn(modifier: Modifier = Modifier,content: @Composable () -> Unit
) {Layout(modifier = modifier,content = content) { measurables, constraints ->val placeables = measurables.map { measurable ->measurable.measure(constraints)}var yPosition = 0layout(constraints.maxWidth, constraints.maxHeight) {placeables.forEach { placeable ->// placeRelative 会根据当前 layoutDirection 自动调整子元素的位置(从左至右或从右至左)placeable.placeRelative(x = 0, y = yPosition)yPosition += placeable.height}}}
}@Composable
fun BodyContent(modifier: Modifier = Modifier) {MyOwnColumn(modifier.padding(8.dp)) {Text("MyOwnColumn")Text("places items")Text("vertically.")Text("We've done it by hand!")}
}
固有特性测量
Compose的每一个组件不允许多次进行测量的,多次测量在运行时会抛异常,但是有些情况需要进行多次测量,比如有种场景,希望中间分割线高度与两边文案的高度保持相等。
采取的方案可以预先获取到两边文案组件高度信息,然后计算两边高度的最大值,即可确定当前父组件的高度值,只需让分割线的高度占满整个父组件即可。
使用内置组件固有特性测量
@Composable
fun Greeting() {MyOwnRow(modifier = Modifier.height(IntrinsicSize.Min)) { //使用了自定义的固有测量属性,不然Divider高度占满屏幕Text(text = "Hello Kotlin",fontSize = 10.sp)Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp)//.layoutId("Divider"))Text(text = "Hello Android",fontSize = 18.sp)}}
自定义固有特性测量
需要额外重写measurePolicy中的固有特性测量Intrinsic系列方法。
当使用Modifier.width(IntrinsicSize. Max)时,在测量阶段便会调用maxIntrinsicWidth方法,以此类推。
Row组件同样的固有特性效果。因为我们的需求场景只使用了Modifier.height(IntrinsicSize. Min),所以仅重写minIntrinsicHeight方法就可以了。
@Composable
fun Greeting() {MyOwnRow(modifier = Modifier.height(IntrinsicSize.Min)) { //使用了自定义的固有测量属性Text(text = "Hello Kotlin",fontSize = 10.sp //字体大小不一样,高度不一样)Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp)//.layoutId("Divider"))Text(text = "Hello Android",fontSize = 18.sp //字体大小不一样,高度不一样)}}@Composable
fun MyOwnRow(modifier: Modifier = Modifier, content: @Composable () -> Unit) {Layout(modifier = modifier, content = content, measurePolicy = object : MeasurePolicy {override fun MeasureScope.measure(measurables: List<Measurable>,constraints: Constraints): MeasureResult {val placeables = measurables.map { measurable ->//给控件设置layoutId可以直接找到控件,自定义测量规则//measurable.layoutId=="Divider"measurable.measure(constraints)}var positionX = 0return layout(constraints.maxWidth, constraints.maxHeight) {placeables.forEach { placeable ->placeable.placeRelative(positionX, 0)positionX += placeable.width}}}override fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int {var maxHeight = 0measurables.forEach {//找出最大的高度并赋值给maxHeight//遍历每个子组件,计算其在最大宽度下的最小固有高度,并确保最终的最大高度至少为当前的最大高度maxHeight = it.minIntrinsicHeight(width).coerceAtLeast(maxHeight)}return maxHeight}})
}
固有特性测量的本质就是允许父组件预先获取到每个子组件宽高信息后,影响自身在测量阶段获取到的constraints宽高信息,从而间接影响子组件的测量过程。在上面的例子中我们通过预先测量文案子组件的高度,从而确定了父组件在测量时获取到的constraints高度信息,并根据这个高度指定了分割线高度。
四、项目中使用Jetpack Compose
1.检查AGP版本,确保升级是新版本,最低4.2
2.检查Kotlin版本,具体可查看
https://developer.android.com/jetpack/androidx/releases/compose-kotlin
例如:kotlin版本是1.9.0对应compose Compiler版本是1.5.0/1.5.1
3.修改配置信息
3.1 启用compose
buildFeatures {compose true}
3.2 配置Compose Compiler版本
composeOptions {kotlinCompilerExtensionVersion '1.5.0'
}
kotlin版本对应
3.3 确保JVM版本为Java 8
3.4 添加依赖
dependencies {// Integration with activitiesimplementation 'androidx.activity:activity-compose:版本号'// Compose Material Designimplementation 'androidx.compose.material:material:版本号 '// Animationsimplementation 'androidx.compose.animation:animation:版本号' implementation 'androidx.compose.ui:ui:版本号'implementation 'androidx.compose.ui:ui-graphics:版本号'implementation 'androidx.compose.ui:ui-tooling-preview:版本号'// Integration with ViewModelsimplementation 'androidx.lifecycle:lifecycle-viewmodel-compose:版本号'// UI TestsandroidTestImplementation 'androidx.compose.ui:ui-test-junit4:版本号'
}
Compose与View相互操作
View中使用Compose
使用ComposeView作为桥梁
xml文件中加入ComposeView
例如:
<?xml version="1.0" encoding="utf-8"?>
<com.example.test.nestedscroll.MyNestedScrollViewxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:id="@+id/nestedscrollview"android:layout_height="match_parent"android:orientation="vertical"><androidx.appcompat.widget.LinearLayoutCompatandroid:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/linearLayoutCompat"android:orientation="vertical">//使用composeView<androidx.compose.ui.platform.ComposeViewandroid:id="@+id/compose_image_view"android:layout_width="wrap_content"android:layout_height="wrap_content"/><LinearLayoutandroid:id="@+id/tablayout_viewpager"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><com.google.android.material.tabs.TabLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:id="@+id/tablayout"/><androidx.viewpager2.widget.ViewPager2android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/viewpager_view"/></LinearLayout></androidx.appcompat.widget.LinearLayoutCompat></com.example.test.nestedscroll.MyNestedScrollView>
//代码中,先根据 id 查找出来,再 setContent 即可:
val imageView = findViewById<ComposeView>(R.id.compose_image_view)
imageView.setContent {ComposeImageView()
}
@Composable
fun ComposeImageView(){Image(painter = painterResource(R.drawable.ic_test1),contentDescription = null,contentScale = ContentScale.Crop,modifier = Modifier.fillMaxSize().height(300.dp))
}
动态添加
val linearLayoutCompat =findViewById<LinearLayoutCompat>(R.id.linearLayoutCompat)
linearLayoutCompat.addView(ComposeView(this@MainActivity3).apply {setContent {ComposeImageView()}
},0)
直接在 ComposeView
中添加view视图,会抛出 Cannot add views to ComposeView; only Compose content is supported 错误
//1
<androidx.compose.ui.platform.ComposeViewandroid:id="@+id/tablayout_viewpager"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><com.google.android.material.tabs.TabLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:id="@+id/tablayout"/><androidx.viewpager2.widget.ViewPager2android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/viewpager_view"/></androidx.compose.ui.platform.ComposeView>//2
val composeView = ComposeView(context)
composeView.addView(TextView(context))
Compose中使用View
1.有少数View暂时没有Compose版本,比如WebView,MapView
2.之前有写好的UI不想动,想直接拿过来用
Compose中使用AndroidView
例如:
@Composable
fun MyTextView(text: String) {AndroidView(fatory = { context ->TextView(context).apply {setText(text)}},update = { view -> view.setText(text)})
}
这个桥梁是AndroidView
@Composable
@UiComposable
fun <T : View> AndroidView(factory: (Context) -> T,modifier: Modifier = Modifier,update: (T) -> Unit = NoOpUpdate
) {AndroidView(factory = factory,modifier = modifier,update = update,onRelease = NoOpUpdate)
}
factory接收一个Context参数,用来构建一个View,update方法是一个callback,inflate之后会执行,读取状态值变化之后,也会被执行。
例如:
@Composable
fun MyComposeWithButton() {// 定义一个可变状态变量var buttonText by remember { mutableStateOf("Click Me") }Box(modifier = Modifier.fillMaxSize()) {// 使用 AndroidView 来嵌入 ButtonAndroidView(factory = { context ->Button(context).apply {setText(buttonText)setOnClickListener {buttonText = "Clicked"}}},modifier = Modifier.fillMaxSize(),update = { button ->// 当 buttonText 发生变化时,更新 Button 的文本button.text = buttonText})// 添加一个按钮,点击时重置文本Button(onClick = { buttonText = "Reset Text" },modifier = Modifier.align(alignment = androidx.compose.ui.Alignment.BottomCenter)) {androidx.compose.material3.Text("Reset Text")}}
}
Compose和View的关系
窗口视图架构
Activity的具体实现类是PhoneWindow,在Activity执行attach的时候,会创建一个PhoneWindow对象。PhoneWindow作为装载根视图DecordView的顶级容器,Activity通过setContentView实际上是调用PhoneWindow来创建DecordView,并解析xml布局加载到DecordView的contentView部分。
Compose调用setContent其实就是往ContentView里面放布局
通过android studio的LayoutInspector看到ComposeActivity的布局结构
·
public fun ComponentActivity.setContent(parent: CompositionContext? = null,content: @Composable () -> Unit
) {//decorView 的第一个子View如果是 ComposeView 直接用,如果不是就创建一个ComposeView ,然后添加到跟布局val existingComposeView = window.decorView.findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as? ComposeViewif (existingComposeView != null) with(existingComposeView) {...setContent(content)} else ComposeView(this).apply {...setContent(content)...setOwners()setContentView(this, DefaultActivityContentLayoutParams)}
}
decorView 的第一个子View如果不是ComposeView就创建一个 ,然后添加到跟布局。而ComposeView 里又通过 Wrapper_android.kt 创建了一个 AndroidComposeView。AndroidComposeView 是继承ViewGroup。dispatchDraw方法也是通过Canvas画布进行绘制。
我们点开任何一个Compose组件函数,一系列调用最终都会到了Layout.kt的Layout()方法,Layout() 核心是调用ReusableComposeNode ()方法。这里有个参数 factory,factory 是一个构造器函数, factory 被调用就会创建一个LayoutNote,定义的布局属性modifier也在这里设置给了LayoutNode。每个组件最终都是一个LayoutNote
AndroidComposeView中有个root节点,而每个ComposeView的组件内部其实是一个LayoutNode,会添加到Root节点中,形成一个组件树
Wrapper.android.kt
具体测量源码分析参考文章链接
https://juejin.cn/post/6981805443219718151
Jetpack Compose使用对比
官网示例:https://zhuanlan.zhihu.com/p/386826633
相关资料链接:
xml对应可替换Compose的View
https://www.jetpackcompose.app/What-is-the-equivalent-of-RecyclerView-in-Jetpack-Compose
compose对应kotlin版本
https://developer.android.com/jetpack/androidx/releases/compose-kotlin
官方文档
https://developer.android.com/develop/ui/compose/documentation?hl=zh-cn
五、总结
Compose重新定义了Android UI的开发方式,提升了开发效率。
- 声明式UI,不需要手动刷新数据
- 取消XML,完成消除了混合写法的(XML+Java、kotlin)的局限性
- 兼容性,大多数的Jetpack库(如ViewModel,LiveData等)以及Kotlin协程都适用于Compose,也能和现有的View体系共存,一个View项目中引入Compose
- 更简单的动画和触摸事件Api,Compose提供了很多可以直接用的Material组件
- 简化代码的数量,比如RecyclerView需要的Adapter适配器,使用LazyColumn,减少一些bug
相关文章:
Android Compose入门和基本使用
文章目录 一、Jetpack Compose 介绍Jetpack Compose是什么Composable 函数命令式和声明式UI组合和继承 二、状态管理什么是状态Stateremember状态提升 三、自定义布局Layout ModifierLayout Composable固有特性测量使用内置组件固有特性测量自定义固有特性测量 四、项目中使用J…...
xLua的Lua调用C#的2,3,4
使用Lua在Unity中创建游戏对象,组件: 相关代码如下: Lua --Lua实例化类 --C# Npc objnew Npc() --通过调用构造函数创建对象 local objCS.Npc() obj.HP100 print(obj.HP) local obj1CS.Npc("admin") print(obj1.Name)--表方法希…...
使用 Python 连接 PostgreSQL 数据库,从 `mimic - III` 数据库中筛选数据并导出特定的数据图表
要使用 Python 连接 PostgreSQL 数据库,从 mimic - III 数据库中筛选数据并导出特定的数据图表,你可以按照以下步骤操作: 安装所需的库:psycopg2 用于连接 PostgreSQL 数据库,pandas 用于数据处理,matplot…...
算法刷题记录——LeetCode篇(2.6) [第151~160题](持续更新)
更新时间:2025-04-06 算法题解目录汇总:算法刷题记录——题解目录汇总技术博客总目录:计算机技术系列博客——目录页 优先整理热门100及面试150,不定期持续更新,欢迎关注! 152. 乘积最大子数组 给你一个…...
Dijkstra求最短路径问题(优先队列优化模板java)
首先 1. 主类定义与全局变量 public class Main {static int N 100010; // 最大节点数static int INF Integer.MAX_VALUE; // 无穷大static ArrayList<Pair>[] G new ArrayList[N]; // 邻接表存储图static int[] dis new int[N]; // 存储每个节点的最短…...
【软件测试】性能测试 —— 基础概念篇
🥰🥰🥰来都来了,不妨点个关注叭! 👉博客主页:欢迎各位大佬!👈 本期内容主要介绍性能测试相关知识,首先我们需要了解性能测试是什么,本期内容主要介绍性能测试…...
Jmeter脚本使用要点记录
一,使用Bean shell获取请求响应的数据 byte[] result prev.getResponseData(); String str new String(result); System.out.println(str);其中,prev是jmeter的内置变量,直接使用即可。 二,不同的流程中传参数 vars.put(&quo…...
HTML5
HTML5是对HTML标准的第5次修订 HTML是超文本标记语言的简称,是为【网页创建和其它可在网页浏览器中所看到信息】而设计的一种标记性语言。 H5优点:跨平台使用将互联网语义化,更好地被人类与机器所理解降低了对浏览器的依赖,更好地…...
算法—博弈问题
1.博弈问题 1.前提:每一步都是最优解的情况下,先手的那个人已经确定了胜负 用dp数组记录每一步操作后的结果,如果下一步会出现必输结果,那么说明执行这步操作的人必胜,因为必输结果的下一步操作后都是必胜的结果,所以在…...
vector模拟实现(2)
1.构造函数 2.拷贝构造 我们利用push_back和reserve来实现拷贝构造。 3.迭代器的实现 由于底层是一段连续的空间,所以我们选择用指针来实现迭代器。 4.swap 这里的swap函数是有两种方法,一种是开辟一段新的空间,然后memcpy来把原来的数据拷…...
【嵌入式系统设计师】知识点:第3章 嵌入式硬件设计
提示:“软考通关秘籍” 专栏围绕软考展开,全面涵盖了如嵌入式系统设计师、数据库系统工程师、信息系统管理工程师等多个软考方向的知识点。从计算机体系结构、存储系统等基础知识,到程序语言概述、算法、数据库技术(包括关系数据库、非关系型数据库、SQL 语言、数据仓库等)…...
输入框输入数字且保持精度
在项目中如果涉及到金额等需要数字输入且保持精度的情况下,由于输入框是可以随意输入文本的,所以一般情况下可能需要监听输入框的change事件,然后通过正则表达式去替换掉不匹配的文本部分。 由于每次文本改变都会被监听,包括替换…...
Vue3中的Inject用法全解析
大家好呀~今天给大家带来一个超级实用的Vue3技巧:如何使用inject进行组件间的通信!如果你对组件间的数据传递、事件触发感兴趣,那一定不要错过这篇文章哦!话不多说,直接开整~ 🌟 什么…...
FPGA同步复位、异步复位、异步复位同步释放仿真
FPGA同步复位、异步复位、异步复位同步释放仿真 xilinx VIVADO仿真 行为仿真 综合后功能仿真,综合后时序仿真 实现后功能仿真,实现后时序仿真 目录 前言 一、同步复位 二、异步复位 三、异步复位同步释放 总结 前言 本文将详细介绍FPGA同步复位、异…...
深度解析需求分析:理论、流程与实践
深度解析需求分析:理论、流程与实践 一、需求分析的目标(一)准确捕捉用户诉求(二)为开发提供清晰指引 二、需求分析流程(一)需求获取(二)需求整理(三…...
QT学习笔记4--事件
1. 鼠标事件 1.1 鼠标按下 QObject中的mousePressEvent()方法 在子类中重写该方法,就可以处理鼠标按下 void myLabel::mousePressEvent(QMouseEvent *ev) {if (ev->button() Qt::LeftButton) {QString str QString("mouse press x %1, y %2").…...
AnimateCC基础教学:json数据结构的测试
一.核心代码: const user1String {"name": "张三", "age": 30, "gender": "男"}; const user1Obj JSON.parse(user1String); console.log("测试1:", user1Obj.name, user1Obj.age, user1Obj.gender);/*const u…...
针对Qwen-Agent框架的源码阅读与解析:FnCallAgent与ReActChat篇
在《针对Qwen-Agent框架的Function Call及ReAct的源码阅读与解析:Agent基类篇》中,我们已经了解了Agent基类的大体实现。这里我们就再详细学习一下FnCallAgent类和ReActChat的实现思路,从而对Agent的两条主流技术路径有更深刻的了解。同时&am…...
在docker中安装RocketMQ
第一步你需要有镜像包,这个2023年的时候docker就不能用pull拉取镜像了,需要你自己找 第二步我用的是FinalShell,用别的可视化界面也用, 在你自己平时放镜像包的地方创建一个叫rocketmq的文件夹,放入镜像包后,创建一个…...
Spring Boot + Kafka 消息队列从零到落地
背景 依赖 <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>2.8.1</version> </dependency> 发送消息 //示例: private final KafkaTemplate<St…...
《打破语言壁垒:bilingual_book_maker 让外文阅读更轻松》
在寻找心仪的外文电子书时,常常会因语言障碍而感到困扰。虽然可以将文本逐段复制到在线翻译工具中,但这一过程不仅繁琐,还会打断阅读的连贯性,让人难以沉浸其中。为了克服这一难题,我一直在寻找一种既能保留原文&#…...
JCR一区文章,壮丽细尾鹩莺算法Superb Fairy-wren Optimization-附Matlab免费代码
本文提出了一种新颖的基于群体智能的元启发式优化算法——壮丽细尾鹩优化算法(SFOA),SFOA从精湛的神仙莺的生活习性中汲取灵感。融合了精湛的神仙莺群体中幼鸟的发育、繁殖后鸟类喂养幼鸟的行为以及它们躲避捕食者的策略。通过模拟幼鸟生长、繁殖和摄食阶…...
Kafka 如何实现 Exactly Once
Kafka 中实现 Exactly Once Semantics(EOS,精确一次语义),是为了确保: 每条消息被处理一次且仅一次,既不会丢失,也不会重复消费。 这是一种在分布式消息系统中非常难实现的语义。Kafka 从 0.11 …...
在K8S中,内置的污点主要有哪些?
在Kubernetes (K8S)中,内置的污点(Taints)主要用于自动化的节点亲和性和反亲和性管理。当集群中的节点出现某种问题或满足特定条件时,kubelet会自动给这些节点添加内置污点。以下是一些常见的内置污点: node.kubernete…...
AI大模型:(二)2.1 从零训练自己的大模型概述
目录 1. 分词器训练 1.1 分词器概述 1.2 训练简述 2.预训练 2.1 预训练概述 2.2 预训练过程简介 3.微调训练 3.1 微调训练概述 3.2 微调过程简介 4.人类对齐 4.1 人类对齐概述 4.2 人类对齐训练过程简介 近年来,大语言模型(LLM)如GPT-4、Claude、LLaMA等…...
电动垂直起降飞行器(eVTOL)
电动垂直起降飞行器(eVTOL)的详细介绍,涵盖定义、技术路径、应用场景、市场前景及政策支持等核心内容: 一、定义与核心特性 eVTOL(Electric Vertical Take-off and Landing)即电动垂直起降飞行器…...
LM Studio本地部署大模型
现在的AI可谓是火的一塌糊涂, 看到使用LM Studio部署本地模型非常的方便, 于是我也想在自己的本地试试 LM Studio 简介 LM Studio 是一款专为本地运行大型语言模型(LLMs)设计的桌面应用程序,支持 Windows 和 macOS 系统。它允许用户在个人电…...
PyTorch 深度学习 || 6. Transformer | Ch6.1 Transformer 框架
1. Transformer 框架...
SLAM文献之-SLAMesh: Real-time LiDAR Simultaneous Localization and Meshing
SLAMesh 是一种基于 LiDAR 的实时同步定位与建图(SLAM)算法,其核心创新点在于将定位与稠密三维网格重建相结合,通过动态构建和优化多边形网格(Mesh)来实现高精度定位与环境建模。以下是其算法原理的详细解析…...
[Python] 位置相关的贪心算法-刷题+思路讲解版
位置贪心-题目目录 例题1 - 香蕉商人编程实现输入描述输出描述思路AC代码 例题2 - 分糖果编程实现输入描述输入样例输出样例思路AC代码 例题4 - 分糖果II编程实现输入描述输出描述输入样例思路AC代码 例题3 - 分糖果III编程实现输入描述输出描述输入样例输出样例思路AC代码 例题…...
练习题:125
目录 Python题目 题目 题目分析 需求理解 关键知识点 实现思路分析 代码实现 代码解释 导入 random 模块: 指定范围: 生成随机整数: 输出结果: 运行思路 结束语 Python题目 题目 生成一个指定范围内的随机整数。 …...
实战设计模式之迭代器模式
概述 与上一篇介绍的解释器模式一样,迭代器模式也是一种行为设计模式。它提供了一种方法来顺序访问一个聚合对象中的各个元素,而无需暴露该对象的内部表示。简而言之,迭代器模式允许我们遍历集合数据结构中的元素,而不必了解这些集…...
Spring-AOP详解(AOP概念,原理,动态代理,静态代理)
目录 什么是AOP:Spring AOP核心概念需要先引入AOP依赖:1.切点(Pointcut):2.连接点:3.通知(Advice):4.切面: 通知类型:Around:环绕通知,此注解标注的通知方法在目标方法前,…...
【dify应用】将新榜排行数据免费保存到飞书表格
新榜中导出数据是收费的,如何免费导出呢 接口分析 切换分类排行,数据是在这个接口中请求的 参数: {"rankType":1,"rankDate":"2025-04-05","type":["财富"],"size":25,"…...
【Linux】线程池详解及基本实现
📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! &…...
基于论文的大模型应用:基于SmartETL的arXiv论文数据接入与预处理(一)
1. 背景 arXiv简介(参考DeepSeek大模型生成内容): arXiv(发音同“archive”,/ˈɑːrkaɪv/)是一个开放的学术预印本平台,主要用于研究人员分享和获取尚未正式发表或已完成投稿的学术论文。创…...
Leetcode 3508. Implement Router
Leetcode 3508. Implement Router 1. 解题思路2. 代码实现 题目链接:3508. Implement Router 1. 解题思路 这一题就是按照题意写作一下对应的函数即可。 我们需要注意的是,这里,定义的类当中需要包含以下一些内容: 一个所有i…...
Nmap全脚本使用指南!NSE脚本全详细教程!Kali Linux教程!(六)
脚本类别 discovery(发现) sip-methods 已演示过。这里不再演示。 436. smb-enum-domains 尝试枚举系统上的域及其策略。这通常需要凭据,但 Windows 2000 除外。除了实际域之外,通常还会显示“内置”域。Windows 在域列表中返…...
了解适配器模式
目录 适配器模式定义 适配器模式角色 适配器模式的实现 适配器的应用场景 适配器模式定义 适配器模式,也叫包装模式。将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。 简单来说就是目标类不能直接…...
C语言:几种字符串常用的API
字符串的常用操作 C 语言的标准库 <string.h> 提供了很多用于处理字符串的函数。 1. strlen - 计算字符串长度 size_t strlen(const char *str);功能:计算字符串 str 的长度,不包含字符串结束符 \0。 2.strcpy - 复制字符串 char *strcpy(char…...
Django构建安全中间件实用示例
Django安全中间件实用指南 推荐超级课程: 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战目录 Django安全中间件实用指南什么是Django中的中间件?Django中的安全中间件特性配置示例配置示例配置示例示…...
排序算法(快速排序,选择排序......)【泪光2929】
hello,大家好!今天给大家分享一下各种排序: 1,选择排序 首先从原始数组中 选择最小的1个数据,将其和位于第1个位置的数据交换。接着从剩下的n-1个数据中选择次小的1个元素,将其和第2个位置的数据交换然后…...
UE5学习记录part14
第17节 enemy behavior 173 making enemies move: AI Pawn Navigation 按P查看体积 So its very important that our nav mesh bounds volume encompasses all of the area that wed like our 因此,我们的导航网格边界体积必须包含我们希望 AI to navigate in and …...
树莓派llama.cpp部署DeepSeek-R1-Distill-Qwen-1.5B
树莓派的性能太低了,我们需要对模型进行量化才能使用,所以现在的方案是,在windows上将模型格式和量化处理好,然后再将模型文件传输到树莓派上。而完成上面的操作就需要部署llama.cpp。 三、环境的准备 这里要求大家准备…...
Llama 4 最新发布模型分析
1. 引言 在2025年4月5日,Meta公司正式发布了最新一代大型语言模型Llama 4系列,包括Llama 4 Scout和Llama 4 Maverick。该模型添加了多模态支持,能够处理文本、图像、音频和视频数据,实现更加充分的AI功能应用。 2. 技术特性 2.1…...
Llama 4 家族:原生多模态 AI 创新的新时代开启
0 要点总结 Meta发布 Llama 4 系列的首批模型,帮用户打造更个性化多模态体验Llama 4 Scout 是有 170 亿激活参数、16 个专家模块的模型,同类中全球最强多模态模型,性能超越以往所有 Llama 系列模型,能在一张 NVIDIA H100 GPU 上运…...
如何让eDrawings html文件在Chrome浏览器上展示——allWebPlugin中间件扩展
应用背景 eDrawing html文件是仅可在 Internet Explorer 5.5 和以上版本中查阅,由于IE浏览器限制,目前使用非常不方便,为了不修改html的请提下,在chrome浏览器查阅原本html文件,可使用安装allWebPlugin中间件扩展。 a…...
【内网安全】DHCP 饿死攻击和防护
正常情况:PC2可以正常获取到DHCP SERVER分别的IP地址查看DHCP SERCER 的ip pool地址池可以看到分配了一个地址、Total 253个 Used 1个 使用kali工具进行模拟攻击 进行DHCP DISCOVER攻击 此时查看DHCP SERVER d大量的抓包:大量的DHCP Discover包 此时模…...
keepalived高可用介绍
keepalived 是 Linux 一个轻量级的高可用解决方案,提供了心跳检测和资源接管、检测集群中的系统服务,在集群节点间转移共享IP 地址的所有者等。 工作原理 keepalived 通过 VRRP(virtual router redundancy protocol)虚拟路由冗余…...
基于大模型的脑梗死全流程诊疗技术方案
目录 《基于大模型的脑梗死全流程诊疗技术方案》一、核心算法实现1. 多模态特征融合算法(术前规划)2. 术中实时预警算法二、系统模块设计1. 术前规划系统流程图2. 术中实时监控系统架构三、技术验证方案1. 模型验证矩阵2. 实验验证设计四、关键技术创新点五、工程实现规范1. …...