Flutter for iOS 開發者

原文:https://flutterchina.club/flutter-for-ios/#views

本文檔適用那些希望將現有 iOS 經驗應用于 Flutter 的開發者。如果你擁有 iOS 開發基礎,那么你可以使用這篇文檔開始學習 Flutter 的開發。

開發 Flutter 時,你的 iOS 經驗和技能將會大有裨益,因為 Flutter 依賴于移動操作系統的眾多功能和配置。Flutter 是用于為移動設備構建用戶界面的全新方式,但它也有一個插件系統用于和 iOS(及 Android)進行非 UI 任務的通信。如果你是 iOS 開發專家,則你不必將 Flutter 徹底重新學習一遍。

你可以將此文檔作為 cookbook,通過跳轉并查找與你的需求最相關的問題。

Views

UIView 相當于 Flutter 中的什么?

在 iOS 中,構建 UI 的過程中將大量使用 view 對象。這些對象都是 UIView 的實例。它們可以用作容器來承載其他的 UIView,最終構成你的界面布局。

在 Flutter 中,你可以粗略地認為 Widget 相當于 UIView 。Widget 和 iOS 中的控件并不完全等價,但當你試圖去理解 Flutter 是如何工作的時候,你可以認為它們是“聲明和構建 UI 的方法”。

然而,Widget 和 UIView 還是有些區別的。首先,widgets 擁有不同的生存時間:它們一直存在且保持不變,直到當它們需要被改變。當 widgets 和它們的狀態被改變時,Flutter 會構建一顆新的 widgets 樹。作為對比,iOS 中的 views 在改變時并不會被重新創建。但是與其說 views 是可變的實例,不如說它們被繪制了一次,并且直到使用 setNeedsDisplay() 之后才會被重新繪制。

此外,不像 UIView,由于不可變性,Flutter 的 widgets 非常輕量。這是因為它們本身并不是什么控件,也不會被直接繪制出什么,而只是 UI 的描述。

Flutter 包含了 Material 組件庫。這些 widgets 遵循了 Material 設計規范。MD 是一個靈活的設計系統,并且為包括 iOS 在內的所有系統進行了優化。

但是用 Flutter 實現任何的設計語言都非常的靈活和富有表現力。在 iOS 平臺,你可以使用 Cupertino widgets 來構建遵循了 Apple’s iOS design language 的界面。

我怎么來更新 Widgets?

在 iOS 上更新 views,只需要直接改變它們就可以了。在 Flutter 中,widgets 是不可變的,而且不能被直接更新。你需要去操縱 widget 的 state。

這也正是有狀態的和無狀態的 widget 這一概念的來源。一個 StatelessWidget 正如它聽起來一樣,是一個沒有附加狀態的 widget。

StatelessWidget 在你構建初始化后不再進行改變的界面時非常有用。

舉個例子,你可能會用一個 UIImageView 來展示你的 logo image 。如果這個 logo 在運行時不會改變,那么你就可以在 Flutter 中使用 StatelessWidget 。

如果你希望在發起 HTTP 請求時,依托接收到的數據動態的改變 UI,請使用 StatefulWidget。當 HTTP 請求結束后,通知 Flutter 框架 widget 的 State 更新了,好讓系統來更新 UI。

有狀態和無狀態的 widget 之間一個非常重要的區別是,StatefulWidget 擁有一個 State 對象來存儲它的狀態數據,并在 widget 樹重建時攜帶著它,因此狀態不會丟失。

如果你有疑惑,請記住以下規則:如果一個 widget 在它的 build 方法之外改變(例如,在運行時由于用戶的操作而改變),它就是有狀態的。如果一個 widget 在一次 build 之后永遠不變,那它就是無狀態的。但是,即便一個 widget 是有狀態的,包含它的父親 widget 也可以是無狀態的,只要父 widget 本身不響應這些變化。

下面的例子展示了如何使用一個 StatelessWidget 。一個常見的 StatelessWidgetText widget。如果你查看 Text 的實現,你會發現它是 StatelessWidget 的子類。

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

