Flutter—iOS開發(fā)者快速上手

Flutter for iOS 開發(fā)者

本文檔適用那些希望將現(xiàn)有 iOS 經(jīng)驗應用于 Flutter 的開發(fā)者。如果你擁有 iOS 開發(fā)基礎蜒什,那么你可以使用這篇文檔開始學習 Flutter 的開發(fā)院峡。

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

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

Views

UIView 相當于 Flutter 中的什么苹粟?

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

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

然而场靴,Widget 和 UIView 還是有些區(qū)別的。首先港准,widgets 擁有不同的生存時間:它們一直存在且保持不變旨剥,直到當它們需要被改變。當 widgets 和它們的狀態(tài)被改變時浅缸,F(xiàn)lutter 會構建一顆新的 widgets 樹轨帜。作為對比,iOS 中的 views 在改變時并不會被重新創(chuàng)建衩椒。但是與其說 views 是可變的實例蚌父,不如說它們被繪制了一次,并且直到使用 setNeedsDisplay() 之后才會被重新繪制毛萌。

此外苟弛,不像 UIView,由于不可變性阁将,F(xiàn)lutter 的 widgets 非常輕量膏秫。這是因為它們本身并不是什么控件,也不會被直接繪制出什么做盅,而只是 UI 的描述缤削。

Flutter 包含了 Material 組件庫。這些 widgets 遵循了 Material 設計規(guī)范吹榴。MD 是一個靈活的設計系統(tǒng)僻他,并且為包括 iOS 在內(nèi)的所有系統(tǒng)進行了優(yōu)化

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

我怎么來更新 Widgets婿斥?

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

這也正是有狀態(tài)的和無狀態(tài)的 widget 這一概念的來源哈恰。一個 StatelessWidget 正如它聽起來一樣,是一個沒有附加狀態(tài)的 widget志群。

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

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

如果你希望在發(fā)起 HTTP 請求時桑涎,依托接收到的數(shù)據(jù)動態(tài)的改變 UI彬向,請使用 StatefulWidget。當 HTTP 請求結束后攻冷,通知 Flutter 框架 widget 的 State 更新了娃胆,好讓系統(tǒng)來更新 UI。

有狀態(tài)和無狀態(tài)的 widget 之間一個非常重要的區(qū)別是等曼,StatefulWidget 擁有一個 State 對象來存儲它的狀態(tài)數(shù)據(jù)里烦,并在 widget 樹重建時攜帶著它,因此狀態(tài)不會丟失涉兽。

如果你有疑惑招驴,請記住以下規(guī)則:如果一個 widget 在它的 build 方法之外改變(例如,在運行時由于用戶的操作而改變)枷畏,它就是有狀態(tài)的别厘。如果一個 widget 在一次 build 之后永遠不變,那它就是無狀態(tài)的拥诡。但是触趴,即便一個 widget 是有狀態(tài)的,包含它的父親 widget 也可以是無狀態(tài)的渴肉,只要父 widget 本身不響應這些變化冗懦。

下面的例子展示了如何使用一個 StatelessWidget 。一個常見的 StatelessWidgetText widget仇祭。如果你查看 Text 的實現(xiàn)披蕉,你會發(fā)現(xiàn)它是 StatelessWidget 的子類。

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

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

但是爬凑,如果你希望 I like Flutter 在點擊 FloatingActionButton 時動態(tài)的改變呢徙缴?

為了實現(xiàn)這個,用 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 中調(diào)用 addSubview() 或在子 view 中調(diào)用 removeFromSuperview() 來動態(tài)地添加或移除子 views昵时。在 Flutter 中,由于 widget 不可變椒丧,所以沒有和 addSubview() 直接等價的東西壹甥。作為替代,你可以向 parent 傳入一個返回 widget 的函數(shù)壶熏,并用一個布爾值來控制子 widget 的創(chuàng)建句柠。

下面這個例子展示了在點擊 FloatingActionButton 時如何動態(tài)地切換兩個 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 中棒假,你通過調(diào)用 animate(withDuration:animations:) 方法來給一個 view 創(chuàng)建動畫溯职。在 Flutter 中,使用動畫庫來包裹 widgets帽哑,而不是創(chuàng)建一個動畫 widget谜酒。

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

