Kotlin Multiplatform 是跨平台开发的未来吗? 关于如何开始的提示

已发表: 2021-01-29

如今,我们可以观察到移动开发的趋势是更快地发布应用程序。 有许多尝试通过在不同平台(如 Android 和 iOS)之间共享通用代码部分来减少开发时间。 一些解决方案已经流行起来,而其他解决方案仍在开发中。 今天,我想讨论第二组中的一种最新方法——Kotlin Multiplatform Mobile(简称 KMM)。

什么是 Kotlin 多平台移动设备?

KMM 是一个 SDK,主要旨在在平台之间共享业务逻辑——在大多数情况下,这部分无论如何都必须相同。 这要归功于共享模块的一组多个编译器。 例如,Android 目标使用 Kotlin/JVM 变体,而 iOS 则使用 Kotlin/Native 变体。 然后可以将共享模块添加到典型的原生应用程序项目中,负责 UI 的开发人员可以专注于在他们熟悉的环境中为用户提供最佳体验——Android Studio for Android 和 Xcode for iOS。

Kotlin 多平台与 Flutter

目前,跨平台应用程序开发最流行的解决方案之一是 Flutter。 它专注于“编写一个应用程序并在任何地方运行它”的规则——它有效,但仅适用于简单的应用程序。 在实际情况下,开发人员通常不得不为每个平台编写原生代码来填补空白,例如,当缺少某些插件时。 使用这种方法,应用程序在不同平台上看起来相同,这有时是可取的,但在某些情况下,它可能会违反特定的设计准则。

跨平台开发服务图标

准备好构建自己的应用了吗?

选择颤振

虽然听起来很相似,但 Kotlin Multiplatform 并不是一个跨平台的解决方案——它不会试图重新发明轮子。 开发人员仍然可以使用他们熟悉和喜欢的工具。 它只是简化了重用以前应该多次编写的部分代码的过程,例如发出网络请求、存储数据和其他业务逻辑。

Kotlin 多平台的优缺点

KMM 的优点

  • 开发的应用程序对于每个平台都是 100% 原生的- 很容易与当前使用的代码和第三方库集成
  • 易于使用——几乎所有的 Android 开发者都已经在使用 Kotlin,因此他们几乎不需要额外的知识就可以开始使用
  • UI 可以针对每个目标平台进行拆分——该应用程序将与任何给定的生态系统保持一致
  • 共享逻辑允许开发人员同时在两个操作系统上添加新功能并修复错误

KMM 的缺点

  • 许多组件仍处于 Alpha/Beta 阶段,未来可能会不稳定或发生变化

哪些公司使用 KMM?

根据官方网站,公司对这项技术的兴趣越来越大,而且名单越来越长。 其中,有 Autodesk、VMWare、Netflix 或 Yandex 等知名品牌。

如何开始使用 Kotlin 多平台?

深入了解信息的最佳位置是官方指南,但在本文中,我想展示一个相当简单但比“Hello World”更有趣的示例,即应用程序获取和显示Randall Munroe 的最新漫画(根据 CC BY-NC 2.5 许可),其标题来自 xkcd.com API。

要涵盖的功能:

  • 项目设置
  • 共享模块中的网络
  • 适用于 Android 和 iOS 的简单 UI

注意:我希望这个示例对 Android 和 iOS 开发人员都一样容易阅读,所以在某些地方我故意省略了一些特定于平台的良好实践,只是为了清楚发生了什么

项目设置

首先,确保您安装了最新版本的 Android Studio 和 Xcode ,因为它们都是构建此项目所必需的。 然后,在 Android Studio 中,安装 KMM 插件。 这个插件简化了很多事情——要创建一个新项目,只需单击 Create New Project 并选择 KMM Application。

创建一个新的 Kotlin Multiplatform Mobile 项目

创建项目后,导航到共享目录中的build.gradle.kts文件。 在这里,您必须指定所有必需的依赖项。 在这个例子中,我们将使用 ktor 作为网络层,使用kotlinx.serialization来解析来自后端的 json 响应,并使用 kotlin 协程来异步完成这一切。

为简单起见,下面我提供的清单显示了必须添加到已经存在的依赖项的所有依赖项。 添加依赖项时,只需同步项目即可(会出现提示)。 首先,将序列化插件添加到插件部分。

 插件{
   kotlin("plugin.serialization") 版本 "1.4.0"
}