閱讀上面的代碼,你可能會注意到 Text widget 并不顯示地攜帶任何狀態。它通過傳入給它的構造器的數據來渲染,除此之外再無其他。

但是,如果你希望 I like Flutter在點擊 FloatingActionButton 時動態的改變呢?

為了實現這個,用 StatefulWidget 包裹 Text 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 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),
      ),
    );
  }
}

我怎么對 widget 布局?我的 Storyboard 在哪?

在 iOS 中,你可能會用 Storyboard 文件來組織 views,并對它們設置約束,或者,你可能在 view controller 中使用代碼來設置約束。在 Flutter 中,你通過編寫一個 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 catalog 中查看 Flutter 提供的布局。

我怎么在我的約束中添加或移除組件?

在 iOS 中,你在父 view 中調用 addSubview() 或在子 view 中調用 removeFromSuperview() 來動態地添加或移除子 views。在 Flutter 中,由于 widget 不可變,所以沒有和 addSubview() 直接等價的東西。作為替代,你可以向 parent 傳入一個返回 widget 的函數,并用一個布爾值來控制子 widget 的創建。

下面這個例子展示了在點擊 FloatingActionButton 時如何動態地切換兩個 widgets:

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),
      ),
    );
  }
}

我怎么對 widget 做動畫?

在 iOS 中,你通過調用 animate(withDuration:animations:) 方法來給一個 view 創建動畫。在 Flutter 中,使用動畫庫來包裹 widgets,而不是創建一個動畫 widget。

在 Flutter 中,使用 AnimationController 。這是一個可以暫停、尋找、停止、反轉動畫的 Animation<double> 類型。它需要一個 Ticker 當 vsync 發生時來發送信號,并且在每幀運行時創建一個介于 0 和 1 之間的線性插值(interpolation)。你可以創建一個或多個的 Animation 并附加給一個 controller。

例如,你可能會用 CurvedAnimation 來實現一個 interpolated 曲線。在這個場景中,controller 是動畫過程的“主人”,而 CurvedAnimation 計算曲線,并替代 controller 默認的線性模式。

當構建 widget 樹時,你會把 Animation 指定給一個 widget 的動畫屬性,比如 FadeTransition 的 opacity,并告訴控制器開始動畫。

下面這個例子展示了在點擊 FloatingActionButton 之后,如何使用 FadeTransition 來讓 widget 淡出到 logo 圖標:

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 widgets, Animations tutorial 以及 Animations overview。

我該怎么繪圖?

在 iOS 上,你通過 CoreGraphics 來在屏幕上繪制線條和形狀。Flutter 有一套基于 Canvas 類的不同的 API,還有 CustomPaintCustomPainter 這兩個類來幫助你繪圖。后者實現你在 canvas 上的繪圖算法。

想要學習如何實現一個筆跡畫筆,請參考 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),
    );
  }
}

Widget 的透明度在哪里?

在 iOS 中,什么東西都會有一個 .opacity 或是 .alpha 的屬性。在 Flutter 中,你需要給 widget 包裹一個 Opacity widget 來做到這一點。

我怎么創建自定義的 widgets?

在 iOS 中,你編寫 UIView 的子類,或使用已經存在的 view 來重載并實現方法,以達到特定的功能。在 Flutter 中,你會組合(composing)多個小的 widgets 來構建一個自定義的 widget(而不是擴展它)。

舉個例子,如果你要構建一個 CustomButton ,并在構造器中傳入它的 label?那就組合 RaisedButton 和 label,而不是擴展 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"),
  );
}

導航

我怎么在不同頁面之間跳轉?

在 iOS 中,你可以使用管理了 view controller 棧的 UINavigationController 來在不同的 view controller 之間跳轉。

