MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin中的跨平台UI框架MultiPlatform UI

2024-01-231.3k 阅读

Kotlin 跨平台 UI 框架 MultiPlatform UI 简介

MultiPlatform UI,简称为 MPUI,是专门为 Kotlin 开发者打造的一套跨平台用户界面框架。在移动应用、桌面应用以及 Web 应用开发需求日益增长的当下,开发者期望一套代码能够在多个平台上复用,从而提高开发效率、降低维护成本。MPUI 正是顺应这一趋势诞生的。它基于 Kotlin 的多平台特性,允许开发者使用 Kotlin 语言编写跨平台的 UI 代码,让 UI 逻辑能够在 Android、iOS、桌面(Windows、MacOS、Linux)以及 Web 等平台间共享。

环境搭建

  1. 安装 Kotlin 插件: 如果你使用的是 Intellij IDEA,首先要确保安装了 Kotlin 插件。打开 IDEA,进入 Settings(Windows/Linux)或 Preferences(MacOS),在 Plugins 中搜索 Kotlin 并安装。安装完成后重启 IDEA 使插件生效。
  2. 创建 Kotlin 多平台项目: 在 IDEA 中,选择 File -> New -> Project。在弹出的窗口中,左侧选择 Kotlin,右侧选择 Multiplatform 项目模板。填写项目名称和路径后点击 Create
  3. 配置 Gradle: 项目创建完成后,会生成一个基本的项目结构。在项目的 build.gradle.kts 文件中,配置 MPUI 相关的依赖。例如,添加如下依赖:
kotlin {
    android()
    iosX64()
    // 其他平台配置...

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:multiplatform-ui-core:0.1.0")
                // 其他通用依赖
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:multiplatform-ui-android:0.1.0")
                // Android 特定依赖
            }
        }
        val iosX64Main by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:multiplatform-ui-ios:0.1.0")
                // iOS 特定依赖
            }
        }
        // 其他平台的 sourceSets 依赖配置
    }
}

这里以 0.1.0 版本为例,实际使用中请根据最新版本号进行调整。

核心组件

  1. 视图(View): MPUI 中的视图是构建用户界面的基本元素。类似于 Android 的 View 或 iOS 的 UIView。例如,创建一个简单的文本视图:
import org.jetbrains.kotlinx.multiplatform.ui.View
import org.jetbrains.kotlinx.multiplatform.ui.Text

val textView: View = Text("Hello, MultiPlatform UI!")
  1. 布局(Layout): 布局用于管理视图在界面上的排列方式。MPUI 提供了多种布局方式,如线性布局(LinearLayout)、相对布局(RelativeLayout)等。以线性布局为例:
import org.jetbrains.kotlinx.multiplatform.ui.*

val linearLayout: View = LinearLayout(orientation = Orientation.VERTICAL) {
    addView(Text("First Text"))
    addView(Text("Second Text"))
}

这里创建了一个垂直方向的线性布局,并在其中添加了两个文本视图。 3. 样式(Style): 样式用于定义视图的外观属性,如颜色、字体大小、边距等。可以通过创建样式对象并应用到视图上。

import org.jetbrains.kotlinx.multiplatform.ui.*

val textStyle = TextStyle(
    color = Color.Black,
    fontSize = 16.sp
)

val styledTextView: View = Text("Styled Text", style = textStyle)

跨平台 UI 开发实践

  1. 共享 UI 逻辑: 假设我们要开发一个简单的登录界面,该界面在 Android 和 iOS 上都有相同的基本布局和逻辑。首先在 commonMainsourceSet 中创建一个 LoginViewModel
package com.example.shared

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class LoginViewModel {
    private val _username = MutableStateFlow("")
    val username: StateFlow<String> get() = _username

    private val _password = MutableStateFlow("")
    val password: StateFlow<String> get() = _password

    fun setUsername(newUsername: String) {
        _username.value = newUsername
    }

    fun setPassword(newPassword: String) {
        _password.value = newPassword
    }

    fun login() {
        // 这里可以添加实际的登录逻辑,例如调用 API
        println("Logging in with username: ${_username.value}, password: ${_password.value}")
    }
}

然后创建一个通用的登录界面布局:

package com.example.shared

import org.jetbrains.kotlinx.multiplatform.ui.*

fun loginScreen(viewModel: LoginViewModel): View = Column {
    TextField(
        value = viewModel.username.value,
        onValueChange = { viewModel.setUsername(it) },
        placeholder = { Text("Username") }
    )
    TextField(
        value = viewModel.password.value,
        onValueChange = { viewModel.setPassword(it) },
        placeholder = { Text("Password") },
        isPassword = true
    )
    Button(onClick = { viewModel.login() }) {
        Text("Login")
    }
}
  1. Android 平台实现: 在 androidMainsourceSet 中,将通用的登录界面集成到 Android 应用中。创建一个 LoginActivity