例如,你可能會用 CurvedAnimation 來實現(xiàn)一個 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 widgetsAnimations tutorial 以及 Animations overview齐婴。

我該怎么繪圖油猫?

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

想要學習如何實現(xiàn)一個筆跡畫筆鸥昏,請參考 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 來做到這一點屉更。

我怎么創(chuàng)建自定義的 widgets渤早?

在 iOS 中,你編寫 UIView 的子類,或使用已經(jīng)存在的 view 來重載并實現(xiàn)方法扛芽,以達到特定的功能赋朦。在 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 也有類似的實現(xiàn),使用了 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恶复。對系統(tǒng)級別的 App 來說怜森,這個 scheme 取決于 App速挑。為了在 Flutter 中實現(xiàn)這個功能,你可以創(chuàng)建一個原生平臺的整合層副硅,或者使用現(xiàn)有的 plugin姥宝,例如 url_launcher

線程和異步

我怎么編寫異步的代碼恐疲?

Dart 是單線程執(zhí)行模型腊满,但是它支持 Isolate(一種讓 Dart 代碼運行在其他線程的方式)、事件循環(huán)和異步編程培己。除非你自己創(chuàng)建一個 Isolate 碳蛋,否則你的 Dart 代碼永遠運行在 UI 線程,并由 event loop 驅(qū)動省咨。Flutter 的 event loop 和 iOS 中的 main loop 相似——Looper 是附加在主線程上的肃弟。

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

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

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

一旦 await 到網(wǎng)絡請求完成,通過調(diào)用 setState() 來更新 UI快耿,這會觸發(fā) widget 子樹的重建囊陡,并更新相關數(shù)據(jù)。

下面的例子展示了異步加載數(shù)據(jù)掀亥,并用 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 的區(qū)別,請參考下一章節(jié)搪花。

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

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

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

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

這就是對諸如網(wǎng)絡請求或數(shù)據(jù)庫訪問等 I/O 操作的典型做法逞盆。

然而,有時候你需要處理大量的數(shù)據(jù)屋确,這會導致你的 UI 掛起纳击。在 Flutter 中,使用 Isolate 來發(fā)揮多核心 CPU 的優(yōu)勢來處理那些長期運行或是計算密集型的任務攻臀。

Isolates 是分離的運行線程焕数,并且不和主線程的內(nèi)存堆共享內(nèi)存。這意味著你不能訪問主線程中的變量刨啸,或者使用 setState() 來更新 UI堡赔。正如它們的名字一樣,Isolates 不能共享內(nèi)存设联。

下面的例子展示了一個簡單的 isolate善已,是如何把數(shù)據(jù)返回給主線程來更新 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() 是一個運行于自己獨立執(zhí)行線程上的 Isolate离例。在 isolate 里换团,你可以執(zhí)行 CPU 密集型任務(例如解析一個龐大的 json),或是計算密集型的數(shù)學操作宫蛆,如加密或信號處理等艘包。

你可以運行下面的完整例子:

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

我怎么發(fā)起網(wǎng)絡請求?

在 Flutter 中耀盗,使用流行的 http package 做網(wǎng)絡請求非常簡單想虎。它把你可能需要自己做的網(wǎng)絡請求操作抽象了出來,讓發(fā)起請求變得簡單叛拷。

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

dependencies:
  ...
  http: ^0.11.3+16

發(fā)起網(wǎng)絡請求,在 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 更新狀態(tài)你雌,并在結束后隱去。

在下面的例子中,build 函數(shù)被拆分成三個函數(shù)婿崭。如果 showLoadingDialog()true (當 widgets.length == 0 時)拨拓,則渲染 ProgressIndicator。否則氓栈,當數(shù)據(jù)從網(wǎng)絡請求中返回時渣磷,渲染 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');
}

對于圖片伞插,F(xiàn)lutter 像 iOS 一樣,遵循了一個簡單的基于像素密度的格式争便。Image assets 可能是 1.0x 2.0x 3.0x 或是其他的任何倍數(shù)。這些所謂的 devicePixelRatio 傳達了物理像素到單個邏輯像素的比率断医。

Assets 可以被放置到任何屬性文件夾中——Flutter 并沒有預先定義的文件結構始花。在 pubspec.yaml 文件中聲明 assets (和位置),然后 Flutter 會把他們識別出來孩锡。

