第 5 部分 - 实际代码共享策略

本部分举例说明了如何共享常见应用程序方案的代码。

数据层

数据层由存储引擎和信息读写方法组成。 为了提高性能、灵活性和跨平台兼容性,建议对 Xamarin 跨平台应用程序使用 SQLite 数据库引擎。 该引擎在多种平台上运行,包括 Windows、Android、iOS 和 Mac。

SQLite

SQLite 是一个开源数据库实现。 可在 SQLite.org 中找到源和文档。每个移动平台上都提供 SQLite 支持:

即使在所有平台上都提供数据库引擎,访问数据库的本机方法也不同。 iOS 和 Android 都提供内置 API 来访问可从 Xamarin.iOS 或 Xamarin.Android 使用的 SQLite,但使用本机 SDK 方法无法共享代码(除了 SQL 查询本身之外,假设它们存储为字符串)。 有关本机数据库功能的详细信息,请在 iOS 或 Android 的 SQLiteOpenHelper 类中搜索 CoreData;这些选项不是跨平台的,因此不在本文档的讲解范围内。

ADO.NET

Xamarin.iOS 和 Xamarin.Android 都支持 System.DataMono.Data.Sqlite(有关详细信息,请参阅 Xamarin.iOS 文档)。 使用这些命名空间,可以编写能在两个平台上工作的 ADO.NET 代码。 编辑项目的引用以包含 System.Data.dllMono.Data.Sqlite.dll,并将这些 using 语句添加到代码:

using System.Data;
using Mono.Data.Sqlite;

然后,下面的示例代码将工作:

string dbPath = Path.Combine (
        Environment.GetFolderPath (Environment.SpecialFolder.Personal),
        "items.db3");
bool exists = File.Exists (dbPath);
if (!exists)
    SqliteConnection.CreateFile (dbPath);
var connection = new SqliteConnection ("Data Source=" + dbPath);
connection.Open ();
if (!exists) {
    // This is the first time the app has run and/or that we need the DB.
    // Copy a "template" DB from your assets, or programmatically create one like this:
    var commands = new[]{
        "CREATE TABLE [Items] (Key ntext, Value ntext);",
        "INSERT INTO [Items] ([Key], [Value]) VALUES ('sample', 'text')"
    };
    foreach (var command in commands) {
        using (var c = connection.CreateCommand ()) {
            c.CommandText = command;
            c.ExecuteNonQuery ();
        }
    }
}
// use `connection`... here, we'll just append the contents to a TextView
using (var contents = connection.CreateCommand ()) {
    contents.CommandText = "SELECT [Key], [Value] from [Items]";
    var r = contents.ExecuteReader ();
    while (r.Read ())
        Console.Write("\n\tKey={0}; Value={1}",
                r ["Key"].ToString (),
                r ["Value"].ToString ());
}
connection.Close ();

ADO.NET 的实际实现显然会跨不同的方法和类拆分(此示例仅用于演示目的)。

SQLite-NET - 跨平台 ORM

对象关系映射器 (ORM) 尝试简化在类中建模的数据的存储。 不需要手动编写 SQL 查询来对从类字段和属性中手动提取的数据执行 CREATE TABLE 或 SELECT、INSERT 和 DELETE 操作,ORM 会添加一个代码层来执行此操作。 通过使用反射来检查类的结构,ORM 可以自动创建与类匹配的表和列,并生成用于读取和写入数据的查询。 这样,应用程序代码只需将对象实例发送和检索到 ORM,后者负责后台的所有 SQL 操作。

SQLite-NET 充当一个简单的 ORM,可让你在 SQLite 中保存和检索类。 它结合了编译器指令和其他技巧,使得跨平台 SQLite 访问不再复杂。

SQLite-NET 的功能:

  • 表通过向 Model 类添加属性来定义。
  • 数据库实例由 SQLiteConnection(SQLite-Net 库中的主类)的子类表示。
  • 可以使用对象插入、查询和删除数据。 不需要 SQL 语句(尽管可以根据需要编写 SQL 语句)。
  • 可以对 SQLite-NET 返回的集合执行基本 Linq 查询。

SQLite-NET 的源代码和文档在 github 上的 SQLite-Net 中提供,并且已在两个案例研究中实现。 下面显示了 SQLite-NET 代码的简单示例(来自 Tasky Pro 案例研究)。

首先,TodoItem 类使用属性定义字段作为数据库主键:

