给 iOS 开发者的 Flutter 指南(上)

297 阅读28分钟
原文链接: mp.weixin.qq.com

这篇文章是为那些想将已有的 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 进行描述时,StatefulWidget  会是很有用的。

举个例子,在 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  @override 4  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}1415class SampleAppPage extends StatefulWidget {16  SampleAppPage({Key key}) : super(key: key);1718  @override19  _SampleAppPageState createState() => _SampleAppPageState();20}2122class _SampleAppPageState extends State<SampleAppPage> {23  // Default placeholder text24  String textToShow = "I Like Flutter";25  void _updateText() {26    setState(() {27      // update the text28      textToShow = "Flutter is Awesome!";29    });30  }31  @override32  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@override 2Widget 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  @override 4  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}1415class SampleAppPage extends StatefulWidget {16  SampleAppPage({Key key}) : super(key: key);1718  @override19  _SampleAppPageState createState() => _SampleAppPageState();20}2122class _SampleAppPageState extends State<SampleAppPage> {23  // Default value for toggle24  bool toggle = true;25  void _toggle() {26    setState(() {27      toggle = !toggle;28    });29  }3031  _getToggleChild() {32    if (toggle) {33      return Text('Toggle One');34    } else {35      return CupertinoButton(36        onPressed: () {},37        child: Text('Toggle Two'),38      );39    }40  }4142  @override43  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  @override 4  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}1415class MyFadeTest extends StatefulWidget {16  MyFadeTest({Key key, this.title}) : super(key: key);1718  final String title;1920  @override21  _MyFadeTest createState() => _MyFadeTest();22}2324class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {25  AnimationController controller;26  CurvedAnimation curve;2728  @override29  void initState() {30    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);31    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);32  }3334  @override35  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  }5960  @override61  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); 3 4  final List<Offset> points; 5 6  void paint(Canvas canvas, Size size) { 7    var paint = Paint() 8      ..color = Colors.black 9      ..strokeCap = StrokeCap.round10      ..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  }1617  bool shouldRepaint(SignaturePainter other) => other.points != points;18}1920class Signature extends StatefulWidget {21  SignatureState createState() => SignatureState();22}2324class SignatureState extends State<Signature> {2526  List<Offset> _points = <Offset>[];2728  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; 3 4  CustomButton(this.label); 5 6  @override 7  Widget build(BuildContext context) { 8    return RaisedButton(onPressed: () {}, child: Text(label)); 9  }10}

像你使用其他 Flutter 的 widget 一样,下面我们使用 CustomButton

1@override2Widget 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'; 2 3import 'package:flutter/material.dart'; 4import 'package:http/http.dart' as http; 5 6void main() { 7  runApp(SampleApp()); 8} 910class SampleApp extends StatelessWidget {11  @override12  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}2223class SampleAppPage extends StatefulWidget {24  SampleAppPage({Key key}) : super(key: key);2526  @override27  _SampleAppPageState createState() => _SampleAppPageState();28}2930class _SampleAppPageState extends State<SampleAppPage> {31  List widgets = [];3233  @override34  void initState() {35    super.initState();3637    loadData();38  }3940  @override41  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  }5253  Widget getRow(int i) {54    return Padding(55      padding: EdgeInsets.all(10.0),56      child: Text("Row ${widgets[i]["title"]}")57    );58  }5960  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); 4 5  // The 'echo' isolate sends its SendPort as the first message 6  SendPort sendPort = await receivePort.first; 7 8  List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts"); 910  setState(() {11    widgets = msg;12  });13}1415// The entry point for the isolate16static dataLoader(SendPort sendPort) async {17  // Open the ReceivePort for incoming messages.18  ReceivePort port = ReceivePort();1920  // Notify any other isolates what port this isolate listens to.21  sendPort.send(port.sendPort);2223  await for (var msg in port) {24    String data = msg[0];25    SendPort replyTo = msg[1];2627    String dataURL = data;28    http.Response response = await http.get(dataURL);29    // Lots of JSON to parse30    replyTo.send(json.decode(response.body));31  }32}3334Future 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';  2  3import 'package:flutter/material.dart';  4import 'package:http/http.dart' as http;  5import 'dart:async';  6import 'dart:isolate';  7  8void main() {  9  runApp(SampleApp()); 10} 11 12class SampleApp extends StatelessWidget { 13  @override 14  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} 24 25class SampleAppPage extends StatefulWidget { 26  SampleAppPage({Key key}) : super(key: key); 27 28  @override 29  _SampleAppPageState createState() => _SampleAppPageState(); 30} 31 32class _SampleAppPageState extends State<SampleAppPage> { 33  List widgets = []; 34 35  @override 36  void initState() { 37    super.initState(); 38    loadData(); 39  } 40 41  showLoadingDialog() { 42    if (widgets.length == 0) { 43      return true; 44    } 45 46    return false; 47  } 48 49  getBody() { 50    if (showLoadingDialog()) { 51      return getProgressDialog(); 52    } else { 53      return getListView(); 54    } 55  } 56 57  getProgressDialog() { 58    return Center(child: CircularProgressIndicator()); 59  } 60 61  @override 62  Widget build(BuildContext context) { 63    return Scaffold( 64        appBar: AppBar( 65          title: Text("Sample App"), 66        ), 67        body: getBody()); 68  } 69 70  ListView getListView() => ListView.builder( 71      itemCount: widgets.length, 72      itemBuilder: (BuildContext context, int position) { 73        return getRow(position); 74      }); 75 76  Widget getRow(int i) { 77    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}")); 78  } 79 80  loadData() async { 81    ReceivePort receivePort = ReceivePort(); 82    await Isolate.spawn(dataLoader, receivePort.sendPort); 83 84    // The 'echo' isolate sends its SendPort as the first message 85    SendPort sendPort = await receivePort.first; 86 87    List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts"); 88 89    setState(() { 90      widgets = msg; 91    }); 92  } 93 94// the entry point for the isolate 95  static dataLoader(SendPort sendPort) async { 96    // Open the ReceivePort for incoming messages. 97    ReceivePort port = ReceivePort(); 98 99    // Notify any other isolates what port this isolate listens to.100    sendPort.send(port.sendPort);101102    await for (var msg in port) {103      String data = msg[0];104      SendPort replyTo = msg[1];105106      String dataURL = data;107      http.Response response = await http.get(dataURL);108      // Lots of JSON to parse109      replyTo.send(json.decode(response.body));110    }111  }112113  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'; 2 3import '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'; 2 3import 'package:flutter/material.dart'; 4import 'package:http/http.dart' as http; 5 6void main() { 7  runApp(SampleApp()); 8} 910class SampleApp extends StatelessWidget {11  @override12  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}2223class SampleAppPage extends StatefulWidget {24  SampleAppPage({Key key}) : super(key: key);2526  @override27  _SampleAppPageState createState() => _SampleAppPageState();28}2930class _SampleAppPageState extends State<SampleAppPage> {31  List widgets = [];3233  @override34  void initState() {35    super.initState();36    loadData();37  }3839  showLoadingDialog() {40    return widgets.length == 0;41  }4243  getBody() {44    if (showLoadingDialog()) {45      return getProgressDialog();46    } else {47      return getListView();48    }49  }5051  getProgressDialog() {52    return Center(child: CircularProgressIndicator());53  }5455  @override56  Widget build(BuildContext context) {57    return Scaffold(58        appBar: AppBar(59          title: Text("Sample App"),60        ),61        body: getBody());62  }6364  ListView getListView() => ListView.builder(65      itemCount: widgets.length,66      itemBuilder: (BuildContext context, int position) {67        return getRow(position);68      });6970  Widget getRow(int i) {71    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));72  }7374  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;34Future<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 image2images/2.0x/my_icon.png  // 2.0x image3images/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@override2Widget 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: flutter5  intl: "^0.15.6"

要使用 flutter_localizations  包,还需要在 app widget 中指定 localizationsDelegates  和 supportedLocales

 1import 'package:flutter_localizations/flutter_localizations.dart'; 2 3MaterialApp( 4 localizationsDelegates: [ 5   // Add app-specific localization delegate[s] here 6   GlobalMaterialLocalizations.delegate, 7   GlobalWidgetsLocalizations.delegate, 8 ], 9 supportedLocales: [10    const Locale('en', 'US'), // English11    const Locale('he', 'IL'), // Hebrew12    // ... other locales the app supports13  ],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 因你而更优秀:

▼ 往期精彩回顾 ▼ 给前端工程师的 Flutter 指南 Google 工程师中文演讲 | 深入了解 Flutter 的高性能图形渲染 未来可期:Google I/O 大会的首个 Flutter 演讲议题公布 戳“阅读原文”一起来充电吧!