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

Expect 和 Actual

expect 关键字用于定义一个多平台通用的声明,即该声明在所有平台上都可用,并且需要在特定平台上实现。actual 关键字通常与 expect 关键字配合使用,用于定义多平台通用的接口和函数,从而允许在不同的平台上使用相同的 API。官方建议只对平台API使用expect/actual,否则使用普通接口。

比如获取IPv4地址:

 获取本机ip地址

这里使用的可能不是KMP的最佳实践,而是借鉴Android程序使用的MVVM模式。关于KMP项目的最佳实践,大家可以参考其他文档。

如前小节图例,在Android设备和PC设备上,获取IP地址的方式不同,因此业务侧的实现也会存在差异。

interface ServerViewModel { // 定义接口,用来统一各平台函数实现。
    fun getLocalIpAddressV4(address: MutableStateFlow<String>)
    ...
}

class ComServerViewModel {
    private val _ipAddress: MutableStateFlow<String> = MutableStateFlow("")
    val ipAddress = _ipAddress.asStateFlow() // 由状态流记录结果
    ...
    fun getLocalIpAddressV4() { // 界面通过viewmodel调用该函数
        // 调用expect函数获取各平台viewmodel实现对象
        getMsgViewModel().getLocalIpAddressV4(_ipAddress) 
    }
    ...
}

expect fun getMsgViewModel(): ServerViewModel  // expect函数,不同模块分别实现。

Android实现

Android获取ip地址需通过Connectivity获取网络状态,获取到ip地址。(考虑到文件传输对流量消耗的影响,仅支持在WiFi下,当然也可以使用流量,甚至通过花生壳等将文件映射到外网访问,但请注意隐私)。

class AndroidServerViewModel : ViewModel(), ServerViewModel {
    override fun getLocalIpAddressV4(address: MutableStateFlow<String>) {
        val request =
            NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .build()
        val connectivityManager =
            FileServerApp.app.applicationContext.getSystemService(ConnectivityManager::class.java)
        connectivityManager.registerNetworkCallback( // 注册网络回调
            request,
            object : ConnectivityManager.NetworkCallback() {
                override fun onUnavailable() {
                    super.onUnavailable()
                    LogTool.i("Network is onUnavailable")
                }

                override fun onAvailable(network: Network) {
                    super.onAvailable(network)
                    LogTool.i("Network is onAvailable")
                }

                @RequiresApi(VERSION_CODES.Q)
                override fun onCapabilitiesChanged(
                    network: Network,
                    networkCapabilities: NetworkCapabilities
                ) {
                    super.onCapabilitiesChanged(network, networkCapabilities)
                    var ipAddress: String? = ""
                    if (Build.VERSION.SDK_INT > VERSION_CODES.Q) { // Q版本以上需通过LinkPropertiese获取。
                        ipAddress =
                            connectivityManager?.getLinkProperties(network)?.linkAddresses?.get(1)?.address?.address
                                ?.let { numericToTextFormat(it) }
                    } else { // Q版本以下通过WifiInfo获取,在Q版本以上被废弃,获取不到结果。
                        val wifiInfo = networkCapabilities.transportInfo as WifiInfo
                        ipAddress = intToIp(wifiInfo.ipAddress)
                    }
                    runBlocking { // 通过协程将结果发送出去。
                        address.emit(ipAddress ?: "")
                    }
                }
            })
        }
    private fun numericToTextFormat(src: ByteArray): String {
        return (src[0].toInt() and 0xff).toString() + "." + (src[1].toInt() and 0xff) + "." + (src[2].toInt() and 0xff) + "." + (src[3].toInt() and 0xff)
    }

    private fun intToIp(i: Int): String =
        ((i and 0xFF).toString() + " . " + ((i shr 8) and 0xFF) + " . "
            + ((i shr 16) and 0xFF) + " . " + ((i shr 24) and 0xFF))
    }
    ...
    
    // expect函数的实现,由actual关键字修饰。
    actual fun getMsgViewModel(): ServerViewModel = AndroidServerViewModel()

JVM实现

在JVM平台,直接通过java.net包中的Inet4Address获取ip地址即可。

class DesktopServerViewModel : ServerViewModel, ViewModel() {
    override fun getLocalIpAddressV4(address: MutableStateFlow<String>) {
        viewModelScope.launch { // 由于获取ip地址为耗时动作,因此启动协程执行。
            withContext(Dispatchers.IO) {
                Inet4Address.getLocalHost().hostAddress?.let {
                    address.emit(it) // 发送结果
                }
            }
        }
    }
    ...
}

actual fun getMsgViewModel(): ServerViewModel = DesktopServerViewModel()

配置信息读写

interface ServerViewModel {
    ...
    fun loadConfigs(config: MutableStateFlow<HttpFileServerConfig?>)

    fun updateConfigs(newConfig: HttpFileServerConfig)
    ...
}

class ComServerViewModel {
    ...
    private val _httpConfig: MutableStateFlow<HttpFileServerConfig?> = MutableStateFlow(null)
    val httpServerConfig = _httpConfig.asStateFlow()
    ...
    fun loadConfigs() {
        getMsgViewModel().loadConfigs(_httpConfig)
    }

    fun updateConfig(newConfig: HttpFileServerConfig) {
        _httpConfig.value?.serverPort = newConfig.serverPort
        getMsgViewModel().updateConfigs(newConfig)
    }
    ...
}
...
expect fun getMsgViewModel(): ServerViewModel

和获取ip地址一样,读写配置文件也用了不同平台不同的方式进行。在JVM平台,使用文件.conf存储;在Android平台,使用SharedPreference存储。

Android实现
class AndroidServerViewModel : ViewModel(), ServerViewModel {
    ...
    override fun loadConfigs(config: MutableStateFlow<HttpFileServerConfig?>) {
        fileServerConfig.serverPort = sp.getInt(PROP_KEY_SERVER_PORT, 8080)
        fileServerConfig.fileDirectory = sp.getString(
            PROP_KEY_SERVER_ROOT, getPlatform().getPlatformDefaultRoot()
        )!!
        LogTool.i(fileServerConfig.toString())
        runBlocking {
            config.emit(fileServerConfig)
        }
    }

    override fun updateConfigs(newConfig: HttpFileServerConfig) {
        fileServerConfig = newConfig.copy()
        sp.edit().apply {
            putInt(PROP_KEY_SERVER_PORT, fileServerConfig.serverPort)
            putString(PROP_KEY_SERVER_ROOT, getPlatform().getPlatformDefaultRoot())
        }.apply()
        LogTool.i(fileServerConfig)
    }
    ...

    private companion object {
        private val sp: SharedPreferences =
            FileServerApp.app.getSharedPreferences(PROP_FILE_NAME, Context.MODE_PRIVATE)
        private var fileServerConfig = HttpFileServerConfig()
    }
}

actual fun getMsgViewModel(): ServerViewModel = AndroidServerViewModel()

JVM实现

class DesktopServerViewModel : ServerViewModel, ViewModel() {
    ...
    override fun loadConfigs(config: MutableStateFlow<HttpFileServerConfig?>) {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                try {
                    val configFile = File(PROP_FILE_NAME)
                    if (!configFile.exists()) {
                        configFile.createNewFile()
                    } else if (configFile.isDirectory) {
                        configFile.delete()
                        configFile.createNewFile()
                    } else {
                        LogTool.i("Config file is exist.")
                    }
                    prop.load(FileInputStream(PROP_FILE_NAME))
                    fileServerConfig.serverPort =
                        prop.getProperty(PROP_KEY_SERVER_PORT)?.toInt() ?: DEFAULT_SERVER_PORT
                    fileServerConfig.fileDirectory =
                        prop.getProperty(PROP_KEY_SERVER_ROOT) ?: DEFAULT_SERVER_ROOT
                    config.emit(fileServerConfig)
                    LogTool.i(config.toString())
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
        }
    }

    override fun updateConfigs(newConfig: HttpFileServerConfig) {
        fileServerConfig = newConfig.copy()
        prop.setProperty(PROP_KEY_SERVER_PORT, fileServerConfig.serverPort.toString())
        prop.setProperty(PROP_KEY_SERVER_ROOT, fileServerConfig.fileDirectory)
    }
    ...

    private companion object {
        private const val PROP_FILE_NAME = ".conf"
        private val prop = Properties()
        private var fileServerConfig = HttpFileServerConfig()
    }
}

actual fun getMsgViewModel(): ServerViewModel = DesktopServerViewModel()

生成了一个apk,想用的可以体验一下,欢迎吐槽。 基于KMP的Android静态文件服务程序

Exe和Deb的暂没环境,改天再打包。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值