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 就會得到更新。

StatefulWidgetStatelessWidget 最重要的區別就是,StatefulWidget 中有一個 State對象,它用來存儲一些狀態的信息,並在整個生命週期內保持不變。

如果你對此還存有疑慮,記住一點:如果一個 widget 在 build 方法之外(比如運行時下發生用戶點擊事件)被修改,那麼就應該是有狀態的。如果一個 widget 一旦生成就不再發生改變,那麼它就是無狀態的。然而,即使一個 widget 是有狀態的,如果不是自身直接響應修改(或別的輸入),那麼他的父容器也可以是無狀態的。

下面是如何使用 StatelessWidget 的示例。Text 是一個常用的 StatelessWidget。如果你看了 Text 的源代碼,就會發現它繼承於 StatelessWidget

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

如上述代碼所示, Text 沒有攜帶任何狀態。它只會渲染初始化時傳入內容。

如果你希望在點擊 FloatingActionButtonI like Flutter 能產生動態的改變,只需要把 Text 放到 StatefulWidget 中,並在用戶點擊按鈕時更新它即可。

下面是示例代碼:

 class SampleApp extends StatelessWidget {
   // This widget is the root of your application.
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       title: 'Sample App',
       theme: ThemeData(
         primarySwatch: Colors.blue,
       ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text
  String textToShow = "I Like Flutter";
  void _updateText() {
    setState(() {
      // update the text
      textToShow = "Flutter is Awesome!";
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

1.3 如何對 widget 佈局? Storyboard 在哪?

在 iOS 開發中,你可能會經常使用 Storyboard 來組織你的視圖,並直接通過 Storyboard 或者 在 ViewController 中通過代碼來設置約束。而在 Flutter 中,你要通過代碼來對 widget 進行 組織來形成一個 widget 樹狀結構。

下面的例子展示瞭如何展示一個帶有 padding 的 widget:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: CupertinoButton(
        onPressed: () {
          setState(() { _pressedCount += 1; });
        },
        child: Text('Hello'),
        padding: EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}

你可以爲任何 widget 添加 padding,來達到類似在 iOS 中視圖約束的作用。

你可以在widget 目錄中查看 Flutter 提供 的所有 widget 佈局方法。

1.4 如何添加或移除一個組件?

在 iOS 中,你可以通過調用父視圖的 addSubview() 方法或者 removeFromSuperview() 方法 來動態的添加或移除視圖。

在 Flutter 中,因爲 widget 是不可變的,所以沒有提供直接同 addSubview() 作用相同的方法。但是你可以通過向父視圖傳遞一個返回值是 widget 的方法,並通過一個 boolean flag 來控制子視圖的存在。

下面的例子中像你展示瞭如何讓用戶通過點擊 FloatingActionButton 按鈕來達到在兩個 widget 中切換的目的:

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return CupertinoButton(
        onPressed: () {},
        child: Text('Toggle Two'),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

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 效果:

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyFadeTest createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Container(
          child: FadeTransition(
            opacity: curve,
            child: FlutterLogo(
              size: 100.0,
            )
          )
        )
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        child: Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }

  @override
  dispose() {
    controller.dispose();
    super.dispose();
  }
}

關於更多的內容,可以查看Animation 和 Motion widgetsAnimations 教程, 以及Animations 概覽

1.6 如何渲染到屏幕上?

在 iOS 裏,可以使用 CoreGraphics 繪製線條和圖形到屏幕上。Flutter 裏有一套基於 Canvas實現的 API,有兩個類可以幫助你進行繪製:CustomPaintCustomPainter,後者實現了繪製圖形到 canvas 的算法。

想要學習在 Flutter 裏如何實現一個畫筆,可以查看 Collin 在 StackOverflow 裏的回答。

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);

  final List<Offset> points;

  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }

  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {

  List<Offset> _points = <Offset>[];

  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
    );
  }
}

1.7 如何設置視圖 Widget 的透明度?

在 iOS 裏,視圖都有一個 opacity 或者 alpha 屬性。而在 Flutter 裏,大部分時候你都需要封裝 widget 到一個 Opacity widget 中來實現這一功能。

1.8 如何構建自定義 widgets?

在 iOS 裏,你可以直接繼承 UIView 或者使用已經存在的視圖,然後重寫並實現對應的方法來達到想要的效果。在 Flutter 裏,構建自定義 widget 需要通過合成一些小的 widget(而不是對它們進行擴展)來實現。

例如,如果你要構建一個 CustomButton,並在構造器中傳入它的文本標籤?那就組合 RaisedButton 和文本標籤,而不是繼承 RaisedButton

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(onPressed: () {}, child: Text(label));
  }
}

像你使用其他 Flutter 的 widget 一樣,下面我們使用 CustomButton

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}

二、導航

2.1 如何在不同頁面之間切換?

在 iOS 裏,想要在多個 viewcontroller 中切換,可以使用 UINavigationController 管理 viewcontroller 構成的棧進行顯示。

在 Flutter 中,使用 NavigatorRoutes 也可以實現類似的功能。一個 Routes 是應用中屏幕或者頁面的抽象概念,而一個 Navigator 是管多個 Route 的 widget。

可以把 Route 理解爲 UIViewController。而 Navigator 的工作方式和 iOS 的 UINavigationController 類似,當你想要進入或退出一個新頁面的時候,它也可以進行 push()pop() 操作。

想要在不同頁面間跳轉,你有兩個選擇:

1.構建由 route 名稱組成的 Map(MaterialApp)

2.直接跳轉到一個 route(WidgetApp)

下面的示例構建了一個 Map

void main() {
  runApp(CupertinoApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}

通過把 route 的名稱 push 給一個 Navigator 來跳轉:

Navigator.of(context).pushNamed('/b');

Navigator 類不僅用來處理 Flutter 中的路由,還被用來獲取你剛 push 到棧中的路由返回的結果。通過 await 等待路由返回的結果來達到這點。

舉個例子,要跳轉到“位置”路由來讓用戶選擇一個地點,你可能要這麼做:

Navigator 類對 Flutter 中的路由事件做處理,還可以用來獲取入棧之後的路由的結果。這需要通過 push() 返回的 Future 中的await 來實現。

例如,要打開一個“定位”頁面來讓用戶選擇他們的位置,你需要做如下事情:

Map coordinates = await Navigator.of(context).pushNamed('/location');

然後,在”定位“頁面中,一旦用戶選擇了自己的定位,就 pop() 出棧並返回結果。

Navigator.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 代碼:

UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
  if ([viewController isKindOfClass:[UINavigationController class]]) {
    [((UINavigationController*)viewController) popViewControllerAnimated:NO];
  }

三、線程和異步

3.1 如何編寫異步代碼?

Dart 是單線程執行模型,支持 Isolate(一種在其他線程運行 Dart 代碼的方法)、事件循環和異步編程。 除非生成了 Isolate,否則所有 Dart 代碼將永遠在主 UI 線程運行,並由事件循環驅動。Flutter 中的事件循環類似於 iOS 中的 main loop—,也就是主線程上的 Looper

Dart 的單線程模型並不意味着你需要以阻塞 UI 的形式來執行代碼,相反,你更應該使用 Dart 語言提供的異步功能, 比如使用 async / await 來實現異步操作。

例如,你可以使用 async / await 來執行網絡代碼以避免 UI 掛起,讓 Dart 來完成這個繁重的任務:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

一旦 await 等待的網絡操作結束,通過調用 setState() 來更新 UI,這將會觸發 widget 子樹的重新構建並更新數據。

下面的示例展示瞭如何異步加載數據,並在 ListView 中展示出來:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();

    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
          itemCount: widgets.length,
          itemBuilder: (BuildContext context, int position) {
            return getRow(position);
          }));
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

更多關於在後臺工作的信息,以及 Flutter 和 iOS 的區別,請參考下一章節。

3.2 如何讓你的工作在後臺線程執行?

由於 Flutter 是單線程模型,而且執行着一個 event loop(就像 Node.js),你不需要爲線程管理或 是開啓後臺線程操心。如果你在處理 I/O 操作,例如磁盤訪問或網絡請求,那麼你安全地使用 async / await 就可以了。但是,如果你需要大量的計算來讓 CPU 保持忙碌狀態,你需要使用 Isolate 來防治阻塞 event loop。