package com.example.android

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.shared.LoginViewModel
import com.example.shared.loginScreen
import org.jetbrains.kotlinx.multiplatform.ui.android.AndroidComposeView

class LoginActivity : AppCompatActivity() {
    private val viewModel = LoginViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(AndroidComposeView(this) {
            loginScreen(viewModel)
        })
    }
}
  1. iOS 平台实现: 在 iosX64MainsourceSet 中,将通用的登录界面集成到 iOS 应用中。创建一个 LoginViewController
package com.example.ios

import com.example.shared.LoginViewModel
import com.example.shared.loginScreen
import org.jetbrains.kotlinx.multiplatform.ui.ios.IosViewController
import org.jetbrains.kotlinx.multiplatform.ui.View

class LoginViewController : IosViewController() {
    private val viewModel = LoginViewModel()

    override fun createView(): View {
        return loginScreen(viewModel)
    }
}

同时,在 iOS 的 AppDelegate 中设置初始视图控制器:

import UIKit
import kotlinwrappers

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let rootViewController = LoginViewController()
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = rootViewController
        window?.makeKeyAndVisible()
        return true
    }
}

响应式编程与数据绑定

  1. 状态管理: MPUI 支持使用 Kotlin Flow 进行状态管理。在前面的登录界面示例中,LoginViewModel 使用 MutableStateFlow 来管理用户名和密码的状态。视图可以观察这些状态流并做出相应的更新。例如,当用户名发生变化时,TextField 的显示内容会实时更新:
TextField(
    value = viewModel.username.value,
    onValueChange = { viewModel.setUsername(it) },
    placeholder = { Text("Username") }
)

这里 value 属性绑定到 viewModel.username.value,当 viewModel.username 的值发生变化时,TextField 的显示文本也会改变。 2. 事件处理: 视图的事件处理也非常直观。例如,Button 的点击事件:

Button(onClick = { viewModel.login() }) {
    Text("Login")
}

当按钮被点击时,会调用 viewModel.login() 方法,从而触发登录逻辑。

自定义组件

  1. 创建自定义视图: 假设我们要创建一个带有边框的文本视图。首先定义一个自定义视图类:
import org.jetbrains.kotlinx.multiplatform.ui.*

class BorderedTextView(
    text: String,
    style: TextStyle? = null,
    borderColor: Color = Color.Black,
    borderWidth: Dp = 1.dp
) : View() {
    private val textView = Text(text, style)

    override fun measure(widthMeasureSpec: MeasureSpec, heightMeasureSpec: MeasureSpec) {
        textView.measure(widthMeasureSpec, heightMeasureSpec)
        measuredWidth = textView.measuredWidth + borderWidth.value * 2
        measuredHeight = textView.measuredHeight + borderWidth.value * 2
    }

    override fun layout(left: Int, top: Int, right: Int, bottom: Int) {
        textView.layout(
            left + borderWidth.value,
            top + borderWidth.value,
            right - borderWidth.value,
            bottom - borderWidth.value
        )
    }

    override fun draw(canvas: Canvas) {
        canvas.drawRect(
            0f,
            0f,
            measuredWidth.toFloat(),
            measuredHeight.toFloat(),
            borderColor
        )
        textView.draw(canvas)
    }
}

然后可以在其他地方使用这个自定义视图:

val borderedTextView: View = BorderedTextView("Custom Text", borderColor = Color.Red)
  1. 封装自定义组件: 为了方便复用,可以将多个视图组合成一个自定义组件。例如,创建一个包含用户名和密码输入框以及登录按钮的登录组件:
package com.example.shared

import org.jetbrains.kotlinx.multiplatform.ui.*

fun LoginComponent(viewModel: LoginViewModel): View = Column {
    TextField(
        value = viewModel.username.value,
        onValueChange = { viewModel.setUsername(it) },
        placeholder = { Text("Username") }
    )
    TextField(
        value = viewModel.password.value,
        onValueChange = { viewModel.setPassword(it) },
        placeholder = { Text("Password") },
        isPassword = true
    )
    Button(onClick = { viewModel.login() }) {
        Text("Login")
    }
}

这样在不同的界面中,只需要引入这个 LoginComponent 即可:

package com.example.shared

import org.jetbrains.kotlinx.multiplatform.ui.*

fun mainScreen(viewModel: LoginViewModel): View = Column {
    LoginComponent(viewModel)
    // 其他内容
}

与原生平台的交互

  1. 调用原生功能: 有时候需要在 MPUI 应用中调用原生平台的功能,比如调用 Android 的摄像头或 iOS 的相册。在 Android 平台,可以通过创建一个 AndroidBridge 类来实现:
package com.example.android

import android.content.Intent
import android.provider.MediaStore
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.example.shared.CameraBridge

class AndroidBridge(private val launcher: ActivityResultLauncher<Intent>) : CameraBridge {
    override fun openCamera() {
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        launcher.launch(intent)
    }
}

commonMain 中定义 CameraBridge 接口:

package com.example.shared

interface CameraBridge {
    fun openCamera()
}

在 iOS 平台,可以通过创建 IosBridge 类来实现类似功能:

package com.example.ios

import UIKit
import com.example.shared.CameraBridge

class IosBridge : CameraBridge {
    override fun openCamera() {
        let picker = UIImagePickerController()
        picker.sourceType =.camera
        picker.delegate = self
        UIApplication.shared.keyWindow?.rootViewController?.present(picker, animated: true, completion: nil)
    }
}
  1. 原生视图嵌入: 在某些情况下,可能需要将原生视图嵌入到 MPUI 界面中。在 Android 上,可以通过 AndroidView 来实现。例如,嵌入一个原生的 WebView
import android.webkit.WebView
import org.jetbrains.kotlinx.multiplatform.ui.android.AndroidView
import org.jetbrains.kotlinx.multiplatform.ui.*

val webView: View = AndroidView { context ->
    WebView(context).apply {
        loadUrl("https://www.example.com")
    }
}

在 iOS 上,可以通过 IosView 嵌入原生视图,比如嵌入一个 UITableView

import UIKit
import org.jetbrains.kotlinx.multiplatform.ui.ios.IosView
import org.jetbrains.kotlinx.multiplatform.ui.*

val tableView: View = IosView {
    let tableView = UITableView(frame: CGRect.zero)
    // 配置 tableView
    tableView
}

性能优化

  1. 减少视图层级: 复杂的视图层级会导致性能下降,因为每个视图都需要进行测量、布局和绘制。尽量简化视图层级,例如,避免不必要的嵌套布局。如果可以使用一个线性布局完成的布局,就不要使用多层嵌套的相对布局。
  2. 视图复用: 在列表等场景中,使用视图复用机制。MPUI 中类似于 Android 的 RecyclerView 或 iOS 的 UITableView 的组件,都支持视图复用。例如,创建一个简单的列表:
import org.jetbrains.kotlinx.multiplatform.ui.*

val dataList = listOf("Item 1", "Item 2", "Item 3")

val listView: View = LazyColumn {
    items(dataList) { item ->
        Text(item)
    }
}

这里的 LazyColumn 会自动复用视图,提高性能。 3. 异步加载: 对于需要加载大量数据或进行网络请求的操作,使用异步加载。例如,在登录界面中,如果登录请求需要较长时间,可以使用 Kotlin 的协程进行异步处理:

import kotlinx.coroutines.launch

class LoginViewModel {
    //...

    fun login() {
        launch {
            // 模拟网络请求
            delay(2000)
            println("Logging in with username: ${_username.value}, password: ${_password.value}")
        }
    }
}

这样在进行登录操作时,不会阻塞主线程,保证了界面的流畅性。

适配不同平台特性

  1. 平台特定样式: 不同平台有不同的设计风格和样式规范。例如,Android 和 iOS 的按钮样式就有所不同。可以通过平台特定的样式文件来进行适配。在 androidMain 中创建 styles.xml 文件,定义 Android 平台的按钮样式:
<style name="CustomButtonStyle" parent="Widget.MaterialComponents.Button.TextButton">
    <item name="android:backgroundTint">@color/colorPrimary</item>
    <item name="android:textColor">@color/white</item>
</style>

iosX64Main 中,可以通过代码设置 iOS 平台按钮的样式:

Button(onClick = { /* 点击逻辑 */ }) {
    Text("Login")
       .modifier(
            background(Color.blue),
            foregroundColor(Color.white)
        )
}
  1. 屏幕适配: 不同平台的屏幕尺寸和分辨率差异较大。MPUI 提供了一些工具来帮助进行屏幕适配。例如,使用 dp(密度无关像素)来定义尺寸,这样在不同密度的屏幕上都能保持合适的显示效果。
val container = Column(
    modifier = Modifier
       .fillMaxWidth()
       .height(200.dp)
) {
    // 子视图
}

同时,对于不同平台的屏幕方向变化,可以通过监听屏幕方向变化事件,并调整布局来适配。在 Android 上,可以在 Activity 中重写 onConfigurationChanged 方法:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    // 根据屏幕方向调整布局
}

在 iOS 上,可以通过 UIDeviceorientationDidChangeNotification 通知来监听屏幕方向变化:

NotificationCenter.default.addObserver(self, selector: #selector(handleDeviceOrientationChange), name: UIDevice.orientationDidChangeNotification, object: nil)

@objc func handleDeviceOrientationChange() {
    // 根据屏幕方向调整布局
}

通过以上各个方面的介绍,我们对 Kotlin 中的跨平台 UI 框架 MultiPlatform UI 有了较为深入的了解和实践。从环境搭建、核心组件使用,到跨平台开发实践、性能优化以及平台适配等,MPUI 为开发者提供了一套完整且强大的跨平台 UI 解决方案,帮助开发者更高效地构建多平台应用。