Jetpack Compose Runtime : 声明式 UI 的基础

简介: Jetpack Compose Runtime : 声明式 UI 的基础

目前有一个正在进行的 Jetpack Compose中文手册 项目,旨在帮助开发者更好的理解和掌握Compose框架,目前仍还在开荒中,欢迎大家关注与加入! 本文已经收录到该手册中,欢迎查阅

compose.runtime

Jetpack Compose 不只是一个 UI 框架,更是一个通用的 NodeTree 管理引擎。本文介绍 compose.runtime 如何通过 NodeTreecompose.ui 提供支持。

大家知道 Jetpack Compose 不仅限于在 Android 中使用 ,Compose For Desktop、 Compose For Web 等项目也已相继发布,未来也许还会出现 Compose For iOS 。Compose 能够在不同平台上实现相似的声明式UI开发体验,这得益于其分层的设计。

Compose 在代码上自下而上依次分为6层:

Modules Description
compose.compiler 基于 Kotlin compiler plugin 对 @Composable 进行编译期代码生成和优化
compose.runtime 提供 NodeTree管理、State管理等,声明式UI的基础运行时
compose.ui Android设备相关的基础UI能力,例如 layout、measure、drawing、input 等
compose.foundation 通用的UI组件,包括 Column、Row 等容器、以及各种 Shape 等
compose.animation 负责动画的实现、提升用户体验
compose.material 提供符合 Material Design 标准的UI组件

image.png

其中 compose.runtimecompose.compiler 最为核心,它们是支撑声明式UI的基础。

Jake Wharton 在他的博客提到 :

What this means is that Compose is, at its core, a general-purpose tool for managing a tree of nodes of any type. Well a “tree of nodes” describes just about anything, and as a result Compose can target just about anything.

- jakewharton.com/a-jetpack-c…

compose.runtime 提供了 NodeTree 管理等基础能力,此部分与平台无关,在此基础上各平台只需实现UI的渲染就是一套完整的声明式UI框架。而 compose.compiler 通过编译期的优化,帮助开发者书写更简单的代码调用 runtime 的能力。


从 Composable 到 NodeTree

“Compose 也好,React、Flutter 也好,其代码本质上都是对一颗树形结构的描述。”

所谓“数据启动UI",就是当state变化时,重建这颗树型结构并基于这棵NodeTree刷新UI。 当然,处于性能考虑,当 NodeTree 需要重建时,各框架会使用 VirtualDom 、GapBuffer(或称SlotTable) 等不同技术对其进行“差量”更新,避免“全量”重建。compose.runtime 的重要工作之一就是负责 NodeTree 的创建与更新。

image.png

如上,React 基于 VDOM "差量" 更新右侧的DOM树。

Compose 中的 NodeTree

对于 OOP 语言,我们通常使用如下方式描述一颗树:

fun TodoApp(items: List<TodoItem>): Node {
  return Stack(Orientation.Vertical).apply {
    for (item in items) {
      children.add(Stack(Orientation.Horizontal).apply {
        children.add(Text(if (item.completed) "x" else " "))
        children.add(Text(item.title))
      })
    }
  }
}

TodoApp 返回 Node 对象,可以被父 Node 继续 add,循环往复构成一棵完整的树。

但是 OOP 的写法模板代码多,不够简洁,且缺乏安全性。返回值 Node 成为句柄被随意引用甚至修改,这破坏了声明式UI中 “不可变性” 的原则,如果 UI 可以随意修改,diff 算法的准确性将无法保证。

因此,为了保证 UI 的不可变性,我们设法抹去返回值 Node:

fun Composer.TodoApp(items: List<TodoItem>) {
  Stack(Orientation.Vertical) {
    for (item in items) {
      Stack(Orientation.Horizontal) {
        Text(if (item.completed) "x" else " ")
        Text(item.title)
      }
    }
  }
}
fun Composer.Stack(orientation:Int, content: Composer.() -> Unit) {
    emit(StackNode(orientation)) {
        content()
    }
}
fun Composer.Text() {
    ...
}

TodoApp 返回 Node 对象,可以被父 Node 继续 add,循环往复构成一棵完整的树。

但是 OOP 的写法模板代码多,不够简洁,且缺乏安全性。返回值 Node 成为句柄被随意引用甚至修改,这破坏了声明式UI中 “不可变性” 的原则,如果 UI 可以随意修改,diff 算法的准确性将无法保证。

因此,为了保证 UI 的不可变性,我们设法抹去返回值 Node:

fun Composer.TodoApp(items: List<TodoItem>) {
  Stack(Orientation.Vertical) {
    for (item in items) {
      Stack(Orientation.Horizontal) {
        Text(if (item.completed) "x" else " ")
        Text(item.title)
      }
    }
  }
}
fun Composer.Stack(orientation:Int, content: Composer.() -> Unit) {
    emit(StackNode(orientation)) {
        content()
    }
}
fun Composer.Text() {
    ...
}

通过 Composer 提供的上下文, 将创建的 Node emit 到树上的合适位置。

interface Composer {
  // add node as a child to the current Node, execute
  // `content` with `node` as the current Node
  fun emit(node: Node, content: () -> Unit = {})
}

Composer.Stack() 作为一个无返回值的函数,使得 NodeTree 的构建从 OOP 方式变为了 FP(函数式编程) 方式。

Compose Compiler 的加持

compose.compiler 的意义是让 FP 的写法进一步简单,添加一个 @Composable 注解, TodoApp 不必定义成 Composer 的扩展函数, 但是在编译期会修改 TodoApp 的签名,添加 Composer 参数。

@Composable
fun TodoApp {
  Stack {
    for (item in items) {
      Stack(Orientation.Horizontal){
        Text(if (item.completed) "x" else " ")
        Text(item.title))
      })
    }
  }
}

在 Compiler 的加持下,我们可以使用 @Composable 高效地写代码。 抛开语言上的差异不讲,Compose 比 Flutter 写起来要舒服得多。但无论写法上有多少差别,其归根结度还是会转换为对 NodeTree 的操作


NodeTree操作:Applier、ComposeNode、Composition

Compose 的 NodeTree 管理涉及 ApplierCompositionCompose Nodes 的工作:

Composition 作为起点,发起首次的 composition,通过 Composalbe 的执行,填充 Slot Table,并基于 Table 创建 NodeTree。渲染引擎基于 Compose Nodes 渲染 UI, 每当 recomposition 发生时,都会通过 Applier 对 NodeTree 进行更新。 因此

“Composable 的执行过程就是创建 Node 并构建 NodeTree 的过程。 ”SSSSSS

image.png

Applier:变更 NodeTree 的节点

前文提到,出于性能考虑,NodeTree 会使用 “差量” 方式自我更新,而这正是基于 Applier 实现的。 Applier 使用 Visitor 模式遍历树上的 Node ,每种 NodeTree 的运算都需要配套一个 Applier

Applier 提供回调,基于回调我们可以对 NodeTree 进行自定义修改:

image.png

insertTopDowninsertBottomUp 都用来添加节点,针对不同的树形结构选择不同的添加顺序有助于提高性能。 参考: insertTopDown

insertTopDown(自顶向下) insertBottomUp(自底向上)

image.png

我们可以实现自定义的 NodeApplier, 如下:

class Node {
  val children = mutableListOf<Node>()
}
class NodeApplier(node: Node) : AbstractApplier<Node>(node) {
  override fun onClear() {}
  override fun insertBottomUp(index: Int, instance: Node) {}
  override fun insertTopDown(index: Int, instance: Node) {
    current.children.add(index, instance) // `current` is set to the `Node` that we want to modify.
  }
  override fun move(from: Int, to: Int, count: Int) {
    current.children.move(from, to, count)
  }
  override fun remove(index: Int, count: Int) {
    current.children.remove(index, count)
  }
}

Applier 需要在 composition/recomposition 过程中被调用。composition 是通过 Composition 中对 Root Composable 的调用发起的,进而调用全部 Composalbe 最终形成NodeTree。

Composition:Composalbe 执行的起点

fun Composition(applier: Applier<*>, parent: CompositionContext) 创建 Composition对象,参数传入 ApplierRecomposer

val composition = Composition(
    applier = NodeApplier(node = Node()),
    parent = Recomposer(Dispatchers.Main)
)
composition.setContent {
    // Composable function calls
}

Recomposer 非常重要,他负责 Compose 的 recomposiiton 。当 NodeTree 首次创建之后,与 state 建立关联,监听 state 的变化发生重组。这个关联的建立是通过 Recomposer 的 “快照系统” 完成的。重组后,Recomposer 通过调用 Applier 完成 NodeTree 的变更 。

关于 “快照系统” 以及 Recomposer 的原理可以参考:

Composition#setContent 为后续 Compodable 的调用提供了容器:

interface Composition {
    val hasInvalidations: Boolean
    val isDisposed: Boolean
    fun dispose()
    fun setContent(content: @Composable () -> Unit)
}

