跨平台应用程序案例研究:Tasky

TaskyPortable 是一个简单的待办事项列表应用程序。 本文档讨论如何根据构建跨平台应用程序文档的指导来设计和构建该应用。 讨论涵盖以下方面:

设计过程

建议在开始编码之前为想要实现的目标创建路线图。 尤其是在跨平台开发、构建的功能将以多种方式公开的情况下。 在开始时明确构建的目标,可以帮助你在开发周期的后期节省时间和精力。

要求

设计应用程序的第一步是确定所需的功能。 这些可以是高级别的目标,也可以是详细的用例。 Tasky 具有简单的功能要求:

  • 查看任务列表
  • 添加、编辑和删除任务
  • 将任务的状态设置为“已完成”

设计时,你可考虑如何应用特定于平台的功能。 例如,Tasky 是否可以利用 iOS 地理围栏或 Windows Phone 动态磁贴? 即使没有在第一个版本中使用特定于平台的功能,你也应该提前计划,以确保业务和数据层能够兼容此类功能。

用户界面设计

从可以跨目标平台实现的高级别设计入手。 请注意特定于平台的 UI 限制。 例如,TabBarController 在 iOS 中可以显示 5 个以上的按钮,而在 Windows Phone 中的等效项最多可以显示 4 个按钮。 使用所选工具绘制屏幕流(图纸设计工作)。

Draw the screen-flow using the tool of your choice paper works

数据模型

了解需要存储哪些数据将有助于确定要使用的持久性机制。 请参阅跨平台数据访问,获取可用存储机制的相关信息和如何选择机制的帮助。 对于此项目,我们将使用 SQLite.NET。

Tasky 需要为每个“TaskItem”存储三个属性:

  • Name – String
  • 备注 - 字符串
  • 完成 - 布尔值

核心功能

请考虑用户界面满足要求所需要使用的 API。 待办事项列表需要以下函数:

  • 列出所有任务 - 显示所有可用任务的主屏幕列表
  • 获取一个任务 - 当触摸任务行时
  • 保存一个任务 - 编辑任务时
  • 删除一个任务 - 删除任务时
  • 创建空任务 - 创建新任务时

若要实现代码重用,应该一进入可移植类库即应用此 API。

实现

一旦应用程序设计通过,请考虑如何将它作为跨平台应用程序实现。 这将成为应用程序的体系结构。 按“构建跨平台应用程序”文档中的指导,应用程序代码应分为以下部分:

  • 通用代码 – 包含可重用代码以存储任务数据的通用项目;公开模型类和 API 来管理数据的保存和加载。
  • 特定于平台的代码 – 将通用代码用作“后端”,为每个操作系统实现本机 UI 的平台特定项目。

Platform-specific projects implement a native UI for each operating system, utilizing the common code as the back end

以下部分将详述这两部分。

通用 (PCL) 代码

Tasky Portable 使用可移植类库策略来共享通用代码。 有关代码共享选项的说明,请参阅“共享代码选项”文档。

所有通用代码(包括数据访问层、数据库代码和协定)都放置在库项目中。

完整的 PCL 项目说明如下。 可移植库中的所有代码都与每个目标平台兼容。 部署后,每个本机应用将引用该库。

When deployed, each native app will reference that library

下面的类图显示了按层分组的类。 SQLiteConnection类是 Sqlite-NET 包中的样板代码。 其余类是 Tasky 的自定义代码。 TaskItemManagerTaskItem类表示向特定于平台的应用程序公开的 API。

The TaskItemManager and TaskItem classes represent the API that is exposed to the platform-specific applications

使用命名空间分隔层有助于管理每个层之间的引用。 特定于平台的项目应仅需要包含业务层的using语句。 数据访问层和数据层应由业务层中TaskItemManager公开的 API 封装。

参考

可移植类库需要能跨多个平台使用,每个平台都有不同级别的平台和框架功能支持。 因此,这限制了哪些包和框架库是可以使用的。 例如,Xamarin.iOS 不支持 c# dynamic关键字,因此可移植类库不能使用依赖于动态代码的任何包,即使此类代码适用于 Android。 Mac 的 Visual Studio 将阻止你添加不兼容的包和引用,但你需要记住有哪些限制,避免之后措手不及。