Flutter 也有類似的實現,使用了 NavigatorRoutes。一個路由是 App 中“屏幕”或“頁面”的抽象,而一個 Navigator 是管理多個路由的 widget 。你可以粗略地把一個路由對應到一個 UIViewController。Navigator 的工作原理和 iOS 中 UINavigationController 非常相似,當你想跳轉到新頁面或者從新頁面返回時,它可以 push()pop() 路由。

在頁面之間跳轉,你有幾個選擇:

  • 具體指定一個由路由名構成的 Map。(MaterialApp)
  • 直接跳轉到一個路由。(WidgetApp)

下面是構建一個 Map 的例子:

void main() {
  runApp(MaterialApp(
    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'),
    },
  ));
}

通過把路由的名字 push 給一個 Navigator 來跳轉:

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

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

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

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

之后,在 location 路由中,一旦用戶選擇了地點,攜帶結果一起 pop() 出棧:

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

我怎么跳轉到其他 App?

在 iOS 中,要跳轉到其他 App,你需要一個特定的 URL Scheme。對系統級別的 App 來說,這個 scheme 取決于 App。為了在 Flutter 中實現這個功能,你可以創建一個原生平臺的整合層,或者使用現有的 plugin,例如 url_launcher。

線程和異步

我怎么編寫異步的代碼?

Dart 是單線程執行模型,但是它支持 Isolate(一種讓 Dart 代碼運行在其他線程的方式)、事件循環和異步編程。除非你自己創建一個 Isolate ,否則你的 Dart 代碼永遠運行在 UI 線程,并由 event loop 驅動。Flutter 的 event loop 和 iOS 中的 main loop 相似——Looper 是附加在主線程上的。

Dart 的單線程模型并不意味著你寫的代碼一定是阻塞操作,從而卡住 UI。相反,使用 Dart 語言提供的異步工具,例如 async / await ,來實現異步操作。

舉個例子,你可以使用 async / await 來讓 Dart 幫你做一些繁重的工作,編寫網絡請求代碼而不會掛起 UI:

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 的區別,請參考下一章節。

你是怎么把工作放到后臺線程的?

由于 Flutter 是單線程并且跑著一個 event loop 的(就像 Node.js 那樣),你不必為線程管理或是開啟后臺線程而操心。如果你正在做 I/O 操作,如訪問磁盤或網絡請求,安全地使用 async / await 就完事了。如果,在另外的情況下,你需要做讓 CPU 執行繁忙的計算密集型任務,你需要使用 Isolate 來避免阻塞 event loop。

對于 I/O 操作,通過關鍵字 async,把方法聲明為異步方法,然后通過await關鍵字等待該異步方法執行完成(譯者語:這和javascript中是相同的):

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;
  }
}

我怎么發起網絡請求?

在 Flutter 中,使用流行的 http package 做網絡請求非常簡單。它把你可能需要自己做的網絡請求操作抽象了出來,讓發起請求變得簡單。

要使用 http 包,在 pubspec.yaml 中把它添加為依賴:

dependencies:
  ...
  http: ^0.11.3+16

發起網絡請求,在 http.get() 這個 async 方法中使用 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);
    });
  }
}

我怎么展示一個長時間運行的任務的進度?

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

在 Flutter 中,使用一個 ProgressIndicator widget。通過一個布爾 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);
    });
  }
}

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

我怎么在 Flutter 中引入 image assets?多分辨率怎么辦?

iOS 把 images 和 assets 作為不同的東西,而 Flutter 中只有 assets。被放到 iOS 中 Images.xcasset 文件夾下的資源在 Flutter 中被放到了 assets 文件夾中。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。把基礎圖片(1.0x)放置到 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");
}

更多細節,參見 Adding Assets and Images in Flutter。

我在哪里放置字符串?我怎么做本地化?

不像 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 定義了 App 支持哪些地區。上面的例子使用了一個 MaterialApp ,所以它既有 GlobalWidgetsLocalizations 用于基礎 widgets,也有 MaterialWidgetsLocalizations 用于 Material wigets 的本地化。如果你使用 WidgetsApp ,則無需包括后者。注意,這兩個代理雖然包括了“默認”值,但如果你想讓你的 App 本地化,你仍需要提供一或多個代理作為你的 App 本地化副本。