舉個例子,要把一個叫 my_icon.png 的圖片放到 Flutter 工程中亥贸,你可能想要把存儲它的文件夾叫做 images躬窜。把基礎圖片(1.0x)放置到 images 文件夾中,并把其他變體放置在子文件夾中炕置,并接上合適的比例系數(shù):

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

更多細節(jié),參見 Adding Assets and Images in Flutter朴摊。

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

不像 iOS 擁有一個 Localizable.strings 文件甚纲,F(xiàn)lutter 目前并沒有一個用于處理字符串的系統(tǒng)口锭。目前,最佳實踐是把你的文本拷貝到靜態(tài)區(qū),并在這里訪問鹃操。例如:

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

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

Text(Strings.welcomeMessage)

默認情況下韭寸,F(xiàn)lutter 只支持美式英語字符串。如果你要支持其他語言荆隘,請引入 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 支持哪些地區(qū)褒脯。上面的例子使用了一個 MaterialApp ,所以它既有 GlobalWidgetsLocalizations 用于基礎 widgets仪壮,也有 MaterialWidgetsLocalizations 用于 Material wigets 的本地化憨颠。如果你使用 WidgetsApp ,則無需包括后者积锅。注意爽彤,這兩個代理雖然包括了“默認”值,但如果你想讓你的 App 本地化缚陷,你仍需要提供一或多個代理作為你的 App 本地化副本适篙。

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

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

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

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

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

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

如果你的 Flutter 工程中的 iOS 文件夾中擁有 Podfile腹尖,請僅在你為每個平臺集成時使用它》ゲ保總體來說热幔,使用 pubspec.yaml 來在 Flutter 中聲明外部依賴。一個可以找到優(yōu)秀 Flutter 包的地方是 Pub讼庇。

ViewControllers

ViewController 相當于 Flutter 中的什么绎巨?

在 iOS 中,一個 ViewController 代表了用戶界面的一部分蠕啄,最常用于一個屏幕场勤,或是其中一部分。它們被組合在一起用于構建復雜的用戶界面歼跟,并幫助你拆分 App 的 UI和媳。在 Flutter 中,這一任務回落到了 widgets 中哈街。就像在界面導航部分提到的一樣留瞳,一個屏幕也是被 widgets 來表示的,因為“萬物皆 widget骚秦!”她倘。使用 NavigatorRoute 之間跳轉,或者渲染相同數(shù)據(jù)的不同狀態(tài)作箍。

我該怎么監(jiān)聽 iOS 中的生命周期事件硬梁?

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

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

  • inactive - 應用處于不活躍的狀態(tài)嗤详,并且不會接受用戶的輸入个扰。這個事件僅工作在 iOS 平臺,在 Android 上沒有等價的事件葱色。
  • paused - 應用暫時對用戶不可見递宅,雖然不接受用戶輸入,但是是在后臺運行的。
  • resumed - 應用可見办龄,也響應用戶的輸入烘绽。
  • suspending - 應用暫時被掛起,在 iOS 上沒有這一事件俐填。

更多關于這些狀態(tài)的細節(jié)和含義安接,請參見 AppLifecycleStatus documentation

布局

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

在 iOS 中盏檐,你可能用 UITableView 或 UICollectionView 來展示一個列表。在 Flutter 中驶悟,你可以用 ListView 來達到相似的實現(xiàn)胡野。在 iOS 中,你通過代理方法來確定行數(shù)痕鳍,每一個 index path 的單元格硫豆,以及單元格的尺寸。

由于 Flutter 中 widget 的不可變特性笼呆,你需要向 ListView 傳遞一個 widget 列表熊响,F(xiàn)lutter 會確保滾動是快速且流暢的。

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: 代理方法來實現(xiàn)。在 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;
  }
}

我怎么動態(tài)地更新 ListView剔难?

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

在 Flutter 中,如果你想通過 setState() 方法來更新 widget 列表环鲤,你會很快發(fā)現(xiàn)你的數(shù)據(jù)展示并沒有變化纯趋。這是因為當 setState() 被調(diào)用時,F(xiàn)lutter 渲染引擎會去檢查 widget 樹來查看是否有什么地方被改變了冷离。當它得到你的 ListView 時吵冒,它會使用一個 == 判斷,并且發(fā)現(xiàn)兩個 ListView 是相同的西剥。沒有什么東西是變了的痹栖,因此更新不是必須的。

