Jetpack Compose简介
Jetpack Compose 是Google 为Android开发推出的一种新型UI构建工具,它基于Kotlin语言,采用声明性的语法,使得UI构建更加简单、直观。与传统的XML布局不同,Jetpack Compose使用代码来描述UI,开发者可以直接在代码中设置UI元素的属性,而无需使用XML进行配置。
它也可以被用来基于KMP实现跨平台的UI实现,以达到各平台UI一致。
程序入口
在KMP Compose中,仅需处理与平台强相关的部分代码,如Android程序启动方式、文件系统目录结构、权限等,其他均在Commom中进行编写。
由于平台差异,在Android,Windows和Linux不同系统上的运行入口不同。在Windows和Linux平台,是基于JVM执行的,所以入口是main方法,而Android则通常为Activity。
Android应用
作为Android应用,Jetpack Compose的程序通常是在Activity中进行调用。如果熟悉Android应用开发的,可以跳过本小节。
MainActivity在示例程序中已经创建好,只需要在其中增加业务即可。如作为文件服务器,则需要申请存储权限。
class MainActivity : ComponentActivity() {
private var checkPermission = false
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 检查是否有存储权限
val granted = Environment.isExternalStorageManager()
if (!granted) {
checkPermission = true
// 在activity中请求存储权限
val intent: Intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
.setData(Uri.parse("package:$packageName"))
startActivityForResult(intent, 0)
return
}
setContent {
App() // 由Compose对象App进入界面绘制
}
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onResume() {
super.onResume()
if (checkPermission) {
setContent {
App()
}
}
}
}
Android需申请文件管理权限,用于文件服务访问内部存储文件。为了支撑文件下载,通过FileProvider授权内部存储文件的读取。创建SocketServer则需要申请网络权限。
在Android上,基于前台Service运行文件服务,因此也申请了前台服务权限。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:name=".app.FileServerApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:usesCleartextTraffic="true">
<service
android:name=".service.FileServerService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.vicky.server.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>
JVM应用
由于Windows和Linux上是基于JVM运行的,JVM应用的启动入口为主函数入口main。在KMP应用中,启动入口也是main函数,在main.kt中。
...
fun main() = application {
val windowState = rememberWindowState().also {
it.size = DpSize(360.dp, 600.dp) // 设置窗口大小。
}
Window(
onCloseRequest = ::exitApplication,
title = "HttpFileServer", // 窗口标题
state = windowState,
resizable = false,
) {
App() // 由Compose对象App进入界面绘制
}
}
主界面
在程序入口中,MainActivity和main.kt中,都调用了App()。
@Composable // Jetpack Compose UI注解
@Preview // 支持预览的注解(虽然我在KMP里根本没调出来...可能是As版本的问题或者系统问题?)
fun App() {
AppMainView() // 创建自定义界面。
}
创建自定义界面。界面简单,包括提示文本,输入框,启动/停止按钮。
...
@Composable
fun AppMainView() {
val serverViewModel = ComServerViewModel() // 创建viewmodel对象
serverViewModel.loadConfigs() // 加载配置信息
MaterialTheme { // Material 主题
Scaffold { innerPadding ->
val ipAddress by serverViewModel.ipAddress.collectAsState()
var serverState by remember { mutableStateOf(false) }
val httpServerConfig by serverViewModel.httpServerConfig.collectAsState()
var serverPort by remember { mutableStateOf(DEFAULT_SERVER_PORT) }
val serverTipStr = stringResource(Res.string.serverTip)
var serverTip by remember { mutableStateOf(serverTipStr) }
var startupEnable by remember { mutableStateOf(true) }
Column(
modifier = Modifier.padding(
commonPadding,
innerPadding.calculateTopPadding(),
commonPadding,
innerPadding.calculateBottomPadding()
).fillMaxWidth()
) {
serverTip = if (serverState) {
"http://$ipAddress${
if (serverPort == httpServerConfig?.serverPort) "" else ":${httpServerConfig?.serverPort}"
}"
} else {
serverTipStr
}
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
Text( //提示文本
modifier = Modifier
.padding(vertical = commonPadding),
textAlign = TextAlign.End,
text = if (serverState) stringResource(Res.string.visitInfo) else "",
color = Color.DarkGray,
fontSize = primaryFontSize
)
SelectionContainer { // 可选择区域
Text(
modifier = Modifier
.padding(vertical = commonPadding),
textAlign = TextAlign.Start,
text = serverTip,
color = Color.DarkGray,
fontSize = primaryFontSize
)
}
}
Text( // 本机ip地址
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(vertical = commonPadding),
textAlign = TextAlign.Center,
text = "${stringResource(Res.string.ipInfo)}$ipAddress",
color = Color.DarkGray,
fontSize = primaryFontSize
)
TextField( // 输入框
value = serverPort.toString(),
onValueChange = {
if (getPlatform().getOSType() == Const.OsType.OS_ANDROID) {
serverPort = it.toInt()
if (serverPort <= 1024) { // 由于Android设备权限限制,不能开启小于1024的端口
serverTip = "Android设备请设置端口号大于1024"
startupEnable = false
return@TextField
}
}
startupEnable = true
serverViewModel.updateConfig(HttpFileServerConfig(serverPort = serverPort))
},
enabled = !serverState, // 服务启动时,不能修改端口
modifier = Modifier.align(Alignment.CenterHorizontally),
// 限制键盘仅能输入数字
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Button(
onClick = {
if (serverState) { // 状态控制按钮动作。
// 停止服务端
serverViewModel.stopFileServer()
} else {
// 启动服务端
serverViewModel.startFileServer()
}
serverState = !serverState
},
enabled = startupEnable,
modifier = Modifier.align(Alignment.CenterHorizontally)
.padding(vertical = commonPadding)
) {
Text(text = stringResource(if (serverState) Res.string.stop else Res.string.startup))
}
serverViewModel.getLocalIpAddressV4()
}
}
}
}
嗯,运行起来就大致如下,非常简单。
在界面中,Android和JVM不同的仅为端口号部分,由于Android的限制,小于1024的端口号无法被应用使用,因此通过特定的平台代码,控制了相关逻辑。
而在业务上,特定平台的实现则比较多。比如ViewModel的协程Scope,在Android和JVM平台的实现不同等。