我在跨平台移动开发方面的经验缺乏一些重要的元素,例如颤音或Xamarin。因此,本文不是该空间中工具的全面分析。
多年来,我尝试了一些不同的工具来进行跨平台开发,包括PhoneGap(事后看来,我可能应该避免使用),React Native,QT和一些Kotlin Anitial。一般来说,我坚信UI应该是本地的,并且像PhoneGap这样的工具只是为了简单的应用程序而削减任何内容。尽管React Native具有其优缺点,但它并没有赢得我作为开发人员的胜利。相反,我更喜欢拥有跨平台核心和本机UI的想法。作为一个在Kitkat(4.4?ðä)左右从Android转到iOS的人,因为我自然更倾向于基于LLVM的语言,因此C ++是我的跨平台代码的首选。在iOS上,通过称为Objective-C ++的混合物桥接C ++和Objective-C相对容易。我从事一些基于这个想法的大型项目,我可以证明这是一个有效的解决方案。但是,Objective-C每天变得越来越流行,Objective-C ++是一个更可怕的野兽。我不能说我发现写它很愉快。此外,我看不到在C ++中编写应用程序级代码的任何令人信服的理由。也许对于OS级代码,但这是另一个讨论的话题。经过几次C ++尝试,我尝试了Kotlin Native(KN),即使在最早的版本中,它也具有更好的工具和IDE支持。 Kotlin是一种有趣的语言,可以读写,而有了“本地”部分,我们甚至可以摆脱JVM。因此,如果您已经沉浸在Android生态系统中,喜欢Kotlin,并喜欢在Android Studio中工作,那么KN应该是您的好选择。但是,在本文中,我想探索一个更“生锈的”观点。让我们潜水。
我几次都在iOS上涉足Rust,看起来很像C ++。您构建一个静态库,将C标头用作胶水,最终在调试中挣扎。当您仅将一小部分逻辑提取到共享库中并通过薄界面与之交互时,这种方法很简单。但是,如果您想将大部分App Logic放入共享的LIB中怎么办?那是事情变得棘手的时候。
最近,我偶然发现了在Rust London会议上的一个项目,引起了我的注意。它称为Crux,它是一个库,可帮助您实现功能性核心和命令性的外壳范式。换句话说,它允许您将应用程序逻辑与UI代码分开,并在平台之间共享。
尽管功能核心和命令性外壳的想法听起来很简单,但实际的实现可能很棘手。当您开始研究它时,您将不可避免地遇到障碍和挑战,尤其是在将核心逻辑与用户界面分开时。
在“可变命名”之后的第二大挑战是找到适当的架构要使用。传统的MVC/MVP体系结构可能并不总是最合适的,我发现很难跟踪我过去使用的应用程序中的所有数据流。此外,现实世界的用户界面可能是复杂且动态的,它在UI层中增加了更多状态和交互。
这是无副作用核心的功能概念。关键有助于建立基础。对我来说,这对于弄清楚如何构建我的代码以及如何以符合人体工程学且易于阅读的方式隔离核心逻辑确实很有帮助。在几个小时内,我创建了一个与dall-e apis交互的小应用程序(很明显,对吗?在下一节中,我将分享我的最初印象。
设置
由于该项目处于很早的阶段,因此将其设置并不像React Native那样无缝。但是,如果您决定使用此真正项目的堆栈,则为内部工具做出贡献并不是什么大问题。实际上,大多数大型项目,甚至是单平台的项目,都包含一个不同的bash脚本的动物园,无论如何都会制作文件。 book对其工作方式有很好的解释,甚至提供示例应用程序。
就个人而言,我发现最好使用本书从头开始设置该项目。这样,我就可以看到所有地方查看是否出现问题的地方。我花了不到一个小时的时间来建立核心和iOS项目,而且过程很简单。幸运的是,核心配置在.rs和toml文件中,非常容易遵循。
对于iOS,您需要一些bash脚本(哦,我讨厌写bash)。但是就我而言,即使需要在Bash中进行一些自定义,Chatgpt也可以忍受拷贝。长话短说,您需要将核心编译为静态库,使用uniffi板条板生成UI语言绑定,并将这些步骤添加到Xcode Project中,以便您无需手动重建和重新链接核心。 UNIFFI需要编写一个IDL接口定义语言文件,以描述目标语言可用的方法和数据结构。我分别为iOS/Android和Web生成了Swift/Kotlin和TS。
UDL看起来像这样:
namespace core {
sequence<u8> handle_event([ByRef] sequence<u8> msg);
sequence<u8> view();
};
最后,项目结构看起来像这样(屏幕截图上没有Android和Web):
发展
在开发方面,您可能会在Xcode/Android Studio和Rust和Web开发的任何东西之间分配时间。我见过一些勇敢的灵魂试图在Emacs进行移动开发,但归根结底,他们比队友慢得多。
好消息是,首先在核心上工作,制作界面和编写测试,然后将Xcode/Studio交换为核心核心位,非常方便。就我个人而言,我使用Clion作为Rust,我不敢一次打开3个以上(Clion/Xcode/Android Studio)。 Rust的编译非常缓慢,这对我来说不是问题,因为我的Swift/objc项目在工作中花费了大约50分钟的时间才能在顶部配置MacPro(不是MacBookð)上进行清洁。但是,对于Web开发人员而言,这可能有些阻碍。但是适当的项目模块化可以帮助这一点。
起初在Rust中编写代码可能有些挑战,但是我发现很多想法与Swift类似,因此这并不像完全不同的体验。像Swift这样的枚举,不是吗? ð
#[derive(Serialize, Deserialize)]
pub enum Event {
Reset,
Ask(String),
Gen(String),
#[serde(skip)]
Set(Result<Response<gpt::ChatCompletion>>),
#[serde(skip)]
SetImage(Result<Response<gpt::PictureMetadata>>),
}
在调试时,您可以通过 lldb“断点集” 命令使用断点来调试您链接的静态库中的Swift和Rust Code。它不像在Android Studio中调试纯Kotlin项目那样方便,但仍然可以完成工作。
日志中的精确行:
但是,我看不到分别调试核心和外壳的任何问题。实际上,能够独立调试每个组件可能会很有帮助,因为它可以更轻松地查明任何错误或问题的来源。
Interop呢...我不会撒谎,这不是理想的。特别是,Rust和Swift之间的互动并不像Swift/Objective-C和Kotlin/Java之间的无缝。例如, f64 不能像通过边界那样传递(是逻辑上的,但仍然)。但是,有一些作弊表可以帮助理解Interop规则。对于Swift,适用以下规则:
- 原语映射到他们明显的迅速对应物(例如
u32
变成UInt32
,string
变成 koud>String
等。 )。 - 称为
interface T
的对象接口表示为swift协议TProtocol
和一个具体的swift类T
,符合它。 li> - 声明
enum T
或[Enum] interface T
的枚举表示为swift EnumT
具有适当的变体。 - 使用Swift的内置可选类型语法表示可选类型
T?
。 - 序列表示为快速阵列,并将映射作为迅速字典。
- 错误表示为符合
Error
协议的快速枚举。 - 具有关联错误类型的函数调用用
throws
在Swift中标记。
我记得Kotlin Native的类似规则。实际上,核心和外壳之间的接口应为简洁。我认为这些局限性不好,但也不会受到太大伤害。
建筑学
谈论建筑模式。 您是否看过没有谈论模式的移动工程? Crux是受榆树的启发,book和Elm docs值得一读的好页面,值得一读,所以让我们跳过描述。总的来说,我看到移动到单向和消息传递体系结构。它们干净且非常严格,这使更新代码更容易,并且当一个文本字段在各个层中具有三个不同的状态时,不会引入不一致。的确,Uikit或Vanila Android库并不是最合适的(尽管仍然可以重复使用一些想法),但是SwiftUi和Jetpack构成的合适性非常好。如果您写手势互动和动画重型UI-这将是具有挑战性的。就像您进行了一些手势驱动的过渡一样,您是否应该将当前状态保持在UI中或将其传递给核心?或uitableview(ios)和recyclerview(android)具有一些不同的细胞生命周期,因此对于细胞模型,核心将如何处理它。有点具有挑战性,但仍然有可能,没有一如既往的银子弹。
我最喜欢的部分是功能功能。功能提供了一种处理副作用的好方法,例如网络,数据库和系统框架。当然,您可以在C中编写一个HTTP库并在任何地方使用它,也许您甚至可以将持久性标准化仅使用SQLite。但是,有许多不同的事情需要考虑,例如音频/视频,文件系统,通知,生物识别技术,甚至是苹果铅笔等外围设备。而且,您的系统已经有很好的库来处理这些事情,甚至可以优化这些内容(在iOS上的服务质量或URLSESSION配置)可以更有效。这就是功能的来源 - 他们允许您声明所需的内容,同时保留平台代码的实施细节。这是保持代码模块化和可维护的好方法。
当核心处理需要进行HTTP调用的事件时,它实际上是指示Shell进行呼叫。
fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
match event {
Event::Ask(question) => {
model.questions_number += 1;
gpt::API::new().make_request(&question, &caps.http).send(Event::Set);
},
...
和shell正在发送请求
switch req.effect {
...
case .http(let hr):
// create and start URLSession task
}
可以将相同的逻辑应用于数据库(只有单独的kv-storage and chirationalitation),生物特征。
最后的想法
尽管我是不熟悉Crux且还没有流利的生锈,但我还是能够构建一个在iOS,Android和Web(几乎)(几乎)(几乎)(几乎)的时间(几乎)的时间(几乎)的时间(几乎)的时间这三个从头开始。
Crux仍处于早期阶段,例如在我注意到时,HTTP功能不支持标题和身体。但是我寄希望于这个项目将继续增长并吸引更多的贡献者,因为它的背后想法真的很酷。
即使您不想将Rust用于跨平台开发,我认为值得一看该项目,以了解您如何能够重复使用自己喜欢的堆栈中的某些想法。归根结底,任何有助于我们编写更好,更模块化和更可维护的代码的东西都是胜利。