然后添加依赖项。

 源集{
   val commonMain 通过获取 {
       依赖{
           实施(“org.jetbrains.kotlinx:kotlinx-序列化-json:1.0.0”)
           实施(“org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9-native-mt-2”)

           实现(“io.ktor:ktor-client-core:1.4.1”)
           实现(“io.ktor:ktor-client-json:1.4.1”)
           实现(“io.ktor:ktor-client-serialization:1.4.1”)
       }
   }
   val androidMain 通过获取 {
       依赖{
           实现(“io.ktor:ktor-client-android:1.4.1”)
       }
   }
   val iosMain 通过获取 {
       依赖{
           实现(“io.ktor:ktor-client-ios:1.4.1”)
       }
   }
}

值得一提的是,在撰写本文时,iOS 上的协程库的稳定版本存在一些问题——这就是为什么使用的版本有 native-mt-2 后缀(代表原生多线程)的原因。 您可以在此处查看此问题的当前状态。

共享模块中的网络

首先,我们需要一个表示响应的类——这些字段存在于后端返回的 json 中。

 导入 kotlinx.serialization.Serializable

@Serializable
数据类 XkcdResponse(
   val img:字符串,
   val 标题:字符串,
   验证日:诠释,
   val月份:整数,
   值年:整数,
)

接下来,我们需要使用 HTTP 客户端创建一个表示 API 的类。 如果我们没有提供 json 中存在的所有字段,我们可以使用ignoreUnknownKeys属性,以便序列化程序可以忽略丢失的字段。 此示例只有一个由挂起函数表示的端点。 这个修饰符告诉编译器这个函数是异步的。 我将使用特定于平台的代码对其进行更多描述。

 导入 io.ktor.client.*
导入 io.ktor.client.features.json.*
导入 io.ktor.client.features.json.serializer.*
导入 io.ktor.client.request.*

类 XkcdApi {
   私人 val baseUrl = "https://xkcd.com"

   私人 val httpClient = HttpClient() {
       安装(JsonFeature){
           序列化器 = KotlinxSerializer(
               kotlinx.serialization.json.Json {
                   忽略未知键=真
               }
           )
       }
   }

   暂停乐趣 fetchLatestComic() =
       httpClient.get<XkcdResponse>("$baseUrl/info.0.json")

}

当我们的网络层准备好时,我们可以移动到领域层并创建一个代表本地数据模型的类。 在这个例子中,我跳过了更多的字段,只留下了漫画标题和图片的 URL。

 数据类 ComicModel(
   val imageUrl:字符串,
   val 标题:字符串
)

这一层的最后一部分是创建一个用例,该用例将触发网络请求,然后将响应映射到本地模型。

 类GetLatestComicUseCase(私人val xkcdApi:XkcdApi){
   暂停乐趣 run() = xkcdApi.fetchLatestComic()
       .let { ComicModel(it.img, it.title) }
}

简单的安卓用户界面

是时候移动到androidApp目录了——这是存储原生 Android 应用程序的地方。 首先,我们需要向位于此处的另一个build.gradle.kts文件添加一些特定于 Android 的依赖项。 同样,下面的清单只显示了应该添加到已经存在的依赖项中的依赖项。 这个应用程序将使用 Model-View-ViewModel 架构(前两行),并使用 Glide 从返回的 URL 加载漫画图像(后两行)

 依赖{
   实现(“androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0”)
   实现(“androidx.lifecycle:lifecycle-livedata-ktx:2.2.0”)
   实施(“com.github.bumptech.glide:glide:4.11.0”)
   annotationProcessor("com.github.bumptech.glide:compiler:4.11.0")
}

默认情况下,新创建的项目应包含 MainActivity 及其布局文件activity_main.xml 。 让我们为其添加一些视图——一个TextView用于标题,一个 ImageView 用于漫画本身。

 <?xml 版本="1.0" 编码="utf-8"?>
<线性布局 xmlns:andro
   安卓:
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   机器人:重力=“中心”
   安卓:方向=“垂直”>

   <文本视图
       安卓:
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

   <图像视图
       安卓:
       android:layout_width="match_parent"
       android:layout_height="wrap_content"/>

</线性布局>

然后我们需要一些应用程序状态的表示- 它可以是加载新漫画,显示它,或者我们可能在加载时遇到错误。

 密封类状态{
   对象加载:状态()
   类成功(验证结果:ComicModel):状态()
   对象错误:状态()
}