對於 I/O 操作,把方法聲明爲 async 方法,然後通過 await 來等待異步方法的執行完成:

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

這就是處理網絡或數據庫請求等 I/O 操作的經典做法。

然而,有時候你需要處理大量的數據,從而導致 UI 掛起。在 Flutter 裏,當處理長期運行或者運算密集的任務時,可以使用 Isolate 來發揮出多核 CPU 的優勢。

Isolates 是相互隔離的執行線程,並不和主線程共享內存。這意味着你不能夠訪問主線程的變量,也不能 使用 setState() 來更新 UI。Isolates 正如起字面意思是不能共享內存(例如靜態變量表)的。

下面的例子展示了在一個簡單的 isolate 中,如何把數據推到主線程上用來更新 UI:

loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message
  SendPort sendPort = await receivePort.first;

  List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");

  setState(() {
    widgets = msg;
  });
}

// The entry point for the isolate
static dataLoader(SendPort sendPort) async {
  // Open the ReceivePort for incoming messages.
  ReceivePort port = ReceivePort();

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    String data = msg[0];
    SendPort replyTo = msg[1];

    String dataURL = data;
    http.Response response = await http.get(dataURL);
    // Lots of JSON to parse
    replyTo.send(json.decode(response.body));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

在這裏,dataLoader() 就是運行在獨立線程上的 Isolate。在 Isolate 中,你可以處理 CPU 密集型任務(如解析一個 龐大的 JSON 文件),或者處理複雜的數學運算,比如加密操作或者信號處理等。

下面是一個完整示例:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    if (widgets.length == 0) {
      return true;
    }

    return false;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message
    SendPort sendPort = await receivePort.first;

    List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");

    setState(() {
      widgets = msg;
    });
  }

// the entry point for the isolate
  static dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);

    await for (var msg in port) {
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(json.decode(response.body));
    }
  }

  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

3.3 如何發起網絡請求?

在 Flutter 裏,想要構造網絡請求十分簡單,直接使用 http 庫即可。它把你可能要實現的網絡操作進行了抽象封裝,讓處理網絡請求變得十分簡單。

要使用 http 庫,需要在 pubspec.yaml 中把它添加爲依賴:

dependencies:
  ...
  http: ^0.11.3+16

構造網絡請求,需要在 async 方法 http.get() 中調用 await

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

3.4 如何展示耗時任務的進度?

在 iOS 中,在後臺運行耗時任務時,會使用 UIProgressView

在 Flutter 中,應該使用 ProgressIndicator。它在渲染時通過一個 boolean flag 來控制是否顯示進度。在耗時任務開始前,告訴 Flutter 去更新狀態,並在任務結束後隱藏。

在下面的例子中,build 函數被分爲三個不同的函數。

showLoadingDialog()true (當 widgets.length == 0),則渲染 ProgressIndicator。否則,當數據從網絡請求中返回時,渲染 ListView

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    return widgets.length == 0;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

四、工程結構、本地化、依賴和資源

4.1 如何在 Flutter 中引入 圖片資源?如何處理多分辨率?

在 iOS中,圖片和其他資源會被視爲不同的資源分別處理,而在 Flutter 中只有資源這一個概念。 iOS 裏被放置在 Images.xcasset 文件夾的資源在 Flutter 中都被放置到了 assets 文件夾中。 和 iOS 一樣,assets 中可以放置任意類型的文件,而不僅僅是圖片。 例如,你可以把一個 JSON 文件放置到 my-assets 文件夾中。

my-assets/data.json

pubspec.yaml 中聲明 assets:

assets:
 - my-assets/data.json

在代碼中通過使用 AssetBundle 訪問資源:

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('my-assets/data.json');
}

對於圖片,Flutter 和 iOS 一樣遵循了一個簡單的基於屏幕密度的格式。

Image assets 可能是 1.0x 2.0x 3.0x 或者其他任意的倍數。而 devicePixelRatio 則 表達了物理分辨率到邏輯分辨率的對照比例。

Assets 可以放在任何屬性的文件夾中—Flutter 沒有任何預置的文件結構。你需要在 pubspec.yaml 中 聲明 assets (包括路徑),然後 Flutter 將會識別它們。