一個更新 ListView 的簡單方法是瞭空,在 setState() 中創(chuàng)建一個新的 list揪阿,并把舊 list 的數(shù)據(jù)拷貝給新的 list疗我。雖然這樣很簡單,但當數(shù)據(jù)集很大時南捂,并不推薦這樣做:

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 來構建列表溺健。這個方法在你想要構建動態(tài)列表麦牺,或是列表擁有大量數(shù)據(jù)時會非常好用。

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

與創(chuàng)建一個 “ListView” 不同矿瘦,創(chuàng)建一個 ListView.builder 接受兩個主要參數(shù):列表的初始長度枕面,和一個 ItemBuilder 方法。

ItemBuilder 方法和 cellForItemAt 代理方法非常類似缚去,它接受一個位置潮秘,并且返回在這個位置上你希望渲染的 cell。

最后易结,也是最重要的枕荞,注意 onTap() 函數(shù)里并沒有重新創(chuàng)建一個 list,而是 .add 了一個 widget搞动。

ScrollView 相當于 Flutter 里的什么躏精?

在 iOS 中,你給 view 包裹上 ScrollView 來允許用戶在需要時滾動你的內(nèi)容鹦肿。

在 Flutter 中矗烛,最簡單的方法是使用 ListView widget。它表現(xiàn)得既和 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 添加一個點擊監(jiān)聽者歪架?

在 iOS 中,你給一個 view 添加 GestureRecognizer 來處理點擊事件霹陡。在 Flutter 中和蚪,有兩種方法來添加點擊監(jiān)聽者:

  1. 如果 widget 本身支持事件監(jiān)測,直接傳遞給它一個函數(shù)烹棉,并在這個函數(shù)里實現(xiàn)響應方法攒霹。例如,RaisedButton widget 擁有一個 RaisedButton 參數(shù):

    @override
    Widget build(BuildContext context) {
      return RaisedButton(
        onPressed: () {
          print("click");
        },
        child: Text("Button"),
      );
    }
    
    
  2. 如果 widget 本身不支持事件監(jiān)測浆洗,則在外面包裹一個 GestureDetector催束,并給它的 onTap 屬性傳遞一個函數(shù):

    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 你可以監(jiān)聽更廣闊范圍內(nèi)的手勢辅髓,比如:

  • Tapping
    • onTapDown — 在特定位置輕觸手勢接觸了屏幕泣崩。
    • onTapUp — 在特定位置產(chǎn)生了一個輕觸手勢,并停止接觸屏幕洛口。
    • onTap — 產(chǎn)生了一個輕觸手勢矫付。
    • onTapCancel — 觸發(fā)了 onTapDown 但沒能觸發(fā) tap。
  • Double tapping
    • onDoubleTap — 用戶在同一個位置快速點擊了兩下屏幕第焰。
  • Long pressing
    • onLongPress — 用戶在同一個位置長時間接觸屏幕买优。
  • Vertical dragging
    • onVerticalDragStart — 接觸了屏幕,并且可能會垂直移動挺举。
    • onVerticalDragUpdate — 接觸了屏幕杀赢,并繼續(xù)在垂直方向移動。
    • onVerticalDragEnd — 之前接觸了屏幕并垂直移動湘纵,并在停止接觸屏幕前以某個垂直的速度移動脂崔。
  • Horizontal dragging
    • onHorizontalDragStart — 接觸了屏幕,并且可能會水平移動梧喷。
    • onHorizontalDragUpdate — 接觸了屏幕砌左,并繼續(xù)在水平方向移動。
    • 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 實現(xiàn)了一套漂亮的 MD 組件,并且開箱可用偿凭。它接管了一大堆你需要的樣式和主題产弹。

為了充分發(fā)揮你的 App 中 MD 組件的優(yōu)勢,聲明一個頂級 widget弯囊,MaterialApp痰哨,用作你的 App 入口。MaterialApp 是一個便利組件常挚,包含了許多 App 通常需要的 MD 風格組件作谭。它通過一個 WidgetsApp 添加了 MD 功能來實現(xiàn)。

