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的暂没环境,改天再打包。