通过声明式方式构建用户界面,这在 Web 开发领域已被广泛采用,如今许多大型应用程序都是遵循这些原则开发的。举例来说,谷歌推出了 Jetpack Compose,苹果则在 WWDC19 上发布了 SwiftUI,两者都得到了开发者社区的热烈反响。本文中,我们将演示如何在 NativeScript 中使用 Jetpack Compose,共同探索构建精彩用户界面的新可能性,为基于 NativeScript 构建的 Android 应用提供了一种高效且愉快的 Jetpack Compose 集成方案。
创建 NativeScript 应用
我们可以使用标准的 TypeScript 模板来创建一个应用:
ns create jetpackcompose --ts
cd jetpackcompose
这会搭建一个通常被称为“原生风味”(vanilla)的 NativeScript 应用。当然,你也可以选用自己最熟悉的框架。为 Angular(及其他大多数框架)设置插件,通常只需要注册视图即可,我们稍后会在下文中演示。
安装 Jetpack Compose 插件:
npm install @nativescript/jetpack-compose
注意: Jetpack Compose 要求你的最低 SDK 版本至少为 API 21 (Lollipop)。你可以在 app.gradle 文件中添加 minSdkVersion 21 来设置。
如果你计划直接在 Android Studio 中构建你的库,那么无需做其他事情,只需将构建好的 .aar 文件放入 App_Resources/Android/libs/ 目录,然后跳转到下一节。但如果你打算直接在 App_Resources/Android/src/main/java 目录下的 .kt 文件中编写 Kotlin 代码,那么还需要额外几步操作。
首先,在 app.gradle 中添加你的 Compose 依赖项:
dependencies {
def compose_version = "1.2.1"
implementation "androidx.compose.ui:ui:$compose_version"
// 工具支持 (预览等)
implementation "androidx.compose.ui:ui-tooling:$compose_version"
// 添加你的 Jetpack Compose UI 所需的其他依赖项
// 比如 Material Design:
// implementation 'androidx.compose.material:material:$compose_version'
}
接着,修改 android 部分以启用 Compose:
android {
// 其他设置,如 targetSdk 等。
buildFeatures {
compose true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
composeOptions {
kotlinCompilerExtensionVersion '1.3.2'
}
}
最后,通过创建文件 App_Resources/Android/gradle.properties 来启用 Kotlin:
useKotlin=true
kotlinVersion=1.7.20 # 你可以在这里选择你的 Kotlin 版本
使用 Jetpack Compose
A. 创建你的 Jetpack Compose 视图和包装器
创建文件 App_Resources/Android/src/main/java/BasicView.kt:
package com.example
import android.content.Context
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
class BasicView {
fun generateComposeView(view: ComposeView): ComposeView {
return view.apply {
setContent {
MaterialTheme {
Text("Hello from Jetpack Compose")
}
}
}
}
fun updateData(value: Map<Any, Any>) {
}
var onEvent: ((String) -> Unit)? = null
}
若想使用插件默认处理 Compose 视图的方式,你的实现遵循以下接口非常重要:
class Example {
fun generateComposeView(view: ComposeView): ComposeView {
// 将你的 Compose 视图渲染到 ComposeView 中
}
fun updateData(value: Map<Any, Any>) {
// 此函数接收来自 NativeScript 的数据
// value 是一个转换为 Map 的 JS 对象
}
// 这是你将发送回 NativeScript 的事件
// 需要传递数据时,只需调用 onEvent?.invoke(v)
var onEvent: ((Any) -> Unit)? = null
}
B. 通过 composeId 注册你的 Jetpack Compose 视图
这一步可以在 NativeScript 应用的启动引导文件中完成(通常是 app.ts 或 main.ts)。
import { registerJetpackCompose, ComposeDataDriver } from '@nativescript/jetpack-compose';
// A. 你可以使用 'ns typings android --aar {path/to/{name}.aar}' 为你自己的 Compose 提供者生成类型定义
// B. 否则,可以通过声明你知道已提供的包解析路径来忽略此步骤
declare var com;
registerJetpackCompose('sampleView', (view) => new ComposeDataDriver(new com.example.BasicView(), view));
此外,如果你想使用 Angular,可以注册 Compose 视图本身:
import { registerElement } from '@nativescript/angular';
import { JetpackCompose } from '@nativescript/jetpack-compose';
registerElement('JetpackCompose', () => JetpackCompose)
C. 插入到任意 NativeScript 布局中
app/main-page.xml
<Page
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:jc="@nativescript/jetpack-compose"
class="page">
<StackLayout>
<jc:JetpackCompose composeId="sampleView" height="100" />
</StackLayout>
</Page>
现在你可以使用 ns debug android 来运行应用了。
使用 Android Studio 开发和预览 Jetpack Compose
运行一次应用后,你可以在 Android Studio 中打开 platforms/android 文件夹,在那里你能找到 BasicView.kt 文件。从这里开始,你可以修改它并预览你的更改(只要在你想预览的 @Composable 函数上加上 @Preview 装饰器即可)。
重要提示: 保存这个文件并不会改变你 App_Resources 内部的 BasicView.kt 文件,所以编辑完成后,请务必小心地将文件内容复制回去!这将是未来改进开发体验的一个方面。
或者你也可以创建一个新的 Android 库来开发所有的 Jetpack Compose 视图。
与 NativeScript 互相发送和接收数据
首先,让我们给 BasicView 添加一些绑定,让它现在能在 updateData 中接收数据并展示出来,同时在数据更新后发出一个事件:
package com.example
import android.content.Context
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
class BasicView {
data class ExampleUiState(
val text: String = ""
) {}
class ExampleViewModel(
) : ViewModel() {
var uiState by mutableStateOf(ExampleUiState())
}
var mViewModel = ExampleViewModel()
fun generateComposeView(view: ComposeView): ComposeView {
return view.apply {
setContent {
MaterialTheme {
val uiState = mViewModel.uiState;
// 在 Compose 世界里
Text(uiState.text)
}
}
}
}
fun updateData(value: Map<Any, Any>) {
val v = value["data"] as String;
onEvent?.invoke(v)
mViewModel.uiState = ExampleUiState(v);
}
var onEvent: ((String) -> Unit)? = null
}
在 NativeScript 布局中使用你的 Jetpack Compose
app/main-page.xml:
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page"
xmlns:jc="@nativescript/jetpack-compose">
<StackLayout>
<Label text="下面这个视图就是在 NativeScript 内部的 Jetpack Compose!" textWrap="true"></Label>
<jc:JetpackCompose composeEvent="{{ onEvent }}" data="{{ text }}" composeId="sampleView"></jc:JetpackCompose>
<Label text="这又回到了 NativeScript"></Label>
<TextView textChange="{{ onTextChange }}" text="{{ text }}" textWrap="true"></TextView>
</StackLayout>
</Page>
app/main-page.ts:
import { Observable } from '@nativescript/core';
import { registerJetpackCompose, ComposeDataDriver } from '@nativescript/jetpack-compose';
import { EventData, Page, PropertyChangeData } from '@nativescript/core';
// A. 你可以使用 'ns typings android --aar {path/to/{name}.aar}' 为你自己的 Compose 提供者生成类型定义
// B. 否则,可以通过声明你知道已提供的包解析路径来忽略此步骤
declare var com;
registerJetpackCompose('sampleView', (view) => new ComposeDataDriver(new com.example.BasicView(), view));
export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new DemoModel();
}
export class DemoModel extends Observable {
text = '';
onEvent(evt: JetpackComposeEventData<string>) {
console.log('onEvent', evt.data);
}
onTextChange(evt: PropertyChangeData) {
console.log('textChange', evt.value);
this.set('text', evt.value);
}
}
现在每当你修改 NativeScript 的 TextView 中的文本时,它都会同步更新 Jetpack Compose 视图中的文本!
颜色选择器示例
最后这个例子,将通过使用一个颜色选择器来改变 NativeScript 视图的背景色:
app.gradle
implementation "com.github.skydoves:colorpicker-compose:1.0.0"
package com.example
import android.content.Context
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.github.skydoves.colorpicker.compose.ColorEnvelope
import com.github.skydoves.colorpicker.compose.HsvColorPicker
import com.github.skydoves.colorpicker.compose.ImageColorPicker
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
class ColorPickerCompose {
fun generateComposeView(view: ComposeView): ComposeView {
return view.apply {
setContent {
val controller = rememberColorPickerController()
HsvColorPicker(
modifier = Modifier
.fillMaxWidth()
.height(450.dp)
.padding(10.dp),
controller = controller,
onColorChanged = { colorEnvelope: ColorEnvelope ->
onEvent?.invoke(colorEnvelope.hexCode)
}
)
}
}
}
fun updateData(value: Map<Any, Any>) {}
var onEvent: ((String) -> Unit)? = null
}
<StackLayout backgroundColor="{{ backgroundColor }}">
<Label text="下面这个视图就是在 NativeScript 内部的 Jetpack Compose!" textWrap="true"></Label>
<StackLayout backgroundColor="lightblue">
<jc:JetpackCompose composeEvent="{{ onEvent }}" data="{{ text }}" composeId="jetpackCompose"></jc:JetpackCompose>
</StackLayout>
<Label text="这又回到了 NativeScript"></Label>
<TextView text="{{ backgroundColor }}" textWrap="true"></TextView>
</StackLayout>