當初始化時,WidgetsAppMaterialApp 會使用你指定的代理為你創建一個 Localizations widget。Localizations widget 可以隨時從當前上下文中訪問設備的地點,或者使用 Window.locale。

要訪問本地化文件,使用 Localizations.of() 方法來訪問提供代理的特定本地化類。如需翻譯,使用 intl_translation 包來取出翻譯副本到 arb 文件中。把它們引入 App 中,并用 intl 來使用它們。

更多 Flutter 中國際化和本地化的細節,請訪問 internationalization guide ,那里有不使用 intl 包的示例代碼。

注意,在 Flutter 1.0 beta 2 之前,在 Flutter 中定義的 assets 不能在原生一側被訪問。原生定義的資源在 Flutter 中也不可用,因為它們在獨立的文件夾中。

Cocoapods 相當于什么?我該如何添加依賴?

在 iOS 中,你把依賴添加到 Podfile 中。Flutter 使用 Dart 構建系統和 Pub 包管理器來處理依賴。這些工具將本機 Android 和 iOS 包裝應用程序的構建委派給相應的構建系統。

如果你的 Flutter 工程中的 iOS 文件夾中擁有 Podfile,請僅在你為每個平臺集成時使用它??傮w來說,使用 pubspec.yaml 來在 Flutter 中聲明外部依賴。一個可以找到優秀 Flutter 包的地方是 Pub。

ViewControllers

ViewController 相當于 Flutter 中的什么?

在 iOS 中,一個 ViewController 代表了用戶界面的一部分,最常用于一個屏幕,或是其中一部分。它們被組合在一起用于構建復雜的用戶界面,并幫助你拆分 App 的 UI。在 Flutter 中,這一任務回落到了 widgets 中。就像在界面導航部分提到的一樣,一個屏幕也是被 widgets 來表示的,因為“萬物皆 widget!”。使用 NavigatorRoute 之間跳轉,或者渲染相同數據的不同狀態。

我該怎么監聽 iOS 中的生命周期事件?

在 iOS 中,你可以重寫 ViewController 中的方法來捕獲它的視圖的生命周期,或者在 AppDelegate 中注冊生命周期的回調函數。在 Flutter 中沒有這兩個概念,但你可以通過 hook WidgetsBinding 觀察者來監聽生命周期事件,并監聽 didChangeAppLifecycleState() 的變化事件。

可觀察的生命周期事件有:

  • inactive - 應用處于不活躍的狀態,并且不會接受用戶的輸入。這個事件僅工作在 iOS 平臺,在 Android 上沒有等價的事件。
  • paused - 應用暫時對用戶不可見,雖然不接受用戶輸入,但是是在后臺運行的。
  • resumed - 應用可見,也響應用戶的輸入。
  • suspending - 應用暫時被掛起,在 iOS 上沒有這一事件。

更多關于這些狀態的細節和含義,請參見 AppLifecycleStatus documentation 。

布局

UITableView 和 UICollectionView 相當于 Flutter 中的什么?

在 iOS 中,你可能用 UITableView 或 UICollectionView 來展示一個列表。在 Flutter 中,你可以用 ListView 來達到相似的實現。在 iOS 中,你通過代理方法來確定行數,每一個 index path 的單元格,以及單元格的尺寸。

由于 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;
  }
}

我怎么知道列表的哪個元素被點擊了?

iOS 中,你通過 tableView:didSelectRowAtIndexPath: 代理方法來實現。在 Flutter 中,使用傳遞進來的 widget 的 touch handle:

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;
  }
}

我怎么動態地更新 ListView?

在 iOS 中,你改變列表的數據,并通過 reloadData() 方法來通知 table 或是 collection view。

在 Flutter 中,如果你想通過 setState() 方法來更新 widget 列表,你會很快發現你的數據展示并沒有變化。這是因為當 setState() 被調用時,Flutter 渲染引擎會去檢查 widget 樹來查看是否有什么地方被改變了。當它得到你的 ListView 時,它會使用一個 == 判斷,并且發現兩個 ListView 是相同的。沒有什么東西是變了的,因此更新不是必須的。

一個更新 ListView 的簡單方法是,在 setState() 中創建一個新的 list,并把舊 list 的數據拷貝給新的 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() 函數里并沒有重新創建一個 list,而是 .add 了一個 widget。

ScrollView 相當于 Flutter 里的什么?

在 iOS 中,你給 view 包裹上 ScrollView 來允許用戶在需要時滾動你的內容。

在 Flutter 中,最簡單的方法是使用 ListView widget。它表現得既和 iOS 中的 ScrollView 一致,也能和 TableView 一致,因為你可以給它的 widget 做垂直排布:

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

更多關于在 Flutter 總如何排布 widget 的文檔,請參閱 layout tutorial

手勢檢測及觸摸事件處理

我怎么給 Flutter 的 widget 添加一個點擊監聽者?

在 iOS 中,你給一個 view 添加 GestureRecognizer 來處理點擊事件。在 Flutter 中,有兩種方法來添加點擊監聽者:

  1. 如果 widget 本身支持事件監測,直接傳遞給它一個函數,并在這個函數里實現響應方法。例如,RaisedButton widget 擁有一個 RaisedButton 參數:

    @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");
              },
            ),
          ),
        );
      }
    }
    

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

使用 GestureDetector 你可以監聽更廣闊范圍內的手勢,比如:

  • Tapping
    • onTapDown — 在特定位置輕觸手勢接觸了屏幕。
    • onTapUp — 在特定位置產生了一個輕觸手勢,并停止接觸屏幕。
    • onTap — 產生了一個輕觸手勢。
    • onTapCancel — 觸發了 onTapDown 但沒能觸發 tap。
  • Double tapping
    • onDoubleTap — 用戶在同一個位置快速點擊了兩下屏幕。
  • Long pressing
    • onLongPress — 用戶在同一個位置長時間接觸屏幕。
  • Vertical dragging
    • onVerticalDragStart — 接觸了屏幕,并且可能會垂直移動。
    • onVerticalDragUpdate — 接觸了屏幕,并繼續在垂直方向移動。
    • onVerticalDragEnd — 之前接觸了屏幕并垂直移動,并在停止接觸屏幕前以某個垂直的速度移動。
  • Horizontal dragging
    • 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();
            }
          },
        ),
      ),
    );
  }
}

主題和文字

我怎么給 App 設置主題?

Flutter 實現了一套漂亮的 MD 組件,并且開箱可用。它接管了一大堆你需要的樣式和主題。

為了充分發揮你的 App 中 MD 組件的優勢,聲明一個頂級 widget,MaterialApp,用作你的 App 入口。MaterialApp 是一個便利組件,包含了許多 App 通常需要的 MD 風格組件。它通過一個 WidgetsApp 添加了 MD 功能來實現。

但是 Flutter 足夠地靈活和富有表現力來實現任何其他的設計語言。在 iOS 上,你可以用 Cupertino library 來制作遵守 Human Interface Guidelines 的界面。查看這些 widget 的集合,請參閱 Cupertino widgets gallery

你也可以在你的 App 中使用 WidgetApp,它提供了許多相似的功能,但不如 MaterialApp 那樣強大。

對任何子組件定義顏色和樣式,可以給 MaterialApp widget 傳遞一個 ThemeData 對象。舉個例子,在下面的代碼中,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(),
    );
  }
}

我怎么給 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'),
      ),
    ),
  );
}

我怎么給我的 Text widget 設置樣式?

除了字體以外,你也可以給 Text widget 的樣式元素設置自定義值。Text widget 接受一個 TextStyle 對象,你可以指定許多參數,比如:

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

表單輸入

Flutter 中表單怎么工作?我怎么拿到用戶的輸入?