public class TodoItem : IBusinessEntity
{
    public TodoItem () {}
    [PrimaryKey, AutoIncrement]
    public int ID { get; set; }
    public string Name { get; set; }
    public string Notes { get; set; }
    public bool Done { get; set; }
}

这样,可以在 SQLiteConnection 实例上使用以下代码行(无 SQL 语句)创建 TodoItem 表:

CreateTable<TodoItem> ();

表中的数据也可使用 SQLiteConnection 上的其他方法来操作(同样不需要 SQL 语句):

Insert (TodoItem); // 'task' is an instance with data populated in its properties
Update (TodoItem); // Primary Key field must be populated for Update to work
Table<TodoItem>.ToList(); // returns all rows in a collection

有关完整示例,请参阅案例研究源代码。

文件访问

对于任何应用程序,文件访问肯定都是关键部分。 应用程序中可能包含的文件的常见示例包括:

  • SQLite 数据库文件。
  • 用户生成的数据(文本、图像、声音、视频)。
  • 用于缓存的已下载数据(图像、html 或 PDF 文件)。

System.IO 直接访问

Xamarin.iOS 和 Xamarin.Android 都支持使用 System.IO 命名空间中的类访问文件系统。

每个平台都有不同的访问限制,必须考虑到这些限制:

  • iOS 应用程序在文件系统访问非常受限的沙盒中运行。 Apple 通过指定备份的特定位置(以及其他未备份的位置)来进一步指示应如何使用文件系统。 有关更多详细信息,请参阅在 Xamarin.iOS 中使用文件系统指南。
  • Android 还仅限访问与应用程序相关的某些目录,但它也支持外部媒体(例如 SD 卡),也支持访问共享数据。
  • Windows Phone 8 (Silverlight) 不允许直接访问文件 - 只能使用 IsolatedStorage 操作文件。
  • Windows 8.1 WinRT 和 Windows 10 UWP 项目仅通过 Windows.Storage API 提供异步文件操作,这些 API 与其他平台不同。

iOS 和 Android 示例

下面显示了一个编写和读取文本文件的简单示例。 如果使用 Environment.GetFolderPath,则可以在 iOS 和 Android 上运行相同的代码,两者都基于其文件系统约定返回有效的目录。

string filePath = Path.Combine (
        Environment.GetFolderPath (Environment.SpecialFolder.Personal),
        "MyFile.txt");
System.IO.File.WriteAllText (filePath, "Contents of text file");
Console.WriteLine (System.IO.File.ReadAllText (filePath));

若要详细了解特定于 iOS 的文件系统功能,请参阅 Xamarin.iOS 使用文件系统文档。 编写跨平台文件访问代码时,请记住,某些文件系统区分大小写,并且使用不同的目录分隔符。 最好在构造文件或目录路径时始终对文件名和 Path.Combine() 方法使用相同的大小写。

适用于 Windows 8 和 Windows 10 的 Windows.Storage

《使用 Xamarin.Forms 创建移动应用》书籍的第 20 章:异步和文件 I/O 介绍了 Windows 8.1 和 Windows 10 的示例

如果使用 DependencyService,则可以受支持的 API 在这些平台上读取和归档文件:

StorageFolder localFolder = ApplicationData.Current.LocalFolder;
IStorageFile storageFile = await localFolder.CreateFileAsync("MyFile.txt",
                                        CreationCollisionOption.ReplaceExisting);
await FileIO.WriteTextAsync(storageFile, "Contents of text file");

有关更多详细信息,请参阅本书第 20 章

Windows Phone 7 和 8 (Silverlight) 上的独立存储

独立存储是一种常见的 API,用于跨所有 iOS、Android 和较旧的 Windows Phone 平台保存和加载文件。

它是在 Xamarin.iOS 和 Xamarin.Android 中实现的 Windows Phone (Silverlight) 中访问文件的默认机制,支持编写常见的文件访问代码。 可以跨所有三个平台在共享项目上引用 System.IO.IsolatedStorage 类。

有关详细信息,请参阅 Windows Phone 的独立存储概述

可移植类库中不可使用独立存储 API。 PCL 的一种替代方法是 PCLStorage NuGet

PCL 中的跨平台文件访问

还有一个与 PCL 兼容的 NuGet - PCLStorage,它支持 Xamarin 支持的平台和最新的 Windows API 的跨平台文件访问。

网络运营