但是 Flutter 足夠地靈活和富有表現(xiàn)力來實現(xiàn)任何其他的設計語言奄毡。在 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 對象,你可以指定許多參數(shù)男窟,比如:

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

表單輸入

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

我們已經(jīng)提到 Flutter 使用不可變的 widget蝎宇,并且狀態(tài)是分離的弟劲,你可能會好奇在這種情境下怎么處理用戶的輸入。在 iOS 中姥芥,你經(jīng)常在需要提交數(shù)據(jù)時查詢組件當前的狀態(tài)或動作兔乞,但這在 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 的裝飾構造器參數(shù)重傳遞 InputDecoration 來展示“小提示”强品,或是占位符文字:

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

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

就像展示“小提示”一樣,向 Text widget 的裝飾器構造器參數(shù)中傳遞一個 InputDecoration的榛。

然而琼了,你并不想在一開始就顯示錯誤信息。相反夫晌,當用戶輸入了驗證信息雕薪,更新狀態(tài)昧诱,并傳入一個新的 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 中發(fā)起一個網(wǎng)絡請求奶赠,它就直接在 Dart 的上下文中運行鱼填。你并不會用上平常在 iOS 或 Android 上使用的原生 API。你的 Flutter 程序仍然被原生平臺的 ViewController 管理作一個 view毅戈,但是你并不會直接訪問 ViewController 自身苹丸,或是原生框架。

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

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

如果你在 Pub 上找不到符合你需求的插件赖淤,你可以自己編寫 蜀漆,并且發(fā)布在 Pub 上

我怎么訪問 GPS 傳感器咱旱?

使用 location 社區(qū)插件确丢。

我怎么訪問攝像頭绷耍?

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

我怎么登錄 Facebook鲜侥?

登錄 Facebook 可以使用 flutter_facebook_login 社區(qū)插件褂始。

我怎么使用 Firebase 特性?

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

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

我怎創(chuàng)建自己的原生集成層?

如果有一些 Flutter 和社區(qū)插件遺漏的平臺相關的特性舀寓,可以根據(jù) developing packages and plugins 頁面構建自己的插件胆数。

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

數(shù)據(jù)庫和本地存儲

我怎么在 Flutter 中訪問 UserDefaults育谬?

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

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

CoreData 相當于 Flutter 中的什么互站?

在 iOS 中,你通過 CoreData 來存儲結構化的數(shù)據(jù)僵缺。這是一個 SQL 數(shù)據(jù)庫的上層封裝胡桃,讓查詢和關聯(lián)模型變得更加簡單。

在 Flutter 中磕潮,使用 SQFlite 插件來實現(xiàn)這個功能翠胰。

通知

我怎么推送通知?

在 iOS 中自脯,你需要向蘋果開發(fā)者平臺中注冊來允許推送通知之景。

在 Flutter 中,使用 firebase_messaging 插件來實現(xiàn)這一功能膏潮。

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

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌斑鸦,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件潦嘶,死亡現(xiàn)場離奇詭異,居然都是意外死亡崇众,警方通過查閱死者的電腦和手機掂僵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來顷歌,“玉大人锰蓬,你說我怎么就攤上這事⊙梅裕” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵溪窒,是天一觀的道長坤塞。 經(jīng)常有香客問我,道長澈蚌,這世上最難降的妖魔是什么摹芙? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮宛瞄,結果婚禮上浮禾,老公的妹妹穿的比我還像新娘。我一直安慰自己份汗,他們只是感情好盈电,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杯活,像睡著了一般匆帚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旁钧,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天吸重,我揣著相機與錄音,去河邊找鬼歪今。 笑死嚎幸,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的寄猩。 我是一名探鬼主播嫉晶,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了车遂?” 一聲冷哼從身側響起封断,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舶担,沒想到半個月后坡疼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡衣陶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年柄瑰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剪况。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡教沾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出译断,到底是詐尸還是另有隱情授翻,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布孙咪,位于F島的核電站堪唐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏翎蹈。R本人自食惡果不足惜淮菠,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望荤堪。 院中可真熱鬧合陵,春花似錦、人聲如沸澄阳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碎赢。三九已至举庶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間揩抡,已是汗流浹背户侥。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留峦嗤,地道東北人蕊唐。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像烁设,于是被迫代替她去往敵國和親替梨。 傳聞我的和親對象是個殘疾皇子钓试,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容