- Flutter 教程
- Flutter - 首页
- Flutter - 简介
- Flutter - 安装
- 在 Android Studio 中创建简单应用程序
- Flutter - 架构应用程序
- Dart 编程入门
- Flutter - Widget 入门
- Flutter - 布局入门
- Flutter - 手势入门
- Flutter - 状态管理
- Flutter - 动画
- Flutter - 编写 Android 特定代码
- Flutter - 编写 iOS 特定代码
- Flutter - 包入门
- Flutter - 访问 REST API
- Flutter - 数据库概念
- Flutter - 国际化
- Flutter - 测试
- Flutter - 部署
- Flutter - 开发工具
- Flutter - 编写高级应用程序
- Flutter - 总结
- Flutter 有用资源
- Flutter - 快速指南
- Flutter - 有用资源
- Flutter - 讨论
Flutter - 快速指南
Flutter - 简介
通常,开发移动应用程序是一项复杂且具有挑战性的任务。有很多框架可用于开发移动应用程序。Android 提供了一个基于 Java 语言的原生框架,而 iOS 提供了一个基于 Objective-C/Swift 语言的原生框架。
但是,要开发支持这两个操作系统的应用程序,我们需要使用两种不同的框架用两种不同的语言进行编码。为了帮助克服这种复杂性,存在支持这两个操作系统的移动框架。这些框架从简单的基于 HTML 的混合移动应用程序框架(使用 HTML 作为用户界面和 JavaScript 作为应用程序逻辑)到复杂的特定语言框架(承担将代码转换为原生代码的繁重工作)不等。无论其简单性或复杂性如何,这些框架总是有很多缺点,其中一个主要缺点是其性能缓慢。
在这种情况下,Flutter——一个基于 Dart 语言的简单且高性能的框架,通过直接在操作系统的画布上渲染 UI 而不是通过原生框架来提供高性能。
Flutter 还提供了许多现成的 Widget(UI)来创建现代应用程序。这些 Widget 已针对移动环境进行了优化,使用 Widget 设计应用程序就像设计 HTML 一样简单。
具体来说,Flutter 应用程序本身就是一个 Widget。Flutter Widget 也支持动画和手势。应用程序逻辑基于响应式编程。Widget 可以选择具有状态。通过更改 Widget 的状态,Flutter 将自动(响应式编程)比较 Widget 的状态(旧的和新的),并仅使用必要的更改渲染 Widget,而不是重新渲染整个 Widget。
我们将在接下来的章节中讨论完整的架构。
Flutter 的特性
Flutter 框架为开发人员提供了以下特性:
现代且响应式的框架。
使用 Dart 编程语言,非常容易学习。
快速开发。
美观流畅的用户界面。
庞大的 Widget 目录。
在多个平台上运行相同的 UI。
高性能应用程序。
Flutter 的优势
Flutter 配备了美观且可自定义的 Widget,可实现高性能和出色的移动应用程序。它满足所有自定义需求和要求。除此之外,Flutter 还提供了许多其他优势,如下所述:
Dart 拥有大量的软件包存储库,可让您扩展应用程序的功能。
开发人员只需要为两个应用程序(Android 和 iOS 平台)编写一个代码库。Flutter 未来也可能扩展到其他平台。
Flutter 需要较少的测试。由于其单一代码库,如果我们为这两个平台编写一次自动化测试就足够了。
Flutter 的简单性使其成为快速开发的理想选择。其自定义能力和可扩展性使其更加强大。
使用 Flutter,开发人员可以完全控制 Widget 及其布局。
Flutter 提供了很棒的开发者工具,以及令人惊叹的热重载功能。
Flutter 的缺点
尽管 Flutter 拥有众多优点,但它也存在以下缺点:
由于它是用 Dart 语言编写的,因此开发人员需要学习新的语言(尽管它很容易学习)。
现代框架尽可能地将逻辑和 UI 分开,但在 Flutter 中,用户界面和逻辑是混合在一起的。我们可以通过智能编码和使用高级模块来分离用户界面和逻辑来克服这一点。
Flutter 只是另一个用于创建移动应用程序的框架。在用户数量庞大的细分市场中,开发人员难以选择合适的开发工具。
Flutter - 安装
本章将详细指导您在本地计算机上安装 Flutter。
在 Windows 上安装
在本节中,让我们看看如何在 Windows 系统中安装Flutter SDK及其要求。
步骤 1 - 访问 URL,https://flutterdart.cn/docs/get-started/install/windows 并下载最新的 Flutter SDK。截至 2019 年 4 月,版本为 1.2.1,文件为 flutter_windows_v1.2.1-stable.zip。
步骤 2 - 将 zip 压缩文件解压缩到一个文件夹中,例如 C:\flutter\
步骤 3 - 更新系统路径以包含 flutter bin 目录。
步骤 4 - Flutter 提供了一个工具 flutter doctor,用于检查是否满足 Flutter 开发的所有要求。
flutter doctor
步骤 5 - 运行上述命令将分析系统并显示其报告,如下所示:
Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel stable, v1.2.1, on Microsoft Windows [Version 10.0.17134.706], locale en-US) [√] Android toolchain - develop for Android devices (Android SDK version 28.0.3) [√] Android Studio (version 3.2) [√] VS Code, 64-bit edition (version 1.29.1) [!] Connected device ! No devices available ! Doctor found issues in 1 category.
该报告表明所有开发工具都可用,但设备未连接。我们可以通过 USB 连接 Android 设备或启动 Android 模拟器来解决此问题。
步骤 6 - 安装最新的 Android SDK(如果 flutter doctor 报告需要)。
步骤 7 - 安装最新的 Android Studio(如果 flutter doctor 报告需要)。
步骤 8 - 启动 Android 模拟器或将真实的 Android 设备连接到系统。
步骤 9 - 为 Android Studio 安装 Flutter 和 Dart 插件。它提供了启动模板以创建新的 Flutter 应用程序,以及在 Android Studio 本身中运行和调试 Flutter 应用程序的选项等。
打开 Android Studio。
单击文件 → 设置 → 插件。
选择 Flutter 插件并单击安装。
当提示安装 Dart 插件时,单击是。
重新启动 Android Studio。
在 macOS 上安装
要在 macOS 上安装 Flutter,您需要执行以下步骤:
步骤 1 - 访问 URL,https://flutterdart.cn/docs/get-started/install/macos 并下载最新的 Flutter SDK。截至 2019 年 4 月,版本为 1.2.1,文件为 flutter_macos_v1.2.1-stable.zip。
步骤 2 - 将 zip 压缩文件解压缩到一个文件夹中,例如 /path/to/flutter
步骤 3 - 更新系统路径以包含 flutter bin 目录(在 ~/.bashrc 文件中)。
> export PATH = "$PATH:/path/to/flutter/bin"
步骤 4 - 使用以下命令在当前会话中启用更新的路径,然后对其进行验证。
source ~/.bashrc source $HOME/.bash_profile echo $PATH
Flutter 提供了一个工具 flutter doctor,用于检查是否满足 Flutter 开发的所有要求。它类似于 Windows 版本。
步骤 5 - 安装最新的 XCode(如果 flutter doctor 报告需要)。
步骤 6 - 安装最新的 Android SDK(如果 flutter doctor 报告需要)。
步骤 7 - 安装最新的 Android Studio(如果 flutter doctor 报告需要)。
步骤 8 - 启动 Android 模拟器或将真实的 Android 设备连接到系统以开发 Android 应用程序。
步骤 9 - 打开 iOS 模拟器或将真实的 iPhone 设备连接到系统以开发 iOS 应用程序。
步骤 10 - 为 Android Studio 安装 Flutter 和 Dart 插件。它提供了启动模板以创建新的 Flutter 应用程序,以及在 Android Studio 本身中运行和调试 Flutter 应用程序的选项等。
打开 Android Studio
单击首选项 → 插件
选择 Flutter 插件并单击安装
当提示安装 Dart 插件时,单击是。
重新启动 Android Studio。
在 Android Studio 中创建简单应用程序
在本章中,让我们创建一个简单的Flutter应用程序,以了解在 Android Studio 中创建 Flutter 应用程序的基本知识。
步骤 1 - 打开 Android Studio
步骤 2 - 创建 Flutter 项目。为此,请单击文件 → 新建 → 新建 Flutter 项目
步骤 3 - 选择 Flutter 应用程序。为此,请选择Flutter 应用程序并单击下一步。
步骤 4 - 按如下所示配置应用程序,然后单击下一步。
项目名称:hello_app
Flutter SDK 路径:<path_to_flutter_sdk>
项目位置:<path_to_project_folder>
描述:基于 Flutter 的 Hello World 应用程序
步骤 5 - 配置项目。
将公司域名设置为flutterapp.tutorialspoint.com,然后单击完成。
步骤 6 - 输入公司域名。
Android Studio 创建了一个功能最少的完全可用的 Flutter 应用程序。让我们检查一下应用程序的结构,然后更改代码以执行我们的任务。
应用程序的结构及其用途如下:
此处解释了应用程序结构的各个组件:
android - 自动生成的源代码以创建 Android 应用程序
ios - 自动生成的源代码以创建 iOS 应用程序
lib - 包含使用 Flutter 框架编写的 Dart 代码的主要文件夹
lib/main.dart - Flutter 应用程序的入口点
test - 包含用于测试 Flutter 应用程序的 Dart 代码的文件夹
test/widget_test.dart - 示例代码
.gitignore - Git 版本控制文件
.metadata - 由 Flutter 工具自动生成
.packages - 自动生成以跟踪 Flutter 包
.iml - Android Studio 使用的项目文件
pubspec.yaml - 由Pub(Flutter 包管理器)使用
pubspec.lock - 由 Flutter 包管理器Pub自动生成
README.md - 用 Markdown 格式编写的项目描述文件
步骤 7 - 将lib/main.dart 文件中的 Dart 代码替换为以下代码:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Hello World Demo Application', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Home page'), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.title), ), body: Center( child: Text( 'Hello World', ) ), ); } }
让我们逐行了解 Dart 代码。
第 1 行 - 导入 Flutter 包material。material 是一个 Flutter 包,用于根据 Android 指定的材料设计指南创建用户界面。
第 3 行 - 这是 Flutter 应用程序的入口点。调用runApp函数并向其传递MyApp类的对象。runApp函数的目的是将给定的 Widget 附加到屏幕上。
第 5-17 行 - Widget 用于在 Flutter 框架中创建 UI。StatelessWidget是一个 Widget,它不维护 Widget 的任何状态。MyApp扩展了StatelessWidget并覆盖了其build 方法。build方法的目的是创建应用程序 UI 的一部分。这里,build方法使用MaterialApp,一个 Widget 来创建应用程序的根级 UI。它具有三个属性 - title、theme和home。
title是应用程序的标题
theme是 Widget 的主题。在这里,我们使用ThemeData类及其属性primarySwatch将蓝色设置为应用程序的整体颜色。
home 是应用程序的内部 UI,我们设置了另一个 Widget,MyHomePage
第 19 - 38 行 − MyHomePage 与 MyApp 相同,除了它返回 Scaffold 组件。Scaffold 是一个顶级组件,与 MaterialApp 组件并列使用,用于创建符合 Material Design 的 UI。它有两个重要的属性,appBar 用于显示应用程序的标题,body 用于显示应用程序的实际内容。AppBar 是另一个用于渲染应用程序标题的组件,我们已在 appBar 属性中使用它。在 body 属性中,我们使用了 Center 组件,它将它的子组件居中。Text 是最终的也是最内部的组件,用于显示文本,它显示在屏幕的中央。
步骤 8 − 现在,使用 运行 → 运行 main.dart 运行应用程序。
步骤 9 − 最后,应用程序的输出如下所示 −
Flutter - 架构应用程序
在本章中,让我们讨论 Flutter 框架的架构。
组件
Flutter 框架的核心概念是 在 Flutter 中,一切皆为组件。组件基本上是用于创建应用程序用户界面的用户界面组件。
在 Flutter 中,应用程序本身就是一个组件。应用程序是顶级组件,其 UI 使用一个或多个子组件(组件)构建,这些子组件又使用其子组件构建。这种 组合性 特性帮助我们创建任何复杂度的用户界面。
例如,Hello World 应用程序(在上一章中创建)的组件层次结构如下所示 −
这里以下几点值得注意 −
MyApp 是用户创建的组件,它使用 Flutter 原生组件 MaterialApp 构建。
MaterialApp 有一个 home 属性来指定主页的用户界面,它又是一个用户创建的组件 MyHomePage。
MyHomePage 使用另一个 Flutter 原生组件 Scaffold 构建
Scaffold 具有两个属性 – body 和 appBar
body 用于指定其主要用户界面,appBar 用于指定其标题用户界面
标题 UI 使用 Flutter 原生组件 AppBar 构建,主体 UI 使用 Center 组件构建。
Center 组件有一个属性 Child,它引用实际内容,并且它使用 Text 组件构建
手势
Flutter 组件通过一个特殊的组件 GestureDetector 支持交互。GestureDetector 是一个不可见的组件,能够捕获用户交互,例如其子组件的点击、拖动等。Flutter 的许多原生组件都通过使用 GestureDetector 支持交互。我们还可以通过将 GestureDetector 组件与现有组件组合来将交互功能整合到现有组件中。我们将在后续章节中单独学习手势。
状态的概念
Flutter 组件通过提供一个特殊的组件 StatefulWidget 来支持 状态维护。组件需要从 StatefulWidget 组件派生才能支持状态维护,所有其他组件都应该从 StatefulWidget 派生。Flutter 组件在原生中是 反应式 的。这类似于 reactjs,并且 StatefulWidget 只要其内部状态发生变化就会自动重新渲染。重新渲染通过查找旧组件 UI 和新组件 UI 之间的差异并仅渲染必要的更改来进行优化
层
Flutter 框架最重要的概念是,该框架根据复杂性被分为多个类别,并以复杂度递减的顺序清晰地排列在多层中。一层使用其紧邻下一层构建。最顶层是特定于 Android 和 iOS 的组件。下一层包含所有 Flutter 原生组件。下一层是 渲染层,它是一个低级渲染器组件,渲染 Flutter 应用中的所有内容。层级一直延伸到核心平台特定代码
Flutter 中层的概览如下所示 −
以下几点总结了 Flutter 的架构 −
在 Flutter 中,一切皆为组件,一个复杂的组件是由已存在的组件组合而成的。
必要时可以使用 GestureDetector 组件整合交互功能。
必要时可以使用 StatefulWidget 组件维护组件的状态。
Flutter 提供分层设计,以便可以根据任务的复杂性对任何层进行编程。
我们将在后续章节中详细讨论所有这些概念。
Flutter - Dart 编程入门
Dart 是一种开源通用编程语言。它最初由 Google 开发。Dart 是一种面向对象的语言,具有 C 风格的语法。它支持诸如接口、类等编程概念,与其他编程语言不同,Dart 不支持数组。Dart 集合可用于复制数据结构,如数组、泛型和可选类型。
以下代码显示了一个简单的 Dart 程序 −
void main() { print("Dart language is easy to learn"); }
变量和数据类型
变量 是命名的存储位置,数据类型 只是指与变量和函数关联的数据的类型和大小。
Dart 使用 var 关键字声明变量。var 的语法定义如下:
var name = 'Dart';
final 和 const 关键字用于声明常量。它们定义如下 −
void main() { final a = 12; const pi = 3.14; print(a); print(pi); }
Dart 语言支持以下数据类型 −
数字 − 用于表示数字字面量 - 整数和双精度浮点数。
字符串 − 表示一系列字符。字符串值用单引号或双引号指定。
布尔值 − Dart 使用 bool 关键字表示布尔值 - true 和 false。
列表和映射 − 用于表示对象的集合。一个简单的列表可以定义如下 −。
void main() { var list = [1,2,3,4,5]; print(list); }
上面显示的列表生成 [1,2,3,4,5] 列表。
映射可以定义如下 −
void main() { var mapping = {'id': 1,'name':'Dart'}; print(mapping); }
动态 − 如果未定义变量类型,则其默认类型为动态。以下示例说明了动态类型变量 −
void main() { dynamic name = "Dart"; print(name); }
决策和循环
决策块在执行指令之前评估条件。Dart 支持 If、If..else 和 switch 语句。
循环用于重复执行代码块,直到满足特定条件。Dart 支持 for、for..in、while 和 do..while 循环。
让我们了解一个关于控制语句和循环用法的简单示例 −
void main() { for( var i = 1 ; i <= 10; i++ ) { if(i%2==0) { print(i); } } }
以上代码打印从 1 到 10 的偶数。
函数
函数是一组语句,它们一起执行特定的任务。让我们看看这里显示的 Dart 中的一个简单函数 −
void main() { add(3,4); } void add(int a,int b) { int c; c = a+b; print(c); }
以上函数将两个值相加,并生成 7 作为输出。
面向对象编程
Dart 是一种面向对象的语言。它支持面向对象的编程特性,如类、接口等。
类是创建对象的蓝图。类定义包括以下内容 −
- 字段
- Getter 和 Setter
- 构造函数
- 函数
现在,让我们使用以上定义创建一个简单的类 −
class Employee { String name; //getter method String get emp_name { return name; } //setter method void set emp_name(String name) { this.name = name; } //function definition void result() { print(name); } } void main() { //object creation Employee emp = new Employee(); emp.name = "employee1"; emp.result(); //function call }
Flutter - Widget 入门
正如我们在上一章中学到的,组件在 Flutter 框架中无处不在。我们已经在前面的章节中学习了如何在前面的章节中创建新的组件。
在本章中,让我们了解创建组件背后的实际概念以及 Flutter 框架中提供的不同类型的组件。
让我们检查 Hello World 应用程序的 MyHomePage 组件。为此目的的代码如下所示 −
class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(this.title), ), body: Center(child: Text( 'Hello World',)), ); } }
在这里,我们通过扩展 StatelessWidget 创建了一个新的组件。
请注意,StatelessWidget 只需要在派生类中实现一个单一方法 build。build 方法获取构建组件所需的上下文环境,通过 BuildContext 参数,并返回它构建的组件。
在代码中,我们使用了 title 作为构造函数参数之一,还使用了 Key 作为另一个参数。title 用于显示标题,Key 用于在构建环境中识别组件。
在这里,build 方法调用 Scaffold 的 build 方法,后者又调用 AppBar 和 Center 的 build 方法来构建其用户界面。
最后,Center 的 build 方法调用 Text 的 build 方法。
为了更好地理解,下面给出了相同的可视化表示 −
组件构建可视化
在 Flutter 中,组件可以根据其功能分为多个类别,如下所示 −
- 平台特定组件
- 布局组件
- 状态维护组件
- 平台无关/基本组件
现在让我们详细讨论每个组件。
平台特定组件
Flutter 具有特定于特定平台(Android 或 iOS)的组件。
Android 特定组件是根据 Android 操作系统的 Material Design 指南 设计的。Android 特定组件称为 Material 组件。
iOS 特定组件是根据 Apple 的 Human Interface Guidelines 设计的,它们被称为 Cupertino 组件。
一些最常用的 Material 组件如下 −
- Scaffold
- AppBar
- BottomNavigationBar
- TabBar
- TabBarView
- ListTile
- RaisedButton
- FloatingActionButton
- FlatButton
- IconButton
- DropdownButton
- PopupMenuButton
- ButtonBar
- TextField
- Checkbox
- Radio
- Switch
- Slider
- 日期和时间选择器
- SimpleDialog
- AlertDialog
一些最常用的 Cupertino 组件如下 −
- CupertinoButton
- CupertinoPicker
- CupertinoDatePicker
- CupertinoTimerPicker
- CupertinoNavigationBar
- CupertinoTabBar
- CupertinoTabScaffold
- CupertinoTabView
- CupertinoTextField
- CupertinoDialog
- CupertinoDialogAction
- CupertinoFullscreenDialogTransition
- CupertinoPageScaffold
- CupertinoPageTransition
- CupertinoActionSheet
- CupertinoActivityIndicator
- CupertinoAlertDialog
- CupertinoPopupSurface
- CupertinoSlider
布局组件
在 Flutter 中,可以通过组合一个或多个组件来创建组件。为了将多个组件组合成一个组件,Flutter 提供了大量具有布局功能的组件。例如,可以使用 Center 组件将子组件居中。
一些流行的布局组件如下 −
容器(Container) - 一个使用BoxDecoration部件装饰的矩形框,包含背景、边框和阴影。
居中(Center) - 将其子部件居中。
行(Row) - 将其子部件水平排列。
列(Column) - 将其子部件垂直排列。
堆叠(Stack) - 将一个部件叠加在另一个部件之上。
我们将在后续的布局部件简介章节中详细介绍布局部件。
状态维护组件
在Flutter中,所有部件都派生自StatelessWidget或StatefulWidget。
派生自StatelessWidget的部件不包含任何状态信息,但它可能包含派生自StatefulWidget的部件。应用程序的动态特性来自于部件的交互行为以及交互过程中状态的变化。例如,点击一个计数器按钮将使计数器的内部状态增加/减少1,并且Flutter部件的响应特性将使用新的状态信息自动重新渲染部件。
我们将在后续的状态管理章节中详细学习StatefulWidget部件的概念。
平台无关/基本组件
Flutter提供了大量的基本部件,可以以平台无关的方式创建简单和复杂的用户界面。让我们在本节中了解一些基本部件。
文本(Text)
Text部件用于显示一段字符串。可以使用style属性和TextStyle类设置字符串的样式。此目的的示例代码如下:
Text('Hello World!', style: TextStyle(fontWeight: FontWeight.bold))
Text部件有一个特殊的构造函数Text.rich,它接受类型为TextSpan的子部件来指定具有不同样式的字符串。TextSpan部件本质上是递归的,它接受TextSpan作为其子部件。此目的的示例代码如下:
Text.rich( TextSpan( children: <TextSpan>[ TextSpan(text: "Hello ", style: TextStyle(fontStyle: FontStyle.italic)), TextSpan(text: "World", style: TextStyle(fontWeight: FontWeight.bold)), ], ), )
Text部件最重要的属性如下:
maxLines,int - 显示的最大行数
overflow,TextOverFlow - 使用TextOverFlow类指定如何处理视觉溢出
style,TextStyle - 使用TextStyle类指定字符串的样式
textAlign,TextAlign - 使用TextAlign类指定文本的对齐方式,例如右对齐、左对齐、两端对齐等。
textDirection,TextDirection - 文本流的方向,从左到右或从右到左。
图像(Image)
Image部件用于在应用程序中显示图像。Image部件提供了不同的构造函数来从多个来源加载图像,如下所示:
Image - 使用ImageProvider的通用图像加载器
Image.asset - 从Flutter项目的资源加载图像
Image.file - 从系统文件夹加载图像
Image.memory - 从内存加载图像
Image.Network - 从网络加载图像
在Flutter中加载和显示图像最简单的选项是将图像作为应用程序的资源,并在需要时将其加载到部件中。
在项目文件夹中创建一个名为assets的文件夹,并将所需的图像放置其中。
在pubspec.yaml中指定资源,如下所示:
flutter: assets: - assets/smiley.png
现在,在应用程序中加载并显示图像。
Image.asset('assets/smiley.png')
下面显示了Hello World应用程序的MyHomePage部件的完整源代码和结果。
class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.title), ), body: Center( child: Image.asset("assets/smiley.png")), ); } }
加载的图像如下所示:
Image部件最重要的属性如下:
image,ImageProvider - 要加载的实际图像
width,double - 图像的宽度
height,double - 图像的高度
alignment,AlignmentGeometry - 如何在其边界内对齐图像
图标(Icon)
Icon部件用于显示IconData类中描述的字体中的字形。加载简单电子邮件图标的代码如下:
Icon(Icons.email)
在Hello World应用程序中应用它的完整源代码如下:
class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(this.title),), body: Center( child: Icon(Icons.email)), ); } }
加载的图标如下所示:
Flutter - 布局入门
由于Flutter的核心概念是一切皆为部件,因此Flutter将用户界面布局功能整合到部件本身。Flutter提供了很多专门设计的部件,例如Container、Center、Align等,仅仅用于布局用户界面。通过组合其他部件构建的部件通常使用布局部件。让我们在本节中学习Flutter布局的概念。
布局部件的类型
根据其子部件,布局部件可以分为两类:
- 支持单个子部件的部件
- 支持多个子部件的部件
让我们在接下来的部分中学习这两种部件及其功能。
单子部件
在此类别中,部件只有一个部件作为其子部件,并且每个部件都具有特殊的布局功能。
例如,Center部件只是将其子部件相对于其父部件居中,而Container部件提供了完全的灵活性,可以使用不同的选项(如填充、装饰等)将其子部件放置在其内部的任何给定位置。
单子部件是创建具有单个功能的高质量部件(如按钮、标签等)的绝佳选择。
使用Container部件创建简单按钮的代码如下:
class MyButton extends StatelessWidget { MyButton({Key key}) : super(key: key); @override Widget build(BuildContext context) { return Container( decoration: const BoxDecoration( border: Border( top: BorderSide(width: 1.0, color: Color(0xFFFFFFFFFF)), left: BorderSide(width: 1.0, color: Color(0xFFFFFFFFFF)), right: BorderSide(width: 1.0, color: Color(0xFFFF000000)), bottom: BorderSide(width: 1.0, color: Color(0xFFFF000000)), ), ), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0), decoration: const BoxDecoration( border: Border( top: BorderSide(width: 1.0, color: Color(0xFFFFDFDFDF)), left: BorderSide(width: 1.0, color: Color(0xFFFFDFDFDF)), right: BorderSide(width: 1.0, color: Color(0xFFFF7F7F7F)), bottom: BorderSide(width: 1.0, color: Color(0xFFFF7F7F7F)), ), color: Colors.grey, ), child: const Text( 'OK',textAlign: TextAlign.center, style: TextStyle(color: Colors.black) ), ), ); } }
这里,我们使用了两个部件 - 一个Container部件和一个Text部件。部件的结果是一个自定义按钮,如下所示:
让我们检查一下Flutter提供的一些最重要的单子布局部件:
Padding - 用于通过给定的填充来排列其子部件。这里,填充可以通过EdgeInsets类提供。
Align - 使用alignment属性的值在其内部对齐其子部件。alignment属性的值可以通过FractionalOffset类提供。FractionalOffset类以距左上角的距离来指定偏移量。
一些可能的偏移量值如下:
FractionalOffset(1.0, 0.0) 表示右上角。
FractionalOffset(0.0, 1.0) 表示左下角。
关于偏移量的示例代码如下:
Center( child: Container( height: 100.0, width: 100.0, color: Colors.yellow, child: Align( alignment: FractionalOffset(0.2, 0.6), child: Container( height: 40.0, width: 40.0, color: Colors.red, ), ), ), )
FittedBox - 它缩放子部件,然后根据指定的适配方式对其进行定位。
AspectRatio - 它尝试将子部件的大小调整为指定的纵横比。
ConstrainedBox
Baseline
FractionalSizedBox
IntrinsicHeight
IntrinsicWidth
LimitedBox
OffStage
OverflowBox
SizedBox
SizedOverflowBox
Transform
CustomSingleChildLayout
我们的Hello World应用程序使用基于Material的布局部件来设计主页。让我们修改我们的Hello World应用程序,使用如下指定的基本布局部件来构建主页:
Container - 通用、单子、基于框的容器部件,具有对齐、填充、边框和边距以及丰富的样式功能。
Center - 简单、单子容器部件,将子部件居中。
下面是MyHomePage和MyApp部件的修改后的代码:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MyHomePage(title: "Hello World demo app"); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration(color: Colors.white,), padding: EdgeInsets.all(25), child: Center( child:Text( 'Hello World', style: TextStyle( color: Colors.black, letterSpacing: 0.5, fontSize: 20, ), textDirection: TextDirection.ltr, ), ) ); } }
这里,
Container部件是顶级或根部件。Container使用decoration和padding属性配置其内容布局。
BoxDecoration具有许多属性,如颜色、边框等,用于装饰Container部件,这里使用color设置容器的颜色。
Container部件的padding通过使用EdgeInsets类设置,该类提供了指定填充值的选项。
Center是Container部件的子部件。同样,Text是Center部件的子部件。Text用于显示消息,Center用于相对于父部件Container居中显示文本消息。
上面给出的代码的最终结果是一个布局示例,如下所示:
多子部件
在此类别中,给定部件将有多个子部件,并且每个部件的布局都是唯一的。
例如,Row部件允许将其子部件水平排列,而Column部件允许将其子部件垂直排列。通过组合Row和Column,可以构建任何复杂程度的部件。
让我们在本节中学习一些常用的部件。
Row - 允许将其子部件水平排列。
Column - 允许将其子部件垂直排列。
ListView - 允许将其子部件作为列表排列。
GridView - 允许将其子部件作为画廊排列。
Expanded - 用于使Row和Column部件的子部件占用最大可能的区域。
Table - 基于表格的部件。
Flow - 基于流的部件。
Stack - 基于堆叠的部件。
高级布局应用程序
在本节中,让我们学习如何使用单子布局部件和多子布局部件创建具有自定义设计的复杂产品列表用户界面。
为此,请按照以下顺序操作:
在Android Studio中创建一个新的Flutter应用程序product_layout_app。
将main.dart代码替换为以下代码:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue,), home: MyHomePage(title: 'Product layout demo home page'), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(this.title),), body: Center(child: Text( 'Hello World', )), ); } }
这里,
我们通过扩展StatelessWidget而不是默认的StatefulWidget创建了MyHomePage部件,然后删除了相关的代码。
现在,根据指定的如下所示的设计创建一个新的部件ProductBox:
ProductBox的代码如下所示。
class ProductBox extends StatelessWidget { ProductBox({Key key, this.name, this.description, this.price, this.image}) : super(key: key); final String name; final String description; final int price; final String image; Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(2), height: 120, child: Card( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Image.asset("assets/appimages/" +image), Expanded( child: Container( padding: EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text(this.name, style: TextStyle(fontWeight: FontWeight.bold)), Text(this.description), Text("Price: " + this.price.toString()), ], ) ) ) ] ) ) ); } }
请注意代码中的以下内容:
ProductBox使用了四个参数,如下所示:
name - 产品名称
description - 产品描述
price - 产品价格
image - 产品图片
ProductBox使用了七个内置部件,如下所示:
- Container
- Expanded
- Row
- Column
- Card
- 文本(Text)
- 图像(Image)
ProductBox使用上述部件进行设计。部件的排列或层次结构在下面所示的图中指定:
现在,将一些虚拟图片(见下文)放置到应用程序的assets文件夹中,并在pubspec.yaml文件中配置assets文件夹,如下所示:
assets: - assets/appimages/floppy.png - assets/appimages/iphone.png - assets/appimages/laptop.png - assets/appimages/pendrive.png - assets/appimages/pixel.png - assets/appimages/tablet.png
iPhone.png
Pixel.png
Laptop.png
Tablet.png
Pendrive.png
Floppy.png
最后,在MyHomePage部件中使用ProductBox部件,如下所示:
class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title:Text("Product Listing")), body: ListView( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0), children: <Widget> [ ProductBox( name: "iPhone", description: "iPhone is the stylist phone ever", price: 1000, image: "iphone.png" ), ProductBox( name: "Pixel", description: "Pixel is the most featureful phone ever", price: 800, image: "pixel.png" ), ProductBox( name: "Laptop", description: "Laptop is most productive development tool", price: 2000, image: "laptop.png" ), ProductBox( name: "Tablet", description: "Tablet is the most useful device ever for meeting", price: 1500, image: "tablet.png" ), ProductBox( name: "Pendrive", description: "Pendrive is useful storage medium", price: 100, image: "pendrive.png" ), ProductBox( name: "Floppy Drive", description: "Floppy drive is useful rescue storage medium", price: 20, image: "floppy.png" ), ], ) ); } }
这里,我们使用ProductBox作为ListView部件的子部件。
产品布局应用程序(product_layout_app)的完整代码(main.dart)如下:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Product layout demo home page'), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Product Listing")), body: ListView( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0), children: <Widget>[ ProductBox( name: "iPhone", description: "iPhone is the stylist phone ever", price: 1000, image: "iphone.png" ), ProductBox( name: "Pixel", description: "Pixel is the most featureful phone ever", price: 800, image: "pixel.png" ), ProductBox( name: "Laptop", description: "Laptop is most productive development tool", price: 2000, image: "laptop.png" ), ProductBox( name: "Tablet", description: "Tablet is the most useful device ever for meeting", price: 1500, image: "tablet.png" ), ProductBox( name: "Pendrive", description: "Pendrive is useful storage medium", price: 100, image: "pendrive.png" ), ProductBox( name: "Floppy Drive", description: "Floppy drive is useful rescue storage medium", price: 20, image: "floppy.png" ), ], ) ); } } class ProductBox extends StatelessWidget { ProductBox({Key key, this.name, this.description, this.price, this.image}) : super(key: key); final String name; final String description; final int price; final String image; Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(2), height: 120, child: Card( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Image.asset("assets/appimages/" + image), Expanded( child: Container( padding: EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text( this.name, style: TextStyle( fontWeight: FontWeight.bold ) ), Text(this.description), Text( "Price: " + this.price.toString() ), ], ) ) ) ] ) ) ); } }
应用程序的最终输出如下所示:
Flutter - 手势入门
手势主要是用户与移动(或任何基于触摸的设备)应用程序交互的方式。手势通常定义为用户为了激活移动设备的特定控件而进行的任何物理动作/移动。手势可以像轻触移动设备屏幕一样简单,也可以像在游戏应用程序中使用的更复杂的动作。
这里提到了一些广泛使用的手势:
轻触(Tap) - 用指尖短暂触摸设备表面,然后松开指尖。
双击(Double Tap) - 在短时间内连续点击两次。
拖动(Drag) - 用指尖触摸设备表面,然后以稳定的方式移动指尖,最后松开指尖。
轻扫(Flick) - 类似于拖动,但以更快的速度进行。
捏合(Pinch) - 使用两只手指捏合设备表面。
散开/缩放(Spread/Zoom) - 捏合的反向操作。
平移 - 用指尖触碰设备表面,并在任何方向移动,而不松开指尖。
Flutter 通过其独有的 Widget,GestureDetector,为所有类型的手势提供了极好的支持。GestureDetector 是一个非视觉 Widget,主要用于检测用户的手势。要识别针对某个 Widget 的手势,可以将该 Widget 放置在 GestureDetector Widget 内部。GestureDetector 将捕获手势并根据手势分派多个事件。
下面列出了一些手势及其对应的事件:
- 点击
- onTapDown
- onTapUp
- onTap
- onTapCancel
- 双击
- onDoubleTap
- 长按
- onLongPress
- 垂直拖动
- onVerticalDragStart
- onVerticalDragUpdate
- onVerticalDragEnd
- 水平拖动
- onHorizontalDragStart
- onHorizontalDragUpdate
- onHorizontalDragEnd
- 平移
- onPanStart
- onPanUpdate
- onPanEnd
现在,让我们修改 hello world 应用以包含手势检测功能,并尝试理解这个概念。
将MyHomePage Widget 的主体内容更改为如下所示:
body: Center( child: GestureDetector( onTap: () { _showDialog(context); }, child: Text( 'Hello World', ) ) ),
请注意,在这里,我们在 Widget 层次结构中将GestureDetector Widget 放置在 Text Widget 上方,捕获了 onTap 事件,然后最终显示了一个对话框窗口。
实现 *_showDialog* 函数,以便在用户点击hello world 消息时显示对话框。它使用通用的showDialog 和AlertDialog Widget 创建一个新的对话框 Widget。代码如下所示:
// user defined function void _showDialog(BuildContext context) { // flutter defined function showDialog( context: context, builder: (BuildContext context) { // return object of type Dialog return AlertDialog( title: new Text("Message"), content: new Text("Hello World"), actions: <Widget>[ new FlatButton( child: new Text("Close"), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); }
应用程序将使用热重载功能在设备中重新加载。现在,只需点击消息“Hello World”,它将显示如下所示的对话框:
现在,通过点击对话框中的关闭选项关闭对话框。
完整代码(main.dart)如下:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Hello World Demo Application', theme: ThemeData( primarySwatch: Colors.blue,), home: MyHomePage(title: 'Home page'), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; // user defined function void _showDialog(BuildContext context) { // flutter defined function showDialog( context: context, builder: (BuildContext context) { // return object of type Dialog return AlertDialog( title: new Text("Message"), content: new Text("Hello World"), actions: <Widget>[ new FlatButton( child: new Text("Close"), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(this.title),), body: Center( child: GestureDetector( onTap: () { _showDialog(context); }, child: Text( 'Hello World', ) ) ), ); } }
最后,Flutter 还通过Listener Widget 提供了一种低级的手势检测机制。它将检测所有用户交互,然后分派以下事件:
- PointerDownEvent
- PointerMoveEvent
- PointerUpEvent
- PointerCancelEvent
Flutter 还提供了一小组 Widget 来执行特定以及高级手势。这些 Widget 列出如下:
Dismissible - 支持轻扫手势来关闭 Widget。
Draggable - 支持拖动手势来移动 Widget。
LongPressDraggable - 支持拖动手势来移动 Widget,当其父 Widget 也可拖动时。
DragTarget - 接受任何Draggable Widget
IgnorePointer - 从手势检测过程中隐藏 Widget 及其子元素。
AbsorbPointer - 停止手势检测过程本身,因此任何重叠的 Widget 也无法参与手势检测过程,因此不会引发任何事件。
Scrollable - 支持滚动 Widget 内可用的内容。
Flutter - 状态管理
在应用程序中管理状态是应用程序生命周期中最重要和必要的流程之一。
让我们考虑一个简单的购物车应用程序。
用户将使用其凭据登录应用程序。
用户登录后,应用程序应在所有屏幕中保留已登录的用户详细信息。
同样,当用户选择产品并将其保存到购物车中时,购物车信息应在页面之间保留,直到用户结账。
用户及其购物车信息在任何实例中都称为该实例下应用程序的状态。
状态管理可以根据特定状态在应用程序中持续的时间分为两类。
短暂的 - 持续几秒钟,例如动画的当前状态或单个页面,例如产品的当前评分。Flutter 通过 StatefulWidget 支持它。
应用程序状态 - 持续整个应用程序,例如已登录的用户详细信息、购物车信息等。Flutter 通过 scoped_model 支持它。
导航和路由
在任何应用程序中,从一个页面/屏幕导航到另一个页面/屏幕定义了应用程序的工作流程。处理应用程序导航的方式称为路由。Flutter 提供了一个基本的路由类 - MaterialPageRoute 和两个方法 - Navigator.push 和 Navigator.pop,来定义应用程序的工作流程。
MaterialPageRoute
MaterialPageRoute 是一个 Widget,用于通过用特定于平台的动画替换整个屏幕来呈现其 UI。
MaterialPageRoute(builder: (context) => Widget())
在这里,builder 将接受一个函数来构建其内容,方法是提供应用程序的当前上下文。
Navigation.push
Navigation.push 用于使用 MaterialPageRoute Widget 导航到新屏幕。
Navigator.push( context, MaterialPageRoute(builder: (context) => Widget()), );
Navigation.pop
Navigation.pop 用于导航到上一个屏幕。
Navigator.pop(context);
让我们创建一个新的应用程序来更好地理解导航概念。
在 Android Studio 中创建一个新的 Flutter 应用程序,product_nav_app
将 assets 文件夹从 product_nav_app 复制到 product_state_app,并在 pubspec.yaml 文件中添加 assets。
flutter: assets: - assets/appimages/floppy.png - assets/appimages/iphone.png - assets/appimages/laptop.png - assets/appimages/pendrive.png - assets/appimages/pixel.png - assets/appimages/tablet.png
将默认启动代码(main.dart)替换为我们的启动代码。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage( title: 'Product state demo home page' ), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.title), ), body: Center( child: Text('Hello World',) ), ); } }
让我们创建一个 Product 类来组织产品信息。
class Product { final String name; final String description; final int price; final String image; Product(this.name, this.description, this.price, this.image); }
让我们在 Product 类中编写一个 getProducts 方法来生成我们的虚拟产品记录。
static List<Product> getProducts() { List<Product> items = <Product>[]; items.add( Product( "Pixel", "Pixel is the most feature-full phone ever", 800, "pixel.png" ) ); items.add( Product( "Laptop", "Laptop is most productive development tool", 2000, " laptop.png" ) ); items.add( Product( "Tablet", "Tablet is the most useful device ever for meeting", 1500, "tablet.png" ) ); items.add( Product( "Pendrive", "Pendrive is useful storage medium", 100, "pendrive.png" ) ); items.add( Product( "Floppy Drive", "Floppy drive is useful rescue storage medium", 20, "floppy.png" ) ); return items; } import product.dart in main.dart import 'Product.dart';
让我们包含我们的新 Widget,RatingBox。
class RatingBox extends StatefulWidget { @override _RatingBoxState createState() =>_RatingBoxState(); } class _RatingBoxState extends State<RatingBox> { int _rating = 0; void _setRatingAsOne() { setState(() { _rating = 1; }); } void _setRatingAsTwo() { setState(() { _rating = 2; }); } void _setRatingAsThree() { setState(() { _rating = 3; }); } Widget build(BuildContext context) { double _size = 20; print(_rating); return Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: <Widget>[ Container( padding: EdgeInsets.all(0), child: IconButton( icon: ( _rating >= 1? Icon( Icons.star, size: _size, ) : Icon( Icons.star_border, size: _size, ) ), color: Colors.red[500], onPressed: _setRatingAsOne, iconSize: _size, ), ), Container( padding: EdgeInsets.all(0), child: IconButton( icon: ( _rating >= 2? Icon( Icons.star, size: _size, ) : Icon( Icons.star_border, size: _size, ) ), color: Colors.red[500], onPressed: _setRatingAsTwo, iconSize: _size, ), ), Container( padding: EdgeInsets.all(0), child: IconButton( icon: ( _rating >= 3 ? Icon( Icons.star, size: _size, ) : Icon( Icons.star_border, size: _size, ) ), color: Colors.red[500], onPressed: _setRatingAsThree, iconSize: _size, ), ), ], ); } }
让我们修改我们的 ProductBox Widget 以与我们的新 Product 类一起使用。
class ProductBox extends StatelessWidget { ProductBox({Key key, this.item}) : super(key: key); final Product item; Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(2), height: 140, child: Card( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Image.asset("assets/appimages/" + this.item.image), Expanded( child: Container( padding: EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text(this.item.name, style: TextStyle(fontWeight: FontWeight.bold)), Text(this.item.description), Text("Price: " + this.item.price.toString()), RatingBox(), ], ) ) ) ] ), ) ); } }
让我们重写我们的 MyHomePage Widget 以与 Product 模型一起使用,并使用 ListView 列出所有产品。
class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; final items = Product.getProducts(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Product Navigation")), body: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return GestureDetector( child: ProductBox(item: items[index]), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductPage(item: items[index]), ), ); }, ); }, )); } }
在这里,我们使用了 MaterialPageRoute 导航到产品详细信息页面。
现在,让我们添加 ProductPage 来显示产品详细信息。
class ProductPage extends StatelessWidget { ProductPage({Key key, this.item}) : super(key: key); final Product item; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.item.name), ), body: Center( child: Container( padding: EdgeInsets.all(0), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Image.asset("assets/appimages/" + this.item.image), Expanded( child: Container( padding: EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text( this.item.name, style: TextStyle( fontWeight: FontWeight.bold ) ), Text(this.item.description), Text("Price: " + this.item.price.toString()), RatingBox(), ], ) ) ) ] ), ), ), ); } }
应用程序的完整代码如下:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class Product { final String name; final String description; final int price; final String image; Product(this.name, this.description, this.price, this.image); static List<Product> getProducts() { List<Product> items = <Product>[]; items.add( Product( "Pixel", "Pixel is the most featureful phone ever", 800, "pixel.png" ) ); items.add( Product( "Laptop", "Laptop is most productive development tool", 2000, "laptop.png" ) ); items.add( Product( "Tablet", "Tablet is the most useful device ever for meeting", 1500, "tablet.png" ) ); items.add( Product( "Pendrive", "iPhone is the stylist phone ever", 100, "pendrive.png" ) ); items.add( Product( "Floppy Drive", "iPhone is the stylist phone ever", 20, "floppy.png" ) ); items.add( Product( "iPhone", "iPhone is the stylist phone ever", 1000, "iphone.png" ) ); return items; } } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Product Navigation demo home page'), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; final items = Product.getProducts(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Product Navigation")), body: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return GestureDetector( child: ProductBox(item: items[index]), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProductPage(item: items[index]), ), ); }, ); }, ) ); } } class ProductPage extends StatelessWidget { ProductPage({Key key, this.item}) : super(key: key); final Product item; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.item.name), ), body: Center( child: Container( padding: EdgeInsets.all(0), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Image.asset("assets/appimages/" + this.item.image), Expanded( child: Container( padding: EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text(this.item.name, style: TextStyle(fontWeight: FontWeight.bold)), Text(this.item.description), Text("Price: " + this.item.price.toString()), RatingBox(), ], ) ) ) ] ), ), ), ); } } class RatingBox extends StatefulWidget { @override _RatingBoxState createState() => _RatingBoxState(); } class _RatingBoxState extends State<RatingBox> { int _rating = 0; void _setRatingAsOne() { setState(() { _rating = 1; }); } void _setRatingAsTwo() { setState(() { _rating = 2; }); } void _setRatingAsThree() { setState(() { _rating = 3; }); } Widget build(BuildContext context) { double _size = 20; print(_rating); return Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.max, children: <Widget>[ Container( padding: EdgeInsets.all(0), child: IconButton( icon: ( _rating >= 1 ? Icon( Icons.star, size: _size, ) : Icon( Icons.star_border, size: _size, ) ), color: Colors.red[500], onPressed: _setRatingAsOne, iconSize: _size, ), ), Container( padding: EdgeInsets.all(0), child: IconButton( icon: ( _rating >= 2 ? Icon( Icons.star, size: _size, ) : Icon( Icons.star_border, size: _size, ) ), color: Colors.red[500], onPressed: _setRatingAsTwo, iconSize: _size, ), ), Container( padding: EdgeInsets.all(0), child: IconButton( icon: ( _rating >= 3 ? Icon( Icons.star, size: _size, ) : Icon( Icons.star_border, size: _size, ) ), color: Colors.red[500], onPressed: _setRatingAsThree, iconSize: _size, ), ), ], ); } } class ProductBox extends StatelessWidget { ProductBox({Key key, this.item}) : super(key: key); final Product item; Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(2), height: 140, child: Card( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Image.asset("assets/appimages/" + this.item.image), Expanded( child: Container( padding: EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text(this.item.name, style: TextStyle(fontWeight: FontWeight.bold)), Text(this.item.description), Text("Price: " + this.item.price.toString()), RatingBox(), ], ) ) ) ] ), ) ); } }
运行应用程序并点击任意一个产品项目。它将显示相关的详细信息页面。我们可以通过点击后退按钮移动到主页。应用程序的产品列表页面和产品详细信息页面如下所示:
Flutter - 动画
动画是任何移动应用程序中一个复杂的流程。尽管其复杂性,动画将用户体验提升到一个新的水平,并提供丰富的用户交互。由于其丰富性,动画成为现代移动应用程序不可或缺的一部分。Flutter 框架认识到动画的重要性,并提供了一个简单直观的框架来开发所有类型的动画。
简介
动画是在特定持续时间内按特定顺序显示一系列图像/图片的过程,以产生运动的错觉。动画最重要的方面如下:
动画有两个不同的值:起始值和结束值。动画从起始值开始,经过一系列中间值,最后以结束值结束。例如,要使 Widget 淡出,初始值将是完全不透明,最终值将是零不透明。
中间值可以是线性的或非线性的(曲线)的,并且可以进行配置。了解动画按其配置方式工作。每个配置都会为动画提供不同的感觉。例如,使 Widget 淡出将是线性的,而球的弹跳将是非线性的。
动画过程的持续时间会影响动画的速度(缓慢或快速)。
控制动画过程的能力,例如启动动画、停止动画、重复动画特定次数、反转动画过程等。
在 Flutter 中,动画系统不会执行任何真实的动画。相反,它仅提供每帧渲染图像所需的 value。
基于动画的类
Flutter 动画系统基于 Animation 对象。核心动画类及其用法如下:
Animation
在特定持续时间内生成两个数字之间的插值 value。最常见的 Animation 类如下:
Animation<double> - 在两个十进制数字之间插值 value
Animation<Color> - 在两种颜色之间插值颜色
Animation<Size> - 在两种尺寸之间插值尺寸
AnimationController - 用于控制动画本身的特殊 Animation 对象。每当应用程序准备好新帧时,它都会生成新的 value。它支持基于线性的动画,并且 value 从 0.0 开始到 1.0 结束
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
在这里,controller 控制动画,duration 选项控制动画过程的持续时间。vsync 是一个用于优化动画中使用的资源的特殊选项。
CurvedAnimation
类似于 AnimationController,但支持非线性动画。CurvedAnimation 可以与 Animation 对象一起使用,如下所示:
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
Tween<T>
派生自 Animatable<T>,用于生成除 0 和 1 之外的任何两个数字之间的数字。它可以通过使用 animate 方法并将实际的 Animation 对象传递给它来与 Animation 对象一起使用。
AnimationController controller = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this); Animation<int> customTween = IntTween( begin: 0, end: 255).animate(controller);
Tween 也可以与 CurvedAnimation 一起使用,如下所示:
AnimationController controller = AnimationController( duration: const Duration(milliseconds: 500), vsync: this); final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut); Animation<int> customTween = IntTween(begin: 0, end: 255).animate(curve);
在这里,controller 是实际的动画控制器。curve 提供非线性的类型,customTween 提供从 0 到 255 的自定义范围。
Flutter 动画的工作流程
动画的工作流程如下:
在 StatefulWidget 的 initState 中定义并启动动画控制器。
AnimationController(duration: const Duration(seconds: 2), vsync: this); animation = Tween<double>(begin: 0, end: 300).animate(controller); controller.forward();
添加基于动画的监听器,addListener 来更改 Widget 的状态。
animation = Tween<double>(begin: 0, end: 300).animate(controller) ..addListener(() { setState(() { // The state that has changed here is the animation object’s value. }); });
内置 Widget,AnimatedWidget 和 AnimatedBuilder 可以用来跳过此过程。这两个 Widget 都接受 Animation 对象并获取动画所需的当前 value。
在 Widget 的构建过程中获取动画 value,然后将其应用于宽度、高度或任何相关属性,而不是原始 value。
child: Container( height: animation.value, width: animation.value, child: <Widget>, )
工作应用程序
让我们编写一个简单的基于动画的应用程序,以了解 Flutter 框架中动画的概念。
在 Android Studio 中创建一个新的Flutter 应用程序,product_animation_app。
将 assets 文件夹从 product_nav_app 复制到 product_animation_app,并在 pubspec.yaml 文件中添加 assets。
flutter: assets: - assets/appimages/floppy.png - assets/appimages/iphone.png - assets/appimages/laptop.png - assets/appimages/pendrive.png - assets/appimages/pixel.png - assets/appimages/tablet.png
删除默认启动代码(main.dart)。
添加导入和基本 main 函数。
import 'package:flutter/material.dart'; void main() => runApp(MyApp());
创建从 StatefulWidgtet 派生的 MyApp Widget。
class MyApp extends StatefulWidget { _MyAppState createState() => _MyAppState(); }
创建 _MyAppState Widget 并实现 initState 和 dispose,以及默认的 build 方法。
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin { Animation<double> animation; AnimationController controller; @override void initState() { super.initState(); controller = AnimationController( duration: const Duration(seconds: 10), vsync: this ); animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); controller.forward(); } // This widget is the root of your application. @override Widget build(BuildContext context) { controller.forward(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue,), home: MyHomePage(title: 'Product layout demo home page', animation: animation,) ); } @override void dispose() { controller.dispose(); super.dispose(); } }
这里,
在 initState 方法中,我们创建了一个动画控制器对象(controller),一个动画对象(animation)并使用 controller.forward 启动了动画。
在 dispose 方法中,我们处置了动画控制器对象(controller)。
在 build 方法中,通过构造函数将动画发送到 MyHomePage Widget。现在,MyHomePage Widget 可以使用动画对象来为其内容设置动画。
现在,添加 ProductBox Widget
class ProductBox extends StatelessWidget { ProductBox({Key key, this.name, this.description, this.price, this.image}) : super(key: key); final String name; final String description; final int price; final String image; Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(2), height: 140, child: Card( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Image.asset("assets/appimages/" + image), Expanded( child: Container( padding: EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text(this.name, style: TextStyle(fontWeight: FontWeight.bold)), Text(this.description), Text("Price: " + this.price.toString()), ], ) ) ) ] ) ) ); } }
创建一个新的 Widget,MyAnimatedWidget,使用不透明度执行简单的淡入淡出动画。
class MyAnimatedWidget extends StatelessWidget { MyAnimatedWidget({this.child, this.animation}); final Widget child; final Animation<double> animation; Widget build(BuildContext context) => Center( child: AnimatedBuilder( animation: animation, builder: (context, child) => Container( child: Opacity(opacity: animation.value, child: child), ), child: child), ); }
在这里,我们使用了 AniatedBuilder 来执行我们的动画。AnimatedBuilder 是一个 Widget,它在执行动画的同时构建其内容。它接受一个 animation 对象来获取当前动画 value。我们使用了动画 value,animation.value 来设置子 Widget 的不透明度。实际上,Widget 将使用不透明度概念为子 Widget 设置动画。
最后,创建 MyHomePage Widget 并使用动画对象为其任何内容设置动画。
class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title, this.animation}) : super(key: key); final String title; final Animation<double> animation; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Product Listing")),body: ListView( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0), children: <Widget>[ FadeTransition( child: ProductBox( name: "iPhone", description: "iPhone is the stylist phone ever", price: 1000, image: "iphone.png" ), opacity: animation ), MyAnimatedWidget(child: ProductBox( name: "Pixel", description: "Pixel is the most featureful phone ever", price: 800, image: "pixel.png" ), animation: animation), ProductBox( name: "Laptop", description: "Laptop is most productive development tool", price: 2000, image: "laptop.png" ), ProductBox( name: "Tablet", description: "Tablet is the most useful device ever for meeting", price: 1500, image: "tablet.png" ), ProductBox( name: "Pendrive", description: "Pendrive is useful storage medium", price: 100, image: "pendrive.png" ), ProductBox( name: "Floppy Drive", description: "Floppy drive is useful rescue storage medium", price: 20, image: "floppy.png" ), ], ) ); } }
在这里,我们使用了 FadeAnimation 和 MyAnimationWidget 为列表中的前两个项目设置动画。FadeAnimation 是一个内置动画类,我们使用它来使用不透明度概念为其子元素设置动画。
完整代码如下:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatefulWidget { _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin { Animation<double> animation; AnimationController controller; @override void initState() { super.initState(); controller = AnimationController( duration: const Duration(seconds: 10), vsync: this); animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); controller.forward(); } // This widget is the root of your application. @override Widget build(BuildContext context) { controller.forward(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue,), home: MyHomePage(title: 'Product layout demo home page', animation: animation,) ); } @override void dispose() { controller.dispose(); super.dispose(); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title, this.animation}): super(key: key); final String title; final Animation<double> animation; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Product Listing")), body: ListView( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0), children: <Widget>[ FadeTransition( child: ProductBox( name: "iPhone", description: "iPhone is the stylist phone ever", price: 1000, image: "iphone.png" ), opacity: animation ), MyAnimatedWidget( child: ProductBox( name: "Pixel", description: "Pixel is the most featureful phone ever", price: 800, image: "pixel.png" ), animation: animation ), ProductBox( name: "Laptop", description: "Laptop is most productive development tool", price: 2000, image: "laptop.png" ), ProductBox( name: "Tablet", description: "Tablet is the most useful device ever for meeting", price: 1500, image: "tablet.png" ), ProductBox( name: "Pendrive", description: "Pendrive is useful storage medium", price: 100, image: "pendrive.png" ), ProductBox( name: "Floppy Drive", description: "Floppy drive is useful rescue storage medium", price: 20, image: "floppy.png" ), ], ) ); } } class ProductBox extends StatelessWidget { ProductBox({Key key, this.name, this.description, this.price, this.image}) : super(key: key); final String name; final String description; final int price; final String image; Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(2), height: 140, child: Card( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Image.asset("assets/appimages/" + image), Expanded( child: Container( padding: EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text( this.name, style: TextStyle( fontWeight: FontWeight.bold ) ), Text(this.description), Text( "Price: " + this.price.toString() ), ], ) ) ) ] ) ) ); } } class MyAnimatedWidget extends StatelessWidget { MyAnimatedWidget({this.child, this.animation}); final Widget child; final Animation<double> animation; Widget build(BuildContext context) => Center( child: AnimatedBuilder( animation: animation, builder: (context, child) => Container( child: Opacity(opacity: animation.value, child: child), ), child: child ), ); }
编译并运行应用程序以查看结果。应用程序的初始版本和最终版本如下所示:
Flutter - 编写 Android 特定代码
Flutter 提供了一个通用框架来访问平台特定的功能。这使开发人员能够使用平台特定的代码扩展Flutter框架的功能。可以通过该框架轻松访问平台特定的功能,例如相机、电池电量、浏览器等。
访问平台特定代码的总体思路是通过简单的消息传递协议。Flutter 代码(客户端)和平台代码(主机)绑定到一个通用的消息通道。客户端通过消息通道向主机发送消息。主机监听消息通道,接收消息并执行必要的功能,最后通过消息通道将结果返回给客户端。
平台特定代码架构如下图所示:
消息传递协议使用标准消息编解码器(StandardMessageCodec 类),该编解码器支持 JSON 类值的二进制序列化,例如数字、字符串、布尔值等。序列化和反序列化在客户端和主机之间透明地工作。
让我们编写一个简单的应用程序,使用Android SDK打开浏览器,并了解如何
在 Android Studio 中创建一个新的 Flutter 应用程序,flutter_browser_app
将 main.dart 代码替换为以下代码:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); 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'), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.title), ), body: Center( child: RaisedButton( child: Text('Open Browser'), onPressed: null, ), ), ); } }
在这里,我们创建了一个新的按钮来打开浏览器,并将它的 onPressed 方法设置为 null。
现在,导入以下包:
import 'dart:async'; import 'package:flutter/services.dart';
在这里,services.dart 包含调用平台特定代码的功能。
在 MyHomePage 小部件中创建一个新的消息通道。
static const platform = const MethodChannel('flutterapp.tutorialspoint.com/browser');
编写一个方法 _openBrowser 来通过消息通道调用平台特定方法 openBrowser 方法。
Future<void> _openBrowser() async { try { final int result = await platform.invokeMethod( 'openBrowser', <String, String>{ 'url': "https://flutterdart.cn" } ); } on PlatformException catch (e) { // Unable to open the browser print(e); } }
在这里,我们使用 platform.invokeMethod 调用 openBrowser(将在后续步骤中解释)。openBrowser 有一个参数 url,用于打开特定的 URL。
将 RaisedButton 的 onPressed 属性的值从 null 更改为 _openBrowser。
onPressed: _openBrowser,
打开 MainActivity.java(在 android 文件夹内)并导入所需的库:
import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import io.flutter.app.FlutterActivity; 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 io.flutter.plugins.GeneratedPluginRegistrant;
编写一个方法 openBrowser 来打开浏览器
private void openBrowser(MethodCall call, Result result, String url) { Activity activity = this; if (activity == null) { result.error("ACTIVITY_NOT_AVAILABLE", "Browser cannot be opened without foreground activity", null); return; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); activity.startActivity(intent); result.success((Object) true); }
现在,在 MainActivity 类中设置通道名称:
private static final String CHANNEL = "flutterapp.tutorialspoint.com/browser";
编写 Android 特定的代码,在 onCreate 方法中设置消息处理:
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler( new MethodCallHandler() { @Override public void onMethodCall(MethodCall call, Result result) { String url = call.argument("url"); if (call.method.equals("openBrowser")) { openBrowser(call, result, url); } else { result.notImplemented(); } } });
在这里,我们使用 MethodChannel 类创建了一个消息通道,并使用 MethodCallHandler 类来处理消息。onMethodCall 是实际负责通过检查消息来调用正确的平台特定代码的方法。onMethodCall 方法从消息中提取 url,然后仅当方法调用为 openBrowser 时才调用 openBrowser。否则,它返回 notImplemented 方法。
应用程序的完整源代码如下:
main.dart
MainActivity.java
package com.tutorialspoint.flutterapp.flutter_browser_app; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import io.flutter.app.FlutterActivity; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.GeneratedPluginRegistrant; public class MainActivity extends FlutterActivity { private static final String CHANNEL = "flutterapp.tutorialspoint.com/browser"; @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) { String url = call.argument("url"); if (call.method.equals("openBrowser")) { openBrowser(call, result, url); } else { result.notImplemented(); } } } ); } private void openBrowser(MethodCall call, Result result, String url) { Activity activity = this; if (activity == null) { result.error( "ACTIVITY_NOT_AVAILABLE", "Browser cannot be opened without foreground activity", null ); return; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); activity.startActivity(intent); result.success((Object) true); } }
main.dart
import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/services.dart'; void main() => runApp(MyApp()); 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' ), ); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; static const platform = const MethodChannel('flutterapp.tutorialspoint.com/browser'); Future<void> _openBrowser() async { try { final int result = await platform.invokeMethod('openBrowser', <String, String>{ 'url': "https://flutterdart.cn" }); } on PlatformException catch (e) { // Unable to open the browser print(e); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.title), ), body: Center( child: RaisedButton( child: Text('Open Browser'), onPressed: _openBrowser, ), ), ); } }
运行应用程序并点击“打开浏览器”按钮,您会看到浏览器已启动。浏览器应用程序 - 首页如下图所示:
Flutter - 编写 iOS 特定代码
访问 iOS 特定代码与 Android 平台类似,只是它使用 iOS 特定的语言 - Objective-C 或 Swift 以及 iOS SDK。否则,概念与 Android 平台相同。
让我们为 iOS 平台编写与上一章相同的应用程序。
让我们在 Android Studio(macOS)中创建一个新的应用程序,flutter_browser_ios_app
按照上一章中的步骤 2-6 操作。
启动 Xcode 并点击文件 → 打开
选择 Flutter 项目 ios 目录下的 Xcode 项目。
打开Runner → Runner 路径下的 AppDelegate.m。它包含以下代码:
#include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end
我们添加了一个方法 openBrowser,用于使用指定的 url 打开浏览器。它接受单个参数 url。
- (void)openBrowser:(NSString *)urlString { NSURL *url = [NSURL URLWithString:urlString]; UIApplication *application = [UIApplication sharedApplication]; [application openURL:url]; }
在 didFinishLaunchingWithOptions 方法中,找到控制器并将其设置为 controller 变量。
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
在 didFinishLaunchingWithOptions 方法中,将浏览器通道设置为 flutterapp.tutorialspoint.com/browse:
FlutterMethodChannel* browserChannel = [ FlutterMethodChannel methodChannelWithName: @"flutterapp.tutorialspoint.com/browser" binaryMessenger:controller];
创建一个变量 weakSelf 并设置当前类:
__weak typeof(self) weakSelf = self;
现在,实现 setMethodCallHandler。通过匹配 call.method 调用 openBrowser。通过调用 call.arguments 获取 url,并在调用 openBrowser 时传递它。
[browserChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { if ([@"openBrowser" isEqualToString:call.method]) { NSString *url = call.arguments[@"url"]; [weakSelf openBrowser:url]; } else { result(FlutterMethodNotImplemented); } }];
完整代码如下:
#include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // custom code starts FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; FlutterMethodChannel* browserChannel = [ FlutterMethodChannel methodChannelWithName: @"flutterapp.tutorialspoint.com /browser" binaryMessenger:controller]; __weak typeof(self) weakSelf = self; [browserChannel setMethodCallHandler:^( FlutterMethodCall* call, FlutterResult result) { if ([@"openBrowser" isEqualToString:call.method]) { NSString *url = call.arguments[@"url"]; [weakSelf openBrowser:url]; } else { result(FlutterMethodNotImplemented); } }]; // custom code ends [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - (void)openBrowser:(NSString *)urlString { NSURL *url = [NSURL URLWithString:urlString]; UIApplication *application = [UIApplication sharedApplication]; [application openURL:url]; } @end
打开项目设置。
转到功能并启用后台模式。
添加*后台获取和远程通知**。
现在,运行应用程序。它的工作原理与 Android 版本类似,但将打开 Safari 浏览器而不是 Chrome 浏览器。
Flutter - 包入门
Dart 组织和共享一组功能的方式是通过包。Dart 包仅仅是可共享的库或模块。通常,Dart 包与 Dart 应用程序相同,只是 Dart 包没有应用程序入口点 main。
包的通用结构(考虑一个演示包 my_demo_package)如下所示:
lib/src/* - 私有 Dart 代码文件。
lib/my_demo_package.dart - 主 Dart 代码文件。它可以像这样导入到应用程序中:
import 'package:my_demo_package/my_demo_package.dart'
如果需要,其他私有代码文件可以导出到主代码文件(my_demo_package.dart)中,如下所示:
export src/my_private_code.dart
lib/* - 任意数量的 Dart 代码文件,以任何自定义文件夹结构排列。代码可以这样访问:
import 'package:my_demo_package/custom_folder/custom_file.dart'
pubspec.yaml - 项目规范,与应用程序相同。
包中的所有 Dart 代码文件都只是 Dart 类,并且 Dart 代码没有特殊要求才能将其包含在包中。
包的类型
由于 Dart 包基本上是相似功能的小集合,因此可以根据其功能进行分类。
Dart 包
通用 Dart 代码,可在 Web 和移动环境中使用。例如,english_words 就是这样一个包,它包含大约 5000 个单词,并具有诸如名词(列出英语中的名词)、音节(指定单词中的音节数)等基本实用程序函数。
Flutter 包
通用 Dart 代码,依赖于 Flutter 框架,只能在移动环境中使用。例如,fluro 是 Flutter 的自定义路由器。它依赖于 Flutter 框架。
Flutter 插件
通用 Dart 代码,依赖于 Flutter 框架以及底层平台代码(Android SDK 或 iOS SDK)。例如,camera 是一个与设备相机交互的插件。它依赖于 Flutter 框架以及底层框架来访问相机。
使用 Dart 包
Dart 包托管并发布到活动服务器 https://pub.dartlang.org。此外,Flutter 提供了一个简单的工具 pub 来管理应用程序中的 Dart 包。使用包所需的步骤如下:
将包名称和所需的版本包含在 pubspec.yaml 中,如下所示:
dependencies: english_words: ^3.1.5
可以通过检查在线服务器找到最新的版本号。
使用以下命令将包安装到应用程序中:
flutter packages get
在 Android Studio 中开发时,Android Studio 会检测 pubspec.yaml 中的任何更改,并向开发人员显示 Android Studio 包警报,如下所示:
可以使用菜单选项在 Android Studio 中安装或更新 Dart 包。
使用以下命令导入必要的文件并开始工作:
import 'package:english_words/english_words.dart';
使用包中提供的任何方法:
nouns.take(50).forEach(print);
在这里,我们使用 nouns 函数获取并打印前 50 个单词。
开发 Flutter 插件包
开发 Flutter 插件类似于开发 Dart 应用程序或 Dart 包。唯一的例外是插件将使用系统 API(Android 或 iOS)来获取所需的平台特定功能。
正如我们已经在前面的章节中学习了如何访问平台代码,让我们开发一个简单的插件 my_browser 来理解插件开发过程。my_browser 插件的功能是允许应用程序在平台特定的浏览器中打开给定的网站。
启动 Android Studio。
点击文件 → 新建 Flutter 项目并选择 Flutter 插件选项。
您会看到一个 Flutter 插件选择窗口,如下所示:
输入 my_browser 作为项目名称,然后点击下一步。
在窗口中输入插件名称和其他详细信息,如下所示:
在下面显示的窗口中输入公司域名 flutterplugins.tutorialspoint.com,然后点击完成。它将生成一个启动代码来开发我们的新插件。
打开 my_browser.dart 文件并编写一个方法 openBrowser 来调用平台特定的 openBrowser 方法。
Future<void> openBrowser(String urlString) async { try { final int result = await _channel.invokeMethod( 'openBrowser', <String, String>{ 'url': urlString } ); } on PlatformException catch (e) { // Unable to open the browser print(e); } }
打开 MyBrowserPlugin.java 文件并导入以下类:
import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle;
在这里,我们必须导入从 Android 打开浏览器所需的库。
在 MyBrowserPlugin 类中添加新的私有变量 mRegistrar,类型为 Registrar。
private final Registrar mRegistrar;
在这里,Registrar 用于获取调用代码的上下文信息。
添加一个构造函数,在 MyBrowserPlugin 类中设置 Registrar。
private MyBrowserPlugin(Registrar registrar) { this.mRegistrar = registrar; }
更改 registerWith 以在 MyBrowserPlugin 类中包含我们的新构造函数。
public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), "my_browser"); MyBrowserPlugin instance = new MyBrowserPlugin(registrar); channel.setMethodCallHandler(instance); }
更改 onMethodCall 以在 MyBrowserPlugin 类中包含 openBrowser 方法。
@Override public void onMethodCall(MethodCall call, Result result) { String url = call.argument("url"); if (call.method.equals("getPlatformVersion")) { result.success("Android " + android.os.Build.VERSION.RELEASE); } else if (call.method.equals("openBrowser")) { openBrowser(call, result, url); } else { result.notImplemented(); } }
编写平台特定的 openBrowser 方法以在 MyBrowserPlugin 类中访问浏览器。
private void openBrowser(MethodCall call, Result result, String url) { Activity activity = mRegistrar.activity(); if (activity == null) { result.error("ACTIVITY_NOT_AVAILABLE", "Browser cannot be opened without foreground activity", null); return; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); activity.startActivity(intent); result.success((Object) true); }
my_browser 插件的完整源代码如下:
my_browser.dart
import 'dart:async'; import 'package:flutter/services.dart'; class MyBrowser { static const MethodChannel _channel = const MethodChannel('my_browser'); static Future<String> get platformVersion async { final String version = await _channel.invokeMethod('getPlatformVersion'); return version; } Future<void> openBrowser(String urlString) async { try { final int result = await _channel.invokeMethod( 'openBrowser', <String, String>{'url': urlString}); } on PlatformException catch (e) { // Unable to open the browser print(e); } } }
MyBrowserPlugin.java
package com.tutorialspoint.flutterplugins.my_browser; 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 io.flutter.plugin.common.PluginRegistry.Registrar; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; /** MyBrowserPlugin */ public class MyBrowserPlugin implements MethodCallHandler { private final Registrar mRegistrar; private MyBrowserPlugin(Registrar registrar) { this.mRegistrar = registrar; } /** Plugin registration. */ public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel( registrar.messenger(), "my_browser"); MyBrowserPlugin instance = new MyBrowserPlugin(registrar); channel.setMethodCallHandler(instance); } @Override public void onMethodCall(MethodCall call, Result result) { String url = call.argument("url"); if (call.method.equals("getPlatformVersion")) { result.success("Android " + android.os.Build.VERSION.RELEASE); } else if (call.method.equals("openBrowser")) { openBrowser(call, result, url); } else { result.notImplemented(); } } private void openBrowser(MethodCall call, Result result, String url) { Activity activity = mRegistrar.activity(); if (activity == null) { result.error("ACTIVITY_NOT_AVAILABLE", "Browser cannot be opened without foreground activity", null); return; } Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); activity.startActivity(intent); result.success((Object) true); } }
创建一个新项目 my_browser_plugin_test 来测试我们新创建的插件。
打开 pubspec.yaml 并将 my_browser 设置为插件依赖项。
dependencies: flutter: sdk: flutter my_browser: path: ../my_browser
Android Studio 将提示 pubspec.yaml 已更新,如下面的 Android Studio 包警报所示:
点击获取依赖项选项。Android Studio 将从 Internet 获取包并为应用程序正确配置它。
打开 main.dart 并包含 my_browser 插件,如下所示:
import 'package:my_browser/my_browser.dart';
从 my_browser 插件调用 openBrowser 函数,如下所示:
onPressed: () => MyBrowser().openBrowser("https://flutterdart.cn"),
main.dart 的完整代码如下:
import 'package:flutter/material.dart'; import 'package:my_browser/my_browser.dart'; void main() => runApp(MyApp()); 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' ), );, } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(this.title), ), body: Center( child: RaisedButton( child: Text('Open Browser'), onPressed: () => MyBrowser().openBrowser("https://flutterdart.cn"), ), ), ); } }
运行应用程序并点击“打开浏览器”按钮,您会看到浏览器已启动。您会看到浏览器应用程序 - 首页,如下图所示:
您会看到浏览器应用程序 - 浏览器屏幕,如下图所示: