给 iOS 开发者的 Flutter 指南(上)
这篇文章是为那些想将已有的 iOS 开发经验运用到 Flutter 开发中的 iOS 开发者所作。 如果你理解 iOS framework 的基本原理,那么你可以将这篇文章作为学习 Flutter 开发的起点。
本文结构如下:
1. 视图(上)
2. 导航(上)
3. 线程和异步(上)
4. 工程结构、本地化、依赖和资源(上)
5. ViewControllers(下)
6. 布局(下)
7. 手势检测与 touch 事件处理(下)
8. 主题和文字(下)
9. 表单输入(下)
10. 和硬件、第三方服务以及系统平台交互(下)
11. 数据库和本地存储(下)
12. 通知(下)
一、Views
1.1 UIView 相当于 Flutter 中的什么?
在 iOS 中,你在 UI 中创建的大部分视图都是 UIView
的实例。而在构造布局时,这些视图也可以作为其他视图的容器。
在 Flutter 中,Widget 可以类比为
UIView
,你可以把它理解为“声明和构造 UI 的方法”,但它们又并非完全相同:
首先,widget 拥有着不同的生命周期: 整个生命周期内它是不可变的,且只能够存活到被修改的时候。一旦 widget 实例或者它的状态发生了改变, Flutter 框架就会创建一个新的由 Widget 实例构造而成的树状结构。而在 iOS 里,修改一个视图并不会导致它重新创建实例,它作为一个可变对象,只会绘制一次,只有调用 setNeedsDisplay(
)
之后才会发生重绘。
其次,Flutter 的 widget 是很轻量的,一部分原因就是由于它的不可变特性。因为它并不是视图,也不直接绘制任何内容,而是作为对 UI 及其特性的一种描述,而被“注入”到视图中去。
Flutter 包含了 Material Components 库。内容都是一些遵循了 Material Design 设计规范的组件。Material Design 是一种灵活的支持全平台的设计体系,其中也包括了 iOS。
但是 Flutter 的灵活性和表现力使其能够适配任何的设计语言。在 iOS 中,你可以通过 Cupertino widgets 来构造类似于Apple iOS 设计语言的接口。
1.2 我该如何更新 Widgets?
在 iOS 可以直接对视图进行修改。但是在 Flutter 中,widget 都是不可变的,所以也不能够直接对其修改。所以,你必须通过修改 widget 的 state 来达到更新视图的目的。
于是,就引入了 Stateful widget 和 Stateless widget 的概念。和字面意思相同,StatelessWidget
就是 一个没有绑定状态的 widget。
当某个 widget 不需要依赖任何别的初始配置来对这个 widget 进行描述时,StatelessWidget
会是很有用的。
举个例子,在 iOS 中,你需要把 logo 当作 image 并将它放置在 UIImageView
中, 如果在运行时这个 logo 不会发生变化,那么对应 Flutter 中你应该使用 StatelessWidget
。
但是如果你想要根据 HTTP 请求的返回结果动态的修改 UI,那么你应该使用 StatefulWidget
。在 HTTP 请求结束 后,通知 Flutter 更新这个 widget 的 State
,然后 UI 就会得到更新。
StatefulWidget
和 StatelessWidget
最重要的区别就是,StatefulWidget
中有一个State 对象,它用来存储一些状态的信息,并在整个生命周期内保持不变。
如果你对此还存有疑虑,记住一点:如果一个 widget 在 build
方法之外(比如运行时下发生用户点击事件)被修改,那么就应该是有状态的。如果一个 widget 一旦生成就不再发生改变,那么它就是无状态的。然而,即使一个 widget 是有状态的,如果不是自身直接响应修改(或别的输入),那么他的父容器也可以是无状态的。
下面是如何使用 StatelessWidget
的示例。Text
是一个常用的 StatelessWidget
。如果你看了 Text
的源代码,就会发现它继承于 StatelessWidget
。
1Text(
2 'I like Flutter!',
3 style: TextStyle(fontWeight: FontWeight.bold),
4);
如上述代码所示, Text
没有携带任何状态。它只会渲染初始化时传入内容。
如果你希望在点击 FloatingActionButton
时 I like Flutter
能产生动态的改变,只需要把 Text
放到 StatefulWidget
中,并在用户点击按钮时更新它即可。
下面是示例代码:
1class SampleApp extends StatelessWidget {2 // This widget is the root of your application.3 @override4 Widget build(BuildContext context) {5 return MaterialApp(6 title: 'Sample App',7 theme: ThemeData(8 primarySwatch: Colors.blue,9 ),
10 home: SampleAppPage(),
11 );
12 }
13}
14
15class SampleAppPage extends StatefulWidget {
16 SampleAppPage({Key key}) : super(key: key);
17
18 @override
19 _SampleAppPageState createState() => _SampleAppPageState();
20}
21
22class _SampleAppPageState extends State<SampleAppPage> {
23 // Default placeholder text
24 String textToShow = "I Like Flutter";
25 void _updateText() {
26 setState(() {
27 // update the text
28 textToShow = "Flutter is Awesome!";
29 });
30 }
31 @override
32 Widget build(BuildContext context) {
33 return Scaffold(
34 appBar: AppBar(
35 title: Text("Sample App"),
36 ),
37 body: Center(child: Text(textToShow)),
38 floatingActionButton: FloatingActionButton(
39 onPressed: _updateText,
40 tooltip: 'Update Text',
41 child: Icon(Icons.update),
42 ),
43 );
44 }
45}
1.3 如何对 widget 布局? Storyboard 在哪?
在 iOS 开发中,你可能会经常使用 Storyboard 来组织你的视图,并直接通过 Storyboard 或者 在 ViewController 中通过代码来设置约束。而在 Flutter 中,你要通过代码来对 widget 进行 组织来形成一个 widget 树状结构。
下面的例子展示了如何展示一个带有 padding 的 widget:
1@override2Widget build(BuildContext context) {3 return Scaffold(4 appBar: AppBar(5 title: Text("Sample App"),6 ),7 body: Center(8 child: CupertinoButton(9 onPressed: () {
10 setState(() { _pressedCount += 1; });
11 },
12 child: Text('Hello'),
13 padding: EdgeInsets.only(left: 10.0, right: 10.0),
14 ),
15 ),
16 );
17}
你可以为任何 widget 添加 padding,来达到类似在 iOS 中视图约束的作用。
你可以在widget 目录中查看 Flutter 提供的所有 widget 布局方法。
1.4 如何添加或移除一个组件?
在 iOS 中,你可以通过调用父视图的 addSubview()
方法或者 removeFromSuperview()
方法 来动态的添加或移除视图。
在 Flutter 中,因为 widget 是不可变的,所以没有提供直接同 addSubview()
作用相同的方法。但是你可以通过向父视图传递一个返回值是 widget 的方法,并通过一个 boolean flag 来控制子视图的存在。
下面的例子中像你展示了如何让用户通过点击 FloatingActionButton
按钮来达到在两个 widget 中切换的目的:
1class SampleApp extends StatelessWidget {2 // This widget is the root of your application.3 @override4 Widget build(BuildContext context) {5 return MaterialApp(6 title: 'Sample App',7 theme: ThemeData(8 primarySwatch: Colors.blue,9 ),
10 home: SampleAppPage(),
11 );
12 }
13}
14
15class SampleAppPage extends StatefulWidget {
16 SampleAppPage({Key key}) : super(key: key);
17
18 @override
19 _SampleAppPageState createState() => _SampleAppPageState();
20}
21
22class _SampleAppPageState extends State<SampleAppPage> {
23 // Default value for toggle
24 bool toggle = true;
25 void _toggle() {
26 setState(() {
27 toggle = !toggle;
28 });
29 }
30
31 _getToggleChild() {
32 if (toggle) {
33 return Text('Toggle One');
34 } else {
35 return CupertinoButton(
36 onPressed: () {},
37 child: Text('Toggle Two'),
38 );
39 }
40 }
41
42 @override
43 Widget build(BuildContext context) {
44 return Scaffold(
45 appBar: AppBar(
46 title: Text("Sample App"),
47 ),
48 body: Center(
49 child: _getToggleChild(),
50 ),
51 floatingActionButton: FloatingActionButton(
52 onPressed: _toggle,
53 tooltip: 'Update Text',
54 child: Icon(Icons.update),
55 ),
56 );
57 }
58}
1.5 如何添加动画?
在 iOS 里,你可以使用调用视图的 animate(withDuration:animations:)
方法来创建动画。
在 Flutter 里,通过使用动画库将 widget 封装到 animated widget 中来实现带动画效果。AnimationController
是一个可以暂停、寻找、停止、反转动画的 Animation<double>
类型。它需要一个 Ticker
,在屏幕刷新时发出信号量,并在运行时对每一帧都产生一个 0~1 的线性差值。你可以创建一个或多个 Animation
,并把它们添加到控制器中。
比如,你可以使用 CurvedAnimation
来实现一个曲线翻页动画。这种情况下,控制器就是动画进度的主要数据源, 而 CurvedAnimation
计算曲线并替换控制器的默认线性运动。和 widget 一样,在 Flutter 里动画也可以复合嵌套。
当构建一个 widget 树时,可以将 Animation
赋值给 widget 用户表现动画能力的属性, 比如 FadeTransition
的 opacity 属性,然后告诉控制器启动动画。
下面的示例描述了当你点击 FloatingActionButton
时,如何实现一个视图渐淡出成 logo 的 FadeTransition
效果:
1class SampleApp extends StatelessWidget {2 // This widget is the root of your application.3 @override4 Widget build(BuildContext context) {5 return MaterialApp(6 title: 'Fade Demo',7 theme: ThemeData(8 primarySwatch: Colors.blue,9 ),
10 home: MyFadeTest(title: 'Fade Demo'),
11 );
12 }
13}
14
15class MyFadeTest extends StatefulWidget {
16 MyFadeTest({Key key, this.title}) : super(key: key);
17
18 final String title;
19
20 @override
21 _MyFadeTest createState() => _MyFadeTest();
22}
23
24class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
25 AnimationController controller;
26 CurvedAnimation curve;
27
28 @override
29 void initState() {
30 controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
31 curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
32 }
33
34 @override
35 Widget build(BuildContext context) {
36 return Scaffold(
37 appBar: AppBar(
38 title: Text(widget.title),
39 ),
40 body: Center(
41 child: Container(
42 child: FadeTransition(
43 opacity: curve,
44 child: FlutterLogo(
45 size: 100.0,
46 )
47 )
48 )
49 ),
50 floatingActionButton: FloatingActionButton(
51 tooltip: 'Fade',
52 child: Icon(Icons.brush),
53 onPressed: () {
54 controller.forward();
55 },
56 ),
57 );
58 }
59
60 @override
61 dispose() {
62 controller.dispose();
63 super.dispose();
64 }
65}
更多信息,请参阅 Animation & Motion widgets, Animations tutorial 以及 Animations overview。
1.6 如何渲染到屏幕上?
在 iOS 里,可以使用 CoreGraphics
绘制线条和图形到屏幕上。Flutter 里有一套基于 Canvas
实现的 API,有两个类可以帮助你进行绘制:CustomPaint
和 CustomPainter
,后者实现了绘制图形到 canvas 的算法。
想要学习在 Flutter 里如何实现一个画笔,可以查看 Collin 在 StackOverflow 里的回答。
1class SignaturePainter extends CustomPainter {2 SignaturePainter(this.points);34 final List<Offset> points;56 void paint(Canvas canvas, Size size) {7 var paint = Paint()8 ..color = Colors.black9 ..strokeCap = StrokeCap.round
10 ..strokeWidth = 5.0;
11 for (int i = 0; i < points.length - 1; i++) {
12 if (points[i] != null && points[i + 1] != null)
13 canvas.drawLine(points[i], points[i + 1], paint);
14 }
15 }
16
17 bool shouldRepaint(SignaturePainter other) => other.points != points;
18}
19
20class Signature extends StatefulWidget {
21 SignatureState createState() => SignatureState();
22}
23
24class SignatureState extends State<Signature> {
25
26 List<Offset> _points = <Offset>[];
27
28 Widget build(BuildContext context) {
29 return GestureDetector(
30 onPanUpdate: (DragUpdateDetails details) {
31 setState(() {
32 RenderBox referenceBox = context.findRenderObject();
33 Offset localPosition =
34 referenceBox.globalToLocal(details.globalPosition);
35 _points = List.from(_points)..add(localPosition);
36 });
37 },
38 onPanEnd: (DragEndDetails details) => _points.add(null),
39 child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
40 );
41 }
42}
1.7 如何设置视图 Widget 的透明度?
在 iOS 里,视图都有一个 opacity 或者 alpha 属性。而在 Flutter 里,大部分时候你都需要封装 widget 到一个 Opacity widget 中来实现这一功能。
1.8 如何构建自定义 widgets?
在 iOS 里,你可以直接继承 UIView
或者使用已经存在的视图,然后重写并实现对应的方法来达到想要的效果。在 Flutter 里,构建自定义 widget 需要通过合成一些小的 widget(而不是对它们进行扩展)来实现。
例如,如果你要构建一个 CustomButton
,并在构造器中传入它的文本标签?那就组合 RaisedButton
和文本标签,而不是继承 RaisedButton
:
1class CustomButton extends StatelessWidget {2 final String label;34 CustomButton(this.label);56 @override7 Widget build(BuildContext context) {8 return RaisedButton(onPressed: () {}, child: Text(label));9 }
10}
像你使用其他 Flutter 的 widget 一样,下面我们使用 CustomButton
:
1@override
2Widget build(BuildContext context) {
3 return Center(
4 child: CustomButton("Hello"),
5 );
6}
二、导航
2.1 如何在不同页面之间切换?
在 iOS 里,想要在多个 viewcontroller 中切换,可以使用 UINavigationController
管理 viewcontroller 构成的栈进行显示。
在Flutter 中,使用 Navigator
和 Routes
也可以实现类似的功能。一个 Routes
是应用中屏幕或者页面的抽象概念,而一个 Navigator
是管多个 Route
的 widget。
可以把 Route
理解为 UIViewController
。而 Navigator
的工作方式和 iOS 的 UINavigationController
类似,当你想要进入或退出一个新页面的时候,它也可以进行 push()
和 pop()
操作。
想要在不同页面间跳转,你有两个选择:
1.构建由 route 名称组成的 Map(MaterialApp)
2.直接跳转到一个 route(WidgetApp)
下面的示例构建了一个 Map
:
1void main() {2 runApp(MaterialApp(3 home: MyAppHome(), // becomes the route named '/'4 routes: <String, WidgetBuilder> {5 '/a': (BuildContext context) => MyPage(title: 'page A'),6 '/b': (BuildContext context) => MyPage(title: 'page B'),7 '/c': (BuildContext context) => MyPage(title: 'page C'),8 },9 ));
10}
通过把 route 的名称 push
给一个 Navigato
r
来跳转:
1Navigator.of(context).pushNamed('/b');
Navigator
类不仅用来处理 Flutter 中的路由,还被用来获取你刚 push 到栈中的路由返回的结果。通过 await
等待路由返回的结果来达到这点。
举个例子,要跳转到“位置”路由来让用户选择一个地点,你可能要这么做:
Navigator
类对 Flutter 中的路由事件做处理,还可以用来获取入栈之后的路由的结果。这需要通过 push()
返回的 Future
中的 await 来实现。
例如,要打开一个“定位”页面来让用户选择他们的位置,你需要做如下事情:
1Map coordinates = await Navigator.of(context).pushNamed('/location');
然后,在”定位“页面中,一旦用户选择了自己的定位,就 pop()
出栈并返回结果。
1Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
2.2 如何跳转到其他应用?
在 iOS 里,想要跳转到其他应用,可以使用特定的 URL scheme。对于系统级别的应用,scheme 都是 取决于应用的。在 Flutter 里想要实现这个功能,需要创建原生平台的整合层,或者使用已经存在的插件,例如 url_launcher。
2.3 如何退回到 iOS 原生的 viewcontroller?
在 Dart 代码中调用 SystemNavigator.pop() 将会调用下面的 iOS 代码:
1UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
2 if ([viewController isKindOfClass:[UINavigationController class]]) {
3 [((UINavigationController*)viewController) popViewControllerAnimated:NO];
4 }
三、线程和异步
3.1 如何编写异步代码?
Dart 是单线程执行模型,支持 Isolate
(一种在其他线程运行 Dart 代码的方法)、事件循环和异步编程。 除非生成了 Isolate
,否则所有 Dart 代码将永远在主 UI 线程运行,并由事件循环驱动。Flutter 中的事件循环类似于 iOS 中的 main loop—,也就是主线程上的 Looper
。
Dart 的单线程模型并不意味着你需要以阻塞 UI 的形式来执行代码,相反,你更应该使用 Dart 语言提供的异步功能, 比如使用 async
/ await
来实现异步操作。
例如,你可以使用 async
/ await
来执行网络代码以避免 UI 挂起,让 Dart 来完成这个繁重的任务:
1loadData() async {
2 String dataURL = "https://jsonplaceholder.typicode.com/posts";
3 http.Response response = await http.get(dataURL);
4 setState(() {
5 widgets = json.decode(response.body);
6 });
7}
一旦 await
等待的网络操作结束,通过调用 setState()
来更新 UI,这将会触发 widget 子树的重新构建并更新数据。
下面的示例展示了如何异步加载数据,并在 ListView
中展示出来:
1import 'dart:convert';23import 'package:flutter/material.dart';4import 'package:http/http.dart' as http;56void main() {7 runApp(SampleApp());8}9
10class SampleApp extends StatelessWidget {
11 @override
12 Widget build(BuildContext context) {
13 return MaterialApp(
14 title: 'Sample App',
15 theme: ThemeData(
16 primarySwatch: Colors.blue,
17 ),
18 home: SampleAppPage(),
19 );
20 }
21}
22
23class SampleAppPage extends StatefulWidget {
24 SampleAppPage({Key key}) : super(key: key);
25
26 @override
27 _SampleAppPageState createState() => _SampleAppPageState();
28}
29
30class _SampleAppPageState extends State<SampleAppPage> {
31 List widgets = [];
32
33 @override
34 void initState() {
35 super.initState();
36
37 loadData();
38 }
39
40 @override
41 Widget build(BuildContext context) {
42 return Scaffold(
43 appBar: AppBar(
44 title: Text("Sample App"),
45 ),
46 body: ListView.builder(
47 itemCount: widgets.length,
48 itemBuilder: (BuildContext context, int position) {
49 return getRow(position);
50 }));
51 }
52
53 Widget getRow(int i) {
54 return Padding(
55 padding: EdgeInsets.all(10.0),
56 child: Text("Row ${widgets[i]["title"]}")
57 );
58 }
59
60 loadData() async {
61 String dataURL = "https://jsonplaceholder.typicode.com/posts";
62 http.Response response = await http.get(dataURL);
63 setState(() {
64 widgets = json.decode(response.body);
65 });
66 }
67}
更多关于在后台工作的信息,以及 Flutter 和 iOS 的区别,请参考下一章节。
3.2 如何让你的工作在后台线程执行?
由于 Flutter 是单线程模型,而且执行着一个 event loop(就像 Node.js),你不需要为线程管理或 是开启后台线程操心。如果你在处理 I/O 操作,例如磁盘访问或网络请求,那么你安全地使用 async
/ await
就可以了。但是,如果你需要大量的计算来让 CPU 保持忙碌状态,你需要使用 Isolate
来防治阻塞 event loop。
对于 I/O 操作,把方法声明为 async
方法,然后通过 await
来等待异步方法的执行完成:
1loadData() async {
2 String dataURL = "https://jsonplaceholder.typicode.com/posts";
3 http.Response response = await http.get(dataURL);
4 setState(() {
5 widgets = json.decode(response.body);
6 });
7}
这就是处理网络或数据库请求等 I/O 操作的经典做法。
然而,有时候你需要处理大量的数据,从而导致 UI 挂起。在 Flutter 里,当处理长期运行或者运算密集的任务时,可以使用 Isolate
来发挥出多核 CPU 的优势。
Isolates 是相互隔离的执行线程,并不和主线程共享内存。这意味着你不能够访问主线程的变量,也不能 使用 setState()
来更新 UI。Isolates 正如起字面意思是不能共享内存(例如静态变量表)的。
下面的例子展示了在一个简单的 isolate 中,如何把数据推到主线程上用来更新 UI:
1loadData() async {2 ReceivePort receivePort = ReceivePort();3 await Isolate.spawn(dataLoader, receivePort.sendPort);45 // The 'echo' isolate sends its SendPort as the first message6 SendPort sendPort = await receivePort.first;78 List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");9
10 setState(() {
11 widgets = msg;
12 });
13}
14
15// The entry point for the isolate
16static dataLoader(SendPort sendPort) async {
17 // Open the ReceivePort for incoming messages.
18 ReceivePort port = ReceivePort();
19
20 // Notify any other isolates what port this isolate listens to.
21 sendPort.send(port.sendPort);
22
23 await for (var msg in port) {
24 String data = msg[0];
25 SendPort replyTo = msg[1];
26
27 String dataURL = data;
28 http.Response response = await http.get(dataURL);
29 // Lots of JSON to parse
30 replyTo.send(json.decode(response.body));
31 }
32}
33
34Future sendReceive(SendPort port, msg) {
35 ReceivePort response = ReceivePort();
36 port.send([msg, response.sendPort]);
37 return response.first;
38}
在这里,dataLoader()
就是运行在独立线程上的 Isolate
。在 Isolate
中,你可以处理 CPU 密集型任务(如解析一个 庞大的 JSON 文件),或者处理复杂的数学运算,比如加密操作或者信号处理等。
下面是一个完整示例:
1import 'dart:convert';23import 'package:flutter/material.dart';4import 'package:http/http.dart' as http;5import 'dart:async';6import 'dart:isolate';78void main() {9 runApp(SampleApp());10}1112class SampleApp extends StatelessWidget {13 @override14 Widget build(BuildContext context) {15 return MaterialApp(16 title: 'Sample App',17 theme: ThemeData(18 primarySwatch: Colors.blue,19 ),20 home: SampleAppPage(),21 );22 }23}2425class SampleAppPage extends StatefulWidget {26 SampleAppPage({Key key}) : super(key: key);2728 @override29 _SampleAppPageState createState() => _SampleAppPageState();30}3132class _SampleAppPageState extends State<SampleAppPage> {33 List widgets = [];3435 @override36 void initState() {37 super.initState();38 loadData();39 }4041 showLoadingDialog() {42 if (widgets.length == 0) {43 return true;44 }4546 return false;47 }4849 getBody() {50 if (showLoadingDialog()) {51 return getProgressDialog();52 } else {53 return getListView();54 }55 }5657 getProgressDialog() {58 return Center(child: CircularProgressIndicator());59 }6061 @override62 Widget build(BuildContext context) {63 return Scaffold(64 appBar: AppBar(65 title: Text("Sample App"),66 ),67 body: getBody());68 }6970 ListView getListView() => ListView.builder(71 itemCount: widgets.length,72 itemBuilder: (BuildContext context, int position) {73 return getRow(position);74 });7576 Widget getRow(int i) {77 return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));78 }7980 loadData() async {81 ReceivePort receivePort = ReceivePort();82 await Isolate.spawn(dataLoader, receivePort.sendPort);8384 // The 'echo' isolate sends its SendPort as the first message85 SendPort sendPort = await receivePort.first;8687 List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");8889 setState(() {90 widgets = msg;91 });92 }9394// the entry point for the isolate95 static dataLoader(SendPort sendPort) async {96 // Open the ReceivePort for incoming messages.97 ReceivePort port = ReceivePort();9899 // Notify any other isolates what port this isolate listens to.
100 sendPort.send(port.sendPort);
101
102 await for (var msg in port) {
103 String data = msg[0];
104 SendPort replyTo = msg[1];
105
106 String dataURL = data;
107 http.Response response = await http.get(dataURL);
108 // Lots of JSON to parse
109 replyTo.send(json.decode(response.body));
110 }
111 }
112
113 Future sendReceive(SendPort port, msg) {
114 ReceivePort response = ReceivePort();
115 port.send([msg, response.sendPort]);
116 return response.first;
117 }
118}
3.3 如何发起网络请求?
在 Flutter 里,想要构造网络请求十分简单,直接使用 http 库即可。它把你可能要实现的网络操作进行了抽象封装,让处理网络请求变得十分简单。
要使用 http 库,需要在 pubspec.yaml
中把它添加为依赖:
1dependencies:
2 ...
3 http: ^0.11.3+16
构造网络请求,需要在 async
方法 http.get()
中调用 await
:
1import 'dart:convert';23import 'package:flutter/material.dart';4import 'package:http/http.dart' as http;5[...]6 loadData() async {7 String dataURL = "https://jsonplaceholder.typicode.com/posts";8 http.Response response = await http.get(dataURL);9 setState(() {
10 widgets = json.decode(response.body);
11 });
12 }
13}
3.4 如何展示耗时任务的进度?
在 iOS 中,在后台运行耗时任务时,会使用 UIProgressView
。
在 Flutter 中,应该使用 ProgressIndicator
。它在渲染时通过一个 boolean flag 来控制是否显示进度。在耗时任务开始前,告诉 Flutter 去更新状态,并在任务结束后隐藏。
在下面的例子中,build 函数被分为三个不同的函数。
当 showLoadingDialog()
是 true
(当 widgets.length == 0
),则渲染 ProgressIndicator
。否则,当数据从网络请求中返回时,渲染 ListView
。
1import 'dart:convert';23import 'package:flutter/material.dart';4import 'package:http/http.dart' as http;56void main() {7 runApp(SampleApp());8}9
10class SampleApp extends StatelessWidget {
11 @override
12 Widget build(BuildContext context) {
13 return MaterialApp(
14 title: 'Sample App',
15 theme: ThemeData(
16 primarySwatch: Colors.blue,
17 ),
18 home: SampleAppPage(),
19 );
20 }
21}
22
23class SampleAppPage extends StatefulWidget {
24 SampleAppPage({Key key}) : super(key: key);
25
26 @override
27 _SampleAppPageState createState() => _SampleAppPageState();
28}
29
30class _SampleAppPageState extends State<SampleAppPage> {
31 List widgets = [];
32
33 @override
34 void initState() {
35 super.initState();
36 loadData();
37 }
38
39 showLoadingDialog() {
40 return widgets.length == 0;
41 }
42
43 getBody() {
44 if (showLoadingDialog()) {
45 return getProgressDialog();
46 } else {
47 return getListView();
48 }
49 }
50
51 getProgressDialog() {
52 return Center(child: CircularProgressIndicator());
53 }
54
55 @override
56 Widget build(BuildContext context) {
57 return Scaffold(
58 appBar: AppBar(
59 title: Text("Sample App"),
60 ),
61 body: getBody());
62 }
63
64 ListView getListView() => ListView.builder(
65 itemCount: widgets.length,
66 itemBuilder: (BuildContext context, int position) {
67 return getRow(position);
68 });
69
70 Widget getRow(int i) {
71 return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
72 }
73
74 loadData() async {
75 String dataURL = "https://jsonplaceholder.typicode.com/posts";
76 http.Response response = await http.get(dataURL);
77 setState(() {
78 widgets = json.decode(response.body);
79 });
80 }
81}
四、工程结构、本地化、依赖和资源
4.1 如何在 Flutter 中引入 图片资源?如何处理多分辨率?
在 iOS中,图片和其他资源会被视为不同的资源分别处理,而在 Flutter 中只有资源这一个概念。 iOS 里被放置在 Images.xcasset
文件夹的资源在 Flutter 中都被放置到了 assets 文件夹中。 和 iOS 一样,assets 中可以放置任意类型的文件,而不仅仅是图片。 例如,你可以把一个 JSON 文件放置到 my-assets
文件夹中。
1my-assets/data.json
在 pubspec.yaml
中声明 assets:
1assets:
2 - my-assets/data.json
在代码中通过使用 AssetBundle
访问资源:
1import 'dart:async' show Future;
2import 'package:flutter/services.dart' show rootBundle;
3
4Future<String> loadAsset() async {
5 return await rootBundle.loadString('my-assets/data.json');
6}
对于图片,Flutter 和 iOS 一样遵循了一个简单的基于屏幕密度的格式。
Image assets 可能是 1.0x 2.0x 3.0x
或者其他任意的倍数。而 devicePixelRatio
则 表达了物理分辨率到逻辑分辨率的对照比例。
Assets 可以放在任何属性的文件夹中—Flutter 没有任何预置的文件结构。你需要在 pubspec.yaml
中 声明 assets (包括路径),然后 Flutter 将会识别它们。
例如,要添加一个名为 my_icon.png
的图片到你的 Flutter 工程中,你可以把它存储在 images
文件夹下。 把基础的图片(一倍图)放到 images
文件夹下,然后把其他倍数的图片放置到对应的比例下的子文件夹中:
1images/my_icon.png // Base: 1.0x image
2images/2.0x/my_icon.png // 2.0x image
3images/3.0x/my_icon.png // 3.0x image
接着,在 pubspec.yaml
文件夹中声明这些图片:
1assets:
2 - images/my_icon.jpeg
你可以用 AssetImage
来访问这些图片:
1return AssetImage("images/a_dot_burr.jpeg");
或者在 Image
widget 中直接使用:
1@override
2Widget build(BuildContext context) {
3 return Image.asset("images/my_image.png");
4}
关于更多的细节,请参见 在 Flutter 中添加资源和图片。
4.2 字符串存储在哪里?如何处理本地化?
iOS 里有 Localizable.strings
文件,而 Flutter 则不同,目前并没有关于字符串的处理系统。 目前,最佳的方案就是在静态区声明你的文本,然后进行访问。例如:
1class Strings {
2 static String welcomeMessage = "Welcome To Flutter";
3}
并且这样访问你的字符串:
1Text(Strings.welcomeMessage)
默认情况下,Flutter 只支持美式英语的本地化字符串。如果你需要添加其他语言支持,请引入 flutter_localizations
库。 同时你可能还需要添加 intl
库来使用 i10n 机制,比如 日期/时间的格式化等。
1dependencies:
2 # ...
3 flutter_localizations:
4 sdk: flutter
5 intl: "^0.15.6"
要使用 flutter_localizations
包,还需要在 app widget 中指定 localizationsDelegates
和 supportedLocales
。
1import 'package:flutter_localizations/flutter_localizations.dart';23MaterialApp(4 localizationsDelegates: [5 // Add app-specific localization delegate[s] here6 GlobalMaterialLocalizations.delegate,7 GlobalWidgetsLocalizations.delegate,8 ],9 supportedLocales: [
10 const Locale('en', 'US'), // English
11 const Locale('he', 'IL'), // Hebrew
12 // ... other locales the app supports
13 ],
14 // ...
15)
16
supportedLocales
指定了应用支持的语言,而这些 delegates 则包含了实际的本地化内容。上面的示例 使用了一个 MaterialApp
,所以它既使用了处理基础 widget 本地化的 GlobalWidgetsLocalizations
, 也使用了处理 Material widget 本地化的 MaterialWidgetsLocalizations
。如果你在应用中使用的是 WidgetsApp
,就不需要后者了。注意,这两个 delegates 虽然都包含了“默认”值,但是如果你想要实现本地化,就必须在本地提供一个或多个 delegates 的实现副本。
当初始化的时候,WidgetsApp
(或 MaterialApp
)会根据你提供的 delegates 创建一个 Localizations
widget。 Localizations
widget 可以随时从当前上下文中中获取设备所用的语言,也可以使用 Window.locale
。
要使用本地化资源,使用 Localizations.of()
方法可以访问提供代理的特定本地化类。使用 intl_translation 库解压翻译的副本到 arb 文件,然后在应用中通过 intl
来引用它们。
关于 Flutter 中国际化和本地化的细节内容,请参看 internationalization guide,里面包含有使用和不使用 intl
库的示例代码。
注意在 Flutter 1.0 beta 2 之前,在 Flutter 里定义的资源是不能被原生代码访问的,反之亦然,而原生的资源也是不能在 Flutter 中使用,因为它们都被放在了独立的文件夹中。
4.3 Cocoapods 相当于 Flutter 中的什么?该如何添加依赖?
在 iOS 里,可以通过 Podfile
添加依赖。而 Flutter 使用 Dart 构建系统和 Pub 包管理器来处理依赖。这些工具将原生应用的打包任务分发给相应 Android 或 iOS 构建系统。
如果你的 Flutter 项目 iOS 文件夹中存在 Podfile,那么请仅在里面添加原生平台的依赖。总而言之, 在 Flutter 中使用 pubspec.yaml
来声明外部依赖。你可以通过 Pub 来查找一些优秀的 Flutter 第三方包。
(未完待续)
诚挚邀请大家参与 Flutter 官方的开发者调查,扫描下方二维码,填写调查问卷,Flutter 因你而更优秀:
![](/assets/blank.gif)
给 iOS 开发者的 Flutter 指南(上)相关推荐
- 给 iOS 开发者的 Flutter 指南
目录 Views 视图 What is the equivalent of a UIView in Flutter? UIView 相当于 Flutter 中的什么? How do I update ...
- 给 Android 开发者的 Flutter 指南(上)
这篇文档旨在帮助 Android 开发者通过 Flutter 开发移动应用.如果你了解 Android 框架的基本知识,你就可以使用这篇文档作为 Flutter 开发的快速入门. 你的 Android ...
- flutter 局部状态和全局状态区别_给 Android 开发者的 Flutter 指南
这篇文档旨在帮助 Android 开发者利用既有的 Android 知识来通过 Flutter 开发移动应用.如果你了解 Android 框架的基本知识,你就可以使用这篇文档作为 Flutter 开发 ...
- flutter 局部状态和全局状态区别_Android 开发者遇到 5G、AI,写给 Android 开发者的 Flutter 指南
前言 Flutter 是 Google 用以帮助开发者在 iOS 和 Android 两个平台开发高质量原生 UI 的移动 SDK.Flutter 兼容现有的代码,免费并且开源,在全球开发者中广泛被 ...
- 八天让iOS开发者上手Flutter!(一)
flutter现在是越来越火了,现在作为一个iOS开发,如果你不会flutter都好像不算个正常人似的?而且现在的flutter情况,有点像2012年那会儿刚刚兴起的iOS,Android开发一样,会 ...
- 一个iOS开发者的Flutter“历险记”
1. 官方简介 Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面. 官方介绍: 快速开发: 毫秒级的热重载,修改后,您的应用界面会立即更新.使用丰富的.完 ...
- 作为一个iOS开发者,应该继续钻研SwiftUI还是尝试接触Flutter了呢?
5年 iOS 开发经验,日常爱好学习各种开发语言,对移动端有一线技术持续关注,Swift 狂热粉丝,推动 手淘iOS客户端落地 Swift,SwiftUI 实践者. (关注公众号-app不内卷,可以一 ...
- 让阿里告诉你, iOS开发者为什么要学 Flutter !
2019 年无疑是 Flutter 技术如火如荼发展的一年.每一个移动开发者都在为 Flutter 带来的"快速开发.富有表现力和灵活的 UI.原生性能"的特色和理念而痴狂,从超级 ...
- 开发者分享在PC上制作iOS游戏的经验(下)
开发者分享在PC上制作iOS游戏的经验(下) 发布时间:2013-05-01 09:48:18 Tags: AI, 市场营销, 手势, 测试, 游戏平衡 简介 这时候我们已经创造了一款可行的游戏,即 ...
最新文章
- android profiler 简书,使用AndroidStudio提供的Android Profiler工具和mat进行内存泄漏分析...
- curl 与 httpie 命令
- 可以节约很多代码的几个正则表达式
- python调用mysql数据库sql语句过长有问题吗_python连接MYSQL数据库,调用update语句后无法更新数据,解决...
- 《Windows PowerShell实战指南(第2版)》——1.4 搭建自己的实验环境
- (转)Android IPC机制详解
- GitHub或正式登陆中国!拟在中国设立分公司
- CRT、ATL、MFC 三者介绍和关系
- L1-039 古风排版 (20 分)—团体程序设计天梯赛
- TOMCAT 优化设置
- 全国地级市坐标、名称、编码获取 / 全球城市坐标位置
- Bypass个人原创文章汇总
- 知网下载论文CAJ格式转为PDF格式
- 求逆矩阵的c语言程序,求逆矩阵——C语言
- 美国加拿大结婚证公证及使馆认证流程时间用于国内法院离婚
- 英语esl语言课程等级105c,说一下英语ESL的等级
- close函数 qt_QT中关闭应用程序和窗口的函数(quit(),exit()以及close()的区别)
- 关于51CTO被脱裤,几点关于密码的建议
- 基于 SpringMVC 的 POST 提交表单出现 405 错误的解决方法之一
- 氟胶耐腐蚀油罐泵出口应用性能