大多数移动应用程序都具有网络组件,例如:

  • 下载图像、视频和音频(例如缩略图、照片、音乐)。
  • 下载文档(例如 HTML、PDF)。
  • 上传用户数据(例如照片或文本)。
  • 访问 Web 服务或第三方 API(包括 SOAP、XML 或 JSON)。

.NET Framework 提供了几个不同的类来访问网络资源:HttpClientWebClientHttpWebRequest

HttpClient

System.Net.Http 命名空间中的 HttpClient 类在 Xamarin.iOS、Xamarin.Android 和大多数 Windows 平台中可用。 有一个 Microsoft HTTP 客户端库 NuGet,它可用于将此 API 引入可移植类库(以及 Windows Phone 8 Silverlight)。

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://xamarin.com");
var response = await myClient.SendAsync(request);

WebClient

WebClient 类提供一个简单的 API,用于从远程服务器检索远程数据。

通用 Windows 平台操作必须是异步操作,即使 Xamarin.iOS 和 Xamarin.Android 支持同步操作(可在后台线程上完成)也是如此。

简单异步 WebClient 操作的代码如下:

var webClient = new WebClient ();
webClient.DownloadStringCompleted += (sender, e) =>
{
    var resultString = e.Result;
    // do something with downloaded string, do UI interaction on main thread
};
webClient.Encoding = System.Text.Encoding.UTF8;
webClient.DownloadStringAsync (new Uri ("http://some-server.com/file.xml"));

WebClient 还具有 DownloadFileCompletedDownloadFileAsync,它们用于检索二进制数据。

HttpWebRequest

HttpWebRequest 提供比 WebClient 更多的自定义,因此需要更多代码才能使用。

简单同步 HttpWebRequest 操作的代码如下:

var request = HttpWebRequest.Create(@"http://some-server.com/file.xml ");
request.ContentType = "text/xml";
request.Method = "GET";
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
    if (response.StatusCode != HttpStatusCode.OK)
        Console.WriteLine("Error fetching data. Server returned status code: {0}", response.StatusCode);
    using (StreamReader reader = new StreamReader(response.GetResponseStream()))
    {
        var content = reader.ReadToEnd();
        // do something with downloaded string, do UI interaction on main thread
    }
}

Web 服务文档中有一个示例。

可访问性

从快速 Wi-Fi 或 4G 连接到接收不良区域和慢速 EDGE 数据链路,移动设备在各种网络条件下运行。 因此,在尝试连接到远程服务器之前,最好检测网络是否可用,如果可用,则检测哪种类型网络可用。

移动应用在以下情况中可能执行的操作包括:

  • 如果网络不可用,提醒用户。 如果用户已手动禁用网络(例如飞行模式或关闭 Wi-Fi),则用户可自行解决问题。
  • 如果连接为 3G,应用程序的行为可能有所不同(例如,Apple 不允许通过 3G 下载超过 20 MB 的应用)。 应用程序可以使用此信息来提醒用户在检索大型文件时下载时间过长。
  • 即使网络可用,在发起其他请求之前,也最好先验证与目标服务器的连接。 这可以防止应用的网络操作反复超时,还允许向用户显示有更多信息的错误消息。

WebServices

请参阅有关使用 Web 服务的文档,其中介绍了如何使用 Xamarin.iOS 访问 REST、SOAP 和 WCF 终结点。 可以手动编写 Web 服务请求和分析响应,但有库可用于简化此操作,包括 Azure、RestSharp 和 ServiceStack。 甚至可以在 Xamarin 应用中访问基本的 WCF 操作。

Azure

Microsoft Azure 是一个云平台,为移动应用提供各种服务,包括数据存储和同步以及推送通知。

若要免费试用,请访问 azure.microsoft.com

RestSharp

RestSharp 是一个 .NET 库,可包含在移动应用程序中,以提供便于更轻松访问 Web 服务的 REST 客户端。 它通过提供简单的 API 来请求数据和分析 REST 响应,从而提供帮助。 RestSharp 可能很有用

RestSharp 网站包含有关如何使用 RestSharp 实现 REST 客户端的文档。 RestSharp 在 github 上提供 Xamarin.iOS 和 Xamarin.Android 示例。

Web 服务文档中还有 Xamarin.iOS 代码片段。

ServiceStack

与 RestSharp 不同,ServiceStack 既是托管 Web 服务的服务器端解决方案,也是可在移动应用程序中实现来访问这些服务的客户端库。

ServiceStack 网站介绍了项目的用途,还提供文档和代码示例的链接。 这些示例包括 Web 服务的完整服务器端实现,以及可访问它的各种客户端应用程序。