例如,要添加一個名爲 my_icon.png 的圖片到你的 Flutter 工程中,你可以把它存儲在 images文件夾下。 把基礎的圖片(一倍圖)放到 images 文件夾下,然後把其他倍數的圖片放置到對應的比例下的子文件夾中:

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

接着,在 pubspec.yaml 文件夾中聲明這些圖片:

assets:
 - images/my_icon.jpeg

你可以用 AssetImage 來訪問這些圖片:

return AssetImage("images/a_dot_burr.jpeg");

或者在 Image widget 中直接使用:

@override
Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}

關於更多的細節,請參見 在 Flutter 中添加資源和圖片。

4.2 字符串存儲在哪裏?如何處理本地化?

iOS 裏有 Localizable.strings 文件,而 Flutter 則不同,目前並沒有關於字符串的處理系統。 目前,最佳的方案就是在靜態區聲明你的文本,然後進行訪問。例如:

class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

並且這樣訪問你的字符串:

Text(Strings.welcomeMessage)

默認情況下,Flutter 只支持美式英語的本地化字符串。如果你需要添加其他語言支持,請引入 flutter_localizations 庫。 同時你可能還需要添加 intl 庫來使用 i10n 機制,比如 日期/時間的格式化等。

 dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"

要使用 flutter_localizations 包,還需要在 app widget 中指定 localizationsDelegatessupportedLocales

import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
 localizationsDelegates: [
   // Add app-specific localization delegate[s] here
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // English
    const Locale('he', 'IL'), // Hebrew
    // ... other locales the app supports
  ],
  // ...
)

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 第三方包。

五、ViewControllers

5.1 ViewController 相當於 Flutter 中的什麼?

在 iOS 裏,一個 ViewController 是用戶界面的一部分,通常是作爲屏幕或者其中的一部分來使用。 這些組合在一起構成了複雜的用戶界面,並以此對應用的 UI 做不斷的擴充。 在 Flutter 中,這一任務又落到了 Widget 這裏。就像在導航那一章提到的, Flutter 中的屏幕也是使用 Widgets 表示的,因爲“萬物皆 widget!”。使用 Naivgator 在不同的 Route 之間切換,而不同的路由則代表了不同的屏幕或頁面,或是不同的狀態,也可能是渲染相同的數據。

5.2 如何監聽 iOS 中的生命週期?

在 iOS 裏,可以重寫 ViewController 的方法來捕獲自身的生命週期,或者在 AppDelegate 中註冊生命 週期的回調。Flutter 中則沒有這兩個概念,但是你可以通過在 WidgetsBinding 的 observer 中掛鉤子,也可以 通過監聽didChangeAppLifecycleState() 事件,來實現相應的功能。

可監聽的生命週期事件有:

  • inactive - 應用當前處於不活躍狀態,不接收用戶輸入事件。這個事件只在 iOS 上有效,Android 中沒有類似的狀態。
  • paused - 應用處於用戶不可見狀態,不接收用戶輸入事件,但仍在後臺運行。
  • resumed - 應用可見,也響應用戶輸入。
  • suspending - 應用被掛起,在 iOS 平臺沒有這一事件。

更多細節,請參見 AppLifecycleStatus文檔

六、佈局

6.1 UITableView 和 UICollectionView 相當於 Flutter 中的什麼?

在 iOS 裏,你可能使用 UITableView 或者 UICollectionView 來展示一個列表。而在 Flutter 裏,你可以使用 ListView 來達到類似的實現。在 iOS 中,你通過 delegate 方法來確定顯示的行數,相應位置的 cell,以及 cell 的尺寸。

由於 Flutter 中 widget 的不可變特性,你需要向 ListView 傳遞一個 widget 列表,Flutter 會確保滾動快速而流暢。

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
    }
    return widgets;
  }
}

6.2 如何確定列表中被點擊的元素?

在 iOS 中,tableView:didSelectRowAtIndexPath: 代理方法可以用來實現該功能。而在 Flutter 中,需要通過 widget 傳遞進來的 touch 響應處理來實現。

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(GestureDetector(
        child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i"),
        ),
        onTap: () {
          print('row tapped');
        },
      ));
    }
    return widgets;
  }
}