注意:你将看到项目引用未使用的框架库。 这些引用包含在 Xamarin 项目模板中。 编译应用时,链接过程将删除未引用的代码,因此即使System.Xml已被引用,它也不会包含在最终应用程序中,因为我们不使用任何 Xml 函数。

数据层 (DL)

数据层包含执行数据物理存储的代码——无论储存是以数据库、平面文件还是其他机制进行。 Tasky 数据层由两个部分组成:SQLite-NET 库和用于连接它而添加的自定义代码。

Tasky 依赖于 Sqlite-net NuGet 包(由 Frank Krueger 发布)来嵌入提供对象关系映射 (ORM) 数据库接口的 SQLite-NET 代码。 该TaskItemDatabase类继承自SQLiteConnection,并添加所需的 Create、Read、Update、Delete (CRUD) 方法,以从 SQLite 读取和向其写入数据。 它是通用 CRUD 方法的简单样板实现,可重复用于其他项目。

TaskItemDatabase 是一个单一实例,可确保执行所有访问时针对同一实例。 通过锁定防止来自多个线程的并发访问。

Windows Phone 上的 SQLite

虽然 iOS 和 Android 都将 SQLite 作为操作系统的一部分一起交付,但 Windows Phone 不包含兼容的数据库引擎。 若要在三个平台上共享代码,需要 Windows Phone 本机版本的 SQLite。 有关为 SQLite 设置 Windows Phone 项目的详细信息,请参阅“使用本地数据库”

使用接口通用化数据访问

数据层依赖于BL.Contracts.IBusinessIdentity层,以便其可以实现需要主键的抽象数据访问方法。 随后,任何实现接口的业务层类都可以保存在数据层中。

该接口仅指定要充当主键的整数属性:

public interface IBusinessEntity {
    int ID { get; set; }
}

基类实现接口并添加 SQLite-NET 属性,以将其标记为自动递增的主键。 随后,在业务层中实现此基类的任何类都可以保存在数据层中:

public abstract class BusinessEntityBase : IBusinessEntity {
    public BusinessEntityBase () {}
    [PrimaryKey, AutoIncrement]
    public int ID { get; set; }
}

使用接口的数据层中的泛型方法示例如下GetItem<T>

public T GetItem<T> (int id) where T : BL.Contracts.IBusinessEntity, new ()
{
    lock (locker) {
        return Table<T>().FirstOrDefault(x => x.ID == id);
    }
}

锁定以防止并发访问

TaskItemDatabase类中实现锁定,以防止对数据库的并发访问。 这是为了确保序列化来自不同线程的并发访问(否则 UI 组件可能会在后台线程更新数据库时尝试读取数据库)。 下面展示了如何实现锁定的示例:

static object locker = new object ();
public IEnumerable<T> GetItems<T> () where T : BL.Contracts.IBusinessEntity, new ()
{
    lock (locker) {
        return (from i in Table<T> () select i).ToList ();
    }
}
public T GetItem<T> (int id) where T : BL.Contracts.IBusinessEntity, new ()
{
    lock (locker) {
        return Table<T>().FirstOrDefault(x => x.ID == id);
    }
}

大多数数据层代码可以在其他项目中重新使用。 层中唯一特定于应用程序的代码是TaskItemDatabase构造函数中的CreateTable<TaskItem>调用。

数据访问层 (DAL)

TaskItemRepository类使用强类型 API 封装数据存储机制,该 API 允许创建、删除、检索和更新TaskItem对象。

使用条件编译

该类使用条件编译来设置文件位置——这是一个实现平台差异的示例。 返回路径的属性编译到每个平台上的不同代码。 代码和特定于平台的编译器指令展示如下:

public static string DatabaseFilePath {
    get {
        var sqliteFilename = "TaskDB.db3";
#if SILVERLIGHT
        // Windows Phone expects a local path, not absolute
        var path = sqliteFilename;
#else
#if __ANDROID__
        // Just use whatever directory SpecialFolder.Personal returns
        string libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); ;
#else
        // 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"); // Library folder
#endif
        var path = Path.Combine (libraryPath, sqliteFilename);
                #endif
                return path;
    }
}

根据平台的不同,在 iOS 的输出将为“<app path>/Library/TaskDB.db3”,在 Android 的输出为“<app path>/Documents/TaskDB.db3”,在 Windows Phone 的输出则为“TaskDB.db3”。

业务层 (BL)

业务层实现模型类和用来管理这些模型类的门面。 在 Tasky 中,模型是TaskItem类,TaskItemManager实现门面模式,以提供用于管理TaskItems的 API。

门面

TaskItemManager包装DAL.TaskItemRepository以提供 Get、Save 和 Delete 方法,这些方法将由应用程序和 UI 层引用。

如果需要,将在此处放置业务规则和逻辑,例如保存对象之前必须满足的任何验证规则。

特定于平台的代码的 API

编写通用代码后,必须生成用户界面以收集和显示其公开的数据。 TaskItemManager类实现外观门面模式,提供一个简单的 API 供应用程序代码访问。

在每个特定于平台的项目中编写的代码通常紧密耦合到该设备的本机 SDK,并且仅使用由TaskItemManager定义的 API 访问通用代码。 这包括代码公开的方法和业务类,例如TaskItem

图像不会跨平台共享,而是单独添加到每个项目。 这一点很重要,因为每个平台使用不同的文件名、目录和分辨率以不同的方式处理图像。

剩余部分讨论 Tasky UI 特定于平台的实现的详细信息。

iOS 应用

只有少数类需要通过通用 PCL 项目存储和检索数据来实现 iOS Tasky 应用程序。 完整的 iOS Xamarin.iOS 项目如下所示:

iOS project is shown here

此图中显示了这些分组到层中的类。

The classes are shown in this diagram, grouped into layers

参考

iOS 应用引用特定于平台的 SDK 库,例如 Xamarin.iOS 和 MonoTouch.Dialog-1。

它还必须引用TaskyPortableLibrary PCL 项目。 引用列表如下所示:

The references list is shown here

应用程序层和用户界面层是使用这些引用在此项目中实现的。

应用程序层 (AL)

应用程序层包含特定于平台的类,这些类需要将 PCL 公开的对象“绑定”到 UI。 特定于 iOS 的应用程序有两个类来帮助显示任务:

  • EditingSource – 此类用于将任务列表绑定到用户界面。 因为MonoTouch.Dialog用于“任务”列表,所以我们需要实现此帮助程序,才能在UITableView中启用轻扫删除功能。 轻扫删除在 iOS 上很常见,但对 Android 或 Windows Phone 则不然,因此仅能在特定于 iOS 的项目上实现该功能。
  • TaskDialog – 此类用于将单个任务绑定到 UI。 它使用MonoTouch.Dialog反射 API 将TaskItem对象与包含正确属性的类“包装”在一起,以允许正确设置输入屏幕的格式。

TaskDialog类使用MonoTouch.Dialog属性,以基于类的属性创建屏幕。 该类如下所示:

public class TaskDialog {
    public TaskDialog (TaskItem task)
    {
        Name = task.Name;
        Notes = task.Notes;
        Done = task.Done;
    }
    [Entry("task name")]
    public string Name { get; set; }
    [Entry("other task info")]
    public string Notes { get; set; }
    [Entry("Done")]
    public bool Done { get; set; }
    [Section ("")]
    [OnTap ("SaveTask")]    // method in HomeScreen
    [Alignment (UITextAlignment.Center)]
    public string Save;
    [Section ("")]
    [OnTap ("DeleteTask")]  // method in HomeScreen
    [Alignment (UITextAlignment.Center)]
    public string Delete;
}

请注意,这些OnTap属性需要方法名称,而这些方法必须存在于创建MonoTouch.Dialog.BindingContext的类中(在本例中为下一部分讨论的HomeScreen类)。

用户界面层 (UI)

