第 4 部分 - 处理多个平台
处理平台差异和功能
差异不仅仅是一个“跨平台”问题;“相同”平台上的设备具有不同的功能(尤其是各种可用的 Android 设备)。 最明显的和最基本的问题是屏幕大小,但其他设备属性可能会有所不同,并且要求应用程序检查某些功能,然后根据这些功能存在与否采取不同的行为。
这意味着所有应用程序都需要处理功能的正常降级,否则会提供一个无吸引力、最为基本的功能集。 通过将 Xamarin 与每个平台的本机 SDK 深度集成,应用程序可以利用特定于平台的功能,因此设计应用以使用这些功能是有意义的。
有关平台在功能方面的差异的概述,请参阅平台功能文档。
平台差异示例
跨平台存在的基本元素
移动应用程序的一些特征是通用的。 这些更高级别的概念通常适用于所有设备,因此可以构成应用程序设计的基础:
- 通过选项卡或菜单选择功能
- 数据和滚动列表
- 数据的单个视图
- 编辑数据的单个视图
- 导航返回
设计高级屏幕流时,可以基于这些概念建立一种常见的用户体验。
特定于平台的属性
除了所有平台上存在的基本元素外,还需要解决设计中的关键平台差异。 可能需要考虑(并专门编写代码来处理)以下差异:
- 屏幕大小 – 某些平台(如 iOS 和早期 Windows Phone 版本)具有标准化屏幕大小,以此类屏幕为目标则相对简单。 Android 设备具有多种屏幕尺寸,因此在应用程序中提供支持需要更多工作量。
- 导航隐喻 – 在平台间(例如硬件“后退”按钮、全景 UI 控件)和平台内(Android 2 和 4、iPhone 与 iPad)有所不同。
- 键盘 – 某些 Android 设备具有物理键盘,而其他设备只有软件键盘。 当软键盘遮挡部分屏幕时便可检测到的代码需要对这些差异敏感。
- 触摸和手势 – 操作系统对手势识别的支持各不相同,尤其是在每个操作系统的早期版本中。 早期版本的 Android 对触摸操作的支持非常有限,这意味着支持较旧的设备可能需要单独的代码
- 推送通知 - 每个平台上都有不同的功能/实现(例如 Windows 上的动态磁贴)。
特定于设备的功能
确定应用程序所需的最低功能必须是什么;或决定在每个平台上利用哪些附加功能。 需要代码来检测特征并禁用功能或提供替代方案(例如,地理定位的替代方案是让用户键入位置或从地图中进行选择):
- 摄像头 – 不同设备的功能各不相同:某些设备没有摄像头,其他设备则同时具有前置和后置摄像头。 某些摄像头能够录制视频。
- 地理定位和地图 - 并非所有设备都支持 GPS 或 Wi-Fi 定位。 应用还需要满足每个方法支持的不同准确度级别。
- 加速计、陀螺仪和指南针 – 这些功能通常仅在每个平台上的一系列设备中找到,因此,当硬件不受支持时,应用几乎总是需要提供回退。
- Twitter 和 Facebook - 仅在 iOS5 和 iOS6 上分别“内置”。 在早期版本和其他平台上,需要提供自己的身份验证函数,并直接与每个服务的 API 进行交互。
- 近场通信 (NFC) - 仅在(一些)Android 手机上(在写入时)。
处理平台差异
通过同一代码库支持多个平台有两种不同的方法,但每种方法都各有利弊。
- 平台抽象 – 业务外观模式,提供跨平台的统一访问,并将特定平台实现抽象化为单个统一的 API。
- 分化实现 – 通过接口、继承或条件编译等架构工具,以不同的实现方式调用特定的平台功能。
平台抽象
类抽象
使用在共享代码中定义的接口或基类,并在特定于平台的项目中实现或扩展。 使用类抽象编写和扩展共享代码特别适用于可移植类库,因为它们可用的框架子集有限,并且不能包含用于支持特定于平台的代码分支的编译器指令。
接口
使用接口可以实现特定于平台的类,这些类仍可传递到共享库以利用通用代码。
该接口在共享代码中定义,并以参数或属性的形式传递到共享库。
然后,特定于平台的应用程序可以实现接口,并且仍利用共享代码来“处理”它。
优点
实现可以包含特定于平台的代码,甚至可以引用特定于平台的外部库。
缺点
必须创建实现并将其传递到共享代码中。 如果在共享代码深处使用了接口,则接口最终通过多个方法参数传递,或者通过调用链向下推送。 如果共享代码使用了许多不同的接口,则必须在共享代码的某个位置创建和设置这些接口。
继承
共享代码可以实现可在一个或多个特定于平台的项目中扩展的抽象类或虚拟类。 这类似于使用接口,但已实现某些行为。 接口和继承,哪个是更好的设计选择?关于这个问题,存在不同的观点:特别是因为 C# 只允许单一继承,因此它可以决定 API 今后的设计方式。 请谨慎使用继承。
接口的优缺点同样适用于继承,另外一个优点是基类可以包含一些实现代码(也许是与整个平台无关的实现,可以选择进行扩展)。
Xamarin.Forms
请参阅 Xamarin.Forms 文档。
其他跨平台库
这些库还为 C# 开发人员提供跨平台功能:
- Xamarin.Essentials – 常用功能的跨平台 API。
- SkiaSharp – 跨平台 2D 图形。
条件编译
在某些情况下,共享代码仍需在每个平台上以不同的方式工作,可能访问行为不同的类或功能。 条件编译最适合共享资产项目,即在定义了不同符号的多个项目中引用相同的源文件。
Xamarin 项目始终定义 __MOBILE__
,这同时适用于 iOS 和 Android 应用程序项目(请注意这些符号在修复前后的双下划线)。
#if __MOBILE__
// Xamarin iOS or Android-specific code
#endif
iOS
Xamarin.iOS 定义了 __IOS__
,它可用于检测 iOS 设备。
#if __IOS__
// iOS-specific code
#endif
还有特定于 Watch 和 TV 的符号:
#if __TVOS__
// tv-specific stuff
#endif
#if __WATCHOS__
// watch-specific stuff
#endif
Android
仅应编译到 Xamarin.Android 应用程序中的代码可以使用以下
#if __ANDROID__
// Android-specific code
#endif
每个 API 版本还定义了新的编译器指令,因此,如果面向较新的 API,此类代码将允许你添加功能。 每个 API 级别都包含所有“较低”级别符号。 此功能对于支持多个平台并不真正有用;通常,__ANDROID__
符号便已足够。
#if __ANDROID_11__
// code that should only run on Android 3.0 Honeycomb or newer
#endif
Mac
Xamarin.Mac 定义了 __MACOS__
,它仅可用于编译 macOS:
#if __MACOS__
// macOS-specific code
#endif
通用 Windows 平台 (UWP)
使用 WINDOWS_UWP
。 Xamarin 平台符号等字符串周围没有下划线。
#if WINDOWS_UWP
// UWP-specific code
#endif
使用条件编译
条件编译的一个简单的案例研究示例是设置 SQLite 数据库文件的文件位置。 这三个平台对指定文件位置的要求略有不同:
- iOS – Apple 倾向于将非用户数据放置在特定位置(库目录),但此目录没有系统常量。 生成正确的路径需要特定于平台的代码。
- Android –
Environment.SpecialFolder.Personal
返回的系统路径是可以接受的数据库文件存储位置。 - Windows Phone – 独立存储机制不允许指定完整路径,只允许指定相对路径和文件名。
- 通用 Windows 平台 – 使用
Windows.Storage
API。
以下代码使用条件编译来确保 DatabaseFilePath
对于每个平台均正确:
public static string DatabaseFilePath
{
get
{
var filename = "TodoDatabase.db3";
#if SILVERLIGHT
// Windows Phone 8
var path = filename;
#else
#if __ANDROID__
string libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
#else
#if __IOS__
// we need to put in /Library/ on iOS5.1 to meet Apple's iCloud terms
// (they don't want non-user-generated data in Documents)
string documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal); // Documents folder
string libraryPath = Path.Combine (documentsPath, "..", "Library");
#else
// UWP
string libraryPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
#endif
#endif
var path = Path.Combine(libraryPath, filename);
#endif
return path;
}
}
结果是可在所有平台上生成和使用类,并将 SQLite 数据库文件放置在每个平台上的不同位置。