解决使用 Flutter 时常见的跨平台问题
已发表: 2022-03-10我在网上看到很多关于使用 Flutter 进行 Web 开发的困惑,而且很遗憾,这通常是出于错误的原因。
具体来说,人们有时会将其与旧的基于 Web 的移动(和桌面)跨平台框架相混淆,后者基本上只是在包装应用程序中运行的浏览器中运行的网页。
从某种意义上说,这确实是跨平台的,因为您只能访问通常在 Web 上可访问的界面,因为您只能访问相同的界面。
然而,Flutter 并非如此:它在每个平台上原生运行,这意味着每个应用程序的运行就像在 Android 和 iOS 上用 Java/Kotlin 或 Objective-C/Swift 编写时一样。 您需要知道这一点,因为这意味着您需要注意这些非常多样化的平台之间的许多差异。
在本文中,我们将看到其中的一些差异以及如何克服它们。 更具体地说,我们将讨论存储和 UI 差异,这是开发人员在编写他们想要跨平台的 Flutter 代码时最常引起混淆的差异。
示例 1:存储
我最近在我的博客上写道,与移动应用程序相比,在 Web 应用程序中存储 JWT 需要一种不同的方法。
这是因为平台存储选项的性质不同,并且需要了解每个平台及其原生开发工具。
网络
当您编写 Web 应用程序时,您拥有的存储选项是:
- 向/从磁盘下载/上传文件,这需要用户交互,因此仅适用于打算由用户读取或创建的文件;
- 使用 cookie,可能会或可能无法从 JS 访问(取决于它们是否为
httpOnly
),并与请求一起自动发送到给定域并在它们作为响应的一部分出现时保存; - 使用 JS
localStorage
和sessionStorage
,网站上的任何 JS 都可以访问,但只能从作为该网站页面一部分的 JS 访问。
移动的
移动应用程序的情况完全不同。 存储选项如下:
- 该应用程序可访问的本地应用程序文档或缓存存储;
- 用户创建/可读文件的其他本地存储路径;
-
NSUserDefaults
和SharedPreferences
分别在 iOS 和 Android 上用于 key-value 存储; - iOS 上的
Keychain
和 Android 上的KeyStore
,分别用于安全存储任何数据和加密密钥。
如果您不知道这一点,您的实现就会变得一团糟,因为您需要知道您实际使用的存储解决方案以及优缺点是什么。
跨平台解决方案:初步方法
使用 Flutter shared_preferences
包在 Web 上使用localStorage
,在 Android 上使用SharedPreferences
在 iOS 上使用NSUserDefaults
。 这些对您的应用程序有完全不同的影响,特别是如果您要存储会话令牌之类的敏感信息:客户端可以读取localStorage
,因此如果您容易受到 XSS 的攻击,这是一个问题。 尽管移动应用程序并不真正容易受到 XSS 的攻击,但SharedPreferences
和NSUserDefaults
不是安全存储方法,因为它们可能在客户端受到破坏,因为它们不是安全存储且未加密。 这是因为它们是针对用户偏好的,正如这里在 iOS 和 Android 文档中提到的那样,在谈论安全库时,该库旨在为SharedPreferences
提供包装器,专门用于在存储数据之前对数据进行加密。
移动设备上的安全存储
移动设备上唯一的安全存储解决方案分别是 iOS 和 Android 上的Keychain
和KeyStore
,而Web 上没有安全存储。
但是, Keychain
和KeyStore
在本质上是非常不同的: Keychain
是一种通用的凭证存储解决方案,而KeyStore
用于存储(并且可以生成)加密密钥,可以是对称密钥,也可以是公钥/私钥。
这意味着,例如,如果您需要存储会话令牌,则在 iOS 上,您可以让操作系统管理加密部分并将您的令牌发送到Keychain
,而在 Android 上,这更像是手动体验,因为您需要生成(不是硬编码,这很糟糕)密钥,使用它来加密令牌,将加密的令牌存储在SharedPreferences
并将密钥存储在KeyStore
。
有不同的方法,就像安全方面的大多数事情一样,但最简单的可能是使用对称加密,因为不需要公钥加密,因为您的应用程序同时加密和解密令牌。
显然,您不需要编写所有这些工作的特定于移动平台的代码,例如,有一个 Flutter 插件可以完成所有这些工作。
网络上缺乏安全存储
实际上,这就是迫使我写这篇文章的原因。 我写过关于使用该软件包将 JWT 存储在移动应用程序上的文章,人们想要它的 Web 版本,但正如我所说, Web 上没有安全存储。 它不存在。
这是否意味着您的 JWT 必须公开?
一点都不。 您可以使用httpOnly
cookie,不是吗? 这些是 JS 无法访问的,只会发送到您的服务器。 这样做的问题是它们总是被发送到您的服务器,即使您的一个用户单击其他人网站上的 GET 请求 URL,并且该 GET 请求具有您或您的用户不喜欢的副作用。 这实际上也适用于其他请求类型,只是更复杂。 它被称为跨站点请求伪造,您不希望这样。 它是 Mozilla 的 MDN 文档中提到的网络安全威胁之一,您可以在其中找到更完整的解释。
有预防方法。 实际上,最常见的是有两个令牌:一个作为httpOnly
cookie 到达客户端,另一个作为响应的一部分。 后者必须存储在localStorage
而不是 cookie 中,因为我们不希望它自动发送到服务器。
解决两者
如果您同时拥有移动应用程序和 Web 应用程序怎么办?
这可以通过以下两种方式之一处理:
- 使用相同的后端端点,但使用与 cookie 相关的 HTTP 标头手动获取和发送 cookie;
- 创建一个单独的非 Web 后端端点,该端点生成与 Web 应用程序使用的任一令牌不同的令牌,然后如果客户端能够提供仅限移动设备的令牌,则允许常规 JWT 授权。
在不同的平台上运行不同的代码
现在,让我们看看我们如何在不同的平台上运行不同的代码,以便能够弥补差异。
创建一个 Flutter 插件
尤其是解决存储问题,一种方法是使用插件包:插件提供通用的 Dart 接口,可以在不同的平台上运行不同的代码,包括原生平台特定的 Kotlin/Java 或 Swift/Objective-C 代码. 开发包和插件相当复杂,但在 Web 和其他地方(例如 Flutter 书籍)的许多地方都有解释,包括 Flutter 官方文档。

