Xamarin-4-x-跨平台应用开发-全-

Xamarin 4.x 跨平台应用开发(全)

原文:zh.annas-archive.org/md5/183290FB388A7F8EC527693139A6FD11

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Xamarin 为 C#开发 iOS 和 Android 应用程序打造了优秀的产品:Xamarin Studio,Visual Studio 的插件,Xamarin.iOS 和 Xamarin.Android。Xamarin 让你直接访问每个平台的本地 API,并具有共享 C#代码的灵活性。使用 Xamarin 和 C#,相比于 Java 或 Objective-C,你可以获得更高的生产效率,并且与 HTML 或 JavaScript 解决方案相比,仍然保持出色的性能。

在本书中,我们将开发一个现实世界的示例应用程序,以展示你可以使用 Xamarin 技术做什么,并在 iOS 和 Android 的核心平台概念上进行构建。我们还将涵盖高级主题,如推送通知、获取联系人、使用相机和 GPS 定位。随着 Xamarin 3 的推出,引入了一个名为 Xamarin.Forms 的新框架。我们将介绍 Xamarin.Forms 的基础知识以及如何将其应用于跨平台开发。最后,我们将介绍提交应用程序到 Apple App Store 和 Google Play 需要做些什么。

本书涵盖的内容

第一章,Xamarin 设置,是关于安装适合进行跨平台开发的 Xamarin 软件和本地 SDK 的指南。指导 Windows 用户如何在本地网络中连接 Mac,以便在 Visual Studio 中进行 iOS 开发。

第二章, 平台你好!,带你一步步在 iOS 和 Android 上创建一个简单的计算器应用程序,同时也涵盖了每个平台的一些基本概念。

第三章,iOS 和 Android 之间的代码共享,介绍了可以使用 Xamarin 的代码共享技术和项目设置策略。

第四章, XamSnap - 一个跨平台应用,介绍了一个示例应用程序,我们将在整本书中构建它。在本章中,我们将为该应用程序编写所有共享代码,并完成单元测试。

第五章, iOS 的 XamSnap,展示了如何为 XamSnap 实现 iOS 用户界面,并涵盖了各种 iOS 开发概念。

第六章, 安卓的 XamSnap,展示了如何实现 XamSnap 的 Android 版本,并介绍了 Android 特定的开发概念。

第七章, 在设备上部署和测试,带你经历将第一个应用程序部署到设备的痛苦过程。我们还讨论为什么在真实设备上测试应用程序很重要。

第八章,联系人、相机和位置,介绍了库 Xamarin.Mobile,作为跨平台方式访问用户的联系人、相机和 GPS 位置,并将这些功能添加到我们的 XamSnap 应用程序中。

第九章,带有推送通知的 Web 服务,展示了如何使用 Windows Azure 实现 XamSnap 的真实后端 Web 服务,利用 Azure Functions 和 Azure Notification Hubs。

第十章,第三方库,涵盖了使用 Xamarin 的各种第三方库选项,以及如何甚至利用原生 Java 和 Objective-C 库。

第十一章,Xamarin.Forms,帮助我们探索 Xamarin 的最新框架 Xamarin.Forms,以及如何利用它构建跨平台应用程序。

第十二章,应用商店提交,将引导我们完成将你的应用提交到苹果 App Store 和 Google Play 的过程。

你需要为这本书准备什么

对于这本书,你需要一台运行至少 OS X 10.10 的 Mac 电脑。苹果要求 iOS 应用程序必须在 Mac 上编译,因此 Xamarin 也有同样的要求。你可以使用 Xamarin Studio(最适合 Mac)或 Visual Studio(最适合 Windows)作为 IDE。在 Windows 上的开发人员可以通过连接到本地网络上的 Mac 来在 Visual Studio 上开发 iOS 应用程序。访问xamarin.com/downloadvisualstudio.com/download以下载合适的软件。

这本书适合谁

这本书适合已经熟悉 C#并希望学习使用 Xamarin 进行移动开发的开发人员。如果你在 ASP.NET、WPF、WinRT、Windows Phone 或 UWP 方面有过工作经验,那么使用这本书来开发原生 iOS 和 Android 应用程序将会非常得心应手。

约定

在这本书中,你会发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:"使用await关键字在 C#中运行异步代码。"

一段代码如下设置:

class ChuckNorris
{
    void DropKick()
    {
        Console.WriteLine("Dropkick!");
    }
}

当我们希望引起您对代码块中特定部分的注意时,相关的行或项目会以粗体显示:

class ChuckNorris
{
    void DropKick()
    {
        Console.WriteLine("Dropkick!");
    }
}

任何命令行输入或输出都如下编写:

# xbuild MyProject.csproj

新术语重要词汇以粗体显示。你在屏幕上看到的词,例如菜单或对话框中的,文本中会像这样显示:"为了下载新模块,我们将转到文件 | 设置 | 项目名称 | 项目解释器。"

注意

警告或重要提示会以这样的框显示。

提示

提示和技巧会像这样显示。

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。要给我们发送一般反馈,只需发送电子邮件到 feedback@packtpub.com,并在邮件的主题中提及书籍的标题。如果你对某个主题有专业知识,并且有兴趣撰写或为书籍做贡献,请查看我们的作者指南www.packtpub.com/authors

客户支持

既然你现在拥有一本 Packt 的书,我们有很多方法可以帮助你充分利用你的购买。

下载示例代码

你可以从你的账户在www.packtpub.com下载本书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给你。

你可以通过以下步骤下载代码文件:

  1. 使用你的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标悬停在顶部支持标签上。

  3. 点击代码下载 &勘误

  4. 搜索框中输入书籍名称。

  5. 选择你想要下载代码文件的那本书。

  6. 从下拉菜单中选择你购买本书的地方。

  7. 点击代码下载

文件下载后,请确保你使用最新版本的软件解压或提取文件夹:

  • 对于 Windows 系统,使用 WinRAR / 7-Zip。

  • 对于 Mac 系统,使用 Zipeg / iZip / UnRarX。

  • 对于 Linux 系统,使用 7-Zip / PeaZip。

本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Xamarin 4x-Cross-Platform-Application-Development-Third-Edition。我们还有其他丰富的书籍和视频代码包,可以在github.com/PacktPublishing/找到。请查看!

下载本书的色彩图片

我们还为你提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的色彩图片。色彩图片将帮助你更好地理解输出的变化。你可以从www.packtpub.com/sites/default/files/downloads/Xamarin4xCrossPlatformApplicationDevelopmentThirdEdition_ColorImages.pdf下载这个文件。

勘误

尽管我们已经竭尽全力确保内容的准确性,但错误仍然在所难免。如果你在我们的书中发现了一个错误——可能是文本或代码中的错误,如果你能向我们报告,我们将不胜感激。这样做可以避免其他读者产生困扰,并帮助我们改进本书后续版本。如果你发现任何勘误信息,请通过访问www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入你的勘误详情。一旦你的勘误信息被核实,你的提交将被接受,并且勘误信息将被上传到我们的网站,或者添加到该书标题下的现有勘误列表中。

要查看之前提交的勘误信息,请前往www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将显示在勘误部分。

盗版

在互联网上对版权材料的盗版是所有媒体持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可。如果你在互联网上以任何形式遇到我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

如果你怀疑有盗版材料,请通过 copyright@packtpub.com 联系我们,并提供相关链接。

我们感谢你帮助保护我们的作者和我们为你提供有价值内容的能力。

问题

如果你对本书的任何方面有问题,可以通过 questions@packtpub.com 联系我们,我们将尽力解决问题。

第一章:Xamarin 设置

如果你正在阅读这本书,你可能已经深深爱上了 C#、.NET 和像 Microsoft Visual Studio 这样的工具。当你考虑到学习新平台、新 IDE、新的应用程序模型,或许还有一两种编程语言的困难时,使用本地 SDK 的移动开发似乎令人畏惧。Xamarin 旨在用 C#为.NET 开发者提供开发本地 iOS、Android 和 Mac 应用程序的工具。

选择 Xamarin 而不是在 Android 上使用 Java,在 iOS 上使用 Objective-C/Swift 开发移动应用程序有很多优势。你可以在这两个平台之间共享代码,并且可以利用 C#和.NET 基类库的高级语言功能来提高生产效率。否则,你将不得不为 Android 和 iOS 分别编写整个应用程序。

与其他使用 JavaScript 和 HTML 开发跨平台应用程序的技术相比,Xamarin 具有一些独特的优势。C#通常比 JavaScript 性能更好,Xamarin 让开发者可以直接访问每个平台的本地 API。这使得 Xamarin 应用程序能够拥有类似于 Java 或 Objective-C 对应程序的本地外观和性能。Xamarin 的工具通过将 C#编译成本地 ARM 可执行文件,该文件可以作为 iOS 或 Android 应用程序进行打包。它将一个精简版的 Mono 运行时与你的应用程序捆绑在一起,只包括你的应用程序使用的基类库功能。

在本章中,我们将介绍使用 Xamarin 进行开发所需的一切。到本章结束时,我们将安装所有适当的 SDK 和工具,以及应用程序商店提交所需的所有开发者账户。

在本章中,我们将涵盖:

  • Xamarin 工具和技术介绍

  • 安装 Xcode,苹果的 IDE

  • 安装所有 Xamarin 工具和软件

  • 将 Visual Studio 连接到 Mac

  • 设置 Android 模拟器

  • 加入 iOS 开发者计划

  • 注册 Google Play

了解 Xamarin

Xamarin 开发了三个用于开发跨平台应用程序的核心产品:Xamarin StudioXamarin.iOSXamarin.Android。Xamarin Studio 是一个 C# IDE,而Xamarin.iOSXamarin.Android是使 C#应用程序能够在 iOS 和 Android 上运行的核心工具。这些工具允许开发者利用 iOS 和 Android 上的本地库,并建立在 Mono 运行时之上。

Mono,一个开源的 C#和.NET 框架实现,最初由 Novell 开发,用于 Linux 操作系统。由于 iOS 和 Android 同样基于 Linux,Novell 能够开发 MonoTouch 和 Mono for Android 作为针对新移动平台的产品。发布后不久,一家更大的公司收购了 Novell,Mono 团队离开成立了一家主要针对移动开发的新公司。Xamarin 因此成立,专注于使用 C#在 iOS 和 Android 上进行开发的这些工具。

为跨平台应用开发准备开发机器可能需要一些时间。更糟糕的是,苹果和谷歌各自对其平台上的开发都有不同的要求。如果你计划在 Windows 上使用 Visual Studio 进行开发,那么你的设置将与在 Mac OS X 上有所不同。请记住,在 Windows 上进行 iOS 开发需要在你的本地网络上有一台 Mac。让我们看看你的机器上需要安装哪些内容。

在 Mac OS X 上进行 Xamarin 开发的构建块如下:

  • Xcode:这是苹果用于用 Objective-C 开发 iOS 和 Mac 应用程序的核心 IDE。

  • Mac 上的 Mono 运行时:在 OS X 上编译和运行 C#程序需要这个

  • Java:这是在 OS X 上运行 Java 应用程序的核心运行时

  • Android SDK:这包含了谷歌的标准 SDK、设备驱动程序和用于原生 Android 开发的模拟器

  • Xamarin.iOS:这是 Xamarin 用于 iOS 开发的核心产品

  • Xamarin.Android:这是 Xamarin 用于 Android 开发的核心产品

在 Windows 上进行 Xamarin 开发所需的软件如下:

  • Visual Studio 或 Xamarin Studio:这两个 IDE 都可以用于 Windows 上的 Xamarin 开发。

  • .NET Framework 4.5 或更高版本:这随 Visual Studio 或 Windows 的最新版本一起提供。

  • Java:这是在 Windows 上运行 Java 应用程序的核心运行时。

  • Android SDK:这包含了谷歌的标准 SDK、设备驱动程序和用于原生 Android 开发的模拟器。

  • 本地网络上设置为 Xamarin.iOS 开发的 Mac:作为苹果许可协议的一部分,苹果要求在 OS X 上进行 iOS 开发。需要按照上述列表设置一台 Mac 用于 Xamarin.iOS 开发。

  • Xamarin for Windows:这是 Xamarin 用于 Windows 的核心产品,包括 Xamarin.Android 和 Xamarin.iOS。

每个安装都需要一些时间来下载和安装。如果你能访问快速的网络连接,这将有助于加快安装和设置过程。准备好一切后,让我们一步一步地继续前进,希望我们可以避开你可能遇到的几个死胡同。

安装 Xcode

为了让事情进行得更顺利,让我们首先为 Mac 安装 Xcode。除了 Apple 的 IDE,它还将安装 Mac 上最常用的开发工具。确保你至少有 OS X 10.10(Yosemite)版本,并在 App Store 中找到 Xcode,如下面的截图所示:

安装 Xcode

这将需要一些时间来下载和安装。我建议你可以利用这段时间享受一杯美味的咖啡,或者同时进行另一个项目。

安装 Xcode 会安装 iOS SDK,这是进行 iOS 开发的一般要求。由于 Apple 的限制,iOS SDK 只能在 Mac 上运行。Xamarin 已经尽一切可能确保他们遵循 Apple 的 iOS 指南,例如动态代码生成。Xamarin 的工具还尽可能利用 Xcode 的特性,以避免重新发明轮子。

在 Mac OS X 上安装 Xamarin

安装 Xcode 之后,需要安装其他几个依赖项,然后才能使用 Xamarin 的工具进行开发。幸运的是,Xamarin 通过创建一个简单的一体化安装程序改善了这一体验。

通过执行以下步骤来安装 Xamarin:

  1. 访问xamarin.com,点击大型的下载 Xamarin按钮。

  2. 填写一些关于你自己的基本信息,然后点击下载适用于 OS X 的 Xamarin Studio

  3. 下载XamarinInstaller.dmg并挂载磁盘映像。

  4. 启动Xamarin.app,并接受出现的任何 OS X 安全警告。

  5. 按照安装程序进行操作;默认选项将正常工作。你可以选择安装Xamarin.Mac,但本书不涉及该主题。

Xamarin 安装程序将下载并安装所需的前提条件,如 Mono 运行时、Java、Android SDK(包括 Android 模拟器和工具)以及你开始运行所需的一切。

最后你会得到类似于以下截图所示的内容,然后我们可以继续学习跨平台开发中的更多高级主题:

在 Mac OS X 上安装 Xamarin

设置 Android 模拟器

历史上,Android 模拟器在性能上一直比物理设备开发要慢。为了解决这个问题,Google 生产了一个支持在桌面电脑上进行硬件加速的 x86 模拟器。它默认没有在Android Virtual DeviceAVD)管理器中安装,所以让我们来设置它。

通过执行以下步骤可以安装 x86 Android 模拟器:

  1. 打开 Xamarin Studio。

  2. 启动工具 | 打开 Android SDK 管理器...

  3. 滚动到Extras;安装Intel x86 Emulator Accelerator (HAXM 安装程序)

  4. 滚动到Android 6.0 (API 23);安装Intel x86 Atom System Image

  5. 可选步骤,安装你感兴趣的其他软件包。至少确保你已经安装了 Android SDK 管理器默认为你选择安装的所有内容。

  6. 关闭Android SDK Manager并导航到你的 Android SDK 目录,默认位于~/Library/Developer/Xamarin/android-sdk-macosx

  7. 导航到extras/intel/Hardware_Accelerated_Execution_Manager并启动IntelHAXM_6.0.3.dmg来安装 HAXM 驱动。

  8. 切换回 Xamarin Studio 并启动工具 | 打开 Google Emulator Manager...

  9. 点击创建...

  10. 输入你选择的 AVD 名称,例如x86 Emulator

  11. 选择一个适合你显示器的通用设备,例如Nexus 5

  12. CPU/ABI中,确保你选择支持Intel Atom (x86)的选项。

  13. 创建设备后,继续点击启动...以确保模拟器正常运行。

提示

这些说明在 Windows 上应该非常相似。默认情况下,Android SDK 在 Windows 上的安装路径为C:\Program Files (x86)\Android\android-sdk。同样,HAXM 安装程序在 Windows 上名为intelhaxm-android.exe

模拟器启动需要一些时间,因此在处理 Android 项目时,让模拟器保持运行是一个好主意。Xamarin 在这里使用标准的 Android 工具,因此即使是 Java 开发者也会感受到缓慢模拟器的痛苦。如果一切正常启动,你会看到一个 Android 启动屏幕,然后是一个虚拟的 Android 设备,可以从 Xamarin Studio 部署应用程序,如下面的截图所示:

设置 Android 模拟器

市面上有许多 Android 模拟器选项,例如 Genymotion 或 Visual Studio Android Emulator。使用 Xamarin 不会限制你在 Android 模拟器中的选择,所以如果默认的 Android 模拟器不适用于你,可以自由尝试。

在 Windows 上安装 Xamarin

自从 2016 年微软收购 Xamarin 以来,任何版本的 Visual Studio 都包含了 Xamarin。版本如下:

  • Visual Studio Community:这是一个任何人都可以免费使用的版本。对于公司使用这个版本有一些限制。

  • Visual Studio Professional:这是公司应该使用的通用版本。在 Visual Studio 方面,它包括了 Team Foundation Server 的功能。

  • Visual Studio Enterprise:包含了 Visual Studio 和 Xamarin 的额外功能。Xamarin 的特性包括嵌入式程序集、实时 Xamarin 检查器和 Xamarin 分析器。

当首次在 Windows PC 上为 Xamarin 开发设置环境时,有两个选择需要考虑。如果你已经安装了 Visual Studio,那么你可以仅使用 Xamarin 安装程序,将必要的 Visual Studio 扩展和项目模板添加到现有安装中。如果你还没有安装 Visual Studio,那么在 Visual Studio 2015 安装程序中有一个简单的选项可以安装 Xamarin。

如果你想要通过 Visual Studio 安装程序进行安装:

  1. www.visualstudio.com/downloads/下载你所需的 Visual Studio 版本。

  2. 运行 Visual Studio 安装程序。

  3. 跨平台移动开发下,确保选择C#/.NET (Xamarin v4.1.0)(版本号将根据你使用的版本而变化)。这将自动选择你需要用于 Xamarin 开发的 Android SDK 和其他组件。

  4. 你还可以选择安装其他有用的工具,比如针对 Windows 10 的Microsoft Web 开发工具通用 Windows 应用开发工具。

在你点击下一步之前,你的安装程序应该看起来像这样:

在 Windows 上安装 Xamarin

安装 Xamarin 的第二种选择是从 Xamarin 官网进行:

  1. xamarin.com/download 下载适用于 Windows 的 Xamarin 安装程序。

  2. 运行 XamarinInstaller.exe,它将在你的电脑上下载并安装所有必需的组件。

Xamarin 安装程序与你在 Mac OS X 上看到的过程非常相似,应该非常简单直接。如果需要,它会将 Xamarin 添加到现有的 Visual Studio 安装中,并安装 Xamarin Studio。

为 iOS 开发将 Visual Studio 连接到 Mac

iOS 开发需要运行在 Mac OS X 上的 Xcode。幸运的是,Xamarin 已经使从 Windows 电脑进行远程开发成为可能。

要将你的电脑连接到 Mac:

  1. 首先打开或创建一个 Xamarin.iOS 项目。

  2. Visual Studio 会自动提示Xamarin Mac 代理说明

  3. 按照 Visual Studio 中的详细说明和截图,在 Mac 上启用远程登录。

  4. 应该会出现一个列出你 Mac 地址的Xamarin Mac 代理对话框。

  5. 点击连接...,并输入你在 Mac 上的用户名和密码。

连接后,你应该会看到如下截图所示的内容:

为 iOS 开发将 Visual Studio 连接到 Mac

连接后,你可以直接按下播放按钮,针对 iOS 模拟器或你选择的 iOS 设备调试你的项目。在 Visual Studio 中你期望的所有功能也可以用于 iOS 开发:断点、鼠标悬停评估、添加监视等。

加入 iOS 开发者计划

要部署到 iOS 设备,Apple 要求加入其 iOS 开发者计划。会员费用为每年 99 美元,你可以使用它来部署 200 个用于开发目的的设备。你还可以访问测试服务器,以实施更高级的 iOS 功能,如应用内购买、推送通知和 iOS 游戏中心。在物理设备上测试你的 Xamarin.iOS 应用是很重要的,因此我建议你在开始 iOS 开发之前先获得一个账户。在桌面运行的模拟器与实际移动设备上的性能差异很大。还有一些仅在实际设备上运行时才会发生的特定于 Xamarin 的优化。我们将在后面的章节中详细介绍在设备上测试应用的原因。

提示

自从 iOS 9 以来,苹果创建了一种可以从任何 Apple ID 在 iOS 设备上侧载应用程序的方法。建议仅用于在少量设备上进行测试,并且无法测试高级功能,如应用内购买或推送通知。然而,如果你只是想试试 iOS,这是一种无需支付 99 美元开发者费用的入门好方法。

通过以下步骤可以注册 iOS 开发者计划:

  1. 前往developer.apple.com/programs/ios

  2. 点击注册

  3. 使用现有的 iTunes 账户登录或创建一个新的账户。以后无法更改,所以请选择适合你公司的账户。

  4. 可以选择以个人或公司身份注册。两者的价格都是 99 美元,但作为公司注册需要将文件传真给苹果公司,并需要你公司会计师的协助。

  5. 审阅开发者协议。

  6. 填写苹果的开发者调查问卷。

  7. 购买 99 美元的开发者注册。

  8. 等待确认电子邮件。

你应该在两个工作日内收到一封看起来类似于以下截图的电子邮件:

注册 iOS 开发者计划

从这里,你可以继续设置你的账户:

  1. 从你收到的电子邮件中点击立即登录,或者前往itunesconnect.apple.com

  2. 使用你的 iTunes 账户登录。

  3. 同意在仪表盘主页上出现的任何附加协议。

  4. 从 iTunes Connect 仪表盘前往协议、税务和银行信息

  5. 在这一部分,你将看到三列,分别是联系方式银行信息税务信息

  6. 在这些部分中为你的账户填写适当的信息。对于公司账户,很可能会需要会计师的协助。

当一切完成后,你的协议、税务和银行信息部分应该看起来类似于以下截图:

注册 iOS 开发者计划

成功注册 iOS 开发者账户后,你现在可以部署到 iOS 设备并将你的应用发布到苹果 App Store。

注册成为 Google Play 开发者。

与 iOS 不同,将你的应用程序部署到 Android 设备是免费的,只需要在设备设置中进行一些更改。Google Play 开发者账户只需一次性支付 25 美元,并且不需要每年续费。但是,与 iOS 一样,如果你打算将应用提交到 Google Play 或需要实现这些功能之一,你需要一个 Google Play 账户。

要注册成为 Google Play 的开发者,请执行以下步骤:

  1. 前往play.google.com/apps/publish

  2. 使用现有 Google 账户登录,或者创建一个新的账户。这之后无法更改,所以如果需要,请选择适合你公司的账户。

  3. 同意协议并输入你的信用卡信息。

  4. 选择一个开发者名称并输入账户的其他重要信息。同样,选择适合你公司的名称,以便用户在应用商店中看到。

如果一切填写正确,你将得到如下 Google Play 开发者控制台:

注册成为 Google Play 开发者

如果你打算销售付费应用或应用内购买,在这一点上,我建议你设置你的Google 商家账户。这将使 Google 能够根据你所在国家的适当税法支付你的应用销售收益。如果这是为你的公司设置的,我建议寻求公司会计师或簿记员的帮助。

以下是设置 Google 商家账户的步骤:

  1. 点击设置商家账户按钮。

  2. 第二次使用你的 Google 账户登录。

  3. 填写销售应用所需的信息:地址、电话号码、税务信息以及显示在客户信用卡账单上的名称。

完成后,你会注意到开发者控制台中关于设置商家账户的帮助提示现在不见了,如下截图所示:

注册成为 Google Play 开发者

在这一点上,你可能会认为我们的账户已经完全设置好了,但在能够销售应用之前,还有一个关键步骤:我们必须输入银行信息。

使用以下步骤可以为你的 Google 商家账户设置银行:

  1. 返回到play.google.com/apps/publish的 Google Play 开发者控制台

  2. 点击财务报告部分。

  3. 点击标题为访问你的商家账户以获取详细信息的小链接。

  4. 你应该会看到一个警告,提示你没有设置银行账户。点击指定银行账户链接开始操作。

  5. 输入你的银行信息。同样,可能需要公司的会计师。

  6. 几天后,在你的账户中寻找来自 Google 的小额存款。

  7. 通过访问checkout.google.com/sell确认金额。

  8. 点击设置标签,然后是财务

  9. 接下来,点击验证账户

  10. 输入你银行账户中出现的金额,并点击验证存款

你的 Google 商家账户也是你可以取消或退款客户订单的地方。Google Play 与 iOS App Store 的不同之处在于,所有客户问题都直接指向开发者。

摘要

在本章中,我们讨论了 Xamarin 的核心产品,无论你是在使用 Mac OS X 还是 Windows PC,都可以用 C#开发 Android 和 iOS 应用程序。我们安装了 Xcode,然后运行了 Xamarin 一站式安装程序,它安装了 Java、Android SDK、Xamarin Studio、Xamarin.iOS 和 Xamarin.Android。在 Windows 上,我们在 Visual Studio 内设置了 Xamarin,并在本地网络上连接了一台 Mac 用于 iOS 开发。我们为调试应用程序时获得更快、更流畅的体验而设置了 x86 Android 模拟器。最后,我们设置了 iOS 和 Google Play 开发者账户,以便分发我们的应用程序。

在本章中,你应该已经获得了使用 Xamarin 构建跨平台应用程序所需的一切。你的开发计算机应该已经准备就绪,你应该已经安装了所有本地 SDK,准备开发下一个风靡全球的应用程序。

本章中的概念将为我们奠定更高级主题的基础,这需要安装适当的软件以及拥有苹果和谷歌的开发者账户。我们将把应用程序部署到真实设备上,并实现更高级的功能,如推送通知。在下一章中,我们将创建我们的第一个 iOS 和 Android 应用程序,并介绍每个平台的基础知识。

第二章:你好,平台!

如果你熟悉在 Windows 上使用 Visual Studio 开发应用程序,那么使用 Xamarin Studio 应该非常直接。Xamarin 使用相同的概念,即一个解决方案包含一个或多个项目,并且它为 iOS 和 Android 应用程序创建了几种新的项目类型。还有几个项目模板可以让你快速启动常见应用程序的开发。

Xamarin Studio 支持多种开箱即用的项目类型,包括标准的.NET 类库和控制台应用程序。你无法在 Mac 上的 Xamarin Studio 中本地开发 Windows 应用程序,但你可以肯定的是,可以在 Xamarin Studio 中开发应用程序的共享代码部分。我们将在后面的章节中关注共享代码,但请记住,Xamarin 使你能够在支持 C#的大部分平台之间共享一个通用的 C#后端。

在本章中,我们将涵盖:

  • 为 iOS 创建一个简单的计算器应用程序

  • 苹果的 MVC 模式

  • Xcode 和故事板

  • 为安卓创建计算器应用程序

  • 安卓活动

  • Xamarin 的安卓设计师

建立你的第一个 iOS 应用程序

启动 Xamarin Studio 并开始一个新的解决方案。与 Visual Studio 一样,新建解决方案对话框中有许多可以创建的项目类型。Xamarin Studio(前称MonoDevelop)支持开发许多不同类型的项目,如针对 Mono 运行时或.NET Core 的 C#应用程序、NUnit 测试项目,甚至除了 C#之外的其他语言,如 VB 或 C++。

Xamarin Studio 支持以下 iOS 项目类型:

  • 单视图应用: 这是一个基本的项目类型,它设置了一个 iOS 故事板以及一个单一视图和控制器。

  • 主从应用: 一种项目类型,其中包含你可以点击查看详细信息的项目列表。在 iPhone/iPod 上,它将使用多个控件占据整个屏幕区域,而在 iPad 上使用 iOS 的UISplitViewController

  • 标签应用: 这种项目类型会自动为具有标签布局的应用程序设置UITabViewController

  • 基于页面的应用: 这种项目类型会自动设置UIPageViewController,以便在屏幕间以轮播的方式分页。

  • WebView 应用: 这种项目类型用于创建“混合”应用程序,部分是 HTML,部分是原生应用。该应用程序设置为利用 Xamarin Studio 的 Razor 模板功能。

  • 类库: 这是一个在其他 iOS 应用程序项目中使用的类库。

  • 绑定库: 这是一个 iOS 项目,可以为 Objective-C 库创建 C#绑定。

  • UI 测试应用: 用于运行 UI 测试的 NUnit 测试项目,可以在本地或 Xamarin Test Cloud 上运行。

  • 单元测试应用: 这是一个特殊的 iOS 应用程序项目,可以运行 NUnit 测试。

要开始,请创建一个新解决方案,并导航到iOS | App,然后创建一个如以下截图所示的单视图应用

构建你的第一个 iOS 应用程序

提示

在 Visual Studio 中,你可以在新解决方案对话框中从Visual C# | iOS | Universal | 单视图应用创建正确类型的项目。

在下一步中,我们将需要:

  1. 选择一个应用名称

  2. 选择一个组织标识符,这是一个“反向”域名,用来唯一标识你的应用。

  3. 选择你想要支持的 iOS 设备;你可以保留默认设置。

  4. 选择你想要支持的最低 iOS 版本;你可以保留默认设置。

  5. 最后一步,选择一个目录来放置你的项目,然后点击创建

提示

在 Visual Studio 中,你可以通过打开 iOS 项目的项目选项来访问这些设置。Xamarin Studio 在其新项目对话框中有额外的步骤,但事后你总是可以编辑这些设置。

你会注意到,项目模板会自动创建几个文件和文件夹。这些文件如下:

  • References:这是你熟知的.NET 其他库的标准引用。

  • Components:这个文件夹将包含从 Xamarin 组件商店添加的任何组件。有关 Xamarin 组件商店的更多信息,请参见第九章,带推送通知的 Web 服务

  • Resources:这个目录将包含任何你想要直接复制到应用程序包中的图片或普通文件。

  • AppDelegate.cs:这是苹果用于处理应用中应用程序级别事件的主类。

  • Entitlements.plist:这是一个设置文件,苹果用它来声明某些 iOS 功能(如推送通知和 iCloud)的权限。通常你只有在使用高级 iOS 功能时才需要使用它。

  • *ViewController.cs:这是表示应用中第一个屏幕的控制器。它将与你的项目同名。

  • Info.plist:这是苹果版本的一个清单文件,可以声明应用程序的各种设置,如应用标题、图标、启动画面和其他常见设置。

  • LaunchScreen.storyboard:这是一个用于布局应用程序启动画面的 Storyboard 文件。默认情况下,Xamarin 的项目模板在这里放置你的项目名称。

  • Main.cs:这个文件包含了 C#程序的标准入口点:static void Main()。你很可能不需要修改这个文件。

  • MainStoryboard.storyboard:这是你的应用程序的 Storyboard 定义文件。它将包含你的应用中的视图布局、控制器列表以及应用内导航的过渡效果。Storyboard 正如其名:是你 iOS 应用程序中不同屏幕的图解/流程图。

现在,让我们运行应用程序,看看从项目模板中默认得到什么。点击 Xamarin Studio 左上角的大播放按钮。你将看到模拟器正在运行你的第一个 iOS 应用程序,如下截图所示:

构建你的第一个 iOS 应用程序

到目前为止,你的应用只是一个纯白色的屏幕,这并不令人兴奋或有用。在继续前进之前,让我们对 iOS 开发有更多的了解。

根据你的应用程序支持的最低 iOS 版本,你也可以在不同的 iOS 模拟器版本上运行应用程序。苹果还提供了针对 iPad 以及市场上所有不同 iOS 设备的模拟器。重要的是要知道这些是模拟器而非仿真器。仿真器将运行封装版的移动操作系统(就像 Android 那样)。仿真器通常性能较慢,但能更接近真实操作系统的复制。苹果的模拟器作为本地 Mac 应用程序运行,并不是真正的操作系统。其好处是相较于 Android 仿真器,它们运行得非常快。

理解苹果的 MVC 模式

在深入 iOS 开发之前,了解苹果公司在 iOS 开发中的设计模式是非常重要的。你可能在其他技术(如ASP.NET)中使用过模型视图控制器(MVC)模式,但苹果公司对此范式的实现略有不同。苹果为开发 iOS 应用程序的用户界面提供了一套核心 API,称为 UIKit。Xamarin 应用程序可以通过直接使用 C#中的这些 API 来充分利用 UIKit。UIKit 主要基于 MVC 设计模式。

MVC设计模式包括以下内容:

  • 模型:这是驱动应用程序的后端业务逻辑。这可以是任何代码,例如,向服务器发起网络请求或保存数据到本地SQLite数据库。

  • 视图:这是屏幕上实际的用户界面。在 iOS 的术语中,这是从UIView派生的任何类。例如工具栏、按钮,以及用户在屏幕上看到和与之交互的任何其他内容。

  • 控制器:这是MVC模式中的工作马。控制器与模型层交互,并将结果更新到视图层。与视图层类似,任何控制器类都将从UIViewController派生。这是 iOS 应用程序中大部分代码所在的地方。

下图展示了 MVC 设计模式:

理解苹果的 MVC 模式

为了更好地理解这个模式,让我们通过以下常见场景的示例来一步步了解:

  1. 我们有一个 iOS 应用程序,其中包含一个搜索框,需要查询网站上的职位列表。

  2. 用户将在UITextField文本框中输入一些文本,并点击UIButton按钮开始搜索。这是视图层。

  3. 某些代码将响应按钮与视图交互,显示一个UIActivityIndicatorView加载指示器,并调用另一个类中的方法来执行搜索。这是控制器层。

  4. 被调用的类中将发起一个网络请求,并异步返回一个职位列表。这是模型层。

  5. 控制器随后将使用职位列表更新视图,并隐藏加载指示器。

注意

有关 Apple 的 MVC 模式的更多信息,请访问developer.apple.com/library/mac/documentation/general/conceptual/devpedia-cocoacore/MVC.html的文档网站。

需要注意的是,你可以自由地应用中模型层做任何想做的事情。这里我们可以使用普通的 C#类,这些类可以在其他平台如 Android 上复用。这包括使用 C#的基类库BCL)的任何功能,比如与网络服务或数据库交互。我们将在书中深入探讨跨平台架构和代码共享概念。

使用 iOS 设计师

由于我们纯白色的应用程序相当乏味,让我们通过一些控件来修改应用程序的视图层。为此,我们将在 Xamarin Studio 或 Visual Studio 中修改项目中的MainStoryboard.storyboard文件。可选地,你也可以在 Xcode 中打开故事板文件,这在 Xamarin.iOS 设计师之前是编辑故事板文件的方法。如果 Xamarin 设计师中不存在 iOS 故事板的功能,或者你需要编辑较旧的 iOS 格式如 XIB 文件,使用 Xcode 仍然有用。但是,Xcode 的体验并不好,因为 Xcode 中的自定义控件呈现为普通的白色方块。Xamarin 的设计师实际上运行你的自定义控件中的绘图代码,因此你可以准确地看到应用程序在运行时的样子。

让我们通过执行以下步骤向我们的应用程序添加一些控件:

  1. 在 Xamarin Studio 中打开本章早前创建的项目。

  2. 双击MainStoryboard.storyboard文件。

  3. iOS 设计师界面将会打开,你可以看到应用程序中单一控制器的布局。

  4. 在右侧的文档大纲标签页中,你会看到你的控制器在其布局层次结构中包含了一个单一视图。

  5. 在左上角,你会注意到一个工具箱,其中包含多种类型的对象,你可以将它们拖放到控制器的视图中。

  6. 在搜索框中搜索UILabel,并将标签拖动到屏幕顶部居中位置。

  7. 双击标签以将标签文本编辑为零(0)。你也可以从右下角的属性标签页中填写这个值。

  8. 同样,搜索 UIButton 并创建 10 个编号为0-9的按钮,以形成一个数字键盘。你可以通过使用属性标签来编辑按钮上的文本。你也可以使用复制/粘贴来加速创建过程。双击按钮会添加一个点击事件处理程序,这对于在其他平台上使用 Visual Studio 进行开发的人来说可能很熟悉。

  9. 运行应用程序。

你的应用程序应该看起来更像一个真正的应用程序(计算器),如下面的截图所示:

使用 iOS 设计器

提示

在 Windows 上的 Visual Studio 中,这些步骤与 Mac 上的 Xamarin Studio 相同。请记住,要使用 Xamarin.iOS 设计器,你必须保持与本地网络上的 Mac 连接。有关连接到 Mac 的说明,请参见第一章,Xamarin 设置

此时你可能会想知道如何为应用添加用户交互选项。在 Xcode 的 iOS 设计器中,你会创建一个出口,使每个视图在 C#中可见。出口是引用故事板或 XIB 文件中的视图的引用,在运行时将用视图的实例填充。你可以将这个概念与其他技术中为控件命名的概念进行比较,例如ASP.NETWebFormsWPFWindows Presentation Foundation)。幸运的是,Xamarin 的 iOS 设计器比在 Xcode 中设置出口要简单一些。你只需在属性标签的名称字段中填写,Xamarin Studio 就会在部分类中生成一个属性,使你能够从控制器访问标签和按钮。此外,你还可以从故事板文件中连接一个动作,这是一个在事件发生时将被调用的方法。Xamarin Studio 将 iOS 动作作为部分方法公开,以便在你的类中实现。

让我们按照以下方式为应用添加一些交互:

  1. 切换回 Xamarin Studio。

  2. 再次双击 MainStoryboard.storyboard 文件。

  3. 选择你之前创建的标签,并导航到属性窗格,确保你已选择小部件标签页。

  4. 名称字段中输入 label

  5. 创建一个带有文本+的按钮用于加法。

  6. 切换到事件标签页。

  7. Up Inside字段中输入名称 OnAdd。你可以将此视为按钮的“点击”事件。

  8. Xamarin Studio 将指导你将 OnAdd 方法放置在 UIViewController 中的位置。

  9. 对每个数字按钮重复此过程,但将Up Inside事件命名为 OnNumber

  10. 为计算器创建一个带有文本=的新按钮。

  11. 切换到事件标签页。

  12. Up Inside字段中输入名称 OnEquals

Xamarin 在这方面已经大大改善了从 Xcode 中的体验。对于更熟悉 Visual Studio 等工具的人来说,Xcode 有一个奇怪的界面。创建出口的方法涉及到点击并从控件拖动到 Objective-C 头文件。仅仅填写一个名称字段对于有 C#背景的开发者来说要简单得多,也更直观。

既然我们已经定义了两个出口,你的控制器将可以使用两个新的属性。在你的解决方案中展开*ViewController.cs文件并打开*ViewController.designer.cs文件。你会看到你的属性定义如下:

[Outlet] 
[GeneratedCode ("iOS Designer", "1.0")] 
MonoTouch.UIKit.UILabel label { get; set; } 

修改这个文件不是一个好主意,因为如果你在设计师或 Xcode 中做出进一步更改,IDE 会重新构建它。尽管如此,了解幕后实际工作原理是一个好习惯。

打开你的*ViewController.cs文件,让我们在你的控制器方法中输入以下代码:

partial void OnAdd(UIButton sender) 
{ 
    if (!string.IsNullOrEmpty(label.Text)) 
    { 
        label.Text += "+"; 
    } 
} 

partial void OnNumber(UIButton sender) 
{ 
    if (string.IsNullOrEmpty(label.Text) || label.Text == "0") 
    { 
        label.Text = sender.CurrentTitle; 
    } 
    else 
    { 
        label.Text += sender.CurrentTitle; 
    } 
} 

partial void OnEquals(UIButton sender) 
{ 
    //Simple logic for adding up the numbers 
    string[] split = label.Text.Split('+'); 
    int sum = 0; 
    foreach (string text in split) 
    { 
        int x; 
        if (int.TryParse(text, out x)) 
            sum += x; 
    } 
    label.Text = sum.ToString(); 
} 

这段代码的大部分只是用于实现计算器操作的通用 C#逻辑。在OnAdd方法中,如果标签文本非空,我们会添加一个+符号。在OnNumber方法中,我们适当地替换或追加标签文本。最后,在OnEquals方法中,我们使用字符串分割操作和整数转换计算标签中的表达式。然后,我们将结果放入标签文本中。

运行你的应用,你将能够与计算器进行交互,如下面的截图所示:

使用 iOS 设计师

现在是一个自己完成这个练习并完成计算器的好时机。添加减法、乘法、除法按钮以及一个"清除"按钮,这将完成简单计算器。这应该能让你掌握使用 Apple 的UIButtonUILabel API 以及 UIKit 框架的基础知识。

既然我们已经介绍了在 Xamarin 的 iOS 设计师中布局控件以及在 C#中与出口交互的基础知识,那么让我们来了解一下 iOS 应用程序的标准生命周期。处理应用程序级事件的主要位置是在AppDelegate类中。

如果你打开你的AppDelegate.cs文件,你可以重写以下方法:

  • FinishedLaunching:这是应用程序的第一个入口点,应该返回true

  • DidEnterBackground:这意味着用户点击了设备上的主页按钮,或者有其他应用,如电话,切换到前台。你应该执行任何需要保存用户进度或 UI 状态的操作,因为 iOS 可能会在应用退到后台时杀死你的应用。当你的应用在后台时,用户可能正在浏览主屏幕或打开其他应用。你的应用实际上是在内存中被暂停,直到被用户恢复。

  • WillEnterForeground:这意味着用户已经从后台重新打开了你的应用程序。你可能需要在这里执行其他操作,比如刷新屏幕上的数据等。

  • OnResignActivation:当操作系统在应用程序顶部显示系统弹窗时会发生这种情况。例如日历提醒或用户从屏幕顶部向下滑动的菜单。

  • OnActivated:这发生在OnResignActivation方法执行后,用户返回到你的应用时立即发生。

  • ReceiveMemoryWarning:这是操作系统发出的警告,要求释放应用程序中的内存。由于 C#的垃圾收集器,这在 Xamarin 中通常不需要,但如果应用程序中有任何重对象,如图片等,这是一个处理它们的好地方。如果无法释放足够的内存,操作系统可能会终止你的应用程序。

  • HandleOpenUrl:如果你实现了URL 方案,这是会调用的,它是 iOS 平台上相当于桌面平台的文件扩展名关联。如果你注册了你的应用程序以打开不同类型的文件或 URL,这个方法将被调用。

同样,在你的*ViewController.cs文件中,你可以在控制器上覆盖以下方法:

  • ViewDidLoad:当与你的控制器关联的视图加载时,会发生这种情况。在运行 iOS 6 或更高版本的设备上,它只发生一次。

  • ViewWillAppear:这发生在你的视图在屏幕上出现之前。如果应用程序导航过程中有任何视图需要刷新,这通常是最好的地方。

  • ViewDidAppear:这发生在任何过渡动画完成后,你的视图在屏幕上显示之后。在某些不常见的情况下,你可能需要在这里而不是在ViewWillAppear中执行操作。

  • ViewWillDisappear:在您的视图被隐藏之前会调用此方法。你可能需要在这里执行一些清理操作。

  • ViewDidDisappear:这发生在完成显示屏幕上不同控制器的过渡动画之后。与出现的 方法一样,这发生在ViewWillDisappear之后。

还有更多可以覆盖的方法,但许多方法在新版本的 iOS 中已被弃用。熟悉苹果的文档网站 developer.apple.com/library/ios。在尝试理解苹果 API 的工作原理时,阅读每个类和方法的文档非常有帮助。学习如何阅读(不一定是编写)Objective-C 也是一个有用的技能,这样你在开发 iOS 应用程序时能够将 Objective-C 示例转换为 C#。

构建你的第一个 Android 应用程序

在 Xamarin Studio 中设置 Android 应用程序与在 iOS 上一样简单,并且与 Visual Studio 中的体验非常相似。Xamarin Studio 包含了几个特定的 Android 项目模板,以便快速开始开发。

Xamarin Studio 包含以下项目模板:

  • Android 应用:一个标准的 Android 应用程序,目标是安装在机器上的最新 Android SDK。

  • Wear 应用:一个针对 Android Wear,适用于智能手表设备的项目。

  • WebView 应用:一个使用 HTML 实现部分功能的混合应用的工程模板。支持 Razor 模板。

  • 类库:只能被 Android 应用程序项目引用的类库。

  • 绑定库:一个用于设置可以从 C# 调用的 Java 库的项目。

  • UI 测试应用:一个 NUnit 测试项目,用于在本地或 Xamarin Test Cloud 上运行 UI 测试。

  • 单元测试应用:这是一个特殊的 Android 应用程序项目,可以运行 NUnit 测试。

启动 Xamarin Studio 并开始一个新的解决方案。在新建解决方案对话框中,在Android部分创建一个新的Android 应用。选择

最终你将得到一个类似于以下截图的解决方案:

构建你的第一个 Android 应用程序

提示

在 Visual Studio 中,Android 项目模板位于Android | 空白应用下。

你会注意到,以下特定于 Android 的文件和文件夹已经为你创建:

  • Components 文件夹。这与 iOS 项目相同,是添加来自 Xamarin 组件商店的组件的地方。

  • Assets 文件夹:这个目录将包含具有 AndroidAsset 构建动作的文件。这个文件夹将包含要随 Android 应用程序捆绑的原始文件。

  • Properties/AndroidManifest.xml:这个文件包含了关于你的 Android 应用程序的标准声明,如应用程序名称、ID 和权限。

  • Resources 文件夹:资源包括可以经由 Android 资源系统加载的图片、布局、字符串等。每个文件将在 Resources.designer.cs 中生成一个 ID,你可以使用它来加载资源。

  • Resources/drawable 文件夹:通常将应用程序使用的任何图片放在这里。

  • Resources/layout 文件夹:这包含了 Android 用来声明 UI 的 *.axml(Android XML)文件。布局可以是整个活动片段对话框或要在屏幕上显示的子控件

  • Resources/mipmap-* 文件夹:包含在不同 Android 设备主屏幕上显示的应用程序图标。这些文件夹中的应用图标因为它们用于与设备当前密度不同的分辨率。

  • Resources/values 文件夹:这包含了声明应用程序中字符串(和其他类型)的键值对的 XML 文件。这是在 Android 上通常设置多语言本地化的方式。

  • MainActivity.cs:这是MainLauncher操作和你的安卓应用程序的第一个活动。在 Android 应用中没有static void Main函数;执行从设置了MainLaunchertrue的活动开始。

现在让我们执行以下步骤来运行应用程序:

  1. 点击播放按钮编译并运行应用程序。

  2. 可能会出现一个选择设备对话框。

  3. 选择你喜欢的模拟器,并点击启动模拟器。如果你在第一章,Xamarin 设置中设置了 x86 模拟器,我建议使用它。

  4. 等待几秒钟让模拟器启动。一旦启动,建议在你从事 Android 项目工作时让它保持运行。这将为你节省大量等待时间。

  5. 你现在应该在设备列表中看到已启用的模拟器;选择它,然后点击确定

  6. 第一次将应用部署到模拟器或设备时,Xamarin Studio 需要安装一些东西,比如 Mono 共享运行时和 Android 平台工具。

  7. 切换到安卓模拟器。

  8. 你的应用程序将会出现。

提示

在 Windows 上的 Visual Studio 中,你也可以尝试使用Visual Studio Emulator for Android。这是一个不错的模拟器,预装在 Visual Studio 2015 中。

当所有工作完成后,你已经部署了你的第一个安卓应用程序,其中包括一个单一按钮。你的应用看起来将如下截图所示:

构建你的第一个安卓应用程序

了解安卓活动

安卓操作系统非常注重活动(Activity)这一概念。活动是用户在屏幕上可以执行的任务或工作单元。例如,用户会进行拨号活动来拨打一个号码,并进行第二个活动与通讯录互动以找到该号码。每个安卓应用程序都是由一个或多个活动组成,用户可以启动这些活动,并通过按下设备上的硬件返回键来退出或取消。用户的历史记录保存在安卓的后退堆栈中,在特殊情况下,你可以通过代码操作它。当一个新的活动开始时,前一个活动会被暂停并保存在内存中供以后使用,除非操作系统内存不足。

活动之间是松耦合的;在某种程度上,你可以认为它们在内存中拥有完全独立的状态。静态类、属性和字段将保持应用程序的生命周期,但常见做法是将状态通过安卓捆绑包传递。这对于传递列表中显示的项目的标识符,以便在新活动中编辑该项目非常有用。

活动有以下生命周期回调方法,你可以重写:

  • OnCreate: 当你的活动被创建时,这是第一个被调用的方法。在这里设置你的视图并执行其他加载逻辑。最重要的是,你将在这里调用SetContentView来设置你的活动视图。

  • OnResume: 当你的活动视图在屏幕上可见时会被调用。如果活动是第一次显示,或者用户从另一个活动返回到它时,都会调用此方法。

  • OnPause: 当用户离开你的活动时会被调用。它可能发生在导航到应用内的新活动之前、锁屏或按下主页按钮时。假设用户可能不会返回,因此你需要在这里保存用户所做的任何更改。

  • OnStart: 当活动的视图即将在屏幕上显示时,紧随OnResume之前发生。当活动开始或用户从另一个活动返回到它时,会发生此方法。

  • OnStop: 当活动的视图不再在屏幕上显示时,紧随OnPause之后发生。

  • OnRestart: 当用户从上一个活动返回到你的活动时,会发生此方法。

  • OnActivityResult: 此方法用于在 Android 上与其他应用程序中的活动进行通信。它与StartActvityForResult结合使用;例如,你可以用这个方法与 Facebook 应用程序交互以登录用户。

  • OnDestroy: 当你的活动即将从内存中释放时会被调用。在这里执行任何可能帮助操作系统的额外清理工作,例如处理活动使用的任何其他重量级对象。

Android 生命周期的流程图如下:

理解 Android 活动

与 iOS 不同,Android 并未对其开发者实施任何设计模式。然而,在一定程度上理解 Android 活动生命周期是不可或缺的。活动中许多概念与 iOS 上的控制器有相似之处;例如,OnStart相当于ViwWillAppear,而OnResume则相当于ViewDidAppear

在处理活动时需要注意的其他方法如下:

  • StartActivity(Type type): 此方法在应用程序内启动一个新活动,并不向活动传递任何额外信息。

  • StartActivity(Intent intent): 这是一个用于通过Intent启动新活动的重载方法。它使你能够向新活动传递额外信息,并且你也可以启动其他应用程序中的活动。

  • StartActivityForResult: 此方法启动一个新活动,并预期在活动操作完成后收到OnActivityResult

  • Finish: 这将关闭当前活动,并在完全关闭且不再在屏幕上显示时调用OnDestroy。根据后退栈上当前的内容,用户将返回到上一个活动或主屏幕。

  • SetContentView:此方法设置要为活动显示的主要视图。它应该在活动在屏幕上显示之前在OnCreate方法内调用。

  • FindViewById:这是一个用于定位在活动中显示的视图的方法。它有一个泛型版本,用于返回适当类型的视图。

你可以将intent视为描述从一个活动过渡到另一个活动的对象。你还可以通过意图传递附加数据,以及修改活动的显示方式和用户的导航历史。

除了活动之外,Android 还有片段(fragment)的概念。你可以将片段视为在父活动中显示的微型活动。片段对于在应用中复用不同的 UI 部分非常有用,还可以帮助你实现在平板电脑上的分屏导航。

Xamarin 的 Android 设计师

Android 项目的默认模板比 iOS 具有更多内置功能,因此我们稍后会有一些控件需要删除。Android 用户界面布局在 XML 文件中定义,这些文件对人类可读和可编辑。然而,Xamarin Studio 提供了一个优秀的设计工具,允许你拖放控件来定义你的 Android 布局。让我们为你的应用程序添加更多功能,并开始使用 Android 设计师。

返回 Xamarin Studio,执行以下步骤为你的应用添加功能:

  1. 在 Xamarin Studio 中打开本章前面创建的 Android 项目。

  2. 在项目中的资源 | 布局下,打开Main.axml

  3. 你会看到 Android 设计师在 Xamarin Studio 中打开。

  4. 删除 Android 项目模板中现有的标签和按钮。

  5. 从右侧的工具箱部分拖动一个TextView到空白布局中。

  6. 在标签中输入一些默认文本,如0

  7. 在右侧的属性窗格中,你会看到id值设置为@+id/textView1。我们将它改为@+id/text,以便稍后可以用 C#与标签交互。

  8. 现在,从工具箱部分拖动一个GridLayout,并在属性面板下设置行数为 4 和列数为 3。

  9. 工具箱部分拖动 10 个Button控件,并将它们的文本编号为0-9

  10. 将它们的id设置为从0-9编号的@+id/button0

  11. 创建两个更多带有 id @+id/plus@+id/equals 的按钮,将它们的文本分别设置为+=

提示

在 Visual Studio 中,Xamarin.Android 设计器与其 Xamarin Studio 对应部分基本相同。主要区别在于编辑控件属性时,使用的是标准的 Visual Studio 属性编辑器。你可能会发现通过属性窗格的工具栏按钮在A 到 Z和分组排序之间切换很有用。

现在,如果你尝试编译并运行你的应用程序,你可能会注意到一些编译错误。现在,打开 MainActivity.cs 并删除 OnCreate 方法中的代码,除了调用 SetContentView 的那一行。

你的 MainActivity 应该看起来像这样:

[Activity(Label = "Calculator", MainLauncher = true, Icon = "@mipmap/icon")] 
public class MainActivity : Activity 
{
  protected override void OnCreate(Bundle savedInstanceState) 
  {
    base.OnCreate(savedInstanceState);
    SetContentView(Resource.Layout.Main); 
  }
}

现在,启动你的 Android 应用程序,它应该与你设计师所做的更改完全相同,如下所示:

Xamarin 的 Android 设计器

切换回 Xamarin Studio 并打开 MainActivity.cs 文件。我们将修改活动以与在 Xamarin.Android 设计器中设置好的布局进行交互。我们使用 FindViewById 方法通过我们在布局文件中设置的 ID 来获取视图。Xamarin Studio 还自动生成了一个名为 Resource 的静态类,以便引用你的标识符。

首先,在 MainActivity.cs 中声明一个类级别的私有字段:

TextView text; 

让我们在 OnCreate 中通过以下代码获取 TextView 字段的实例:

text = FindViewById<TextView>(Resource.Id.text); 

Resource 类是一个静态类,Xamarin 设计器会为你填充它。为了将来的参考,你可能需要构建你的 Android 项目,以便新的 IDs 和其他资源在 Xamarin Studio 的 C# 文件中显示。

MainActivity.cs 中创建一个我们将用于点击事件的方法,它将与我们在 iOS 上所做的非常相似:

private void OnNumber(object sender, EventArgs e) 
{ 
    var button = (Button)sender; 
    if (string.IsNullOrEmpty(text.Text) || text.Text == "0") 
    { 
        text.Text = button.Text; 
    } 
    else 
    { 
        text.Text += button.Text; 
    } 
}

接下来,让我们在活动中的 OnCreate 方法里为 number1 绑定 Click 事件:

var button = FindViewById<Button>(Resource.Id.number1); 
button.Click += OnNumber; 

为所有的数字按钮 0-9 重复这段代码。

接下来,让我们为 "add" 和 "equals" 按钮设置事件处理程序,就像我们在 iOS 应用中所做的那样:

private void OnAdd(object sender, EventArgs e) 
{ 
    if (!string.IsNullOrEmpty(text.Text)) 
    { 
        text.Text += "+"; 
    } 
} 

private void OnEquals(object sender, EventArgs e) 
{ 
    //This is the same simple calculator logic as on iOS 
    string[] split = text.Text.Split('+'); 
    int sum = 0;  
    foreach (string text in split) 
    { 
        int x; 
        if (int.TryParse(text, out x)) 
            sum += x; 
    } 
    text.Text = sum.ToString(); 
} 

接下来,让我们在活动中的 OnCreate 方法里为这些按钮绑定 Click 事件:

var add = FindViewById<Button>(Resource.Id.add); 
add.Click += OnAdd; 
var equals = FindViewById<Button>(Resource.Id.equals); 
equals.Click += OnEquals;; 

现在,如果我们运行应用程序,我们将得到一个与本章前面展示的 iOS 计算器功能完全相同的 Android 应用:

Xamarin 的 Android 设计器

总结:

在本章中,我们在 Xamarin Studio 中创建了第一个 iOS 应用程序。我们介绍了苹果的 MVC 设计模式,以更好地理解 UIViewControllerUIView 之间的关系,同时也介绍了如何在 Xamarin Studio 中使用 iOS 设计器编辑 storyboard 文件。接下来,我们在 Xamarin Studio 中创建了第一个 Android 应用程序,并学习了 Android 中的活动生命周期。我们还使用了 Xamarin 的 Android 设计器来修改 Android XML 布局。

从本章涵盖的主题来看,你应该能够使用 Xamarin 的工具为 iOS 和 Android 开发简单的应用程序,并且信心满满。你应该对原生 SDK 和设计模式有一个基本的了解,以完成在 iOS 和 Android 上的任务。

在下一章中,我们将介绍使用 Xamarin Studio 在平台之间共享代码的各种技术。我们将讨论架构跨平台应用程序的不同方法,以及如何在 Visual Studio 或 Xamarin Studio 中设置项目和解决方案。

第三章:iOS 与 Android 之间的代码共享

Xamarin 的工具承诺在可能的情况下利用每个平台的本地 API,在 iOS 和 Android 之间共享大部分代码。这样做更多的是软件工程的实践,而不是编程技能或对每个平台的知识。为了构建一个支持代码共享的 Xamarin 应用程序,必须将应用程序分离为不同的层次。我们将介绍基础知识以及针对特定情况考虑的具体选项。

在本章中,我们将涵盖以下内容:

  • 用于代码共享的 MVVM 设计模式

  • 项目和解决方案的组织策略

  • 可移植类库(PCLs)

  • 针对特定平台代码的预处理器语句

  • 依赖注入(DI)简化

  • 控制反转(IoC)

学习 MVVM 设计模式

模型-视图-视图模型MVVM)设计模式最初是为了使用XAMLWPFWindows Presentation Foundation)应用程序而发明的,用于将 UI 与业务逻辑分离,并充分利用数据绑定。以这种方式构建的应用程序有一个独特的视图模型层,它与用户界面没有依赖关系。这种架构本身针对单元测试以及跨平台开发进行了优化。由于应用程序的视图模型类对 UI 层没有依赖,你可以轻松地将 iOS 用户界面替换为 Android 界面,并针对视图模型层编写测试。MVVM 设计模式与前面章节讨论的 MVC 设计模式也非常相似。

MVVM 设计模式包括以下内容:

  • 模型:模型层是驱动应用程序的后端业务逻辑以及任何伴随的业务对象。这可以是任何从向服务器发起网络请求到使用后端数据库的内容。

  • 视图:这一层是屏幕上实际看到用户界面。在跨平台开发中,它包括任何特定于平台的代码,用于驱动应用程序的用户界面。在 iOS 上,这包括整个应用程序中使用的控制器,在 Android 上,则包括应用程序的活动。

  • 视图模型:这一层在 MVVM 应用程序中充当粘合剂。视图模型层协调视图和模型层之间的操作。视图模型层将包含视图获取或设置的属性,以及每个视图上用户可以进行的每个操作的函数。如果需要,视图模型还将在模型层上调用操作。

下图展示了 MVVM 设计模式:

学习 MVVM 设计模式

需要注意的是,视图(View)和视图模型(ViewModel)层之间的交互传统上是通过 WPF 的数据绑定来创建的。然而,iOS 和 Android 没有内置的数据绑定机制,因此本书将采用的方法是从视图手动调用视图模型层。有几个框架提供了数据绑定功能,例如MVVMCrossXamarin.Forms

为了更好地理解这一模式,让我们实现一个常见场景。假设我们在屏幕上有一个搜索框和一个搜索按钮。当用户输入一些文本并点击按钮时,将向用户显示产品和价格列表。在我们的示例中,我们将使用 C# 5 中可用的asyncawait关键字来简化异步编程。

要实现此功能,我们将从一个简单的model类(也称为business对象)开始,如下所示:

public class Product 
{ 
    public int Id { get; set; } //Just a numeric identifier 
    public string Name { get; set; } //Name of the product 
    public float Price { get; set; } //Price of the product 
} 

接下来,我们将根据搜索词实现我们的模型层以检索产品。这里执行业务逻辑,表达实际需要如何执行搜索。以下代码行中可以看到这一点:

// An example class, in the real world would talk to a web 
// server or database. 
public class ProductRepository 
{ 
  // a sample list of products to simulate a database 
  private Product[] products = new[] 
  { 
    new Product { Id = 1, Name = "Shoes", Price = 19.99f }, 
    new Product { Id = 2, Name = "Shirt", Price = 15.99f }, 
    new Product { Id = 3, Name = "Hat", Price = 9.99f }, 
  }; 

  public async Task<Product[]> SearchProducts(string searchTerm) 
  { 
    // Wait 2 seconds to simulate web request 
    await Task.Delay(2000); 

    // Use Linq-to-objects to search, ignoring case 
    searchTerm = searchTerm.ToLower(); 

    return products.Where(p =>
      p.Name.ToLower().Contains(searchTerm)) 
      .ToArray(); 
  } 
} 

需要注意的是,ProductProductRepository类都被认为是跨平台应用程序模型层的一部分。有些人可能认为ProductRepository是一个服务,通常是一个自包含的用于获取数据的类。将此功能分为两个类是一个好主意。Product类的任务是保存有关产品的信息,而ProductRepository负责检索产品。这是单一职责原则的基础,该原则指出每个类应该只有一个工作或关注点。

接下来,我们将按以下方式实现一个ViewModel类:

public class ProductViewModel 
{ 
  private readonly ProductRepository repository =
      new ProductRepository(); 

  public string SearchTerm 
  { 
    get; 
    set; 
  } 

  public Product[] Products 
  { 
    get; 
    private set; 
  } 

  public async Task Search() 
  { 
    if (string.IsNullOrEmpty(SearchTerm)) 
      Products = null; 
    else 
      Products = await repository.SearchProducts(SearchTerm); 
  } 
} 

从这里开始,你的特定平台代码就开始了。每个平台将处理管理ViewModel类的实例,设置SearchTerm属性,并在点击按钮时调用Search。当任务完成后,用户界面层将更新屏幕上显示的列表。

如果你熟悉与 WPF 一起使用的 MVVM 设计模式,你可能会注意到我们没有为数据绑定实现INotifyPropertyChanged。由于 iOS 和 Android 没有数据绑定的概念,我们省略了此功能。如果你计划为移动应用程序提供一个 WPF 或 Windows UWP 版本,或者使用提供数据绑定的框架,你应在需要的地方实现支持。

提示

要了解更多关于INotifyPropertyChanged的信息,请查看 MSDN 上的这篇文章:msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged

比较项目组织策略

在这一点上,你可能会问自己,如何在 Xamarin Studio 中设置解决方案以处理共享代码,同时也有特定平台的项目?Xamarin.iOS 应用程序只能引用 Xamarin.iOS 类库;因此,设置解决方案可能会遇到问题。有几种设置跨平台解决方案的策略,每种策略都有其自身的优点和缺点。

跨平台解决方案的选项如下:

  • 文件链接:对于这个选项,你可以从普通的.NET 4.0 或.NET 4.5 类库开始,该类库包含所有共享代码。然后,你需要为每个希望应用运行的平台创建一个新项目。每个特定平台的项目将包含一个子目录,其中链接了第一个类库中的所有文件。要设置这个,将现有文件添加到项目中,并选择添加对文件的链接选项。任何单元测试都可以针对原始类库运行。文件链接的优点和缺点如下:

    • 优点:这种方法非常灵活。你可以选择链接或不链接某些文件,并且可以使用如#if IPHONE之类的预处理器指令。你还可以在 Android 和 iOS 上引用不同的库。

    • 缺点:你必须在三个项目中管理文件的存在:核心库、iOS 和 Android。如果这是一个大型应用程序,或者有很多人在处理它,这可能会很麻烦。自从共享项目出现后,这个选项也有些过时了。

  • 克隆项目文件:这非常类似于文件链接,主要的区别在于除了主项目之外,每个平台都有一个类库。将 iOS 和 Android 项目放在主项目同一目录下,文件可以添加而无需链接。你可以通过右键单击解决方案并选择显示选项 | 显示所有文件轻松地添加文件。单元测试可以针对原始类库或特定平台的版本运行:

    • 优点:这种方法与文件链接一样灵活,但你不需要手动链接任何文件。你仍然可以使用预处理器指令,并在每个平台上引用不同的库。

    • 缺点:你仍然需要在三个项目中管理文件的存在。此外,还需要一些手动文件整理来设置这个。你最终在每个平台上还要管理一个额外的项目。自从共享项目出现后,这个选项也有些过时了。

  • 共享项目:从 Visual Studio 2013 开始,微软创建了共享项目的概念,以实现 Windows 8 和 Windows Phone 应用程序之间的代码共享。Xamarin 也在 Xamarin Studio 中实现了共享项目,作为实现代码共享的另一种选项。共享项目实际上与文件链接相同,因为添加对共享项目的引用实际上将其文件添加到你的项目中:

    • 优点:这种方法与文件链接相同,但更加整洁,因为你的共享代码位于一个单一的项目中。Xamarin Studio 还提供了一个下拉菜单,可以在引用的每个项目之间切换,这样你就可以看到预处理器语句在代码中的效果。

    • 缺点:由于共享项目中的所有文件都会被添加到每个平台的主项目中,因此在共享项目中包含特定平台的代码可能会变得不美观。如果你有一个大型团队,或者团队成员经验不足,预处理语句可能会迅速失控。共享项目也不会编译成 DLL,所以如果没有源代码,就没有办法分发这种类型的项目。

  • 便携式类库:一旦你对 Xamarin 更加熟悉,这将是最佳选择;你从创建一个所有共享代码的便携式类库(PCL)项目开始解决方案。这是一种特殊的项目类型,允许多个平台引用同一个项目,使你可以使用每个平台中可用的 C#和.NET 框架的最小子集。每个特定平台的项目将直接引用这个库,以及任何单元测试项目:

    • 优点:你所有的共享代码都在一个项目中,所有平台都使用相同的库。由于不可能使用预处理器语句,PCL 库的代码通常更整洁。特定平台的代码通常通过接口或抽象类进行抽象。

    • 缺点:根据你面向的平台数量,你将受限于.NET 的一个子集。特定平台的代码需要使用依赖注入,这对于不熟悉这一主题的开发者来说可能是一个更高级的话题。

设置共享项目

为了完全理解每个选项以及何种情况需要它,让我们为共享项目和便携式类库定义一个解决方案结构。让我们使用本章前面提到的产品搜索示例,并为每种方法设置一个解决方案。

要设置共享项目,请执行以下步骤:

  1. 打开 Xamarin Studio 并开始一个新解决方案。

  2. 多平台 | 应用部分下选择一个新的单视图应用

  3. 将应用命名为ProductSearch,并选择使用共享库

  4. 完成这个新项目向导,Xamarin Studio 将生成三个项目:ProductSearchProductSearch.DroidProductSearch.iOS

  5. ProductProductRepositoryProductViewModel类添加到本章前面提到的ProductSearch项目中。你需要在需要的地方添加using System.Threading.Tasks;using System.Linq;

  6. 点击顶部菜单中的构建 | 构建全部来再次检查一切,这样你就成功设置了一个跨平台解决方案。

完成后,你将得到一个解决方案树,其外观类似于以下截图所示:

设置共享项目

共享项目是开始跨平台开发的一个很好的起点。使用它们不会出错,并且它们提供了最大的灵活性,可以在共享代码中使用#if。共享项目可能不是最佳选择的情况,可能是因为你需要将共享项目分发给其他人,或者拥有非常大的团队或代码库。如果放任不管,预处理器指令确实可能会失控。

提示

在 Visual Studio 中,跨平台应用程序的项目模板可以在跨平台 | 空白应用(原生共享)下找到。需要注意的是,它还会生成一个 Windows Phone 项目,如果不需要,你可以简单地移除它。

使用便携式类库进行工作。

便携式类库PCL)是一个 C#库项目,能够在包括 iOS、Android、Windows、Windows Store 应用、Windows Phone、Silverlight 和 Xbox 360 在内的多个平台上得到支持。PCL 是微软为简化不同.NET 框架版本间开发而做出的努力。Xamarin 也为 iOS 和 Android 增加了对 PCL 的支持。许多流行的跨平台框架和开源库开始开发 PCL 版本,如 Json.NET 和 MVVMCross。

要设置一个共享项目,请执行以下步骤:

  1. 打开 Xamarin Studio 并开始一个新的解决方案。

  2. 多平台 | 应用部分下选择新的单视图应用。或者在 Visual Studio 中,选择跨平台 | 空白应用(原生便携式)

  3. 将应用命名为ProductSearch,并选择使用便携式库

  4. 完成这个新项目向导,Xamarin Studio 将生成三个项目:ProductSearchProductSearch.DroidProductSearch.iOS

  5. 将本章前面提到的ProductProductRepositoryProductViewModel类添加到ProductSearch项目中。你需要在需要的地方添加using System.Threading.Tasks;using System.Linq;

  6. 点击顶部菜单中的构建 | 构建全部以再次检查一切,这样你就成功设置了一个 PCL 跨平台解决方案。

如果你需要将项目作为 DLL 或 NuGet 包共享,PCL 是最佳选择。它还帮助你将特定平台的关注点分离,因为它迫使你使用接口或基类,并结合依赖注入(DI)。如果你需要在 iOS 或 Android 上使用类似本地的库,如 Facebook SDK,也会出现类似的问题。

提示

在撰写本文时,微软刚刚发布了.NET Core 和新的.NET Standard。这将影响未来 PCLs 的工作方式,但不会破坏现有的 Xamarin.iOS 和 Xamarin.Android 项目。不过,这将使你能够继续与.NET Core 和 ASP.NET Core 项目共享代码。

使用预处理器语句

当使用共享项目时,你最有力的工具之一就是使用预处理器语句。如果你不熟悉它们,C# 有能力定义预处理器变量,如 #define IPHONE,然后使用 #if IPHONE#if !IPHONE

下面是使用该技术的简单示例:

#if IPHONE 
  Console.WriteLine("I am running on iOS"); 
#elif ANDROID 
  Console.WriteLine("I am running on Android"); 
#else 
  Console.WriteLine("I am running on ???"); 
#endif 

在 Xamarin Studio 中,你可以在项目选项的 构建 | 编译器 | 定义符号 下定义预处理器变量,用分号分隔。这些变量将被应用到整个项目。请注意,你必须为解决方案中的每个配置设置(调试发布)设置这些变量;这是一个容易遗漏的步骤。你还可以在任何 C# 文件的顶部通过声明 #define IPHONE 来定义这些变量,但它们只会在 C# 文件内应用。

让我们再看一个例子,假设我们想要在每个平台上实现一个打开 URL 的类:

public static class Utility 
{ 
  public static void OpenUrl(string url) 
  { 
    //Open the url in the native browser 
  } 
} 

前面的例子是使用预处理器语句的完美候选者,因为它非常特定于每个平台,而且是一个相当简单的函数。要在 iOS 和 Android 上实现该方法,我们需要利用一些本地 API。重构类,使其如下所示:

#if IPHONE 
  //iOS using statements 
  using MonoTouch.Foundation; 
  using MonoTouch.UIKit; 
#elif ANDROID 
  //Android using statements 
  using Android.App; 
  using Android.Content; 
  using Android.Net; 
#else 
  //Standard .Net using statement 
  using System.Diagnostics; 
#endif 

public static class Utility 
{ 
  #if ANDROID 
    public static void OpenUrl(Activity activity, string url) 
  #else 
    public static void OpenUrl(string url) 
  #endif 
  { 
    //Open the url in the native browser 
    #if IPHONE 
      UIApplication.SharedApplication.OpenUrl(
         NSUrl.FromString(url)); 
    #elif ANDROID 
      var intent = new Intent(Intent.ActionView,
         Uri.Parse(url)); 
      activity.StartActivity(intent); 
    #else 
      Process.Start(url); 
    #endif 
  } 
} 

前一个类别支持三种不同类型的项目:Android、iOS 和标准的 Mono 或 .NET 框架类库。在 iOS 的情况下,我们可以使用苹果 API 中可用的静态类来执行功能。Android 稍微有些复杂,需要 Activity 对象来本地启动浏览器。我们通过修改 Android 上的输入参数来解决这一问题。最后,我们有一个纯 .NET 版本,它使用 Process.Start() 来启动一个 URL。需要注意的是,使用第三种选项在 iOS 或 Android 上本地是无法工作的,这就需要我们使用预处理器语句。

使用预处理器语句通常不是跨平台开发中最干净或最好的解决方案。它们通常最好在困境中使用,或用于非常简单的函数。代码很容易失控,如果有很多 #if 语句,代码可能会变得非常难以阅读,因此适度使用总是更好的。当类大多数是特定于平台的时候,使用继承或接口通常是更好的解决方案。

简化依赖注入

依赖注入 一开始看起来可能是一个复杂的话题,但大部分情况下它是一个简单的概念。它是一个设计模式,旨在使你的应用程序中的代码更加灵活,以便在需要时可以替换某些功能。这个想法围绕在应用程序中设置类之间的依赖关系,以便每个类只与接口或基类/抽象类交互。这给了你在需要实现本地功能时在每个平台上覆盖不同方法的自由。

这个概念源自于SOLID面向对象设计原则,如果你对软件架构感兴趣,这是一组你可能想要研究的规定。SOLID 中的D代表依赖关系。具体来说,该原则声明程序应依赖于抽象,而不是具体(具体类型)。

为了建立这个概念,让我们通过以下例子来逐步了解:

  1. 假设我们需要在应用程序中存储一个设置,以确定声音是开还是关。

  2. 现在我们来声明一个简单的设置接口:interface ISettings { bool IsSoundOn { get; set; } }

  3. 在 iOS 上,我们想使用NSUserDefaults类来实现这个接口。

  4. 同样,在 Android 上,我们会使用SharedPreferences来实现这一点。

  5. 最后,任何需要与这个设置交互的类只需引用ISettings,这样每个平台上的实现都可以被替换。

作为参考,这个例子的完整实现看起来如下片段所示:

public interface ISettings 
{ 
  bool IsSoundOn 
  { 
    get; 
    set; 
  } 
} 

//On iOS 
using UIKit; 
using Foundation; 

public class AppleSettings : ISettings 
{ 
  public bool IsSoundOn 
  { 
    get 
    { 
      return NSUserDefaults.StandardUserDefaults 
        .BoolForKey("IsSoundOn"); 
    } 
    set 
    { 
      var defaults = NSUserDefaults.StandardUserDefaults; 
      defaults.SetBool(value, "IsSoundOn"); 
      defaults.Synchronize(); 
    } 
  } 
} 

//On Android 
using Android.Content; 

public class DroidSettings : ISettings 
{ 
  private readonly ISharedPreferences preferences; 

  public DroidSettings(Context context) 
  { 
    preferences = context.GetSharedPreferences(
       context.PackageName, FileCreationMode.Private); 
  } 

  public bool IsSoundOn 
  { 
    get 
    { 
      return preferences.GetBoolean("IsSoundOn", true); 
    } 
    set 
    { 
      using (var editor = preferences.Edit()) 
      { 
        editor.PutBoolean("IsSoundOn", value); 
        editor.Commit(); 
      } 
    } 
  } 
} 

现在,按照 MVVM 模式,你可能会有一个ViewModel类,它只引用ISettings,如下面的代码片段所示:

public class SettingsViewModel 
{ 
  private readonly ISettings settings; 

  public SettingsViewModel(ISettings settings) 
  { 
    this.settings = settings; 
  } 

  public bool IsSoundOn 
  { 
    get; 
    set; 
  } 

  public void Save() 
  { 
    settings.IsSoundOn = IsSoundOn; 
  } 
} 

对于这样一个简单的例子来说,使用 ViewModel 层并不一定需要,但如果你需要进行其他任务,如输入验证,你可以看到它将非常有用。一个完整的应用程序可能会有更多的设置,并且可能需要向用户展示加载指示器。抽象出你的设置的实现会给你的应用程序带来其他好处,增加灵活性。比如说,你突然需要将 iOS 上的NSUserDefaults替换为 iCloud 版本;你可以通过实现一个新的ISettings类轻松做到这一点,其余的代码将保持不变。这还将帮助你针对新的平台,比如 Windows UWP,你可能选择以特定于平台的方式实现ISettings

实现控制反转

在这一点上,你可能会问自己,如何切换不同的类,比如ISettings的例子?控制反转IoC)是一种设计模式,旨在补充依赖注入并解决这个问题。基本原则是,在应用程序中创建的许多对象都由一个单独的类来管理和创建。在应用程序中,不是使用标准的 C#构造函数来创建你的ViewModelModel类,而是由服务定位器或工厂类来管理它们。

IoC 有许多不同的实现和风格,所以让我们实现一个简单的服务定位器类,以供本书的其余部分使用,如下所示:

public static class ServiceContainer 
{ 
  static readonly Dictionary<Type, Lazy<object>> services = 
    new Dictionary<Type, Lazy<object>>(); 

  public static void Register<T>(Func<T> function) 
  { 
    services[typeof(T)] = new Lazy<object>(() => function()); 
  } 

  public static T Resolve<T>() 
  { 
    return (T)Resolve(typeof(T)); 
  } 

  public static object Resolve(Type type) 
  { 
    Lazy<object> service; 
    if (services.TryGetValue(type, out service)) 
    { 
      return service.Value; 
    } 
    throw new Exception("Service not found!"); 
  } 
} 

这个类受到 XNA/MonoGame 的GameServiceContainer类的简单性的启发,并遵循服务定位器模式。主要区别在于使用泛型和它是一个静态类。

要使用我们的ServiceContainer类,我们只需通过调用Register声明应用中要使用的ISettings或其他接口的版本,如下面的代码所示:

//iOS version of ISettings 
ServiceContainer.Register<ISettings>(() =>
   new AppleSettings()); 

//Android version of ISettings 
ServiceContainer.Register<ISettings>(() => 
   new DroidSettings(this)); 

//You can even register ViewModels 
ServiceContainer.Register<SettingsViewModel>(() => 
   new SettingsViewModel()); 

在 iOS 上,您可以将此注册代码放在static void Main()方法中,或者放在AppDelegate类的FinishedLaunching方法中。这些方法总是在应用程序启动之前调用。

在 Android 上,情况稍微复杂一些。您不能将此代码放在作为主启动器的活动的OnCreate方法中。在某些情况下,Android OS 可能会关闭您的应用程序,但稍后会在另一个活动中重新启动它。这种情况会导致您的应用程序崩溃,因为它会尝试访问尚未注册的容器中的服务。将此代码放在自定义的 Android Application类中是安全的,该类有一个在应用程序中任何活动创建之前调用的OnCreate方法。下面的代码展示了Application类的使用:

[Application] 
public class Application : Android.App.Application 
{ 
  //This constructor is required 
  public Application(IntPtr javaReference, JniHandleOwnership
      transfer): base(javaReference, transfer) 
  { 

  } 

  public override void OnCreate() 
  { 
    base.OnCreate(); 

    //IoC Registration here 
  } 
} 

要从ServiceContainer类中获取服务,我们可以重写SettingsViewModel类的构造函数,如下面的代码所示:

public SettingsViewModel() 
{ 
  this.settings = ServiceContainer.Resolve<ISettings>(); 
} 

同样,您可以使用泛型Resolve方法从 iOS 上的控制器或 Android 上的活动中调用任何需要的ViewModel类。这是管理应用程序内部依赖关系的很好且简单的方法。

当然,有一些优秀的开源库实现了 C#应用程序的 IoC。如果您需要更高级的服务定位功能,或者只是想过渡到一个更复杂的 IoC 容器,您可以考虑切换到其中之一。

这里有一些与 Xamarin 项目一起使用的库:

概要

在本章中,我们了解了 MVVM 设计模式以及如何使用它来更好地构建跨平台应用程序。我们比较了管理包含 iOS 和 Android 项目的 Xamarin Studio 解决方案的几种项目组织策略。我们讨论了可移植类库作为共享代码的首选选项,以及如何使用预处理器语句作为实现平台特定代码的快速而简单的方法。

完成本章节后,你应该已经掌握了使用 Xamarin Studio 在 iOS 和 Android 应用之间共享代码的几种技术。采用 MVVM 设计模式可以帮助你区分共享代码和特定平台的代码。我们还介绍了设置跨平台 Xamarin 解决方案的几种选项。你也应该牢固掌握使用依赖注入和控制反转技术,使共享代码能够访问每个平台的本地 API。在下一章节中,我们将开始编写跨平台应用程序,并深入探讨这些技术的使用。

第四章:XamSnap - 一个跨平台应用

在我看来,真正学会一项编程技能的最佳方式是接受一个需要运用该技能的简单项目。这给新开发者提供了一个可以专注于他们试图学习的概念的项目,而无需处理修复错误或遵循客户需求的负担。为了加深我们对 Xamarin 和跨平台开发的理解,让我们为 iOS 和 Android 开发一个名为 XamSnap 的简单应用。

在本章中,我们将涵盖以下主题:

  • 我们的示例应用概念

  • 我们应用的模型层

  • 模拟网络服务

  • 我们应用的 ViewModel 层

  • 编写单元测试

启动我们的示例应用概念

这个概念很简单:流行的聊天应用 Snapchat 的一个简单克隆。由于短信成本和诸如 iPod Touch 或 iPad 等设备的支持,Apple App Store 中有几个这样的流行应用。这应该是一个对用户可能有用且涵盖为 iOS 和 Android 开发应用的具体主题的实用现实示例。

在开始开发之前,让我们列出我们需要的一组界面:

  • 登录/注册:这个界面将包括用户的标准化登录和注册过程。

  • 对话列表:这个界面将包括一个启动新对话的按钮。

  • 好友列表:这个界面将提供一种在开始新对话时添加新好友的方法。

  • 对话:这个界面将展示你与其他用户之间的消息列表,并提供回复选项。

  • 相机:除了文本消息,Snapchat 还具有发送照片的功能。我们将添加使用设备相机或照片库发送照片的选项。

因此,一个快速的应用程序线框布局可以帮助我们更好地理解应用程序的布局。下图展示了应用中应包含的一组屏幕:

启动我们的示例应用概念

开发我们的模型层

既然我们已经对应用有了很好的了解,下一步就是开发这个应用的商业对象或模型层。让我们首先定义几个类,这些类将包含整个应用中使用的数据。为了组织方便,建议将这些类添加到项目中的 Models 文件夹中。

让我们从表示用户的类开始。该类可以按以下方式创建:

public class User 
{ 
  //NOTE: we will treat this as a unique name 
  public string Name { get; set; } 

  //NOTE: we'll try to use this in a secure way 
  public string Password { get; set; } 
} 

到目前为止非常直观;接下来,我们按照以下方式创建表示对话和消息的类:

public class Conversation 
{ 
  public string Id { get; set; } 

  public string UserName { get; set; } 
} 

public class Message 
{ 
  public string Id { get; set; } 

  //NOTE: the Id of a Conversation 
  public string Conversation { get; set; }  

  public string UserName { get; set; } 

  public string Text { get; set; }

//NOTE: some messages will include photos 
  public string Image { get; set; } 
} 

请注意,我们将字符串用作各种对象的标识符;这将简化我们与在后续章节中作为 Azure Function 运行的后端的集成。UserName 是应用程序设置的值,用于更改与对象关联的用户。

现在让我们继续执行以下步骤来设置我们的解决方案:

  1. 从创建一个新的解决方案开始,作为 iOS 和 Android 的多平台 | 应用 | 单视图应用项目。

  2. 将项目命名为XamSnap,并确保已选择使用可移植类库

  3. 你也可以选择为这个项目使用共享项目,但我更倾向于使用可移植类库。

  4. 点击创建,在指定目录中创建你的解决方案。

提示

与前面的章节一样,Visual Studio 的步骤略有不同。你需要创建一个作为可移植类库的解决方案,并然后添加 iOS 和 Android 项目。不要忘记在 iOS 和 Android 项目中都添加对 PCL 的引用。

编写模拟网络服务。

在开发移动应用时,你可能需要在真正的后端网络服务可用之前就开始开发你的应用。为了防止开发完全停滞,一个好的方法可能是开发一个服务的模拟版本。这在需要编写单元测试,或者等待另一个团队为你的应用开发后端时也很有帮助。

首先,让我们分解一下我们的应用将对网络服务器执行的操作。操作如下:

  1. 使用用户名和密码登录。

  2. 注册一个新账户。

  3. 获取用户的朋友列表。

  4. 通过他们的用户名添加朋友。

  5. 获取用户的现有会话列表。

  6. 获取会话中的消息列表。

  7. 发送消息。

现在让我们定义一个接口,为每个场景提供一个方法。方法如下:

public interface IWebService 
{ 
  Task<User> Login(string userName, string password); 

  Task<User> Register(User user); 

  Task<User[]> GetFriends(string userName); 

  Task<User> AddFriend(string username, string friendName); 

  Task<Conversation[]> GetConversations(string userName); 

  Task<Message[]> GetMessages(string conversation); 

  Task<Message> SendMessage(Message message); 
} 

如你所见,我们通过利用.NET 基类库中的TPL任务并行库)简化了与网络服务的任何异步通信。

由于与网络服务通信可能是一个漫长的过程,因此使用Task<T>类进行这些操作总是一个好主意。否则,你可能无意中在用户界面线程上运行一个耗时的任务,这将导致在操作期间无法接收用户输入。对于网络请求来说,Task绝对是必需的,因为用户可能正在 iOS 和 Android 上使用蜂窝网络连接,这将使我们能够以后使用asyncawait关键字。

提示

如果你不太熟悉 C#中用于简化异步编程的 async/await,查看 MSDN 上的相关主题会很有帮助:msdn.microsoft.com/en-us/library/mt674882.aspx

现在让我们实现一个实现了此接口的服务。将如FakeWebService这样的类放在项目的Fakes文件夹中。让我们从类声明和接口的第一个方法开始:

public class FakeWebService : IWebService
{
  public int SleepDuration { get; set; }

  public FakeWebService()
  {
    SleepDuration = 1000;
  }

  private Task Sleep()
  {
    return Task.Delay(SleepDuration);
  }

  public async Task<User> Login(string userName, string password)
  {
    await Sleep(); 
    return new User { Name = userName }; 
  }
}

我们从一个名为SleepDuration的属性开始,用于存储毫秒数。这用于模拟与 Web 服务器的交互,这可能需要一些时间。在不同情况下更改SleepDuration值也很有用。例如,在编写单元测试时,你可能希望将此值设置得较小,以便测试快速执行。

接下来,我们实现了一个简单的Sleep方法,该方法返回一个引入了若干毫秒延迟的任务。这个方法将在伪服务中用于在每个操作上造成延迟。

最后,Login方法只是在Sleep方法上使用了await调用,并返回了一个具有适当Name的新User对象。目前,任何用户名或密码组合都可以使用;但是,你可能希望在这里编写一些代码来检查特定的凭据。

现在,让我们按照以下方式继续实现FakeWebService类的几个更多方法:

public async Task<User[]> GetFriends(string userId)
{
  await Sleep();
  return new[] 
  { 
    new User { Name = "bobama" }, 
    new User { Name = "bobloblaw" }, 
    new User { Name = "georgemichael" }, 
  };
}

public async Task<User> AddFriend(
  string username, string friendName)
{
  await Sleep(); 
  return new User { Name = friendName };
}

对于这些方法中的每一个,我们都遵循了与Login方法完全相同的模式。每个方法都将延迟并返回一些示例数据。请随意用你自己的值混合这些数据。

现在,让我们按照以下方式实现接口所需的GetConversations方法:

public async Task<Conversation[]> GetConversations(
  string userName)
{
  await Sleep();
  return new[] 
  { 
    new Conversation { Id = "1", UserName = "bobama" },
    new Conversation { Id = "2", UserName = "bobloblaw" }, 
    new Conversation { Id = "3", UserName = "georgemichael" }, 
  };
}

基本上,我们只是创建了一个新的Conversation对象数组,这些对象的 ID 是任意的。我们还确保将UserName值与我们到目前为止在User对象上使用的值相匹配。

接下来,让我们按照以下方式实现GetMessages以获取消息列表:

public async Task<Message[]> GetMessages(string conversation) 
{ 
  await Sleep(); 

  return new[] 
  { 
    new Message 
    { 
      Id = "1", 
      Conversation = conversation, 
      UserName = "bobloblaw", 
      Text = "Hey", 
    }, 
    new Message 
    { 
      Id = "2", 
      Conversation = conversation, 
      UserName = "georgemichael", 
      Text = "What's Up?", 
    }, 
    new Message 
    { 
      Id = "3", 
      Conversation = conversation, 
      UserName = "bobloblaw", 
      Text = "Have you seen that new movie?", 
    }, 
    new Message 
    { 
      Id = "4", 
      Conversation = conversation, 
      UserName = "georgemichael", 
      Text = "It's great!", 
    }, 
  }; 
} 

再次,我们在这里添加了一些任意数据,主要确保UserIdConversationId与我们到目前为止的现有数据相匹配。

最后,我们将再编写一个如下所示发送消息的方法:

public async Task<Message> SendMessage(Message message) 
{ 
  await Sleep(); 

  return message; 
} 

这些方法中的大多数都非常直接。请注意,服务不必完美无缺;它应该只是在延迟后成功地完成每个操作。每个方法还应返回某种测试数据以在 UI 中显示。这将使我们能够在填充 Web 服务的同时实现我们的 iOS 和 Android 应用程序。

接下来,我们需要为持久化应用程序设置实现一个简单的接口。让我们按照以下方式定义一个名为ISettings的接口:

public interface ISettings 
{ 
  User User { get; set; } 

  void Save(); 
} 

我们正在使ISettings同步,但如果你计划将设置存储在云端,你可能想要将Save方法设置为异步并返回Task。由于我们的应用程序只会在本地保存设置,所以实际上我们并不需要这样做。

稍后,我们将在每个平台上使用 Android 和 iOS API 实现此接口。现在,让我们仅实现一个伪版本,稍后在编写单元测试时使用。使用以下代码行实现接口:

public class FakeSettings : ISettings 
{ 
  public User User { get; set; } 

  public void Save() { } 
} 

请注意,伪版本实际上不需要执行任何操作;我们只需要提供一个实现接口的类,并且不抛出任何意外的错误。

这完成了应用程序的模型层。以下是我们到目前为止实现的最终类图:

编写一个模拟网络服务

编写 ViewModel 层

既然我们已经实现了模型层,现在可以继续编写 ViewModel 层了。ViewModel 负责将每个操作呈现给 UI,并提供由视图层填充的属性。这一层的其他常见职责包括输入验证和显示忙碌指示器的简单逻辑。

在此阶段,将上一章中的ServiceContainer类包含到我们的XamSnap PCL 项目中会是一个好主意,因为我们将会通过 ViewModels 与模型层交互时使用它。我们将用它作为一个简单的选项来支持依赖注入和控制反转;然而,你也可以选择你偏好的另一个库来实现这一点。

通常,我们首先为项目中所有 ViewModel 层编写一个基类。这是一个放置所有子类使用的代码部分的好地方,例如:通知变更、方法或常用的接口。

在项目中的新ViewModels文件夹中放置以下代码片段:

public class BaseViewModel 
{ 
  protected readonly IWebService service = 
     ServiceContainer.Resolve<IWebService>(); 
  protected readonly ISettings settings = 
     ServiceContainer.Resolve<ISettings>(); 

  public event EventHandler IsBusyChanged = (sender, e) => { }; 

  private bool isBusy = false; 

  public bool IsBusy 
  { 
    get { return isBusy; } 
    set 
    { 
      isBusy = value; 
      IsBusyChanged(this, EventArgs.Empty); 
    } 
  } 
} 

BaseViewModel类是放置你打算在应用程序中重复使用的任何公共功能的好地方。对于这个应用,我们只需要实现一种方法来指示 ViewModel 层是否忙碌。我们提供了一个属性和一个事件,UI 将能够订阅并在屏幕上显示等待指示器。我们还添加了一些需要的服务字段。另一个可能添加的常见功能是对用户输入的验证;然而,这个应用程序并不真正需要它。

实现我们的 LoginViewModel 类

既然我们已经为所有的 ViewModel 层创建了一个基类,我们可以实现应用程序第一个屏幕的 ViewModel,即登录屏幕。

现在我们按照以下方式实现一个LoginViewModel类:

public class LoginViewModel : BaseViewModel 
{ 
  public string UserName { get; set; } 

  public string Password { get; set; } 

  public async Task Login() 
  { 
    if (string.IsNullOrEmpty(UserName)) 
      throw new Exception("Username is blank."); 

    if (string.IsNullOrEmpty(Password)) 
      throw new Exception("Password is blank."); 

    IsBusy = true; 
    try 
    { 
      settings.User = await service.Login(UserName, Password); 
      settings.Save(); 
    } 
    finally 
    { 
      IsBusy = false; 
    } 
  } 
} 

在这个类中,我们实现了以下功能:

  • 我们继承了BaseViewModel,以获取IsBusy和包含公共服务的字段

  • 我们添加了UserNamePassword属性,由视图层设置

  • 我们添加了一个User属性,以在登录过程完成后设置

  • 我们实现了一个从视图调用的Login方法,对UserNamePassword属性进行验证

  • 我们在调用IWebService上的Login方法期间设置IsBusy

  • 我们通过等待网络服务的Login方法的结果来设置User属性

基本上,这是我们将在应用程序的其余 ViewModel 中遵循的模式。我们为视图层提供由用户输入设置的属性,以及调用各种操作的方法。如果这是一个可能需要一些时间的方法,比如网络请求,你应当始终返回Task,并使用asyncawait关键字。

提示

请注意,我们使用了tryfinally块来将IsBusy设置回false。这将确保即使在抛出异常时也能正确重置。我们计划在 View 层处理错误,这样我们就可以向用户显示本地弹窗,并显示一条消息。

实现我们的 RegisterViewModel 类

既然我们已经完成了用于登录的ViewModel类的编写,我们现在需要创建一个用于用户注册的类。

让我们实现另一个 ViewModel 来注册新用户:

public class RegisterViewModel : BaseViewModel 
{ 
  public string UserName { get; set; } 

  public string Password { get; set; } 

  public string ConfirmPassword { get; set; } 
} 

这些属性将处理用户的输入。接下来,我们需要按照以下方式添加一个Register方法:

public async Task Register() 
{ 
  if (string.IsNullOrEmpty(UserName)) 
    throw new Exception("Username is blank."); 

  if (string.IsNullOrEmpty(Password)) 
    throw new Exception("Password is blank."); 

  if (Password != ConfirmPassword) 
    throw new Exception("Passwords do not match."); 

  IsBusy = true; 
  try 
  { 
    settings.User = await service.Register(new User  
    {  
      Name = UserName, 
      Password = Password,  
    }); 
    settings.Save(); 
  } 
  finally 
  { 
    IsBusy = false; 
  } 
} 

RegisterViewModel类与LoginViewModel类非常相似,但它增加了一个ConfirmPassword属性,以便 UI 设置。关于何时拆分 ViewModel 层的功能,一个好的规则是:当 UI 有新屏幕时,始终创建一个新类。这有助于保持代码整洁,并在一定程度上遵循类的单一职责原则(SRP)SRP指出,一个类应该只有一个目的或责任。我们将尝试遵循这一概念,使我们的类保持小而有序,这在跨平台共享代码时尤为重要。

实现我们的 FriendViewModel 类

接下来是处理用户朋友列表的 ViewModel 层。我们需要一个方法来加载用户的朋友列表并添加新朋友。

现在我们按照以下方式实现FriendViewModel

public class FriendViewModel : BaseViewModel 
{ 
  public User[] Friends { get; private set; } 

  public string UserName { get; set; } 
} 

现在我们需要一种加载朋友列表的方法。该方法如下:

public async Task GetFriends() 
{ 
  if (settings.User == null) 
    throw new Exception("Not logged in."); 

  IsBusy = true; 
  try 
  { 
    Friends = await service.GetFriends(settings.User.Name); 
  } 
  finally 
  { 
    IsBusy = false; 
  } 
} 

最后,我们需要一个添加新朋友并更新本地朋友列表的方法:

public async Task AddFriend()
{
  if (settings.User == null)
    throw new Exception("Not logged in.");
  if (string.IsNullOrEmpty(UserName))
    throw new Exception("Username is blank.");
  IsBusy = true; 

  try 
  { 
    var friend = await service
      .AddFriend(settings.User.Name, UserName); 
    //Update our local list of friends 
    var friends = new List<User>(); 
    if (Friends != null)
      friends.AddRange(Friends); 
    friends.Add(friend); 
    Friends =  friends.OrderBy(f => f.Name).ToArray(); 
  } 
  finally 
  { 
    IsBusy =  false; 
  }
}

同样,这个类相当直接。这里唯一的新东西是,我们添加了一些逻辑,在客户端应用程序中更新朋友列表并对其进行排序,而不是在服务器上。如果你有充足的理由,也可以选择重新加载整个朋友列表。

实现我们的 MessageViewModel 类

我们最终需要的 ViewModel 层将处理消息和对话。我们需要创建一种加载对话和消息的方法,并发送新消息。

让我们开始按照以下方式实现我们的MessageViewModel类:

public class MessageViewModel : BaseViewModel 
{ 
  public Conversation[] Conversations { get; private set; } 

  public Conversation Conversation { get; set; } 

  public Message[] Messages { get; private set; } 

  public string Text { get; set; } 
} 

接下来,让我们按照以下方式实现获取对话列表的方法:

public async Task GetConversations() 
{ 
  if (settings.User == null) 
    throw new Exception("Not logged in."); 

  IsBusy = true; 
  try 
  { 
    Conversations = await service
       .GetConversations(settings.User.Name); 
  } 
  finally 
  { 
    IsBusy = false; 
  } 
} 

同样,我们需要获取对话中的消息列表。我们需要将对话 ID 传递给服务,如下所示:

public async Task GetMessages() 
{ 
  if (Conversation == null) 
    throw new Exception("No conversation."); 

  IsBusy = true; 
  try 
  { 
    Messages = await service
       .GetMessages(Conversation.Id); 
  } 
  finally 
  { 
    IsBusy = false; 
  } 
} 

最后,我们需要编写一些代码来发送消息并更新本地消息列表,如下所示:

public async Task SendMessage() 
{ 
  if (settings.User == null) 
    throw new Exception("Not logged in."); 

  if (Conversation == null) 
    throw new Exception("No conversation."); 

  if (string.IsNullOrEmpty (Text)) 
    throw new Exception("Message is blank."); 

  IsBusy = true; 
  try 
  { 
    var message = await service.SendMessage(new Message  
    {  
        UserName = settings.User.Name,
         Conversation = Conversation.Id, 
        Text = Text 
    }); 

    //Update our local list of messages 
    var messages = new List<Message>(); 
    if (Messages != null) 
      messages.AddRange(Messages); 
    messages.Add(message); 

    Messages = messages.ToArray(); 
  } 
  finally 
  {
    IsBusy = false; 
  } 
} 

这结束了我们应用程序的 ViewModel 层以及 iOS 和 Android 上使用的所有共享代码。对于MessageViewModel类,你也可以选择将GetConversationsConversations属性放在它们自己的类中,因为它们可以被认为是一个单独的责任,但这并不是绝对必要的。

这是我们的 ViewModel 层的最终类图:

实现我们的 MessageViewModel 类

编写单元测试

由于我们迄今为止编写的所有代码都不依赖于用户界面,我们可以轻松地针对我们的类编写单元测试。这一步通常在ViewModel类的首次实现之后进行。测试驱动开发TDD)的倡导者会建议先编写测试,然后再实现功能,所以选择最适合你的方法。无论如何,在从视图层开始使用它们之前,针对共享代码编写测试是一个好主意,这样你可以在它们阻碍 UI 开发之前捕捉到错误。

Xamarin 项目利用了一个名为NUnit的开源测试框架。它最初源自一个名为JUnit的 Java 测试框架,是进行 C#应用程序单元测试的事实标准。Xamarin Studio 提供了几个使用NUnit编写测试的项目模板。

设置一个用于单元测试的新项目

让我们通过执行以下步骤为单元测试设置一个新项目:

  1. 在 Xamarin Studio 的其他 | .Net部分,向你的解决方案中添加一个新的NUnit 库项目。如果使用 Visual Studio,则创建一个.NET 类库并添加 NUnit NuGet 包。

  2. 将项目命名为XamSnap.Tests以保持一致性。

  3. 在项目引用上右键点击,选择编辑引用

  4. 项目选项卡下,向XamSnap添加一个引用,这是你现有的可移植类库。

  5. 现在,打开Test.cs文件,注意以下构成使用 NUnit 单元测试的必要属性:

  • using NUnit.Framework:这个属性是使用 NUnit 时要使用的主要语句。

  • [TestFixture]:这个属性装饰一个类,表示该类有一系列用于运行测试的方法。

  • [Test]:这个属性装饰一个方法,表示这是一个测试。

除了必要的 C#属性之外,还有其他几个在编写测试时很有用的属性,如下所示:

  • [TestFixtureSetUp]:这个属性装饰一个方法,该方法在测试固件类中包含的所有测试之前运行。

  • [SetUp]:这个属性装饰一个方法,该方法在测试固件类中的每个测试前运行。

  • [TearDown]:这个属性装饰一个方法,该方法在测试固件类中的每个测试后运行。

  • [TestFixtureTearDown]:这个属性装饰一个方法,该方法在测试固件类中的所有测试完成后运行。

  • [ExpectedException]:这个属性装饰一个预期会抛出异常的方法。它用于测试那些应该失败的用例。

  • [Category]:这个属性装饰一个测试方法,可以用来组织不同的测试;例如,你可能将快速测试和慢速测试进行分类。

编写断言

下一个要学习的概念是使用 NUnit 编写测试时如何编写断言。断言是一个方法,如果某个值不是真的,它将抛出一个异常。这将导致测试失败,并给出发生情况的描述性解释。NUnit 有几组不同的断言 API;然而,我们将使用更易读、更流畅的 API 版本。

流畅风格 API 的基本语法是使用 Assert.That 方法。以下示例展示了这一点:

Assert.That(myVariable, Is.EqualTo(0)); 

同样,你可以断言相反的情况:

Assert.That(myVariable, Is.Not.EqualTo(0)); 

或者以下任意一项:

  • Assert.That(myVariable, Is.GreaterThan(0));

  • Assert.That(myBooleanVariable, Is.True);

  • Assert.That(myObject, Is.Not.Null);

自由探索 APIs。在 Xamarin Studio 中,有了代码补全功能,你应该能够发现 Is 类中有用的静态成员或方法,以便在测试中使用。

在为我们应用程序编写特定的测试之前,让我们编写一个静态类和方法,以创建在整个测试中使用的全局设置;你可以将 Test.cs 重写如下:

public class BaseTest 
{ 
  [SetUp] 
  public virtual void SetUp() 
  { 
    ServiceContainer.Register<IWebService>(() =>
       new FakeWebService { SleepDuration = 0 }); 
    ServiceContainer.Register<ISettings>(() =>
       new FakeSettings()); 
  } 
} 

我们将在测试中使用此方法来设置模型层中的假服务。此外,这会替换现有的服务,以便我们的测试针对这些类的新实例执行。这是单元测试中的一个好习惯,以确保之前的测试没有留下旧数据。还要注意,我们将 SleepDuration 设置为 0。这将使我们的测试运行得非常快。

首先,在测试项目中创建一个名为 ViewModels 的文件夹,并添加一个名为 LoginViewModelTests 的类,如下所示:

[TestFixture] 
public class LoginViewModelTests : BaseTest 
{ 
  LoginViewModel loginViewModel; 
  ISettings settings; 

  [SetUp] 
  public override void SetUp() 
  { 
    base.SetUp(); 

    settings = ServiceContainer.Resolve<ISettings>(); 
    loginViewModel = new LoginViewModel(); 
  } 

  [Test] 
  public async Task LoginSuccessfully() 
  { 
    loginViewModel.UserName = "testuser"; 
    loginViewModel.Password = "password"; 

    await loginViewModel.Login(); 

    Assert.That(settings.User, Is.Not.Null); 
  } 
} 

注意我们使用了 SetUp 方法。我们重新创建每个测试中使用的对象,以确保之前的测试运行没有留下旧数据。另一点需要注意的是,当在测试方法中使用 async/await 时,你必须返回一个 Task。否则,NUnit 将无法知道测试何时完成。

要运行测试,请使用默认停靠在 Xamarin Studio 右侧的 NUnit 菜单。使用带有齿轮图标的运行测试按钮来运行测试;你应该会得到一个类似以下截图所示的成功结果:

编写断言

你还可以查看测试结果窗格,如果测试失败,它会显示扩展的详细信息;如下面的截图所示:

编写断言

提示

如果使用 Visual Studio,你将需要从 Visual Studio 库安装 NUnit 测试适配器 扩展。你可以在 工具 | 扩展和更新 菜单下找到此选项。Visual Studio 中的单元测试运行器与 Xamarin Studio 一样直观;然而,它默认只支持 MsTest。

要查看测试失败时会发生什么,请继续修改你的测试,按照以下方式针对错误值进行断言:

//Change Is.Not.Null to Is.Null 
Assert.That(settings.User, Is.Null); 

你会在测试结果窗格中得到一个非常详细的错误,如下面的截图所示:

编写断言

现在我们为LoginViewModel类实现另一个测试;确保如果用户名和密码为空,我们能得到适当的结果。测试实现如下:

[Test] 
public async Task LoginWithNoUsernameOrPassword() 
{ 
  //Throws an exception 
  await loginViewModel.Login(); 
} 

如果我们按原样运行测试,将会捕获到一个异常,测试将失败。由于我们预期会发生异常,我们可以通过以下方式装饰该方法,使得只有当异常发生时测试才能通过:

[Test,  
  ExpectedException(typeof(Exception),  
  ExpectedMessage = "Username is blank.")] 

提示

请注意,在我们的视图模型中,如果字段为空,则会抛出一个通用的Exception类型异常。在预期异常类型不同的情况下,你也可以更改预期异常的类型。

随书附带的示例代码中包含了更多测试。建议针对每个ViewModel类上的每个公共操作编写测试。此外,针对任何验证或其他重要的业务逻辑编写测试。我还建议针对模型层编写测试;然而,在我们的项目中还不需要,因为我们只有假的实现。

总结

在本章中,我们概述了一个示例应用程序的概念,这个应用程序将在整本书中构建,名为 XamSnap。我们还为应用程序在模型层实现了核心业务对象。由于我们还没有服务器来支持这个应用程序,我们实现了一个假的网络服务。这使得我们可以在不构建服务器应用程序的情况下继续开发应用程序。我们还实现了视图模型层。这一层将向视图层以简单的方式暴露操作。最后,我们使用 NUnit 编写了覆盖我们至今为止编写的代码的测试。在跨平台应用程序中对共享代码编写测试可能非常重要,因为它是多个应用程序的支柱。

在完成本章之后,你应该已经完整地完成了我们跨平台应用程序的共享库。你应该对应用程序的架构以及其独特的模型层和视图模型层有一个非常牢固的理解。你还应该了解如何编写应用程序部分可能还未能实现的假的版本。在下一章中,我们将实现 XamSnap 的 iOS 版本。

第五章:iOS 的 XamSnap

要开始编写 XamSnap 的 iOS 版本,请打开我们在上一章创建的解决方案。在本章中,我们主要在XamSnap.iOS项目中工作。项目模板将自动创建一个名为ViewController的控制器;请继续并删除它。我们将在进行中创建我们自己的控制器。

在本章中,我们将涵盖以下内容:

  • iOS 应用的基础知识

  • 使用 UINavigationController

  • 实现登录界面

  • Segues 和 UITableView

  • 添加好友列表

  • 添加消息列表

  • 编写消息

了解 iOS 应用的基础知识

在我们开始开发我们的应用程序之前,让我们回顾一下应用程序的主要设置。苹果使用一个名为Info.plist的文件来存储有关任何 iOS 应用的重要信息。这些设置由操作系统本身使用,以及当 iOS 应用程序通过苹果应用商店在设备上安装时。开始开发任何新的 iOS 应用程序,通过填写此文件中的信息。

Xamarin Studio 提供了一个整洁的菜单,用于修改Info.plist文件中的值,如下截图所示:

了解 iOS 应用的基础知识

最重要的设置如下:

  • 应用名称:这是 iOS 中应用图标下方的标题。请注意,这与你在 iOS 应用商店中的应用程序官方名称不同。

  • 包标识符:这是你的应用程序的包标识符或包 ID。这是一个独特的名称,用于识别你的应用程序。约定是使用以你的公司名称开头的反向域名命名风格,如com.jonathanpeppers.xamsnap

  • 版本:这是你的应用程序的版本号,用户在应用商店中可见,如1.0.0

  • 构建:这是为开发者保留的版本号(例如 CI 构建等),如1.0.0.1234

  • 设备:在这里,你可以为你的应用程序选择iPhone/iPodiPad通用(所有设备)。

  • 部署目标:这是你的应用程序运行的最低 iOS 版本。

  • 主界面:这是你的应用的主故事板文件。

  • 设备方向:这是你的应用程序能够旋转并支持的不同位置。

  • 状态栏样式:这些选项可以隐藏应用程序中的顶部状态栏,并全屏运行。

还有其他关于应用图标、启动屏幕等的设置。你也可以在高级标签之间切换,以配置 Xamarin 没有提供友好菜单的其他设置。

为我们的应用程序配置以下设置:

  • 应用名称XamSnap

  • 包标识符com.yourcompanyname.xamsnap;确保你为未来应用命名时,它们以com.yourcompanyname开头。

  • 设备iPhone/iPod

  • 部署目标8.0

  • 支持的设备方向:只选择纵向

Xamarin.iOS 构建选项

如果你右键点击你的项目并选择选项,你可以找到一些针对 Xamarin iOS 应用程序的附加设置,如下面的截图所示。了解在 Xamarin Studio 中为 iOS 特定项目提供了什么是一个好主意。这里有很多内容,但在大多数情况下,默认设置就足够了。

Xamarin.iOS 构建选项

让我们讨论一些最重要的选项,如下:

iOS 构建

  • SDK 版本:这是用于编译应用程序的 iOS SDK 版本。通常最好使用默认版本。

  • 链接器行为:Xamarin 实现了一个名为链接的功能。链接器将移除任何在你的程序集中永远不会调用的代码。这使你的应用程序保持小巧,并允许它们与你的应用程序一起发布核心 Mono 运行的简化版本。除了调试版本外,最好使用仅链接 SDK 程序集的选项。我们将在未来的章节中介绍链接。

  • 支持的架构:这些是处理器的类型。i386是模拟器,ARMv7 + ARM64是针对现代 iOS 设备编译的选项。你通常应该能够在这里使用默认设置,除非升级较旧的 Xamarin.iOS 应用程序。

  • HttpClient 实现:新版本的 Xamarin.iOS 允许你为System.Net.Http.HttpClient选择本地 HTTP 栈。Mono 的实现是默认的,但性能不如本地栈。

  • SSL/TLS 实现:Xamarin.iOS 也有使用本地 API 进行 SSL 的选项。如果你选择使用 Mono,你的应用程序将只支持 TLS 1.0,因此最好在这里使用本地选项。

  • 使用 LLVM 优化编译器:勾选此项将编译出体积更小、运行速度更快的代码,但编译时间会更长。LLVM代表低级虚拟机

  • 去除本地调试符号:当这个选项开启时,Xamarin 会从你的应用程序中移除额外的信息,这些信息可以从 Xamarin Studio 中进行调试。

  • 额外的 mtouch 参数:此字段用于传递给 iOS 的 Xamarin 编译器额外的命令行参数。你可以查看这些参数的完整列表在developer.xamarin.com/api

  • 针对 iOS 优化 PNG 文件:苹果使用自定义的 PNG 格式来加速应用程序内 PNG 的加载。你可以关闭此选项来加快构建速度,或者如果你打算自己优化图像。

iOS 打包签名

  • 签名标识:这是用于识别应用程序创建者并将应用程序部署到设备的证书。我们将在后面的章节中详细介绍这一点。

  • 配置文件:这是一个特定的配置文件,用于将应用程序部署到设备上。它与签名标识协同工作,同时声明分发方法和可以安装应用程序的设备。

  • 自定义权利:这个文件包含了与应用程序权利证明文件一起应用的附加设置,并包含了对应用程序的其他特定声明,比如 iCloud 或推送通知。iOS 应用程序的项目模板为新项目包含了一个默认的Entitlements.plist文件。

对于这个应用程序,你可以保留所有这些选项为默认值。在独自开发实际的 iOS 应用程序时,你应该根据应用程序的需求考虑更改这些设置。

使用UINavigationController

在 iOS 应用程序中,管理不同控制器间导航的关键类是UINavigationController。它是一个父控制器,包含了一个栈中的多个子控制器。用户可以通过在栈顶放置新的控制器来前进,或者使用内置的后退按钮移除控制器并导航回上一个屏幕。

开发者可以使用以下方法操作导航控制器的栈:

  • SetViewControllers:这个方法设置一个子控制器数组。它有一个可选值用来动画过渡。

  • ViewControllers:这是一个属性,用于获取或设置子控制器数组,但不提供动画选项。

  • PushViewController:这个方法将一个新的子控制器放置在栈顶,并可以选择显示动画。

  • PopViewController:这个方法会移除栈顶的子控制器,并可以选择是否动画过渡。

  • PopToViewController:这个方法移除到指定的子控制器,移除其上的所有控制器。它提供了一个动画过渡的选项。

  • PopToRootViewController:这个方法移除除了最底部的控制器之外的所有子控制器。它包括一个显示动画的选项。

  • TopViewController:这是一个属性,返回当前位于栈顶的子控制器。

提示

需要注意的是,如果在动画过程中尝试修改栈,使用动画选项将会导致崩溃。要解决这个问题,可以选择使用SetViewControllers方法并设置整个子控制器列表,或者在组合过渡期间避免使用动画。

让我们通过执行以下步骤,在应用程序中设置导航控制器:

  1. 双击Main.storyboard文件,在 Xamarin Studio 中打开它。

  2. 移除由项目模板创建的控制器。

  3. 从右侧的工具箱中拖动一个导航控制器元素到故事板中。

  4. 注意,已经创建了一个默认的视图控制器元素以及一个导航控制器

  5. 你会看到一个连接两个控制器的segue。我们将在本章后面更详细地介绍这个概念。

  6. 保存故事板文件。

提示

对于 Visual Studio 用户的一个小提示,Xamarin 已经很好地使他们的 Visual Studio 扩展与 Xamarin Studio 完全相同。本章中的所有示例都应如描述的那样在 Xamarin Studio on OS X 或 Windows 上的 Visual Studio 中工作。当然,远程连接的 mac 部署到模拟器或 iOS 设备是一个例外。

如果此时运行应用程序,你将得到一个基本的 iOS 应用,它有一个顶部的状态栏,一个包含默认标题的导航栏的导航控制器,以及一个完全白色的子控制器,如下面的截图所示:

使用 UINavigationController

实现登录界面

由于我们应用程序的第一个屏幕将是登录屏幕,因此让我们从在故事板文件中设置适当的视图开始。我们将使用 Xamarin Studio 编写 C#代码实现登录屏幕,并使用其 iOS 设计师在故事板文件中创建 iOS 布局。

返回 Xamarin Studio 中的项目,并执行以下步骤:

  1. 双击Main.storyboard文件,在 iOS 设计师中打开它。

  2. 选择你的视图控制器,点击属性窗格并选择小部件标签页。

  3. 字段中输入LoginController

  4. 注意到为你生成了LoginController类。如果你愿意,可以创建一个Controllers文件夹并将文件移到其中。

以下截图显示了在 Xamarin Studio 中进行更改后控制器设置的样子:

实现登录界面

现在让我们通过执行以下步骤来修改控制器的布局:

  1. 再次双击Main.storyboard文件返回到 iOS 设计师。

  2. 点击导航栏并编辑标题字段,将其改为Login

  3. 将两个文本字段拖到控制器上。适当地为用户名和密码输入定位和调整它们的大小。你可能还想删除默认文本以使字段为空。

  4. 对于第二个字段,勾选安全文本输入复选框。这将设置控件隐藏密码字段的字符。

  5. 你可能还想为UsernamePassword填写占位符字段。

  6. 将一个按钮拖到控制器上。将按钮的标题设置为Login

  7. 将一个活动指示器拖到控制器上。勾选动画隐藏复选框。

  8. 接下来,通过填写名称字段为每个控件创建出口。分别为这些出口命名为usernamepasswordloginindicator

  9. 保存故事板文件,查看LoginController.designer.cs

你会注意到 Xamarin Studio 已经为每个出口生成了属性:

实现登录界面

去编译应用程序,确保一切正常。在这一点上,我们还需要添加对前一章创建的XamSnap.Core项目的引用。

然后,让我们设置 iOS 应用程序以注册其所有视图模型以及其他将在整个应用程序中使用的服务。我们将使用在第四章,XamSnap - 一个跨平台应用程序中创建的ServiceContainer类来设置我们应用程序中的依赖关系。打开AppDelegate.cs并添加以下方法:

public override bool FinishedLaunching(
   UIApplication application,
   NSDictionary launchOptions) 
{ 
  //View Models 
  ServiceContainer.Register<LoginViewModel>(() =>
     new LoginViewModel()); 
  ServiceContainer.Register<FriendViewModel>(() =>
     new FriendViewModel()); 
  ServiceContainer.Register<RegisterViewModel>(() =>
     new RegisterViewModel()); 
  ServiceContainer.Register<MessageViewModel>(() =>
     new MessageViewModel()); 

  //Models 
  ServiceContainer.Register<ISettings>(() =>
     new FakeSettings()); 
  ServiceContainer.Register<IWebService>(() =>
     new FakeWebService()); 

  return true; 
} 

在后续操作中,我们将用真实的服务替换假服务。现在让我们在LoginController.cs中添加登录功能。首先在类顶部将LoginViewModel添加到成员变量中,如下所示:

readonly LoginViewModel loginViewModel =
   ServiceContainer.Resolve<LoginViewModel>(); 

这会将LoginViewModel的共享实例拉入控制器中的局部变量。这是我们将在整本书中使用的模式,以便将共享视图模型从一个类传递到另一个类。

接下来,重写ViewDidLoad以将视图模型的功能与在 outlets 中设置好的视图连接起来,如下所示:

public override void ViewDidLoad() 
{ 
  base.ViewDidLoad(); 

  login.TouchUpInside += async(sender, e) => 
  { 
    loginViewModel.UserName = username.Text; 
    loginViewModel.Password = password.Text; 

    try 
    { 
      await loginViewModel.Login(); 

      //TODO: navigate to a new screen 
    } 
    catch (Exception exc) 
    { 
      new UIAlertView("Oops!", exc.Message, null, "Ok").Show(); 
    } 
  }; 
} 

我们将在本章后面添加代码以导航到一个新屏幕。

接下来,让我们将IsBusyChanged事件实际连接起来以执行一个操作,如下所示:

public override void ViewWillAppear(bool animated) 
{ 
  base.ViewWillAppear(animated); 

  loginViewModel.IsBusyChanged += OnIsBusyChanged; 
} 

public override void ViewWillDisappear(bool animated) 
{ 
  base.ViewWillDisappear(animated); 

  loginViewModel.IsBusyChanged -= OnIsBusyChanged; 
} 

void OnIsBusyChanged(object sender, EventArgs e) 
{ 
  username.Enabled = 
    password.Enabled = 
    login.Enabled =  
    indicator.Hidden = !loginViewModel.IsBusy; 
} 

现在,你可能会问为什么我们要以这种方式订阅事件。问题是LoginViewModel类将贯穿应用程序的整个生命周期,而LoginController类则不会。如果我们只在ViewDidLoad中订阅事件,但稍后没有取消订阅,那么我们的应用程序将会有内存泄漏。我们还避免了使用 lambda 表达式作为事件,因为否则将无法取消订阅该事件。

请注意,我们不会遇到按钮上的TouchUpInside事件相同的问题,因为它将和控制器一样长时间存在于内存中。这是 C#中事件的一个常见问题,这就是为什么在 iOS 上使用前面的模式是一个好主意。

如果你现在运行应用程序,你应该能够输入用户名和密码,如下面的截图所示。按下登录后,你应该看到指示器出现,所有控件被禁用。你的应用程序将正确调用共享代码,并且在我们添加一个真实的网络服务时应该能正确运行。

实现登录界面

使用 segue 进行导航

Segue 是从一个控制器到另一个控制器的过渡。同样,一个故事板文件是连接在一起的控制器和它们的视图的集合,通过 segue 进行连接。这反过来又允许你同时查看每个控制器的布局和应用程序的一般流程。

有几种类型的 segue,如下所示:

  • 推送:在导航控制器内使用。它将一个新的控制器推送到导航控制器堆栈的顶部。推送使用导航控制器的标准动画技术,通常是最常用的过渡方式。

  • 关系:用于为另一个控制器设置子控制器。例如,导航控制器的根控制器,容器视图,或者在 iPad 应用程序中的分割视图控制器。

  • 模态:使用此方式时,以模态方式呈现的控制器将出现在父控制器的顶部。它将覆盖整个屏幕,直到被关闭。有几种不同类型的过渡动画可供选择。

  • 自定义:这是一种自定义的过渡,包括一个选项,用于自定义类,该类是UIStoryboardSegue的子类。这使你可以细致地控制动画以及下一个控制器的呈现方式。

过渡在执行时也遵循以下模式:

  • 目的地控制器及其视图被创建。

  • 创建一个UIStoryboardSegue的子类的过渡对象。这对于自定义过渡通常很重要。

  • 在源控制器上调用PrepareForSegue方法。在过渡开始之前,这是一个运行任何自定义代码的好地方。

  • 过渡的Perform方法被调用,过渡动画开始。这是自定义过渡的大部分代码所在的地方。

在 Xamarin.iOS 设计师中,你有从按钮或表格行自动触发过渡的选择,或者只是给过渡一个标识符。在第二种情况下,你可以通过使用其标识符在源控制器上调用PerformSegue方法来自己启动过渡。

现在让我们通过执行以下步骤设置一些Main.storyboard文件的方面,来设置一个新的过渡:

  1. 双击Main.storyboard文件,在 iOS 设计师中打开它。

  2. 向故事板中添加一个新的表格视图控制器

  3. 选择你的视图控制器,并导航到属性窗格和小部件标签。

  4. 字段中输入ConversationsController

  5. 视图控制器部分向下滚动,并输入一个标题Conversations

  6. 通过按住Ctrl点击并从LoginController拖动蓝线到ConversationsController,创建一个过渡。

  7. 从出现的弹出菜单中选择显示过渡。

  8. 通过点击选择此过渡,并为其分配一个标识符OnLogin

  9. 保存故事板文件。

你的故事板将与下面截图所示的内容类似:

使用过渡进行导航

打开LoginController.cs文件,并按照本章早些时候标记为TODO的代码行进行修改,如下所示:

PerformSegue("OnLogin", this); 

现在如果你构建并运行应用程序,成功登录后你将导航到新的控制器。过渡将被执行,你将看到导航控制器提供的内置动画。

设置 UITableView

接下来,让我们在第二个控制器上设置表格视图。我们在 iOS 上使用了一个强大的类,叫做 UITableView。它被用在许多场景中,并且与其他平台上列表视图的概念非常相似。UITableView 类由另一个叫做 UITableViewSource 的类控制。它有你需要重写的方法,以设置应该存在多少行以及这些行应该如何在屏幕上显示。

提示

注意 UITableViewSourceUITableViewDelegateUITableViewDataSource 的组合。出于简单考虑,我更喜欢使用 UITableViewSource,因为通常需要使用另外两个类。

在我们开始编码之前,让我们回顾一下在 UITableViewSource 上最常用的方法,如下:

  • RowsInSection:这个方法允许你定义一个部分中的行数。所有表格视图都有多个部分和行。默认情况下,只有一个部分;然而,需要返回一个部分中的行数。

  • NumberOfSections:这是表格视图中的部分数。

  • GetCell:这个方法必须为每一行返回一个单元格。开发者需要决定单元格的外观;你可以设置表格视图来回收单元格。回收单元格可以在滚动时提供更好的性能。

  • TitleForHeader:如果重写这个方法,它是最简单的返回标题字符串的方式。表格视图中的每个部分默认都可以有一个标准的头部视图。

  • RowSelected:当用户选择一行时,将调用此方法。

还有其他可以重写的方法,但大多数情况下这些方法就足够了。如果需要开发具有自定义样式的表格视图,你还可以设置自定义的头部和底部。

现在,让我们打开 ConversationsController.cs 文件,并在 ConversationsController 内部创建一个嵌套类,如下:

class TableSource : UITableViewSource 
{ 
  const string CellName = "ConversationCell"; 
  readonly MessageViewModel messageViewModel =
     ServiceContainer.Resolve<MessageViewModel>(); 

  public override nint RowsInSection(
     UITableView tableview, nint section) 
  { 
    return messageViewModel.Conversations == null ?
       0 : messageViewModel.Conversations.Length; 
  } 

  public override UITableViewCell GetCell(
     UITableView tableView, NSIndexPath indexPath) 
  { 
    var conversation =
       messageViewModel.Conversations[indexPath.Row]; 
    var cell = tableView.DequeueReusableCell(CellName); 
    if (cell == null) 
    { 
      cell = new UITableViewCell(
         UITableViewCellStyle.Default, CellName); 
      cell.Accessory =
         UITableViewCellAccessory.DisclosureIndicator; 
    } 
    cell.TextLabel.Text = conversation.UserName; 
    return cell; 
  } 
} 

我们实现了设置表格视图所需的两个方法:RowsInSectionGetCell。我们返回了视图模型中找到的对话数量,并为每一行设置了我们的单元格。我们还使用了 UITableViewCellAccessory.DisclosureIndicator,以便用户可以看到他们可以点击行。

注意我们实现的单元格回收。使用单元格标识符调用 DequeueReusableCell 会在第一次返回一个 null 单元格。如果为 null,你应该使用相同的单元格标识符创建一个新的单元格。后续调用 DequeueReusableCell 将返回一个现有的单元格,使你能够复用它。你也可以在故事板文件中定义 TableView 单元格,这对于自定义单元格很有用。我们的单元格这里非常简单,所以从代码中定义它更容易。在移动平台上回收单元格对于节省内存和为用户提供流畅的滚动表格非常重要。

接下来,我们需要在 TableView 上设置 TableView 的数据源。对我们的 ConversationsController 类进行以下一些更改:

readonly MessageViewModel messageViewModel = 
  ServiceContainer.Resolve<MessageViewModel>(); 

public override void ViewDidLoad() 
{ 
  base.ViewDidLoad(); 

  TableView.Source = new TableSource(); 
} 

public async override void ViewWillAppear(bool animated) 
{ 
  base.ViewWillAppear(animated); 

  try 
  { 
    await messageViewModel.GetConversations(); 

    TableView.ReloadData(); 
  } 
  catch(Exception exc) 
  { 
    new UIAlertView("Oops!", exc.Message, null, "Ok").Show(); 
  } 
} 

因此,当视图出现时,我们将加载我们的对话列表。在完成该任务后,我们将重新加载表格视图,使其显示我们的对话列表。如果你运行应用程序,你会在登录后看到表格视图中出现一些对话,如下面的截图所示。以后当我们从真正的网络服务加载对话时,一切都会以同样的方式运行。

设置 UITableView

添加好友列表屏幕

我们 XamSnap 应用程序下一个需要的屏幕是我们的好友列表。当创建新对话时,应用程序将加载好友列表以开始对话。我们将遵循一个非常相似的模式来加载我们的对话列表。

首先,我们将通过以下步骤创建一个UIBarButtonItem,它导航到一个名为FriendsController的新控制器:

  1. 双击Main.storyboard文件,在 iOS 设计师中打开它。

  2. 向故事板中添加一个新的表格视图控制器

  3. 选择你的视图控制器,点击属性窗格,确保你选择了控件标签页。

  4. 字段中输入FriendsController

  5. 滚动到视图控制器部分,在标题字段中输入Friends

  6. 工具箱中拖动一个导航项ConversationsController上。

  7. 创建一个新的工具栏按钮元素,并将其放置在新导航栏的右上角。

  8. 在工具栏按钮的属性窗格中,将其标识符设置为添加。这将使用内置的加号按钮,这在 iOS 应用程序中是常用的。

  9. 通过按住Ctrl键,并将蓝色线条从工具栏按钮拖动到下一个控制器,创建一个从工具栏按钮FriendsController的 segue。

  10. 从弹出的菜单中选择显示segue。

  11. 保存故事板文件。

你对故事板的更改应该与以下截图所示类似:

添加好友列表屏幕

你应该会看到一个名为FriendsController的新类,这是 Xamarin Studio 为你生成的。如果你编译并运行应用程序,你会看到我们创建的新工具栏按钮。点击它将导航到新的控制器。

现在,让我们实现UITableViewSource来展示我们的好友列表。首先在FriendsController内部创建一个新的嵌套类,如下所示:

class TableSource : UITableViewSource 
{ 
  const string CellName = "FriendCell"; 
  readonly FriendViewModel friendViewModel =
     ServiceContainer.Resolve<FriendViewModel>(); 

  public override nint RowsInSection(
     UITableView tableview, nint section) 
  { 
    return friendViewModel.Friends == null ?
       0 : friendViewModel.Friends.Length; 
  } 

  public override UITableViewCell GetCell(
     UITableView tableView, NSIndexPath indexPath) 
  { 
    var friend =
       friendViewModel.Friends[indexPath.Row]; 
    var cell = tableView.DequeueReusableCell(CellName); 
    if (cell == null) 
    { 
      cell = new UITableViewCell(
         UITableViewCellStyle.Default, CellName); 
      cell.AccessoryView =
         UIButton.FromType(UIButtonType.ContactAdd); 
      cell.AccessoryView.UserInteractionEnabled = false; 
    } 
    cell.TextLabel.Text = friend.Name; 
    return cell; 
  } 
} 

正如之前所做,我们实现了表格单元格的回收利用,并为每个好友的标签设置了文本。我们使用cell.AccessoryView来提示用户每个单元格都是可点击的,并开始新的对话。我们在按钮上禁用了用户交互,以便当用户点击按钮时,可以选中行。否则,我们就必须为按钮实现一个点击事件。

接下来,我们将按照对话的方式修改FriendsController,如下所示:

readonly FriendViewModel friendViewModel =
   ServiceContainer.Resolve<FriendViewModel>(); 

public override void ViewDidLoad() 
{ 
  base.ViewDidLoad(); 

  TableView.Source = new TableSource(); 
} 

public async override void ViewWillAppear(bool animated) 
{ 
  base.ViewWillAppear(animated); 

  try 
  { 
    await friendViewModel.GetFriends(); 

    TableView.ReloadData(); 
  } 
  catch(Exception exc) 
  { 
    new UIAlertView("Oops!", exc.Message, null, "Ok").Show(); 
  } 
} 

这将和对话列表完全一样:控制器将异步加载朋友列表并刷新表格视图。如果你编译并运行应用程序,你将能够导航到屏幕并查看我们在第四章,XamSnap - 跨平台应用程序中创建的示例朋友列表,如下截图所示:

添加朋友列表屏幕

添加消息列表

现在我们来实现查看对话或消息列表的屏幕。我们将尝试模仿 iOS 内置的短信应用程序的屏幕。为此,我们还将介绍如何创建自定义表格视图单元格的基础知识。

首先,我们需要一个新的MessagesController类;执行以下步骤:

  1. 双击Main.storyboard文件,在 iOS 设计师中打开它。

  2. 向故事板中添加一个新的表格视图控制器

  3. 选择你的视图控制器,点击属性窗格,确保你选择了小部件标签。

  4. 字段中输入MessagesController

  5. 滚动到视图控制器部分,在标题字段中输入Messages

  6. 通过按住Ctrl并将蓝色线条从ConversationsController拖到MessagesController,创建一个 segue。

  7. 从弹出的菜单中选择显示segue。在属性窗格中输入标识符 OnConversation

  8. 现在在MessagesController中的表格视图中创建两个表格视图单元格。你可以重复使用默认创建的现有空白单元格。

  9. 将每个单元格的样式字段更改为Basic

  10. 分别为每个单元格将标识符设置为MyCellTheirCell

  11. 保存故事板文件。

Xamarin Studio 将生成MessagesController.cs。和之前一样,你可以将控制器移动到Controllers文件夹中。现在打开MessagesController.cs,并在嵌套类中实现UITableViewSource,如下所示:

class TableSource : UITableViewSource
{
  const string MyCellName = "MyCell";
  const string TheirCellName = "TheirCell";
  readonly MessageViewModel messageViewModel =
    ServiceContainer.Resolve();
  readonly ISettings settings = ServiceContainer.Resolve();

  public override nint RowsInSection(
    UITableView tableview, nint section)
  {
    return messageViewModel.Messages == null ? 0 :
      messageViewModel.Messages.Length;
  }

  public override UITableViewCell GetCell(
    UITableView tableView, NSIndexPath indexPath)
  {
    var message = messageViewModel.Messages [indexPath.Row];
    bool isMyMessage = message.UserName == settings.User.Name;
    var cell = (BaseMessageCell)tableView.DequeueReusableCell(
      isMyMessage ? MyCellName : TheirCellName);
    cell.TextLabel.Text = message.Text;
    return cell;
  }
}

我们添加了一些逻辑,以检查消息是否来自当前用户,以决定适当的表格单元格标识符。由于我们为两个单元格都使用了Basic样式,我们可以使用单元格上的TextLabel属性来设置UILabel的文本。

现在我们对MessagesController进行必要的更改,如下所示:

readonly MessageViewModel messageViewModel = 
  ServiceContainer.Resolve<MessageViewModel>(); 

public override void ViewDidLoad() 
{ 
  base.ViewDidLoad(); 

  TableView.Source = new TableSource(); 
} 

public async override void ViewWillAppear(bool animated) 
{ 
  base.ViewWillAppear(animated); 

  Title = messageViewModel.Conversation.UserName; 
  try 
  { 
    await messageViewModel.GetMessages(); 
    TableView.ReloadData(); 
  } 
  catch (Exception exc) 
  { 
    new UIAlertView("Oops!", exc.Message, null, "Ok").Show(); 
  } 
} 

这里的唯一新事物是我们将Title属性设置为对话的用户名。

为了完成我们的自定义单元格,我们还需要在 Xcode 中进行以下步骤进行更多更改:

  1. 双击Main.storyboard文件,在 iOS 设计师中打开它。

  2. 通过点击默认文本Title,选择一个标签

  3. 创造性地为两个标签设置样式。我选择使MyCell中的文本为蓝色,TheirCell为绿色。我将TheirCell中的标签对齐设置为右对齐。

  4. 保存故事板文件并返回。

接下来,我们需要更新ConversationsController以导航到这个新屏幕。让我们修改ConversationsController.cs中的TableSource类,如下所示:

readonly ConversationsController controller; 

public TableSource(ConversationsController controller) 
{ 
  this.controller = controller;
}

public override void RowSelected(
  UITableView tableView, NSIndexPath indexPath)
{ 
  var conversation = messageViewModel.Conversations[indexPath.Row]; 
  messageViewModel.Conversation = conversation; 
  controller.PerformSegue("OnConversation", this); 
}

当然,你还需要在控制器中的ViewDidLoad修改一行小代码:

TableView.Source = new TableSource(this); 

如果你现在运行应用程序,你将能够看到如下截图所示的消息列表:

添加消息列表

编写消息

为了我们应用程序的最后一块,我们需要实现一些苹果公司 API 不提供的自定义功能。我们需要添加一个带有按钮的文本字段,使其看起来附着在表格视图的底部。其中大部分工作需要编写一些简单的 C#代码并连接事件。

首先,我们在MessagesController类中添加一些新的成员变量,如下所示:

UIToolbar toolbar; 
UITextField message; 
UIBarButtonItem send; 

我们将在工具栏中放置文本字段和工具栏按钮,如下面的ViewDidLoad中的代码所示:

public override void ViewDidLoad() 
{ 
  base.ViewDidLoad(); 

  //Text Field 
  message = new UITextField( 
    new CGRect(0, 0, TableView.Frame.Width - 88, 32)) 
  { 
    BorderStyle = UITextBorderStyle.RoundedRect, 
    ReturnKeyType = UIReturnKeyType.Send, 
    ShouldReturn = _ => 
    { 
        Send(); 
        return false; 
    }, 
  }; 

  //Bar button item 
  send = new UIBarButtonItem("Send", UIBarButtonItemStyle.Plain, 
    (sender, e) => Send()); 

  //Toolbar 
  toolbar = new UIToolbar( 
    new CGRect(0, TableView.Frame.Height - 44,  
      TableView.Frame.Width, 44)); 
  toolbar.Items = new[] 
  { 
    new UIBarButtonItem(message), 
    send 
  }; 

  TableView.Source = new TableSource(); 
  TableView.TableFooterView = toolbar; 
} 

这项工作大部分是基本的 UI 设置。这不是我们在 Xcode 中能做的事情,因为这是一个非常特定的用例。我们从 C#创建文本字段、工具栏按钮项,并将它们作为UITableView的页脚添加。这将使工具栏显示在我们之前定义的任何行下面的表格视图底部。

现在,我们需要按照以下方式修改ViewWillAppear

public async override void ViewWillAppear(bool animated) 
{ 
  base.ViewWillAppear(animated); 

  Title = messageViewModel.Conversation.Username; 

  messageViewModel.IsBusyChanged += OnIsBusyChanged; 

  try 
  { 
    await messageViewModel.GetMessages(); 
    TableView.ReloadData(); 
    message.BecomeFirstResponder(); 
  } 
  catch (Exception exc) 
  { 
    new UIAlertView("Oops!", exc.Message, null, "Ok").Show(); 
  } 
} 

我们需要订阅IsBusyChanged以显示和隐藏加载指示器。同时我们调用BecomeFirstResponder,这样键盘就会出现并将焦点给予我们的文本字段。

接下来,我们为ViewWillDisapper添加一个重写方法,以清理事件,如下所示:

public override void ViewWillDisappear(bool animated) 
{ 
  base.ViewWillDisappear(animated); 

  messageViewModel.IsBusyChanged -= OnIsBusyChanged; 
} 

然后,让我们为IsBusyChanged设置方法,如下所示:

void OnIsBusyChanged (object sender, EventArgs e) 
{ 
  message.Enabled = send.Enabled = !messageViewModel.IsBusy; 
} 

OnIsBusyChanged用于在加载时禁用我们的一些视图。

最后但并非最不重要的是,我们需要实现一个发送新消息的函数,如下所示:

async void Send() 
{ 
  //Just hide the keyboard if they didn't type anything 
  if (string.IsNullOrEmpty(message.Text)) 
  { 
    message.ResignFirstResponder(); 
    return; 
  } 

  //Set the text, send the message 
  messageViewModel.Text = message.Text; 
  await messageViewModel.SendMessage(); 

  //Clear the text field & view model 
  message.Text = messageViewModel.Text = string.Empty; 

  //Reload the table 
  TableView.InsertRows(new[]  
  {  
    NSIndexPath.FromRowSection( 
      messageViewModel.Messages.Length - 1, 0)  
  }, UITableViewRowAnimation.Automatic); 
} 

这段代码同样直接明了。发送消息后,我们只需清空文本字段并告诉表格视图重新加载新添加的行,如下面的截图所示。使用async关键字使这变得简单。

编写消息

概要

在本章中,我们介绍了苹果和 Xamarin 为开发 iOS 应用程序提供的基本设置。这包括Info.plist文件和 Xamarin Studio 中的项目选项。我们涵盖了UINavigationController,这是 iOS 应用程序导航的基本构建块,并实现了一个带有用户名和密码字段的登录屏幕。接下来,我们介绍了 iOS 的 segue 和UITableView类。我们使用UITableView实现了好友列表屏幕,以及消息列表屏幕。最后,我们添加了一个自定义 UI 功能:在消息列表底部的自定义工具栏。

完成本章节后,你将拥有一个部分功能性的 XamSnap 的 iOS 版本。你将对 iOS 平台和工具有一个更深入的理解,并且拥有足够的知识去开发你自己的 iOS 应用程序。请自行实现本章未涵盖的其余屏幕。如果你感到困惑,可以随时回顾本书附带的完整示例应用程序。

在下一章中,我们将实现在 Android 上的这些用户界面。

第六章:XamSnap for Android

要开始编写 XamSnap 的 Android 版本,请打开前两章的解决方案。我们将要在 XamSnap.Droid 项目中工作,该项目应该已经从 Xamarin 项目模板中设置好了。

在本章中,我们将涵盖:

  • Android 清单文件

  • Android 材料设计

  • 为 XamSnap 编写登录界面

  • Android 的 ListView 和 BaseAdapter

  • 添加好友列表

  • 添加消息列表

介绍 Android 清单文件

所有 Android 应用程序都有一个名为 Android Manifest 的 XML 文件,它声明了关于应用程序的基本信息,文件名为 AndroidManifest.xml。这非常类似于 iOS 上的 Info.plist 文件,但 Xamarin 还提供了 C# 类属性,用于在 Android 清单中放置常见设置。在 项目选项 | Android 应用程序 下还有一个很好的 UI 用于编辑清单文件。

最重要的设置,如下截图所示,如下:

  • 应用程序名称:这是你的应用程序的标题,显示在图标下方。它与在 Google Play 上选择的名称不同。

  • 包名:这就像 iOS 上的应用程序捆绑标识符。这是一个唯一的名字来标识你的应用程序。约定是使用以你的公司名称开头的反向域名风格;例如,com.jonathanpeppers.xamsnap。它必须以小写字母开头并至少包含一个字符。

  • 应用程序图标:这是你的应用程序在 Android 主屏幕上显示的图标。

  • 版本号:这是一个数字,表示你的应用程序的版本。提高这个数字表示在 Google Play 上有更新的版本。

  • 版本名称:这是你应用程序的用户友好版本字符串;例如,1.0.0

  • 最低支持的 Android 版本:这是你的应用程序支持的最低版本的 Android。

  • 目标 Android 版本:这是你的应用程序编译时使用的 Android SDK 的版本。使用更高的版本号可以让你访问新的 API;然而,你可能需要进行一些运行时检查,以免在旧设备上调用这些 API。

  • 安装位置:这定义了你的 Android 应用程序可以安装的不同位置:自动(用户设置)、外部(SD 卡)或内部(设备内部存储)。

介绍 Android 清单文件

除了这些设置,还有一组名为所需权限的复选框。这些将在用户在 Google Play 安装应用程序之前向用户展示。这是 Android 强制实施安全级别的方式,让用户可以看到应用程序将对设备进行哪些更改的访问权限。

以下是一些常用的清单文件权限:

  • Camera:这提供了对设备相机的访问权限

  • 互联网:这提供了通过互联网进行网络请求的访问权限

  • ReadContacts:这提供了读取设备联系人库的访问权限

  • ReadExternalStorage:这提供了读取 SD 卡的权限

  • WriteContacts:这提供了修改设备联系人库的权限

  • WriteExternalStorage:这提供了向 SD 卡写入的权限

除了这些设置之外,很多时候还需要手动更改 Android Manifest。在这种情况下,你可以在 Xamarin Studio 中像编辑标准的 XML 文件一样编辑清单文件。有关有效的 XML 元素和属性完整列表,请访问developer.android.com/guide/topics/manifest/manifest-intro.html

现在,让我们为我们的应用程序填写以下设置:

  • 应用程序名称XamSnap

  • 包名称com.yourcompanyname.xamsnap;确保将来命名的应用程序以com.yourcompanyname开头

  • 版本号:从数字1开始

  • 版本:可以是任何字符串,但建议使用类似版本号的字符串

  • 最低 Android 版本:选择Android 4.0.3 (API Level 15)

  • 所需权限:选择Internet;我们稍后会用到它

在这一点上,请注意我们的 Android 项目已经引用了来自便携式类库的共享代码。展开项目的引用文件夹,注意对XamSnap.Core项目的引用。我们将能够访问在第四章XamSnap - A Cross-Platform App中编写的所有共享代码。

前往Resources目录,在values文件夹中打开Strings.xml;这是你整个 Android 应用中应存储所有文本的地方。这是 Android 的一个约定,它将使你非常容易地为应用程序添加多种语言。让我们将我们的字符串更改为以下内容:

<?xml version="1.0" encoding="utf-8"?> 
<resources> 
    <string name="ApplicationName">XamSnap</string> 
    <string name="ErrorTitle">Oops!</string> 
    <string name="Loading">Loading</string> 
    <string name="Login">Login</string> 
</resources> 

我们将在本章后面使用这些值;在需要向用户显示文本的情况下,可以自由添加新的值。

设置 Material Design

从 Android 5.0 Lollipop 开始,谷歌发布了一个名为Material Design的新主题和颜色调色板,用于 Android 应用程序。对于新应用来说,采用 Material Design 是一个好主意,因为它可以让你轻松设置现代 Android 的外观。有关 Material Design 的更多信息,请查看谷歌的文档:developer.android.com/design/material/index.html

为了让 Material Design(和其他新的 Android 功能)更容易被采用,谷歌还发布了一个名为AppCompat的 Android 库,因此你可以支持在较旧的 Android OS 版本上的这些新功能。Xamarin 在 NuGet 上支持 AppCompat 库的一个版本,以便于 Xamarin.Android 应用程序轻松设置。

要设置 Android 支持库,请按照以下步骤操作:

  1. 右键点击并选择添加包

  2. 搜索Xamarin.Android.Support.v7.AppCompat

  3. 点击添加包

  4. NuGet 将下载库及其依赖项,并在你的 Android 项目中引用它们。

现在让我们实现我们的主应用程序类;从新建文件对话框中添加一个新的Activity。在这个文件中,我们不会继承Activity,但这个模板在文件顶部添加了几个 Android using语句,导入可以在代码中使用的 Android API。创建一个新的Application类,我们可以在其中注册ServiceContainer中的所有内容,如下所示:

[Application(Theme = "@style/Theme.AppCompat.Light")] 
public class Application : Android.App.Application 
{ 
  public Application(
     IntPtr javaReference, JniHandleOwnership transfer)
     : base(javaReference, transfer) 
  {  
  } 

  public override void OnCreate() 
  { 
    base.OnCreate(); 

    //ViewModels 
    ServiceContainer.Register<LoginViewModel>(
       () => new LoginViewModel()); 
    ServiceContainer.Register<FriendViewModel>(
       () => new FriendViewModel()); 
    ServiceContainer.Register<MessageViewModel>(
       () => new MessageViewModel()); 
    ServiceContainer.Register<RegisterViewModel>(
       () => new RegisterViewModel()); 

    //Models 
    ServiceContainer.Register<ISettings>(
       () => new FakeSettings()); 
    ServiceContainer.Register<IWebService>(
       () => new FakeWebService()); 
  } 
} 

我们使用了内置的 Android 主题Theme.AppCompat.Light,这是材料设计的默认浅色主题。注意我们必须遵循的奇怪构造函数,这是 Xamarin 中自定义Application类的当前要求。你可以将这识别为在这种情况下需要添加的样板代码。

现在让我们为应用程序中的所有活动实现一个简单的基类。在XamSnap.Droid项目中创建一个Activities文件夹,并添加一个名为BaseActivity.cs的新文件,内容如下:

[Activity] 
public class BaseActivity<TViewModel> : AppCompatActivity
   where TViewModel : BaseViewModel 
{ 
  protected readonly TViewModel viewModel; 
  protected ProgressDialog progress; 

  public BaseActivity() 
  { 
    viewModel = ServiceContainer.Resolve(typeof(TViewModel)) as
       TViewModel; 
  } 
  protected override void OnCreate(Bundle savedInstanceState) 
  { 
    base.OnCreate(savedInstanceState); 

    progress = new ProgressDialog(this); 
    progress.SetCancelable(false);
    progress.SetTitle(Resource.String.Loading);
  } 

  protected override void OnResume() 
  { 
    base.OnResume(); 
    viewModel.IsBusyChanged += OnIsBusyChanged; 
  }

  protected override void OnPause() 
  { 
    base.OnPause(); 
    viewModel.IsBusyChanged -= OnIsBusyChanged; 
  } 

  void OnIsBusyChanged (object sender, EventArgs e) 
  { 
    if (viewModel.IsBusy) 
      progress.Show(); 
    else 
      progress.Hide(); 
  } 
} 

我们在这里做了几件事来简化我们其他活动的开发。首先,我们使这个类通用,并定义了一个受保护的变量viewModel来存储特定类型的视图模型。请注意,由于平台限制,我们在 iOS 上没有对控制器使用泛型(更多信息请参见 Xamarin 的文档网站:developer.xamarin.com/guides/ios/advanced_topics/limitations/)。我们还实现了IsBusyChanged,并显示了一个简单的ProgressDialog,其中包含来自Strings.xml文件的Loading字符串,以指示网络活动。

让我们为用户显示错误再添加一个方法,如下所示:

protected void DisplayError(Exception exc) 
{ 
  string error = exc.Message; 
  new AlertDialog.Builder(this)
     .SetTitle(Resource.String.ErrorTitle)
     .SetMessage(error)
     .SetPositiveButton(Android.Resource.String.Ok,
       (IDialogInterfaceOnClickListener)null)
     .Show(); 
} 

这个方法将显示一个弹出对话框,指示出现了错误。注意我们也使用了ErrorTitle和内置的 Android 资源中的Ok字符串。

这将完成我们 Android 应用程序的核心设置。从这里我们可以继续实现我们应用程序中各个屏幕的用户界面。

添加登录界面

在创建 Android 视图之前,了解 Android 中可用的不同布局或视图组类型是很重要的。iOS 没有一些这些的等价物,因为 iOS 在其设备上的屏幕尺寸变化较小。由于 Android 具有几乎无限的屏幕尺寸和密度,Android SDK 为视图的自动调整大小和布局提供了大量内置支持。

以下是常见的布局类型:

  • ViewGroup:这是包含子视图集合的视图的基础类。通常你不会直接使用这个类。

  • LinearLayout:这是一个布局,它将子视图排列成行或列(但不能同时排列)。你还可以为每个子项设置权重,让它们占据可用空间的不同百分比。

  • RelativeLayout:这是一个可以更灵活地设置其子项位置的布局。你可以将子视图相对于彼此定位,使它们相互在上方、下方、左侧或右侧。

  • FrameLayout:这个布局将它的子视图直接在屏幕上的z 顺序一个叠一个。当你有一个需要其他视图覆盖其上并可能停靠在一侧的大子视图时,最好使用这个布局。

  • ListView:这会在列表中垂直显示视图,借助确定子视图数量的适配器类。它还支持其子项被选中。

  • GridView:这会在网格中以行和列显示视图。它还需要使用适配器类来提供子项的数量。

在我们开始编写登录界面之前,删除从 Android 项目模板创建的Main.axmlMainActivity.cs文件。接下来,在项目的Resources目录下的layout文件夹中创建一个名为Login.axml的 Android 布局文件。

现在我们可以开始向我们的 Android 布局添加功能,如下所示:

  1. 双击新的布局文件以打开 Android 设计器。

  2. 将两个纯文本视图拖到文本字段部分找到的布局中。

  3. Id字段中,分别输入@+id/username@+id/password

  4. 对于密码字段,将其输入类型属性设置为textPassword

  5. 将一个按钮拖到布局上,并将其文本属性设置为@string/Login

  6. 将按钮的Id属性设置为@+id/login

当你的布局完成后,它看起来会像下面的截图:

添加登录界面

现在在我们之前创建的Activites文件夹中创建一个名为LoginActivity.cs的新 Android 活动文件。让我们按照以下方式实现登录功能:

[Activity(Label = "@string/ApplicationName", MainLauncher = true)] 
public class LoginActivity : BaseActivity<LoginViewModel> 
{ 
  EditText username, password; 
  Button login; 

  protected override void OnCreate(Bundle savedInstanceState) 
  { 
    base.OnCreate(savedInstanceState);

    SetContentView(Resource.Layout.Login); 
    username = FindViewById<EditText>(Resource.Id.username); 
    password = FindViewById<EditText>(Resource.Id.password); 
    login = FindViewById<Button>(Resource.Id.login); 
    login.Click += OnLogin; 
  } 

  protected override void OnResume() 
  { 
    base.OnResume(); 
    username.Text =
       password.Text = string.Empty; 
  } 

  async void OnLogin (object sender, EventArgs e) 
  { 
    viewModel.UserName = username.Text; 
    viewModel.Password = password.Text; 
    try 
    { 
      await viewModel.Login(); 
      //TODO: navigate to a new activity 
    } 
    catch (Exception exc) 
    { 
      DisplayError(exc); 
    } 
  } 
} 

注意我们设置了MainLaunchertrue,以使此活动成为应用的首个活动。我们还利用了本章早些时候设置的ApplicationName值和BaseActivity类。我们还重写了OnResume以清除两个EditText控件,这样如果你返回屏幕,这些值就会被清空。

现在如果你启动应用程序,你将看到我们刚才实现的登录界面,如下面的截图所示:

添加登录界面

提示

对于 Visual Studio 用户来说,请注意,Xamarin 已经很好地使他们的 Visual Studio 扩展与 Xamarin Studio 完全相同。本章中的所有示例都应在 OS X 上的 Xamarin Studio 或 Windows 上的 Visual Studio 中按所述方式工作。

使用 ListView 和 BaseAdapter

现在,让我们在 Android 上实现一个对话列表。UITableViewUITableViewSource在 Android 上的对应物是ListViewBaseAdapter。这些 Android 类有并行概念,例如实现抽象方法和滚动时回收单元格。在 Android 中使用了几种不同类型的适配器,如ArrayAdapterCursorAdaptor,尽管对于简单列表来说,BaseAdapter通常是最合适的选择。

让我们实现我们的对话界面。首先在你的Activities文件夹中创建一个新的 Android Activity,命名为ConversationsActivity.cs。我们首先只对类定义进行少量修改,如下所示:

[Activity(Label = "Conversations")] 
public class ConversationsActivity :
   BaseActivity<MessageViewModel> 
{ 
  //Other code here later 
} 

执行以下步骤以实现几个 Android 布局:

  1. Resources目录的layout文件夹中创建一个新的 Android 布局,命名为Conversations.axml

  2. 工具箱中拖动一个列表视图(ListView)控件到布局中,并将其Id设置为@+id/conversationsList

  3. 创建第二个 Android 布局;在Resources目录下的layout文件夹中命名为ConversationListItem.axml

  4. 工具箱中将一个中等文本(Text Medium)控件拖到布局中。

  5. 将其 ID 设置为@+id/conversationUsername

  6. 最后,让我们在属性(Properties)框的布局(Layout)选项卡中将其边距(Margin)设置为3dp

这将设置我们将在对话界面中使用到的所有布局文件。你的ConversationListItem.axml布局看起来将类似于以下截图所示:

使用 ListView 和 BaseAdapter

现在,我们可以在ConversationsActivity内部作为一个嵌套类实现BaseAdapter,如下所示:

class Adapter : BaseAdapter<Conversation> 
{ 
  readonly MessageViewModel messageViewModel =
     ServiceContainer.Resolve<MessageViewModel>(); 
  readonly LayoutInflater inflater; 

  public Adapter(Context context) 
  { 
    inflater = (LayoutInflater)context.GetSystemService(
       Context.LayoutInflaterService); 
  } 

  public override long GetItemId(int position) 
  { 
    //This is an abstract method, just a simple implementation 
    return position; 
  } 

  public override View GetView(
     int position, View convertView, ViewGroup parent) 
  { 
    if (convertView == null) 
    { 
      convertView = inflater.Inflate(
         Resource.Layout.ConversationListItem, null); 
    } 
    var conversation = this [position]; 
    var username = convertView.FindViewById<TextView>(
       Resource.Id.conversationUsername); 
    username.Text = conversation.Username; 
    return convertView; 
  }

  public override int Count 
  { 
    get { return messageViewModel.Conversations == null ? 0
       : messageViewModel.Conversations.Length; } 
  }

  public override Conversation this[int position] 
  { 
    get { return messageViewModel.Conversations [position]; } 
  } 
} 

以下是适配器内部正在进行的操作的回顾:

  • 我们继承了BaseAdapter<Conversation>

  • 我们传递了一个Context(我们的活动),这样我们就可以取出LayoutInflater。这个类使我们能够加载 XML 布局资源,并将其膨胀成视图对象。

  • 我们实现了GetItemId。这是一个通常用于标识行的一般方法,但现在我们只是返回位置。

  • 我们设置了GetView方法,通过仅当convertView为空时创建新视图来回收convertView变量。我们还取出了布局中的文本视图以设置它们的文本。

  • 我们重写了Count方法,以返回对话的数量。

  • 我们实现了一个索引器,用于根据位置返回一个Conversation对象。

总的来说,这应该和我们之前在 iOS 上的操作非常相似。

现在,让我们通过在ConversationsActivity的正文添加以下内容来在活动中设置适配器:

ListView listView; 
Adapter adapter; 

protected override void OnCreate(Bundle bundle) 
{ 
  base.OnCreate(bundle); 

  SetContentView(Resource.Layout.Conversations); 
  listView = FindViewById<ListView>(
     Resource.Id.conversationsList); 
  listView.Adapter = 
     adapter = new Adapter(this); 
} 

protected async override void OnResume() 
{ 
  base.OnResume(); 
  try 
  { 
    await viewModel.GetConversations(); 
    adapter.NotifyDataSetInvalidated(); 
  } 
  catch (Exception exc) 
  { 
    DisplayError(exc); 
  } 
} 

这段代码将在活动出现在屏幕上时设置适配器并重新加载我们的对话列表。注意,我们在这里调用了NotifyDataSetInvalidated,这样当对话数量更新后,ListView可以重新加载其行。

最后但同样重要的是,我们需要修改之前在LoginActivity中设置的OnLogin方法,以启动我们的新活动,如下所示:

StartActivity(typeof(ConversationsActivity)); 

现在如果我们编译并运行我们的应用程序,登录后我们可以导航到一个对话列表,如下截图所示:

使用 ListView 和 BaseAdapter

实现好友列表

在我们开始实现好友列表屏幕之前,我们首先需要在应用程序的ActionBar中添加一个菜单项。首先在项目的Resources文件夹中创建一个名为menu的新文件夹。接下来,创建一个名为ConversationsMenu.axml的新 Android 布局文件。删除默认创建的布局 XML,并替换为以下内容:

<?xml version="1.0" encoding="utf-8"?> 
<menu > 
  <item android:id="@+id/addFriendMenu"
     android:text="Add Friend"
     android:showAsAction="ifRoom"/> 
</menu> 

我们设置了一个根菜单,其中包含一个菜单项。

以下是我们为 XML 中的项目设置的内容分解:

  • android:id:我们稍后在 C#中会使用它,通过Resource.Id.addFriendMenu引用菜单项。

  • android:icon:这是为菜单项显示的图像资源。我们使用了一个内置的 Android 通用加号图标。

  • android:showAsAction:如果空间足够,这将使菜单项可见。如果设备的屏幕太窄,将显示一个溢出菜单来代替菜单项。

现在,我们可以在ConversationsActivity.cs中进行一些更改,如下所示显示菜单项:

public override bool OnCreateOptionsMenu(IMenu menu) 
{ 
  MenuInflater.Inflate(Resource.Menu.ConversationsMenu, menu); 
  return base.OnCreateOptionsMenu(menu); 
} 

这段代码将使用我们的布局并将其应用到活动中操作栏顶部的菜单。接下来,我们可以添加一些代码,在选中菜单项时运行,如下所示:

public override bool OnOptionsItemSelected(IMenuItem item) 
{ 
  if (item.ItemId == Resource.Id.addFriendMenu) 
  { 
    //TODO: launch the next activity 
  } 
  return base.OnOptionsItemSelected(item); 
} 

现在我们来实现下一个活动。首先复制Resources目录中layout文件夹中的Conversations.axml文件,并将其重命名为Friends.axml。我们在这个文件中唯一要做的更改是将 ListView 的 ID 重命名为@+id/friendsList

接下来,执行以下步骤,创建一个可用于ListView中列表项的布局:

  1. 创建一个名为FriendListItem.axml的新 Android 布局。

  2. 打开布局,并切换到屏幕底部的源代码标签。

  3. 将根LinearLayout XML 元素更改为RelativeLayout元素。

  4. 切换回屏幕底部的设计器标签。

  5. 工具箱中拖动一个大文本控件到布局上,并将其Id设置为@+id/friendName

  6. 工具箱中拖动一个图像视图控件到布局上;你可以让它保留默认的Id或者将其清空。

  7. 将图像视图的图像更改为@android:drawable/ic_menu_add。这是我们本章前面使用的同样的加号图标。你可以在资源对话框下的框架资源标签中选择它。

  8. 将控件的两边宽度和高度设置为wrap_content。这可以在布局标签下的ViewGroup部分找到。

  9. 然后,仅针对图像视图检查与父级右对齐的值。

  10. 最后,在属性框的布局标签下,将控件的两边边距设置为3dp

使用 Xamarin 设计器可以非常高效,但有些开发者更喜欢更高水平的控制。你可以考虑自己编写 XML 作为替代方案,这相当直接,如下面的代码所示:

<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout 

    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"> 
    <TextView 
        android:text="Large Text" 
        android:textAppearance="?android:attr/textAppearanceLarge" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:id="@+id/friendName" 
        android:layout_margin="3dp" /> 
    <ImageView 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:src="img/ic_menu_add" 
        android:layout_margin="3dp" 
        android:layout_alignParentRight="true" /> 
</RelativeLayout> 

既然我们已经拥有了新屏幕所需的所有布局,那么在Activities文件夹中创建一个名为FriendsActivity.cs的 Android 活动吧。让我们按照之前的做法,创建活动的基本定义如下:

[Activity(Label = "Friends")] 
public class FriendsActivity : BaseActivity<FriendViewModel> 
{ 
  protected override void OnCreate(Bundle savedInstanceState) 
  { 
    base.OnCreate(savedInstanceState); 
  } 
} 

现在,让我们实现一个嵌套的Adapter类来设置列表视图项,如下所示:

class Adapter : BaseAdapter<User> 
{ 
  readonly FriendViewModel friendViewModel =
     ServiceContainer.Resolve<FriendViewModel>(); 
  readonly LayoutInflater inflater; 

  public Adapter(Context context) 
  { 
    inflater = (LayoutInflater)context.GetSystemService (
       Context.LayoutInflaterService); 
  } 

  public override long GetItemId(int position) 
  { 
    return position; 
  } 

  public override View GetView(
     int position, View convertView, ViewGroup parent) 
  { 
    if (convertView == null) 
    { 
      convertView = inflater.Inflate(
         Resource.Layout.FriendListItem, null); 
    } 
    var friend = this [position]; 
    var friendname = convertView.FindViewById<TextView>(
       Resource.Id.friendName); 
    friendname.Text = friend.Name; 
    return convertView; 
  }

  public override int Count 
  { 
    get { return friendViewModel.Friends == null ? 0
       : friendViewModel.Friends.Length; } 
  } 

  public override User this[int position] 
  { 
    get { return friendViewModel.Friends[position]; } 
  } 
} 

这个适配器与我们之前为对话屏幕实现的适配器实际上没有区别。我们只需要设置好友的名字,并且使用User对象而不是Conversation对象。

为了完成适配器的设置,我们可以更新FriendsActivity类的主体,如下所示:

ListView listView; 
Adapter adapter; 

protected override void OnCreate(Bundle savedInstanceState) 
{ 
  base.OnCreate(savedInstanceState); 

  SetContentView(Resource.Layout.Friends); 
  listView = FindViewById<ListView>(Resource.Id.friendsList); 
  listView.Adapter =
     adapter = new Adapter(this); 
} 

protected async override void OnResume() 
{ 
  base.OnResume(); 
  try 
  { 
    await viewModel.GetFriends(); 
    adapter.NotifyDataSetInvalidated(); 
  } 
  catch (Exception exc) 
  { 
    DisplayError(exc); 
  } 
} 

最后但同样重要的是,我们可以更新ConversationsActivity类中的OnOptionsItemSelected,如下所示:

public override bool OnOptionsItemSelected(IMenuItem item) 
{ 
  if (item.ItemId == Resource.Id.addFriendMenu) 
  { 
    StartActivity(typeof(FriendsActivity)); 
  } 
  return base.OnOptionsItemSelected(item); 
} 

因此,如果我们编译并运行应用程序,我们可以导航到一个完全实现的好友列表屏幕,如下面的截图所示:

实现好友列表

撰写消息

下一个屏幕有点复杂;我们将需要创建一个ListView,根据行的类型使用多个布局文件。我们还需要执行一些布局技巧,在ListView下方放置一个视图,并设置ListView自动滚动。

对于下一个屏幕,我们首先在Resources目录的layout文件夹中创建一个名为Messages.axml的新布局,然后执行以下步骤:

  1. 在布局中拖动一个新的ListView。将其Id设置为@+id/messageList

  2. 勾选从底部堆叠的复选框,并将文本模式设置为alwaysScroll。这将设置它从底部向上显示项目。

  3. LinearLayout部分的布局选项卡中,将ListView权重值设置为1

  4. 在布局上拖动一个新的RelativeLayout。让其Id保持默认值,或者移除它。

  5. RelativeLayout内拖动一个新的按钮。将其Id设置为@+id/sendButton

  6. 布局选项卡中勾选与父容器右对齐的复选框。

  7. RelativeLayout内,从文本字段部分拖动一个新的纯文本到按钮左侧。将其Id设置为@+id/messageText

  8. 布局选项卡中,将To Left Of设置为@+id/sendButton,并将其宽度设置为match_parent

  9. 勾选居中于父容器以修复垂直居中问题。

完成后,XML 文件如下所示:

<?xml version="1.0" encoding="utf-8"?> 
<LinearLayout  

    android:orientation="vertical" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent"> 
    <ListView 
        android:minWidth="25px" 
        android:minHeight="25px" 
        android:layout_width="match_parent" 
        android:layout_height="match_parent" 
        android:id="@+id/messageList" 
        android:stackFromBottom="true" 
        android:transcriptMode="alwaysScroll" 
        android:layout_weight="1" /> 
    <RelativeLayout 
        android:minWidth="25px" 
        android:minHeight="25px" 
        android:layout_width="match_parent" 
        android:layout_height="wrap_content"> 
        <EditText 
            android:layout_width="match_parent" 
            android:layout_height="wrap_content" 
            android:id="@+id/messageText" 
            android:layout_toLeftOf="@+id/sendButton" 
            android:layout_centerInParent="true" /> 
        <Button 
            android:text="Send" 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content" 
            android:id="@+id/sendButton" 
            android:layout_alignParentRight="true" /> 
    </RelativeLayout> 
</LinearLayout> 

接下来,执行以下步骤来制作另外两个 Android 布局:

  1. Resources目录的layout文件夹中创建一个名为MyMessageListItem.axml的新布局。

  2. 打开布局并切换到源代码选项卡。将根 XML 元素更改为RelativeLayout

  3. 切换回内容选项卡,并将两个TextView控件拖动到布局上。

  4. Id字段中,分别输入@+id/myMessageText@+id/myMessageDate

  5. 对于这两个视图,将边距设置为3dp,将宽度和高度设置为wrap_content

  6. 对于第一个 TextView,在样式选项卡下将其颜色设置为@android:color/holo_blue_bright

  7. 对于第二个 TextView,在布局选项卡下勾选对齐父级右侧复选框。

  8. 创建一个名为TheirMessageListItem.axml的新布局,并重复该过程。为新的布局中的第一个 TextView 选择不同的颜色。

最后,我们需要为屏幕创建一个新的活动。在Activities目录中创建一个名为MessagesActivity.cs的新 Android 活动。从以下标准代码开始设置活动:

[Activity(Label = "Messages")] 
public class MessagesActivity : BaseActivity<MessageViewModel> 
{ 
  protected override void OnCreate(Bundle savedInstanceState) 
  { 
    base.OnCreate(savedInstanceState); 
  } 
} 

接下来,让我们实现一个比我们之前实现的更复杂的适配器,如下所示:

class Adapter : BaseAdapter<Message> 
{ 
  readonly MessageViewModel messageViewModel =
     ServiceContainer.Resolve<MessageViewModel>(); 
  readonly ISettings settings =
     ServiceContainer.Resolve<ISettings>(); 
  readonly LayoutInflater inflater; 
  const int MyMessageType = 0, TheirMessageType = 1; 

  public Adapter (Context context) 
  { 
    inflater = (LayoutInflater)context.GetSystemService (
       Context.LayoutInflaterService); 
  } 

  public override long GetItemId(int position) 
  { 
    return position; 
  } 

  public override int Count 
  { 
    get { return messageViewModel.Messages == null ? 0
      : messageViewModel.Messages.Length; } 
  } 

  public override Message this[int position] 
  { 
    get { return messageViewModel.Messages[position]; } 
  } 

  public override int ViewTypeCount 
  { 
    get { return 2; } 
  } 

  public override int GetItemViewType(int position) 
  { 
    var message = this [position]; 
    return message.UserName == settings.User.Name ?
       MyMessageType : TheirMessageType; 
  } 
} 

这包括除我们的GetView实现之外的所有内容,我们稍后会讨论这一点。这里的第一个变化是一些MyMessageTypeTheirMessageType的常量。然后我们实现了ViewTypeCountGetItemViewType。这是 Android 的机制,用于在列表视图中为列表项使用两种不同的布局。我们为用户的消息使用一种类型的布局,而为对话中的另一个用户使用不同的布局。

接下来,我们按照以下方式实现GetView

public override View GetView(
   int position, View convertView, ViewGroup parent) 
{ 
  var message = this [position]; 
  int type = GetItemViewType(position); 
  if (convertView == null) 
  { 
    if (type == MyMessageType) 
    { 
      convertView = inflater.Inflate(
         Resource.Layout.MyMessageListItem, null); 
    } 
    else 
    { 
      convertView = inflater.Inflate(
         Resource.Layout.TheirMessageListItem, null); 
    } 
  } 
  TextView messageText; 
  if (type == MyMessageType) 
  { 
    messageText = convertView.FindViewById<TextView>(
       Resource.Id.myMessageText); 
  } 
  else 
  { 
    messageText = convertView.FindViewById<TextView>(
       Resource.Id.theirMessageText); 
  } 
  messageText.Text = message.Text; 
  return convertView; 
} 

提示

需要注意的是,在 Android 中使用唯一 ID 作为每个视图的最佳实践。即使在这种情况下代码看起来有点丑陋,但最好还是这样做,因为当存在具有相同 ID 的视图的多个布局时,FindViewById不能按预期工作。

让我们通过以下步骤分解我们的实现过程:

  1. 我们首先获取对应于行位置的message对象。

  2. 接下来,我们获取决定是当前用户的消息还是对话中另一个用户的视图类型。

  3. 如果convertViewnull,我们会根据类型充气适当的布局。

  4. 接下来,我们从convertView中取出两个文本视图,messageTextdateText。我们必须使用类型值以确保使用正确的资源 ID。

  5. 我们使用message对象在两个文本视图中设置适当的文本。

  6. 我们返回convertView

现在,让我们通过设置适配器的其余部分来完成MessagesActivity。首先,让我们实现一些成员变量和OnCreate方法,如下所示:

ListView listView; 
EditText messageText; 
Button sendButton; 
Adapter adapter; 

protected override void OnCreate(Bundle savedInstanceState) 
{ 
  base.OnCreate(savedInstanceState); 

  Title = viewModel.Conversation.UserName; 
  SetContentView(Resource.Layout.Messages); 
  listView = FindViewById<ListView>(Resource.Id.messageList); 
  messageText = FindViewById<EditText>(Resource.Id.messageText); 
  sendButton = FindViewById<Button>(Resource.Id.sendButton); 

  listView.Adapter =
     adapter = new Adapter(this); 

  sendButton.Click += async (sender, e) => 
  { 
    viewModel.Text = messageText.Text; 
    try 
    { 
      await viewModel.SendMessage(); 
      messageText.Text = string.Empty; 
      adapter.NotifyDataSetInvalidated(); 
    } 
    catch (Exception exc) 
    { 
      DisplayError(exc); 
    } 
  }; 
} 

到目前为止,与本章中的先前活动相比,这个活动相当标准。我们还必须在OnCreate中连接sendButton的点击事件,以便发送消息并刷新列表。我们还使用了一个技巧,通过将列表视图的选择设置为最后一个项目来滚动到列表末尾。

接下来,我们需要实现OnResume来加载消息,使适配器无效,然后滚动列表视图到底部,如下所示:

protected async override void OnResume() 
{ 
  base.OnResume(); 
  try 
  { 
    await viewModel.GetMessages(); 
    adapter.NotifyDataSetInvalidated(); 
    listView.SetSelection(adapter.Count); 
  } 
  catch (Exception exc) 
  { 
    DisplayError(exc); 
  } 
} 

最后但同样重要的是,我们需要修改ConversationsActivity.cs文件,使得在点击列表视图中的行时能够向前导航:

protected override void OnCreate(Bundle savedInstanceState) 
{ 
  base.OnCreate(savedInstanceState); 

  //Leave code here unmodified 

  listView.ItemClick += (sender, e) => 
  { 
    viewModel.Conversation = viewModel.Conversations[e.Position]; 
    StartActivity(typeof(MessagesActivity)); 
  }; 
} 

因此,最后,如果你编译并运行该应用,你将能够导航到消息界面并向列表中添加新消息,如下面的截图所示:

编写消息

总结

在本章中,我们首先回顾了 Android Manifest 文件中的基本设置。接下来,我们实现了一个自定义的Application类来设置我们的ServiceContainer。然后,我们介绍了不同类型的 Android 布局,并使用原生的 Android 视图实现了一个登录界面。之后,我们通过使用 Android 布局并覆盖一些内置方法,在 Android 操作栏中设置了一个菜单。我们实现了好友列表界面,并学习了ListView和适配器的基础知识。最后,我们实现了消息界面,并使用了列表视图适配器和布局中更高级的功能。

完成本章后,你将拥有一个部分功能性的 XamSnap 的 Android 版本。你将对 Android SDK 和工具有了更深入的理解。你应该有信心使用 Xamarin 开发自己的 Android 应用程序。尝试自己实现本章未涵盖的剩余界面。如果你遇到困难,随时可以查看本书附带的完整示例应用程序。在下一章中,我们将介绍如何部署到移动设备上,以及为什么在真实设备上测试你的应用程序非常重要。

第七章:在设备上部署和测试

部署到设备既重要又有些麻烦,尤其是第一次尝试时。某些问题只会在移动设备上发生,无法在 iOS 仿真器或 Android 仿真器中复现。您还可以测试只有在真实设备上才能实现的功能,如 GPS、摄像头、内存限制或蜂窝网络连接。在为 Xamarin 开发时,也存在一些常见的陷阱,只有在物理设备上测试时才会显现。

在本章中,我们将涵盖以下内容:

  • iOS 配置

  • 安卓设备调试设置

  • 链接器

  • 提前编译(AOT)

  • 使用 Xamarin 常见的内存陷阱

在开始本章之前,需要注意的是,要部署到 iOS 设备,需要一个有效的 iTunes 账户或 iOS 开发者计划会员资格。可以随时回到第一章,Xamarin 设置,了解该过程。

iOS 配置

苹果对将应用程序部署到 iOS 设备有一个严格的过程。尽管对于开发者来说这个过程可能相当复杂和痛苦,但苹果可以通过阻止普通用户侧载可能恶意应用程序来提供一定级别的安全性。

在我们将应用程序部署到 iOS 设备之前,我们将在iOS 开发中心设置一些事情。我们将从为您的账户创建一个应用 ID 或捆绑 ID 开始。这是任何 iOS 应用程序的主要标识符。

开始时请访问 developer.apple.com/account 并执行以下步骤:

  1. 使用您的开发者账户登录。

  2. 在右侧导航栏中点击证书、ID 和配置文件

  3. 点击应用 IDs

  4. 点击加号按钮添加新的 iOS 应用 ID。

  5. 名称字段中,输入一些有意义的文字,例如YourCompanyNameWildcard

  6. 选择通配符应用 ID单选按钮。

  7. 捆绑 ID字段中,为您的公司选择一个反向域名格式的名称,例如com.yourcompanyname.*

  8. 点击继续

  9. 检查最终设置并点击提交

保持此网页打开,因为我们在整个章节中都会使用它。

我们刚刚为您的账户注册了一个通配符捆绑 ID;将此作为您希望用此账户标识的所有未来应用程序的前缀。稍后,当您准备将应用程序部署到苹果应用商店时,您将创建一个显式应用 ID,如com.yourcompanyname.yourapp。这允许您将特定应用程序部署到商店,而通配符 ID 最好用于将应用程序部署到测试设备。

接下来我们需要找到你计划调试应用程序的每个设备的唯一标识符。苹果要求每个设备都在你的账户下注册,并且每个开发者每种设备类型最多可注册 110 个设备(110 个 iPhone、iPad、iPod、Apple TV 或 Apple Watch)。绕过这一要求的唯一方式是注册 iOS 开发者企业计划,该计划除了标准的 99 美元开发者费用外,还需支付 299 美元的年费。

开始启动 Xcode 并执行以下步骤:

  1. 在顶部菜单中点击窗口 | 设备

  2. 使用 USB 线连接你的目标设备。

  3. 在左侧导航栏中,你应该看到你的设备名称;选择它。

  4. 注意你的设备的标识符值。将其复制到剪贴板。

以下截图显示了在 Xcode 中选择你的设备后的屏幕样子:

iOS 配置

返回到developer.apple.com/account(希望本章早些时候它还保持打开状态),并执行以下步骤:

  1. 点击左侧导航栏中的设备 | 全部

  2. 点击页面右上角的加号按钮。

  3. 为你的设备输入一个有意义的名称,并将剪贴板中的标识符粘贴到UDID字段中。

  4. 点击继续

  5. 检查你输入的信息并点击注册

在以后,当你的账户完全设置好后,你只需在 Xcode 中点击用于开发按钮,就可以跳过这第二个步骤。

以下截图显示了你的设备列表在完成时的样子:

iOS 配置

接下来,我们需要生成一个证书,以代表你的账户作为开发者。在 Xcode 5 之前,你必须使用 Mac 上的钥匙串应用程序创建一个证书签名请求。Xcode 的新版本将这一流程集成到 Xcode 中,使得操作更加简便。

打开 Xcode 并执行以下步骤:

  1. 在顶部菜单中导航至Xcode | 偏好设置

  2. 选择账户标签页。

  3. 点击左下角的加号按钮,然后点击添加 Apple ID

  4. 输入你的开发者账户的电子邮件和密码。

  5. 创建账户后,点击右下角的查看详情

  6. 点击左下角的下载全部按钮。

  7. 如果这是一个新账户,Xcode 会显示一个警告,提示还没有证书存在。勾选每个框并点击请求以生成证书。

Xcode 现在将自动为你的账户创建一个开发者证书,并将其安装到你的 Mac 钥匙串中。

以下截图显示了设置你的账户后屏幕的样子:

iOS 配置

接下来,我们需要创建一个配置文件。这是允许应用程序安装在 iOS 设备上的最终文件。配置文件包含一个 App ID、一个设备 ID 列表,最后还有开发者的证书。你还需要在 Mac 的钥匙串中拥有开发者证书的私钥才能使用配置文件。

以下是几种配置文件类型:

  • 开发:这用于调试或发布版本;当你的应用程序处于开发阶段时,你会积极使用这种类型的配置文件。

  • Ad Hoc:这主要用于发布版本;这种证书非常适合进行 beta 测试或分发给一组小用户。使用这种方法,你可以通过企业开发者账户向无限数量的用户分发。

  • App Store:这用于提交到 App Store 的发布版本。你不能使用此证书将应用程序部署到你的设备;它只能用于商店提交。

让我们回到developer.apple.com/apple,通过执行以下步骤创建一个新的配置文件:

  1. 点击左侧导航栏中的配置文件 | 全部

  2. 点击页面右上角的加号按钮。

  3. 选择iOS 应用开发并点击继续

  4. 选择本章前面创建的通配符 App ID 并点击继续

  5. 选择我们在本章前面创建的证书并点击继续

  6. 选择你想要部署到的设备并点击继续

  7. 输入一个合适的配置文件名称,如YourCompanyDev

  8. 点击继续,你的配置文件将被创建。

下面的截图展示了你创建后最终会得到的新配置文件。不必担心下载文件;我们将使用 Xcode 导入最终的配置文件。

iOS 配置

要导入配置文件,请回到 Xcode 并执行以下步骤:

  1. 导航到对话框顶部菜单中的Xcode | 偏好设置

  2. 选择账户标签。

  3. 选择你的账户并点击查看详情

  4. 点击左下角的下载全部按钮。

  5. 几秒钟后,你的配置文件将出现。

Xcode 应该会自动包含你在 Apple 开发者网站上创建的所有配置文件。Xcode 还会自行创建一些配置文件。

在最新版本的 Xamarin Studio 中,你可以查看这些配置文件,但无法同步它们。导航到 Xamarin Studio | 偏好设置 | 开发者账户,从 Xamarin Studio 中查看配置文件。你也可以在 Xamarin 的文档网站上查看关于 iOS 配置的文档,网址为developer.xamarin.com/guides/ios/getting_started/device_provisioning/

安卓设备设置

与在 iOS 设备上部署应用程序的麻烦相比,Android 就轻松多了。要将应用程序部署到设备上,你只需在设备上设置几个选项。这是由于与 iOS 相比,Android 的开放性。大多数用户的 Android 设备调试功能是关闭的,但任何希望尝试编写 Android 应用程序的用户都可以轻松地开启它。

首先打开设置应用。你可能需要通过查看设备上的所有应用程序来找到它,如下所示:

  1. 向下滚动并点击标有开发者选项的部分。

  2. 在顶部的操作栏中,你可能需要将一个开关切换到开启位置。这个操作在每个设备上都有所不同。

  3. 向下滚动并勾选USB 调试

  4. 将会出现一个警告确认提示;点击确定

提示

请注意,一些较新的 Android 设备使得普通用户开启 USB 调试变得更加困难。你需要点击开发者选项七次来开启这个选项。

下面的截图展示了在过程中你的设备的样子:

Android 设备设置

启用这个选项后,你只需通过 USB 连接你的设备,并在 Xamarin Studio 中调试一个 Android 应用程序。你会在选择设备对话框中看到你的设备列表。请注意,如果你使用的是 Windows 系统,或者你的设备是非标准的,你可能需要访问设备制造商的网站来安装驱动程序。大多数三星和 Nexus 设备会自动安装它们的驱动程序。在 Android 4.3 及更高版本中,在开始 USB 调试会话之前,设备上还会出现一个确认对话框。

下面的截图展示了在选择设备对话框中三星 Galaxy 设备的样子。Xamarin Studio 将显示型号号码,这并不总是一个你可能认识的名字。你可以在你的设备的设置中查看这个型号号码。

Android 设备设置

了解链接器

为了让 Xamarin 应用程序在移动设备上保持小型和轻量级,Xamarin 为编译器创建了一个名为链接器的功能。其主要目的是从核心 Mono 程序集(如System.dll)和特定平台的程序集(Mono.Android.dllXamarin.iOS.dll)中移除未使用的代码;然而,如果设置得当,它也可以为你自己的程序集提供同样的好处。如果不运行链接器,整个 Mono 框架可能大约有 30 兆字节。这就是为什么在设备构建中默认启用链接,这样你可以保持应用程序的小巧。

链接器使用静态分析来处理程序集中的各种代码路径。如果它确定一个方法或类从未被使用,它会从该程序集中移除未使用的代码。这个过程可能会很耗时,因此默认情况下,在模拟器中运行的构建会跳过这一步。

Xamarin 应用程序有以下三个主要的链接器设置:

  • 不链接:在这种情况下,链接器编译步骤将被跳过。这对于在模拟器中运行的构建或如果你需要诊断链接器的潜在问题最为合适。

  • 仅链接 SDK 程序集:在这种情况下,链接器只会在核心 Mono 程序集上运行,如System.dllSystem.Core.dllSystem.Xml.dll

  • 链接所有程序集:在这种情况下,链接器将对应用程序中的所有程序集运行,包括你正在使用的任何类库或第三方程序集。

这些设置可以在任何 Xamarin.iOS 或 Xamarin.Android 应用程序的项目选项中找到。这些设置通常不会出现在类库中,因为它们通常与将要部署的 iOS 或 Android 应用程序相关联。

链接器还可能在运行时引起潜在问题,因为有时它的分析会错误地确定一段代码未被使用。如果你在System.Reflection命名空间中使用特性而不是直接访问方法或属性,这可能会发生。这就是为什么在物理设备上测试你的应用程序很重要,因为设备构建启用了链接。

为了说明这个问题,让我们看一下以下代码示例:

//Just a simple class for holding info 
public class Person 
{ 
  public int Id { get; set; } 
  public string Name { get; set; } 
} 

//Then somewhere later in your code 
var person = new Person { Id = 1, Name = "Chuck Norris" }; 
var propInfo = person.GetType().GetProperty("Name"); 
string value = propInfo.GetValue(person) as string; 
Console.WriteLine("Name: " + value); 

运行前面的代码,在不链接仅链接 SDK 程序集的选项下将正常工作。然而,如果你在链接所有程序集的选项下尝试运行此代码,你会遇到类似以下的异常:

Unhandled Exception: 
System.ArgumentException: Get Method not found for 'Name'
   at System.Reflection.MonoProperty.GetValue (System.Object obj,
   BindingFlags invokeAttr, System.Reflection.Binder binder,
   System.Object[] index, System.Globalization.CultureInfo culture)
   at System.Reflection.PropertyInfo.GetValue (System.Object obj) 

由于从未直接从代码中使用Name属性的 getter,链接器将其从程序集中剥离。这导致反射代码在运行时失败。

尽管你的代码可能会出现潜在问题,但链接所有程序集的选项仍然非常有用。有些优化只能在此模式下执行,Xamarin 可以将你的应用程序缩减到尽可能小的尺寸。如果你的应用程序需要性能或极小的下载尺寸,请尝试这个选项。然而,应进行彻底测试,以确保链接程序集不会引起任何问题。

为了解决代码中的问题,Xamarin 提供了一套完整的解决方案,以防止代码中的特定部分被剥离。

以下是一些选项:

  • 使用[Preserve]标记类成员;这将强制链接器包含带属性的方法、字段或属性。

  • 使用[Preserve(AllMembers=true)]标记整个类;这将保留类中的所有代码。

  • 使用[assembly: Preserve]标记整个程序集;这是一个程序集级别的属性,将保留其中的所有代码。

  • 通过修改项目选项中的附加 mtouch 参数来跳过整个程序集;使用--linkskip=System来跳过整个程序集。这可以用于那些你没有源代码的程序集。

  • 通过 XML 文件自定义链接,当你需要跳过没有源代码的具体类或方法的链接时,这是最佳选择。在附加 mtouch 参数中使用 --xml=YourFile.xml

以下是一个演示自定义链接的示例 XML 文件:

<linker> 
  <assembly fullname="mscorlib"> 
    <type fullname="System.Environment"> 
      <field name="mono_corlib_version" /> 
      <method name="get_StackTrace" />  
    </type> 
  </assembly> 
  <assembly fullname="My.Assembly.Name"> 
    <type fullname="MyTypeA" preserve="fields" /> 
      <method name=".ctor" /> 
    </type> 
    <type fullname="MyTypeB" />                          
      <method signature="System.Void MyFunc(System.Int32 x)" /> 
      <field signature="System.String _myField" /> 
    </type> 
  </assembly> 
</linker> 

自定义链接是选项中最复杂的,通常是最后的选择。幸运的是,大多数 Xamarin 应用程序不需要解决许多链接问题。

了解 AOT 编译

Windows 上的 Mono 和 .NET 运行时基于即时编译JIT)器。C# 和其他 .NET 语言被编译成微软中间语言MSIL)。在运行时,MSIL 会即时编译成本地代码(正好在需要时),以在任何类型的架构上运行你的应用程序。Xamarin.Android 遵循这一确切模式。然而,由于苹果对动态生成代码的限制,iOS 上不允许使用即时编译(JIT)器。

为了绕过这一限制,Xamarin 开发了一个名为提前编译AOT)的新选项,你的 C# 代码被编译成特定于平台的本地机器代码。除了使 .NET 在 iOS 上成为可能之外,AOT 还具有其他好处,例如启动时间更短,性能可能更好。

AOT 也有一些与 C# 泛型相关的限制。为了提前编译程序集,编译器需要对代码进行一些静态分析,以确定类型信息。泛型在这种情况下会带来一些问题。

AOT 不支持一些在 C# 中完全有效的情况。首先是泛型接口,如下所示:

interface MyInterface<T>  
{ 
  T GetMyValue(); 
} 

编译器无法提前确定可能实现此接口的类,特别是涉及多个程序集时。第二个限制与第一个相关:你不能覆盖包含泛型参数或返回值的虚拟方法。

以下是一个简单的例子:

class MyClass<T> 
{ 
  public virtual T GetMyValue()  
  { 
    //Some code here 
  } 
} 

class MySubClass : MyClass<int> 
{ 
  public override int GetMyValue() 
  { 
    //Some code here 
  } 
} 

再次强调,编译器的静态分析无法在编译时确定哪些类可能会覆盖这个方法。

另一个限制是,你不能在泛型类中使用 DllImport,如下面的代码所示:

class MyGeneric<T> 
{ 
  [DllImport("MyImport")] 
  public static void MyImport(); 
} 

如果你不太熟悉这个语言特性,DllImport 是一种从 C# 调用本地 C/C++ 方法的方式。在泛型类中使用它们是不支持的。

这些限制是为什么在设备上进行测试很重要的另一个原因,因为上述代码在其他可以运行 C# 代码的平台上是没问题的,但在 Xamarin.iOS 上不行。

避免常见的内存陷阱

移动设备上的内存绝对不是无限的资源。因此,你的应用程序中的内存使用可能比桌面应用程序更重要。有时,你可能会发现需要使用内存分析器或改进代码以更有效地使用内存。

以下是最常见的内存陷阱:

  • 垃圾回收器GC)无法快速回收大对象以跟上应用程序的步伐

  • 你的代码无意中导致了内存泄漏

  • 一个 C#对象被垃圾回收,但后来被本地代码尝试使用

让我们看看第一个问题,即 GC 无法跟上。假设我们有一个 Xamarin.iOS 应用程序,其中有一个用于在 Twitter 上分享图像的按钮,如下所示:

twitterShare.TouchUpInside += (sender, e) => 
{ 
  var image = UImage.FromFile("YourLargeImage.png"); 
  //Share to Twitter 
}; 

现在假设图像是用户相册中的 10MB 图像。如果用户点击按钮并迅速取消 Twitter 帖子,应用程序可能会出现内存不足的情况。iOS 通常会强制关闭使用过多内存的应用程序,你不会希望用户在使用你的应用时遇到这种情况。

最佳解决方案是在使用完图像后调用其Dispose方法,如下所示:

var image = UImage.FromFile("YourLargeImage.png"); 
//Share to Twitter 
image.Dispose(); 

更好的方法将是利用 C#的using语句,如下所示:

using(var image = UImage.FromFile("YourLargeImage.png")) 
{ 
  //Share to Twitter 
} 

C#的using语句会自动在try-finally块中调用Dispose,因此即使抛出异常,对象也将被释放。我建议尽可能对任何IDisposable类使用using语句。对于小对象如NSString来说,这并不总是必要的,但对于更大、更重的UIKit对象来说,这总是一个好主意。

提示

在 Android 上,与Bitmap类也可能发生类似情况。虽然略有不同,但最好是在此类上调用Dispose方法,这与你在 iOS 上对UIImage的处理是一样的。

内存泄漏是下一个潜在问题。C#作为一种管理的、垃圾回收的语言,防止了很多内存泄漏,但并非全部。C#中最常见的泄漏是由事件引起的。

假设我们有一个带有事件的静态类,如下所示:

static class MyStatic 
{ 
  public static event EventHandler MyEvent; 
} 

现在,假设我们需要从 iOS 控制器中订阅事件,如下所示:

public override void ViewDidLoad() 
{ 
  base.ViewDidLoad(); 

  MyStatic.MyEvent += (sender, e) => 
  { 
    //Do something 
  }; 
} 

这里的问题是,静态类将持有对控制器的引用,直到事件被取消订阅。这是许多开发者可能会忽略的情况。为了在 iOS 上解决这个问题,我会在ViewWillAppear中订阅事件,并在ViewWillDisappear中取消订阅。在 Android 上,使用OnStartOnStop,或者OnPauseOnResume

你会正确实现此事件,如下所示:

public override void ViewWillAppear() 
{ 
  base.ViewWillAppear(); 
  MyStatic.MyEvent += OnMyEvent; 
} 

public override void ViewWillDisappear() 
{ 
  base.ViewWillDisappear (); 
  MyStatic.MyEvent -= OnMyEvent; 
} 

然而,事件并不是内存泄漏的必然原因。例如,在ViewDidLoad方法中订阅按钮的TouchUpInside事件是没问题的。由于按钮与控制器在内存中的生命周期相同,一切都可以被垃圾回收,而不会造成问题。

对于最后一个问题,垃圾回收器有时可能会移除一个 C#对象;后来,一个 Objective-C 对象尝试访问它。

下面是一个添加按钮到UITableViewCell的例子:

public override UITableViewCell GetCell(
   UITableView tableView, NSIndexPath indexPath) 
{ 
  var cell = tableView.DequeueReusableCell("MyCell"); 
  //Remaining cell setup here 

  var button = UIButton.FromType(UIButtonType.InfoDark); 
  button.TouchUpInside += (sender, e) => 
  { 
    //Do something 
  }; 
  cell.AccessoryView = button; 
  return cell; 
} 

我们将内置的信息按钮作为单元格的附件视图添加。这里的问题是,按钮将被垃圾回收,但其 Objective-C 对应物仍将在屏幕上显示时被使用。如果过了一段时间后点击按钮,你可能会遇到类似下面的崩溃情况:

mono-rt: Stacktrace:
mono-rt:   at <unknown>
mono-rt:   at (wrapper managed-to-native) MonoTouch.UIKit.UIApplication.UIApplicationMain
    (int,string[],intptr,intptr) 
mono-rt:   at MonoTouch.UIKit.UIApplication.Main (string[],string,string) 
... Continued ...
=================================================================
Got a SIGSEGV while executing native code. This usually indicates
a fatal error in the mono runtime or one of the native libraries 
used by your application.
================================================================

这不是最描述性的错误消息,但一般来说,你知道原生 Objective-C 代码中出了问题。要解决这个问题,请创建一个UITableViewCell的自定义子类,并为按钮创建一个专用的成员变量,如下所示:

public class MyCell : UITableViewCell 
{ 
  UIButton button;

  public MyCell() 
  { 
    button = UIButton.FromType(UIButtonType.InfoDark); 
    button.TouchUpInside += (sender, e) =>  
    { 
      //Do something 
    }; 
    AccessoryView = button; 
  } 
} 

现在,你的GetCell实现看起来可能如下所示:

public override UITableViewCell GetCell(
   UITableView tableView, NSIndexPath indexPath) 
{ 
  var cell = tableView.DequeueReusableCell("MyCell") as MyCell; 
  //Remaining cell setup here 
  return cell; 
} 

由于按钮不是一个局部变量,它不会比需要的时候更早地被垃圾回收。这样可以避免崩溃,并且在某些方面,这段代码看起来更整洁。在 Android 上,C#与 Java 之间的交互也可能出现类似情况;然而,由于两者都是垃圾回收语言,这种情况不太可能发生。

概括

在本章中,我们开始学习设置 iOS 供应配置文件的过程,以便部署到 iOS 设备。接下来,我们查看了将应用程序部署到 Android 设备所需的设备设置。我们发现了 Xamarin 链接器,以及它如何使应用程序变得更小、性能更好。我们讨论了解决由你的代码和链接器引起问题的各种设置,并解释了 iOS 上的 AOT 编译及其出现的限制。最后,我们涵盖了 Xamarin 应用程序可能遇到的常见内存陷阱。

在移动设备上测试 Xamarin 应用程序有多种原因。由于 Xamarin 必须绕过的平台限制,一些错误只能在设备上显示。你的电脑强大得多,因此在使用模拟器与物理设备上的性能表现会有所不同。在下一章中,我们将使用 Windows Azure 创建一个真实的网络服务来驱动我们的 XamChat 应用程序。我们将使用一个名为 Azure Mobile Services 的功能,并在 iOS 和 Android 上实现推送通知。

第八章:联系人、相机和位置

当前移动应用程序最关键的一些特性基于我们的设备可以收集的新类型数据。像 GPS 位置和相机这样的功能是 Instagram 或 Twitter 等现代应用程序的基石。开发一个应用程序而不使用这些功能是非常困难的。因此,让我们探讨使用 Xamarin 利用这一功能的方法。

在本章中,我们将执行以下操作:

  • 介绍 Xamarin.Mobile 库

  • 在 Android 和 iOS 上读取通讯录

  • 获取我们设备的 GPS 位置

  • 从相机和照片库中提取照片

介绍 Xamarin.Mobile

为了简化这些特性在多个平台上的开发,Xamarin 开发了一个名为 Xamarin.Mobile 的库。它为 iOS、Android 甚至 Windows 平台提供了一个单一的 API,用于访问联系人、GPS 位置、屏幕方向、相机和照片库。它还利用 任务并行库TPL)提供一个现代的 C# API,使开发者比使用原生替代方案更高效。这使你能够使用 C# 中的 asyncawait 关键字编写优美、清晰的异步代码。你还可以在 iOS 和 Android 上重用相同的代码,除了 Android 平台所必需的一些差异。

要安装 Xamarin.Mobile,请在 Xamarin Studio 中打开 Xamarin 组件商店,并将 Xamarin.Mobile 组件添加到项目中,如下面的截图所示:

介绍 Xamarin.Mobile

在我们深入了解如何使用 Xamarin.Mobile 之前,让我们回顾一下该库提供的命名空间和功能:

  • Xamarin.Contacts:这包含了使你能够与完整通讯录交互的类。它包括从联系人的照片、电话号码、地址、电子邮件、网站等所有内容。

  • Xamarin.Geolocation:结合加速度计,这可以让你访问设备的 GPS 位置,包括高度、屏幕方向、经度、纬度和速度。你可以明确跟踪设备的位置,或者随着时间的推移监听 GPS 位置的变化。

  • Xamarin.Media:这可以访问设备的摄像头(如果设备有多个摄像头)和内置照片库。这是向任何应用程序添加照片选择功能的一种简单方法。

Xamarin.Mobile 是一个开源项目,采用标准的 Apache 2.0 许可证。你可以为项目做贡献或在 GitHub 页面提交问题,地址是github.com/xamarin/Xamarin.Mobile。请随意在您的应用程序中使用 Xamarin.Mobile,或者为了自己的目的对其进行分叉和修改。

在本章中,我们将向之前章节构建的 XamSnap 示例应用程序添加许多功能。如有需要,你可能希望访问第六章,XamSnap for Android,或者参考本书附带的示例源代码。

访问联系人

为了开始探索 Xamarin.Mobile 提供的内容,让我们访问 Xamarin 应用程序内的地址簿。通过从用户的联系人列表加载朋友,来改进 XamSnap 的添加好友功能。确保从组件商店为 iOS 和 Android 项目添加 Xamarin.Mobile。

导航至XamSnap可移植类库。首先,我们需要将IWebService接口拆分,通过将一个方法移动到新的IFriendService接口中:

public interface IFriendService 
{ 
    Task<User[]> GetFriends(string userName); 
} 

接下来,在FriendViewModel中,我们需要使用新的IFriendService接口而不是旧的接口:

private IFriendService friendService =  
  ServiceContainer.Resolve<IFriendService>(); 

public async Task GetFriends() 
{ 
  //previous code here, use 'friendService' instead of 'service' 
  Friends = await friendService.GetFriends(settings.User.Name);  
} 

现在,我们需要在 iOS 项目中实现IFriendService,以便能够从设备的联系人列表中加载。导航至XamSnap.iOS项目,并添加一个实现IFriendService的新类:

public class ContactsService : IFriendService 
{ 
  public async Task<User[]> GetFriends(string userName) 
  { 
    var book = new Xamarin.Contacts.AddressBook(); 
    await book.RequestPermission(); 

    var users = new List<User>(); 
    foreach (var contact in book) 
    { 
      users.Add(new User 
      { 
        Name = contact.DisplayName, 
      }); 
    } 
    return users.ToArray();     
  } 
} 

要使用 Xamarin.Mobile 加载联系人,你首先必须创建一个AddressBook对象。接下来,我们需要调用RequestPermissions来请求用户允许访问地址簿。这是一个重要的步骤,因为 iOS 设备要求在应用程序访问用户联系人之前必须这样做。这防止了可能恶意应用在用户不知情的情况下获取联系人。

接下来,我们使用foreach遍历AddressBook对象,并创建现有应用程序已经理解的User对象的实例。这正是 MVVM 设计模式在分层方面的优势的绝佳例子。当我们更换模型层的逻辑时,UI 仍然可以正常工作,无需任何更改。

接下来,我们需要修改我们的AppDelegate.cs文件,以使用我们的ContactsService作为IFriendService接口:

ServiceContainer.Register<IFriendService>( 
  () => new ContactsService()); 

如果在这个时候编译并运行应用程序,你会看到标准的 iOS 弹窗,请求访问联系人,如下面的截图所示:

访问联系人

如果你意外点击了不允许,可以通过导航到设备上的设置 | 隐私 | 联系人来更改此设置。在 iOS 模拟器中,还可以通过关闭应用程序并前往设置 | 通用 | 重置 | 重置位置与隐私来重置所有隐私提示。

如果我们的应用程序被授予了正确的访问权限,我们应该能够看到联系人列表,而无需修改应用程序 UI 层的任何代码。以下屏幕截图显示了 iOS 模拟器中的默认联系人列表:

访问联系人

在 Android 上检索联系人

以非常类似的方式,我们可以使用 Xamarin.Mobile 在 Android 中获取联系人列表。Xamarin.Mobile 中的所有 API 在 Android 上都是相同的,除了在某些地方需要传递Android.Content.Context。这是因为许多原生 Android API 需要引用当前活动(或其他如Application的上下文)才能正常工作。首先,通过在 Xamarin Studio 中导航到Android | Android Application创建一个标准的 Android 应用程序项目。确保从组件商店向项目添加 Xamarin.Mobile。

按如下方式添加IFriendService的 Android 等效项:

public class ContactsService : IFriendService 
{ 
  public async Task<User[]> GetFriends(string userName) 
  { 
    var book = new  
        Xamarin.Contacts.AddressBook(Application.Context); 
    await book.RequestPermission(); 

    var users = new List<User>(); 
    foreach (var contact in book) 
    { 
      users.Add(new User 
      { 
        Name = contact.DisplayName, 
      }); 
    } 
    return users.ToArray();     
  } 
} 

这段调用 Xamarin.Mobile 的代码与我们为 iOS 编写的代码相同,不同之处在于这里需要为AddressBook构造函数中的 Android Context传递Application.Context。我们的代码修改完成了;但是,如果你现在运行应用程序,将会抛出异常。Android 需要在清单文件中要求权限,这样当从 Google Play 下载时,它会通知用户其访问通讯录的权限。

我们必须修改AndroidManifest.xml文件,并按以下方式声明一个权限:

  1. 打开 Android 项目的项目选项。

  2. 构建下选择Android Application标签页。

  3. 所需权限部分,勾选ReadContacts

  4. 点击OK保存更改。

现在如果你运行应用程序,你将获得设备上所有联系人的列表,如下截图所示:

在 Android 上获取联系人

查找 GPS 位置

使用 Xamarin.Mobile 跟踪用户的 GPS 位置与访问他们的联系人一样简单。iOS 和 Android 设置访问权限的过程类似,但在位置的情况下,你无需从代码请求权限。iOS 会自动显示标准警报请求权限。而 Android 只需要在清单中进行设置。

举个例子,让我们为 XamSnap 应用添加一个功能,在聊天对话中为消息标记 GPS 位置。你可以将其视为像其他应用一样给照片标记位置。确保从组件商店向项目添加 Xamarin.Mobile。

首先,让我们实现一个用于存储纬度和经度的Location类:

public class Location
{
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

接下来,让我们在Message类中添加一个Location属性:

public Location Location { get; set; }

现在,让我们创建一个新的ILocationService接口,用于查询 GPS 位置:

public interface ILocationService
{
    Task<Location> GetCurrentLocation();
}

现在,我们需要更新MessageViewModel类,以使用位置服务并在新消息上标记 GPS 位置:

//As a member variable
private ILocationService locationService = 
  ServiceContainer.Resolve<ILocationService>();
//Then in SendMessage()
var location = await locationService.GetCurrentLocation();
var message = await service.SendMessage(new Message
{
    UserName = settings.User.Name,
    Conversation = Conversation.Id,
    Text = Text,
    Location = location,
});

接下来,让我们为 iOS 实现ILocationService接口。在 iOS 项目中创建一个新类:

public class LocationService : ILocationService 
{ 
  private const int Timeout = 3000; 
  private Geolocator _geolocator; 

  public async Task<Location> GetCurrentLocation() 
  { 
    try 
    { 
      //NOTE: wait until here to create Geolocator 
      //  so that the iOS prompt appears on GetCurrentLocation() 
      if (_geolocator == null) 
        _geolocator = new Geolocator(); 

      var location = await _geolocator.GetPositionAsync(Timeout); 

      Console.WriteLine("GPS location: {0},{1}", 
        location.Latitude, location.Longitude); 

      return new Location 
      { 
        Latitude = location.Latitude, 
        Longitude = location.Longitude, 
      }; 
    } 
    catch (Exception exc) 
    { 
      Console.WriteLine("Error finding GPS location: " + exc); 

      //If anything goes wrong, just return null 
      return null; 
    } 
  } 
} 

我们在这里所做的首先是在需要时创建一个Geolocator对象。这样可以延迟 iOS 权限弹窗,直到你实际去发送消息。然后我们使用async/await查询 GPS 定位,并设置三秒的超时时间。我们记录找到的位置并创建一个新的Location对象,供应用程序的其余部分使用。如果发生任何错误,我们确保记录它们并将我们的Location实例返回为null

接下来,在AppDelegate.cs中注册我们的新服务:

ServiceContainer.Register<ILocationService>( 
  () => new LocationService()); 

最后,在我们的Info.plist文件中有一个设置是 iOS 访问用户位置所必需的,并且它还允许开发者在权限弹窗中显示一条消息。

打开Info.plist文件,并按如下所示更改:

  1. 点击源代码标签。

  2. 点击添加新条目行上的加号按钮。

  3. 在下拉菜单中,选择使用期间的位置访问描述

  4. 字段中为用户输入文本。

如果你编译并运行应用程序,你应该会在添加新消息时看到一个 iOS 权限提示,如下面的截图所示:

查找 GPS 定位

如果你观察 Xamarin Studio 中的控制台日志,你将能够看到 GPS 坐标被添加到Message对象中。为了实际工作,你将需要部署到物理 iOS 设备上才能看到返回的 GPS 定位。

实现 Android 上的 GPS 定位

正如前一部分所述,使用 Xamarin.Mobile 获取 GPS 位置与我们在 iOS 上使用的 API 几乎相同。首先,我们需要像之前一样创建一个ILocationService,只需更改一行我们为 iOS 创建的代码:

if (_geolocator == null) 
  _geolocator = new Geolocator(Application.Context); 

然后,在Application.cs中注册我们的新服务:

ServiceContainer.Register<ILocationService>( 
  () => new LocationService()); 

同样,这看起来与 iOS 的代码相同,除了Geolocator的构造函数。如果在这一点上运行应用程序,它将开始运行且没有错误。然而,Geolocator对象不会触发任何事件。我们首先需要从 Android 清单文件中添加访问位置的权限。在OnResume中开始定位器,在OnPause中停止它也是一个好主意。这将通过在屏幕上不再显示此活动时停止 GPS 定位来节省电池。

让我们创建一个AndroidManifest.xml文件,并声明两个权限,如下所示:

  1. 打开 Android 项目的项目选项。

  2. 构建下选择Android 应用程序标签。

  3. 点击添加 Android 清单

  4. 所需权限部分,勾选AccessCoarseLocationAccessFineLocation

  5. 点击确定保存你的更改。

现在,如果你编译并运行应用程序,你将获得与新发送的消息关联的 GPS 定位信息。大多数 Android 模拟器都有模拟 GPS 定位的选项。x86 HAXM 模拟器位于底部点菜单下,然后是扩展控制 | 位置,如下面的截图所示:

在 Android 上实现 GPS 定位

访问照片库和相机

Xamarin.Mobile 的最后一个主要功能是访问照片,以使用户能够向你的应用程序添加自己的内容。使用一个名为MediaPicker的类,你可以从设备的相机或照片库中获取照片,并可以选择性地为操作显示你自己的 UI。

让我们修改MessageViewModel以支持照片。首先,添加以下属性:

public string Image { get; set; } 

接下来,我们需要修改SendMessage方法中的以下几行:

if (string.IsNullOrEmpty(Text) && string.IsNullOrEmpty(Image))
   throw new Exception("Message is blank.");

//Then further down 
var message = await service.SendMessage(new Message
{
     UserName = settings.User.Name,
     Conversation = Conversation.Id,
     Text = Text,
     Image = Image,
     Location = location,
});
//Clear our variables 
Text =
      Image = null;  

然后,我们需要修改 UI 层以提示选择照片。打开MessagesController.cs并在类的顶部添加以下变量:

UIBarButtonItem photo; 
MediaPicker picker; 

ViewDidLoad方法中,我们需要设置MediaPicker和一个新的UIBarButtonItem来选择照片:

picker = new MediaPicker(); 
photo = new UIBarButtonItem(UIBarButtonSystemItem.Camera,  
  (sender, e) => 
  { 
    //In case the keyboard is up 
    message.ResignFirstResponder(); 

    var actionSheet = new UIActionSheet("Choose photo?"); 
    actionSheet.AddButton("Take Photo"); 
    actionSheet.AddButton("Photo Library"); 
    actionSheet.AddButton("Cancel"); 
    actionSheet.Clicked += OnActionSheetClicked; 
    actionSheet.CancelButtonIndex = 2; 
    actionSheet.ShowFrom(photo, true); 
  }); 

在这里我们使用UIActionSheet类来提示用户决定他们是想拍摄新照片还是打开现有照片。现在让我们实现OnActionSheetClicked方法:

async void OnActionSheetClicked( 
  object sender, UIButtonEventArgs e) 
{ 
  MediaPickerController controller = null; 
  try 
  { 
    if (e.ButtonIndex == 0) 
    { 
      if (!picker.IsCameraAvailable) 
      { 
        new UIAlertView("Oops!",  
          "Sorry, camera not available on this device!", null,  
          "Ok").Show(); 
        return; 
      } 

      controller = picker.GetTakePhotoUI( 
        new StoreCameraMediaOptions()); 
      PresentViewController(controller, true, null); 

      var file = await controller.GetResultAsync(); 
      messageViewModel.Image = file.Path; 
      Send(); 
    } 
    else if (e.ButtonIndex == 1) 
    { 
      controller = picker.GetPickPhotoUI(); 
      PresentViewController(controller, true, null); 

      var file = await controller.GetResultAsync(); 
      messageViewModel.Image = file.Path; 
      Send(); 
    } 
  } 
  catch (TaskCanceledException) 
  { 
    //Means the user just cancelled 
  } 
  finally 
  { 
    controller?.DismissViewController(true, null); 
  } 
} 

使用MediaPicker非常直接;你只需调用GetTakePhotoUIGetPickPhotoUI来获取一个MediaPickerController实例。然后,你可以调用PresentViewController以模态形式在当前控制器顶部显示控制器。调用GetResultAsync之后,我们使用结果MediaFile对象将照片路径传递给我们的 ViewModel 层。还需要使用try-catch块,以防用户取消并调用DismissViewController隐藏模态界面。

接下来,我们需要修改UITableViewSource以显示照片:

public override UITableViewCell GetCell( 
  UITableView tableView, NSIndexPath indexPath)
  {
     var message = messageViewModel.Messages[indexPath.Row];
     bool isMyMessage = message.UserName == settings.User.Name;
     var cell = tableView.DequeueReusableCell( 
       isMyMessage ? MyCellName : TheirCellName);
     cell.TextLabel.Text = message.Text ?? string.Empty;
     cell.ImageView.Image = string.IsNullOrEmpty(message.Image) ?
       null : UIImage.FromFile(message.Image);
     return cell; 
  }  

我们需要处理的最后一个情况是在ViewWillAppear方法中:

//Just after subscribing to IsBusyChanged 
if (PresentedViewController != null) 
  return; 

如果我们不进行这项更改,选择照片后照片列表将会刷新,这可能导致一些奇怪的行为。

现在你应该能够运行应用程序并在屏幕上选择照片。以下屏幕截图显示了我从照片库中选择的 iOS 模拟器中的默认照片:

访问照片库和相机

在 Android 上访问照片

与 iOS 相比,我们在 Android 上需要使用稍微不同的模式从相机或照片库中检索照片。Android 中的一个常见模式是调用StartActivityForResult从另一个应用程序启动活动。当此活动完成后,将调用OnActivityResult以通知你的活动操作已完成。因此,Xamarin.Mobile 在 Android 上不能使用与 iOS 相同的 API。

首先,让我们修改 Android 的布局以处理照片。在Messages.axml中的EditText之前添加一个新的ImageButton,如下所示:

<ImageButton 
  android:layout_width="wrap_content" 
  android:layout_height="wrap_content" 
  android:id="@+id/photoButton" 
  android:layout_alignParentLeft="true" 
  android:src="img/ic_menu_camera" /> 

然后在EditText中添加android:layout_toRightOf="@+id/photoButton"属性。

接下来,我们需要按照以下方式修改MyMessageListItemTheirMessageListItem

<!-MyMessageListItem--> 
<ImageView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:id="@+id/myMessageImage" />
<TextView   android:text="Message"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:id="@+id/myMessageText"
   android:layout_margin="3dp"
   android:textColor="@android:color/holo_blue_bright"
   android:layout_toRightOf="@id/myMessageImage" /> 
<!-TheirMessageListItem--> 
<ImageView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:id="@+id/theirMessageImage" />
<TextView
   android:text="Message"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:id="@+id/theirMessageText"
   android:layout_margin="3dp"
   android:textColor="@android:color/holo_green_light"
   android:layout_alignParentRight="true" />  

在这两种情况下,修改 Android XML 要容易得多,因为设计师在向现有视图的左右添加新视图时有时会有些挑剔。

现在,让我们在MessagesActivity.cs文件的顶部添加几个成员变量,如下所示:

MediaPicker picker; 
ImageButton photoButton; 
bool choosingPhoto; 

接下来,让我们按如下方式重新排列OnCreate方法:

protected override void OnCreate(Bundle savedInstanceState) 
{ 
  base.OnCreate(savedInstanceState); 

  Title = viewModel.Conversation.UserName; 
  SetContentView(Resource.Layout.Messages); 
  listView = FindViewById<ListView>(Resource.Id.messageList); 
  messageText = FindViewById<EditText>(Resource.Id.messageText); 
  sendButton = FindViewById<Button>(Resource.Id.sendButton); 
  photoButton = FindViewById<ImageButton>( 
    Resource.Id.photoButton); 

  picker = new MediaPicker(this); 

  listView.Adapter = 
    adapter = new Adapter(this); 
  sendButton.Click += (sender, e) => Send();

  photoButton.Click += (sender, e) => 
  { 
    var dialog = new AlertDialog.Builder(this) 
      .SetTitle("Choose photo?") 
      .SetPositiveButton("Take Photo", OnTakePhoto) 
      .SetNegativeButton("Photo Library", OnChoosePhoto) 
      .SetNeutralButton("Cancel", delegate { }) 
      .Create(); 
    dialog.Show(); 
  }; 
} 

async void Send() 
{ 
  viewModel.Text = messageText.Text; 
  try 
  { 
    await viewModel.SendMessage(); 
    messageText.Text = string.Empty; 
    adapter.NotifyDataSetInvalidated(); 
  } 
  catch (Exception exc) 
  { 
    DisplayError(exc); 
  } 
} 

我们在这里所做的就是当点击photoButton时创建一个AlertDialog。这与我们在 iOS 上所做的完全相同,为用户提供选项,要么拍照,要么从现有的照片库中选择。我们还把sendButton的点击处理程序移到了一个Send方法中,这样我们可以重用它。

现在,让我们实现所需的OnTakePhotoOnChoosePhoto方法:

 void OnTakePhoto(object sender, EventArgs e)
 {
     var intent = picker.GetTakePhotoUI(
       new StoreCameraMediaOptions());
     choosingPhoto = true;
     StartActivityForResult(intent, 1);
 }
 void OnChoosePhoto(object sender, EventArgs e)
 {
     var intent = picker.GetPickPhotoUI();
     choosingPhoto = true;
     StartActivityForResult(intent, 1);
 } 

在每种情况下,我们都会调用GetPickPhotoUIGetTakePhotoUI以获取一个 Android Intent对象的实例。这个对象用于在应用程序内启动新的活动。StartActivityForResult也会启动Intent对象,并期望从新活动中返回一个结果。

接下来,我们需要实现OnActivityResult以处理当新活动完成时会发生什么:

protected async override void OnActivityResult(
  int requestCode, Result resultCode, Intent data)
{
   if (resultCode == Result.Ok)
   {
       var file = await data.GetMediaFileExtraAsync(this);
       viewModel.Image = file.Path;
       Send();
   }
} 

如果成功,我们将获取一个MediaFile并将它的路径传递给我们的 ViewModel 层。我们调用之前设置的Send方法,该方法用于发送消息。

我们还需要在OnResume方法中添加以下代码:

if (choosingPhoto) 
{
   choosingPhoto = false;
   return;
} 

这可以防止用户导航到新活动以选择照片然后返回时出现一些奇怪的行为。这和我们之前在 iOS 上需要做的事情非常相似。

为了使这些更改生效,我们需要修改我们的AndroidManifest.xml文件,并按如下声明两个权限:

  1. 打开 Android 项目的项目选项。

  2. 构建下选择Android 应用程序标签页。

  3. 点击添加 Android 清单

  4. 所需权限部分,勾选相机写入外部存储

  5. 点击确定以保存更改。

你现在应该能够运行应用程序并发送照片作为消息,如下截图所示:

在 Android 上访问照片

概要

在本章中,我们了解了 Xamarin.Mobile 库以及它如何以跨平台的方式加速常见任务。我们从地址簿中检索联系人,并随时间设置 GPS 位置更新。最后,我们从相机和照片库中加载照片。

完成本章后,你应该完全掌握 Xamarin.Mobile 库及其为跨平台开发提供的常见功能。它提供了干净、现代的 API,具有async/await功能,可以跨 iOS、Android 和 Windows Phone 访问。使用 Xamarin.Mobile 在不同平台上访问联系人、GPS 和照片是非常简单的。

在下一章中,我们将使用 Windows Azure 创建一个真实的网络服务,来驱动我们的 XamSnap 应用程序。我们将使用一个称为 Azure Functions 的功能,并在 iOS 和 Android 上实现推送通知。

第九章:带推送通知的 web 服务

现代移动应用程序以其网络连接性为特征。一个不与 web 服务器交互的移动应用既难得一见,也可能是一个无聊的应用。在本书中,我们将使用Windows Azure云平台为我们的 XamSnap 应用实现服务器端后端。我们将使用一个名为Azure Functions的功能,它非常适合作为我们应用程序的简单后端,并且可以通过Azure Notification Hubs发送推送通知。完成这一章节后,我们的 XamSnap 示例应用程序将更接近一个真正的应用程序,并允许其用户相互交互。

本章节,我们将涵盖以下主题:

  • Windows Azure 提供的服务

  • 设置你的 Azure 账户

  • Azure Functions 作为 XamSnap 的后端

  • 为 XamSnap 实现真实的 web 服务

  • 编写客户端代码以调用 Azure Functions

  • 使用苹果推送通知服务

  • 使用 Google Cloud Messaging 发送通知

学习 Windows Azure

Windows Azure 是微软在 2010 年推出的卓越云平台。Azure 提供了基础设施即服务IaaS)和平台即服务PaaS),用于构建现代的 web 应用程序和服务。这意味着它可以直接为你提供虚拟机的访问,你可以在其中部署任何你选择的操作系统或软件。这称为 IaaS。Azure 还提供了多个用于构建应用程序的平台,如Azure Web AppsSQL Azure。这些平台被称为 PaaS,因为你可以在高层次部署软件,并且不需要直接处理虚拟机或管理软件升级。

让我们了解 Windows Azure 提供的以下更常见服务:

  • 虚拟机:Azure 提供各种规模的虚拟机访问。你可以安装几乎任何你选择的操作系统;Azure 图库中有许多预制的发行版可供选择。

  • Web Apps:你可以部署任何类型的网站,这些网站将在 Microsoft IIS 中运行,从 ASP .NET 站点到 PHPNode.js

  • SQL Azure:这是基于云的微软 SQL Server 版本,它是一个功能完整的区域数据库管理系统RDMS)用于存储数据。

  • 移动应用:这是一个用于构建移动应用 web 服务的简单平台。它使用 SQL Azure 作为后端存储,并基于 Node.js 的简单 JavaScript 脚本系统来添加业务逻辑。

  • Azure Functions:Windows Azure 推出的首款支持新兴“无服务器”架构的产品,这成为了当今的热门词汇。你可以在浏览器中直接使用多种语言开发简单的 API、后台作业、web 钩子等。Azure 会根据传入的请求自动扩展你的函数。

  • 存储:Azure 提供了块存储,用于存储二进制文件,以及表存储,这是一种 NoSQL 数据持久化解决方案。

  • 服务总线(Service bus):这是一个基于云的解决方案,用于创建队列,以便与其他云服务之间的通信提供便利。它还包括通知中心,作为向移动应用提供推送通知的简单方式。

  • 通知中心(Notification Hubs):这是一种向 Android、iOS 和 Windows 设备等不同平台发送推送通知的简单方式。

  • DocumentDB:一个功能完备的 NoSQL 数据存储,与其他 NoSQL 数据库(如MongoDB)相当。

  • HDInsight:在 Windows Azure 中运行的 Apache Hadoop 版本,用于管理极大数据集,这也被称为大数据。

除了这些服务外,还有许多正在积极开发的新服务。我们将使用 Azure Functions,并利用 Azure Storage Tables,为 XamSnap 构建我们的 Web 服务。你可以访问windowsazure.com了解提供的完整价格和服务列表。

在本书中,我们选择使用 Windows Azure 作为 XamSnap 的 Web 服务后端进行演示,因为它与 C#、Visual Studio 和其他 Microsoft 工具相辅相成。但是,除了 Azure 之外,还有许多其他选择,你可能想要看看。选择 Xamarin 并不会限制你的应用程序可以交互的 Web 服务类型。

下面是一些更常见的服务:

  • Firebase:谷歌提供的这项服务与 Azure Mobile Apps 类似,包括数据存储和推送通知等功能。你可以访问firebase.google.com了解更多信息。

  • Urban airship:这项服务为跨多个平台的移动应用提供推送通知。你可以访问urbanairship.com了解更多信息。

  • 亚马逊网络服务(Amazon Web Services):这项服务是一个完整的云解决方案,与 Windows Azure 相当。它拥有部署云应用所需的一切,包括完全的虚拟机支持。此外,还有一个名为 AWS Mobile Hub 的功能,专门针对移动开发而定制。你可以访问aws.amazon.com获取更多信息。

此外,你可以使用本地 Web 服务器或低成本的托管服务,用你选择的语言和技术开发自己的 Web 服务。

设置你的 Azure 账户

要开始使用 Windows Azure 进行开发,你可以订阅一个月的免费试用,并获得 200 美元的 Azure 信用。与此相伴的是,它的许多服务都有免费层级,为你提供性能较低的版本。因此,如果你的试用期结束,你可以根据所使用的服务,继续开发,费用很少或没有。

首先,导航到azure.microsoft.com/en-us/free,然后执行以下步骤:

  1. 点击 开始免费 链接。

  2. 使用 Windows Live ID 登录。

  3. 出于安全考虑,通过你的手机或短信验证你的账户。

  4. 输入支付信息。这只在你超出消费限额时使用。在开发你的应用程序时,你不会意外超出预算——通常在真实用户开始与服务互动之前不会意外消费。

  5. 勾选我同意政策,并点击注册

  6. 检查最终设置并点击提交

如果所有必需的信息都正确输入,你现在终于可以访问你的 Azure 账户了。你可以点击页面右上角的门户链接来访问你的账户。将来,你可以在 portal.azure.com 管理你的 Azure 服务。

Azure 门户使用一组名为 blades 的面板,以便快速导航并深入了解更详细的信息,如下面的屏幕截图所示:

设置你的 Azure 账户

这就完成了你的 Windows Azure 注册。与 Apple 和 Google Play 开发者计划相比,这相当简单。随意尝试,但不必太担心花费问题。Azure 大多数服务都有免费版本,并且还提供一定量的免费带宽。你可以访问 azure.microsoft.com/en-us/pricing 获取更多关于定价的信息。

请注意,关于 Windows Azure 价格昂贵的误解很多。你可以在免费层为应用程序进行所有开发而不花一分钱。将应用程序投入生产时,你可以轻松地增加或减少虚拟机实例的数量,以控制成本。通常,如果你没有很多用户,你不会花很多钱。同样,如果你恰好有很多用户,你应该能赚取足够的收入。

探索 Azure Functions

对于 XamSnap 的服务器端,我们将使用 Azure Functions 以及 Azure Storage Tables 为应用程序提供后端存储。Azure Functions 是加速服务器端应用程序开发的简单解决方案,可以利用 Windows Azure 的所有功能。我们将使用 .NET 基础类库中的标准 HttpClient 类,从 C# 与服务进行交互。

Azure Functions 的几个亮点如下:

  • 你可以使用多种编程语言编写函数,如 JavaScript、C#、Python、PHP,以及一些脚本语言,如 Batch、Bash 和 PowerShell

  • Azure Functions 与 Visual Studio Team Services、Bitbucket 和 GitHub 集成,支持持续集成CI)场景

  • 你可以轻松地使用 Azure Active Directory、Windows Live ID、Facebook、Google 和 Twitter 设置身份验证

  • 函数可以通过 HTTP、计划或定时器、Azure 队列等触发

  • Azure Functions 真正实现了无服务器,并且可以动态扩展处理大量数据

你可以了解到为什么 Azure Functions 对于简单的移动应用程序是一个好的选择。加速开发以及它提供的许多特性非常适合我们的 XamSnap 示例应用程序。

portal.azure.com访问你的账户,并执行以下步骤来创建 Azure Function:

  1. 点击页面左上角的加号按钮。

  2. 通过菜单导航到计算 | 函数应用

  3. 输入你选择的域名,比如yourname-xamsnap

  4. 选择一个订阅,以便将服务放置在下面。

  5. 选择一个现有的资源组,或者创建一个新的名为xamsnap的资源组。

  6. 选择一个动态应用服务计划开始。如果你已经有了一个应用服务计划,可以使用现有以经典模式运行的计划。

  7. 选择一个现有的存储账户或创建一个新的。

  8. 查看你的最终设置并点击创建按钮。

管理门户将显示进度,创建你的 Azure Function App 实例可能需要几秒钟。

让我们创建一个简单的 Hello World 函数来观察其工作情况:

  1. 导航到你的 Function App。

  2. 点击快速入门

  3. 点击选择 C#的Webhook + API,然后点击创建此函数

  4. Azure 门户会提供一个快速浏览,如果需要,你可以跳过。

  5. 滚动到底部,点击运行以查看 Azure Function 的操作。

完成后,你应在日志窗口中看到输出,以及带有Hello Azure输出的成功 HTTP 请求。你应该会看到类似于以下截图的内容:

探索 Azure Functions

创建和调用 Azure Functions

为了开始为 XamSnap 设置后端,我们需要创建一个登录函数。我们还需要实现由应用程序其他部分使用的IWebService接口。由于我们的 MVVM 架构,我们应该能够替换当前正在使用的假服务,而不需要更改位于其上的任何层。

返回 Azure 门户,选择你的 Function App 实例,并执行以下步骤:

  1. 点击新建函数按钮。

  2. 选择空 - C#模板。

  3. 输入login作为函数名称。

  4. 点击创建按钮。

  5. 点击集成部分。

  6. 添加一个带有默认设置的HTTP触发器和输出,然后点击保存

  7. 添加一个Azure 表存储输出,将表名更改为users,然后点击保存

现在让我们为我们的函数编写一些代码,切换到开发部分,并添加以下代码作为起点:

#r "Microsoft.WindowsAzure.Storage" 

using System.Net; 
using System.Text; 
using Microsoft.WindowsAzure.Storage.Table; 

private const string PartitionKey = "XamSnap"; 

public static async Task<HttpResponseMessage>  
  Run(HttpRequestMessage req, CloudTable outputTable,  
  TraceWriter log) 
{ 
  dynamic data = await req.Content.ReadAsAsync<object>(); 
  string userName = data?.userName; 
  string password = data?.password; 

  if (string.IsNullOrEmpty(userName) ||  
    string.IsNullOrEmpty(password)) 
  { 
    return new HttpResponseMessage(HttpStatusCode.BadRequest); 
  } 
} 

首先,我们添加了对 Azure 存储 SDK 的引用。这是内置的,可供 Azure Functions 使用,我们稍后会用到它。接下来,我们添加了一些 using 语句和一个常量。我们创建了一个静态函数,处理我们之前定义的输入和输出。req是 HTTP 输入,outputTable是 Azure 表输出。log是一个TraceWriter,可用于调试和日志记录。最后,我们使用了内置方法将 POST 数据读取到usernamepassword变量中,以便在我们的函数中使用。

然后,我们需要填充我们功能的剩余部分。将此代码放在我们开始的功能的底部:

//Let's hash all incoming passwords 
password = Hash(password); 

var operation = TableOperation.Retrieve<User>( 
  PartitionKey, userName); 
var result = outputTable.Execute(operation); 
var existing = result.Result as User; 
if (existing == null) 
{ 
  operation = TableOperation.Insert(new User 
  { 
    RowKey = userName, 
    PartitionKey = PartitionKey, 
    PasswordHash = password, 
  }); 
  result = outputTable.Execute(operation); 

  if (result.HttpStatusCode == (int)HttpStatusCode.Created) 
  { 
    return new HttpResponseMessage(HttpStatusCode.OK); 
  } 
  else 
  { 
    return new HttpResponseMessage( 
      (HttpStatusCode)result.HttpStatusCode); 
  } 
} 
else if (existing.PasswordHash != password) 
{ 
  return new HttpResponseMessage(HttpStatusCode.Unauthorized); 
} 
else 
{ 
  return new HttpResponseMessage(HttpStatusCode.OK); 
} 

让我们总结一下我们在前面的 C# 中做了什么:

  1. 首先,我们用稍后要添加的函数对传入的密码进行哈希处理。请注意,Azure Functions 有内置的身份验证功能,这对于生产应用来说非常棒。对于我们的示例应用,我们至少采取措施,不将密码以明文形式存储到我们的数据库中。

  2. 接下来,我们使用了 Azure 存储 SDK 来检查现有用户。

  3. 如果没有结果,我们将继续创建一个新用户。分区键和行键是 Azure 表存储中的概念。在大多数情况下,你会选择一个键来分区你的数据,比如州或邮政编码,而行键是一个唯一的键。对于这个示例,我们只是为分区键使用了一个常量值。

  4. 否则,我们比较哈希密码并返回成功。

  5. 如果密码不匹配,我们将返回一个未经授权的错误代码。

之后,我们只需要一点代码来定义Hash函数和User类:

private static string Hash(string password) 
{ 
  var crypt = new System.Security.Cryptography.SHA256Managed(); 
  var hash = new StringBuilder(); 
  byte[] crypto = crypt.ComputeHash( 
    Encoding.UTF8.GetBytes(password), 0,  
    Encoding.UTF8.GetByteCount(password)); 
  foreach (byte b in crypto) 
  { 
    hash.Append(b.ToString("x2")); 
  } 
  return hash.ToString(); 
} 

public class User : TableEntity 
{ 
  public string PasswordHash { get; set; } 
} 

我们使用了System.Security命名空间中内置的 SHA-256 哈希算法。这至少比常见的被破解的 MD5 哈希要安全一些。我们还声明了User类作为一个表实体,并带有一个额外的列包含哈希。

在这里,只需确保点击保存按钮以应用你的更改。Azure Functions 还提供了通过几个源代码控制提供程序为你的脚本提供源代码控制的选项。如果你想在本地的你喜欢的编辑器而不是网站编辑器中更改脚本,可以充分利用这个功能。你应该能够通过以下示例 JSON 测试该功能:

{ 
  "userName":"test", 
  "password":"password" 
} 

要获取 Azure 存储 SDK 的完整文档,请确保查看 MSDN:msdn.microsoft.com/en-us/library/azure/mt347887.aspx

在 C# 中使用 HttpClient

我们的 server-side 更改完成后,下一步是在我们的 XamSnap iOS 和 Android 应用程序中实现我们的新服务。幸运的是,由于我们使用了名为IWebService的接口,我们只需实现该接口即可在我们的应用程序中使其工作。

现在,通过执行以下步骤,我们可以在 iOS 应用程序中开始设置我们的服务:

  1. 打开我们之前在书中创建的XamSnap.Core项目。

  2. 在项目中创建一个Azure文件夹。

  3. 创建一个名为AzureWebService.cs的新类。

  4. 将类设置为public并实现IWebService

  5. 在你的代码中右键点击IWebService,选择重构 | 实现接口

  6. 将会出现一行;按Enter键以插入方法存根。

当这个设置完成后,你的类看起来会像下面这样:

public class AzureWebService : IWebService 
{ 
  #region IWebService implementation 

  public Task<User> Login(string username, string password) 
  { 
    throw new NotImplementedException(); 
  } 

  // -- More methods here --  

  #endregion 
} 

接下来,我们需要添加对 JSON .NET 库的引用。为此,我们将使用 NuGet 来添加库。右键点击XamSnap.Core项目,选择添加 | 添加包,并安装 Json .NET。

现在,让我们修改我们的AzureWebService.cs文件。为了开始,我们将进行以下更改:

using System.Net.Http; 
using System.Net.Http.Headers; 
using System.Threading.Tasks; 
using Newtonsoft.Json; 

public class AzureWebService : IWebService 
{ 
  private const string BaseUrl =  
    "https://xamsnap.azurewebsites.net/api/"; 
  private const string ContentType = "application/json"; 
  private readonly HttpClient httpClient = new HttpClient(); 

  // -- Existing code here -- 
} 

我们定义了一些 using 语句和几个变量,这些将在这个类中用到。请确保你填写了 Azure Function App 的正确 URL。

接下来,让我们编写一些辅助方法,以简化调用网络请求的过程:

private async Task<HttpResponseMessage> Post( 
  string url, string code, object obj) 
{ 
  string json = JsonConvert.SerializeObject(obj); 
  var content = new StringContent(json); 
  content.Headers.ContentType =  
    new MediaTypeHeaderValue(ContentType); 

  var response = await httpClient.PostAsync( 
    BaseUrl + url + "?code=" + code, content); 
  response.EnsureSuccessStatusCode(); 
  return response; 
} 

private async Task<T> Post<T>(string url, string code, object obj) 
{ 
  var response = await Post(url, code, obj); 
  string json = await response.Content.ReadAsStringAsync(); 
  return JsonConvert.DeserializeObject<T>(json); 
}} 

这段代码的大部分是在 C#中实现调用 RESTful 端点的基础。首先,我们将对象序列化为 JSON,并创建一个带有头部声明为 JSON 的StringContent对象。我们用code参数格式化 URL,这是 Azure Functions 默认开启的一个简单安全机制。接下来,我们向服务器发送一个 POST 请求,并调用EnsureSuccessStatusCode,以便对失败的请求抛出异常。最后,我们添加了第二个方法,将 JSON 响应解析为 C#对象。我们的某些 Azure Functions 将返回数据,所以我们需要这个功能。

现在,让我们按照以下方式实现我们的第一个方法Login

public async Task<User> Login(string userName, string password) 
{ 
  await Post("login", "key_here", new 
  { 
    userName, 
    password, 
  }); 

  return new User 
  { 
    Name = userName, 
    Password = password, 
  }; 
}} 

这非常简单,因为我们已经设置了辅助方法。我们只需要传递我们的函数名称、它的键以及代表我们想要传递给 HTTP 请求的 JSON 的对象。你可以在 Azure Portal 的开发部分下的Function URL找到所需的键。

接下来,打开AppDelegate.cs文件以设置我们的新服务,并添加以下代码:

//Replace this line 
ServiceContainer.Register<IWebService>( 
  () => new FakeWebService()); 

//With this line 
ServiceContainer.Register<IWebService>( 
  () => new AzureWebService()); 

现在,如果你在登录时编译并运行你的应用程序,你的应用应该能够成功调用 Azure Function,并将新用户插入 Azure Table Storage。

提示:

如果你正在寻找一个快速管理 Azure Tables 的方法,微软已经发布了一个免费的工具,叫做 Azure Storage Explorer。它适用于 Mac OS X 和 Windows,可以在storageexplorer.com找到。第二个选择是 Visual Studio 中的Cloud Explorer,如果你安装了 Azure SDK for .NET,就可以使用。

添加更多的 Azure Functions。

我们还需要实现几个方法,用于我们的IWebService实现。让我们从添加两个新的 Azure Functions 开始,一个用于获取用户朋友列表,另一个用于添加朋友。

返回 Azure Portal,执行以下步骤:

  1. 点击新建函数按钮。

  2. 选择Empty - C#模板。

  3. 输入friends作为函数名称。

  4. 点击创建按钮。

  5. 点击集成部分。

  6. 添加一个带有默认设置的HTTP触发器和输出,然后点击保存

  7. 添加一个Azure Table Storage输入,将表名更改为friends,然后点击保存

  8. 对名为addfriend的第二个函数重复这些步骤,但将Azure Table Storage设置为输出而不是输入。

接下来,让我们使用以下 C#代码实现friends Azure Function:

#r "Microsoft.WindowsAzure.Storage" 

using System.Net; 
using Microsoft.WindowsAzure.Storage.Table; 

public async static Task<HttpResponseMessage> Run( 
  HttpRequestMessage req, IQueryable<TableEntity> inputTable, 
  TraceWriter log) 
{ 
    dynamic data = await req.Content.ReadAsAsync<object>(); 
    string userName = data?.userName; 
    if (string.IsNullOrEmpty(userName)) 
    { 
      return new HttpResponseMessage(HttpStatusCode.BadRequest); 
    } 

    var results = inputTable 
      .Where(r => r.PartitionKey == userName) 
      .Select(r => new { Name = r.RowKey }) 
      .ToList(); 
    return req.CreateResponse(HttpStatusCode.OK, results); 
} 

这比我们的login函数简单一些。Azure Functions 可以选择使用不同于我们之前使用的CloudTable的不同类型的参数。当使用IQueryable时,我们只需编写 LINQ 表达式即可提取此函数所需的数据:指定用户的 friend 列表。我们计划将用户的名字作为PartitionKey,朋友的名字作为RowKey。然后我们只需在 HTTP 响应中返回这些值。

现在,让我们使用以下 C#代码实现addfriend函数:

#r "Microsoft.WindowsAzure.Storage" 

using System.Net; 
using Microsoft.WindowsAzure.Storage.Table; 

public async static Task<HttpResponseMessage> Run( 
  HttpRequestMessage req, CloudTable outputTable, TraceWriter log) 
{ 
  dynamic data = await req.Content.ReadAsAsync<object>(); 
  string userName = data?.userName; 
  string friendName = data?.friendName; 
  if (string.IsNullOrEmpty(userName) || 
    string.IsNullOrEmpty(friendName)) 
  { 
    return new HttpResponseMessage(HttpStatusCode.BadRequest); 
  } 

  var operation = TableOperation.InsertOrReplace(new TableEntity 
  { 
    PartitionKey = userName, 
    RowKey = friendName, 
  }); 
  var result = outputTable.Execute(operation); 

  return req.CreateResponse( 
    (HttpStatusCode)result.HttpStatusCode); 
} 

就像之前使用login函数一样,我们使用CloudTable向 Azure Storage Table 添加一行。同样,我们处理空白输入的可能性,并返回 Azure Storage SDK 返回的相同状态码。

最后,让我们修改AzureWebService.cs

public Task<User[]> GetFriends(string userName) 
{ 
  return Post<User[]>("friends", "key_here", new 
  { 
    userName 
  }); 
}
public async Task<User> AddFriend( 
  string userName, string friendName) 
{ 
  await Post("addfriend", "key_here", new 
  { 
    userName, 
    friendName 
  }); 

  return new User 
  { 
    Name = friendName 
  }; 
} 

我们调用本章前面创建的帮助方法,以便轻松处理 HTTP 输入和输出到我们的 Azure Functions。确保为每个 Azure Function 使用正确的密钥。您可能需要使用工具向friends Azure Storage 表插入或填充一些测试数据,以便我们的 Azure Function 可以处理。

最后,我们需要创建三个更多的 Azure Functions 来处理对话和消息。返回 Azure 门户,并执行以下步骤:

  1. 点击新建函数按钮。

  2. 选择Empty - C#模板。

  3. 输入conversations作为函数名称。

  4. 点击创建按钮。

  5. 点击集成部分。

  6. 添加一个带有默认设置的HTTP触发器和输出,然后点击保存

  7. 添加一个Azure Table Storage输入,将表名更改为friends,然后点击保存

  8. 对名为messages的第二个函数重复这些步骤,表名为messages

  9. 对名为sendmessage的第三个函数重复这些步骤,但将Azure Table Storage设置为输出而不是输入。

conversations函数的 C#代码如下:

#r "Microsoft.WindowsAzure.Storage" 

using System.Net; 
using Microsoft.WindowsAzure.Storage.Table; 

public async static Task<HttpResponseMessage> Run( 
  HttpRequestMessage req, IQueryable<Conversation> inputTable, 
  TraceWriter log) 
{ 
  dynamic data = await req.Content.ReadAsAsync<object>(); 
  string userName = data?.userName; 
  if (string.IsNullOrEmpty(userName)) 
  { 
    return new HttpResponseMessage(HttpStatusCode.BadRequest); 
  } 

  var results = inputTable 
    .Where(r => r.PartitionKey == userName) 
    .Select(r => new { Id = r.RowKey, UserName = r.UserName }) 
    .ToList(); 
  return req.CreateResponse(HttpStatusCode.OK, results); 
} 

public class Conversation : TableEntity 
{ 
  public string UserName { get; set; } 
} 

这段代码几乎与我们之前编写的friends函数相同。但是,我们需要定义一个Conversation类,以便在表中对默认的RowKeyPartitionKey之外添加一个额外的列。

接下来,让我们为messages函数添加以下 C#代码:

#r "Microsoft.WindowsAzure.Storage" 

using System.Net; 
using Microsoft.WindowsAzure.Storage.Table; 

public async static Task<HttpResponseMessage> Run( 
  HttpRequestMessage req, IQueryable<Message> inputTable, 
  TraceWriter log) 
{ 
  dynamic data = await req.Content.ReadAsAsync<object>(); 
  string conversation = data?.conversation; 
  if (string.IsNullOrEmpty(conversation)) 
  { 
    return new HttpResponseMessage(HttpStatusCode.BadRequest); 
  } 

  var results = inputTable 
    .Where(r => r.PartitionKey == conversation) 
    .Select(r => new { Id = r.RowKey,  
      UserName = r.UserName, Text = r.Text }) 
    .ToList(); 
  return req.CreateResponse(HttpStatusCode.OK, results); 
} 

public class Message : TableEntity 
{ 
  public string UserName { get; set; } 
  public string Text { get; set; } 
} 

同样,对于我们为friendsconversations函数所做的,这应该非常直观。

最后,让我们按照以下方式为sendmessage函数添加以下代码:

#r "Microsoft.WindowsAzure.Storage" 

using System.Net; 
using Microsoft.WindowsAzure.Storage.Table; 

public async static Task<HttpResponseMessage> Run( 
  HttpRequestMessage req, CloudTable outputTable, TraceWriter log) 
{ 
  dynamic data = await req.Content.ReadAsAsync<object>(); 
  if (data == null) 
    return req.CreateResponse(HttpStatusCode.BadRequest); 

  var operation = TableOperation.InsertOrReplace(new Message 
  { 
    PartitionKey = data.Conversation, 
    RowKey = data.Id, 
    UserName = data.UserName, 
    Text = data.Text, 
  }); 
  var result = outputTable.Execute(operation); 

  return req.CreateResponse( 
    (HttpStatusCode)result.HttpStatusCode); 
} 

public class Message : TableEntity 
{ 
    public string UserName { get; set; } 
    public string Text { get; set; } 
} 

这个函数与我们处理addfriend的方式非常相似。在本章后面,我们将在该函数中发送推送通知。

在继续之前,让我们实现IWebService接口的其余部分。可以按照以下方式完成:

public Task<Conversation[]> GetConversations(string userName) 
{ 
  return Post<Conversation[]>("conversations", "key_here", new 
  { 
    userName 
  }); 
} 

public Task<Message[]> GetMessages(string conversation) 
{ 
  return Post<Message[]>("messages", "key_here", new 
  { 
    conversation 
  }); 
} 

public async Task<Message> SendMessage(Message message) 
{ 
  message.Id = Guid.NewGuid().ToString("N"); 
  await Post("sendmessage", "key_here", message); 
  return message; 
} 

我们客户端代码中的每个方法都非常简单,与我们调用其他 Azure 函数时所做的类似。SendMessage是我们唯一需要新做的一件事:为新的消息生成一个唯一的消息 ID。

这完成了我们IWebService的实现。如果你在此时运行应用程序,它将和之前一样运行,区别在于实际上应用程序正在与真实的网络服务器通信。新消息将保存在 Azure 存储表中,我们的 Azure 函数将处理所需的定制逻辑。请随意尝试我们的实现;你可能会发现一些 Azure 函数功能,它们与你的应用程序非常契合。

在这一点上,另一个好的练习是在我们的 Android 应用程序中设置AzureWebService。你应该能够在你的Application类中的ServiceContainer.Register调用进行替换。所有功能将完全与 iOS 相同。跨平台开发不是很好吗?

使用苹果推送通知服务

在 Azure 的角度来看,使用 Azure 通知中心在 iOS 上实现推送通知非常简单。最复杂的部分是完成苹果公司创建证书和配置文件的过程,以便配置你的 iOS 应用程序。在继续之前,请确保你有一个有效的 iOS 开发者计划账户,因为没有它你将无法发送推送通知。如果你不熟悉推送通知的概念,请查看苹果的文档,链接为tinyurl.com/XamarinAPNS

要发送推送通知,你需要设置以下内容:

  • 已注册的显式 App ID 与苹果

  • 针对该 App ID 的一个配置文件

  • 用于触发推送通知的服务器证书

苹果提供了开发和生产两种证书,你可以使用它们从你的服务器发送推送通知。

设置你的配置文件

让我们从developer.apple.com/account开始,执行以下步骤:

  1. 点击标识符链接。

  2. 点击窗口右上角的加号按钮。

  3. 为捆绑 ID 输入描述,例如XamSnap

  4. 显式 App ID部分输入你的捆绑 ID。这应该与你Info.plist文件中设置的捆绑 ID 相匹配,例如,com.yourcompanyname.xamsnap

  5. 应用服务下,确保勾选了推送通知

  6. 现在,点击继续

  7. 审核你的最终设置,然后点击提交

这将创建一个显式 App ID,类似于我们可以在以下屏幕截图中看到的 ID,我们可以使用它来发送推送通知:

设置你的配置文件

对于推送通知,我们必须使用一个显式 App ID 的配置文件,这不是一个开发证书。现在让我们设置一个配置文件:

  1. 点击右侧供应配置文件下的开发链接。

  2. 点击右上角的加号按钮。

  3. 勾选iOS 应用开发并点击继续

  4. 选择我们刚刚创建的应用 ID 并点击继续

  5. 选择开发者并点击继续

  6. 选择你将要使用的设备并点击继续

  7. 为配置文件输入一个名称并点击生成

  8. 下载配置文件并安装,或者在XCode偏好设置 | 账户中使用同步按钮。

完成后,你应该会看到一个如下所示的成功的网页:

设置你的供应配置文件

设置推送通知的证书

接下来,我们执行以下步骤来设置服务器需要的证书:

  1. 点击右侧证书下的开发链接。

  2. 点击右上角的加号按钮。

  3. 启用苹果推送通知服务 SSL(沙盒)并点击继续

  4. 像之前一样选择你的应用 ID 并点击继续

  5. 按照苹果的说明创建一个新的证书签名请求。你也可以参考第七章,在设备上部署和测试,或者找到之前的*.certSigningRequest文件。

  6. 然后,点击继续

  7. 上传签名请求文件并点击生成

  8. 接下来,点击下载

  9. 打开文件,将证书导入钥匙串

  10. 钥匙串中找到证书。它将被命名为Apple Development iOS Push Services,并包含你的捆绑 ID。

  11. 右键点击证书并将其导出到你的文件系统的某个位置。输入一个你能记住的密码。

这将创建我们需要从 Azure 通知中心向用户发送推送通知的证书。

返回 Azure 门户,执行以下步骤创建 Azure 通知中心:

  1. 导航到存放你的Azure Function App的资源组。

  2. 点击加号按钮,向资源组添加新服务。

  3. 选择一个通知中心名称命名空间,例如xamsnap

  4. 确保选择了所需的数据中心和资源组并点击创建

剩下的工作就是回到 Azure 门户,从你的 Azure 通知中心上传证书。你可以在通知服务 | 苹果(APNS) | 上传证书中找到这个设置,如下截图所示:

为推送通知设置证书

这个上传完成了我们需要从苹果方面进行的配置。

为推送通知进行客户端侧的更改

接下来,让我们回到 Xamarin Studio 中的XamSnap.iOS项目,进行客户端必要的推送通知更改。我们首先需要在共享代码中添加几个新的类。

在我们的 XamSnap PCL 项目中,创建一个名为INotificationService的新接口,如下所示:

public interface INotificationService 
{ 
  void Start(string userName); 
  void SetToken(object deviceToken); 
} 

接下来,我们需要在登录完成后调用Start。在LoginViewModel.cs中,在成功登录后添加以下几行:

//At the top of the class 
readonly INotificationService notificationService =  
  ServiceContainer.Resolve<INotificationService>();

//Later, after a successful login 
notificationService.Start(UserName); 

接下来,让我们在 iOS 项目中的一个名为AppleNotificationService的新类中实现这个接口,如下所示:

public class AppleNotificationService : INotificationService 
{ 
  private readonly CultureInfo enUS =  
    CultureInfo.CreateSpecificCulture("en-US"); 
  private SBNotificationHub hub; 
  private string userName; 
} 

我们需要定义一个CultureInfo对象供稍后使用,还需要两个私有变量,分别用于我们的通知中心和当前登录的用户名。

现在,让我们实现Start方法:

public void Start(string userName) 
{ 
  this.userName = userName; 

  var pushSettings =  
    UIUserNotificationSettings.GetSettingsForTypes( 
    UIUserNotificationType.Alert |  
    UIUserNotificationType.Badge |  
    UIUserNotificationType.Sound, null); 

  UIApplication.SharedApplication 
    .RegisterUserNotificationSettings(pushSettings); 
} 

我们将用户名存储在成员变量中,然后调用原生 iOS API 来为远程通知设置注册。

接下来,我们需要如下实现SetToken方法:

public void SetToken(object deviceToken) 
{ 
    if (hub == null) 
    { 
        hub = new SBNotificationHub("yourconnection", "xamsnap"); 
    } 

    string template = "{"aps": {"alert": "$(message)"}}"; 
    var tags = new NSSet(userName); 
    hub.RegisterTemplateAsync((NSData)deviceToken, "iOS",  
      template, DateTime.Now.AddDays(90).ToString(enUS), tags, 
      errorCallback => 
      { 
        if (errorCallback != null) 
          Console.WriteLine("Push Error: " + errorCallback); 
      }); 
}} 

首先,如有需要,我们创建了一个新的通知中心。确保将yourconnection替换为只有监听权限的真实连接字符串。这可以在 Azure 门户的设置 | 访问策略 | DefaultListenSharedAccessSignature中找到。接下来,我们声明了一个 iOS 模板,它使用message变量以 iOS 推送通知的正确格式。这是通知中心的一个特性,支持跨平台推送通知。最后,我们将设备令牌与通知中心注册,并记录可能发生的任何错误。

接下来,我们需要对AppDelegate.cs进行一些 iOS 特定的更改:

public override void DidRegisterUserNotificationSettings( 
  UIApplication application,  
  UIUserNotificationSettings notificationSettings) 
{ 
  application.RegisterForRemoteNotifications(); 
} 

public override void RegisteredForRemoteNotifications( 
  UIApplication application, NSData deviceToken) 
{ 
  var notificationService =  
    ServiceContainer.Resolve<INotificationService>(); 
  notificationService.SetToken(deviceToken); 
} 

public override void FailedToRegisterForRemoteNotifications( 
  UIApplication application, NSError error) 
{ 
  Console.WriteLine("Push Error: " + error.LocalizedDescription); 
} 

在前面的代码片段中,我们实现了一些重要方法。DidRegisterUserNotificationSettings是用户接受 iOS 权限弹窗时的回调。RegisteredForRemoteNotifications将在 Apple 成功从其服务器返回设备令牌时发生。我们将设备令牌通过INotificationService传递给 Azure 通知中心。我们还实现了FailedToRegisterForRemoteNotifications,以报告整个过程中可能发生的任何错误。

最后,我们需要添加一个小修改来注册我们的INotificationService实现:

ServiceContainer.Register<INotificationService>( 
  () => new AppleNotificationService()); 

从服务器端发送推送通知

由于我们已经成功为 iOS 配置了推送通知,现在是从我们的sendmessage Azure Function 实际发送它们的时候了。Azure Functions 开箱即支持通知中心,但在撰写本文时,无法将它们作为输出使用,并指定针对特定用户的标签。幸运的是,Azure Functions 只是 C#代码,因此我们可以轻松利用 Azure 通知中心 SDK 从代码手动发送推送通知。让我们切换到 Azure 门户,并在服务器端进行剩余的更改。

首先,让我们在顶部添加几条语句以包含 Azure 通知中心 SDK:

#r "Microsoft.Azure.NotificationHubs"  
using Microsoft.Azure.NotificationHubs; 

接下来,让我们添加一个快速发送推送通知的方法:

private async static Task SendPush( 
  string userName, string message) 
{ 
  var dictionary = new Dictionary<string, string>(); 
  dictionary["message"] = userName + ": " + message; 

  var hub = NotificationHubClient 
    .CreateClientFromConnectionString("yourconnection "xamsnap"); 
  await hub.SendTemplateNotificationAsync(dictionary, userName); 
} 

确保将yourconnection替换为具有发送监听权限的有效连接字符串。默认情况下,您可以在 Azure 门户中使用名为DefaultFullSharedAccessSignature的那个。

最后,我们需要在 Azure 函数被调用时实际发送推送通知:

//Place this right before returning the HTTP response 
await SendPush((string)data.UserName, (string)data.Text); 

要测试推送通知,请部署应用程序并使用辅助用户登录。登录后,你可以使用主页按钮将应用程序后台运行。接下来,在你的 iOS 模拟器上以主要用户身份登录并发送消息。你应该会收到推送通知,如下面的截图所示:

从服务器端发送推送通知

提示

如果你遇到一些问题,尝试从 Azure 门户下的通知中心发送测试通知,然后点击故障排除 | 测试发送。你可以使用本章中使用的原生格式或自定义模板格式发送测试通知。

实现 Google Cloud Messaging

由于我们已经在前面的共享代码和 Azure 上设置好了所需的一切,此时为 Android 设置推送通知将少很多工作。继续操作,你需要一个带有验证电子邮件地址的 Google 帐户;不过,如果你有的话,我建议使用在Google Play注册的账户。你可以参考关于 Google Cloud Messaging (GCM) 的完整文档,地址是 developers.google.com/cloud-messaging/

提示

请注意,Google Cloud Messaging 需要 Android 设备上安装了 Google APIs,并且 Android 操作系统至少是版本 2.2。

首先,访问 cloud.google.com/console,然后执行以下步骤:

  1. 点击创建项目按钮。

  2. 输入一个适当的项目名称,如XamSnap

  3. 同意服务条款

  4. 点击创建按钮。

  5. 在创建你的第一个项目时,你可能需要验证与你的账户关联的手机号码。

  6. 注意概述页面上的项目编号字段。我们稍后需要这个数字。

下面的截图展示了我们的项目小部件在仪表盘标签上的样子:

实现 Google Cloud Messaging

现在,我们可以按照以下步骤继续我们的设置:

  1. 点击使用 Google APIs小部件。

  2. 点击,搜索Google Cloud Messaging for Android

  3. 点击顶部的启用按钮以启用服务。你可能需要接受另一个协议。

  4. 点击顶部警告提示中出现的前往凭据

  5. 点击我需要哪些凭据?按钮。

  6. 点击限制密钥,选择IP 地址,并输入0.0.0.0/0

  7. 复制密钥到剪贴板以备后用,并点击保存

  8. 切换到 Azure 门户,导航到你的 Azure 通知中心实例中的通知服务 | Google (GCM)部分。

  9. API 密钥字段中粘贴 API 密钥,并点击保存。请注意,第一次,Google 控制台可能需要长达五分钟的时间密钥才能生效。

这就完成了我们在 Azure 方面的设置。我们需要为 Xamarin.Android 应用获取几个开源库。首先,从 NuGet 安装 Xamarin.Azure.NotificationHubs.Android,然后从 Xamarin 组件商店安装 Google Cloud Messaging Client

接下来,创建一个名为 Constants.cs 的新类,如下所示:

public static class Constants 
{ 
    public const string ProjectId = "yourprojectid"; 
    public const string ConnectionString = "yourconnectionstring"; 
    public const string HubName = "xamsnap"; 
} 

使用之前在 Google 云控制台 概览 页面找到的项目编号填写 ProjectId 值。ConnectionStringHubName 应该与为 iOS 输入的内容完全相同。

接下来,我们需要设置一些权限以支持我们应用中的推送通知。在这个文件中的命名空间声明之上,添加以下内容:

[assembly: Permission(Name =  
  "@PACKAGE_NAME@.permission.C2D_MESSAGE")] 
[assembly: UsesPermission(Name =  
  "@PACKAGE_NAME@.permission.C2D_MESSAGE")] 
[assembly: UsesPermission(Name =  
  "com.google.android.c2dm.permission.RECEIVE")] 
[assembly: UsesPermission( 
  Name = "android.permission.GET_ACCOUNTS")] 
[assembly: UsesPermission( 
  Name = "android.permission.WAKE_LOCK")] 

你也可以在我们的 AndroidManifest.xml 文件中进行这些更改;然而,使用 C# 属性可能更好,因为它在输入时提供了代码补全的能力。

接下来,创建另一个名为 PushBroadcastReceiver.cs 的新类,如下所示:

[BroadcastReceiver(Permission =  
  Gcm.Client.Constants.PERMISSION_GCM_INTENTS)] 
[IntentFilter(new string[] {  
  Gcm.Client.Constants.INTENT_FROM_GCM_MESSAGE },  
  Categories = new string[] { "@PACKAGE_NAME@" })] 
[IntentFilter(new string[] {  
  Gcm.Client.Constants.INTENT_FROM_GCM_REGISTRATION_CALLBACK },  
  Categories = new string[] { "@PACKAGE_NAME@" })] 
[IntentFilter(new string[] {  
  Gcm.Client.Constants.INTENT_FROM_GCM_LIBRARY_RETRY },  
  Categories = new string[] { "@PACKAGE_NAME@" })] 
public class PushBroadcastReceiver :  
  GcmBroadcastReceiverBase<PushHandlerService> 
{ } 

PushBroadcastReceiver.cs 类设置了 BroadcastReceiver,这是安卓应用之间通信的原生方式。关于这个主题的更多信息,请查看安卓文档中的相关内容:developer.android.com/reference/android/content/BroadcastReceiver.html.

接下来,创建最后一个名为 PushHandlerService.cs 的类,如下所示:

[Service] 
public class PushHandlerService : GcmServiceBase  
{ 
  public PushHandlerService() : base (PushConstants.ProjectNumber)  
  { } 
} 

现在,右键点击 GcmServiceBase 并选择 重构 | 实现抽象成员。接下来,让我们逐个实现每个成员:

protected async override void OnRegistered( 
  Context context, string registrationId) 
{     
  var notificationService =  
    ServiceContainer.Resolve<INotificationService>(); 
  notificationService.SetToken(registrationId); 
} 

上述代码与我们之前在 iOS 上的操作非常相似。我们只需将 registrationId 值发送给 INotificationService

接下来,当接收到消息时,我们需要编写以下代码:

protected override void OnMessage( 
  Context context, Intent intent) 
{ 
  string message = intent.Extras.GetString("message"); 
  if (!string.IsNullOrEmpty(message)) 
  { 
    var notificationManager = (NotificationManager) 
      GetSystemService(Context.NotificationService); 

    var notification = new NotificationCompat.Builder(this) 
      .SetContentIntent( 
        PendingIntent.GetActivity(this, 0,  
          new Intent(this, typeof(LoginActivity)), 0)) 
      .SetSmallIcon(Android.Resource.Drawable.SymActionEmail) 
      .SetAutoCancel(true) 
      .SetContentTitle("XamSnap") 
      .SetContentText(message) 
      .Build(); 
    notificationManager.Notify(1, notification); 
  } 
} 

这段代码实际上会从通知中提取值,并在安卓设备的消息中心显示它们。我们使用了内置资源 SymActionEmail 来在通知中显示一个电子邮件图标。

然后,我们只需要实现两个更多的抽象方法。现在,我们只需使用 Console.WriteLine 来报告这些事件,如下所示:

protected override void OnUnRegistered( 
  Context context, string registrationId) 
{ 
  Console.WriteLine("GCM unregistered!"); 
} 

protected override void OnError ( 
  Context context, string errorId) 
{ 
  Console.WriteLine("GCM error: " + errorId); 
} 

在未来的开发中,你应该考虑在调用 OnUnRegistered 时从 Azure 移除注册。有时,用户的 registrationId 会发生变化,因此这里是应用程序得到通知的地方。

接下来,我们需要为安卓实现 INotificationService。首先创建一个名为 GoogleNotificationService.cs 的新文件,并添加以下代码:

public class GoogleNotificationService : INotificationService 
{ 
  readonly Context context; 
  NotificationHub hub; 
  string userName; 

  public GoogleNotificationService(Context context) 
  { 
    this.context = context; 
  } 

  public void SetToken(object deviceToken) 
  { 
    hub = new NotificationHub( 
      Constants.HubName, Constants.ConnectionString, context); 
    try 
    { 
      string template = "{"data":{"message":"$(message)"}}"; 
      hub.RegisterTemplate((string)deviceToken,  
        "Android", template, userName); 
    } 
    catch (Exception exc) 
    { 
      Console.WriteLine("RegisterTemplate Error: " + exc.Message); 
    } 
  } 

  public void Start(string userName) 
  { 
    this.userName = userName; 
    GcmClient.CheckDevice(context); 
    GcmClient.CheckManifest(context); 
    GcmClient.Register(context, Constants.ProjectId); 
  } 
} 

接下来,打开 Application.cs 并添加以下行来注册我们的新服务:

ServiceContainer.Register<INotificationService>( 
  () => new GoogleNotificationService(this)); 

现在,如果你重复在 iOS 上测试推送通知的步骤,你应该能够向我们的安卓应用发送一个推送通知。甚至更好,你应该能够跨平台发送推送通知,因为 iOS 用户可以向安卓用户发送消息:

实现 Google 云消息传递

总结

在本章中,我们了解了 Windows Azure 提供的服务:基础设施即服务和平台即服务。我们注册了一个免费的 Windows Azure 账户并设置了一个 Azure Function App 实例。我们实现了客户端代码,以便针对我们的 Azure Functions 发起请求。最后,我们使用 Azure 通知中心为 iOS 实现了推送通知,以通过 Apple 推送通知服务和 Google 云消息将消息集中发送到 iOS 和 Android 设备。

使用 Azure Functions,我们可以在不编写太多服务器端代码的情况下完成任务。在下一章中,我们将探讨如何使用 Xamarin 使用第三方库。这包括从 Xamarin 组件商店到使用本地 Objective-C 或 Java 库的所有内容。

第十章:第三方库

Xamarin 支持.NET 框架的一个子集,但大部分包括了您在.NET 基类库中期望的所有标准 API。因此,大量的 C#开源库可以直接在 Xamarin 项目中使用。此外,如果一个开源项目没有 Xamarin 或可移植类库版本,将代码移植到 Xamarin 项目中通常非常直接。Xamarin 还支持调用原生 Objective-C 和 Java 库,因此我们将探索这些作为重用现有代码的额外手段。

在本章中,我们将涵盖以下内容:

  • Xamarin 组件商店

  • 移植现有的 C#库

  • Objective-C 绑定

  • Java 绑定

Xamarin 组件商店

向项目中添加第三方组件的主要且明显的方式是通过 Xamarin 组件商店。组件商店与所有 C#开发者都熟悉的NuGet 包管理器非常相似,不同之处在于组件商店还包含不免费的付费组件。所有 Xamarin 组件还必须包含完整的示例项目和入门指南,而 NuGet 在其包中并不固有地提供文档。

所有Xamarin.iOSXamarin.Android项目都带有一个Components文件夹。要开始使用,只需右键点击该文件夹,选择获取更多组件来启动商店对话框,如下面的截图所示:

Xamarin 组件商店

在撰写本书时,有超过 200 个组件可用于增强您的 iOS 和 Android 应用程序。这是寻找 Xamarin 应用程序中最常见组件的好地方。每个组件都附有插图、可能的演示视频、评论以及其他在购买付费组件之前需要的信息。

最知名且有用的组件如下:

  • Json.NET:这是在 C#中解析和序列化 JSON 的事实上的标准。

  • RestSharp:这是一个在.NET 中常用的简单 REST 客户端。

  • SQLite.NET:这是一个简单的对象关系映射ORM)工具,用于在移动应用程序中操作本地 SQLite 数据库。

  • Facebook SDK:这是 Facebook 提供的标准软件开发工具包,用于将 Facebook 的服务集成到您的应用程序中。

  • Xamarin.Mobile:这是一个跨平台库,通过公共 API 访问设备的联系人、GPS、照片库和相机。

  • ZXing.Net.Mobile:流行的条形码扫描库ZXingZebra Crossing)的.NET 版本。

请注意,其中一些库是原生 Java 或 Objective-C 库,而有些则是纯 C#库。Xamarin 从底层开始构建,以支持调用原生库,因此组件商店提供了许多 Objective-C 或 Java 开发者在开发移动应用程序时会使用的常见库。

你也可以将你自己的组件提交到组件商店。如果你有一个有用的开源项目,或者只是想赚点外快,创建一个组件很简单。我们在这本书中不会涉及,但可以访问components.xamarin.com/submit了解该主题的完整文档,如下面的截图所示:

Xamarin 组件商店

迁移现有 C#库

尽管 Xamarin 正在成为一个流行的平台,但许多开源.NET 库在支持Xamarin.iOSXamarin.Android方面还远远跟不上。但在这些情况下,你绝对不是没有机会。通常,如果库有 Silverlight 或 Windows Phone 版本,你可以简单创建一个 iOS 或 Android 类库,并添加文件,无需更改代码。

为了说明这个过程,让我们迁移一个没有 Xamarin 或可移植类库支持的的开源项目。我选择了一个名为Ninject的依赖注入库,因为它的实用性和与忍者的关联。更多关于该库的信息可以在www.ninject.org/找到。

让我们开始设置库以与 Xamarin 项目一起工作,如下所示:

  1. 首先,从github.com/ninject/ninject下载 Ninject 的源代码。

  2. 创建一个名为Ninject.iOS的新的解决方案,其中包含一个iOS 类库项目。

  3. Ninject主项目中的所有文件链接进来。确保使用添加现有文件夹对话框以加快此过程。

提示

如果你不太熟悉 GitHub,我建议下载 GitHub 桌面客户端,这是一个适用于 Windows 或 OS X 的优质客户端应用,可在desktop.github.com/找到。

现在,尝试编译Ninject.iOS项目;你会在一个名为DynamicMethodFactory.cs的文件中遇到几个编译错误,如下面的截图所示:

迁移现有 C#库

打开DynamicMethodInjectorFactory.cs文件,并注意文件顶部以下代码:

#if !NO_LCG 
namespace Ninject.Injection 
{ 
    using System; 
    using System.Reflection; 
    using System.Reflection.Emit; 
    using Ninject.Components; 

/// *** File contents here *** 

#endif 

由于苹果平台的限制,在 iOS 上无法使用System.Reflection.Emit。幸运的是,库作者创建了一个名为NO_LCG(代表轻量级代码生成)的预处理器指令,以允许库在不支持System.Reflection.Emit的平台运行。

为了修复我们的 iOS 项目,请按照以下步骤操作:

  1. 打开项目选项,导航到构建 | 编译器部分。

  2. 配置下拉菜单中,为调试发布定义符号字段添加NO_LCG

  3. 点击确定以保存你的更改。

如果你现在编译项目,它将成功完成,并创建一个Ninject.iOS.dll文件,你可以从任何Xamarin.iOS项目中引用它。你也可以直接引用Ninject.iOS项目,而不是使用*.dll文件。

在这一点上,你可能希望重复该过程以创建一个Xamarin.Android类库项目。幸运的是,Xamarin.Android支持System.Reflection.Emit,所以如果你愿意,可以跳过添加额外的预处理器指令。

Objective-C 绑定

Xamarin 开发了一个复杂的系统,用于在 iOS 项目中从 C#调用本地 Objective-C 库。Xamarin.iOS的核心使用相同的技术来调用UIKitCoreGraphics和其他 iOS 框架中的本地 Apple API。开发者可以使用简单的接口和属性创建 iOS 绑定项目,将 Objective-C 类和方法暴露给 C#。

为了帮助创建 Objective-C 绑定,Xamarin 创建了一个名为Objective Sharpie的小工具,它可以处理 Objective-C 头文件并导出有效的 C#定义,以便添加到绑定项目中。这个工具是大多数绑定的良好起点,在大多数情况下,它可以让你的绑定项目完成大约 75%的工作。大多数时候,你可能想要手动编辑并精细调整,使其更友好地适应 C#。

提示

请注意,iOS 绑定项目可以在 Visual Studio 中创建;然而,Objective Sharpie 是一个 OS X 的命令行工具。它利用了 Xcode 中包含的工具,因此 iOS 绑定开发最好在 Mac OS X 上完成。

作为示例,我们将为 iOS 编写 Google Analytics 库的绑定。这是一个简单且有用的库,可以跟踪你的 iOS 或 Android 应用程序中的用户活动。在编写时,Google Analytics SDK 的版本是 3.17,因此随着新版本的发布,这些说明可能会发生变化。

developer.xamarin.com/guides/cross-platform/macios/binding/objective-sharpie/下载并安装 Objective Sharpie,并执行以下步骤:

  1. tinyurl.com/GoogleAnalyticsForiOS下载最新的 iOS Google Analytics SDK。

  2. 创建一个新的iOS | 绑定库项目,名为GoogleAnalytics.iOS

  3. 从第一步中提取 zip 文件的内容,并将GoogleAnalytics文件夹移动到与绑定项目相同的目录中。

  4. 打开终端并导航到新项目的同一目录。

  5. 使用以下命令运行Objective Sharpie

        sharpie bind --output=. --namespace=GoogleAnalytics.iOS 
          --sdk=iphoneos10.0 ./GoogleAnalytics/Library/*.h 
        mv -f ApiDefinitions.cs ApiDefinition.cs 
        mv -f StructsAndEnums.cs Structs.cs 

Objective Sharpie 将输出两个文件:ApiDefinitions.csStructs.cs。接下来的两个命令将把文件复制到由绑定库项目模板创建的默认文件之上。

提示

请注意,在编写此命令时,使用了 iOS 10 SDK。要发现你需要为--sdk选项输入什么,请运行sharpie xcode --sdks,你将在输出中看到打印出的值。

现在,回到你的绑定项目,你会注意到 Objective Sharpie 已经为库中头文件中发现的每个类生成了一个接口定义。它还生成了库使用的许多 enum 值,并在可能的情况下更改大小写和命名约定以更接近 C#。

在阅读绑定内容时,你会注意到几个 C# 属性,它们定义了关于 Objective-C 库的不同方面,例如以下内容:

  • BaseType:这会将接口声明为一个 Objective-C 类。基类(也称为超类)会传递给属性。如果没有基类,应使用 NSObject

  • Export:这会在 Objective-C 类上声明一个方法或属性。传递一个将 Objective-C 名称映射到 C# 名称的字符串。Objective-C 方法名通常如下形式:myMethod:someParam:someOtherParam

  • Static:这会将方法或属性标记为 C# 中的 static

  • Bind:用于属性上,将 getter 或 setter 映射到不同的 Objective-C 方法。Objective-C 属性可以为属性的 getter 或 setter 重命名。

  • NullAllowed:这允许将 null 传递给方法或属性。默认情况下,如果发生这种情况,将抛出异常。

  • Field:这会声明一个 Objective-C 字段,在 C# 中作为公共变量暴露。

  • Model:这标识了一个类到 Xamarin.iOS,其方法可以选择性地被重写。这通常用于 Objective-C 委托。

  • Internal:这用 C# 内部关键字标记生成的成员。它可以用来隐藏那些你不想暴露给外部世界的特定成员。

  • Abstract:这标识了一个 Objective-C 方法为必需的,与 Model 密切相关。在 C# 中,它将生成一个抽象方法。

需要知道的唯一其他规则是如何定义构造函数。由于 C# 接口不支持构造函数,Xamarin 必须为此发明一个约定。

要定义除了默认构造函数之外的构造函数,请使用以下代码:

[Export("initWithFrame:")] 
IntPtr Constructor(RectangleF frame); 

这将在类上定义一个构造函数,该构造函数以 RectangleF 作为参数。方法名 Constructor 和返回类型 IntPtr 会让 Xamarin 编译器生成一个构造函数。

现在,让我们回到我们的绑定项目以完成所有设置。如果在这一点上编译项目,你会得到几个编译错误。让我们逐一修复它们,如下所示:

  1. 将 Google Analytics 下载中的 libGoogleAnalyticsServices.alibAdIdAccess.a 添加为本地引用

  2. 更改 Structs.cs 中找到的枚举 GAILogLevelGAIDispatchResult 的基类型为 ulong

  3. ApiDefinitions.cs 中找到的 Constants 类中移除 [Static] 的重复声明。

  4. 移除所有的 Verify 属性。这些是 Objective Sharpie 对其执行的操作不确定的地方。在我们的示例中,它们都是好的,所以安全地移除它们。

在这一点上,如果你尝试在 iOS 项目中使用该库,你会得到如下错误:

Error MT5210: Native linking failed, undefined symbol:
 _FooBar. Please verify that all the necessary frameworks
 have been referenced and native libraries are properly
 linked in.

我们需要定义 Objective-C 库使用的其他框架和库。这类似于 C#中引用的工作方式。如果我们查看 Google Analytics 文档,它会告诉你必须添加CoreDataSystemConfigurationlibsqlite3.dylib

右键点击到libGoogleAnalyticsServices的本地引用,选择属性,并进行以下更改:

  1. Frameworks设置为CoreData SystemConfiguration

  2. Linker Flags设置为-lsqlite3

原生 Objective-C 库通过以下选项之一引用其他库:

  • 框架:将它们添加到LinkWith属性的Frameworks值中,用空格分隔。

  • 弱框架:以同样的方式将它们添加到LinkWith属性的WeakFrameworks属性中。弱框架是可以忽略的库(如果找不到)。在这种情况下,iOS 6 中添加了AdSupport;然而,这个库仍然可以在旧版本的 iOS 上工作。

  • 动态库:如libz.dylib可以在LinkerFlags中声明。通常,去掉.dylib扩展名,并将lib替换为-l

实施这些更改后,你将能够从 iOS 项目中成功使用该库。要了解有关 Objective-C 绑定的完整文档,请访问 Xamarin 文档网站:developer.xamarin.com/guides/ios/

Java 绑定

与 iOS 类似,Xamarin 完全支持通过Xamarin.Android从 C#调用 Java 库。原生 Android SDK 以这种方式工作,开发者可以利用Android Java Bindings项目在 C#中利用其他原生 Java 库。这里的主要区别是,与 Objective-C 绑定相比,手动操作要少得多。Java 语法与 C#非常相似,因此许多映射都是一一对应的。此外,Java 的库中包含了元数据信息,Xamarin 利用这些信息自动生成调用 Java 所需的 C#代码。

举个例子,让我们为 Google Analytics SDK 的 Android 版本创建一个绑定。在开始之前,下载 SDK:developers.google.com/analytics/devguides/collection/android/v3/。在撰写本文时,Google Analytics 正在迁移到 Google Play Services,但我们将使用这个 Java 库作为一个练习,用于创建供 C#使用的 Java 绑定。

让我们按照以下步骤开始创建 Java 绑定:

  1. 在 Xamarin Studio 中启动一个全新的Android | Library | Bindings Library项目。如果你愿意,可以使用与 iOS 相同的解决方案。

  2. 将项目命名为GoogleAnalytics.Droid

  3. 从 Android SDK 中将libGoogleAnalyticsServices.jar添加到项目下的Jars文件夹中。

  4. 构建项目。你将得到一些错误,我们稍后会解决这些问题。

你在 Java 绑定上花费的大部分时间将用于修复阻止生成的 C#代码编译的小问题。但是不要担心;许多库在第一次尝试时无需进行任何更改就能正常工作。通常,Java 库越大,你需要做的工作就越多,以使其与 C#一起工作。

提示

请注意,如果你首次编译时没有错误,但是有许多警告,提示类似于unsupported major.minor version 52.0的内容,那么你需要安装较新版本的 Java JDK。从tinyurl.com/XamarinJDK8下载 JDK 1.8,并在设置中指向 Xamarin Studio 或 Visual Studio 的新版本 JDK。

你可能会遇到以下问题类型:

  • Java 混淆:如果库通过像ProGuard这样的混淆工具运行,那么类和方法名称可能不是有效的 C#名称。

  • 协变返回类型:Java 对于子类中重写方法的返回类型有不同的规则。因此,你可能需要修改生成的 C#代码的返回类型以编译通过。

  • 可见性:Java 的访问性规则与 C#的不同;子类中方法的可见性可以改变。有时你需要在 C#中改变可见性以使其编译通过。

  • 命名冲突:有时,C#代码生成器可能会犯一些错误,生成两个名称相同的成员或类。

  • Java 泛型:Java 中的泛型类常常会在 C#中引起问题。

在 Java 绑定中使用 XPath

因此,在我们开始解决 Java 绑定中的这些问题之前,首先让我们清理项目中的命名空间。默认情况下,Java 命名空间的形式为com.mycompany.mylibrary,所以让我们将定义更改为更接近 C#的形式。在项目的Transforms目录中,打开Metadata.xml,并在根元数据节点内添加以下 XML 标签:

<attr path="/api/package[@name='com.google.analytics.tracking   
  .android']" name="managedName">GoogleAnalytics.Tracking</attr> 

attr节点告诉 Xamarin 编译器需要替换 Java 定义中的什么内容,以另一个值。在这种情况下,我们将包的managedName替换为GoogleAnalytics.Tracking,因为它在 C#中更有意义。路径值可能看起来有点奇怪,这是因为它使用了名为XPath的 XML 匹配查询语言。一般来说,可以把它看作是 XML 的模式匹配查询。要了解 XPath 语法的完整文档,请查看网络上的一些资源,例如w3schools.com/xpath

在这一点上,你可能会问自己,XPath 表达式与什么匹配?回到 Xamarin Studio,在顶部的解决方案上右键点击。选择 显示选项 | 显示所有文件。在 obj 文件夹下的 Debug 文件夹中打开 api.xml。这是 Java 定义文件,描述了 Java 库中的所有类型和方法。你可能注意到这里的 XML 直接与我们即将编写的 XPath 表达式相关。

接下来的一步,让我们移除所有我们不打算在此库中使用的包(或命名空间)。对于大型库来说,这通常是个好主意,因为你不想浪费时间修复你甚至不会从 C# 调用的库部分的问题。

Metadata.xml 中添加以下声明:

<remove-node path="/api/package[@name='com.google.analytics
   .containertag.common']" /> 
<remove-node path="/api/package[@name='com.google.analytics
   .containertag.proto']" /> 
<remove-node path="/api/package[@name='com.google.analytics
   .midtier.proto.containertag']" /> 
<remove-node path="/api/package[@name='com.google.android
   .gms.analytics.internal']" /> 
<remove-node path="/api/package[@name='com.google.android
   .gms.common.util']" /> 
<remove-node 
   path="/api/package[@name='com.google.tagmanager']" /> 
<remove-node
   path="/api/package[@name='com.google.tagmanager.proto']" /> 
<remove-node
   path="/api/package[@name='com.google.tagmanager.protobuf.nano']" /> 

提示

请注意,移除这些命名空间实际上并没有从你的绑定中删除编译后的 Java 代码。它只是阻止绑定项目生成使用此命名空间中的类的 C# 代码。

现在当你构建库时,我们可以开始解决问题。你收到的第一个错误将是如下所示的内容:

GoogleAnalytics.Tracking.GoogleAnalytics.cs(74,74):
 Error CS0234: The type or namespace name `TrackerHandler'
 does not exist in the namespace `GoogleAnalytics.Tracking'.
 Are you missing an assembly reference?

如果我们在 api.xml 文件中找到 TrackerHandler,我们会看到以下类声明:

<class
   abstract="true" deprecated="not deprecated"
   extends="java.lang.Object"
   extends-generic-aware="java.lang.Object"
   final="false" name="TrackerHandler"
   static="false" visibility=""/> 

那么,你能发现问题所在吗?我们需要填写 visibility XML 属性,不知何故它是空的。在 Metadata.xml 中添加以下行:

<attr
  path="/api/package[@name='com.google.analytics
  .tracking.android']/class[@name='TrackerHandler']"
  name="visibility">public</attr> 

这个 XPath 表达式将在 com.google.analytics.tracking.android 包内定位 TrackerHandler 类,并将 visibility 更改为 public

如果你现在构建项目,它将成功完成,但会有一些警告。在 Java 绑定项目中,尽可能修复警告是个好主意,因为它们通常表示一个类或方法被排除在绑定之外。注意以下警告:

GoogleAnalytics.Droid: Warning BG8102:
 Class GoogleAnalytics.Tracking.CampaignTrackingService has 
 unknown base type android.app.IntentService (BG8102) 
 (GoogleAnalytics.Droid)

要解决这个问题,在 api.xml 中找到 CampaignTrackingService 的类型定义,如下所示:

<class
   abstract="false" deprecated="not deprecated"
   extends="android.app.IntentService"
   extends-generic-aware="android.app.IntentService"
   final="false" name="CampaignTrackingService"
   static="false" visibility="public"> 

解决此问题的方法是将基类更改为 Xamarin.AndroidIntentService 的定义。在 Metadata.xml 中添加以下代码:

<attr
   path="/api/package[@name='com.google.analytics
   .tracking.android']/class[@name='CampaignTrackingService']"
   name="extends">mono.android.app.IntentService</attr> 

这将 extends 属性更改为使用 Mono.Android.dll 中的 IntentService。我通过在 Xamarin Studio 的 程序集浏览器 中打开 Mono.Android.dll 并查看 Register 属性找到了这个类的 Java 名称,如下面的截图所示:

在 Java 绑定中使用 XPath

在 Xamarin Studio 中查看 *.dll 文件,你只需打开它们即可。你也可以在你的项目中的 References 文件夹里双击任何程序集。

如果你现在构建绑定项目,我们剩下最后一个错误,如下所示:

GoogleAnalytics.Tracking.CampaignTrackingService.cs(24,24):
 Error CS0507:
 `CampaignTrackingService.OnHandleIntent(Intent)':
 cannot change access modifiers when overriding `protected' 
 inherited member
 `IntentService.OnHandleIntent(Android.Content.Intent)'
 (CS0507) (GoogleAnalytics.Droid)

如果你导航到 api.xml 文件,你可以看到 OnHandleIntent 的定义如下:

<method
   abstract="false" deprecated="not deprecated" final="false"
   name="onHandleIntent" native="false" return="void"
   static="false" synchronized="false" visibility="public"> 

我们可以看到,这个类的 Java 方法是public,但基类是protected。因此,最好的解决办法是将 C# 版本也改为protected。编写一个匹配此条件的 XPath 表达式要复杂一些,但幸运的是,Xamarin 有一个简单的方法来获取它。如果你在 Xamarin Studio 的错误面板中双击错误消息,你会在生成的 C# 代码中看到以下注释:

// Metadata.xml XPath method reference:
   path="/api/package[@name='com.google.analytics
   .tracking.android']/class[@name='CampaignTrackingService']
   /method[@name='onHandleIntent' and count(parameter)=1 and
   parameter[1][@type='android.content.Intent']]" 

复制path的值,并在Metadata.xml中添加以下内容:

<attr path="/api/package[@name='com.google.analytics
   .tracking.android']/class[@name='CampaignTrackingService']
   /method[@name='onHandleIntent' and count(parameter)=1 and
   parameter[1][@type='android.content.Intent']]"
   name="visibility">protected</attr> 

现在,我们可以构建项目,并且只有与[Obsolete]成员被覆盖相关的警告(无需担心)。这个库现在可以用于你的Xamarin.Android项目中了。

但是,如果你开始使用这个库,会注意到方法的参数名称是p0p1p2等等。以下是EasyTracker类的几个方法定义:

public static EasyTracker GetInstance(Context p0); 
public static void SetResourcePackageName(string p0); 
public virtual void ActivityStart(Activity p0); 
public virtual void ActivityStop(Activity p0); 

你可以想象,在不了解正确的参数名称的情况下使用 Java 库会有多困难。之所以这样命名参数,是因为 Java 库的元数据不包括为每个参数设置正确名称的信息。因此,Xamarin.Android尽其所能,按顺序自动为每个参数命名。

要重命名这个类中的参数,我们可以在Metadata.xml中添加以下内容:

<attr path="/api/package[@name='com.google.analytics
   .tracking.android']/class[@name='EasyTracker']
   /method[@name='getInstance']/parameter[@name='p0']"
   name="name">context</attr> 
<attr path="/api/package[@name='com.google.analytics
   .tracking.android']/class[@name='EasyTracker']
   /method[@name='setResourcePackageName']/parameter[@name='p0']"
   name="name">packageName</attr> 
<attr path="/api/package[@name='com.google.analytics
   .tracking.android']/class[@name='EasyTracker']
   /method[@name='activityStart']/parameter[@name='p0']"
   name="name">activity</attr> 
<attr path="/api/package[@name='com.google.analytics
   .tracking.android']/class[@name='EasyTracker'] 
  /method[@name='activityStop']/parameter[@name='p0']"
   name="name">activity</attr> 

在重新构建绑定项目时,这将有效地为EasyTracker类中的这四个方法重命名参数。此时,我建议你查看计划在应用程序中使用的类,并重命名这些参数,以便它们对你更有意义。你可能需要参考 Google Analytics 的文档来确保命名正确。幸运的是,SDK 中包含了一个javadocs.zip文件,提供了库的 HTML 参考资料。

要了解有关实现 Java 绑定的完整参考,请务必查看 Xamarin 的文档网站:developer.xamarin.com/guides/android/。我们在为 Google Analytics 库创建绑定时遇到的肯定还有比这更复杂的情况。

摘要

在本章中,我们从 Xamarin 组件商店向 Xamarin 项目添加了库,并将现有的 C# 库 Ninject 移植到了Xamarin.iOSXamarin.Android。接下来,我们安装了 Objective Sharpie 并探索了其生成 Objective-C 绑定的用法。最后,我们为 iOS 的 Google Analytics SDK 编写了一个功能性的 Objective-C 绑定,以及为 Android 的 Google Analytics SDK 编写了一个 Java 绑定。我们还编写了几个 XPath 表达式来清理 Java 绑定。

对于从您的 Xamarin.iOSXamarin.Android 应用程序中使用现有的第三方库,有几种可用的选项。我们从使用 Xamarin 组件商店、移植现有代码,以及设置可供 C# 使用的 Java 和 Objective-C 库等方面进行了全面了解。在下一章中,我们将介绍 Xamarin.Mobile 库,作为一种访问用户联系人、相机和 GPS 位置的方法。

第十一章:Xamarin.Forms

自从 Xamarin 公司成立以来,他们的宗旨一直是将 iOS 和 Android 的本地 API 以符合 C# 习惯的方式呈现出来。这在最初是一个很好的策略,因为使用 Xamarin.iOS 或 Xamarin.Android 构建的应用程序几乎与本地 Objective-C 或 Java 应用程序无法区分。代码共享通常仅限于非 UI 代码,这为 Xamarin 生态系统留下了一个潜在的空白:跨平台 UI 抽象。Xamarin.Forms 就是解决这一问题的方案,它是一个跨平台 UI 框架,在每个平台上渲染本地控件。Xamarin.Forms 是一个非常适合那些懂 C#(和 XAML)但可能不想深入了解本地 iOS 和 Android API 的人的框架。

在本章中,我们将进行以下操作:

  • 创建 Xamarin.Forms 中的 Hello World

  • 讨论 Xamarin.Forms 架构

  • 在 Xamarin.Forms 中使用 XAML

  • 学习 Xamarin.Forms 中的数据绑定和 MVVM

在 Xamarin.Forms 中创建 Hello World

要了解如何组合 Xamarin.Forms 应用程序,让我们先创建一个简单的 Hello World 应用程序。

打开 Xamarin Studio 并执行以下步骤:

  1. 从新建解决方案对话框中创建一个新的多平台 | 应用 | 表单应用项目。

  2. 为你的解决方案取一个合适的名字,比如 HelloForms

  3. 确保使用便携式类库被选中。

  4. 点击下一步,然后点击创建

注意到成功创建了以下三个新项目:

  • HelloForms

  • HelloForms.Android

  • HelloForms.iOS

Xamarin.Forms 应用程序中,大部分代码将是共享的,每个特定平台的项目只是一小部分代码,用于启动 Xamarin.Forms 框架。

让我们检查一下 Xamarin.Forms 应用程序的最小组成部分:

  • HelloForms PCL 库中的 App.xamlApp.xaml.cs —— 这个类是 Xamarin.Forms 应用程序的主要启动点。一个简单的属性 MainPage 被设置为应用程序中的第一页。在默认项目模板中,HelloFormsPage 被创建,其中有一个单一标签,在 iOS 上将渲染为 UILabel,在 Android 上将渲染为 TextView

  • HelloForms.Android Android 项目中的 MainActivity.cs —— Android 应用程序的主要启动活动。对于 Xamarin.Forms 来说,这里重要的是调用 Forms.Init(this, bundle),它初始化了 Xamarin.Forms 框架的 Android 特定部分。接下来是调用 LoadApplication(new App()),它启动了我们的 Xamarin.Forms 应用程序。

  • HelloForms.iOS iOS 项目中的 AppDelegate.cs —— 与 Android 非常相似,不同之处在于 iOS 应用程序使用 UIApplicationDelegate 类启动。Forms.Init() 将初始化 Xamarin.Forms 的 iOS 特定部分,与 Android 的 LoadApplication(new App()) 类似,将启动 Xamarin.Forms 应用程序。

接着运行 iOS 项目;你应该能看到类似以下截图的内容:

在 Xamarin.Forms 中创建 Hello World

如果您运行 Android 项目,将得到一个非常类似于以下屏幕截图所示的 iOS UI,但使用的是原生 Android 控件:

在 Xamarin.Forms 中创建 Hello World

提示

尽管这本书没有涉及,但 Xamarin.Forms 也支持 Windows Phone、WinRT 和 UWP 应用程序。然而,要为 Windows 平台开发,需要一台运行 Windows 和 Visual Studio 的 PC。如果您能让 Xamarin.Forms 应用程序在 iOS 和 Android 上运行,那么让 Windows Phone 版本运行应该轻而易举。

理解 Xamarin.Forms 背后的架构

开始使用 Xamarin.Forms 非常简单,但深入了解其幕后原理以理解一切是如何组合在一起的总是好的。在这本书的前几章中,我们使用原生 iOS 和 Android API 直接创建了一个跨平台应用程序。某些应用程序更适合这种开发方法,因此在选择最适合您应用框架时,理解 Xamarin.Forms 应用程序与传统 Xamarin 应用程序之间的区别非常重要。

Xamarin.Forms 是原生 iOS 和 Android API 之上的抽象,您可以直接从 C#调用。因此,Xamarin.Forms 使用的是与传统 Xamarin 应用程序相同的 API,同时提供了一个框架,允许您以跨平台的方式定义 UI。这样的抽象层在很多方面都是非常好的,因为它不仅允许您共享推动 UI 的代码,还可以共享可能在标准 Xamarin 应用程序中共享的任何后端 C#代码。然而,主要缺点是性能上会有轻微的影响,这可能使得创建完美流畅的体验变得更加困难。Xamarin.Forms 提供了编写渲染器效果的选项,允许您以特定于平台的方式覆盖 UI。这使您能够根据需要使用原生控件。

请查看以下图表中 Xamarin.Forms 应用程序与传统 Xamarin 应用程序之间的区别:

理解 Xamarin.Forms 背后的架构

在这两个应用程序中,应用程序的业务逻辑和后端代码可以共享,但 Xamarin.Forms 允许您也可以共享 UI 代码,这是一个巨大的优势。

此外,Xamarin.Forms 应用程序有两个项目模板可供选择,让我们来了解每个选项:

  • Xamarin.Forms Shared:创建一个包含所有 Xamarin.Forms 代码的共享项目,以及 iOS 项目和 Android 项目

  • Xamarin.Forms Portable:创建一个包含所有共享 Xamarin.Forms 代码的便携式类库PCL),以及 iOS 项目和 Android 项目

通常来说,这两种选项对任何应用程序都适用。共享项目基本上是一组代码文件,当其他项目引用它时,这些文件会自动添加。使用共享项目允许你使用预处理器语句来实现平台特定的代码。另一方面,PCL 项目创建了一个可移植的.NET 程序集,可以在 iOS、Android 以及各种其他平台上使用。PCL 不能使用预处理器语句,因此你通常可以使用接口或抽象/基类来设置平台特定的代码。在大多数情况下,我认为 PCL 是一个更好的选择,因为它本质上鼓励更好的编程实践。有关这两种代码共享技术的优缺点,请参见第三章,iOS 和 Android 之间的代码共享

在 Xamarin.Forms 中使用 XAML

除了从 C#代码定义 Xamarin.Forms 控件外,Xamarin 还提供了在可扩展应用程序标记语言(XAML)中开发用户界面的工具。XAML 是一种声明性语言,基本上是一组映射到 Xamarin.Forms 框架中某个控件的 XML 元素。使用 XAML 类似于使用 HTML 定义网页上的 UI,不同之处在于 Xamarin.Forms 中的 XAML 创建代表原生 UI 的 C#对象。

为了了解 XAML 在 Xamarin.Forms 中的工作原理,让我们创建一个带有不同类型的 Xamarin.Forms 控件的新页面。回到之前的项目HelloForms,打开HelloFormsPage.xaml文件。在<ContentPage>标签之间添加以下 XAML 代码:

<StackLayout Orientation="Vertical" Padding="10,20,10,10"> 
    <Label Text="My Label" XAlign="Center" /> 
    <Button Text="My Button" /> 
    <Entry Text="My Entry" /> 
    <Image Source="https://www.xamarin.com/content/images/ 
      pages/branding/assets/xamagon.png" /> 
    <Switch IsToggled="true" /> 
    <Stepper Value="10" /> 
</StackLayout> 

接着在 iOS 上运行应用程序;你的应用程序看起来会像以下截图:

在 Xamarin.Forms 中使用 XAML

在 Android 上,应用程序与 iOS 看起来完全相同,只是使用了原生的 Android 控件而不是 iOS 的对应控件:

在 Xamarin.Forms 中使用 XAML

在我们的 XAML 中,我们创建了一个StackLayout控件,它是其他控件的容器。它可以按顺序垂直或水平地布局控件,由Orientation值定义。我们还应用了四周和底部 10 的填充,以及顶部 20 的调整以适应 iOS 状态栏。如果你熟悉 WPF 或 Silverlight,你可能熟悉这种定义矩形的语法。Xamarin.Forms 使用相同的语法,即左、上、右和下值,由逗号分隔。

我们还使用了几个内置的 Xamarin.Forms 控件来看看它们是如何工作的:

  1. Label:我们在这章前面已经使用过它。仅用于显示文本,在 iOS 上它对应于UILabel,在 Android 上对应于TextView

  2. Button:一个通用按钮,可以被用户点击。这个控件在 iOS 上映射到UIButton,在 Android 上映射到Button

  3. Entry:这个控件是单行文本输入。在 iOS 上它映射到UITextField,在 Android 上映射到EditText

  4. Image:这是一个简单的控件,用于在屏幕上显示图像,对应于 iOS 上的UIImage和 Android 上的ImageView。我们使用了这个控件的Source属性,它从网络地址加载图像。在这个属性上使用 URL 很好,但在可能的情况下最好将图像包含在你的项目中以获得最佳性能。

  5. Switch:这是一个开关或切换按钮。在 iOS 上映射到UISwitch,在 Android 上映射到Switch

  6. Stepper:这是一个通用输入控件,通过两个加减按钮来输入数字。在 iOS 上,这对应于UIStepper,而在 Android 上,Xamarin.Forms 使用两个按钮来实现这一功能。

这些只是 Xamarin.Forms 提供的一些控件。还有更复杂的控件,比如ListViewTableView,你可以期待它们用于提供移动 UI。

尽管在这个例子中我们使用了 XAML,但你也可以用 C# 实现 Xamarin.Forms 页面。下面是使用 C# 实现的例子:

public class UIDemoPageFromCode : ContentPage 
{ 
  public UIDemoPageFromCode() 
  { 
    var layout = new StackLayout  
    { 
      Orientation = StackOrientation.Vertical, 
      Padding = new Thickness(10, 20, 10, 10), 
    }; 

    layout.Children.Add(new Label  
    { 
      Text = "My Label", 
      XAlign = TextAlignment.Center, 
    }); 

    layout.Children.Add(new Button  
    { 
      Text = "My Button", 
    }); 

    layout.Children.Add(new Image  
    { 
      Source = "https://www.xamarin.com/content/images/pages/ 
        branding/assets/xamagon.png", 
    }); 

    layout.Children.Add(new Switch  
    { 
      IsToggled = true, 
    }); 

    layout.Children.Add(new Stepper  
    { 
      Value = 10, 
    }); 

    Content = layout; 
  } 
} 

因此,你可以看到使用 XAML 可能会更加易读,通常在声明 UI 方面比 C# 更好。但是,使用 C# 来定义你的 UI 仍然是一个可行且直接的方法。

使用数据绑定和 MVVM

在这一点上,你应该已经掌握了 Xamarin.Forms 的基础知识,但可能会想知道 MVVM 设计模式如何融入其中。MVVM 设计模式最初是为了与 XAML 一起使用,以及 XAML 提供的强大的数据绑定功能,因此它与 Xamarin.Forms 一起使用是一个完美的设计模式。

让我们了解一些关于如何使用 Xamarin.Forms 设置数据绑定和 MVVM 的基础知识:

  1. 你的模型和 ViewModel 层在本书前面讲到的 MVVM 模式中基本上保持不变。

  2. 你的 ViewModel 应该实现INotifyPropertyChanged接口,这有助于数据绑定。为了简化在 Xamarin.Forms 中的操作,你可以使用BindableObject基类,并在你的 ViewModel 中值发生变化时调用OnPropertyChanged

  3. Xamarin.Forms 中的任何Page或控件都有一个BindingContext,这是它绑定的数据对象。通常,你可以为每个视图的BindingContext属性设置一个相应的 ViewModel。

  4. 在 XAML 中,你可以使用Text="{Binding Name}"这样的语法设置数据绑定。这个例子将控件的Text属性绑定到BindingContext中的对象的Name属性。

  5. 结合数据绑定,事件可以使用ICommand接口转换为命令。因此,例如,Button的点击事件可以绑定到 ViewModel 公开的命令。Xamarin.Forms 中有一个内置的Command类来支持这一点。

提示

在 Xamarin.Forms 中,也可以使用Binding类通过 C# 代码设置数据绑定。但是,通常使用 XAML 设置绑定要容易得多,因为语法已经通过 XAML 标记扩展简化了。

现在我们已经了解了基础知识,让我们一步一步地部分转换本书前面提到的XamSnap示例应用程序以使用 Xamarin.Forms。在大多数情况下,我们可以重用 Model 和 ViewModel 层,尽管我们需要进行一些小改动以支持与 XAML 的数据绑定。

让我们从创建一个由 PCL 支持的新的 Xamarin.Forms 应用程序开始,名为XamSnap

  1. 首先,在XamSnap项目中创建三个名为ViewsViewModelsModels的文件夹。

  2. 从前面的章节中添加适当的ViewModelsModels类到XamSnap应用程序中,这些可以在XamSnap项目中找到。

  3. 构建项目,确保一切已保存。你可能会遇到几个编译错误,我们很快就会解决。

我们需要编辑的第一个类是BaseViewModel类;打开它并进行以下更改:

public class BaseViewModel : BindableObject 
{ 
  protected readonly IWebService service =  
    DependencyService.Get<IWebService>(); 
  protected readonly ISettings settings =  
    DependencyService.Get<ISettings>(); 

  bool isBusy = false; 

  public bool IsBusy 
  { 
    get { return isBusy; } 
    set 
    { 
      isBusy = value; 
      OnPropertyChanged(); 
    } 
  } 
} 

首先,我们移除了对ServiceContainer类的调用,因为 Xamarin.Forms 提供了自己的 IoC 容器,名为DependencyService。它的功能与我们前几章构建的容器非常相似,不同之处在于它只有一个方法Get<T>,并且通过我们很快会设置的程序集属性来设置注册。

此外,我们移除了IsBusyChanged事件,转而使用支持数据绑定的INotifyPropertyChanged接口。从BindableObject继承为我们提供了辅助方法OnPropertyChanged,我们用它来通知 Xamarin.Forms 中的绑定值已更改。注意我们没有向OnPropertyChanged传递包含属性名称的string。这个方法使用了.NET 4.0 的一个不太知名的特性,称为CallerMemberName,它将在运行时自动填充调用属性的名字。

接下来,让我们用DependencyService设置所需的服务。打开 PCL 项目根目录中的App.xaml.cs文件,并在命名空间声明上方添加以下两行:

[assembly: Dependency(typeof(XamSnap.FakeWebService))] 
[assembly: Dependency(typeof(XamSnap.FakeSettings))] 

DependencyService将自动识别这些属性并检查我们声明的类型。这些类型实现的任何接口都将返回给任何未来调用DependencyService.Get<T>的调用者。我通常将所有的Dependency声明放在App.cs文件中,这样它们易于管理和集中管理。

接下来,让我们通过添加一个新属性来修改LoginViewModel

public Command LoginCommand { get; set; } 

我们将很快使用它来绑定Button的命令。在视图模型层的最后一个变化是为MessageViewModel设置INotifyPropertyChanged

Conversation[] conversations; 

public Conversation[] Conversations 
{ 
  get { return conversations; } 
  set 
  { 
    conversations = value; 
    OnPropertyChanged(); 
  } 
} 

同样,你可以对视图模型层中其余的公共属性重复这个模式,但这个例子我们只需要这些。接下来,在Views文件夹中创建一个新的Forms ContentPage Xaml文件,名为LoginPage。在代码隐藏文件LoginPage.xaml.cs中,我们只需要进行一些更改:

public partial class LoginPage : ContentPage 
{     
  readonly LoginViewModel loginViewModel = new LoginViewModel(); 

  public LoginPage() 
  { 
    Title = "XamSnap"; 
    BindingContext = loginViewModel; 

    loginViewModel.LoginCommand = new Command(async () => 
    { 
      try 
      { 
        await loginViewModel.Login(); 
        await Navigation.PushAsync(new ConversationsPage()); 
      } 
      catch (Exception exc) 
      { 
        await DisplayAlert("Oops!", exc.Message, "Ok");                 
      } 
    }); 

    InitializeComponent(); 
  } 
} 

我们在这里做了几件重要的事情,包括将BindingContext设置为我们LoginViewModel。我们设置了LoginCommand,它基本上调用了Login方法,并在出现问题时显示消息。如果成功,它还会导航到一个新页面。我们还设置了Title,它将显示在应用程序的顶部导航栏中。

接下来,打开LoginPage.xaml,我们将在ContentPage内添加以下 XAML 代码:

<StackLayout Orientation="Vertical" Padding="10,10,10,10"> 
    <Entry  
        Placeholder="Username" Text="{Binding UserName}" /> 
    <Entry  
        Placeholder="Password" Text="{Binding Password}"  
        IsPassword="true" /> 
    <Button  
        Text="Login" Command="{Binding LoginCommand}" /> 
    <ActivityIndicator  
        IsVisible="{Binding IsBusy}"  
        IsRunning="true" /> 
</StackLayout> 

这将设置两个文本字段、一个按钮和一个微调器的基础,以及使所有内容正常工作的所有绑定。由于我们从LoginPage代码后台文件中设置了BindingContext,所有属性都绑定到了LoginViewModel

接下来,像之前一样创建一个 XAML 页面ConversationsPage,并编辑ConversationsPage.xaml.cs代码后台文件:

public partial class ConversationsPage : ContentPage 
{     
  readonly MessageViewModel messageViewModel =  
    new MessageViewModel(); 

  public ConversationsPage() 
  { 
    Title = "Conversations"; 
    BindingContext = messageViewModel; 

    InitializeComponent(); 
  } 

  protected async override void OnAppearing() 
  { 
    try 
    { 
      await messageViewModel.GetConversations(); 
    } 
    catch (Exception exc) 
    { 
      await DisplayAlert("Oops!", exc.Message, "Ok"); 
    } 
  } 
} 

在这个案例中,我们重复了许多相同的步骤。不同之处在于我们使用了OnAppearing方法来加载屏幕上显示的对话。

现在,将以下 XAML 代码添加到ConversationsPage.xaml中:

<ListView ItemsSource="{Binding Conversations}"> 
    <ListView.ItemTemplate> 
        <DataTemplate> 
            <TextCell Text="{Binding UserName}" /> 
        </DataTemplate> 
    </ListView.ItemTemplate> 
</ListView> 

在这个例子中,我们使用了ListView来数据绑定一个项目列表并在屏幕上显示。我们定义了一个DataTemplate类,它表示了列表中每个项目的单元集合,这些项目与ItemsSource进行了数据绑定。在我们的例子中,为Conversations列表中的每个项目创建了一个显示UsernameTextCell

最后但同样重要的是,我们必须回到App.xaml.cs文件并修改启动页面:

MainPage = new NavigationPage(new LoginPage());  

我们在这里使用了NavigationPage,以便 Xamarin.Forms 可以在不同的页面之间推送和弹出。这在 iOS 上使用了UINavigationController,因此你可以看到每个平台上是如何使用原生 API 的。

在这一点上,如果你编译并运行应用程序,你将得到一个功能性的 iOS 和 Android 应用程序,可以登录并查看对话列表:

使用数据绑定和 MVVM

概述

在本章中,我们介绍了 Xamarin.Forms 的基础知识以及如何用它来构建自己的跨平台应用程序。Xamarin.Forms 对于某些类型的应用程序非常出色,但如果你需要编写更复杂的 UI 或利用原生绘图 API,它可能会有所限制。我们了解了如何使用 XAML 声明 Xamarin.Forms UI,并理解了 Xamarin.Forms 控件在每个平台上的渲染方式。我们还深入探讨了数据绑定的概念以及如何将 MVVM 设计模式与 Xamarin.Forms 一起使用。最后但同样重要的是,我们从本书前面开始将XamSnap应用程序移植到 Xamarin.Forms,并且能够重用我们的大部分现有代码。

在下一章中,我们将介绍将应用程序提交到 iOS App Store 和 Google Play 的过程。将你的应用程序放入商店可能是一个耗时的过程,但下一章的指导将为你提供一个良好的开端。

第十二章:应用商店提交

既然你已经完成了跨平台应用程序的开发,下一步显然是将你的应用发布在 Google Play 和 iOS App Store 上。Xamarin 应用与 Java 或 Objective-C 应用完全以相同的方式分发;然而,将你的应用通过这个过程可能会有些痛苦。iOS 有一个官方的审批系统,这使得应用商店提交的过程比 Android 要长。开发者可能需要等待一周或更长时间,这取决于应用被拒绝的次数。与调试应用程序相比,Android 在提交应用到 Google Play 时需要一些额外的步骤,但你仍然可以在短短几小时内提交你的应用程序。

在本章中,我们将涵盖:

  • App Store 审核指南

  • 向 App Store 提交 iOS 应用

  • 设置 Android 签名密钥

  • 向 Google Play 提交 Android 应用

  • 在应用商店成功的技巧

遵循 iOS App Store 审核指南

你的应用程序名称、应用图标、屏幕截图和其他方面都在苹果公司的网站 iTunes Connect 上声明。销售报告、应用商店拒绝、合同和银行信息以及应用更新都可以通过网站itunesconnect.apple.com进行管理。

苹果公司指南的主要目的是保持 iOS App Store 的安全,免受恶意软件的侵害。iOS App Store 上几乎找不到恶意软件。通常,iOS 应用程序能对你做的最糟糕的事情就是向你推送大量广告。在一定程度上,这些指南还加强了应用内支付时苹果公司的收入分成。遗憾的是,苹果公司的一些有争议的指南在 iOS 上的关键领域排除了竞争对手:

遵循 iOS App Store 审核指南

这里的关键点是让你的应用程序通过商店审批过程,避免面临 App Store 拒绝。只要你不故意违反规定,大多数应用程序在获得批准时不会遇到困难。最常见的拒绝与开发者的错误有关,这是件好事,因为你不希望向公众发布一个带有严重问题的应用。

App Store 审核指南相当长,因此我们将分解成你可能遇到的最常见情况。完整的指南列表可以在developer.apple.com/app-store/review/guidelines/找到。

需要注意的一些通用规则包括:

  • 出现崩溃、有错误或严重失败的应用程序将被拒绝

  • 与宣传不符或含有隐藏功能的应用程序将被拒绝

  • 使用非公开 Apple API,或在文件系统上禁止的位置读写文件的应用程序将被拒绝

  • 提供价值不大或过度开发的应用(如手电筒、打嗝或放屁应用)将被拒绝

  • 应用程序不能未经商标持有者许可,将商标词作为应用名称或关键词

  • 应用程序不得非法分发受版权保护的材料

  • 可以通过移动友好的网站简单实现的应用程序,如包含大量 HTML 内容但没有本地功能的应用程序,可能会被拒绝

这些规则有助于保持 iOS App Store 的整体质量和安全性,否则可能会降低。由于这些规则,有时很难将功能非常少的应用程序放入商店,因此请确保您的应用程序足够有用和吸引人,以便 App Store 审核团队能够允许它在商店上可用。

关于开发者在 iTunes Connect 中犯的错误或不正确标记的一些规则如下:

  • 提到其他移动平台,如 Android 的应用程序或元数据将被拒绝

  • 标签不正确或不适当的应用程序类别/类型、屏幕截图或图标将被拒绝

  • 开发者必须为应用程序提供适当的年龄评级和关键词

  • 在应用程序审核时,支持、隐私政策和市场营销 URL 必须能正常使用

  • 开发者不应声明未使用的 iOS 功能;例如,如果您的应用程序实际上未使用 Game Center 或 iCloud,请不要声明这些功能的使用

  • 在未经用户同意的情况下使用位置或推送通知等功能的应用程序将被拒绝

这些有时可能仅仅是开发者的一部分失误。只需确保在最终提交到 iOS App Store 之前,再次检查您的应用程序的所有信息。

此外,苹果还有关于应用程序内可以包含内容的以下规则:

  • 包含令人反感内容或可能被认为粗鲁的应用程序将被拒绝

  • 设计用来打扰或让用户感到厌恶的应用程序将被拒绝

  • 包含过多暴力图像的应用程序将被拒绝

  • 针对特定政府、种族、文化或公司作为敌人的应用程序将被拒绝

  • 图标或屏幕截图不符合 4+年龄评级的应用程序可能会被拒绝

App Store 为儿童和成人提供应用程序。苹果还支持17 岁以上的应用程序年龄限制;然而,这将严重限制潜在用户数量。最好让应用程序保持清洁,适合尽可能多的年龄段。

下一个规则类别,如下所示,与 App Store 中苹果 70/30 的收入分成有关:

  • 链接到网站上销售的产品或软件的应用程序可能会被拒绝。

  • 使用 iOS 应用内购买IAPs)以外的支付机制的应用程序将被拒绝。

  • 使用 IAP 购买实物商品的应用程序将被拒绝。

  • 应用可以展示在应用外部购买的数字内容,只要你不从应用内链接或购买。应用内购买的数字内容必须使用 IAPs(应用内购买)。

只要你不是试图规避苹果应用商店的收入分成,这些规则很容易遵守。在你的应用内解锁数字内容时,请始终使用 IAPs。

最后但同样重要的是,以下是一些与 App Store 拒绝相关的一般性建议:

  • 如果你的应用需要用户名和密码,请确保在演示账户信息部分提供凭证,以便应用审查团队使用。

  • 如果你的应用包含 IAPs 或其他需要应用审查团队明确测试的功能,请确保在审查备注中提供操作说明,以便审查团队能够到达应用中的适当屏幕。

  • 提前规划!不要让你的产品应用被拒绝而影响截止日期;至少在你的计划中预留几周时间用于应用商店的审批。另一个选择是在正式发布前提前提交一个测试版本以待审批,并将发布日期设定在未来。你可以在接近发布日期时上传最终版本。

  • 如有疑问,请在 iTunes Connect 的审查备注部分尽可能详细地描述。

如果你的应用被拒绝,大多数情况下解决方法都很简单。苹果的审查团队会明确引用指南,如果违反了规则,还会附上相关的崩溃日志和屏幕截图。如果可以在不提交新版本的情况下解决问题,你可以在 iTunes Connect 网站的解决方案中心回应应用审查团队。如果你上传了一个新版本,这将会让你的应用排在审查队列的最后。

对于 iOS 的功能,肯定有更深入和具体的规则,所以如果你打算对某个 iOS 功能进行创新或非同寻常的尝试,请确保查看完整的指导方针。如果你对某个具体指导方针不确定,最好寻求专业法律意见。拨打苹果支持电话不会有任何帮助,因为其支持人员不被允许提供与 App Store 审查指南相关的建议。

向 iOS App Store 提交应用

在将应用提交到商店之前,我们需要检查一个简短的清单,以确保你已经准备好这样做。在流程的某个阶段发现问题遗漏或未正确处理是一件痛苦的事。此外,有一些要求需要由设计师或营销团队来完成,这些工作不一定应该由开发者承担。

在开始提交之前,请确保你已经完成了以下工作:

  • 你的应用程序的Info.plist文件应完全填写好。这包括启动画面图像、应用图标、应用名称以及其他需要为高级功能填写的设置。请注意,这里的 app 名称是在应用图标下显示的内容。它可以与 App Store 名称不同,而且与 App Store 名称不同,它不需要在商店中与其他所有应用保持唯一性。

  • 在 App Store 上至少为你的应用选择三个名称。即使某个名称在 App Store 当前未被占用,也可能不可用,因为它可能已经被其他开发者用于因某些原因而被移除的应用。如果你愿意,也可以提前预留一个名称。

  • 你有一张大的 1024x1024 应用图标图像。除非你通过 iTunes(桌面应用程序)分发企业或 Ad Hoc 版本,否则无需将此文件包含在应用程序中。

  • 针对应用所针对的每个设备,你至少有一张截图。这包括 iPhone 6 Plus、iPhone 6、iPhone 5、iPhone 4、iPad mini、iPad 视网膜和 iPad Pro 尺寸的通用 iOS 应用程序截图。我强烈建议填写所有可能的截图槽位。

  • 你为 App Store 准备了一份精心编写和编辑的描述。

  • 你选择了一系列关键词以提高应用程序的搜索效果。

创建一个分发配置文件

当你再次检查了上述清单后,我们可以开始提交的过程。我们的第一步将是为 App Store 分发创建一个配置文件。

让我们通过执行以下步骤开始创建一个新的配置文件:

  1. 导航至developer.apple.com/account/

  2. 在右侧导航栏中点击证书、ID 与配置文件

  3. 点击配置文件 | 全部

  4. 点击窗口右上角的加号按钮。

  5. 分发下选择App Store并点击继续

  6. 选择你的应用 ID。你应该在第七章,在设备上部署和测试中已经创建了一个;点击继续

  7. 选择为提供配置文件而使用的证书。通常这里只有一个选项。点击继续

  8. 为配置文件选择一个合适的名称,如MyAppAppStore。点击生成

  9. 完成后,你可以手动下载并安装配置文件,或者在 Xcode 的偏好设置 | 账户中同步你的配置文件,正如我们在本书前面所做的那样。

成功后,你将看到以下屏幕:

创建分发配置文件

将你的应用添加到 iTunes Connect

接下来的一系列步骤,我们将开始填写你的应用程序在 Apple App Store 上显示的详细信息。

我们可以通过执行以下一系列步骤开始在 iTunes Connect 中设置你的应用:

  1. 导航到itunesconnect.apple.com并登录。

  2. 点击我的应用

  3. 点击窗口左上角的加号按钮,然后选择新建应用

  4. 选择平台为iOS

  5. 输入在 App Store 上显示的应用名称

  6. 为你的应用选择一个主要语言

  7. 选择你的Bundle ID。你应该在第七章,在设备上部署和测试中已经创建了一个。

  8. SKU字段中输入一个值。这用于在报告中识别你的应用。

  9. 点击继续

  10. 从这里开始,需要填写很多必填信息。如果你遗漏了任何信息,iTunes Connect 在显示警告方面会非常有帮助。该网站对市场营销专业人士以及开发者来说应该是相当用户友好的。

  11. 在进行更改后点击保存

还有许多可选字段。如果你有需要应用审核团队审核你的应用程序的任何额外信息,请确保填写审核备注演示账户信息。完成后,你将看到你的应用程序状态为准备提交,如下截图所示:

将应用添加到 iTunes Connect

现在,我们需要将我们的应用实际上传到 iTunes Connect。你必须从 Xcode 或 Application Loader 上传一个构建版本。这两种方法会产生相同的结果,但如果非开发者提交应用,有些人更喜欢使用 Application Loader。

为 App Store 制作 iOS 二进制文件

我们提交到 App Store 的最后一步是向商店提供包含我们应用程序的二进制文件。我们需要创建应用程序的发布版本,使用本章早些时候创建的分发配置文件进行签名。

Xamarin Studio 使这个过程变得非常简单。我们可以按以下方式配置构建:

  1. 点击 Xamarin Studio 左上角的项目配置下拉菜单,并选择AppStore

  2. 默认情况下,Xamarin Studio 将设置你需要提交此构建配置的所有配置选项。

  3. 接下来,选择你的 iOS 应用程序项目,然后点击Build | Archive for Publishing

几分钟后,Xamarin Studio 将打开已归档构建的菜单,如下所示:

为 App Store 制作 iOS 二进制文件

该过程将在~/Library/Developer/Xcode/Archives中创建一个xarchive文件。验证...按钮将检查你的归档在上传过程中可能出现的任何潜在错误,而签名并分发...将实际提交应用程序到商店。

若要提交你的应用程序到商店,请执行以下步骤:

  1. 点击签名并分发...。别担心,它在上传之前会验证归档。

  2. 选择App Store,然后点击Next

  3. 确保列出的配置文件是为 App Store 准备的,然后点击下一步

  4. 审核你的更改并点击发布

  5. 选择一个位置来保存*.ipa文件,然后点击保存

  6. 点击打开 Application Loader以开始上传过程。

你应该会在 Xamarin Studio 中看到一个与以下截图类似的屏幕:

为 App Store 制作 iOS 二进制文件

从这里开始,使用Application Loader相当直接:

  1. 使用你的 iTunes Connect 凭据登录。

  2. 选择交付你的应用并点击选择

  3. 选择你在 Xamarin Studio 中创建的*.ipa文件,然后点击打开

  4. 审核为构建选择的应用并点击下一步

  5. 如果一切操作都正确,你应该会看到一个上传进度条,然后是一个成功对话框。

如果你回到 iTunes Connect,并导航到TestFlight | Test Flight Builds标签,你会看到你刚才上传的构建状态为处理中

为 App Store 制作 iOS 二进制文件

几分钟后,构建将被处理,并可以添加到 App Store 发布中。下一步是在App Store | iOS App标签下的构建部分选择构建。

点击保存后,你应该可以点击提交审核,而不会有任何剩余警告。接下来,回答关于出口法、广告标识符等三个问题,并点击提交作为最后一步来提交你的应用。

在这一点上,当你的应用在等待苹果员工审核时,你无法控制其状态。这可能需要一周或更长时间,具体取决于待审核应用的工作量和一年中的时间。更新也将通过这个过程,但等待时间通常比新应用提交要短一些。

幸运的是,有些情况下你可以快速推进这个过程。如果你访问developer.apple.com/contact/app-store/?topic=expedite,你可以请求加速应用审核。你的问题必须是关键的错误修复或与你的应用程序相关的时间敏感事件。苹果并不保证接受加速请求,但在需要的时候它可能是一个救星。

此外,如果你提交的构建出现了问题,你可以通过进入应用详情页顶部并选择从审核中移除这个版本来取消提交。在提交后发现错误的情况下,这允许你上传一个新的构建来替代。

签名你的 Android 应用

所有 Android 软件包(apk文件)都由证书或密钥库文件签名,以便在设备上安装。当您调试/开发应用程序时,您的软件包会自动由 Android SDK 生成的开发密钥签名。使用此调试密钥进行开发甚至测试版测试是可以的;然而,它不能用于在 Google Play 分发的应用程序。

完成以下设置以创建签名的 APK:

  1. 在 Xamarin Studio 左上角点击解决方案配置下拉菜单,并选择发布

  2. 接下来,选择您的 Android 应用程序项目,并点击构建 | 归档以发布

  3. 接下来,选择创建的 Android 归档并点击签名与分发

  4. 选择Ad Hoc并点击下一步。Google Play 也是您稍后可能考虑的选项,但它需要更多时间来设置(它也不能上传应用程序的第一个 APK)。

  5. 选择创建新密钥

  6. 填写 Android 密钥库文件所需的信息并点击确定

  7. 选择您创建的密钥库文件并点击下一步

  8. 点击发布并选择保存 APK 的位置。

您的密钥库文件设置应类似于以下截图:

为您的 Android 应用程序签名

完成后,您应该将密钥库文件和密码保存在一个非常安全的地方。默认情况下,Xamarin Studio 将您的密钥放在~/Library/Developer/Xamarin/KeyStore。一旦您使用此keystore文件签名应用程序并将其提交到 Google Play,如果没有相同的密钥签名,您将无法提交应用程序的更新。没有机制可以找回丢失的密钥库文件。如果您不幸丢失了它,您的唯一选择是从商店中删除现有应用并提交包含您更新更改的新应用。这可能会导致您失去大量用户。

将应用提交至 Google Play

一旦您有了签名的 Android 软件包,与 iOS 相比,提交应用程序到 Google Play 相对简单。所有操作都可以通过浏览器中的开发者控制台标签完成,无需使用 OS X 应用程序上传软件包。

在开始提交之前,请确保您已经完成了以下清单上的任务:

  • 您声明了一个AndroidManifest.xml文件,其中包含您的应用程序名称、包名称和图标。

  • 您有一个使用生产密钥签名的apk文件。

  • 您为 Google Play 选择了一个应用程序名称。这个名称在商店中不是唯一的。

  • 您有一个 512x512 高分辨率的 Google Play 图标图像。

  • 您为商店编写并编辑了一个详尽的描述。

  • 您至少需要两张截图。不过,我建议使用所有可用的位置,包括 7 英寸和 10 英寸平板电脑的尺寸。

完成清单检查后,您应完全准备好将应用程序提交至 Google Play。添加新应用的标签如下所示:

提交应用至 Google Play

首先,访问play.google.com/apps/publish并登录您的账户,然后执行以下步骤:

  1. 选择所有应用程序标签,然后点击添加新应用程序

  2. 输入在 Google Play 上显示的应用名称,然后点击上传 APK

  3. 点击将您的第一个 APK 上传至生产环境BetaAlpha渠道。

  4. 浏览到您的签名.apk文件,然后点击确定。您会看到APK标签的勾号变绿。

  5. 选择商店列表标签。

  6. 填写所有必填字段,包括描述高分辨率图标分类隐私政策(或选择表示您没有提交政策的复选框),并提供至少两张截图。

  7. 点击保存。您会看到商店列表标签上的勾号变绿。

  8. 选择内容分级标签,并填写选择应用程序年龄分级的问卷调查。

  9. 选择定价与分销标签。

  10. 选择一个价格和您希望分销的国家。

  11. 接受内容指南美国出口法律的协议。

  12. 点击保存。您会看到定价与分销标签上的勾号变绿。

  13. 如下图所示,在右上角选择准备发布下拉菜单,并选择发布此应用提交应用至 Google Play

几个小时后,您的应用程序将在 Google Play 上提供。无需审批流程,且应用程序的更新同样轻松。

Google Play 开发者计划政策

为了提供一个安全的商店环境,Google 会追溯删除违反其政策的应用程序,并通常会禁止整个开发者账户,而不仅仅是应用程序。Google 的政策旨在提高 Google Play 上可用应用程序的质量,并不像 iOS 的规则集那样冗长。话虽如此,以下是 Google 政策的基本摘要:

  • 应用程序不能包含色情材料、无端的暴力或仇恨言论。

  • 应用程序不能侵犯版权材料。

  • 应用程序不能具有恶意性质,或在用户不知情的情况下捕获用户的私人信息。

  • 应用程序在未经用户同意的情况下,不能修改用户设备的基本功能(如修改主屏幕)。如果应用程序包含此类功能,用户必须能够轻松关闭。

  • 应用程序内的所有数字内容必须使用 Google Play 的应用内购买(或 IAP)。物理商品不能使用 IAP 购买。

  • 应用程序不得滥用可能导致用户产生高额账单的蜂窝网络使用:

就像 iOS 一样,如果你对某项政策有疑虑,最好寻求专业法律意见。要查看政策的完整列表,请访问play.google.com/about/developer-content-policy/

Google Play 开发者计划政策

构建成功移动应用的技巧。

根据我的个人经验,我使用 Xamarin 构建的应用已经提交到 iOS App Store 和 Google Play 有一段时间了。在交付了近 50 款总下载量达数百万的移动应用后,我学到了很多关于什么因素能使移动应用成功或失败的经验。对于最终用户来说,Xamarin 应用与 Java 或 Objective-C 应用是无法区分的,所以你可以通过遵循标准的 iOS 或 Android 应用的相同模式来使你的应用成功。

你可以采取很多措施让你的应用更成功。以下是一些可以遵循的建议:

  • 定价要合适:如果你的应用面向所有人,无处不在,可以考虑采用广告植入或 IAPs 的fremium模式来获得收入。然而,如果你的应用相对小众,将应用定价在 4.99 美元或更高会更有利。高级应用必须保持更高的质量标准,但可以在较少的用户中获得不错的收入。

  • 了解你的竞争对手:如果你的应用领域内还有其他应用,请确保你的应用要么更好,要么提供比竞争对手更广泛的功能集。如果已经有几个应用在与你的应用竞争,那么完全避开这个领域也许也是个好主意。

  • 提示忠实用户进行评价:在用户多次打开你的应用后,提示他们进行评价是一个好主意。这给了那些积极使用你应用的用户一个机会,去撰写好评。

  • 支持你的用户:提供一个有效的支持电子邮件地址或 Facebook 页面,以便你能够轻松地与用户互动。回复 bug 报告和负面评价。Google Play 甚至有向撰写应用评价的用户发送电子邮件的选项。

  • 保持应用体积小:在 iOS 上保持在 100MB 以下,或在 Google Play 上保持在 50MB 以下,这将允许用户使用他们的手机数据计划下载你的应用。这样做可以减少安装应用的摩擦,因为用户会将漫长的下载时间与运行缓慢的应用联系起来。

  • 将你的应用提交到评测网站:尽量在网上获得尽可能多的评测。苹果提供了发送优惠券代码的功能,但对于你的安卓版本应用,你可以发送实际的安卓安装包。将你的应用提交到评测网站或热门 YouTube 频道,可以成为获得免费广告的好方法。

  • 使用应用程序分析或跟踪服务:报告你的应用程序的使用情况和崩溃报告对于理解用户非常有帮助。在野外修复崩溃并修改用户界面以改善消费行为非常重要。

没有银弹可以保证移动应用程序的成功。如果你的应用程序引人注目,满足需求,并且运行快速、正确,你手中可能就有一个下一个热门应用。使用 Xamarin 提供一致的跨平台体验,也将使你在竞争中处于领先地位。

概述

在本章中,我们涵盖了将你的应用程序提交到 iOS App Store 和 Google Play 所需知道的一切。我们介绍了 App Store 审核指南,并为你在审核过程中可能遇到的最常见情况进行了简化。我们详述了为你的应用程序配置元数据并在 iTunes Connect 上传二进制文件的过程。对于 Android,我们讲解了如何创建生产签名密钥并签署你的 Android 包(APK)文件。我们介绍了向 Google Play 提交应用程序的过程,并以关于如何向应用商店成功交付一个有望盈利的应用程序的技巧结束了这一章。

我希望这本书能让你体验到一个端到端的、实用的演练,以使用 Xamarin 开发真实世界的跨平台应用程序。相比其他移动开发选项,C# 应该能让你更加高效。此外,通过共享代码,你将节省时间,同时不会以任何方式限制用户的原生体验。

posted @ 2024-05-23 11:06  绝不原创的飞龙  阅读(8)  评论(0编辑  收藏  举报