用户界面层由以下类组成:

  1. AppDelegate – 包含对外观 API 的调用,用于设置应用程序中使用的字体和颜色的样式。 Tasky 是一个简单的应用程序,因此没有其他初始化任务在FinishedLaunching中运行。
  2. 屏幕 - UIViewController的子类,用于定义每个屏幕及其行为。 屏幕将 UI 与应用程序层类和通用 API (TaskItemManager) 绑定在一起。 在此示例中,屏幕是在代码中创建的,但可能是使用 Xcode 的 Interface Builder 或情节提要 (storyboard) 设计器设计。
  3. 图像 - 视觉元素是每个应用程序的重要组成部分。 Tasky 具有初始屏幕和图标图像,在 iOS 中,这些屏幕和图像必须以常规分辨率和 Retina 分辨率提供。

主屏幕

主屏幕是MonoTouch.Dialog屏幕。显示 SQLite 数据库中的任务列表。 它继承DialogViewController并实现代码来设置Root,以包含要显示的TaskItem对象的集合。

It inherits from DialogViewController and implements code to set the Root to contain a collection of TaskItem objects for display

关于任务列表的显示和与任务列表的交互的两个主要方法包括:

  1. PopulateTable - 使用业务层的TaskManager.GetTasks方法,检索要显示的TaskItem对象的集合。
  2. 已选中 - 当触摸行时,在新屏幕中显示任务。

任务详细信息屏幕

任务详细信息是允许编辑或删除任务的输入屏幕。

Tasky 使用MonoTouch.Dialog的反射 API 显示屏幕,因此没有UIViewController实现。 相反,HomeScreen类使用来自应用层的TaskDialog类,对DialogViewController进行实例化和显示。

此屏幕截图显示了一个空屏幕,该屏幕演示了Entry属性在名称备注字段中设置水印文本:

This screenshot shows an empty screen that demonstrates the Entry attribute setting the watermark text in the Name and Notes fields

任务详细信息屏幕的功能(例如保存或删除任务)必须在HomeScreen类中实现,因为这是创建MonoTouch.Dialog.BindingContext的位置。 以下HomeScreen方法支持“任务详细信息”屏幕:

  1. ShowTaskDetails - 创建一个MonoTouch.Dialog.BindingContext,用于呈现屏幕。 它使用反射创建输入屏幕,以从TaskDialog类中检索属性名称和类型。 其他信息(如输入框的水印文本)通过属性面板上的属性实现。
  2. SaveTask - 此方法通过OnTap属性在TaskDialog类中引用。 此方法在按下保存时被调用,并使用MonoTouch.Dialog.BindingContext检索用户输入的数据,然后再使用TaskItemManager保存更改。
  3. DeleteTask - 此方法通过OnTap属性在TaskDialog类中引用。 它用TaskItemManager删除使用主键(ID 属性)的数据。

Android 应用

完整的 Xamarin.Android 项目如下图所示:

Android project is pictured here

展示按层分组类的类图:

The class diagram, with classes grouped by layer

参考

Android 应用项目必须引用特定于平台的 Xamarin.Android 程序集才能访问来自 Android SDK 的类。

它还必须引用 PCL 项目(例如 TaskyPortableLibrary)用于访问通用数据和业务层代码。

TaskyPortableLibrary to access the common data and business layer code

应用程序层 (AL)

与我们之前介绍的 iOS 版本类似,Android 版本中的应用程序层包含特定于平台的类,这些类是将核心公开的对象“绑定”到 UI 所需要的。

TaskListAdapter - 若要显示对象的 List<T>,我们需要实现适配器来在ListView中显示自定义对象。 适配器控制列表中每个项使用哪个布局;在本例中,代码使用 Android 内置布局SimpleListItemChecked

用户界面 (UI)

Android 应用的用户界面层是代码和 XML 标记的组合。

  • 资源/布局 - 屏幕布局和作为 AXML 文件实现的行单元格设计。 可以手动编写 AXML,也可以使用适用于 Android 的 Xamarin UI 设计器直观地进行布局。
  • 资源/可绘制 - 图像(图标)和自定义按钮。
  • 屏幕–定义每个屏幕及其行为的活动子类。 将 UI 与应用程序层类和通用 API(TaskItemManager)关联在一起。