例如,对于移动平台,已经有一个安全存储插件,即flutter_secure_storage
,您可以在此处找到一个使用示例,但例如,它在 Web 上不起作用。
另一方面,对于也适用于 Web 的简单键值存储,有一个跨平台的 Google 开发的第一方插件包,称为shared_preferences
,它有一个称为shared_preferences_web
的 Web 特定组件,它使用NSUserDefaults
、 SharedPreferences
或localStorage
取决于平台。
颤振上的目标平台
导入package:flutter/foundation.dart
后,您可以将Theme.of(context).platform
与值进行比较:
-
TargetPlatform.android
-
TargetPlatform.iOS
-
TargetPlatform.linux
-
TargetPlatform.windows
-
TargetPlatform.macOS
-
TargetPlatform.fuchsia
并编写您的函数,以便对于您要支持的每个平台,它们执行适当的操作。 这对于下一个平台差异示例特别有用,即小部件在不同平台上的显示方式差异。
特别是对于那个用例,还有一个相当流行的flutter_platform_widgets
插件,它简化了平台感知小部件的开发。
示例 2:相同小部件的显示方式不同
你不能仅仅编写跨平台代码就假装浏览器、手机、电脑和智能手表是同一回事——除非你希望你的 Android 和 iOS 应用程序是一个 WebView,而你的桌面应用程序是用 Electron 构建的. 有很多不这样做的理由,本文的重点不是说服您使用 Flutter 之类的框架来保持您的应用程序原生,并具有随之而来的所有性能和用户体验优势,同时允许您编写大多数时间在所有平台上都相同的代码。
但是,这需要细心和关注,并且至少对您想要支持的平台、它们的实际原生 API 以及所有这些有基本的了解。 React Native 用户需要更加注意这一点,因为该框架使用内置的 OS 小部件,因此您实际上需要通过在两个平台上进行广泛测试来更加注意应用程序的外观,而不能在两者之间切换iOS 和 Material 小部件就像 Flutter 一样。
没有您的要求会发生什么变化
当您切换平台时,您的应用程序 UI 的某些方面会自动更改。 本节还提到了 Flutter 和 React Native 在这方面的变化。
Android 和 iOS 之间 (Flutter)
Flutter 能够在 iOS 上渲染 Material 小部件(以及在 Android 上渲染 Cupertino(类 iOS)小部件),但它不做的是在 Android 和 iOS 上显示完全相同的东西:Material 主题特别适应每个平台的约定.
例如,导航动画和过渡以及默认字体是不同的,但它们不会对您的应用产生太大影响。
在美学或用户体验方面可能会影响您的一些选择的是,一些静态元素也会发生变化。 具体来说,一些图标在两个平台之间会发生变化,应用栏标题在 iOS 上位于中间,在 Android 上位于左侧(在可用空间的左侧,以防有后退按钮或打开抽屉的按钮(此处解释)在 Material Design 指南中,也称为汉堡菜单)。这是带有 Drawer 的 Material 应用在 Android 上的外观:

同样的,非常简单的 Material 应用在 iOS 上的样子:

在移动设备和 Web 之间使用屏幕缺口 (Flutter)
在 Web 上存在一些不同的情况,正如这篇关于使用 Flutter 进行响应式 Web 开发的 Smashing 文章中所提到的:特别是,除了必须针对更大的屏幕进行优化并考虑人们期望浏览您的站点的方式之外——这是那篇文章的主要焦点——你不得不担心有时小部件被放置在浏览器窗口之外的事实。 此外,某些手机的屏幕顶部有凹口,或者由于某种障碍而妨碍正确查看您的应用程序。
这两个问题都可以通过将您的小部件包装在SafeArea
小部件中来避免,这是一种特殊类型的填充小部件,可确保您的小部件落入可以实际显示的位置,而不会妨碍用户查看它们的能力,无论是硬件约束还是软件约束。
在反应原生
React Native 需要更多的关注和对每个平台的更深入的了解,此外还要求您至少运行 iOS 模拟器和 Android 模拟器以便能够在两个平台上测试您的应用程序:它不是同样,它将其 JavaScript UI 元素转换为特定于平台的小部件。 换句话说,您的 React Native 应用程序将始终看起来像 iOS — 带有有时被称为 Cupertino UI 元素 — 而您的 Android 应用程序将始终看起来像常规 Material Design Android 应用程序,因为它使用平台的小部件。
这里的不同之处在于 Flutter 使用自己的低级渲染引擎渲染其小部件,这意味着您可以在一个平台上测试两个应用程序版本。
解决这个问题
除非你想要一些非常具体的东西,否则你的应用程序应该在不同的平台上看起来不同,否则你的一些用户会不高兴。
就像您不应该简单地将移动应用程序发布到网络上一样(正如我在前面提到的 Smashing 帖子中所写的那样),您也不应该将一个充满 Cupertino 小部件的应用程序发布给 Android 用户,因为这会让您感到困惑。大部分。 另一方面,有机会实际运行具有适用于另一个平台的小部件的应用程序,您可以测试该应用程序并将其展示给两个版本的人,而不必为此使用两台设备。
另一面:出于正确的原因使用错误的小部件
但这也意味着您可以在 Linux 或 Windows 工作站上进行大部分 Flutter 开发,而不会牺牲 iOS 用户的体验,然后只需为其他平台构建应用程序,而不必担心对其进行彻底测试。
下一步
跨平台框架很棒,但它们将责任转移给开发人员,以了解每个平台的工作原理以及如何确保您的应用程序适应并为您的用户带来愉快的使用。 其他需要考虑的小事情可能是,例如,如果不同平台上有不同的约定,则对本质上可能是同一事物的事物使用不同的描述。
不必使用不同的语言分别构建两个(或更多)应用程序很好,但您仍然需要记住,本质上,您正在构建多个应用程序,这需要考虑您正在构建的每个应用程序.
更多资源
- Flutter Gallery 网站和 Android 应用程序,展示了不同平台典型的 Flutter 小部件的使用及其平台无关性
- TargetPlatform 上的 Flutter API 文档
- 关于创建包和插件的 Flutter 文档
- Flutter 平台适配文档
- 关于 cookie 的 MDN 文档