谷歌-Flutter-移动开发快速启动指南-全-
谷歌 Flutter 移动开发快速启动指南(全)
原文:
zh.annas-archive.org/md5/2ff865f719115aa08fbcf9d0c2b760d8译者:飞龙
前言
Flutter 是由 Google 开发的跨平台应用开发框架。它使用 Dart 编程语言来满足其开发需求。本书将指导您开始跨平台应用开发的旅程,通过帮助您理解 Flutter 的基本概念。
本书面向对象
本书旨在为对学习 Flutter 的基本概念以及学习如何构建跨平台应用感兴趣的读者提供帮助。
本书涵盖内容
第一章,介绍 Flutter,简要介绍了 Flutter 以及本书如何作为指南帮助您学习使用 Flutter 进行跨平台应用开发。然后,我们将探讨 Flutter 的起源及其如何与现有的移动应用开发世界相结合。
第二章,开始使用 Flutter,涵盖了使用 Flutter 所需工具的安装,使读者熟悉 IDE,并查看 Flutter 中最好的功能之一——热重载。然后,我们将了解每个应用开发工作流程中必需的两个主要概念——调试和测试。
第三章,Widgets 无处不在,遍历了 widget 目录,并解释了如何创建自定义 Widgets。然后,我们将学习如何在这些 Widgets 之间进行路由和导航。
第四章,探索 Widgets 的多样性,探讨了 Flutter 中的限制,并介绍了 Flutter 中的动画。然后,我们将学习如何使用 Listview 和滚动 Widgets,并在本章末尾介绍 silver。
第五章,拓宽我们的 Flutter 视野,解释了网络在应用中扮演的重要角色,并提供了设置和运行服务器以获取 JSON 代码的示例代码。本节之后,我们将了解为什么可访问性很重要,以及开发者可以带来哪些改进以支持应用的可访问性。下一节是关于应用支持国际化。
第六章,使用平台为 Flutter 应用提供动力,解释了如何在 Flutter 代码中包含包,然后介绍如何包含特定平台的通道以支持 Flutter 代码。然后,我们将使用 BatteryManager API 来了解 Android 手机的电池状态。我们将介绍在构建自己的插件之前需要考虑的一些最佳实践,然后介绍如何在 Flutter Pub 网站上发布自己的插件。
第七章,Firebase - Flutter 的最佳朋友,探讨了如何使用 Firestore 云数据库帮助我们更快地构建应用。我们还将查看一个使用 Firestore 云数据库捕获 ListView 的示例。最后,我们将讨论一些关于使用远程配置的应用用例。
第八章,部署 Flutter 应用,介绍了如何在相应的商店上部署和发布 Android 和 iOS 应用。
为了充分利用这本书
在开始阅读之前,一些 Dart 语言的经验将很有帮助,同时还需要在 Android、iOS 或任何移动开发框架上有经验。最后,熟悉面向对象的语言,如 Java 和 C++,以及一些 OOPS 知识将非常有用。
下载示例代码文件
您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择 SUPPORT 选项卡。
-
点击代码下载与勘误。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
一旦文件下载完成,请确保使用最新版本的以下软件解压或提取文件夹:
-
Windows 上的 WinRAR/7-Zip
-
Mac 上的 Zipeg/iZip/UnRarX
-
Linux 上的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上,链接为github.com/PacktPublishing/Google-Flutter-Mobile-Development-Quick-Start-Guide。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在以下链接找到:github.com/PacktPublishing/。查看它们吧!
使用的约定
在本书中使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”
代码块设置如下:
void main() {
debugPaintSizeEnabled=true;
runApp(MyApp());
}
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
Center(
child: Container(
decoration: BoxDecoration(border: Border.all()),
height: 200.0,
width: 200.0,
),
),
任何命令行输入或输出都按以下方式编写:
$ flutter packages get
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”
警告或重要提示看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们始终欢迎读者的反馈。
总体反馈:如果您对本书的任何方面有任何疑问,请在您的邮件主题中提及书名,并将邮件发送至 customercare@packtpub.com。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了本书,为何不在您购买书籍的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需更多关于 Packt 的信息,请访问 packt.com。
第一章:介绍 Flutter
Flutter是谷歌为创建跨平台移动应用程序(iOS 和 Android)而开发的应用程序开发框架。如官方网站flutter.io/所述,它旨在使开发尽可能简单、快速和高效。诸如热重载、庞大的小部件目录、非常好的性能和坚实的社区等因素有助于实现这一目标,使 Flutter 成为一个相当不错的框架。
本书将指导你从开始使用 Flutter 到最终在它上面部署应用程序的旅程。但在那之前,让我们快速介绍一下 Flutter。
在本章中,我们将涵盖以下内容:
-
Flutter 的起源
-
什么是小部件?
-
将 Flutter 与现有框架进行比较
Flutter 的起源
Flutter 的起源与许多著名软件的起源相似。它是在谷歌开发的。最初,Flutter 是一个实验,因为谷歌的开发人员试图从 Chrome 中移除一些兼容性支持,以尝试使其运行得更顺畅。几周后,在移除了许多兼容性支持之后,开发人员发现他们拥有一个比 Chrome 快 20 倍的渲染器,并看到它有成为伟大事物的潜力。
谷歌创建了一个分层框架,它直接与 CPU 和 GPU 通信,以便让开发者尽可能多地自定义应用程序。
什么是小部件?
Flutter 中的所有内容都可以使用小部件创建。方向、布局、不透明度、动画……一切都是小部件。这是 Flutter 的主要特点,从简单的按钮到动画或手势,所有操作都是通过小部件完成的。这很棒,因为它允许用户选择组合而不是继承,使构建应用程序的过程就像搭建乐高塔一样简单。你只需要挑选小部件并将它们组合起来以创建应用程序。
有许多基本的小部件可以帮助你使用 Flutter 构建应用程序。所有这些小部件都编入Flutter 小部件目录。因为 Flutter 中的所有内容都是由小部件组成的,所以你越了解如何使用、创建和组合它们,你使用 Flutter 的能力就越好、越快。在第三章“小部件,无处不在的小部件”中,我们将更详细地介绍小部件和小部件目录。
将 Flutter 与现有框架进行比较
当谈到移动应用开发时,我们可以找到许多不同的方法,但最终,一切归结为原生或跨平台方法。让我们看看不同的方法与 Flutter 相比看起来和运作如何。我们首先将看看原生平台,然后,在查看跨平台方法之前,我们将看看WebView系统,最后我们将看看 Flutter 在这个组合中的位置。
原生平台
原生框架,如 Android 和 iOS SDK,非常稳固。它们是移动应用开发的最佳稳定选择。它们有很多可用的应用,经过深度测试,拥有庞大的社区和公开可用的教程。以下图显示了原生移动应用开发框架的工作原理:

如前图所示,在这个框架中,应用直接与系统对话。这使得原生框架在功能方面成为最强大的选择。然而,它确实有一个缺点:你需要学习两种不同的语言,Android 的 Kotlin 或 Java,iOS 的 Obj-C 或 Swift,以及 SDK。这些语言用于编写具有相同功能的不同应用。每次修改都必须在两个平台上重复,这个过程可能并不那么顺畅。对于小型团队或需要开发速度的人来说,这并不是一个好的选择。
WebView 系统
另一方面,我们有著名的跨平台方法,它以高效著称。在这个方法中,我们可以从单个代码库中获取 Android 和 iOS 的应用,就像在 Flutter 中一样。但每个框架都有一些缺点。
Cordova、Ionic、PhoneGap 和基于 WebView 的框架通常是跨平台框架的好例子,它们对于前端开发者来说尤其是一些好的解决方案。但它们在性能方面有所欠缺,在这些方法中,应用视图由 WebView 渲染的 HTML 组成;这意味着应用基本上是一个网站。
下面的图示展示了基于 WebView 的框架是如何工作的:

该系统使用一个桥接器来实现 JavaScript 与原生系统之间的切换。这个过程可能会非常慢,这取决于你需要的功能,这给这个系统增加了另一个缺点。
其他跨平台方法
让我们以另一个跨平台方法为例,看看它可能存在的不足。Xamarin 是 Windows 平台的跨平台开发解决方案,在我看来,它并不那么方便,尤其是在生产力和编译时间方面。
当查看其他平台时,React Native 可以被认为是跨平台框架中最好的之一,但它严重依赖于 OEM 组件。
让我们来看看 React Native 的工作原理:

React Native 在 WebView 系统中扩展了桥接概念,不仅用于服务,还用于构建小部件。从性能的角度来看,这确实很危险;例如,一个组件在动画过程中可能被构建数百次,但由于桥接概念的扩展,这个组件可能会大幅度减慢速度。这也可能导致其他问题,尤其是在 Android 上,这是最碎片化的操作系统。
Flutter 的方法
在前面的章节中,我们探讨了移动应用开发的多种方法。我们简要地了解了这些方法的工作原理及其缺点。现在让我们来看看 Flutter。
与其他解决方案相比,Flutter 的表现要好得多,因为应用程序是编译AOT(提前编译)而不是像 JavaScript 解决方案那样的JIT(即时编译)。它还消除了桥接的概念,并且不依赖于 OEM 平台。它确实允许自定义组件使用屏幕上的所有像素。这意味着什么?这基本上意味着应用程序在 Android 和 iOS 的每个版本上显示效果相同。
我们确实研究了其他方法的工作原理,所以让我们也来看看 Flutter 的工作原理。您可以在以下图中看到 Flutter 框架的工作方式:

现在,您可以看到其他跨平台方法与 Flutter 之间的区别。如前所述,Flutter 消除了桥接和 OEM 平台,并使用Widgets Rendering来与画布和事件一起工作。它还使用平台通道来使用服务。此外,使用异步消息系统使用平台 API 并不困难,这意味着如果您需要使用特定的 Android 或 iOS 功能,可以轻松实现。
Flutter 还使得使用通道创建插件成为可能,这些插件可以被每个新开发者使用。简单来说:一次编写,到处使用!
为什么使用 Flutter?
Flutter 由于其众多特性和与其他方法不同的几个方面,是跨平台开发的一个好选择,正如我们所看到的。它不仅对开发者来说是一个好选择,对用户和设计师来说也是如此;让我们来看看为什么是这样的:
-
对于用户来说,Flutter 为应用程序提供了吸引人的用户界面,这增强了用户对这些应用程序的使用。
-
对于开发者来说,Flutter 使得新开发者进入构建移动应用的世界变得容易,因为使用 Flutter 构建应用程序非常简单。Flutter 不仅减少了应用程序的开发时间,还减少了创建应用程序的成本和复杂性。
-
对于使用 Flutter 的设计师来说,可以使用为应用程序构思的原设计来创建应用程序,而不会对其任何方面造成妥协。因此,设计师的原有愿景在开发过程中不会改变。
最重要的是,Flutter 是一个非常实用的工具,可以创建原型和模型,这是一个优点,因为它为设计师和开发者提供了一个良好的接触点,这两个角色通常非常疏远。
摘要
在本章中,我们首先对 Flutter 进行了简要介绍,以及本书将如何作为学习使用 Flutter 进行跨平台应用开发的指南。然后,我们讨论了 Flutter 的起源。在探讨为什么 Flutter 是一个好选择之前,我们考察了 Flutter 在现有移动应用开发世界中的位置。
现在,移动开发并不是一个全新的领域,但 Flutter 使得它变得更加有趣且快速。此外,通过改进开发者工作流程,它使移动应用开发更接近游戏体验。
在下一章中,我们将安装 Flutter 框架,并尽可能从示例应用中学习。
第二章:Flutter 入门
在开发任何应用程序之前,了解该系统的安装过程是理想的。在本章中,我们将首先查看如何在您的系统上安装 Flutter 并选择合适的 IDE。然后,我们将继续探索一个在屏幕上显示基本 Hello World 的示例应用程序。在我们查看如何调试和测试我们的应用程序之前,我们将快速了解一下什么是 热重载。
要开发 iOS 应用程序,我建议使用 Mac。我们始终可以在 Android 上使用和测试应用程序,并在部署这些应用程序时使用 macOS。然而,问题总是接踵而至,因此在构建过程中在相应平台上测试应用程序将非常推荐。
在本章中,我们将涵盖以下主题:
-
安装 Flutter
-
选择合适的 IDE
-
探索示例应用程序
-
热重载
-
了解 Flutter 工具及其使用方法
-
在 Flutter 中编写和执行测试
安装 Flutter
让我们从我们的主要应用开始,并在您的系统上安装 Flutter。根据您使用的操作系统,您可以按照以下步骤在您的系统上安装 Flutter。我们将查看如何在 Windows、Mac 和 Linux 上安装 Flutter。
在 Windows 上安装 Flutter
要在 Windows 上安装 Flutter,请按照以下步骤操作:
-
从
storage.googleapis.com/flutter_infra/releases/stable/windows/flutter_windows_v1.2.1-stable.zip下载 Flutter。 -
解压下载的文件,并将其放置在您系统上的目标文件夹中。
-
定位并运行
flutter_console.bat以开始安装。 -
我们接下来需要下载和设置 Node.js;您可以从
nodejs.org/en/download/下载它。 -
最后,我们需要下载和安装 Git For Windows 2.x:
gitforwindows.org/
在 Mac 上安装 Flutter
要在 Mac 上安装 Flutter,请按照以下步骤操作:
-
从
storage.googleapis.com/flutter_infra/releases/stable/macos/flutter_macos_v1.2.1-stable.zip下载 Flutter for Mac。 -
解压下载的文件,并使用 ```$ export PATH=
pwd/flutter/bin:$PATH`` 命令将其放置在您系统上的目标文件夹中。 -
运行
$ flutter doctor以验证一切是否已正确设置。 -
我们接下来需要下载和设置 Node.js;您可以从
www.npmjs.com/get-npm下载它。
我们将使用以下命令:bash、curl、git 2.x、mkdir、rm、unzip 和 which。
- 最后,我们需要下载和安装 Git:
git-scm.com/download/mac
在 Linux 上安装 Flutter
要在 Linux 上安装 Flutter,请按照以下步骤操作:
-
从
storage.googleapis.com/flutter_infra/releases/stable/linux/flutter_linux_v1.2.1-stable.tar.xz下载 Flutter。 -
解压缩下载的文件,并将其放置在您系统上选择的文件夹中,使用
$ tar xf ~/Downloads/flutter_linux_v1.2.1-stable.tar.xz。 -
然后,使用以下命令将 Flutter 添加到您的路径:
$ export PATH="$PATH:pwd/flutter/bin". -
接下来,我们需要下载并设置 Node.js;您可以从
git-scm.com/download/linux下载。
这里是我们将要使用的命令:bash、curl、git 2.x、mkdir、rm、unzip、which 和 xz-utils。
熟悉 IDE
对于 Flutter,最好使用 Android Studio/IntelliJ 或 Visual Studio(VS)代码,操作系统为 Mac/Windows。这些 IDE 是您能找到的用于开发移动应用的最佳选择。但是,为了使用这些 IDE 与 Flutter 一起,我们需要使用一些插件。
我们需要一个 Dart 编译器的插件,另一个用于代码分析,还有一个用于 Flutter 开发者工作流程(构建、运行和调试)的插件。
这些插件可以在 Android Studio 和 VS code 上安装。您只需在相应的插件部分中搜索它们即可。这些 IDE 不仅提供了这些出色的插件选项来支持您的开发。让我们看看在开发应用程序时您可以使用的快速提示。
使用您想要的 IDE 的快速提示
当使用 Flutter 插件时,有一个非常好的选项可以在开发应用程序时使用;它被称为 快速修复 选项。要使用它,请按 Alt + Enter(在 VS code 上为 Ctrl + .)并会显示一个包含一些快速修复的弹出窗口。让我们看看它在屏幕上的样子。以下截图显示了它的外观:

在前面的截图中,快速修复提供了一个添加填充的选项:一个居中的 widget,使用 column 或 row 包裹它,并用新的 widget 包裹它。
这是一个非常有用的选项,因为它将在您嵌套多个 widget 的开发过程中帮助您节省大量时间。同时,保持代码整洁并不是一件容易的事情。
使用快速修复选项还可以做的一件很棒的事情是,您可以按列顺序排列子项以交换与父项的 widget 或完全快速地删除 widget。以下截图显示了这些选项:

谈到嵌套,插件中的一个非常有用的选项是在每个 widget 的末尾存在一些假注释。这有助于您一眼就能理解您所组成的 widget 树。以下截图显示了这些假注释的外观:

这些小贴士一开始可能看起来并不十分有用,但一旦你开始使用 Flutter 开发应用程序,它们将变得至关重要,并帮助你更快地工作。
探索示例应用程序
让我们看看 Flutter 中的代码是如何看起来,并探索其元素。首先,让我们创建一个新的项目;这样,Flutter-cli 将为我们创建一个示例应用程序以供探索。在我们开始查看代码之前,这是本书的 GitHub 仓库:github.com/PacktPublishing/Flutter-Quick-Start-Guide/tree/master/sample_app。
以下截图显示了 Flutter 代码的外观;让我们探索其元素:

正如你所见,应用程序的入口点是 main 函数,其中你可以看到对 runApp 的调用。这是第一条被执行的命令;其任务是设置 Flutter 框架并运行所选应用程序。当我们设置应用程序时,最初,它是一个普通的无状态小部件。
接下来,我们来到 Build 方法。它在之前的截图中被显示为 Widget build(BuildContext context)。Build 方法是返回 MaterialAPP、设置标题和设置通用主题的方法。除此之外,Build 方法还设置了应用程序的路由和主页。
接下来,让我们看看以下截图:

我们正在开发的示例应用程序由一个带有计数器的 Scaffold 组成,该计数器通过一个 浮动操作按钮(FAB)的压力来增加。正如我们在前面的截图中所见,这里没有 setText。计数器仅由一个变量描述,该变量通过 FAB 的 onPressed 动作的处理器进行更新。
让我们继续前进,看看代码中最重要的一行:前一个截图中的第 49 行。在 Flutter 中,你使用 setState() 方法来更新 UI 并与底层变量同步。在这种情况下,我们正在增加 _counter 变量,同时,我们还想让应用程序渲染显示数字的文本。这些是在你创建 Flutter 示例项目时能够使用的一些元素。我们将在下一节中查看 Flutter 的最佳特性之一:热重载。
热重载
在你开始实际在 Flutter 中开发应用程序之前,了解你可以使用哪些 Flutter 特性来使生活更轻松是很好的。热重载就是这样一种特性;它将使开发变得更加容易。它是如何做到这一点的呢?让我们看看。
为了理解热重载是如何成为祝福的,让我们考虑一个正常的开发流程,其中您正在构建一个用于设置应用程序页面的标签。当您导航到您的标签时,您发现某些文本太小。通常,您必须返回并更改代码中的字体大小,然后返回到该点并检查大小是否现在正确。如果不正确,您会再次这样做:返回到代码中更改字体大小,回到该点,并检查它现在是否正确。您将不得不反复这样做,直到您找到正确的字体大小。
这是非常耗时且令人沮丧的,对吧?但在 Flutter 中,我们不必这样做,因为我们有热重载功能。在这种情况下,您只需编辑字体大小并按 ⌘+S。一旦这样做,您的应用将显示代码的更新版本!我们不必重新编译或反复导航到那个特定的屏幕。
这怎么可能呢?热重载使用 Dart 的 JIT 编译功能。编辑的代码将在毫秒内注入到以调试模式运行的应用中,保持其状态在内存中。
这对开发者来说是一个惊人的特性,因为它改变了开发工作流程的方式。您在这里有机会以不同的方式编写代码,这有助于您在不害怕严格工作的情况下对 UI 代码进行更多修改。
调试应用程序
在学习开发任何应用程序时,调试是了解的最重要的事情之一。调试将帮助您识别并处理代码中的错误。错误总是潜伏在角落,了解如何处理它们是至关重要的。为了理解 Flutter 中的调试,我们必须了解以下三个概念:
-
Dart 分析器
-
Dart 观测站
-
视觉调试
在接下来的章节中,我们将详细说明它们是什么以及它们如何帮助进行调试。
Dart 分析器
Dart 分析器检查 Dart 代码中的错误。它本质上是一个 Dart 的 linter,是 dartanalyzer 工具的简单包装。Dart 分析器也包含在 Flutter 插件中,适用于 Android Studio 和 VS code,因此您不必担心在您的 IDE 中单独包含它。
我们还可以创建一个名为 analysis_options.yaml 的文件,并指定一些额外的选项,这将引发错误/警告,并帮助您编写更好的 Flutter 代码。
Dart 观测站
Dart 观测站是一个专门用于调试和分析 Flutter 应用的工具。要设置断点并逐步运行应用,您可以使用 IDE 的帮助。另一种选择是使用 debugger() 语句。这一行将在您放置它的位置中断执行。您还可以指定一个条件,并且只有当条件为真时,应用才会停止:
void function(int aNumber) {
debugger(when: aNumber < 10);
// ...
}
当你运行 Flutter 应用程序时,你将在控制台看到一个指定观察器 URL 的行。该行看起来如下:
Observatory listening on http://127.0.0.1:8100/
你可以通过导航到这个 URL 执行许多操作。你可以打开观察器,使用它来分析应用程序,检查堆栈,分配内存等等。这是一个非常强大的工具;你可以在dart-lang.github.io/observatory/上找到更多关于它的信息。
视觉调试
将会有一些情况,我们需要调试我们应用程序的布局。我们可能需要以某种特定方式对一些小部件进行对齐,或者有时我们可能不知道小部件之间的空间是边距还是填充。在这种情况下,我们需要对应用程序进行视觉调试。在这种情况下进行调试时,启用debugPaintSize选项。
要这样做,将debugPaintSizeEnabled变量设置为true,如下所示:
void main() {
debugPaintSizeEnabled=true;
runApp(MyApp());
}
将会显示以下输出:

如前一张截图所示,每个小部件都被着色,现在可以很容易地区分了。
这是一个非常强大的功能,可以帮助你进行视觉调试,尤其是如果你不是一个那么“像素完美”的前端开发者。
材料网格变量
让我们看看另一个视觉调试变量:它被称为材料网格。在这里,你将通过将debugShowMaterialGrid设置为true来声明你的MaterialApp!你的应用程序将被材料像素网格覆盖——这对于研究应用程序布局非常完美。以下是你应用程序的外观:
showPerformanceOverlay 变量
下一个有用的选项是showPerformanceOverlay。通过将其设置为true,你将在图表的上半部分看到应用程序的性能以图表的形式显示。你的屏幕上将会显示两个图表,如下面的截图所示:

上图显示了 GPU 线程花费的时间,下图显示了 CPU 线程花费的时间。它们还会显示应用程序是否以低于 60Hz 的速度运行;在这种情况下,你可能有一些性能问题。这个功能将帮助你了解应用程序的性能,并验证它是否按预期运行。
请务必仅在发布模式下使用此功能。在调试模式下,性能有意降低以提供热重载并引发更多警告。
Flutter 小部件检查器
如果你是一个网页开发者,你可能会在许多浏览器中轻易错过检查选项。Flutter 以 Flutter 小部件检查器的形式将其带回给你。这是另一个帮助你进行视觉调试应用程序的功能。让我们看看一个显示它的截图:

这是我们在 Android Studio 中可以找到的 Flutter 小部件检查器。这个功能为我们提供了许多选项;其中一些是视觉调试部分中提到的功能的快捷键。要触发检查器,请执行以下步骤:
-
点击 S
elect widget
选项。 -
然后,点击你的设备上的一个小部件。你点击的小部件将在小部件树中被选中并突出显示,如下所示:

一旦触发,你可以看到小部件树,你可以查看小部件组合并了解布局中是否有任何问题。
我们已经探讨了调试以及应用的可视化调试。调试是找出你的应用中是否存在错误的好方法。另一种找出应用工作过程中任何异常或问题的好方法是测试你的应用。在下一节中,我们将探讨如何测试你的 Flutter 应用。
测试 Flutter 应用
随着你的应用越来越大,一套好的测试可以帮助你节省时间,因为测试可以发现正常修改中可能出现的新错误。甚至执行测试驱动开发(TDD)也是一个好主意,因为它可以帮助你定义项目结构并编写更少但更高效的代码。
在 Flutter 中,主要有三种自动化测试:
-
单元测试
-
小部件测试
-
集成测试
让我们详细看看它们。
单元测试
正如其名所示,单元测试是一种用于测试单个代码单元的测试类型。这个小的单元可能是一个函数、一个方法或一个类。通常,在单元测试中,我们不需要写入磁盘、渲染到屏幕或接收外部输入。单元测试必须尽可能小,因此移除任何可能的外部依赖。
这些测试维护成本低,成本也低,并且在执行时间上非常快。单元测试的唯一缺点是,你永远不能完全依赖它,因为它并不测试整个系统。因此,应该使用其他类型的测试。让我们看看如何进行这种类型的测试:
- 将
pubspec.yaml导入到你的测试框架中,如下所示:
dev_dependencies:
flutter_test:
sdk: flutter
- 在
test/unit_test.dart中编写测试代码:
import 'package:test/test.dart';
void main() {
test('the answer to the question', () {
var answer = 42;
expect(answer, 42);
});
}
- 在项目文件夹中运行
flutter test test/unit_test.dart来运行测试。或者,你可以运行flutter test来运行所有测试。
单元测试是在本地 Dart VM 上运行的,使用 Flutter 引擎的无头版本。这使得过程更快,因为它不需要启动真实的 Flutter 引擎或编译真实的应用程序。
小部件测试
小部件测试也称为组件测试。正如其名所示,它用于测试单个小部件,这种测试的目标是验证小部件是否按预期工作并显示。
此外,您可以在测试期间使用WidgetTester实用工具进行多件事情,例如向组件发送输入、在组件树中查找组件、验证值等等。
让我们看看代码中的组件测试是如何看的:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('my first widget test', (WidgetTester tester) async {
// You can use keys to locate the widget you need to test
var sliderKey = UniqueKey();
var value = 0.0;
// Tells the tester to build a UI based on the widget tree passed to it
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MaterialApp(
home: Material(
child: Center(
child: Slider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
);
},
),
);
expect(value, equals(0.0));
// Taps on the widget found by key
await tester.tap(find.byKey(sliderKey));
// Verifies that the widget updated the value correctly
expect(value, equals(0.5));
});
}
在测试过程中,如果您需要查看 UI,您始终可以使用debugDumpApp()函数或使用flutter test/widget_test.dart运行测试。这样,您在测试期间也将能够与组件进行交互。
集成测试
现在,让我们来看看集成测试。这种测试用于测试整个应用程序或应用程序的较大部分。集成测试可以用来验证应用程序是否按预期执行,或者测试代码的性能。集成测试是在真实设备或模拟器上运行的,但它们不能像在组件测试中那样使用无头版本的 Dart VM 运行。
现在,让我们开始编写和运行测试:
- 将
flutter_driver包添加到pubspec:
dev_dependencies:
flutter_driver:
sdk: flutter
-
启用 Flutter 驱动器扩展,并在
main.dart中添加对enableFlutterDriverExtension()函数的调用。 -
使用
flutter drive命令运行集成测试:
flutter drive --target=my_app/test_driver/my_test.dart
概述
在本章中,我们安装了使用 Flutter 所需的工具;然后我们熟悉了用于我们的 IDE,并查看了一个 Flutter 中最好的功能之一——热重载。然后我们学习了每个应用程序开发工作流程中必需的两个基本概念,即调试和测试。
这些概念将帮助我们开始使用 Flutter,并开始用它来构建我们的应用程序。
在下一章中,我们将深入到组件的世界,了解组件目录为我们提供的不同类型的组件。
第三章:组件,无处不在的组件
在 Flutter 中,组件的概念非常重要。正如 第一章 中所述,介绍 Flutter,Flutter 中的所有内容都是一个组件。你可能会对组件有很多疑问,例如“组件的基本类型有哪些?”“我如何创建一个组件?”“有哪些好的组件示例?”等等。
在本章中,我们将一起探讨这些问题。我们首先将查看组件目录,了解将帮助你使用 Flutter 构建应用程序的基本组件。我们还将学习如何创建自定义组件,然后查看 Flutter 应用程序中路由和导航的概念。所有这些主题将在以下章节中介绍:
-
Widgets Catalog
-
创建组件
-
路由和导航
Widgets Catalog
Flutter 团队构建了这个名为 Widgets Catalog 的非常好用的网站(flutter.io/widgets/),在这里你可以探索 Flutter 中已经存在的各种组件,它们被按类别划分。你将在你的应用程序中使用很多这些组件,所以你对它们了解得越多,你就能在应用程序中使用得越高效。
然而,有一些基本的组件被列出,这将帮助你熟悉你在 Widgets Catalog 中将找到的组件类型。以下是一个这些组件的列表:
-
Container -
图片 -
Text -
图标 -
RaisedButton -
Scaffold -
Appbar -
PlaceHolder -
Row -
Column -
ListView
让我们逐一详细探索这些组件。
容器
这是目录中复杂组件之一。它用于在父组件中包含子组件,通过应用一些样式属性来实现。
容器使得应用各种功能成为可能,例如,设置背景颜色,在容器内对齐子组件,对子组件的大小设置一些约束,以及应用一些装饰或转换属性到子组件(例如,你可以旋转一个组件)。当我们查看你可以用这个组件做什么时,它可以被认为是一个复杂的组件。但在大多数情况下,我们只需要它的几个功能。
现在,让我们看看显示组件的代码。代码将如下所示:
Center(
child: Container(
decoration: BoxDecoration(border: Border.all()),
height: 200.0,
width: 200.0,
),
),
将显示以下输出:

有时候,你需要根据条件表达式显示组件——例如,在这个例子中:
function getIcon(bool condition) {
if (condition == true) return Icon(Icons.edit);
else return Container();
}
上述代码展示了容器条件表达式的示例。它的工作方式与大多数条件表达式类似,如果条件为真,你将得到你的常规组件。但如果条件为假,你将得到一个称为null组件的东西。
图片
在您的应用程序中显示图片是您必须拥有的一个功能。如今几乎没有应用程序缺少显示图片的功能。为此,图片小部件就派上用场了。我们可以使用以下代码来使用图片小部件:
Center(
child: Container(
height: 200.0,
width: 200.0,
child: Image.network("https://flutter.io/images/flutter-mark-square-100.png"),
),
),
当您使用前面的代码时,将显示以下输出:

您可以探索它提供的几个构造函数,但我建议您根据您想要使用的源来尝试使用它们。例如,如果您有一个imageProvider,您将使用默认构造函数,但如果图片在AssetBundle中,您应该使用Image.asset构造函数。
这是一个显示图片的小部件,图片有几种不同的格式。以下是图像小部件支持的图像格式列表:
-
JPEG -
PNG -
GIF -
Animated GIF -
WebP -
Animated WebP -
BMP -
WBMP
Text
此小部件与上一个小部件一样简单易懂。它用于在屏幕上以单一样式显示文本。我们还可以在单行或多行上显示文本;这取决于布局约束。使用此小部件时的样式参数是可选的。如果没有提供样式参数,则小部件将使用包围的DefaultTextStyle的样式,如果提供的样式的TextStyle.inherit属性为 true,则给定样式将与默认样式合并。
以下代码可以用来使用文本小部件:
Center(
child: Container(
height: 200.0,
width: 200.0,
child: Text("This is a text"),
),
),
以下截图将显示小部件在屏幕上的显示方式:

有时候您可能想要对这个文本小部件做更多的事情。例如,为了将多个样式(在行中显示一些粗体字)应用于文本,您可以使用TextSpan.rich构造函数,或者为了给文本添加交互性,您可以使用GestureDetector。
我建议使用FlatButton而不是文本小部件来进行交互。
Icon
图标小部件用于使用IconData中描述的字体绘制图标,例如Icon类中预定义的材质IconData。
以下代码可以用来使用Icon小部件:
Center(
child: Container(
height: 200.0,
width: 200.0,
child: Icon(Icons.flag),
),
),
以下截图显示了Icon小部件在屏幕上的外观:

就像文本小部件一样,我们也可以使用Icon小部件添加交互性。为此,我们可以使用GestureDetector。
RaisedButton
此小部件用于显示一个简单的提升按钮。按钮是提升的,因为按钮基于一个材质小部件,当按钮被按下时,其提升会增加。如果onPressed回调为空,则按钮将被禁用,并且它将类似于disabledColor中的平面按钮。
以下代码可以用来使用RaisedButton小部件:
Center(
child: Container(
height: 200.0,
width: 200.0,
child: RaisedButton(
onPressed: () => print("on pressed"),
child: Text("BUTTON"),
color: Colors.blue,
),
),
),
当您使用前面的代码时,将显示以下截图:

建议使用RaisedButton在否则主要平坦的布局中添加维度。我建议不要在对话框或卡片中使用此类按钮。
Scaffold
Scaffold是基于材料设计的基本布局结构。在实践中,如果你使用材料设计,你的应用中的每个屏幕都将有一个Scaffold作为其基础。Scaffold小部件通过提供 API 用于显示抽屉、snackbars、底部面板、浮动操作按钮等。要显示 snackbar 或底部面板,你必须使用Scaffoldstate当前上下文。我们可以通过Scaffold.of来使用它,并使用ScaffoldState.showSnackbar函数。
以下代码可以用于使用Scaffold显示snackbar:
Center(
child: Container(
height: 200.0,
width: 200.0,
child: RaisedButton(
onPressed: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text("HELLO!"),
));
},
child: Text("BUTTON"),
color: Colors.blue,
),
),
),
使用上述代码将显示以下输出:

AppBar
AppBar基本上被用作Scaffold的一个属性,大多数Scaffold都有应用栏。应用栏由一个工具栏和可能的其他小部件组成。例如,它可以托管TabBar、FlexibleSpaceBar,或者可选地跟随PopupMenuButton以执行较少见的操作。
用于AppBar的属性是Scaffold.appBar。它看起来如下:

上述图表显示了appBar组件将放置每个小部件的位置。
如果省略了前导小部件,并且Scaffold有一个抽屉,那么appBar将放置一个按钮以打开抽屉。如果最近的导航器有任何之前的路由,将插入一个BackButton。
PlaceHolder
PlaceHolder是另一个通过其名称解释自己的小部件。PlaceHolder小部件用于为小部件保留位置。它绘制一个表示稍后添加其他小部件的框。
以下代码可以用于PlaceHolder小部件:
Center(
child: Container(
height: 200.0,
width: 200.0,
child: Placeholder(),
),
),
上述代码将显示以下输出:

Column
Column对于在 Flutter 应用中组合布局至关重要。它以垂直数组显示其子项。以下代码可以用于Column小部件:
Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
height: 20.0,
width: 20.0,
color: Colors.red,
),
Container(
height: 20.0,
width: 20.0,
color: Colors.green,
),
Container(
height: 20.0,
width: 20.0,
color: Colors.yellow,
),
],
),
),
将显示以下输出:

然而,Column小部件不支持滚动;为此,我们可以使用ListView。
注意,如果你在列中有更多的子项,而它们将适合可用的空间,系统会将其视为一个错误。这是因为列没有回收布局的能力。
Row
Row小部件类似于Column小部件,但仍有不同。我们可以说是column的水平版本。它在水平数组中绘制子项。
以下代码可以用于Row小部件:
enter(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
height: 20.0,
width: 20.0,
color: Colors.red,
),
Container(
height: 20.0,
width: 20.0,
color: Colors.green,
),
Container(
height: 20.0,
width: 20.0,
color: Colors.yellow,
),
],
),
),
将显示以下输出:

关于滚动的相关故事与Column小部件相同。如果你想要滚动子项,建议使用ListView。
ListView
ListView的行为类似于列或行;唯一的区别是它的子项可以滚动。
ListView小部件有三个构造函数:
-
默认情况下,它在其子属性中接受小部件列表。这对于小列表来说是一个不错的选择,因为构建它时,列表将处理每个子项。
-
ListView.builder接受一个索引构建器,按需构建子项。如果你有很多子项,这是一个不错的选择,因为每次列表处理只有可见的子项。 -
ListView.custom接受SliverChildDelegate,它提供了自定义ListView更多方面的能力。
关于 Row、Column 和 ListView 的注意事项
有时,在构建放置在另一个行/列或任何未提供最大高度约束的场景中的行或列时,可能会在运行时遇到异常。
问题在于内部小部件应该填充所有剩余空间,但外部小部件没有特定的尺寸,也应该填充可用空间。因此,它们无法理解在哪里停止,然后抛出异常。
要解决这样的问题,你必须理解为什么内部列/行正在接收无界约束。考虑以下:
-
如果列/行被放置在另一个列/行中,你可以尝试将内部小部件包裹在一个 expanded 小部件中,表示它应该占用外部小部件的剩余空间,而不是它所希望的全部空间
-
如果小部件被放置在
Listview中,并且被包裹在 expanded 或 flexible 中,那么关键是要移除那个包裹小部件,并手动设置内部小部件的尺寸
你可能还会遇到以下截图所示的黄色和黑色条纹横幅的问题:

这个横幅表明行或列超出了其大小。解决方案是使用ListView并让内容滚动,或者只是减少子项的大小。
创建小部件
我们在上一节中看到了许多小部件,但可能存在你找不到正确现成的你想要的小部件,或者你想要将更多小部件组合起来以创建一个可重用组的情况。因此,你必须创建一个自定义小部件。
在 Flutter 中,你可以使用两种类型的小部件来创建自己的自定义小部件:
-
无状态小部件
-
有状态小部件
让我们更详细地看看它们。
无状态小部件
无状态小部件即使在用户与之交互时也保持不变。这种小部件没有状态,因此它们不能根据内部状态改变。它们只能对更高层的小部件变化做出反应。
要构建无状态小部件,我们将扩展StatelessWidget抽象类,如下所示:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
有状态小部件
有状态的组件是具有内部状态以进行管理的动态组件。有状态的组件可以响应状态变化并相应地改变。状态存储在State对象中。要创建StatefulWidget,你必须扩展StatefulWidget抽象类,如下面的代码所示:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
状态将是一个扩展了State<T extends StatefulWidget>抽象类的类。让我们看看一个示例,其中小部件根据其状态改变背景颜色。相应的代码如下:
class _MyHomePageState extends State<MyHomePage> {
bool value = false;
@override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: value ? Colors.black : Colors.white,
appBar: new AppBar(
title: new Text(widget.title),
),
body: Center(
child: Switch(
value: value,
onChanged: (v) {
setState(() {
value = v;
});
}),
),
);
}
}
要触发框架重新构建小部件并应用更改,你必须调用setState()函数,否则它将看不到任何更改。
路由和导航
我们刚刚看了如何使用小部件,但你不会只使用一个小部件。在一个典型的应用程序中,通常会有多个屏幕。当应用程序有多个屏幕时,用户有一个清晰的路径来浏览这些页面变得非常重要,为此,在应用程序中通过路由和导航页面变得非常重要。
要做到这一点,如果你来自 Android 背景,你会使用更多的活动或片段,而在 iOS 中,你会创建新的viewControllers。
在 Flutter 的世界里,新屏幕是组件!要导航到新路由,我们可以使用Navigator.push()函数,将当前上下文和一个新的MaterialPageRoute作为参数传递:
Within the `FirstScreen` Widget
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
}
SecondScreen将是一个普通的小部件,用于构建屏幕。例如:
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
);
}
}
要返回,我们将使用导航器的另一个函数:Navigator.pop()。此函数将从导航器管理的路由堆栈中删除当前路由。我们还可以使用此函数在浏览屏幕时向用户返回值。让我们在下一节中详细探讨这一点。
导航时返回值
在从一个屏幕切换到另一个屏幕时向读者返回值可以提高你应用程序的用户体验。例如,当打开应用程序时,屏幕上简单的welcome可以提高用户体验。为此目的,在 Flutter 中,我们有Navigator.pop()。
Navigator.pop()函数接受当前上下文作为参数,但它有一个可选的动态参数。这意味着在弹出屏幕时,你可以返回任何值。
查看一下Navigator.push()的返回值,你可以看到它返回一个Future<dynamic>。因此,在推送新屏幕时,你可以等待弹出的返回值。例如:
function getConfirmation(BuildContext context) async {
return await Navigator.push(context, MaterialPageRoute(
builder: (context) => ConfirmationScreen(),
) ?? false;
}]
ConfirmationScreen将如下所示:
class ConfirmationScreen extends StatelessWidget {
Widget build(BuildContext context) => Scaffold(
body: ButtonBar(
children: <Widget>[
RaisedButton(
child: Text("OK"),
onPressed: () => Navigator.pop(context, true),
),
RaisedButton(
child: Text("CANCEL"),
onPressed: () => Navigator.pop(context, false),
),
],
),
);
}
摘要
在本章中,我们浏览了小部件目录;这个目录包含了一些基本的小部件,我们可以在不构建自己的小部件的情况下立即在我们的应用程序中使用它们。了解这些基本小部件是很有好处的,因为你在你的应用程序中会大量使用它们。但有时你将需要定制化的小部件,为了帮助你,我们介绍了无状态和有状态的小部件,这将帮助你定制你的小部件。最后,我们学习了如何在这些小部件中进行导航和路由。
接下来是什么?你可以使用的更多小部件来构建你的应用程序。在下一章中,我们将看到一些可以用来构建美观布局的小部件。
第四章:利用 Widgets 的多样性
在本章中,我们将首先了解 Flutter 中的约束以及它如何帮助您进行应用程序开发。然后,我们将简要介绍动画及其类别,并查看其中的常见模式。然后,我们将继续使用ListView和滚动小部件,最后简要介绍银色元素。所有这些主题将在以下章节中介绍:
-
Flutter 中的约束
-
在 Flutter 中引入动画
-
使用
ListView和滚动小部件 -
引入银色元素
Flutter 中的约束
Flutter 中的每个部件都是由一个RenderBox对象渲染的,该对象接受父级提供的约束并在这些约束内调整自身大小。
约束和大小之间的区别在于,前者给出了高度和宽度的最小值和最大值,而后者由特定的高度和宽度组成。
有三种类型的RenderBox,根据它们的行为区分如下:
-
那些试图尽可能大(
ListView、居中等)的 -
那些试图与子项大小相同的
-
那些试图达到特定大小(图像、文本等)的
每条规则都有例外。
一些小部件的行为会根据它们的构造函数参数而变化。例如,容器小部件通常会尽可能大,但如果给它一个宽度(或高度),它会尝试达到那个特定的大小。
特定约束是无界的(或无限的)。在这种情况下,最大宽度或高度被设置为double.INFINITY。
尝试尽可能大的盒子在与无界约束一起使用时不会工作,并且框架将抛出异常。
这可能发生在弹性盒子(行/列)和可滚动区域(ListView和其他ScrollView子类)中。
约束可以是紧的。这意味着它不留给RenderBox对象选择大小的空间。一个例子是 App 小部件,它强制视图与屏幕大小相同。
弹性盒子(行和列)在有界或无界约束中的行为不同:
-
在有界约束中,它们会尝试在那个方向上尽可能大
-
在无界约束中,它们会尝试将子项适应那个方向
在 Flutter 中引入动画
动画是部件的一个重要特性。有时,开发者认为动画并不是很重要,但设计师知道一套好的动画可以吸引许多用户。它们还贡献于应用程序的外观和感觉,使其更具个性。
Flutter 具有出色的动画支持,使其轻松构建良好的效果和运动。许多小部件在其设计规范中带有标准运动效果,但您可以根据自己的需求进行自定义。
让我们看看动画类别,我们将看到 Flutter 中动画分为的两个类别,然后看看动画的常见模式。
动画类别
通常,动画分为两类:
-
缓动动画:简称 in-betweening。在这种情况下,我们定义开始和结束点、时间轴以及时间和速度的曲线。框架将完成其余的工作,计算过渡并执行它。
-
基于物理的动画:这类动画旨在代表现实世界的表现。
常见模式
作为用户,你可能已经注意到,大多数应用中经常使用某些类型的动画。这些类型的动画是动画中的常见模式。
在 Flutter 中,你可以找到三种常见模式:
-
动画列表/网格:在添加/删除元素时进行动画的简单列表或网格。
-
共享元素过渡:当在具有公共元素的两个页面之间导航时使用。例如,一个在一条路由中显示缩略图并在另一条路由中显示正常图片的图片。
-
交错动画:由一系列组成更大动画的动画序列。它们可以是顺序的或重叠的。
使用 ListView 和滚动组件
Flutter 支持多个滚动组件,例如 Gridview、ListView 和 PageView。列表是最常用的滚动组件,是一个可滚动的线性组件列表。它允许在滚动方向上依次显示其子组件。
ListView
ListView 是一个可滚动的线性列表项,是最常用的组件之一。如果你在 Android 或 iOS 上工作过 ListView,这将非常直接。就像在所有情况下一样,ListView 会依次产生子列表项。构建 ListView 有几种方法,所以让我们逐一看看这些方法。
使用 List
构建 ListView 最简单且最独立的方式是使用显式的 List<Widget> 子组件。这种方法适用于具有固定数量子组件的列表。看看下面的代码:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Travel Utilities';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.map),
title: Text('Bookmarked Favorite Locations'),
),
ListTile(
leading: Icon(Icons.account_balance_wallet),
title: Text('Expense Tracker'),
),
ListTile(
leading: Icon(Icons.photo_album),
title: Text('Photo Album'),
),
ListTile(
leading: Icon(Icons.add_location),
title: Text('Places To Visit Nearby'),
),
ListTile(
leading: Icon(Icons.audiotrack),
title: Text('Podcast'),
),
ListTile(
leading: Icon(Icons.phone),
title: Text('Emergency Contacts'),
),
],
),
),
);
}
}
以下图像显示了运行上述代码后的预览效果:

使用 ListView.Builder
ListView.builder 构造函数需要 IndexedWidgetBuilder,这有助于开发者按需构建子列表项。这非常适合用于大量或无限数量的可见子项,与 ListView 构造函数不同。另一个区别是,在 ListView 的情况下,所有列表项必须首先定义,而在这种情况下,ListView.builder 构造函数将在列表项滚动到屏幕上时创建运行时。
定义 ListView.builder 简单直接,如下面的代码块所示:
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text("Index $index"),
); //ListTile
},
)//ListView.builder
使用前面的代码,你会看到一个ListView构造函数,它显示每个项目的索引,并附有粘合的文本。完整的代码如下:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'ListView Index';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body:
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text("This is Position: $index"),
); //ListTile
},
) //ListView.builder
),//Scaffold
);//MaterialApp
}
}
运行代码后,应用将显示如下:

现在,我们可以添加一个数据源来工作。数据源可以是消息、搜索结果,或者你希望从中获取数据的互联网上的源。我们将使用List<E>.generate构造函数来生成值,如下定义:
List<L>.generate(int length,L generator(int index), {bool growable: true})
这将创建一个长度为positions的值列表,并用通过调用生成器为每个索引在 0 到长度-1 的范围内递增顺序创建的值填充它。创建的列表是固定的,除非将growable值设置为true。
下面是使用数据源生成ListView的完整代码示例:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp(
items: List<String>.generate(100, (i) => "List Item $i"),
));
}
class MyApp extends StatelessWidget {
final List<String> items;
MyApp({Key key, @required this.items}) : super(key: key);
Widget build(BuildContext context) {
final title = 'ListView Index';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body:
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('${items[index]}'),
);//ListTile
},
)//ListView.builder
),//Scaffold
);//MaterialApp
}
}
前面代码的输出如下:

通过调用ListView.separated进行分隔的ListView
在之前的代码执行案例中,我们看到尽管ListTiles被列出,但它们之间没有分隔。为了在ListTiles之间创建分隔符,它还提供了一个辅助构造函数来创建ListView。这个构造函数是ListView.separated。这个分隔符由分隔类调用,以构建一个一设备像素厚的水平线,两侧都有填充。分隔符可以在列表、抽屉或独立内容中使用,垂直或水平使用,具体取决于Axis枚举的值,如下所示ListView.separated构造函数:
ListView.separated({
Key key,
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsetsGeometry padding,
@required IndexedWidgetBuilder itemBuilder,
@required IndexedWidgetBuilder separatorBuilder,
@required int itemCount,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true,
bool addSemanticIndexes: true,
double cacheExtent
})
构造函数可以通过以下方式调用:
ListView.separated(
itemCount: 25,
separatorBuilder: (BuildContext context, int index) => Divider(),
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('item $index'),
);
}, )
这构建了一个固定长度的可滚动线性数组,其中的列表项通过列表项separators分隔。itemBuilder回调将在索引大于或等于0且小于itemCount时被调用。分隔符在列表中的第一个项之后和最后一个项之前构建。separatorBuilder回调将在索引大于或等于0且小于itemCount 1时被调用。
下面是使用ListView.separated的ListView构造函数的示例:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp(
items: List<String>.generate(100, (i) => "List Item $i"),
));
}
class MyApp extends StatelessWidget {
final List<String> items;
MyApp({Key key, @required this.items}) : super(key: key);
Widget build(BuildContext context) {
final title = 'ListView Index';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body:
ListView.separated(
itemCount: 25,
separatorBuilder: (BuildContext context, int index) => Divider(),
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('${items[index]}'),
);//ListTile
},
)//ListView.builder
),//Scaffold
);//MaterialApp
}
}
一旦运行前面的代码,你会看到带有分隔符的ListView。
使用ListView.custom构造函数
通过使用SilverChildDelegate,此方法提供了自定义子模型几个方面的能力,定义了它们构建的方式。为此所需的主要参数是SliverChildDelegate,它构建项目。SliverChildDelegates的类型如下:
-
SliverChildListDelegate -
SliverChildBuilderDelegate
SliverChildListDelegate接受直接的孩子列表,而另一方面,SliverChildBuiderDelegate接受IndexedWidgetBuilder。看看ListView.custom构造函数:
const ListView.custom({
Key key,
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsetsGeometry padding,
double itemExtent,
@required SliverChildDelegate childrenDelegate,
double cacheExtent,
int semanticChildCount
}
)
水平列表
一旦你熟悉了垂直列表,水平列表就很简单了。在这种情况下,我们调用ListView构造函数,传递一个水平scrollDirection。这仅仅覆盖了默认的垂直方向。在这种情况下,我们使用了一个Container小部件,这是一个易于使用的组合了常见的绘制、定位和尺寸小部件的小部件。看看下面的代码:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Horizontal List Example';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Container(
margin: EdgeInsets.symmetric(vertical: 100.0),
height: 300.0,
child: ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
Container(
width: 120.0,
color: Colors.orange,
),
Container(
width: 120.0,
color: Colors.white,
),
Container(
width: 120.0,
color: Colors.green,
),
Container(
width: 120.0,
color: Colors.pink,
),
Container(
width: 120.0,
color: Colors.lime,
), // Container
], // <Widget>[]
), //ListView
), //Container
), // Scaffold
); //MaterialApp
}
}
ListView的scrollDirection: Axis.horizontal属性确保列表可以水平滚动。一旦代码成功运行,你将看到以下结果:

网格列表
就像在水平列表的情况下,事件网格列表也很容易构建。它使用GridView.count构造函数,允许我们指定屏幕上需要多少行和列。在下面的例子中,我们构建了100个小部件,打印出它们的位置值:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Grid List Example';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: GridView.count(
// Create a grid with 3 columns.
crossAxisCount: 3,
// Generate 100 Widgets that display their positions in the List
children: List.generate(100, (index) {
return Center(
child: Text(
'Position $index',
style: Theme.of(context).textTheme.headline,
), // Text
); //Center
}), //List.Generate
), //GridView.count
), //Scaffold
); // MaterialApp
}
}
上述代码将产生以下输出:

介绍 silvers
我们先看了看ListView,现在让我们快速了解一下什么是 silvers,通过从ListView中举一个简单的例子。如果你的布局设计得足够精美,那么视觉上会非常吸引人。这正是 silvers 能帮到你的地方。silvers 是可滚动区域的一部分,通过它你可以将自定义滚动引入你的视图。让我们以ListView为例来看一个简单的例子。如果一个应用栏保持静态,有时可能会遮挡视图,所以在这种情况下,可以使用 silvers 来隐藏应用栏,在你滚动时。
需要注意的一点是,所有的 silvers 组件都应该放在CustomScrollView内部。作为开发者,你可以将你的 silvers 列表组合起来,构建你自定义的可滚动区域。
摘要
在本章的开头,我们开始讨论 Flutter 中的约束。然后我们讨论了 Flutter 中的动画和动画形式。在下一节中,我们执行了一些不同类型列表的示例。在最后一节中,我们探讨了如何自定义动画部分。
在下一章中,我们将拓宽我们的 Flutter 视野,探讨 Flutter 中的网络和可访问性。
第五章:扩展 Flutter 视野
在本章中,我们将首先通过构建一个简单的应用程序来讨论 Flutter 中的网络,该应用程序使用 JSON 从服务器获取数据。每个应用程序都必须具备无障碍功能,以满足大量用户的需求,我们将在无障碍选项中介绍这一点。在最后一节中,我们将讨论本地化,以便您的应用程序在全球范围内增长,支持多种语言。
在本章中,我们将涵盖以下主题:
-
Flutter 中的网络
-
Flutter 中的无障碍功能
-
国际化 Flutter 应用程序
Flutter 中的网络
网络是任何应用程序的骨架,了解如何进行网络调用至关重要。在 Flutter 中处理网络调用简单且遵循标准流程。Flutter 库和方法使开发者更容易构建具有网络功能的应用程序。本章将重点介绍制作网络请求。
使用包
与许多平台一样,Flutter 支持使用由开发者贡献给 Flutter 和 Dart 生态系统的共享包。这通过让开发者快速构建应用程序而不是担心从头开始编写代码来促进开发。一些最常用的包包括但不限于:进行网络请求(HTTP);使用设备 API,例如设备信息(device_info);查找信息并控制摄像头,包括对摄像头流和捕获图像的预览支持(camera);使用 GPS 坐标查找和使用设备位置(geolocator);以及使用第三方平台 SDK(如 Firebase)。您可以在pub.dartlang.org/packages找到 Flutter 支持的完整包列表。
将现有包依赖项添加到应用程序中
一旦您确定要包含的包集,请按照以下步骤添加依赖项。为了本例的目的,我们选择了 HTTP 包添加到应用程序中。此包包含一组高级函数和类,可以帮助开发者在应用程序开发过程中消费 HTTP 资源,并且它是平台无关的。它支持命令行和浏览器:
-
创建依赖项:打开位于您的应用程序文件夹内的
pubspec.yaml文件,并在依赖项下添加http:。所有包都有一个版本号,在它们的
pubspec.yaml文件中指定。包的当前版本显示在包名称旁边。当您提到Plugin_Name_1:时,它被解释为Plugin_Name_1: any。这表示可以使用包的任何版本。建议使用特定版本,以确保在更新时应用程序不会崩溃。 -
安装已添加依赖的包。你可以通过运行
flutter packages get命令来安装它。如果你使用 Android Studio/IntelliJ,你还可以点击pubspecs.yaml顶部的操作栏中的 Package Get 选项。如果你使用 VS code,请点击pubspec.yaml顶部操作栏右侧的 Get Packages。 -
在你的 Dart 代码中包含相应的
import语句。在这种情况下,是import 'package:http/http.dart'。如果你遗漏了任何内容,你总是可以在Pub上的包页面上的安装选项卡中进行交叉检查。 -
在这一点上,最好停止并重新启动应用,以避免使用该包时出现
MissingPluginException等错误。
升级现有包
当你在pubspec.yaml文件中添加了一个包后,第一次运行flutter packages get(在 IntelliJ 中将是Packages Get)时,Flutter 会保存pubspec.lock锁文件中找到的版本。要升级包,你可以运行 Flutter 包升级(在 IntelliJ 中是Upgrade dependencies)。使用此命令,Flutter 将检索包的最高可用版本。如果你在pubspec.yaml中指定了范围约束,它将根据约束的指定获取更新。
构建 REST 服务
对于开发者来说,最突出的任务之一是为项目构建 REST 服务,这些服务可以帮助你以 JSON 格式收集数据,你可以在应用的前端进行反映。想象一下你在开发一个应用,并想模拟一个 REST web 服务来获取你的演示数据。你当然可以使用 Node.js、MongoDB 或其他平台来构建后端服务器,但其中最简单的方法之一是使用 JSON 服务器。JSON 服务器是一个简单的项目,它通过 CRUD 操作模拟 REST API。这个项目几乎不需要设置时间,你可以快速处理数据以确保一切按预期工作。这对于学习构建 REST API 的开发者来说,了解数据如何通过后端进行原型设计和模拟处理是非常理想的。
设置 JSON 服务器
该项目的设置可以在github.com/typicode/json-server找到。请注意,此项目仅为了演示目的构建了一个完整的假 REST API。在我们开始设置之前,请确保以下组件已准备好在你的系统上:
-
Node.js:JSON-Server 建立在 Node.js 之上。如果你已经安装了它,请确保保持其更新。要查看 Node 的版本,请运行
node -v命令。 -
NPM 包:NPM 代表 Node 包管理器,它便于轻松安装、更新、配置和卸载 Node JS 平台模块/包。确保系统上已安装 NPM。如果没有,请参考
www.npmjs.com/get-npm。在此阶段,引用 NPM 是一个独立于 Node 的项目,并且经常更新。要更新 NPM,请使用sudo npm install npm@latest -g命令。 -
cURL:这是一个开源的命令行工具,它使用 URL 语法进行数据传输。如果你已经安装了cURL,请使用curl -V命令(注意V是大写)。如果你需要安装cURL,请运行brew install curl命令。
JSON 服务器作为一个 NPM 包提供,因此我们可以简单地运行以下命令来安装它:
$ npm install -g json-server
-g 选项使包能够在你的系统上全局安装。一旦安装成功,运行以下命令进行交叉检查:
$ json-server -v
构建资源文件
资源是与数据相关联的任何事物。例如,如果你正在制作一个电影评论网站,电影、评论者、用户等等,都是资源。你可以基于这些资源创建 API 端点。API 端点帮助你检索或更新服务器上的数据。在这种情况下,我们将使用资源作为 JSON 文件。这个 JSON 文件将作为你使用 json-server 设置的模拟服务器的配置和数据库文件。
Json-server 在 JSON 文件中工作,创建一个 JSON 文件很简单。创建一个新文件名为 Books.json,填充以下内容,并保存。注意,我们指定的数组名称是 Movie,因此 json-server 将基于此名称创建 REST API:
{
"Movie": [
{
"id": 1,
"Movie Name": "Avengers: Infinity War",
"Year": "2018",
"Category": "Science Fiction"
},
{
"id": 2,
"first_name": "Black Panther",
"Year": "2018",
"Category": "Science Fiction"
},
{
"id": 3,
"first_name": "Mission: Impossible – Fallout",
"Year": "2018",
"Category": "Action"
},
{
"id": 4,
"first_name": "Annihilation",
"Year": "2018",
"Category": "Fantasy"
}
]
}
运行 json-server
最后一步是运行 json-server 以确保本地正在运行模拟服务器。运行以下命令:
$ json-server --watch db.json
恭喜!你现在已经成功设置了 json-server。打开 http://localhost:3000/ URL 检查你是否能看到以下内容。在资源标签下,你将能够看到我们创建的 Movies JSON 文件。
不要关闭终端,因为这会杀死 json-server。如果你的端口 3000 被占用,你可以在 json-server.json 配置文件中设置新端口号的选项。
从服务器获取数据
从服务器获取数据是一个常用的功能。它使用我们在本章前面覆盖的 HTTP 包执行。要遵循的步骤如下:
-
包含 HTTP 包并执行网络请求
-
将响应转换为自定义 Dart 对象
-
使用 Flutter 获取数据并显示
由于我们已经在使用包部分学习了如何添加 HTTP 包,我们现在将继续进行网络请求的制作。在下一步中,我们将使用 JSON-Server 和 http.get() 方法获取示例 JSON 内容。
我们使用 Future 函数,这是 Dart 的一个核心类,用于处理异步任务,并且与 http 一起使用可以返回成功的 http 调用的数据:
Future<http.Response> fetchPost() {
return http.get('http://localhost:3000/Movie/1');
}
现在我们创建一个 Post 类,它将包含我们网络请求的数据。为了确保我们能够从 JSON 正确创建 Post 对象,我们将包括一个 factory 构造函数。在我们的示例中,我们为每个要获取的数组提供了四个数据类别,即 id、movieName、year 和 category:
class Post {
final int id;
final String movieName;
final int year;
final String category;
Post({this.id, this.movieName, this.year, this.category});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
movieName: json['movieName'],
year: json['year'],
category: json['category'],
);
}
}
接下来,我们需要更新 fetchPost 函数以返回一个 Future<Post>,我们将遵循以下三个步骤:
- 首先,使用
dart:convert包将响应体转换为 JSON Map。这个包包含用于在不同数据表示之间转换的编码器和解码器。为了使用它,你首先必须在你的包的pubspec.yaml文件中添加依赖项:
dependencies: convert: ².1.1
现在,在你的 Dart 代码中使用 package:convert/convert.dart 导入。
-
如果我们从服务器收到一个状态码为
200的OK响应,这意味着数据已被获取,你可以使用fromJSON工厂将 JSON Map 转换为Post。 -
如果响应是意外的,你可以标记一个错误。
这里是检查之前提到的情况的代码片段:
Future<Post> fetchPost() async {
final response =
await http.get('http://localhost:3000/Movies/1');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, flag an error.
throw Exception('Failed to load post');
}
}
为了获取数据并显示它,我们使用 Flutter 内置的 FutureBuilder 小部件,它有助于轻松处理异步数据源。为了实现这一点,我们需要两个参数:
-
我们想要与之工作的未来的名称。在我们的示例中,我们称它为
fetchPost()函数。 -
一个构建函数,它根据 Flutter 的状态(加载、成功或错误)告诉 Flutter 应该渲染什么:
FutureBuilder<Post>(
future: post,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.movieName);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner
return CircularProgressIndicator();
},
);
通过在 build() 方法中调用 API 来构建代码是方便的,但并不推荐。这会使 Flutter 每次想要在视图中更改任何内容时都调用 build() 方法,这会导致你的应用因不必要的频繁 API 调用而变慢。更好的方法是,在页面最初加载时调用 API,并使用 StatelessWidget。
使用这种方法,你将使父级小部件负责调用获取方法,存储其结果,然后将它传递给你的小部件:
class MyApp extends StatelessWidget {
final Future<Post> post;
MyApp({Key key, this.post}) : super(key: key);
这是完整的代码,它将使用 json-server 通过读取 Movies.json 来获取 JSON 内容:
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Post> fetchPost() async {
final response =
await http.get('http://localhost:3000/Movies/1');
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
class Post {
final int id;
final String movieName;
final int year;
final String category;
Post({this.id, this.movieName, this.year, this.category});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
movieName: json['movieName'],
year: json['year'],
category: json['category'],
);
}
}
void main() => runApp(MyApp(post: fetchPost()));
class MyApp extends StatelessWidget {
final Future<Post> post;
MyApp({Key key, this.post}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'JSON Fetch Data Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('JSON Fetch Data Example'),
),
body: Center(
child: FutureBuilder<Post>(
future: post,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.movieName);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner
return CircularProgressIndicator();
},
),
),
),
);
}
}
在本章的开头,我们讨论了包是什么以及如何使用它们。一旦包设置好,我们就讨论了如何构建 JSON Server 以获取测试数据。在最后一节,我们简要地看了一下如何将 JSON 数据获取到应用中,展示了我们创建并使用 JSON server 运行的 JSON 文件内容。
Flutter 的可访问性
使您的应用对众多用户可访问可能是一个很好的举措。这也包括有残疾的人,如盲人、听力、语音或运动障碍。根据世界卫生组织关于残疾的报告,全球有超过一亿用户在日常生活中面临身体挑战。技术可以帮助人们实现革命性的变化,这就是当构建满足他们特定需求的应用程序时,可以很好地帮助他们的时候。
并非所有用户都以特定的方式使用应用程序,因此,关注可访问性不仅可以帮助用户下载和使用应用程序,而且还会传播到新的用户层面。
Google 提供了一个用于检查可访问性支持的应用程序,该应用程序作为 Google Play 上的可访问性扫描仪提供,网址为 play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor。此应用程序使您能够找到开发者可以在应用程序中实现的可访问性提供。对于 iOS,XCode 提供了可访问性检查器。
Flutter 支持三个用于可访问性支持的功能:
大字体
随着年龄的增长,很多人无法像年轻时那样看到内容。有些人阅读文本时遇到问题,尤其是当开发者考虑使用默认大小而没有考虑屏幕大小和方向等因素时。最快的方法之一是确保文本在可访问性选项中缩放时考虑消费者的设备规格。
Flutter 具有自动处理文本大小计算的特性。例如,Text 小部件有一个 textScaleFactor 属性,允许对文本进行缩放。字体大小通过乘以 textScaleFactor 值来确定在屏幕上渲染的新字体大小,该大小以逻辑像素为单位。例如,如果 textScaleFactor 是 1.5,文本将比指定的字体大小大 50%,如下所示:
Text(
'Hello India, how are you?',
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontWeight: FontWeight.normal),
textScaleFactor: 1.5,
)
需要记住的一点是,如果您手动设置缩放值,用户的可访问性设置将被覆盖。您必须确保放大值不会导致文本过大,以至于用户会卸载应用程序。如果您没有指定值,它将检查最近的 MediaQuery 祖先(MediaQueryData.textScaleFactor)的 textScaleFactor 或 1.0,如果不存在这样的祖先。确保您在所有可访问性设置中测试文本是否正确显示。
屏幕阅读器
对于视力受损的用户,这个可访问性选项可能很有用。它使用户能够接收到关于屏幕内容的语音反馈。您可以在 iOS 中开启语音控制,或在您的设备上的 Android 应用程序中开启 TalkBack 来导航您的应用程序。例如,当使用 TalkBack 时,用户通过手势执行操作,每个操作都会伴随着可听到的输出,使用户知道他们的手势触发是成功的。TalkBack 有三种类型的手势:基本手势、往返手势和角度手势。请注意,用户应使用单一手势,即使是手指压力,以及稳定的速度,以获得无缝体验。
屏幕对比度
使用足够的颜色对比度指定背景和前景颜色,可以增强用户的可读性。此比率范围从 1 到 21,其中 21 表示最高。
W3C 推荐以下内容:
-
至少 4.5:1 的对比度用于较小的文本(小于 18 点常规字体,或 14 点粗体)
-
至少 3.0:1 的对比度用于较大的文本(18 点及以上常规字体,或 14 点及以上粗体)
可访问性是一个重要的功能,不应被忽视。它确保应用程序对更广泛的受众开放,增加了更好的应用程序使用机会。在向大众推出之前测试可访问性选项同样重要。
国际化 Flutter 应用
如其名所示,如果您的应用程序将由国际受众使用,您将需要考虑为特定语言提供 区域 支持。这意味着您需要以这种方式编写应用程序,以便应用程序根据应用程序支持的每种语言或区域渲染值,如文本和布局。Flutter 通过提供类和部件的支持使其变得简单。Flutter 支持 24 种语言的全球本地化类。
在开始国际化之前,必须在 pubspec.yaml 中添加依赖项:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
下一步是导入 flutter_localizations 库,并为 MaterialApp 指定 localizationsDelegates 和 supportedLocales。同时,导入 package:flutter_localizations/flutter_localizations.dart:
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (BuildContext context) => DemoLocalizations.of(context).title,
localizationsDelegates: [
// ... app-specific localization delegate[s] here
const DemoLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), //Supporting English
const Locale('hi', ''), // Supporting Hindi
const Locale('es', ''), // Supporting Spanish
],
localizationsDelegates 列表包含的是生产本地化值集合的工厂元素。GlobalMaterialLocalizations.delegate 提供了用于材料组件库的本地化字符串和其他值。小部件库的默认文本方向由 GlobalWidgetsLocalizations.delegate定义。
有三种方法需要关注:
-
.load:此方法必须返回一个包含相关资源集合的对象 -
.isSupported:如果找到对区域的支持,则返回True。否则将返回False -
shouldReload:如果此方法返回True,则在加载资源后,所有应用程序小部件都将被重建
以下是为您提供的完整代码:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show SynchronousFuture;
import 'package:flutter_localizations/flutter_localizations.dart';
class DemoLocalizations {
DemoLocalizations(this.locale);
final Locale locale;
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'Locale in English',
},
'es': {
'title': 'Local en españa',
},
'hi': {
'title': 'लोकेल हिंदी में',
},
};
String get title {
return _localizedValues[locale.languageCode]['title'];
}
}
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'es', 'hi'].contains(locale.languageCode);
@override
Future<DemoLocalizations> load(Locale locale) {
// Returning a SynchronousFuture here because an async "load" operation
// isn't needed to produce an instance of DemoLocalizations.
return SynchronousFuture<DemoLocalizations>(DemoLocalizations(locale));
}
@override
bool shouldReload(DemoLocalizationsDelegate old) => false;
}
class DemoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(DemoLocalizations.of(context).title),
),
body: Center(
child: Text(DemoLocalizations.of(context).title),
),
);
}
}
class Demo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (BuildContext context) => DemoLocalizations.of(context).title,
localizationsDelegates: [
const DemoLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''),
const Locale('es', ''),
const Locale('hi', ''),
],
home: DemoApp(),
);
}
}
void main() {
runApp(Demo());
}
在成功运行代码后,你将看到以下输出:

摘要
我们首先讨论了网络在应用程序中扮演的重要角色,以及设置和运行用于获取 JSON 代码的本地服务器的示例代码。这一部分之后,我们了解了为什么可访问性很重要,以及开发者可以为支持应用程序的可访问性提供哪些改进。下一节展示了如何使应用程序支持国际化。
在下一章中,我们将讨论如何使用平台功能来构建应用程序。
第六章:使用平台为 Flutter 应用提供动力
使用 Flutter,您可以同时为 Android 和 iOS 构建应用。它使用 Dart 编程语言来完成这一任务。然而,Dart 不会编译成 Android 的 Dalvik 字节码或 iOS 上的 Objective C 绑定。这表明 Dart 代码默认情况下没有直接访问平台特定 API 的能力。
在以下几组示例中,可能需要与宿主环境进行更深入的集成:
-
使用相机功能和地理标记功能的应用
-
读取设备信息,例如操作系统版本和设备规格
-
从设备读取文件夹和文件
-
向应用推送通知
-
与其他应用共享信息
-
位置跟踪
-
使用传感器
-
使用持久化首选项
根据环境提供的支持,列表将继续。使用 Flutter,启用调用平台特定 API 的功能,这些 API 在 Android 上的 Java/Kotlin 代码、iOS 上的 Objective C 或 Swift 中可用,并不是一个困难的任务。
在本章中,我们将学习如何包含包,然后学习如何进行平台特定的调用。我们将学习如何发布我们自己的插件。
我们将在本章中介绍以下主题:
-
使用 Flutter 包
-
使用平台通道
-
构建和发布您自己的插件
使用 Flutter 包
Flutter 的包网站列出了许多帮助我们快速开发应用的包,这些包避免了从头开始开发某些功能的需求。这些包要么是由 Flutter 团队贡献的,要么是由全球的开发者贡献给 Flutter 和 Dart 生态系统的。您可以通过访问发布网站(pub.dartlang.org/)使用现有的包,或者您可以开发和发布自己的包。我们将在本章的“构建自己的包”部分学习如何构建自己的包。
搜索包
访问发布网站以搜索您在应用中想要使用的包。主页显示了一些开发者使用的流行包。您可以通过关键词搜索来显示结果。在选择插件之前,您可能想查看加权分数。搜索email:@dartlang.org或email:flutter-dev@googlegroups.com将给出官方 Flutter 包的结果:

点击任何结果项将打开包的扩展预览。在这种情况下,我们使用的是 url_launcher 包,这是一个由 Flutter 团队开发的官方 Dart 包。此插件用于在移动平台上启动 URL,如支持 Android 和 iOS 的 Web、电话、短信和电子邮件方案:

当你点击任何一个包时,你会看到插件的名称,以及它的最新版本和发布日期。随后,你会找到五个标签选项,具体如下:
-
README.md:关于插件的详细信息 -
CHANGELOG.md:关于此插件 Changelog 版本的信息,包括每个插件的详细信息 -
Example:演示如何使用插件 -
安装:显示设置插件的步骤 -
版本:显示插件的各个版本,以及以下选项:版本名称(例如 5.0.1),上传日期(例如 2019 年 2 月 8 日),文档,和存档
Flutter 插件支持在 Android 和 iOS 上启动 URL,包括 web、手机、SMS 和电子邮件方案。
将包依赖添加到应用程序中
一旦你决定了想要使用的包,你必须让程序依赖于它。在这种情况下,我们使用的是url_launcher 5.0.1包:
-
打开位于你的
app文件夹内的pubspec.yaml文件。 -
查看包页面上的“安装”标签,你将有机会添加依赖项。在这个例子中,我们在依赖项下添加了
url_launcher: ⁵.0.1。 -
接下来,从终端运行以下命令,这将通过命令行安装包:
flutter packages get
如果你使用的是 Android Studio/IntelliJ 这样的编辑器,请点击pubspec.yaml顶部的“包获取”。在 VS code 的情况下,请点击Pubspec**.**yaml顶部的“获取包”:

- 下一步是将相应的
import片段添加到你的 Dart 代码中:
import 'package:url_launcher/url_launcher.dart';
如果你想要升级到包的新版本,请使用“包升级”选项。在包中有新功能可用且你希望将其包含到项目中时,可以使用此选项。在 IntelliJ 中,可以选择“升级依赖项”。
指定包依赖的方式
指定包依赖有两种方式。正如我们之前讨论的,包是通过使用缩写形式plugin1:添加到pubspec.yaml中的。这意味着plugin1:any_version。作为一个开发者,你总是可以查看CHANGELOG.md或包的最近版本来添加正确的值。为了确保应用程序不会崩溃,指定一个版本是至关重要的,这可以通过两种方式之一来完成:
- 使用范围约束指定版本,如下所示,其中你指定最小和最大版本:
dependencies:
url_launcher: '>=0.4.1 <5.0.1'
- 使用
Carat Syntax进行范围约束:
dependencies:
url_launcher: '⁵.0.1'
将代码添加到文件中
大多数包都提供了如何将包组件包含到代码中的见解。包的“示例”标签提供了关于代码执行的更多信息。在我们的例子中,我们使用一个梯度按钮来打开代码中指定的 URL:
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Packages',
theme: ThemeData(
primarySwatch: Colors.deepOrange,
),
home: MyStatelessWidget(),
);
}
}
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Packages in Flutter'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RaisedButton(
onPressed: _initiateURL,
textColor: Colors.black,
padding: const EdgeInsets.all(0.0),
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
Colors.green, Colors.yellow, Colors.yellowAccent],
),
),
padding: const EdgeInsets.all(10.0),
child: Text('Open Flutter Website'),
),
),
],
),
),
);
}
_initiateURL() async {
const url = 'https://flutter.dev';
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Sorry, We could not launch the URL $url';
}
}
}
之前的代码允许用户简单地点击按钮并打开 Flutter Dev 官方网站。如您所注意到的,我们使用了RaisedButton小部件并指定了子属性,这使得我们能够为按钮提供渐变效果。_initiateURL()方法有一个启动函数,它接受定义的 URL 并解析指定的 URL。它还将处理委托给底层系统:

一旦您成功执行了代码并点击了按钮, 打开 Flutter 网站 将打开参数中指定的 URL。您将在手机的默认浏览器上看到结果,如下所示:

使用平台通道
如本章开头所述,Flutter 是一个跨平台框架,为了访问原生 API,您必须访问原生平台函数。在 Flutter 的情况下,它是通过创建到原生平台的平台通道来实现的。使用这些平台通道,开发者可以调用诸如设备信息、文件和文件夹访问、传感器访问、摄像头、共享首选项等原生函数。
如以下截图所示,平台通道可以简单地想象成 Flutter 中的 Dart 代码和宿主应用特定平台代码之间的通信机制。这确保了宿主服务可以通过 Flutter 的 Dart 代码被调用:

在这一点上,值得注意的是,您必须为每个平台设置一个平台通道。在 Android 的情况下,它们被称为MethodChannels,而在 iOS 中,它们被称为FlutterMethodChannels**。 **
在这一点上,了解平台数据类型支持和编解码器非常重要。标准平台通道采用支持高效二进制序列化的标准消息编解码器。它们看起来像 JSON。当您向它们发送和接收值时,序列化和反序列化会自动发生。
Google 的 Flutter 团队列出了以下表格(来源:flutter.dev/docs/development/platform-integration/platform-channels),展示了 Dart 值在宿主平台侧的接收方式,反之亦然:
| Dart | Android | ** iOS** |
|---|---|---|
null |
null |
nil(嵌套时为 NSNull) |
bool |
java.lang.Boolean |
NSNumber numberWithBool: |
int |
java.lang.Integer |
NSNumber numberWithInt: |
int,if 32 bits,not enough |
java.lang.Long |
NSNumber numberWithLong: |
double |
java.lang.Double |
NSNumber numberWithDouble: |
String |
java.lang.String |
NSString |
Uint8List |
byte[] |
FlutterStandardTypedData typedDataWithBytes: |
Int32List |
int[] |
FlutterStandardTypedData typedDataWithInt32: |
Int64List |
long[] |
FlutterStandardTypedData typedDataWithInt64: |
Float64List |
double[] |
FlutterStandardTypedData typedDataWithFloat64: |
List |
java.util.ArrayList |
NSArray |
Map |
java.util.HashMap |
NSDictionary |
在以下示例中,我们将学习如何在 Android 中使用平台通道来计算 Android 手机的电池百分比,使用 Android 的 BatteryManager API。您可以在以下链接中了解更多关于 API 的信息:developer.android.com/reference/android/os/BatteryManager
创建一个新的 Flutter 项目
请创建一个新的 Flutter 项目。确保 Android SDK 路径已正确设置并包含在项目设置中。
创建 Flutter 平台客户端
为了理解电池级别,可以编写 Android 代码并将其传递到 Dart 代码。应用程序的状态类持有应用程序的当前状态,我们需要扩展它以确保捕获当前的电池状态。
因此,我们将使用 MethodChannel 作为包含单个平台方法的通道,该方法将返回 Android 手机的电池级别。客户端和主机端通过传递给通道构造函数的唯一通道名称进行通信。在我们的案例中,我们将其命名为 call.flutter.io/battery:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('call.flutter.io/battery');
// Get Android battery level.
}
下一步是调用方法通道上的方法,我们将使用返回的结果(电池级别)来更新 setState 内的值:
String _batteryLevel = 'Battery Levels are Unknown';
Future<void> _getPhoneBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = _getBatteryLevel() asyncbatteryLevel;
});
}
最后,将构建方法替换为具有动作 onPressed 的 Button,其结果将作为 Text 值显示电池级别。
完整的 Main.Dart 代码如下所示:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Platform Channel API',
theme: ThemeData(
primarySwatch: Colors.yellow,
),
home: MyHomePage(title: 'Flutter Platform Channel API'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('call.flutter.io/battery');
String _batteryLevel = 'Battery Levels are Unknown';
Future<void> _getPhoneBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text ("Click the button to know your phone battery levels"),
RaisedButton(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[Colors.red, Colors.green, Colors.brown],
),
),
padding: const EdgeInsets.all(10.0),
child: Text('Get Phone Battery Level'),
),
onPressed: _getPhoneBatteryLevel,
textColor: Colors.white,
padding: const EdgeInsets.all(0.0),
),
Text(_batteryLevel),
],
),
),
);
}
}
接下来,我们将对 Android 代码进行修改。
修改 MainActivity.java 文件
导航到项目中的 Android 文件夹,并在项目视图中找到 Java 文件夹中的 MainActivity.java 文件。接下来,在这个文件中的 onCreate 方法内,我们创建一个 MethodChannel 并在其中设置一个 MethodCallHandler。请注意,使用的通道名称应与 Flutter 客户端侧相同,正如我们的案例:call.flutter.io/battery。
包含以下导入:
package androcid.flutterapp1;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
接下来,创建一个 String 来保存通道名称:
private static final String CHANNEL = "call.flutter.io/battery";
最后,添加 MethodChannel 方法:
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
// TODO
}
});
在下一步中,我们必须编写 Android 的 onCreate 方法:
private int getBatteryLevel() {
int phoneBatteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
phoneBatteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
phoneBatteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return phoneBatteryLevel;
}
}
要使用 BatteryManager,最低 API 级别为 21,因此我们使用 VERSION_CODES.LOLLIPOP。
我们的最后一步是完成之前添加的 onMethodCall 方法。使用单个平台方法 getbatteryLevel,您可以简单地调用之前步骤中编写的 Android 代码,并使用响应参数在成功和错误情况下返回响应,如下所示:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("Currently unavailable", "Battery level not
available currently.", null);
}
} else {
result.notImplemented();
}
}
});
}
以下为完整的 Android 代码:
package androcid.flutterapp1;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "call.flutter.io/battery";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("Currently unavailable", "Battery level not available currently.", null);
}
} else {
result.notImplemented();
}
}
});
}
private int getBatteryLevel() {
int phoneBatteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
phoneBatteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
phoneBatteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return phoneBatteryLevel;
}
}
代码运行成功后,您将看到以下输出:

当点击按钮时,手机的电池电量将被显示:

构建和发布你自己的插件
插件在 Flutter 生态系统中扮演着重要的角色。谷歌开发者和开源贡献者已经为几个发布的插件做出了贡献。我们在 第五章,拓宽我们的 Flutter 视野中,展示了如何在代码中包含插件。当构建插件时,以下是一些需要记住的技巧:
-
检查现有插件的可用性:在你从头开始编码之前,查看是否有现有的插件以相同或类似的方式工作。
-
考虑 Dart:由于 Flutter 代码是用 Dart 编写的,因此如果主要的逻辑是用 Dart 编写的,这将是非常理想的,这不仅使得导航变得容易,而且跨平台处理代码也更加容易。
-
避免构建仅支持特定平台的插件:开发者可能会被诱惑开始构建一个包含他们可能需要的所有功能的插件,但只考虑单一平台可能不是一个好主意。这不仅会让用户对其在特定平台上的用例感到困惑,还可能导致应用以意想不到的方式运行。
-
避免构建特定平台的 API 方法:作为开发者,你可能会被诱惑构建一个特定平台的函数,但这可能会导致过度设计。尝试将特定平台的逻辑包含在插件本身中。
-
为功能而构建:确保你计划构建的插件具有特定的用例,提供功能,而不仅仅是调用现有的 API。使用现有的本地库没有问题,但问题在于这些 API 在不同平台之间可能无法按预期工作。更多地关注功能而不是 API 可能有助于你构建更好的插件。
-
详细说明功能和安装:当你的插件发布时,通过提供更多关于插件功能和如何无缝将其包含在他们的代码中的详细信息来支持社区。插件的可视性很重要,因为如果社区喜欢这个插件,他们会对插件进行更高的评分,从而增加其可见性。使用示例,以确保在包含你的插件时没有问题。
要发布你自己的插件,请使用以下命令:
flutter packages pub publish --dry-run
如果执行过程中没有错误,你可以执行以下命令来发布插件,你的插件将在几分钟内上线:
flutter packages pub publish
摘要
在本章的开头,我们学习了如何在 Flutter 代码中包含包,接着是如何包含特定平台的通道以支持 Flutter 代码。我们还使用了 BatteryManager API 来了解安卓手机的电池状态。在最后一节中,我们介绍了一些构建你自己的插件时应该考虑的最佳技巧,随后是发布你自己的插件的方法。
在下一章中,我们将探讨如何使用 Firebase 与 Flutter 结合。
第七章:Firebase - Flutter 的最佳伙伴
使用Firebase构建应用是世界上增长最快的科技趋势之一。使用 Firebase,开发者可以快速构建应用,无需管理基础设施,包括身份验证、存储和同步数据、安全托管 Web 资产和云存储。Firebase 有一个免费的基础计划,提供 1 GB 的存储空间和 100 个并发连接。如果您想升级,您可以在此查看计划:firebase.google.com/pricing/。
在本章中,我们将涵盖以下主题:
-
与 Firebase 连接
-
创建 Cloud Firestore 数据库
-
Firebase 云消息传递
-
Firebase 远程配置
与 Firebase 连接
首先,让我们看看如何连接到 Firebase。我们首先需要确保与 Firebase 的连接是否正确建立;为此,请按照以下步骤操作:
-
在您的 IDE 或编辑器中创建一个新的 Flutter 项目
-
打开
pubspec.yaml文件 -
添加以下依赖项:
dependencies:
flutter:
sdk: flutter
cloud_firestore: ⁰.9.5+2 //Add this line
有关 Flutter Cloud Firestore 插件的最新版本详情,请访问 Pub 网站:pub.dartlang.org/packages/cloud_firestore。
- 接下来,为了建立连接,在您的 IDE 或使用命令行运行以下命令:
flutter packages get
创建 Firebase 项目
一旦建立连接,下一步就是创建一个 Firebase 项目。那么,让我们开始吧。按照以下步骤创建您的项目:
-
打开网站
firebase.google.com并登录或注册。您可以使用您的 Google 凭证在此处登录。 -
接下来,点击添加项目。
-
一旦点击此选项,您将看到以下屏幕:

-
添加项目名称(例如:在我们的案例中为
Firebase Flutter Demo App)。项目 ID会自动生成,或者您可以输入您自己的唯一项目 ID。它们是全球唯一的标识符。 -
在位置中选择国家,然后点击创建项目。之前接受条款和条件。
-
点击创建项目选项,等待几秒钟,直到 Firebase 控制台显示以下截图中的消息:

-
如果您的屏幕显示您的项目已准备好文本,如前一张截图所示,您可以点击继续按钮。
-
完成后,您将看到应用的以下项目设置仪表板:

- 根据您将为哪个应用平台构建应用选择平台特定的 Firebase 配置,并点击相应的图标。在我们的案例中,因为我们正在构建 Android 应用,所以我们将点击 Android 图标继续。
使用包名注册应用
此步骤需要将您应用的平台特定 ID 注册到 Firebase。这将生成我们将添加到项目文件夹中的配置文件。请注意,在您的 Flutter 应用顶级目录中,iOS 和 Android 是包含相应平台特定配置文件的子目录之一:

在您的 Flutter 应用顶级目录中,您可以看到子目录;称为 Android 和 iOS。在这里,您将找到 iOS 和 Android 的平台特定配置文件。
在这里最重要的字段是 Android 包名。这通常是您 app-level build.gradle文件中的applicationId。另一种找到包名的方法是按照以下步骤操作:
-
在 Flutter 应用目录中,检查
android/app/src/main/AndroidManifest.xml文件。 -
在 Manifest 标签下,找到包名的字符串值,这将是包名的值。
-
在 Firebase 对话框中,将步骤 2 中复制的包名粘贴到 Android 包名字段中。
如果您正在为 iOS 和 Android 两个平台开发 Flutter 应用,您需要在同一个 Firebase 项目中注册 iOS 和 Android 版本。但如果您只为一个平台开发,您只需点击其中一个即可。
接下来,您可以添加应用昵称,这是一个可选字段。还有一个可选字段调试签名证书 SHA-1,如果应用使用如 Google Sign in 进行身份验证、Dynamic Links 和 Invites 等功能,则必须使用。在这种情况下,您必须找到调试证书指纹值,并将其复制粘贴到字段中。有关如何构建客户端身份验证的说明,请参阅此链接,developers.google.com/android/guides/client-auth。由于在此示例中我们没有使用这些功能,我们将留空。点击注册应用。
下载和设置配置文件
下一步是下载并设置配置文件。按照以下步骤下载并设置配置文件:
-
点击注册应用后,此步骤中的控制台将生成
google-services.json文件。将此文件下载到您的计算机上。 -
文件下载完成后,前往您的 Flutter 应用目录,并将之前下载的
google-services.json文件移动到android/app目录中。 -
文件移动后,在 Firebase 控制台中,点击下一步,如图所示:

添加 Firebase SDK
现在我们已经下载并设置了配置文件,倒数第二步是将 Firebase SDK 添加到你的项目中。Gradle 的 Google 服务插件确保你下载的 JSON 文件被读取。为了在应用中启用 Google API 或 Firebase 服务,你必须添加一个 google-services 依赖项。需要对 build.gradle 文件进行两个小的修改以使用该插件。请看以下内容:
- 项目级别的
build.gradle(<project>/build.gradle):
buildscript {
dependencies
{
// Add this line
classpath 'com.google.gms:google-services:4.2.0'
}
}
- 应用级别的
build.gradle(<project>/<app-module>/build.gradle):
dependencies {
// Add this line
*implementation 'com.google.firebase:firebase-core:16.0.1'* }
...
// Add to the bottom of the file
apply plugin: 'com.google.gms.google-services'
- 点击立即同步选项以完成此过程。
验证配置
完成前面的步骤后,我们必须验证你的 Flutter 应用是否连接到 Firebase。为了确保这一点,请按照以下步骤操作:
-
构建项目并在连接到你的计算机的设备上运行它。
-
一旦应用在手机上运行,Firebase 控制台将自动检测配置并显示如下成功消息:

- 点击继续到控制台后,你将被带到显示项目名称和其他设置的控制台:

在下一节中,我们将看到如何连接到云数据库。
创建 Cloud Firestore 数据库
一旦 Firebase-Flutter 设置完成,你就可以开始构建应用了。我们现在将设置 Cloud Firestore 数据库并初始化一些值。按照以下步骤操作:
-
在开发选项下,点击数据库。
-
在显示的面板中,点击创建数据库:

- 点击后,你会看到一个弹出面板:Cloud Firestore 的安全规则。选择以测试模式启动并启用:

-
我们选择测试模式,因为我们希望任何拥有数据库引用的人都能读取或写入数据库。当你构建应用的正式版本时,请确保你设置了安全规则。你可以在这里了解这些规则:
firebase.google.com/docs/reference/rules/rules。点击启用后,Cloud Firestore 将配置安全规则并准备好使用。 -
在以下面板中,点击添加集合:

- 我们假设在 Firestore 中我们只有一个集合,称为 Votes。集合是一组文档,构成了数据:

-
点击下一步。
-
一个集合必须至少包含一个文档,这是 Cloud Firestore 的存储单元。你可以使用自动生成的 ID 或自定义 ID。在我们的例子中,我们使用 partyvotes。
-
对于现有的字段,输入名称的值(在我们的例子中,它是 VoteCount),选择数据类型,然后输入 partyvotes 的值。由于它是一个整数,我们选择数字并将其初始值设置为 0:

-
点击 保存。
-
在向你的集合添加了几个文档之后,你的数据库应该看起来像这样:

Firestore 是一个 NoSQL 数据库,这意味着我们不会与行和列一起工作。现在我们将构建应用的布局。使用 Firestore 的详细信息,我们将构建列表布局,这将根据 Firestore 中的值在运行时生成列表项,并在点击列表项时读取/更新 Firestore 数据库中的新值。以下是 main.dart 文件:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// Creating Object to temporary make the list items. We will replace it when we connect it to Firestore
final party = [
{"partyname": "BJP", "rating": 1},
{"partyname": "Congress", "rating": 3},
{"partyname": "AAP", "rating": 5},
{"partyname": "Janata Dal Party", "rating": 9},
{"partyname": "NOTA", "rating": 11},
];
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'State Party Elections - Worker Profile',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Party Votes')),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
// We will add the code here in the next section
return _buildList(context, party);
}
Widget _buildList(BuildContext context, List<Map> snapshot) {
return ListView(
padding: const EdgeInsets.only(top: 22.0),
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, Map data) {
final result = Record.fromMap(data);
// Adding the padding to ensure enough space is given
return Padding(
key: ValueKey(result.name),
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 7.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.red),
borderRadius: BorderRadius.circular(6.0),
),
// Showing the list item, with name towards the left and the votes to the right
child: ListTile(
title: Text(record.partyname),
trailing: Text(record.partyvotes.toString()),
onTap: () => print(record),
),
),
);
}
}
class Record {
final String partyname;
final int partyvotes;
final DocumentReference reference;
Record.fromMap(Map<String, dynamic> map, {this.reference})
: assert(map['partyname'] != null),
assert(map['partyvotes'] != null),
name = map['partyname'],
votes = map['partyvotes'];
Record.fromSnapshot(DocumentSnapshot snapshot)
: this.fromMap(snapshot.data, reference: snapshot.reference);
@override
String toString() => "Record<$partyname:$partyvotes>";
}
我们在 Firestore 云上准备好了集合。在前面的例子中,我们使用了 party 对象。现在是时候使用 Firestore 云数据从我们的集合中显示了。我们可以通过调用 Cloud Firestore 使用一个 Firestore.instance 引用来做到这一点。例如,如果你希望从你的 Firestore 云数据库中调用一个特定的集合,你可以使用以下命令来返回一个快照流:
Firestore.instance.collection('collection_name').snapshots()
流有两种类型:单订阅或广播。流负责提供异步数据序列。用户生成的事件和从文件中读取的数据是两种数据序列。现在,使用 StreamBuilder 小部件,我们将数据流注入我们创建的用户界面。StreamBuilder 的一个经典用例是,每当 Firestore 值发生变化时,列表会自动更新。
在前面的代码中查找 _buildBody 方法,并将内容替换为以下代码:
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('party').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
添加前面的片段会产生一些错误。_buildListItem 方法仍然认为它正在获取一个映射。因此,我们需要做一些修改。
首先,将方法修改为接受 DocumentSnapshot 而不是映射列表:
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot)
{ ....
}
其次,使用构造函数 Record.fromSnapshot() 来构建记录。该方法更新的代码如下:
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final result = Record.fromSnapshot(data);
接下来,使用 onTap: () 方法确保每次点击列表项时,投票都会更新到 Firestore 数据库中。每次点击列表项时,Cloud Firestore 都会通知所有监听器更新后的快照。应用通过 StreamBuilder 激活参与,该小部件用于更新新数据。对于单个用户来说,这是可以的,但是当有多个用户时,可能会发生 竞态条件。
main.dart 的完整代码如下:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'State Party Elections - Worker Profile',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Party Votes')),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('party').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return ListView(
padding: const EdgeInsets.only(top: 22.0),
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final result = Record.fromSnapshot(data);
return Padding(
key: ValueKey(result.name),
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical:
7.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.red),
borderRadius: BorderRadius.circular(6.0),
),
child: ListTile(
title: Text(record.name),
trailing: Text(record.votes.toString()),
onTap: () => Firestore.instance.runTransaction((transaction)
async {
final freshFBsnapshot = await
transaction.get(record.reference);
final updated = Record.fromSnapshot(freshFBsnapshot);
await transaction
.update(record.reference, {'partyvotes':
updated.votes + 1});
}),
),
),
);
}
}
class Record {
final String partyname;
final int partyvotes;
final DocumentReference reference;
Record.fromMap(Map<String, dynamic> map, {this.reference})
: assert(map['partyname'] != null),
assert(map['partyvotes'] != null),
name = map['partyname'],
votes = map['partyvotes'];
Record.fromSnapshot(DocumentSnapshot snapshot)
: this.fromMap(snapshot.data, reference: snapshot.reference);
@override
String toString() => "Record<$partyname:$partyvotes>";
}
一旦运行代码,尝试点击列表项,你将看到更新后的值映射在 Firestore 云数据库上。你也可以尝试在 Firestore 云数据库中更新列表项名称(在我们的例子中,是派对名称),你将看到列表项选项正在更新。
Firebase Cloud Messaging
Firebase Cloud Messaging (FCM) 是一种通过应用通知来提高应用内参与度的有效方式。使用 FCM,你可以向客户端设备发送两种类型的消息:
-
由 FCM SDK 直接处理的推送通知消息
-
数据消息
这两条消息的最大有效载荷为 4KB。当从 Firebase 控制台发送消息时,字符限制为 1,024 个字符。Firebase 拥有云消息传递以及应用内消息传递,但在这个部分,我们将仅讨论 Firebase 云消息传递。
在 Firebase 控制台中,点击左侧面板中的增长 | 云消息传递。随后点击发送第一条消息,如图所示:

要在您的设备上测试消息,需要 FCM 令牌。使用以下 Android 代码生成这些令牌:
FirebaseInstanceId.getInstance().getInstanceId()
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>
() {
@Override
public void onComplete(@NonNull Task<InstanceIdResult>
actionable) {
if (!actionable.isSuccessful()) {
Log.w(TAG, "getInstanceId failed",
actionable.getException());
return;
}
// Get new Instance ID token
String tokenID = actionable.getResult().getToken();
// Log and toast
String message = getString(R.string.msg_token_fmt,
tokenID);
Log.d(TAG, message);
Toast.makeText(MainActivity.this, message,
Toast.LENGTH_SHORT).show();
}
});
FCM 还允许针对特定目标配置消息,例如GeoLocations、应用的版本、语言和用户受众。当您希望向特定用户组发送通知时,这种情况非常理想。
Firebase 远程配置
使用 Firebase 的远程配置 API,您可以在不实际下载应用更新的情况下更改应用。一个例子是在生产环境中推送新的应用更新时,当用户启动应用时,您可以向用户显示有关更新的弹出消息。
要设置 Firebase 远程配置,请转到 Firebase 控制台中的“增长”标签页,并点击“远程配置”,如图所示:

添加参数键和默认值。有一个可选字段用于添加描述。点击“添加参数”继续。请勿在远程配置中存储任何机密信息。Firebase 还允许为参数设置条件。例如,如果您想向印度的用户显示特定的欢迎消息,而向美国的用户显示不同的消息,远程配置将非常有用。
免责声明:一些代码文件是在Apache 2.0 许可证下授权的,并且可在firebase.google.com/docs/cloud-messaging/android/client上找到。
摘要
我们在本章开始时探讨了如何使用 Firestore 云数据库——NoSQL 方式帮助应用开发者实时构建应用,从而加快应用构建速度。我们还查看了一个使用 Firestore 云数据库捕获ListView的示例。本节之后,我们探讨了云消息传递的工作原理。在最后一节中,我们讨论了在您的应用中使用 Firebase 远程配置的一些用例。
在下一章中,我们将探讨如何部署您的 Flutter 应用程序。
第八章:部署 Flutter 应用
部署 Flutter 应用是使开发者能够为应用商店发布准备应用程序的最简单过程之一。到目前为止,您必须已经了解到 Google 的移动应用 SDK 提供了在 iOS 和 Android 上创建高质量原生界面的几个功能,且速度非常快。
在本章中,我们将探讨以下主题:
-
在 Android 上部署
-
在 iOS 上部署
在 Android 上部署
在本书到目前为止的内容中,我们构建的是调试类型。理想情况下,这用于在生成要上传的应用程序的发布版本之前测试应用程序。Flutter 还允许创建应用程序的变体。如果您想构建您开发的应用程序的生产版本,请遵循下面的步骤。
检查 AndroidManifest.xml 文件
此文件包含了一些在构建应用程序的生产版本时非常有用的主要全局设置。它位于 <app dir>/android/app/src/main。当您点击 AndroidManifest.xml 文件时,您将在 Application 标签中找到以下片段:
…..
android:name="io.flutter.app.FlutterApplication"
android:label="flutter_app_battery"
android:icon="@mipmap/ic_launcher">
….
以下代码中可见的属性的说明如下:
-
android.name: 这个属性设置了应用程序的包名 -
android.label: 这个属性设置了应用程序的最终名称 -
android:icon: 这个属性设置了应用程序的启动图标
….
<uses-permission android:name="android.permission.INTERNET"/>
…..
<uses-permission> 标签允许开发者设置应用程序中开发者所需的权限。例如,如果您想使用互联网,必须使用前面的属性,或者如果您想访问相机,必须使用 <uses-permission android:name="android.permission.CAMERA"/>。这将影响所有相机功能。开发者还可以要求在 运行时 模式下请求用户权限。
build.gradle 配置
下一步是检查位于 <app dir>/android/app 的 Gradle 构建文件,并确认以下参数中输入的值是否正确:
- 在以下片段中设置
VersionCode和VersionName。请注意,VersionCode的值对于每个上传的构建必须是唯一的,并且它是一个绝对值。Google Play 商店允许的versionCode的最大值是2100000000。另一方面,VersionName是一个字符串值。VersionName没有在 Play 商店上显示值的用途。字符串值可以作为<major>.<minor>.<point>字符串级联——例如,1.2.2:
…….
…….
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null)
{ flutterVersionCode = '1' }
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null)
{ flutterVersionName = '1.0' }
……
……
-
applicationId: 这允许开发者指定最终的、唯一的应用程序 ID。 -
minSdkVersion和targetSdkVersion: 这两个值指定了应用程序设计运行的最低 API 级别和目标 API 级别:
defaultConfig
{ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "deviceinformation.flutterappbattery"
minSdkVersion 16
targetSdkVersion 27
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
应用程序内的图标
一个时尚、引人注目的图标可以成为某人启动应用的绝佳触发器。默认情况下,启动器图标是一个默认图标。遵循 Android Launcher Icon 指南,你可以构建自己的图标,该图标可用于从移动屏幕启动应用:
-
一旦你的图标文件准备就绪,检查
<app dir>/android/app/src/main/res/目录并将文件放入相应的文件夹中,使用配置限定符。你可以在此处了解更多信息:developer.android.com/guide/topics/resources/providing-resources#AlternativeResources。 -
将文件放置在文件夹中后,只需转到
AndroidManifest.xml并更新应用程序标签的android:icon属性。 -
为了确保图标已替换,使用
Flutter run并检查启动器中的应用图标。
签名应用
这是发布应用到 Google Play 商店之前的关键步骤之一。要发布应用,使用数字签名对应用进行签名是关键部分。按照以下步骤对应用进行签名:
- 创建密钥库:如果你已经有了
keystore,则跳到步骤 2。如果你希望构建新的keystore,请使用KeyTool工具使用以下命令行代码生成一个:
keytool -genkey -v -keystore ~/appkey.jks -keyalg RSA -keysize 2048 -validity 10000 -alias appkey
KeyTool 是 Java JDK 的一部分,它是 Android Studio 安装的一部分。确保在运行命令行之前提供一个绝对路径。此外,请注意生成的文件必须保持私密。
-
从应用中引用密钥库:接下来,创建一个名为
<app dir>/android/key.properties包含对 keystore 的引用。请保持此文件私密。查看以下代码:
storePassword=<password used in the previous step>
keyPassword=<password used in the previous step>
keyAlias=appkey
storeFile=<location of the key store file, e.g. /Users/<user name>/appkey.jks>
- 在 Gradle 中配置签名:转到
<app dir>/android/app/build.gradle文件并将**android {**替换为以下代码:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
- 接下来,删除以下代码:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --
//release` works.
signingConfig signingConfigs.debug
}
}
用以下代码替换它:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
完成这些步骤后,你的应用的发布构建将自动签名。
使用 ProGuard
默认情况下,Flutter 构建生成器不会对 Android Host 进行混淆或精简。你可能想减小 APK 的大小或防止代码被逆向工程。ProGuard 是保护你的代码的一种方式:
- 配置 ProGuard:创建一个名为
**/**android/app/proguard-rules.pro的新文件并添加以下规则:
#Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
使用前面的代码,你可以仅保护 Flutter 中的引擎库。对于保护其他部分,根据你的开发需求添加代码。
- 启用混淆和/或精简:打开
/android/app/build.gradle文件并定位到buildTypes定义:
…..
…..
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
…..
…..
在内部,将配置集的 minifyEnabled 和 useProguard 标志设置为 true。注意也将 ProGuard 指向你在步骤 1 中创建的文件。刷新后的代码将如下所示:
…
…
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
useProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
…
…
构建发布 APK
在成功完成前面的步骤后,生成发布版本只是一个两步过程。使用命令行,执行以下操作:
-
cd <app dir>(注意将<app dir>替换为您的应用程序目录路径)。 -
运行 flutter build apk。这将创建一个发布 APK,位于
<app dir>/build/app/outputs/apk/release/app-release.apk。
此构建可以在 Google Play 商店发布。确保在应用上传之前阅读发布指南。
在 iOS 上部署
就像 Google Play 商店一样,Apple 也有自己的应用发布指南。在构建应用之前,请务必阅读所有相关信息。以下是您可以查看以获取更多关于 Apple 应用发布的详细信息链接:developer.apple.com/app-store/review/。一旦应用提交,就像 Google 一样,Apple 将检查应用是否符合其发布指南。请注意,Flutter 支持 iOS 8.0 及更高版本。在设置 Xcode 构建生成时,这一点很重要。
就像 Google 一样,我们使用Google Play 开发者控制台。在 Apple 的情况下,我们将使用App Store Connect,之前被称为 iTunes。此控制台用于管理您应用的生命周期。此控制台将帮助您设置应用名称、描述和与应用一起发布的应用截图,并管理定价和发布。
注册 Bundle ID
在 Apple 商店发布的每个应用都有一个唯一的 Bundle ID,与 Apple 相关联。要为您的应用注册新的 Bundle ID,请按照以下步骤操作:
-
打开您的 Apple 开发者账户的 App IDs 页面。
-
点击+图标创建新的 Bundle ID。
-
输入应用名称并选择显式 App ID,然后输入一个 ID。
-
选择您的应用将要使用的服务,然后点击继续。
-
下一步确认详细信息。现在,点击注册以注册您的 Bundle ID。
在 App Store connect 上生成应用程序记录
要在 App Store connect 上注册应用,请按照以下步骤操作:
-
在浏览器中打开 Apple App Store connect 并点击我的应用。
-
点击 My Apps 页面左上角的+图标 | 新建应用。
-
在弹出窗口中,填写您的应用详细信息。在“平台”部分,确保选中 iOS。此时,值得一提的是,Flutter 目前尚不支持 tvOS。因此,不要选中该复选框。应用名称不能超过 30 个字符。在 SKU 部分,添加一个在 App Store 中不可见的唯一 ID:

-
选择创建。
-
导航到使用之前步骤创建的应用程序详细信息,并在侧边栏中选择应用信息。
-
在“通用信息”部分选择 Bundle ID。
验证 Xcode 设置
在 Xcode 中验证构建发布设置相对简单,与 Android Studio 相比。首先,导航到 Xcode 中目标设置,并执行以下操作:
-
在 Xcode 中打开您的应用 ios 文件夹中的
Runner.xcworkspace。 -
从 Xcode 项目导航器中选择 Runner 项目,这会显示应用的设置。从主视图的侧边栏中选择 Runner 目标。
-
选择“通用”选项卡。
显示的信息需要你注意交叉检查重要的设置;因此,在“身份”部分,查看以下详细信息:
-
显示名称:这是将在 App Store 以及任何其他使用名称的地方显示的应用名称
-
包标识符:这是你在 App Store Connect 上注册的应用 ID,如前所述
在“签名应用”部分,请查看以下详细信息:
-
自动管理签名:定义 Xcode 是否应该自动管理应用签名和配置。默认设置为
True。 -
团队:选择与你的注册 Apple 开发者账户关联的团队。如果你希望添加更多成员,请点击添加账户,然后更新设置。
最后,在部署部分,检查部署目标:这包含你的应用将支持的最低 iOS 版本值。
选择应用图标
就像在 Android Studio 中一样,即使在 iOS 的情况下,也会创建一个占位符图标。如果你希望使用自己的图标,请在进行以下步骤之前阅读 iOS 应用图标指南:
-
在
Runner文件夹中选择Assets.xcassets;这将在 Xcode 项目导航器中显示 -
如果你的图标已经准备好,请更新占位符图标为你的自定义应用图标
-
要检查图标是否已更新,请使用 Flutter Run 运行你的应用
创建构建存档
这是创建构建存档并将其上传到 Apple Store 的最后一步。在命令行中,在你的应用目录中按照以下步骤操作:
-
运行 flutter build iOS 以创建发布版本。
-
仅当你的 Xcode 版本低于 8.3 时才执行此操作。为了确保 Xcode 刷新发布模式配置,请重新启动你的 Xcode 工作区。
在 Xcode 中,请按照以下步骤配置应用版本和构建:
-
在 Xcode 中,在你的应用的
ios文件夹中打开Runner.xcworkspace。 -
选择产品 | 方案 | Runner。
-
选择产品 | 目标 | 通用 iOS 设备。
-
在 Xcode 项目导航器中选择 Runner,然后在设置视图侧边栏中选择 Runner 目标。
-
在“身份”部分,更新版本号,并将构建标识符更新为唯一的构建编号。这用于跟踪上传的构建数量。每个构建都应该有一个唯一的构建编号。
最后一步是创建构建存档并将其上传到 App Store Connect:
-
选择产品,然后生成构建存档
-
在 Xcode 组织者窗口的侧边栏中 | 选择 iOS 应用 | 选择你刚刚生成的构建存档
-
点击验证按钮
-
存档验证后,你可以点击“上传到 App Store”选项
如果有任何错误,请重新生成构建并尝试再次重复此过程。
摘要
一旦你完成了你的出色应用,部署和发布是关键环节。我们已经介绍了如何在 Play Store 上发布安卓和 iOS 应用。重要的是要知道,应用上传只是列出应用。你还应该将 App Store 视为提高应用可见性的关键技术。


浙公网安备 33010602011771号