主屏幕

主屏幕由活动子类HomeScreen和定义布局的HomeScreen.axml文件组成,其中布局包括按钮和任务列表的位置。 屏幕如下所示:

The screen looks like this

主屏幕代码定义处理程序,该程序用于单击按钮和单击列表中的项,以及填充方法OnResume中的列表(以便其反映“任务详细信息”屏幕中所做的更改)。 数据使用业务层的TaskItemManager和应用程序层的TaskListAdapter加载。

任务详细信息屏幕

任务详细信息屏幕还包含Activity子类和 AXML 布局文件。 布局确定输入控件的位置,C# 类定义加载和保存TaskItem对象的行为。

The class defines the behavior to load and save TaskItem objects

对 PCL 库的所有引用都通过TaskItemManager类。

Windows Phone 应用

完整的 Windows Phone 项目:

Windows Phone App The complete Windows Phone project

下图显示了分组到层中的类:

This diagram presents the classes grouped into layers

参考

特定于平台的项目必须引用所需的特定于平台的库(例如Microsoft.PhoneSystem.Windows)才能创建有效的 Windows Phone 应用程序。

它还必须引用 PCL 项目(例如TaskyPortableLibrary),才能利用TaskItem类和数据库。

TaskyPortableLibrary to utilize the TaskItem class and database

应用程序层 (AL)

与 iOS 和 Android 版本一样,应用程序层包含非视觉元素,这些元素帮助将数据绑定到用户界面。

ViewModels

ViewModels 包装 PCL(TaskItemManager)中的数据,并按 Silverlight/XAML 数据绑定可以使用的方式呈现数据。 这是一个特定于平台的行为的示例(如跨平台应用程序文档中所述)。

用户界面 (UI)

XAML 具有唯一的数据绑定功能,可以在标记中声明,并减少显示对象所需的代码量:

  1. 页面–XAML 文件及其代码隐藏定义了用户界面,并引用 ViewModels 和 PCL 项目以显示和收集数据。
  2. 图像–初始屏幕、背景图像和图标图像是用户界面的关键部分。

MainPage

MainPage 类使用TaskListViewModel来显示使用 XAML 数据绑定功能的数据。 页面DataContext设置为以异步方式填充的视图模型。 XAML 中的{Binding}语法确定数据的显示方式。

TaskDetailsPage

每个任务通过将TaskViewModel绑定到 TaskDetailsPage.xaml 中定义的 XAML 来显示。 任务数据通过业务层的TaskItemManager检索。

结果

生成的应用程序在各个平台上如下所示:

iOS

应用程序使用 iOS 标准用户界面设计,例如位于导航栏中的“添加”按钮,并使用内置加号 (+) 图标。 该程序还使用默认的UINavigationController“后退”按钮行为,并支持表格中的“轻扫删除”。

It also uses the default UINavigationController back button behavior and supports swipe-to-delete in the tableIt also uses the default UINavigationController back button behavior and supports swipe-to-delete in the table

Android

Android 应用程序使用内置控件,包括需要显示的“对钩”的行的内置布局。 除了屏幕后退按钮外,还支持硬件/系统后退行为。

The hardware/system back behavior is supported in addition to an on-screen back buttonThe hardware/system back behavior is supported in addition to an on-screen back button

Windows Phone

Windows Phone 应用程序使用标准布局,填充在屏幕底部的应用栏,而不是顶部的导航栏。

The Windows Phone app uses the standard layout, populating the app bar at the bottom of the screen instead of a nav bar at the topThe Windows Phone app uses the standard layout, populating the app bar at the bottom of the screen instead of a nav bar at the top

总结

本文档详细说明了如何将分层应用程序设计原则用于简单应用程序,以便在三个移动平台(iOS、Android 和 Windows Phone)之间重复使用代码。

文档描述了设计应用程序层的流程,并讨论了在每个层中实现的代码和功能。

代码可以从 github 下载。