现在让我们使用之前创建的业务逻辑添加一个最小的 ViewModel 。 可以导入所有类。 MutableLiveData是一个可观察的字段——视图将观察它的变化并相应地更新自己。 viewModelScope是一个与 viewmodel 的生命周期相关联的协程作用域——如果应用程序关闭,它将自动取消挂起的任务。

 类 MainViewModel : ViewModel() {
   私人 val getLatestComicUseCase = GetLatestComicUseCase(XkcdApi())
   val 漫画 = MutableLiveData<State<ComicModel>>()

   有趣的 fetchComic() {
       viewModelScope.launch {
           Comic.value = State.Loading()
           runCatching { getLatestComicUseCase.run() }
               .onSuccess { Comic.value = State.Success(it) }
               .onFailure { Comic.value = State.Error() }
       }
   }
}

最后一件事 – MainActivity 将所有内容连接起来。

 类 MainActivity : AppCompatActivity(R.layout.activity_main) {
   private val viewModel: MainViewModel bylazy {
      ViewModelProvider(this).get(MainViewModel::class.java)
   }

   覆盖 fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      viewModel.comic.observe(这个) {
         当(它){
            是 State.Loading -> {
               findViewById<TextView>(R.id.titleLabel).text = "加载中"
            }
            是 State.Success -> {
               findViewById<TextView>(R.id.titleLabel).text = it.result.title
               Glide.with(this)
                  .load(it.result.img)
                  .into(findViewById(R.id.image))
            }
            是 State.Error -> {
               findViewById<TextView>(R.id.titleLabel).text = "错误"
            }
         }
      }
      viewModel.fetchComic()
   }
}

就是这样,Android 应用程序已准备就绪!

使用 KMM 开发的 Android 应用程序

iOS的简单用户界面

上面的一切都是在 Android Studio 中完成的,所以对于这一部分,让我们切换到 Xcode 以使其更方便。 为此,只需打开 Xcode 并选择 iosApp 目录——它包含一个预配置的 Xcode 项目。 默认情况下,该项目使用 SwiftUI 作为 GUI,所以为了简单起见,让我们坚持使用它。

首先要做的是创建获取漫画数据的基本逻辑。 就像以前一样,我们需要一些东西来表示状态。

 枚举状态{
    装箱
    案例成功(漫画模型)
    案例错误
}

接下来,让我们再次准备一个 ViewModel

 类 ViewModel:ObservableObject {
    让 getLatestComicUseCase = GetLatestComicUseCase(xkcdApi: XkcdApi())
        
    @Published var Comic = State.loading
        
    在里面() {
        self.comic = .loading
        getLatestComicUseCase.run { fetchedComic,错误
            如果 fetchedComic != nil {
                self.comic = .success(fetchedComic!)
            } 别的 {
                self.comic = .error
            }
        }
    }
}

最后,观点。

注意:为简单起见,我使用 SwiftUI 组件 RemoteImage 来显示图像,就像我在 Android 上使用 Glide 一样。

 结构内容视图:查看{
 
    @ObservedObject private(set) var viewModel: ViewModel
    
    var body: 一些视图 {
        漫画视图()
    }
    --
    私人 func ComicView() -> 一些视图 {
        切换 viewModel.comic {
        案例加载:
            返回 AnyView(文本(“正在加载”))
        案例.result(让漫画):
            返回任何视图(VStack {
                文字(漫画.标题)
                RemoteImage(网址:comic.img)
            })
        案例.错误:
            返回 AnyView(文本(“错误”))
        }
    }
}

就是这样,iOS 应用程序也准备好了!

使用 KMM 开发的 iOS 应用程序

概括

最后,回答标题中的问题——Kotlin Multiplatform 是跨平台开发的未来吗? – 这一切都取决于需求。 如果您想同时为两个移动平台创建一个小的、相同的应用程序,那么可能不会,因为您需要具备两个平台的开发所需的知识。

发布产品图标

与我们的专家一起为您开发下一个应用程序

获取报价

但是,如果您已经拥有一支由 Android 和 iOS 开发人员组成的团队,并且想要提供最佳的用户体验,那么它可以显着减少开发时间。 就像在提供的示例中一样,由于共享模块,应用程序逻辑只实现了一次,并且用户界面以完全特定于平台的方式创建。 那么为什么不试一试呢? 如您所见,它很容易上手。

从业务角度对跨平台开发感到好奇吗? 查看我们关于跨平台开发优势的文章。