6.3 如何動態更新?

在 iOS 中,可以更新列表數據,調用 reloadData 方法通知 tableView 或 collectionView。

在 Flutter 裏,如果你在 setState() 中更新了 widget 列表,你會發現展示的數據並不會立刻更新。這是因爲當 setState() 被調用時,Flutter 的渲染引擎回去檢索 widget 樹是否有改變。當它獲取到 ListView,會進行 == 判斷,然後發現兩個 ListView 是相等的。沒發現有改變,所以也就不會進行更新。

更新 ListView 簡單的方法是在 setState() 創建一個新的 List,然後拷貝舊列表中的所有數據到新列表。這樣雖然簡單,但是像下面示例一樣數據量很大時,並不推薦這樣做。

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

一個高效且有效的方法是使用 ListView.Builder 來構建列表。當你的數據量很大,且需要構建動態列表時,這個方法會非常好用。

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
        itemCount: widgets.length,
        itemBuilder: (BuildContext context, int position) {
          return getRow(position);
        },
      ),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

與創建 ListView 不同,創建 ListView.Builder 需要兩個關鍵參數:初始化列表長度和 ItemBuilder 函數。

ItemBuilder 方法和 cellForItemAt 代理方法非常類似,它接收位置參數,然後返回想要在該位置渲染的 cell。

最後,也是最重要的,注意 onTap() 方法並沒有重新創建列表,而是使用 .add 方法進行添加。

6.4 ScrollView 相當於 Flutter 裏的什麼?

在 iOS 中,把視圖放在 ScrollView 裏來允許用戶在需要時滾動內容。

在 Flutter 中,使用 ListView widget 是最簡單的辦法。它和 iOS 中 ScrollViewTableView 表現一致,也可以給它的 widget 做垂直排版。

@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

關於 Flutter 中排布的更多細節,請參閱 佈局教程

七、手勢檢測與 touch 事件處理

7.1 如何給 Flutter 的 widget 添加點擊事件?

在 iOS 中,通過把 GestureRecognizer 綁定給 UIView 來處理點擊事件。在 Flutter 中, 有兩種方法來添加事件監聽者:

1. 如果 widget 本身支持事件檢測,則直接傳遞處理函數給它。例如,RaisedButton 擁有 一個 onPressed 參數:

@override
Widget build(BuildContext context) {
  return RaisedButton(
    onPressed: () {
      print("click");
    },
    child: Text("Button"),
  );
}

2. 如果 widget 本身不支持事件檢測,那麼把它封裝到一個 GestureDetector 中,並給它的 onTap 參數傳遞一個函數:

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: FlutterLogo(
            size: 200.0,
          ),
          onTap: () {
            print("tap");
          },
        ),
      ),
    );
  }
}

7.2 我怎麼處理 widget 上的其他手勢?

你可以使用 GestureDetector 來監聽更多的手勢,例如:

  • 單擊事件

  • onTapDown —— 用戶在特定區域發生點觸屏幕的一個即時操作。

  • onTapUp —— 用戶在特定區域發生觸摸擡起的一個即時操作。

  • onTap —— 從點觸屏幕之後到觸摸擡起之間的單擊操作。

  • onTapCancel —— 用戶在之前觸發了 onTapDown 時間,但未觸發 tap 事件。

  • 雙擊事件

  • onDoubleTap —— 用戶在同一位置發生快速點擊屏幕兩次的操作。

  • 長按事件

  • onLongPress —— 用戶在同一位置長時間觸摸屏幕的操作。

  • 垂直拖動事件

  • onVerticalDragStart —— 用戶手指接觸屏幕,並且將要進行垂直移動事件。

  • onVerticalDragUpdate —— 用戶手指接觸屏幕,已經開始垂直移動,且會持續進行移動。

  • onVerticalDragEnd —— 用戶之前手指接觸了屏幕併發生了垂直移動操作,並且停止接觸前還在以一定的速率移動。

  • 水平拖動事件

  • onHorizontalDragStart —— 用戶手指接觸屏幕,並且將要進行水平移動事件。

  • onHorizontalDragUpdate —— 用戶手指接觸屏幕,已經開始水平移動,且會持續進行移動。

  • onHorizontalDragEnd —— 用戶之前手指接觸了屏幕併發生了水平移動操作,並且停止接觸前還在以一定的速率移動。

