kotlin 跨平台开发
I wanted to experience Kotlin MultiPlatform development to understand the complexities involved in adopting this for Android and iOS projects. For this, I wanted to target a small use case and solve that by using Kotlin Multiplatform. In this article, I’ll share my observations with you as you take this journey with me.
我想体验Kotlin MultiPlatform开发,以了解在Android和iOS项目中采用它的复杂性。 为此,我想针对一个小用例,并通过使用Kotlin Multiplatform解决该问题。 在本文中,当您与我同行时,我将与您分享我的看法。
(Prerequisites)
- I assume you have familiarity with Android & iOS development.
- I assume you have sufficient exposure to Kotlin & Swift to understand DSL.
First, We’ll go through the problem, followed by one of the possible solutions to the problem and how we’ll use the MultiPlatform approach to solve the problem for Android and iOS, finally conclude by showing how we can use what we built in both platforms.
首先,我们将解决该问题,然后是该问题的可能解决方案之一,以及如何使用MultiPlatform方法来解决Android和iOS的问题,最后通过展示如何使用我们内置的内容来总结两个平台。
(1. Overview)
During a fast-paced App Development cycle, we’ll encounter some problems. We’ll go through them briefly and define a goal we wanted to achieve.
在快速的App开发周期中,我们会遇到一些问题。 我们将简要介绍它们,并定义我们要实现的目标。
(1.1 Problem)
In this section, we’ll walk through four scenarios which can create bottlenecks.
在本节中,我们将介绍四种可能造成瓶颈的方案。
1.1.1 EnvironmentTo help understand the environment issue, the figure below shows the non-working environment in red and working environments in Green. An environment can be DEV, SIT, UAT or PROD. Given our App was pointing to red, we wanted to have the flexibility of switching the environments without performing a rebuild to triage if this issue is related to the environment as opposed to a code path within the App.
1.1.1环境为了帮助理解环境问题,下图显示了红色的非工作环境和绿色的工作环境。 环境可以是DEV,SIT,UAT或PROD。 鉴于我们的应用程序指向红色,因此,如果此问题与环境(而不是应用程序中的代码路径)有关,我们希望具有切换环境的灵活性,而无需执行重新构建以进行分类。
[1.1.1] Environment [1.1.1]环境
1.1.2 FeatureTo understand the issue related to a non-working feature, the figure below shows our App as being composed of Feature blocks and the working features in Green. In contrast, the non-working feature in Red. Given we wanted to identify the problematic feature in Red, we wanted to have the flexibility to turn features on or off without performing a rebuild to triage the problem.
1.1.2功能要了解与无法使用的功能有关的问题,下图显示了我们的App由功能块和绿色的可用功能组成。 相反,红色中的无效功能。 鉴于我们要确定Red中有问题的功能,我们希望能够灵活地打开或关闭功能,而无需执行重建来对问题进行分类。
[1.1.2] Feature
[1.1.2]功能
1.1.3 Tuning ThresholdsConsider we wanted to adjust timing intervals in our App to make them perform better or appear suitable. The figure below shows an App contacting the API and receiving a response. If our timeout interval is too short, our App stops listening soon on the other hand if it is long our App waits for a long time and when this wait is on a critical path of the App, we’ll see a notable lag in our App’s performance. To predict the correct values, we wanted to have a flexibility of adjusting the timeout values to identify what works for our App better, without having to rebuild our App each time.
1.1.3调整阈值考虑我们想在App中调整时间间隔,以使其表现更好或看起来更合适。 下图显示了一个与API联系并收到响应的应用。 如果我们的超时间隔太短,则如果我们的应用很长一段时间,那么我们的应用就会立即停止监听,而如果等待时间很长,并且当此等待处于应用的关键路径上时,我们会发现我们的响应明显滞后应用程序的性能。 为了预测正确的值,我们希望能够灵活地调整超时值,从而确定对我们的App更有效的方法,而不必每次都重新构建App。
[1.1.3] Tuning Thresholds
[1.1.3]调整阈值
1.1.4 Dynamic ValueConsider we wanted to edit a configuration value to test a change quickly without having to rebuild our App. To illustrate this scenario, the figure below shows our App pointing to a non-existing domain example.com while we wanted to perform a quick check to ensure the new domain global.example.com works as expected.
1.1.4动态值考虑到我们想编辑配置值以快速测试更改,而无需重建我们的应用程序。 为了说明这种情况,下图显示了我们的应用程序指向一个不存在的域example.com,而我们想执行一次快速检查以确保新域global.example.com可以正常工作。
[1.1.4] Dynamic Value [1.1.4]动态值
(1.2 Goal)
Having gone through the concerns mentioned above, let us define our goal, as shown below:
克服了上述问题后,让我们定义目标,如下所示:
“Given that the App has the necessary code. we wanted to change how the App behaves once deployed on the device with minimal effort.”
“鉴于该应用具有必要的代码。 我们希望更改应用程序在设备上部署后的行为方式。”
(2. Solution)
Considering our goal, we wanted the flexibility to edit the configuration of our App once deployed. For this, we’ll place a Configuration Entry Point(CEP) in the launcher of our App, that allows us to update the configuration before the Main App is loaded. To illustrate this with an example, the diagram below shows launcher having two entry points to our App, the one shown in Green is the Main Entry Point(MEP) to our App. In contrast, the one shown in yellow allows us to customise the settings before launching the MEP.
考虑到我们的目标,我们希望在部署后能够灵活地编辑应用程序的配置。 为此,我们将在应用程序的启动器中放置一个配置入口点 (CEP),这使我们能够在加载主应用程序之前更新配置。 为了举例说明,下图显示了具有两个应用程序入口点的启动器,绿色显示的是应用程序的主要入口点 (MEP)。 相比之下,黄色显示的内容使我们可以在启动MEP之前自定义设置。
[2.0.1] Solution Overview [2.0.1]解决方案概述
We’ll implement this solution using the MultiPlatform approach to generate native Library for Android & iOS. Our Library consists of three layers, as shown in the diagram below:
我们将使用MultiPlatform方法实施此解决方案,以生成适用于Android和iOS的本地库。 我们的库由三层组成,如下图所示:
[2.0.2] Solution Architecture [2.0.2]解决方案架构
Layer One — This layer contains the data models and the logic of our solution. The code in this layer does not depend on the other two layers and uses Kotlin. The Kotlin code here can natively compile for use in Multiple Platforms such as Android, iOS, Windows, Mac, Linux, JavaScript and JVM to name the prominent ones.
第一层 -该层包含数据模型和解决方案的逻辑。 该层中的代码不依赖于其他两层,而是使用Kotlin。 这里的Kotlin代码可以本地编译以用于多个平台,例如Android,iOS,Windows,Mac,Linux,JavaScript和JVM。
Layer Two — This layer contains platform-specific implementation for saving the configuration values in our solution. The code in this layer depends on Layer One but does not depend on Layer Three. This layer contains platform-specific Kotlin code for iOS and Android.
第二层 -该层包含特定于平台的实现,用于在我们的解决方案中保存配置值。 该层中的代码取决于第一层,但不取决于第三层 。 该层包含适用于iOS和Android的特定于平台的Kotlin代码。
Layer Three — This layer contains native UI implementation on the corresponding platform. The code in this layer depends on Layer Two and Layer One. In the target Android, this layer contains Android layout files and the corresponding Kotlin code for the UI. On the target iOS, this layer contains Swift code that uses the UIKit framework to handle configuration state change in UI.
第三层 -该层包含相应平台上的本机UI实现。 该层中的代码取决于第二层和第一层 。 在目标Android中,此层包含Android布局文件和相应的UI Kotlin代码。 在目标iOS上,此层包含Swift代码,该代码使用UIKit框架来处理UI中的配置状态更改。
Note: Whatever is in Layer Two can also be in Layer Three and vice versa. It is a decision we have to make based on the complexity. In this case, having the persistence layer done completely in Layer Two was simple compared to having to do the UI layer completely in Layer Two. Hence the decision to push the UI implementation to Layer Three.
注意: 无论在第二层中是什么,也可以在第三层中,反之亦然。 我们必须根据复杂度做出决定。 在这种情况下,与必须在第二层中完全完成UI层相比,在第二层中完全完成持久层是很简单的。 因此,决定将UI实现推到第三层。
(2.1 DSL — Layer One)
To have a consistent way of writing the configuration across platforms iOS and Android, we’ll develop DSL’s using Kotlin Lambda Extension. In the implementation of those DSL’s the following attribute are shared.
为了在iOS和Android平台之间以一致的方式编写配置,我们将使用Kotlin Lambda Extension开发DSL。 在那些DSL的实现中,以下属性是共享的。
- key — enables retrieval of value in the configuration. key-启用配置中的值检索。
- description — provides an option to describe the intention of the configuration. 描述 —提供描述配置意图的选项。
2.1.1 Solving Environment Problem using Choice DSLTo tackle the problem highlighted in Environment[1.1.1], we’ll develop a choice DSL, as shown below. This DSL gives us the flexibility to select one of the items from the available choices. In the Choice DSL we have the following:
2.1.1使用Choice DSL解决环境问题为了解决Environment [1.1.1]中突出显示的问题,我们将开发Choice DSL,如下所示。 该DSL使我们可以灵活地从可用选项中选择一项。 在Choice DSL中,我们具有以下功能:
- currentChoiceIndex — a value of the default item selected in the configuration. currentChoiceIndex —在配置中选择的默认项目的值。
- item — available options in this configuration. item-此配置中的可用选项。
Implementation实现
2.1.2 Solving Feature Problem using Switch DSLTo tackle the problem highlighted in the Feature [1.1.2], we’ll develop a switch DSL, as shown below. This DSL gives us the flexibility to turn features ON or OFF. In the Switch DSL we have the following:
2.1.2使用交换机DSL解决功能问题为了解决功能[1.1.2]中强调的问题,我们将开发一个交换机DSL,如下所示。 此DSL使我们可以灵活地打开或关闭功能。 在Switch DSL中,我们有以下内容:
- switchValue — current value of the Switch true/false. switchValue-开关的当前值true / false。
Implementation实现
2.1.3 Solving Tuning Thresholds using Range DSLTo tackle Tuning Threshold [1.1.3], we’ll develop a range DSL, as shown below. This DSL gives us the flexibility to adjust a value within a specified min & max range. In the range DSL we have the following:
2.1.3使用范围DSL解决调整阈值为了解决调整阈值[1.1.3] ,我们将开发一个范围DSL,如下所示。 该DSL使我们可以灵活地在指定的最小和最大范围内调整值。 在DSL范围内,我们有以下内容:
- min — minimum value allowed by this configuration min —此配置允许的最小值
- max — maximum value allowed in this configuration max —此配置中允许的最大值
- currentValue — the present value of this configuration currentValue —此配置的当前值
Implementation实现
2.1.4 Solving Dynamic Value using Editable DSLTo tackle Dynamic Value[1.1.4], we’ll develop an editable DSL, as shown below. This DSL gives us the flexibility to edit a given value to our heart’s content.
2.1.4使用可编辑的DSL解决动态值为了解决动态值[1.1.4] ,我们将开发可编辑的DSL,如下所示。 该DSL使我们可以灵活地编辑给我们心脏内容的给定值。
- currentValue — the present value of this configuration currentValue —此配置的当前值
Implementation实现
2.1.5 AppConfig and Config DSLWe wanted the flexibility to change multiple configurations related to the App and be able to try different variations of the grouping to find what works better for our App. For this, we have AppConfig & Config DSL as shown below.
2.1.5 AppConfig和Config DSL我们希望能够灵活地更改与该App相关的多种配置,并能够尝试分组的不同变体,以找到对我们的App更有效的方法。 为此,我们具有如下所示的AppConfig和Config DSL。
Implementation实现 Implementation实现
(2.2 Persistence — Layer Two)
Once the configuration change is applied, subsequent starts of the Main App must have the new configuration. To do that the persistence layer has two responsibilities one is to load an existing configuration the other is to save a modified configuration. For those responsibilities, we have Android & iOS implementation written using Kotlin in Layer Two.
应用配置更改后,主应用程序的后续启动必须具有新配置。 为此,持久层有两个职责,一个是加载现有配置,另一个是保存修改后的配置。 对于这些职责,我们在第二层中使用Kotlin编写了Android和iOS实现。
- Android uses SharedPreferences Android使用SharedPreferences
- iOS uses NSUserDefaults iOS使用NSUserDefaults
2.2.1 Load Configuration — Layer OneThe following snippet illustrates loading of saved configuration.
2.2.1加载配置-第一层以下代码段说明了已保存配置的加载。
Implementation) 实现 )
2.2.2 Save Configuration — Layer OneThe following snippet illustrates saving an updated configuration.
2.2.2保存配置-第一层以下代码段说明了保存更新的配置。
Implementation) 实现 )
2.2.3 Setting Expectations — Layer OneWe can see from [2.2.1] and [2.2.2] that the code in Layer One is a template of the operation expecting implementation from the platform for Settings. Kotlin native has an elegant way of doing this, as shown below.
2.2.3设置期望-第一层从[2.2.1]和[2.2.2]中可以看到,第一层中的代码是期望从设置平台实现的操作的模板。 如下所示,Kotlin本地人拥有一种优雅的方式来执行此操作。
Implementation) 实施 )
2.2.4 Actual Implementation — Layer TwoTo meet the expectations of Layer One, we have to provide corresponding actual implementations in Layer Two. Kotlin has an excellent way of meeting this expectation, as illustrated below.
2.2.4实际实现-第二层为了满足第一层的期望,我们必须在第二层中提供相应的实际实现。 如下图所示,Kotlin是满足此期望的绝妙方法。
Android ImplementationIn Android, we meet the expectations using SharedPreferences.
Android实现在Android中,我们使用SharedPreferences满足了期望。
Implementation) 实现 )
iOS ImplementationIn iOS, we meet the expectations using NSUserDefaults.
iOS实现在iOS中,我们使用NSUserDefaults达到了期望。
Implementation) 实现 )
(2.3 UI — Layer Three)
When we launch the App through the CEP, we’ll transform the configuration written in DSL into UI controls corresponding to the platform. We have covered this logic using the native implementation of Android & iOS written in Kotlin & Swift.
通过CEP启动应用程序时,我们会将DSL中编写的配置转换为与平台相对应的UI控件。 我们使用用Kotlin&Swift编写的Android和iOS的本机实现涵盖了这种逻辑。
To make sense of how the whole Library works, we’ll look at a sample app that makes use of the Library.
为了理解整个库的工作方式,我们将看一个利用库的示例应用程序。
Note: The more code we have in Layer One than in the other two layers, the easier it is to maintain the codebase. The fixes our improvements applied in Layer One is available to all targets. Our goal in coding all of these layers is to get as much as possible into Layer One.
注意:在第一层中比在其他两层中拥有更多的代码,维护代码库就越容易。 我们在第一层中应用的改进中的修复程序可用于所有目标。 我们对所有这些层进行编码的目标是尽可能多地进入第一层。
(3. Building App using the Library)
After developing all three layers of our Library, Consider we have published Android & iOS Libraries to Maven Central and CocoaPods. In the following section, we’ll use the published libraries on our sample project.
开发完我们库的所有三层之后,考虑我们已经向Maven Central和CocoaPods发布了Android和iOS库。 在以下部分中,我们将在示例项目中使用已发布的库。
(3.1 Dependencies)
To use these libraries natively on the target platform, follow the setup process shown below:
要在目标平台上本地使用这些库,请遵循以下显示的设置过程:
3.1.1 Android Dependency
3.1.1 Android依赖性
build.gradle build.gradle
3.1.2 iOS Dependency
3.1.2 iOS依赖关系
Podfile Podfile
(3.2 Configure the App)
The sample project has two different configurations for Free and Premium users and contains a view with a text whose attributes we would like to configure before launch. The attributes we are interested in are listed below:
该示例项目为Free和Premium用户提供了两种不同的配置,并包含一个带有文本的视图,该文本的属性我们希望在启动之前进行配置。 下面列出了我们感兴趣的属性:
- Visibility 能见度
- Text Size 字体大小
- Current Text 当前文字
- Text Color 文字颜色
3.2.1 Android ConfigurationThe configuration for our sample project is expressed in Android as follows.
3.2.1 Android配置我们的示例项目的配置在Android中表示如下。
Android App Configuration ( Android应用配置 ( Full Snippet) 完整代码段)
3.2.2 iOS ConfigurationThe configuration for our sample project is expressed in iOS as follows.
3.2.2 iOS配置我们的示例项目的配置在iOS中表示如下。
iOS App Configuration ( iOS应用配置 ( Full Snippet) 完整代码段 )
(3.3 Initialise Configuration)
We can initialise our configuration module during the application start as follows.
我们可以在应用程序启动期间初始化配置模块,如下所示。
3.3.1 Initialise Android with UIFor Android, we’ll use the context in Layer Two of our Library for getting a handle on shared preference. On the other hand, we use the Intent on Layer Three of our Library to launch the start activity after we have updated the configuration.
3.3.1使用UI初始化Android对于Android,我们将使用库第二层中的上下文来获取共享首选项的句柄。 另一方面,在更新配置后,我们使用库第三层上的Intent启动启动活动。
Initialise Android with UI 使用UI初始化Android
3.3.2 Initialise iOS with UIFor iOS, we’ll use the group name in Layer Two of our Library to initialise the NSUserDefaults. On the other hand, we use the controller in Layer Three to navigate to the start of our App after updating the configuration.
3.3.2使用UI初始化iOS对于iOS,我们将使用库第二层中的组名来初始化NSUserDefaults。 另一方面,在更新配置后,我们使用第三层中的控制器导航到应用程序的开始。
Initialise iOS with UI 使用UI初始化iOS
(3.4 Using current configuration values)
After initialising our Library, we can obtain the updated configuration values at any point in our App, as shown below. We can use the keys defined in our configuration DSL to acquire the value corresponding to the configuration.
初始化我们的库后,我们可以在App的任何位置获取更新的配置值,如下所示。 我们可以使用配置DSL中定义的密钥来获取与配置相对应的值。
3.4.1 Android Usage
3.4.1 Android使用
Android Usage Android使用情况
3.4.2 iOS Usage
3.4.2 iOS使用
iOS Usage iOS使用情况
(4. Result)
Shows our App has two entry points the MEP and the CEP. In Android on including the Library, this happens automatically as a result of Manifest merge. In iOS, we create two targets and associate the targets to the same App Group, so updating the settings in one target reflects in the other.
显示我们的应用程序具有MEP和CEP两个入口点。 在包含库的Android中,这是由于清单合并而自动发生的。 在iOS中,我们创建了两个目标并将这些目标关联到同一个应用组,因此更新一个目标中的设置会反映在另一个目标中。
[4] Android Left, iOS Right — MEP & CEP [4] Android左,iOS右-MEP和CEP
We launch our App using the CEP. During the launch, we use the DSL to create the UI for the App dynamically. In the illustration below, shows the starting point of the CEP. Each item visible here is an aggregate of configurations that are applied to the App if selected. In the illustration below, we wanted to adjust the premium configuration for our App. Hence we tap on it to edit the configuration further.
我们使用CEP启动我们的应用程序。 在启动过程中,我们使用DSL为应用程序动态创建UI。 在下图中,显示了CEP的起点。 如果选中,此处可见的每个项目都是应用到该应用程序的配置的汇总。 在下图中,我们想为我们的应用程序调整高级配置。 因此,我们点击它以进一步编辑配置。
[4] Android Left, iOS Right — Dynamic UI from DSL [4] Android左,iOS右-来自DSL的动态UI
Once in PREMIUM configuration, we update the settings as shown below:
进入高级配置后,我们将更新设置,如下所示:
[4] Android Left, iOS Right — Update configuration value [4] Android左,iOS右-更新配置值
After updating the configuration, launching our MEP by tapping the Launch PREMIUM button or launching the MEP from the Launcher has the updated configuration values.
更新配置后,通过点击启动高级按钮或从启动器启动MEP来启动我们的MEP具有更新后的配置值。
[4] Android Left, iOS Right — Load using new configuration [4] Android左,iOS右-使用新配置加载
(5. Easter Egg)
We have an easter egg hidden in Layer One of the Common Code that can be useful at the time of shipping the App where we don’t need Layer Two(Persistence) and Layer Three(UI). It is a read-only configuration useful after we have finalised the configuration values.
我们在通用代码的第一层中隐藏了一个复活节彩蛋,在不需要第二层(持久性)和第三层(UI)的情况下,在交付应用程序时会很有用。 完成配置值后,这是一个只读配置。
(5.1 Android)
Making use of this easter egg in Android is a two-step process.
在Android中使用此复活节彩蛋是一个分为两个步骤的过程。
5.1.1 DependencyWe have to package the Non-UI variant separately in Android because we wanted to avoid placing the CEP on Launcher. CEP is auto-configured when we depend on the UI variant of the Library due to Manifest merge.
5.1.1依赖性我们必须在Android中单独打包Non-UI变体,因为我们希望避免将CEP放在Launcher上。 当由于清单合并而依赖库的UI变体时,将自动配置CEP。
Non-UI Dependency 非UI依赖性
5.1.2 Initialising LibraryYou’ll find that the start intent and the context are missing in the initialisation.
5.1.2初始化库您会发现初始化中缺少开始意图和上下文。
Initialising Library 初始化库
(5.2 iOS Initialising Library)
You’ll find that the group name and the start controller are missing in the initialisation.
您会发现初始化中缺少组名和启动控制器。
Initialising Library 初始化库
(5.3 Output)
The figure below shows the App start using the DSL without Persistence Layer and UI Layer from the Library.
下图显示了应用程序开始使用没有库中的持久层和UI层的DSL。
[5] No UI Start [5]没有用户界面开始
We have come to the end of our journey in MultiPlatform, and before we depart, I have some comments. At the time of this writing, MultiPlatform is experimental. Before it stabilises, the implementation shown here allows me to use the code in test environments and gain the confidence to extend its use further.
我们已经结束了MultiPlatform的旅程,在出发之前,我有一些评论。 在撰写本文时,MultiPlatform还处于实验阶段。 在稳定之前,这里显示的实现使我可以在测试环境中使用该代码,并有信心进一步扩展其使用范围。
I undertook this journey because I was excited at the proposition of being able to target Multiple Platforms using Kotlin. I was surprised a few times when I was calling top-level Kotlin functions, Kotlin objects and overloaded member functions from Swift. But, fear not for you can find your way easily using the generated framework headers.
我之所以进行此旅程,是因为我对能够使用Kotlin定位多个平台的提议感到兴奋。 当我从Swift调用顶级Kotlin函数,Kotlin对象和重载成员函数时,我感到有些惊讶。 但是,不用担心,您可以使用生成的框架标头轻松找到自己的方式。
At this point, we conclude this material, hope it is useful to someone who takes the Multiplatform adventure. However, when I look at the illustration on [5], I can’t help but ponder the fact:
至此,我们结束了本材料,希望对参加多平台冒险的人有用。 但是,当我看一下[5]上的插图时,我不禁要思考一个事实:
Though we have our differences, we are the same at the core.
尽管我们有差异,但核心是相同的。
翻译自: https://medium.com/dev-genius/multiplatform-development-using-kotlin-2f9fbe3115ee
kotlin 跨平台开发