此文章是以下链接的中文理解,个人认为是算是比较清楚的入门教程,仅为个人学习使用,需要原文请跳转:
Flutter 入门- 作者:raywenderlich.com
一、学习导航
文章目录
二、开始
- 本教程使用vsCode。
- 您可以使用 iOS 模拟器、Android 模拟器、Web 浏览器、本机桌面应用程序或下述所有应用程序来开发应用程序!
三、简介
- 编程语言:Dart
a. 具有其他现代语言的许多特性,例如 Kotlin 和 Swift。
b. 转编译为 JavaScript 代码。 - 跨平台框架同向对比:
a. Flutter 与 React Native 最为相似,两者都允许反应式和声明式的编程风格。
b. Flutter 不需要使用 JavaScript 桥接,提高了应用程序的启动时间和整体性能(原理:Dart 通过使用 Ahead-Of-Time (AOT) 编译来实现) - 热重载:
a. Dart 还可以使用即时 (JIT) 编译。
b. 使用 Flutter 进行 JIT 编译,允许热重载功能在开发过程中刷新 UI,而无需全新的构建,从而改进了开发工作流程。 - Flutter 框架构建原理:
a. Flutter 框架是围绕 widget 的概念构建的。(Widget是一个抽象的概念,它可以是一个简单的元素,也可以是一个包含其他widget的复杂组合体。)
b. 不仅可以将小部件用于应用的视图,还可以将小部件用于整个屏幕,甚至用于应用本身。
四、设置开发环境
在 Flutter 的入门页面上找到使用 Flutter 框架设置开发机器的说明。具体步骤因平台而异,但遵循以下基本格式:
- 下载开发机器操作系统的安装包,获取 Flutter SDK 的最新稳定版本。
- 将安装捆绑包解压缩到所需位置。
- 将 flutter 该工具添加到您的路径中。
- 运行该命令,该 flutter doctor 命令会提醒您 Flutter 安装的任何问题。
- 安装缺少的依赖项。
- 使用 Flutter 插件/扩展设置你的 IDE。
- 试用应用。
Flutter 网站上提供的说明做得很好,允许你轻松地在你选择的平台上设置开发环境。本教程的其余部分假设你已经为 Flutter 开发设置了 VS Code,并且你已经解决了发现的任何问题 flutter doctor 。您也可以使用 Android Studio 进行后续操作。
若要将项目作为移动应用运行,需要使用以下选项之一:
- 运行 iOS 模拟器或 Android 模拟器。
- 设置 iOS 或 Android 设备进行开发。
- 将代码作为 Web 应用运行。
- 最后,可以将代码作为桌面应用运行。
即使您的最终目标是移动设备,在开发过程中使用 Web 或桌面应用程序也可以让您调整应用程序的大小并观察它在各种屏幕尺寸下的外观。如果您的电脑较旧,则 Web 或桌面版本的加载速度也将比 Android 模拟器或 iOS 模拟器更快。
【注】若要在 iOS 模拟器或 iOS 设备上进行构建和测试,您需要将 macOS 与 Xcode 配合使用。此外,即使您打算使用 VS Code 作为主 IDE,获取 Android SDK 和 Android 模拟器的最简单方法也是安装 Android Studio。
五、创建新项目
- 在安装了 Flutter 扩展的 VS Code 中,通过选择 View ▸ Command Palette 打开命令面板…或在 macOS 上按 Command-Shift-P,或在 Linux 或 Windows 上按 Control-Shift-P。在调色板中输入 Flutter: New Application Project,然后按 Return 键。
- 选择要存储项目的文件夹。然后,输入 ghflutter 作为项目的名称,按 Return 键并等待 Flutter 在 VS Code 中设置项目。项目准备就绪后,您将在编辑器中看到 main.dart。
- 在 VS Code 中,你会在左侧看到一个面板,显示项目结构。有适用于 Android、iOS 和 Web 的文件夹。这些文件夹包含在这些平台上部署应用所需的文件。还有一个包含 main.dart 的 lib 文件夹,其中包含适用于两个平台的代码。在本教程中,您将主要在 lib 文件夹中工作。
六、编辑代码
- 接下来,将 main.dart 中的代码替换为以下内容:
import 'package:flutter/material.dart';
void main() => runApp(const GHFlutterApp());
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'GHFlutter',
home: Scaffold(
appBar: AppBar(
title: const Text('GHFlutter'),
),
body: const Center(
child: Text('GHFlutter'),
),
),
);
}
}
如果您使用的是 Android Studio,请不要担心故意保存,它默认会自动保存。
【注意】:如果您使用的是 VS Code,请记住保存更改以使其生效。从 VS Code 菜单中选择文件 ▸ 保存,或在 macOS 上按 Command-S,在 Windows 或 Linux 上按 Control-S。
- 在上面的代码中, main 使用 => 单行函数的运算符来运行应用。应用有一个名为 GHFlutterApp 的类。
您的应用本身是一个无状态小组件。Flutter 应用中的大多数实体都是小部件,要么是无状态的,要么是有状态的。您可以覆盖 build 以创建应用程序小组件。紧跟在 return 关键字之后,您会看到小 MaterialApp 组件,这有助于您的应用符合 Material Design 指南。
【补充知识点:】
- 无状态(Stateless)Widget:
a. 无状态的widget是不可变的,一旦创建就不能更改内部状态。
b. 它们通常用于表示静态的、不会随时间变化的用户界面元素,比如按钮、图标、文本等。
c. Stateless widget通常通过接收一些配置参数来构建界面,但一旦构建完成,它们的外观和行为就是固定的。- 有状态(Stateful)Widget:
a. 有状态的widget是可变的,它们可以保存和管理状态信息。
b. 这种类型的widget通常用于表示会随时间变化的用户界面元素,比如输入表单、动画等。
c. 有状态的widget分为两部分:widget本身是不可变的,但与之关联的状态是可变的。状态是通过State对象来管理的,它可以在widget的生命周期内发生变化。
七、运行应用程序
- 现在,是时候运行您的简单应用程序了。单击右下角的当前选定平台,获取可以运行应用的所有可用平台的列表。此处的图片显示 Chrome 网络浏览器、iOS 模拟器和 Pixel Android 模拟器可用。
- 选择一个平台(例如 Pixel 移动模拟器),然后等待模拟器启动。
- 模拟器准备就绪后,按 F5 生成并运行,方法是从菜单中选择调试 ▸ 开始调试,或者单击右上角的三角形播放图标。调试控制台将打开,如果您在 Android 上运行,您会看到 Gradle 正在构建。如果在 iOS 上运行,你将看到 Xcode 正在构建项目。
- 下面是在 Android 模拟器中运行的应用:
右上角的 DEBUG 横幅表示应用正在调试模式下运行。
- 通过单击 VS Code 窗口顶部的工具栏右侧的方形红色“停止”按钮来停止正在运行的应用:
- 通过单击 VS Code 左上角的“资源管理器”图标或选择“视图”▸“资源管理器”,返回到“项目资源管理器”视图
八、使用热重载
Flutter 开发最好的方面之一是,您可以在进行更改时热重载您的应用程序。此功能允许您在更新 UI 的不同部分时获得即时反馈。
- 再次生成并运行:
- 现在,在不停止正在运行的应用的情况下,将 main.dart 中的应用栏字符串更改为其他内容:
appBar: AppBar(
title: const Text('GHFlutter App'),
),
- 以前,横幅上写着GHFlutter;现在,它说 GHFlutter App。
- 现在,保存 main.dart。这将自动触发热重载,但您也可以单击“热重载”按钮:
- 你几乎会立即看到正在运行的应用中反映的更改:
【注意:】热重载功能可能并不总是有效——热重载官方文档在解释它不起作用的情况方面做得非常好——但总的来说,当你构建你的 UI 时,它是一个很好的节省时间。
九、导入文件
- 与其将所有 Dart 代码保存在单个 main.dart 文件中,不如希望能够从您创建的其他文件中导入代码。现在,您将看到一个用于导入字符串的示例,当您需要本地化面向用户的字符串时,这将有所帮助。
- 在 lib 文件夹中创建一个名为 strings.dart 的文件,方法是单击 lib,然后单击“新建文件”按钮:
- 将以下行添加到新文件:
const appTitle = 'GHFlutter';
【注意】:如果您有使用其他编程语言的经验,您可能习惯于将字符串保留为类中的静态常量。Dart 允许类之外的顶级常量。将它们分组到同一个文件中就足够了。
- 将以下导入添加到 main.dart 的顶部:
import 'strings.dart' as strings;
- 更改小部件以使用新的 strings.dart。 GHFlutterApp 将如下所示:
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: strings.appTitle,
home: Scaffold(
appBar: AppBar(
title: const Text(strings.appTitle),
),
body: const Center(
child: Text(strings.appTitle),
),
),
);
}
}
- 保存更改并热重载。该应用现在已恢复到其原始状态,但您使用的是 strings.dart 中的字符串。
十、了解小组件
Flutter 应用的几乎每个元素都是一个小部件。小组件被设计为不可变或不可更改的,因为使用不可变的小组件有助于保持应用程序 UI 的轻量级。您可以将小部件想象成蓝图,告诉 UI 应该是什么样子。不同的外观需要不同的蓝图。
您将使用两种基本类型的小部件:
- 无状态:仅依赖于其自身配置信息的小组件,例如图像视图中的静态图像。
- 有状态:需要维护动态信息的小部件。它们通过与 State 对象交互来实现此目的。
只要 Flutter 框架告诉它们,无状态和有状态的小部件都会重新绘制。区别在于,有状态小部件将其配置委托给 State 对象。
十一、创建小组件
- 要制作自己的小部件,请转到 main.dart 的底部并开始键入 stful,这是“stateful”的缩写。这将为您提供类似于以下内容的弹出窗口:
- 按 Return 键选择第一个选项。
- VS Code 将帮助你使用多个游标填写名称。编写 GHFlutter:
- 这是您刚刚添加的新代码:
class GHFlutter extends StatefulWidget {
const GHFlutter({ Key? key }) : super(key: key);
_GHFlutterState createState() => _GHFlutterState();
}
class _GHFlutterState extends State<GHFlutter> {
Widget build(BuildContext context) {
return Container(
);
}
}
以下是一些需要注意的事项:
- 您创建了一个名为 StatefulWidget GHFlutter 的子类。
- 开头 const 的行是类构造函数。
- 您正在重写 createState 以创建有状态小组件的状态对象。
- _GHFlutterState 是状态类的名称。前面 _GHFlutterState 的下划线表示此类是文件专用的。它不能导入到其他文件中。
- build 是构建小部件的主要位置。默认情况下,此值当前返回空 Container 值。接下来,您将用其他东西将其替换掉。
- 将整个 build 方法替换为 _GHFlutterState 以下内容:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(strings.appTitle),
),
body: const Text(strings.appTitle),
);
}
Scaffold 是 Material Design 微件的容器。它充当小部件层次结构的根。在这里,您已将 AppBar 和 body 添加到脚手架中,并且每个都包含一个小 Text 部件。
- 替换 GHFlutterApp 为以下代码:
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: strings.appTitle,
home: const GHFlutter(),
);
}
}
现在,使用你的新 GHFlutter 作为它的 home 属性, GHFlutterApp 而不是构建自己的 Scaffold。
【注意】:const 有时会在小部件和变量前面看到关键字,表示编译时常量。添加 const 并不总是可能或必要的,但这样做可以让 Flutter 进行一些优化。
- 热重载,您将看到新的小部件在运行:
- 您还没有进行太多更改,但现在您已经准备好构建新的小部件了。
十二、进行网络呼叫
之前,您已将 strings.dart 导入到项目中。同样,你可以从 Flutter 框架甚至其他开发者那里导入其他包。例如,你现在将使用几个额外的包来进行 HTTP 网络调用,并将生成的 JSON 响应解析为 Dart 对象。
十三、导入包
- 在 main.dart 的顶部添加两个新的导入:
import 'dart:convert';
import 'package:http/http.dart' as http;
- 您会注意到该 http 软件包不可用。那是因为您尚未将其添加到项目中。
- 导航到项目根文件夹中的 pubspec.yaml。在 dependencies 部分的 cupertino_icons: ^1.0.2 下,添加以下行:
dependencies:
# 其他包,例如:cupertino_icons: ^1.0.2
http: ^0.13.3
【注意】:注意缩进。使用与 cupertino_icons 包相同的两个空格缩进。
- 现在,当你保存 pubspec.yaml 时,VS Code 中的 Flutter 扩展将运行该 flutter pub get 命令。Flutter 将获取声明的 http 包,并在 main.dart 中提供。
【注意】:如果你导入别人的项目时,一开始需要在项目根目录在终端执行下面的命令,以此下载项目依赖
flutter --no-color pub upgrade
- 现在,您将在 main.dart 中最近的两个导入下看到蓝线,表示它们当前未使用。
- 不用担心。您稍后会用到它们。
十四、使用异步代码
Dart 应用程序是单线程的,但 Dart 支持在其他线程上运行代码。它还支持运行不阻止 UI 线程的异步代码。它使用 async/await 模式来执行此操作。
【注意】:许多初学者错误地认为异步方法在另一个线程上运行。虽然 I/O 任务(如委托给系统的网络调用)确实在不同的系统线程上运行,但你自己编写的方法 async 中的代码都在 UI 线程上运行。它只是计划稍后在 UI 不繁忙时运行。如果你真的想在另一个线程上运行一些代码,那么你需要创建一个所谓的新 Dart 隔离。
- 接下来,你将进行异步网络调用以检索 GitHub 团队成员的列表。为此,请添加一个空列表作为属性, _GHFlutterState 以及一个用于保存文本样式的属性:
var _members = <dynamic>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
如前所述,名称开头的下划线使类文件的成员为私有。该 dynamic 关键字告诉 Dart 该列表可以包含任何内容。通常,它使用 dynamic 起来并不理想,因为它选择退出 Dart 具有的类型安全系统。但是,在进行网络调用时,处理 dynamic 是不可避免的。
- 要进行异步 HTTP 调用,请添加到 _loadData _GHFlutterState :
Future<void> _loadData() async {
const dataUrl = 'https://api.github.com/orgs/raywenderlich/members';
final response = await http.get(Uri.parse(dataUrl));
setState(() {
_members = json.decode(response.body) as List;
});
}
【注意】:此处’https://api.github.com/orgs/raywenderlich/members’返回的json格式:
(可用apifox之类的工具模拟接口返回,如果想调用本地接口,不能使用127.0.0.1,只能使用本地ip地址)[ {"login": "0xTim","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "aliHafizji","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "ashfurrow","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "astralbodies","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "byaruhaf","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "charliefulton","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "CodeAndWeb","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "colegeissinger","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "ColinEberhardt","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "cweinberger","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "desianatednerd","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"} ]
在这里,你已经添加了 async 关键字 _loadData ,告诉 Dart 它是异步的。它是异步的另一个线索是 Future 返回类型。你把它放在 await 前面, http.get() 因为这是另一个异步调用,可能需要一段时间。
HTTP 调用完成后,将回调传递给 setState 在 UI 线程上同步运行的回调。在本例中,你将对 JSON 响应进行解码并将其分配给 _members 列表。如果你在不调用 setState 的情况下设置 _members ,那么 Flutter 不会重建 UI,你的用户也不会意识到状态已经改变。
- 将 initState 覆盖添加到 _GHFlutterState :
void initState() {
super.initState();
_loadData();
}
此方法在首次创建状态类时调用 _loadData 。
- 现在你已经在 Dart 中创建了一个成员列表,你需要一种方法来在 UI 的列表中显示它们。
十五、使用 ListView
Dart 提供了一个 ListView,它允许你在列表中显示数据。 ListView 就像 Android 上的 a RecyclerView 或 iOS UICollectionView 上的 一样,在用户滚动列表时回收视图以实现平滑滚动性能。
- 添加 _buildRow 至 _GHFlutterState :
Widget _buildRow(int i) {
return ListTile(
title: Text('${_members[i]['login']}', style: _biggerFont),
);
}
在这里,您将返回一个 ListTile,该 ListTile 显示从 JSON 中解析的 index i 成员的登录名。它还使用您之前创建的文本样式。
- 将 build 方法中的 body 行替换 _GHFlutterState 为以下内容:
body: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _members.length,
itemBuilder: (BuildContext context, int position) {
return _buildRow(position);
}),
填充会在每个列表项周围添加一些空白空间 — 在本例中为 16 个逻辑像素。设置 itemCount 表示 ListView 它总共有多少行。最后,调用 itemBuilder 屏幕上可见的每个新行,以此为契机在 ListTile _buildRow .
【注意】:复制和粘贴有时会弄乱格式。在 macOS 上按 Shift-Option-F 或在 Windows 上按 Shift-Alt-F 来修复格式。如果将 VS Code 设置为保存文件,则保存文件也可能自动格式化。
- 此时,您可能需要完全重启,而不是热重载。“热重启”按钮对此很有用。它仍然比完全停止应用程序并重新构建它更快。
- 重新启动后,你将看到以下内容:
- 这就是进行网络调用、解析数据并在列表中显示结果是多么容易!
十六、添加分隔线
- 若要将分隔符添加到列表中,请使用 ListView.separated ListView.builder 代替 。 body 将脚手架替换为以下代码:
body: ListView.separated(
itemCount: _members.length,
itemBuilder: (BuildContext context, int position) {
return _buildRow(position);
},
separatorBuilder: (context, index) {
return const Divider();
}),
使用 ListView.separated 为您提供了一个 separatorBuilder 选项,允许您在列表磁贴之间添加分隔线。现在您有了分隔线,您还从构建器中删除了填充。
- 热重载。现在,您将看到行之间的分隔线:
- 要为每一行添加填充, ListTile 请使用 Padding inside _buildRow 换行。在 VS Code 中执行此操作的最简单方法是将光标放在 ListTile Command 上并按 Command-。在 macOS 或 Control- 上。在 Windows 上。然后将填充增加到 16.0 ,如下面的动画 GIF 所示。
- 或者,您可以替换 _buildRow 为以下代码以获得相同的结果:
Widget _buildRow(int i) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListTile(
title: Text('${_members[i]['login']}', style: _biggerFont),
),
);
}
- ListTile 现在是 Padding 的子小部件。热重载以查看填充显示在行上,但不显示在分隔符上。
十七、解析为自定义类型
在上一节中,JSON 解析器获取 JSON 响应中的成员并将其分配给 _members 。虽然你把列表定义为 dynamic ,但 Dart 放入列表的实际类型是 Map ,一个包含键值对的数据结构。这相当于 Kotlin 中的 Map 或 Swift 中的 Dictionary。
但是,您还希望能够使用自己的自定义类型。
- 在 main.dart 的底部添加一个新 Member 类型:
class Member {
Member(this.login);
final String login;
}
Member 具有一个构造函数,用于在创建 Member 对象时设置 Login 属性。
- 更新 _members 声明, _GHFlutterState 使其成为 Member 对象列表:
final _members = <Member>[];
- 您使用的 final var 代替 因为,您现在要将项目添加到现有列表中,而不是将新列表重新 _members 分配给 。
- 替换 setState _loadData 为以下代码:
setState(() {
final dataList = json.decode(response.body) as List;
for (final item in dataList) {
final login = item['login'] as String? ?? '';
final member = Member(login);
_members.add(member);
}
});
这会将每个解码的映射转换为一个 Member ,并将其添加到成员列表中。
【注意】:对单 ? 问号和双 ?? 问号感到好奇吗?查看 Dart 基础知识教程,了解有关这些运算符的更多详细信息。
- Flutter 还在内心 ListTile 抱怨,因为它期待 Map 而不是 Member .将 ListTile 行替换 title 为以下内容:
Widget _buildRow(int i) {
return ListTile(
title: Text('${_members[i].login}', style: _biggerFont),
);
}
如果您尝试热重载,则会看到错误,因此请热重启。您将看到与以前相同的屏幕,只是它现在使用您的新 Member 类。
十八、使用 NetworkImage 下载图像
在 GitHub 中,每个成员都有其头像的 URL。您的下一个改进是将该头像添加到类中,并在应用程序中 Member 显示头像。
- 更新 Member 以添加 avatarUrl 属性。它现在应该看起来像这样:
class Member {
Member(this.login, this.avatarUrl);
final String login;
final String avatarUrl;
}
- 由于 avatarUrl 现在是必需的参数,Flutter 在 _loadData .将 setState 回调替换为 _loadData 以下更新版本:
setState(() {
final dataList = json.decode(response.body) as List;
for (final item in dataList) {
final login = item['login'] as String? ?? '';
final url = item['avatar_url'] as String? ?? '';
final member = Member(login, url);
_members.add(member);
}
});
上面的代码使用键 avatar_url 在从 JSON 解析的映射中查找 URL 值,然后将其设置为 url 字符串,并传递给 Member 。
- 现在,您已经可以访问头像的 URL,请将其添加到您的 ListTile .替换 _buildRow 为以下内容:
Widget _buildRow(int i) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListTile(
title: Text('${_members[i].login}', style: _biggerFont),
leading: CircleAvatar(
backgroundColor: Colors.green,
backgroundImage: NetworkImage(_members[i].avatarUrl),
),
),
);
}
这会 CircleAvatar 在您的 ListTile .在等待图像下载时,背景 CircleAvatar 将为绿色。
- 执行热重启,而不是热重启。您将在每一行中看到您的成员头像:
十九、清理代码
- 您的大部分代码现在都在 main.dart 中。为了使代码更简洁一些,您需要将类重构到它们自己的文件中。
在 lib 文件夹中创建名为 member.dart 和 ghflutter.dart 的文件。进入 Member member.dart 和两者 _GHFlutterState ,然后 GHFlutter 进入 ghflutter.dart。
在 member.dart 中不需要任何 import 语句,但 ghflutter.dart 中的导入应该是:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'member.dart';
import 'strings.dart' as strings;
- 您还需要更新 main.dart 中的导入。将整个文件替换为以下内容:
import 'package:flutter/material.dart';
import 'ghflutter.dart';
import 'strings.dart' as strings;
void main() => runApp(const GHFlutterApp());
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: strings.appTitle,
// TODO: add theme here
home: const GHFlutter(),
);
}
}
- 保存所有内容并重新运行应用。您不会看到任何更改,但代码现在更干净了。:]
您可能已经注意到一条新评论: // TODO: add theme here .在那里,您将在本教程结束之前进行最后一次更改。
二十、添加主题
最后一项改进是通过向在 main.dart 中创建的主题属性 MaterialApp 添加主题,轻松地将主题添加到应用中。
- 查找 // TODO: add theme here 并将其替换为以下内容:
theme: ThemeData(primaryColor: Colors.green.shade800),
在这里,您使用绿色阴影作为主题的 Material Design 颜色值。
- 保存并热重新加载以查看新主题的运行情况:
- 到目前为止,该应用程序的屏幕截图来自 Android 模拟器。您还可以在 iOS 模拟器中运行最终主题应用程序:
- 以下是它在 Chrome 网络浏览器上的外观:
也可以将其作为 Windows、Mac 或 Linux 应用程序运行。它只需要一些额外的设置,并为您的应用程序添加桌面支持。在 macOS 上,您还应该授予应用程序访问互联网的权限。
- 下面是在 macOS 上运行的应用:
现在这就是你所说的跨平台!:]
二十二、最后所有代码
- main.dart
import 'package:flutter/material.dart';
import 'strings.dart' as strings;
import 'ghflutter.dart';
void main() => runApp(const GHFlutterApp());
class GHFlutterApp extends StatelessWidget {
const GHFlutterApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: strings.appTitle,
theme: ThemeData(primaryColor: Colors.green.shade800),
home: const GHFlutter(),
);
}
}
- strings.dart
const appTitle = 'GHFlutter APP';
- ghflutter.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'member.dart';
import 'strings.dart' as strings;
class GHFlutter extends StatefulWidget {
const GHFlutter({ Key? key }) : super(key: key);
_GHFlutterState createState() => _GHFlutterState();
}
class _GHFlutterState extends State<GHFlutter> {
var _members = <Member>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(strings.appTitle),
),
body: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _members.length,
itemBuilder: (BuildContext context, int position) {
return _buildRow(position);
}),
);
}
void initState() {
super.initState();
_loadData();
}
Widget _buildRow(int i) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ListTile(
title: Text('${_members[i].login}', style: _biggerFont),
leading: CircleAvatar(
backgroundColor: Colors.green,
backgroundImage: NetworkImage(_members[i].avatarUrl),
),
),
);
}
Future<void> _loadData() async {
const dataUrl = 'https://api.github.com/orgs/raywenderlich/members';
final response = await http.get(Uri.parse(dataUrl));
setState(() {
final dataList = json.decode(response.body) as List;
for (final item in dataList) {
final login = item['login'] as String? ?? '';
final url = item['avatar_url'] as String? ?? '';
final member = Member(login, url);
_members.add(member);
}
});
}
}
【注意】:此处’https://api.github.com/orgs/raywenderlich/members’返回的json格式:
(可用apifox之类的工具模拟接口返回,如果想调用本地接口,不能使用127.0.0.1,只能使用本地ip地址)[ {"login": "0xTim","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "aliHafizji","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "ashfurrow","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "astralbodies","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "byaruhaf","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "charliefulton","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "CodeAndWeb","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "colegeissinger","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "ColinEberhardt","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "cweinberger","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"}, {"login": "desianatednerd","avatar_url": "https://www.baidu.com/img/flexible/logo/pc/result@2.png"} ]
- member.dart
class Member {
Member(this.login, this.avatarUrl);
final String login;
final String avatarUrl;
}