WCF

Xamarin 工具可帮助你使用某些 Windows Communication Foundation (WCF) 服务。 通常,Xamarin 支持 Silverlight 运行时附带的 WCF 的同一客户端子集。 这包括 WCF 的最常见编码和协议实现:使用 BasicHttpBinding 通过 HTTP 传输协议传递文本编码的 SOAP 消息。

由于 WCF 框架的大小和复杂性,当前和将来的服务实现可能会超出 Xamarin 客户端子集域支持的范围。 此外,WCF 支持要求使用仅在 Windows 环境中可用的工具来生成代理。

线程处理

应用程序响应能力对于移动应用程序非常重要 - 用户期望应用程序快速地加载和执行。 将显示停止接受用户输入的“冻结”屏幕,指示应用程序崩溃,因此不要将 UI 线程与长时间运行的阻塞调用(例如网络请求)或慢速本地操作(例如解压缩文件)相关联,这一点很重要。 特别是,启动过程不应包含长时间运行的任务 - 所有移动平台都将终止加载时间过长的应用。

这意味着用户界面应实现快速显示的“进度指示器”或其他“可用”UI,以及执行后台操作的异步任务。 执行后台任务需要使用线程,这意味着后台任务需要一种方法来传达回主线程,以指示进度或完成时间。

并行任务库

使用并行任务库创建的任务可以异步运行并在调用线程上返回,这使得它们对于触发长时间运行的操作非常有用,而不会阻止用户界面。

简单的并行任务操作可能如下所示:

using System.Threading.Tasks;
void MainThreadMethod ()
{
    Task.Factory.StartNew (() => wc.DownloadString ("http://...")).ContinueWith (
        t => label.Text = t.Result, TaskScheduler.FromCurrentSynchronizationContext()
    );
}

键是 TaskScheduler.FromCurrentSynchronizationContext(),它将重用调用方法的线程的 SynchronizationContext.Current(在这里,是指运行 MainThreadMethod 的主线程)来将调用封装回该线程。 这意味着,如果在 UI 线程上调用该方法,它将回到 UI 线程上运行 ContinueWith 操作。

如果代码从其他线程启动任务,请使用以下模式创建对 UI 线程的引用,并且该任务仍可回调该线程:

static Context uiContext = TaskScheduler.FromCurrentSynchronizationContext();

在 UI 线程上调用

对于不使用并行任务库的代码,每个平台都有自己的语法,用于将操作封送回 UI 线程:

  • iOS - owner.BeginInvokeOnMainThread(new NSAction(action))
  • Android - owner.RunOnUiThread(action)
  • Xamarin.Forms - Device.BeginInvokeOnMainThread(action)
  • WindowsDeployment.Current.Dispatcher.BeginInvoke(action)

iOS 和 Android 语法都需要有“context”类可用,这意味着代码需要将此对象传递到需要在 UI 线程回调的任何方法中。

若要在共享代码中调用 UI 线程,请遵循 IDispatchOnUIThread 示例(由 @follesoe 提供)。 声明并编程到共享代码中的 IDispatchOnUIThread 接口,然后实现特定于平台的类,如下所示:

// program to the interface in shared code
public interface IDispatchOnUIThread {
    void Invoke (Action action);
}
// iOS
public class DispatchAdapter : IDispatchOnUIThread {
    public readonly NSObject owner;
    public DispatchAdapter (NSObject owner) {
        this.owner = owner;
    }
    public void Invoke (Action action) {
        owner.BeginInvokeOnMainThread(new NSAction(action));
    }
}
// Android
public class DispatchAdapter : IDispatchOnUIThread {
    public readonly Activity owner;
    public DispatchAdapter (Activity owner) {
        this.owner = owner;
    }
    public void Invoke (Action action) {
        owner.RunOnUiThread(action);
    }
}
// WP7
public class DispatchAdapter : IDispatchOnUIThread {
    public void Invoke (Action action) {
        Deployment.Current.Dispatcher.BeginInvoke(action);
    }
}

Xamarin.Forms 开发人员应在常见代码(共享项目或 PCL)中使用 Device.BeginInvokeOnMainThread

平台和设备功能和降级

平台功能文档中提供了处理不同功能的其他具体示例。 它介绍了如何检测不同的功能,还介绍了如何正常降级应用程序来提供良好的用户体验(即使应用无法正常运行其全部潜力)。