我們已經提到 Flutter 使用不可變的 widget,并且狀態是分離的,你可能會好奇在這種情境下怎么處理用戶的輸入。在 iOS 中,你經常在需要提交數據時查詢組件當前的狀態或動作,但這在 Flutter 中是怎么工作的呢?

在表單處理的實踐中,就像在 Flutter 中任何其他的地方一樣,要通過特定的 widgets。如果你有一個 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),
      ),
    );
  }
}

你可以在這里獲得更多信息,或是完整的代碼列表: Retrieve the value of a text field,來自 Flutter Cookbook 。

Text field 中的 placeholder 相當于什么?

在 Flutter 中,你可以輕易地通過向 Text widget 的裝飾構造器參數重傳遞 InputDecoration 來展示“小提示”,或是占位符文字:

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

我怎么展示驗證錯誤信息?

就像展示“小提示”一樣,向 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 em) {
    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(p);

    return regExp.hasMatch(em);
  }
}

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

我怎么和平臺,以及平臺的原生代碼交互?

Flutter 的代碼并不直接在平臺之下運行,相反,Dart 代碼構建的 Flutter 應用在設備上以原生的方式運行,卻“側步躲開了”平臺提供的 SDK。這意味著,例如,你在 Dart 中發起一個網絡請求,它就直接在 Dart 的上下文中運行。你并不會用上平常在 iOS 或 Android 上使用的原生 API。你的 Flutter 程序仍然被原生平臺的 ViewController 管理作一個 view,但是你并不會直接訪問 ViewController 自身,或是原生框架。

但這并不意味著 Flutter 不能和原生 API,或任何你編寫的原生代碼交互。Flutter 提供了 platform channels ,來和管理你的 Flutter view 的 ViewController 通信和交互數據。平臺管道本質上是一個異步通信機制,橋接了 Dart 代碼和宿主 ViewController,以及它運行于的 iOS 框架。你可以用平臺管道來執行一個原生的函數,或者是從設備的傳感器中獲取數據。

除了直接使用平臺管道之外,你還可以使用一系列預先制作好的 plugins。例如,你可以直接使用插件來訪問相機膠卷或是設備的攝像頭,而不必編寫你自己的集成層代碼。你可以在 Pub 上找到插件,這是一個 Dart 和 Flutter 的開源包倉庫。其中一些包可能會支持集成 iOS 或 Android,或兩者均可。

如果你在 Pub 上找不到符合你需求的插件,你可以自己編寫 ,并且發布在 Pub 上。

我怎么訪問 GPS 傳感器?

使用 location 社區插件。

我怎么訪問攝像頭?

image_picker 在訪問攝像頭時非常常用。

我怎么登錄 Facebook?

登錄 Facebook 可以使用 flutter_facebook_login 社區插件。

我怎么使用 Firebase 特性?

大多數 Firebase 特性被 first party plugins 包含了。這些第一方插件由 Flutter 團隊維護:

你也可以在 Pub 上找到 Firebase 的第三方插件。

我怎創建自己的原生集成層?

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

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

數據庫和本地存儲

我怎么在 Flutter 中訪問 UserDefaults?

在 iOS 中,你可以使用屬性列表來存儲鍵值對的集合,即我們熟悉的 UserDefaults。

在 Flutter 中,可以使用 Shared Preferences plugin 來達到相似的功能。它包裹了 UserDefaluts 以及 Android 上等價的 SharedPreferences 的功能。

CoreData 相當于 Flutter 中的什么?

在 iOS 中,你通過 CoreData 來存儲結構化的數據。這是一個 SQL 數據庫的上層封裝,讓查詢和關聯模型變得更加簡單。

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

通知

我怎么推送通知?

在 iOS 中,你需要向蘋果開發者平臺中注冊來允許推送通知。

在 Flutter 中,使用 firebase_messaging 插件來實現這一功能。

更多使用 Firebase Cloud Messaging API 的信息,請參閱 firebase_messaging 插件文檔。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容