基于Kotlin Multiplatform实现静态文件服务器(二)

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平台的实现不同等。

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin Multiplatform 是一种由 JetBrains 开发的跨平台开发框架。它允许开发人员使用 Kotlin 语言编写代码,然后在多个平台上运行,包括 Android、iOS、Web 等。与传统的跨平台解决方案相比,Kotlin Multiplatform 提供了更高的灵活性和性能。 Kotlin Multiplatform 的核心思想是共享代码。开发人员可以编写一个通用的 Kotlin 模块,其中包含与平台无关的业务逻辑和算法。然后,他们可以根据不同的目标平台,编写平台特定的代码。这样,开发人员可以在不同平台之间共享核心逻辑,减少了重复代码的编写,并且保持了一致性。 Kotlin Multiplatform 目前已经应用于许多项目中。对于 Android 开发人员来说,它提供了更好的性能和开发体验。它允许开发人员在 Android 和 iOS 上使用相同的 Kotlin 代码库,从而加快了开发速度和代码复用。对于 iOS 开发人员来说,Kotlin Multiplatform 可以通过共享核心业务逻辑来简化跨平台开发,并且可以与现有的 Objective-C 或 Swift 代码无缝集成。 总之,Kotlin Multiplatform 是一个强大的跨平台开发框架,可以大大简化和提高开发人员的工作效率。它同时适用于 Android 和 iOS 开发,并且允许开发人员在不同平台之间共享核心逻辑。在未来,我们可以预见 Kotlin Multiplatform 将会在跨平台开发领域发挥更大的作用,并且有望成为开发人员的首选解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值