ComposeNode:创建 UiNode 并进行更新

理论上每个 Composable 的执行都对应一个 Node 的创建, 但是由于 NodeTree 无需全量重建,所以也不是每次都需要创建新 Node。大多的 Composalbe 都会调用 ComposeNode() 接受一个 factory,仅在必要的时候创建 Node。

Layout 的实现为例,

@Composable inline fun Layout(
    content: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    ComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
        },
        skippableUpdate = materializerOf(modifier),
        content = content
    )
}
  • factory:创建 Node 的工厂
  • update:接受 receiver 为 Updater<T>的 lambda,用来更新当前 Node 的属性
  • content:调用子 Composable

ComposeNode() 的实现非常简单:

inline fun <T, reified E : Applier<*>> ComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit,
    noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
    content: @Composable () -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startNode()
    if (currentComposer.inserting) {
        currentComposer.createNode(factory)
    } else {
        currentComposer.useNode()
    }
    Updater<T>(currentComposer).update()
    SkippableUpdater<T>(currentComposer).skippableUpdate()
    currentComposer.startReplaceableGroup(0x7ab4aae9)//在编译期决定真正的GroupId
    content()
    currentComposer.endReplaceableGroup()
    currentComposer.endNode()
}

在 composition 过程中,通过 Composer上下文,更新 SlotTable, content()递归创建子 Node

SlotTable 在更新过程中,通过 diff 决定是否需要对 Node 进行 add/update/remove 等操作。 此处的 startNodeuseNodeendNode 等就是对 SlotTable 的遍历过程。

有关 SlotTable(GapBuffer) 的介绍,可以参考文章:compose.net.cn/principle/g…

SlotTable 的 diff 结果通过 Applier 的回调处理 NodeTree 结构的变化;通过调用 Updater<T>.update() 来处理 Node 属性的变化


Jake wharton 的实验项目 Mosica

基于 compose.runtime 可以实现任意一套声明式UI框架。 J神有一个实验性的项目 Mosica,就很好地展示了这一点 :github.com/JakeWharton…

fun main() = runMosaic {
  var count by mutableStateOf(0)
  setContent {
            Text("The count is: $count")
  }
  for (i in 1..20) {
            delay(250)
            count = i
  }
}

上面是 Mosica 中的一个 Counter 的例子。

Mosica Composition

runMosaic() 创建 Composition、Recomposer 和 Applier

fun runMosaic(body: suspend MosaicScope.() -> Unit) = runBlocking {
  //...
  val job = Job(coroutineContext[Job])
  val composeContext = coroutineContext + clock + job
  val rootNode = BoxNode() //根节点Node
  val recomposer = Recomposer(composeContext) //Recomposer
  val composition = Composition(MosaicNodeApplier(rootNode), recomposer) //Composition
  coroutineScope {
    val scope = object : MosaicScope, CoroutineScope by this {
      override fun setContent(content: @Composable () -> Unit) {
        composition.setContent(content)//调用@Composable
        hasFrameWaiters = true
      }
    }
    //...
    val snapshotObserverHandle = Snapshot.registerGlobalWriteObserver(observer)
    try {
      scope.body()//CoroutineScope中执行setContent{}
    } finally {
      snapshotObserverHandle.dispose()
    }
  }
}

而后,在 Composition 的 setContent{} 中,调用 @Composable。

Mosaic Node

看一下 Mosaic 中的 @Composalbe 和其对应的 Node

@Composable
private fun Box(flexDirection: YogaFlexDirection, children: @Composable () -> Unit) {
  ComposeNode<BoxNode, MosaicNodeApplier>(
    factory = ::BoxNode,
    update = {
      set(flexDirection) {
        yoga.flexDirection = flexDirection
      }
    },
    content = children,
  )
}
@Composable
fun Text(
  value: String,
  color: Color? = null,
  background: Color? = null,
  style: TextStyle? = null,
) {
  ComposeNode<TextNode, MosaicNodeApplier>(::TextNode) {
    set(value) {
      this.value = value
    }
    set(color) {
      this.foreground = color
    }
    set(background) {
      this.background = background
    }
    set(style) {
      this.style = style
    }
  }
}

ComposeNode 通过泛型关联对应的 Node 和 Applier 类型

Box 和 Text 内部都使用 ComposeNode() 创建对应的 Node 对象。其中 Box 是容器类的 Composalbe,在 conent 中进一步创建子 Node。 Box 和 Text 在Updater<T>.update()中更新 Node 属性 。

看一下 BoxNode:

internal class BoxNode : MosaicNode() {
  val children = mutableListOf<MosaicNode>()
  override fun renderTo(canvas: TextCanvas) {
    for (child in children) {
      val childYoga = child.yoga
      val left = childYoga.layoutX.toInt()
      val top = childYoga.layoutY.toInt()
      val right = left + childYoga.layoutWidth.toInt() - 1
      val bottom = top + childYoga.layoutHeight.toInt() - 1
      child.renderTo(canvas[top..bottom, left..right])
    }
  }
  override fun toString() = children.joinToString(prefix = "Box(", postfix = ")")
}
internal sealed class MosaicNode {
  val yoga: YogaNode = YogaNodeFactory.create()
  abstract fun renderTo(canvas: TextCanvas)
  fun render(): String {
    val canvas = with(yoga) {
      calculateLayout(UNDEFINED, UNDEFINED)
      TextSurface(layoutWidth.toInt(), layoutHeight.toInt())
    }
    renderTo(canvas)
    return canvas.toString()
  }
}

BoxNode 继承自 MosaicNode, MosaicNode 在 render() 中,通过 yoga 实现UI的绘制。通过调用 renderTo() 在 Canvas中 递归绘制子 Node,类似 AndroidView 的绘制逻辑。

理论上需要在首次 composition 或者 recomposition 时,调用 Node 的 render() 进行 NodeTree 的绘制, 为简单起见,Mosica 只是使用了定时轮询的方式调用 render()

  launch(context = composeContext) {
    while (true) {
      if (hasFrameWaiters) {
        hasFrameWaiters = false
        output.display(rootNode.render())
      }
      delay(50)
    }
  }
        //counter的state变化后,重新setContent,hasFrameWaiters更新后,重新render
        coroutineScope {
    val scope = object : MosaicScope, CoroutineScope by this {
      override fun setContent(content: @Composable () -> Unit) {
        composition.setContent(content)
        hasFrameWaiters = true
      }
    }
         }

MosaicNodeApplier

最后看一下 MosaicNodeApplier:

internal class MosaicNodeApplier(root: BoxNode) : AbstractApplier<MosaicNode>(root) {
  override fun insertTopDown(index: Int, instance: MosaicNode) {
    // Ignored, we insert bottom-up.
  }
  override fun insertBottomUp(index: Int, instance: MosaicNode) {
    val boxNode = current as BoxNode
    boxNode.children.add(index, instance)
    boxNode.yoga.addChildAt(instance.yoga, index)
  }
  override fun remove(index: Int, count: Int) {
    val boxNode = current as BoxNode
    boxNode.children.remove(index, count)
    repeat(count) {
      boxNode.yoga.removeChildAt(index)
    }
  }
  override fun move(from: Int, to: Int, count: Int) {
    val boxNode = current as BoxNode
    boxNode.children.move(from, to, count)
    val yoga = boxNode.yoga
    val newIndex = if (to > from) to - count else to
    if (count == 1) {
      val node = yoga.removeChildAt(from)
      yoga.addChildAt(node, newIndex)
    } else {
      val nodes = Array(count) {
        yoga.removeChildAt(from)
      }
      nodes.forEachIndexed { offset, node ->
        yoga.addChildAt(node, newIndex + offset)
      }
    }
  }
  override fun onClear() {
    val boxNode = root as BoxNode
    // Remove in reverse to avoid internal list copies.
    for (i in boxNode.yoga.childCount - 1 downTo 0) {
      boxNode.yoga.removeChildAt(i)
    }
  }
}

MosaicNodeApplier 实现了对 Node 的 add/move/remove, 最终都反映到了对 YogaNode 的操作上,通过 YogaNode 刷新 UI


基于 AndroidView 的声明式UI

参考 Mosaic 的示范,我们可以使用 compose.runtime 打造一个基于 Android 原生 View 的声明式 UI 框架。

LinearLayout & TextView Node

@Composable
fun TextView(
    text: String,
    onClick: () -> Unit = {}
) {
    val context = localContext.current
    ComposeNode<TextView, ViewApplier>(
        factory = {
            TextView(context)
        },
        update = {
            set(text) {
                this.text = text
            }
            set(onClick) {
                setOnClickListener { onClick() }
            }
        },
    )
}
@Composable
fun LinearLayout(children: @Composable () -> Unit) {
    val context = localContext.current
    ComposeNode<LinearLayout, ViewApplier>(
        factory = {
            LinearLayout(context).apply {
                orientation = LinearLayout.VERTICAL
                layoutParams = ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT,
                )
            }
        },
        update = {},
        content = children,
    )
}

ViewApplier

ViewApplier 中只实现 add

class ViewApplier(val view: FrameLayout) : AbstractApplier<View>(view) {
    override fun onClear() {
        (view as? ViewGroup)?.removeAllViews()
    }
    override fun insertBottomUp(index: Int, instance: View) {
        (current as? ViewGroup)?.addView(instance, index)
    }
    override fun insertTopDown(index: Int, instance: View) {
    }
    override fun move(from: Int, to: Int, count: Int) {
        // NOT Supported
        TODO()
    }
    override fun remove(index: Int, count: Int) {
        (view as? ViewGroup)?.removeViews(index, count)
    }
}

创建 Composition

创建 Root Composable: AndroidViewApp

@Composable
private fun AndroidViewApp() {
    var count by remember { mutableStateOf(1) }
    LinearLayout {
        TextView(
            text = "This is the Android TextView!!",
        )
        repeat(count) {
            TextView(
                text = "Android View!!TextView:$it $count",
                onClick = {
                    count++
                }
            )
        }
    }
}

最后在 content 调用 AndroidViewApp

fun runApp(context: Context): FrameLayout {
    val composer = Recomposer(Dispatchers.Main)
    GlobalSnapshotManager.ensureStarted()
    val mainScope = MainScope()
    mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
        withContext(coroutineContext + DefaultMonotonicFrameClock) {
            composer.runRecomposeAndApplyChanges()
        }
    }
    mainScope.launch {
        composer.state.collect {
            println("composer:$it")
        }
    }
    val rootDocument = FrameLayout(context)
    Composition(ViewApplier(rootDocument), composer).apply {
        setContent {
            CompositionLocalProvider(localContext provides context) {
                AndroidViewApp()
            }
        }
    }
    return rootDocument
}

效果展示:

image.png

TL;DR

image.png

  • 当 State 变化时触发 recomposition,Composable 重新执行
  • Composable 在执行中,通过 SlotTable 的 diff,找出待变更的 Node
  • 通过 Applier 更新 TreeNode,并在 UI 层渲染这棵树。
  • 基于 compose.runtime ,我们可以实现自己的声明式UI
目录
相关文章
|
7月前
|
存储 缓存 Android开发
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首
这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。
|
3月前
|
开发框架 JavaScript 前端开发
鸿蒙NEXT开发声明式UI是咋回事?
【10月更文挑战第15天】鸿蒙NEXT的声明式UI基于ArkTS,提供高效简洁的开发体验。ArkTS扩展了TypeScript,支持声明式UI描述、自定义组件及状态管理。ArkUI框架则提供了丰富的组件、布局计算和动画能力。开发者仅需关注数据变化,UI将自动更新,简化了开发流程。此外,其前后端分层设计与编译时优化确保了高性能运行,利于生态发展。通过组件创建、状态管理和渲染控制等方式,开发者能快速构建高质量的鸿蒙应用。
166 3
|
7月前
|
存储 数据库 Android开发
安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐
为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤: 1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。 2. **创建UI按钮**:在界面中创建添加和删除按钮。 3. **数据库功能**:使用Room数据库来存储音频文件信息。 4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。 5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。
|
4月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
103 4
|
6月前
|
存储 移动开发 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;
|
7月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
7月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
7月前
|
缓存 Android开发 Kotlin
【安卓app开发】kotlin Jetpack Compose框架 | 先用OKhttp下载远程音频文件再使用ExoPlayer播放
使用 Kotlin 的 Jetpack Compose 开发安卓应用时,可以结合 OkHttp 下载远程音频文件和 ExoPlayer 进行播放。在 `build.gradle` 添加相关依赖后,示例代码展示了如何下载音频并用 ExoPlayer 播放。代码包括添加依赖、下载文件、播放文件及简单的 Compose UI。注意,示例未包含完整错误处理和资源释放,实际应用需补充这些内容。
|
7月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
7月前
|
存储 Android开发 Kotlin
开发安卓app OKhttp下载后使用MediaPlayer播放
在Android Jetpack Compose应用程序中,要使用OkHttp下载远程音频文件并在本地播放,你需要完成以下几个步骤: 1. **添加依赖**:确保`build.gradle`文件包含OkHttp和Jetpack Compose的相关依赖。 2. **下载逻辑**:创建一个`suspend`函数,使用OkHttp发起网络请求下载音频文件到本地。 3. **播放逻辑**:利用`MediaPlayer`管理音频播放状态。 4. **Compose UI**:构建用户界面,包含下载和播放音频的按钮。