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" /><applicationandroid: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"><serviceandroid:name=".service.FileServerService"android:enabled="true"android:exported="false"android:foregroundServiceType="dataSync" /><activityandroid: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><providerandroid:name="androidx.core.content.FileProvider"android:authorities="com.vicky.server.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid: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 = falsereturn@TextField}}startupEnable = trueserverViewModel.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平台的实现不同等。