下面的示例展示了 GestureDetector 是如何實現雙擊時旋轉 Flutter 的 logo 的:

AnimationController controller;
CurvedAnimation curve;

@override
void initState() {
  controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: RotationTransition(
            turns: curve,
            child: FlutterLogo(
              size: 200.0,
            )),
          onDoubleTap: () {
            if (controller.isCompleted) {
              controller.reverse();
            } else {
              controller.forward();
            }
          },
        ),
      ),
    );
  }
}

八、主題和文字

8.1 如何設置應用主題?

Flutter 實現了一套漂亮的 Material Design 組件,而且開箱可用,它提供了許多常用的樣式和主題。

爲了充分發揮應用中 Material Components 的優勢,聲明一個頂級的 widget,MaterialApp,來作爲你的應用 入口。MaterialApp 是一個封裝了大量常用 Material Design 組件的 widget。它基於 WidgetsApp 添加了 Material 的 相關功能。

但是 Flutter 有足夠的靈活性和表現力來實現任何設計語言。在 iOS 上,可以使 用Cupertino library來 製作遵循Human Interface Guidelines的 界面。關於這些 widget 的全部集合,可以參看Cupertino widgets gallery

也可以使用 WidgetApp 來做爲應用入口,它提供了一部分類似的功能接口,但是不如 MaterialApp 強大。

定義所有子組件顏色和樣式,可以直接傳遞 ThemeData 對象給 MaterialApp widget。例如,在下面的代碼中,primary swatch 被設置爲藍色,而文本選中後的顏色被設置爲紅色:

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textSelectionColor: Colors.red
      ),
      home: SampleAppPage(),
    );
  }
}

8.2 如何給 Text widget 設置自定義字體?

在 iOS 裏,可以在項目中引入任何的 ttf 字體文件,並在 info.plist 文件中聲明並進行引用。在 Flutter 裏,把字體放到一個文件夾中,然後在 pubspec.yaml 文件中引用它,就和引用圖片一樣。

fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

然後在 Text widget 中指定字體:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

8.3 我怎麼給我的 Text widget 設置樣式?

除了字體以外,你也可以自定義 Text widget 的其他樣式。Text widget 接收一個 TextStyle對象的參數,可以指定很多參數,例如:

  • color
  • decoration
  • decorationColor
  • decorationStyle
  • fontFamily
  • fontSize
  • fontStyle
  • fontWeight
  • hashCode
  • height
  • inherit
  • letterSpacing
  • textBaseline
  • wordSpacing

九、表單輸入

9.1 Flutter 中如何使用表單?我怎麼拿到用戶的輸入?

我們知道 Flutter 使用的是不可變而且狀態分離的 widget,你可能會好奇這種情況下如何處理用戶的輸入。在 iOS 上,一般會在提交數據時查詢當前組件的數值或動作。那麼在 Flutter 中會怎麼樣呢?

和 Flutter 的其他部分一樣,表單處理要通過特定的 widget 來實現。如果你有一個 TextField或者 TextFormField, 你可以通過 TextEditingController 來 獲取用戶的輸入:

class _MyFormState extends State<MyForm> {
  // Create a text controller and use it to retrieve the current value.
  // of the TextField!
  final myController = TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when disposing of the Widget.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Retrieve Text Input'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: TextField(
          controller: myController,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // When the user presses the button, show an alert dialog with the
        // text the user has typed into our text field.
        onPressed: () {
          return showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // Retrieve the text the user has typed in using our
                // TextEditingController
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: Icon(Icons.text_fields),
      ),
    );
  }
}

你在Flutter CookbookRetrieve the value of a text field中可以找到更多的相關內容以及詳細的代碼列表。

9.2 Text field 中的 placeholder 相當於什麼?

在 Flutter 裏,通過向 Text widget 傳遞一個 InputDecoration 對象,你可以輕易的顯示文本框的提示信息,或是 placeholder。

body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: "This is a hint"),
  ),
)

9.3 如何展示驗證錯誤信息?

就和顯示提示信息一樣,你可以通過向 Text widget 傳遞一個 InputDecoration 來實現。

然而,你並不想在一開始就顯示錯誤信息。相反,在用戶輸入非法數據後,應該更新狀態,並傳遞一個新的 InputDecoration 對象。

 class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
        ),
      ),
    );
  }

  _getErrorText() {
    return _errorText;
  }

  bool isEmail(String emailString) {
    String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(emailString);
  }
}

十、和硬件、第三方服務以及系統平臺交互

10.1 如何與系統平臺以及平臺原生代碼進行交互?

Flutter 並不直接在平臺上運行代碼;而是以 Dart 代碼的方式原生運行於設備之上,這算是繞過了平臺的 SDK 的限制。 這意味着,例如,你用 Dart 發起了一個網絡請求,它會直接在 Dart 的上下文中運行。 你不需要調用寫 iOS 或者 Android 原生應用時常用的 API 接口。你的 Flutter 應用仍舊被原生平臺 的 ViewController 當做一個 view 來管理,但是你不能夠直接訪問 ViewController 自身或是對應的原生框架。

這並不意味着 Flutter 應用不能夠和原生 API,或是原生代碼進行交互。Flutter 提供了用來和宿主 ViewController 通信 和交換數據的 platform channels。 platform channels 本質上是一個橋接了 Dart 代碼與宿主 ViewController 和 iOS 框架的異步通信模型。你可以通過 platform channels 來執行原生代碼的方法,或者獲取設備的傳感器信息等數據。

除了直接使用 platform channels 之外,也可以使用一系列包含了原生代碼和 Dart 代碼,實現了特定功能的現有<u style="text-decoration: none; border-bottom: 1px dashed grey;">插件</u>。例如,你在 Flutter 中可以直接使用插件來訪問相冊或是設備攝像頭,而不需要自己重新集成。Pub 是一個 Dart 和 Flutter 的開源包倉庫,你可以在這裏找到需要的插件。有些包可能支持集成 iOS 或 Android,或兩者皆有。

如果你在 Pub 找不到自己需要的包,你可以自己寫一個, 並發佈到 Pub 上

10.2 如何訪問 GPS 傳感器?

使用 geolocator 插件,這一插件由社區提供。

10.3 如何訪問攝像頭?

image_picker 是常用的訪問相機的插件。

10.4 我怎麼登錄 Facebook?

登錄 Facebook 可以使用 flutter_facebook_login 插件。

10.5 如何集成 Firebase 功能?

大多數的 Firebase 特性都在 官方維護的插件 中實現了。 這些插件由 Flutter 官方團隊維護:

在 Pub 上你也可以找到一些第三方的 Firebase 插件,主要實現了官方插件沒有直接實現的功能。

10.6 如何構建自己的插件?

如果有一些 Flutter 和遺漏的平臺特性,可以 根據 developing packages and plugins 構建 自己的插件。

Flutter 的插件結構,簡單來說,更像是 Android 中的 Event bus:你發送一個消息,並讓接受者處理並反饋 結果給你。這種情況下,接受者就是在 iOS 或 Android 的原生代碼。

十一、數據庫和本地存儲

11.1 Flutter 中如何訪問 UserDefaults?

在 iOS 裏,可以使用屬性列表存儲一個鍵值對的集合,也就是我們所說的 UserDefaults。

在 Flutter 裏,可以使用 Shared Preferences 插件來實現相同的功能。這個插件封裝了 UserDefaults 以及 Android 裏類似的 SharedPreferences

11.2 CoreData 相當於 Flutter 中的什麼

在 iOS 裏,你可以使用 CoreData 來存儲結構化的數據。這是一個基於 SQL 數據庫的上層封裝,可以使關聯模型的查詢變得更加簡單。

在 Flutter 裏,可以使用 SQFlite 插件來實現這個功能。

十二、通知

12.1 如何設置推送通知?

在 iOS 裏,你需要向開發者中心註冊來允許推送通知。

在 Flutter 裏,使用 firebase_messaging 插件來實現這個功能。

關於 Firebase Cloud Messaging API 的更多信息,可以 查看 firebase_messaging 插件文檔。


文末推薦:iOS熱門文集

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章