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

Flutter for iOS 開(kāi)發(fā)者

本文檔適用那些希望將現(xiàn)有 iOS 經(jīng)驗(yàn)應(yīng)用于 Flutter 的開(kāi)發(fā)者。如果你擁有 iOS 開(kāi)發(fā)基礎(chǔ),那么你可以使用這篇文檔開(kāi)始學(xué)習(xí) Flutter 的開(kāi)發(fā)。

開(kāi)發(fā) Flutter 時(shí),你的 iOS 經(jīng)驗(yàn)和技能將會(huì)大有裨益,因?yàn)?Flutter 依賴于移動(dòng)操作系統(tǒng)的眾多功能和配置。Flutter 是用于為移動(dòng)設(shè)備構(gòu)建用戶界面的全新方式,但它也有一個(gè)插件系統(tǒng)用于和 iOS(及 Android)進(jìn)行非 UI 任務(wù)的通信。如果你是 iOS 開(kāi)發(fā)專家,則你不必將 Flutter 徹底重新學(xué)習(xí)一遍。

你可以將此文檔作為 cookbook,通過(guò)跳轉(zhuǎn)并查找與你的需求最相關(guān)的問(wèn)題。

Views

UIView 相當(dāng)于 Flutter 中的什么?

在 iOS 中,構(gòu)建 UI 的過(guò)程中將大量使用 view 對(duì)象。這些對(duì)象都是 UIView 的實(shí)例。它們可以用作容器來(lái)承載其他的 UIView,最終構(gòu)成你的界面布局。

在 Flutter 中,你可以粗略地認(rèn)為 Widget 相當(dāng)于 UIView 。Widget 和 iOS 中的控件并不完全等價(jià),但當(dāng)你試圖去理解 Flutter 是如何工作的時(shí)候,你可以認(rèn)為它們是“聲明和構(gòu)建 UI 的方法”。

然而,Widget 和 UIView 還是有些區(qū)別的。首先,widgets 擁有不同的生存時(shí)間:它們一直存在且保持不變,直到當(dāng)它們需要被改變。當(dāng) widgets 和它們的狀態(tài)被改變時(shí),F(xiàn)lutter 會(huì)構(gòu)建一顆新的 widgets 樹(shù)。作為對(duì)比,iOS 中的 views 在改變時(shí)并不會(huì)被重新創(chuàng)建。但是與其說(shuō) views 是可變的實(shí)例,不如說(shuō)它們被繪制了一次,并且直到使用 setNeedsDisplay() 之后才會(huì)被重新繪制。

此外,不像 UIView,由于不可變性,F(xiàn)lutter 的 widgets 非常輕量。這是因?yàn)樗鼈儽旧聿⒉皇鞘裁纯丶?,也不?huì)被直接繪制出什么,而只是 UI 的描述。

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

但是用 Flutter 實(shí)現(xiàn)任何的設(shè)計(jì)語(yǔ)言都非常的靈活和富有表現(xiàn)力。在 iOS 平臺(tái),你可以使用 Cupertino widgets 來(lái)構(gòu)建遵循了 Apple’s iOS design language 的界面。

我怎么來(lái)更新 Widgets?

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

這也正是有狀態(tài)的和無(wú)狀態(tài)的 widget 這一概念的來(lái)源。一個(gè) StatelessWidget 正如它聽(tīng)起來(lái)一樣,是一個(gè)沒(méi)有附加狀態(tài)的 widget。

StatelessWidget 在你構(gòu)建初始化后不再進(jìn)行改變的界面時(shí)非常有用。

舉個(gè)例子,你可能會(huì)用一個(gè) UIImageView 來(lái)展示你的 logo image 。如果這個(gè) logo 在運(yùn)行時(shí)不會(huì)改變,那么你就可以在 Flutter 中使用 StatelessWidget 。

如果你希望在發(fā)起 HTTP 請(qǐng)求時(shí),依托接收到的數(shù)據(jù)動(dòng)態(tài)的改變 UI,請(qǐng)使用 StatefulWidget。當(dāng) HTTP 請(qǐng)求結(jié)束后,通知 Flutter 框架 widget 的 State 更新了,好讓系統(tǒng)來(lái)更新 UI。

有狀態(tài)和無(wú)狀態(tài)的 widget 之間一個(gè)非常重要的區(qū)別是,StatefulWidget 擁有一個(gè) State 對(duì)象來(lái)存儲(chǔ)它的狀態(tài)數(shù)據(jù),并在 widget 樹(shù)重建時(shí)攜帶著它,因此狀態(tài)不會(huì)丟失。

如果你有疑惑,請(qǐng)記住以下規(guī)則:如果一個(gè) widget 在它的 build 方法之外改變(例如,在運(yùn)行時(shí)由于用戶的操作而改變),它就是有狀態(tài)的。如果一個(gè) widget 在一次 build 之后永遠(yuǎn)不變,那它就是無(wú)狀態(tài)的。但是,即便一個(gè) widget 是有狀態(tài)的,包含它的父親 widget 也可以是無(wú)狀態(tài)的,只要父 widget 本身不響應(yīng)這些變化。

下面的例子展示了如何使用一個(gè) StatelessWidget 。一個(gè)常見(jiàn)的 StatelessWidgetText widget。如果你查看 Text 的實(shí)現(xiàn),你會(huì)發(fā)現(xiàn)它是 StatelessWidget 的子類。

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

閱讀上面的代碼,你可能會(huì)注意到 Text widget 并不顯示地?cái)y帶任何狀態(tài)。它通過(guò)傳入給它的構(gòu)造器的數(shù)據(jù)來(lái)渲染,除此之外再無(wú)其他。

但是,如果你希望 I like Flutter 在點(diǎn)擊 FloatingActionButton 時(shí)動(dòng)態(tài)的改變呢?

為了實(shí)現(xiàn)這個(gè),用 StatefulWidget 包裹 Text widget,并在用戶點(diǎn)擊按鈕時(shí)更新它。

舉個(gè)例子:

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

我怎么對(duì) widget 布局?我的 Storyboard 在哪?

在 iOS 中,你可能會(huì)用 Storyboard 文件來(lái)組織 views,并對(duì)它們?cè)O(shè)置約束,或者,你可能在 view controller 中使用代碼來(lái)設(shè)置約束。在 Flutter 中,你通過(guò)編寫一個(gè) widget 樹(shù)來(lái)聲明你的布局。

下面這個(gè)例子展示了如何展示一個(gè)帶有 padding 的簡(jiǎn)單 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() 來(lái)動(dòng)態(tài)地添加或移除子 views。在 Flutter 中,由于 widget 不可變,所以沒(méi)有和 addSubview() 直接等價(jià)的東西。作為替代,你可以向 parent 傳入一個(gè)返回 widget 的函數(shù),并用一個(gè)布爾值來(lái)控制子 widget 的創(chuàng)建。

下面這個(gè)例子展示了在點(diǎn)擊 FloatingActionButton 時(shí)如何動(dòng)態(tài)地切換兩個(gè) 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),
      ),
    );
  }
}

我怎么對(duì) widget 做動(dòng)畫?

在 iOS 中,你通過(guò)調(diào)用 animate(withDuration:animations:) 方法來(lái)給一個(gè) view 創(chuàng)建動(dòng)畫。在 Flutter 中,使用動(dòng)畫庫(kù)來(lái)包裹 widgets,而不是創(chuàng)建一個(gè)動(dòng)畫 widget。

在 Flutter 中,使用 AnimationController 。這是一個(gè)可以暫停、尋找、停止、反轉(zhuǎn)動(dòng)畫的 Animation<double> 類型。它需要一個(gè) Ticker 當(dāng) vsync 發(fā)生時(shí)來(lái)發(fā)送信號(hào),并且在每幀運(yùn)行時(shí)創(chuàng)建一個(gè)介于 0 和 1 之間的線性插值(interpolation)。你可以創(chuàng)建一個(gè)或多個(gè)的 Animation 并附加給一個(gè) controller。

例如,你可能會(huì)用 CurvedAnimation 來(lái)實(shí)現(xiàn)一個(gè) interpolated 曲線。在這個(gè)場(chǎng)景中,controller 是動(dòng)畫過(guò)程的“主人”,而 CurvedAnimation 計(jì)算曲線,并替代 controller 默認(rèn)的線性模式。

當(dāng)構(gòu)建 widget 樹(shù)時(shí),你會(huì)把 Animation 指定給一個(gè) widget 的動(dòng)畫屬性,比如 FadeTransition 的 opacity,并告訴控制器開(kāi)始動(dòng)畫。

下面這個(gè)例子展示了在點(diǎn)擊 FloatingActionButton 之后,如何使用 FadeTransition 來(lái)讓 widget 淡出到 logo 圖標(biāo):

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

更多信息,請(qǐng)參閱 Animation & Motion widgetsAnimations tutorial 以及 Animations overview

我該怎么繪圖?

在 iOS 上,你通過(guò) CoreGraphics 來(lái)在屏幕上繪制線條和形狀。Flutter 有一套基于 Canvas 類的不同的 API,還有 CustomPaintCustomPainter 這兩個(gè)類來(lái)幫助你繪圖。后者實(shí)現(xiàn)你在 canvas 上的繪圖算法。

想要學(xué)習(xí)如何實(shí)現(xiàn)一個(gè)筆跡畫筆,請(qǐng)參考 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 中,什么東西都會(huì)有一個(gè) .opacity 或是 .alpha 的屬性。在 Flutter 中,你需要給 widget 包裹一個(gè) Opacity widget 來(lái)做到這一點(diǎn)。

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

在 iOS 中,你編寫 UIView 的子類,或使用已經(jīng)存在的 view 來(lái)重載并實(shí)現(xiàn)方法,以達(dá)到特定的功能。在 Flutter 中,你會(huì)組合(composing)多個(gè)小的 widgets 來(lái)構(gòu)建一個(gè)自定義的 widget(而不是擴(kuò)展它)。

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

導(dǎo)航

我怎么在不同頁(yè)面之間跳轉(zhuǎn)?

在 iOS 中,你可以使用管理了 view controller 棧的 UINavigationController 來(lái)在不同的 view controller 之間跳轉(zhuǎn)。

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

在頁(yè)面之間跳轉(zhuǎn),你有幾個(gè)選擇:

  • 具體指定一個(gè)由路由名構(gòu)成的 Map。(MaterialApp)
  • 直接跳轉(zhuǎn)到一個(gè)路由。(WidgetApp)

下面是構(gòu)建一個(gè) 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'),
    },
  ));
}

通過(guò)把路由的名字 push 給一個(gè) Navigator 來(lái)跳轉(zhuǎn):

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

Navigator 類不僅用來(lái)處理 Flutter 中的路由,還被用來(lái)獲取你剛 push 到棧中的路由返回的結(jié)果。通過(guò) await等待路由返回的結(jié)果來(lái)達(dá)到這點(diǎn)。

舉個(gè)例子,要跳轉(zhuǎn)到“位置”路由來(lái)讓用戶選擇一個(gè)地點(diǎn),你可能要這么做:

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

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

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

我怎么跳轉(zhuǎn)到其他 App?

在 iOS 中,要跳轉(zhuǎn)到其他 App,你需要一個(gè)特定的 URL Scheme。對(duì)系統(tǒng)級(jí)別的 App 來(lái)說(shuō),這個(gè) scheme 取決于 App。為了在 Flutter 中實(shí)現(xiàn)這個(gè)功能,你可以創(chuàng)建一個(gè)原生平臺(tái)的整合層,或者使用現(xiàn)有的 plugin,例如 url_launcher。

線程和異步

我怎么編寫異步的代碼?

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

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

舉個(gè)例子,你可以使用 async / await 來(lái)讓 Dart 幫你做一些繁重的工作,編寫網(wǎng)絡(luò)請(qǐng)求代碼而不會(huì)掛起 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)絡(luò)請(qǐng)求完成,通過(guò)調(diào)用 setState() 來(lái)更新 UI,這會(huì)觸發(fā) widget 子樹(shù)的重建,并更新相關(guān)數(shù)據(jù)。

下面的例子展示了異步加載數(shù)據(jù),并用 ListView 展示出來(lái):

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

更多關(guān)于在后臺(tái)工作的信息,以及 Flutter 和 iOS 的區(qū)別,請(qǐng)參考下一章節(jié)。

你是怎么把工作放到后臺(tái)線程的?

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

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

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

這就是對(duì)諸如網(wǎng)絡(luò)請(qǐng)求或數(shù)據(jù)庫(kù)訪問(wèn)等 I/O 操作的典型做法。

然而,有時(shí)候你需要處理大量的數(shù)據(jù),這會(huì)導(dǎo)致你的 UI 掛起。在 Flutter 中,使用 Isolate 來(lái)發(fā)揮多核心 CPU 的優(yōu)勢(shì)來(lái)處理那些長(zhǎng)期運(yùn)行或是計(jì)算密集型的任務(wù)。

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

下面的例子展示了一個(gè)簡(jiǎn)單的 isolate,是如何把數(shù)據(jù)返回給主線程來(lái)更新 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() 是一個(gè)運(yùn)行于自己獨(dú)立執(zhí)行線程上的 Isolate。在 isolate 里,你可以執(zhí)行 CPU 密集型任務(wù)(例如解析一個(gè)龐大的 json),或是計(jì)算密集型的數(shù)學(xué)操作,如加密或信號(hào)處理等。

你可以運(yùn)行下面的完整例子:

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)絡(luò)請(qǐng)求?

在 Flutter 中,使用流行的 http package 做網(wǎng)絡(luò)請(qǐng)求非常簡(jiǎn)單。它把你可能需要自己做的網(wǎng)絡(luò)請(qǐng)求操作抽象了出來(lái),讓發(fā)起請(qǐng)求變得簡(jiǎn)單。

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

dependencies:
  ...
  http: ^0.11.3+16

發(fā)起網(wǎng)絡(luò)請(qǐng)求,在 http.get() 這個(gè) 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);
    });
  }
}

我怎么展示一個(gè)長(zhǎng)時(shí)間運(yùn)行的任務(wù)的進(jìn)度?

在 iOS 中,在后臺(tái)運(yùn)行耗時(shí)任務(wù)時(shí)你會(huì)使用 UIProgressView

在 Flutter 中,使用一個(gè) ProgressIndicator widget。通過(guò)一個(gè)布爾 flag 來(lái)控制是否展示進(jìn)度。在任務(wù)開(kāi)始時(shí),告訴 Flutter 更新?tīng)顟B(tài),并在結(jié)束后隱去。

在下面的例子中,build 函數(shù)被拆分成三個(gè)函數(shù)。如果 showLoadingDialog()true (當(dāng) widgets.length == 0 時(shí)),則渲染 ProgressIndicator。否則,當(dāng)數(shù)據(jù)從網(wǎng)絡(luò)請(qǐng)求中返回時(shí),渲染 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);
    });
  }
}

工程結(jié)構(gòu)、本地化、依賴和資源

我怎么在 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 來(lái)訪問(wèn)它:

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

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

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

Assets 可以被放置到任何屬性文件夾中——Flutter 并沒(méi)有預(yù)先定義的文件結(jié)構(gòu)。在 pubspec.yaml 文件中聲明 assets (和位置),然后 Flutter 會(huì)把他們識(shí)別出來(lái)。

舉個(gè)例子,要把一個(gè)叫 my_icon.png 的圖片放到 Flutter 工程中,你可能想要把存儲(chǔ)它的文件夾叫做 images。把基礎(chǔ)圖片(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 來(lái)訪問(wèn)這些圖片:

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

或者在 Image widget 中直接使用:

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

更多細(xì)節(jié),參見(jiàn) Adding Assets and Images in Flutter。

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

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

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

并且這樣訪問(wèn)你的字符串:

Text(Strings.welcomeMessage)

默認(rèn)情況下,F(xiàn)lutter 只支持美式英語(yǔ)字符串。如果你要支持其他語(yǔ)言,請(qǐng)引入 flutter_localizations 包。你可能也要引入 intl 包來(lái)支持其他的 i10n 機(jī)制,比如日期/時(shí)間格式化。

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
  ],
  // ...
)

這些代理包括了實(shí)際的本地化值,并且 supportedLocales 定義了 App 支持哪些地區(qū)。上面的例子使用了一個(gè) MaterialApp ,所以它既有 GlobalWidgetsLocalizations 用于基礎(chǔ) widgets,也有 MaterialWidgetsLocalizations 用于 Material wigets 的本地化。如果你使用 WidgetsApp ,則無(wú)需包括后者。注意,這兩個(gè)代理雖然包括了“默認(rèn)”值,但如果你想讓你的 App 本地化,你仍需要提供一或多個(gè)代理作為你的 App 本地化副本。

當(dāng)初始化時(shí),WidgetsAppMaterialApp 會(huì)使用你指定的代理為你創(chuàng)建一個(gè) Localizations widget。Localizationswidget 可以隨時(shí)從當(dāng)前上下文中訪問(wèn)設(shè)備的地點(diǎn),或者使用 Window.locale。

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

更多 Flutter 中國(guó)際化和本地化的細(xì)節(jié),請(qǐng)?jiān)L問(wèn) internationalization guide ,那里有不使用 intl 包的示例代碼。

注意,在 Flutter 1.0 beta 2 之前,在 Flutter 中定義的 assets 不能在原生一側(cè)被訪問(wèn)。原生定義的資源在 Flutter 中也不可用,因?yàn)樗鼈冊(cè)讵?dú)立的文件夾中。

Cocoapods 相當(dāng)于什么?我該如何添加依賴?

在 iOS 中,你把依賴添加到 Podfile 中。Flutter 使用 Dart 構(gòu)建系統(tǒng)和 Pub 包管理器來(lái)處理依賴。這些工具將本機(jī) Android 和 iOS 包裝應(yīng)用程序的構(gòu)建委派給相應(yīng)的構(gòu)建系統(tǒng)。

如果你的 Flutter 工程中的 iOS 文件夾中擁有 Podfile,請(qǐng)僅在你為每個(gè)平臺(tái)集成時(shí)使用它??傮w來(lái)說(shuō),使用 pubspec.yaml 來(lái)在 Flutter 中聲明外部依賴。一個(gè)可以找到優(yōu)秀 Flutter 包的地方是 Pub。

ViewControllers

ViewController 相當(dāng)于 Flutter 中的什么?

在 iOS 中,一個(gè) ViewController 代表了用戶界面的一部分,最常用于一個(gè)屏幕,或是其中一部分。它們被組合在一起用于構(gòu)建復(fù)雜的用戶界面,并幫助你拆分 App 的 UI。在 Flutter 中,這一任務(wù)回落到了 widgets 中。就像在界面導(dǎo)航部分提到的一樣,一個(gè)屏幕也是被 widgets 來(lái)表示的,因?yàn)椤叭f(wàn)物皆 widget!”。使用 NavigatorRoute 之間跳轉(zhuǎn),或者渲染相同數(shù)據(jù)的不同狀態(tài)。

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

在 iOS 中,你可以重寫 ViewController 中的方法來(lái)捕獲它的視圖的生命周期,或者在 AppDelegate 中注冊(cè)生命周期的回調(diào)函數(shù)。在 Flutter 中沒(méi)有這兩個(gè)概念,但你可以通過(guò) hook WidgetsBinding 觀察者來(lái)監(jiān)聽(tīng)生命周期事件,并監(jiān)聽(tīng) didChangeAppLifecycleState() 的變化事件。

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

  • inactive - 應(yīng)用處于不活躍的狀態(tài),并且不會(huì)接受用戶的輸入。這個(gè)事件僅工作在 iOS 平臺(tái),在 Android 上沒(méi)有等價(jià)的事件。
  • paused - 應(yīng)用暫時(shí)對(duì)用戶不可見(jiàn),雖然不接受用戶輸入,但是是在后臺(tái)運(yùn)行的。
  • resumed - 應(yīng)用可見(jiàn),也響應(yīng)用戶的輸入。
  • suspending - 應(yīng)用暫時(shí)被掛起,在 iOS 上沒(méi)有這一事件。

更多關(guān)于這些狀態(tài)的細(xì)節(jié)和含義,請(qǐng)參見(jiàn) AppLifecycleStatus documentation 。

布局

UITableView 和 UICollectionView 相當(dāng)于 Flutter 中的什么?

在 iOS 中,你可能用 UITableView 或 UICollectionView 來(lái)展示一個(gè)列表。在 Flutter 中,你可以用 ListView 來(lái)達(dá)到相似的實(shí)現(xiàn)。在 iOS 中,你通過(guò)代理方法來(lái)確定行數(shù),每一個(gè) index path 的單元格,以及單元格的尺寸。

由于 Flutter 中 widget 的不可變特性,你需要向 ListView 傳遞一個(gè) widget 列表,F(xiàn)lutter 會(huì)確保滾動(dòng)是快速且流暢的。

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

我怎么知道列表的哪個(gè)元素被點(diǎn)擊了?

iOS 中,你通過(guò) tableView:didSelectRowAtIndexPath: 代理方法來(lái)實(shí)現(xiàn)。在 Flutter 中,使用傳遞進(jìn)來(lái)的 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;
  }
}

我怎么動(dòng)態(tài)地更新 ListView?

在 iOS 中,你改變列表的數(shù)據(jù),并通過(guò) reloadData() 方法來(lái)通知 table 或是 collection view。

在 Flutter 中,如果你想通過(guò) setState() 方法來(lái)更新 widget 列表,你會(huì)很快發(fā)現(xiàn)你的數(shù)據(jù)展示并沒(méi)有變化。這是因?yàn)楫?dāng) setState() 被調(diào)用時(shí),F(xiàn)lutter 渲染引擎會(huì)去檢查 widget 樹(shù)來(lái)查看是否有什么地方被改變了。當(dāng)它得到你的 ListView 時(shí),它會(huì)使用一個(gè) == 判斷,并且發(fā)現(xiàn)兩個(gè) ListView 是相同的。沒(méi)有什么東西是變了的,因此更新不是必須的。

一個(gè)更新 ListView 的簡(jiǎn)單方法是,在 setState() 中創(chuàng)建一個(gè)新的 list,并把舊 list 的數(shù)據(jù)拷貝給新的 list。雖然這樣很簡(jiǎn)單,但當(dāng)數(shù)據(jù)集很大時(shí),并不推薦這樣做:

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

一個(gè)推薦的、高效的且有效的做法是,使用 ListView.Builder 來(lái)構(gòu)建列表。這個(gè)方法在你想要構(gòu)建動(dòng)態(tài)列表,或是列表?yè)碛写罅繑?shù)據(jù)時(shí)會(huì)非常好用。

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)建一個(gè) “ListView” 不同,創(chuàng)建一個(gè) ListView.builder 接受兩個(gè)主要參數(shù):列表的初始長(zhǎng)度,和一個(gè) ItemBuilder 方法。

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

最后,也是最重要的,注意 onTap() 函數(shù)里并沒(méi)有重新創(chuàng)建一個(gè) list,而是 .add 了一個(gè) widget。

ScrollView 相當(dāng)于 Flutter 里的什么?

在 iOS 中,你給 view 包裹上 ScrollView 來(lái)允許用戶在需要時(shí)滾動(dòng)你的內(nèi)容。

在 Flutter 中,最簡(jiǎn)單的方法是使用 ListView widget。它表現(xiàn)得既和 iOS 中的 ScrollView 一致,也能和 TableView 一致,因?yàn)槟憧梢越o它的 widget 做垂直排布:

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

更多關(guān)于在 Flutter 總?cè)绾闻挪?widget 的文檔,請(qǐng)參閱 layout tutorial。

手勢(shì)檢測(cè)及觸摸事件處理

我怎么給 Flutter 的 widget 添加一個(gè)點(diǎn)擊監(jiān)聽(tīng)者?

在 iOS 中,你給一個(gè) view 添加 GestureRecognizer 來(lái)處理點(diǎn)擊事件。在 Flutter 中,有兩種方法來(lái)添加點(diǎn)擊監(jiān)聽(tīng)者:

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

    @override
    Widget build(BuildContext context) {
      return RaisedButton(
        onPressed: () {
          print("click");
        },
        child: Text("Button"),
      );
    }
    
    
  2. 如果 widget 本身不支持事件監(jiān)測(cè),則在外面包裹一個(gè) GestureDetector,并給它的 onTap 屬性傳遞一個(gè)函數(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 上的其他手勢(shì)?

使用 GestureDetector 你可以監(jiān)聽(tīng)更廣闊范圍內(nèi)的手勢(shì),比如:

  • Tapping
    • onTapDown — 在特定位置輕觸手勢(shì)接觸了屏幕。
    • onTapUp — 在特定位置產(chǎn)生了一個(gè)輕觸手勢(shì),并停止接觸屏幕。
    • onTap — 產(chǎn)生了一個(gè)輕觸手勢(shì)。
    • onTapCancel — 觸發(fā)了 onTapDown 但沒(méi)能觸發(fā) tap。
  • Double tapping
    • onDoubleTap — 用戶在同一個(gè)位置快速點(diǎn)擊了兩下屏幕。
  • Long pressing
    • onLongPress — 用戶在同一個(gè)位置長(zhǎng)時(shí)間接觸屏幕。
  • Vertical dragging
    • onVerticalDragStart — 接觸了屏幕,并且可能會(huì)垂直移動(dòng)。
    • onVerticalDragUpdate — 接觸了屏幕,并繼續(xù)在垂直方向移動(dòng)。
    • onVerticalDragEnd — 之前接觸了屏幕并垂直移動(dòng),并在停止接觸屏幕前以某個(gè)垂直的速度移動(dòng)。
  • Horizontal dragging
    • onHorizontalDragStart — 接觸了屏幕,并且可能會(huì)水平移動(dòng)。
    • onHorizontalDragUpdate — 接觸了屏幕,并繼續(xù)在水平方向移動(dòng)。
    • onHorizontalDragEnd — 之前接觸屏幕并水平移動(dòng)的觸摸點(diǎn)與屏幕分離。

下面這個(gè)例子展示了一個(gè) GestureDetector 是如何在雙擊時(shí)旋轉(zhuǎn) 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 設(shè)置主題?

Flutter 實(shí)現(xiàn)了一套漂亮的 MD 組件,并且開(kāi)箱可用。它接管了一大堆你需要的樣式和主題。

為了充分發(fā)揮你的 App 中 MD 組件的優(yōu)勢(shì),聲明一個(gè)頂級(jí) widget,MaterialApp,用作你的 App 入口。MaterialApp 是一個(gè)便利組件,包含了許多 App 通常需要的 MD 風(fēng)格組件。它通過(guò)一個(gè) WidgetsApp 添加了 MD 功能來(lái)實(shí)現(xiàn)。

但是 Flutter 足夠地靈活和富有表現(xiàn)力來(lái)實(shí)現(xiàn)任何其他的設(shè)計(jì)語(yǔ)言。在 iOS 上,你可以用 Cupertino library 來(lái)制作遵守 Human Interface Guidelines 的界面。查看這些 widget 的集合,請(qǐng)參閱 Cupertino widgets gallery。

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

對(duì)任何子組件定義顏色和樣式,可以給 MaterialApp widget 傳遞一個(gè) ThemeData 對(duì)象。舉個(gè)例子,在下面的代碼中,primary swatch 被設(shè)置為藍(lán)色,并且文字的選中顏色是紅色:

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 設(shè)置自定義字體?

在 iOS 中,你在項(xiàng)目中引入任意的 ttf 文件,并在 info.plist 中設(shè)置引用。在 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 設(shè)置樣式?

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

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

表單輸入

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

我們已經(jīng)提到 Flutter 使用不可變的 widget,并且狀態(tài)是分離的,你可能會(huì)好奇在這種情境下怎么處理用戶的輸入。在 iOS 中,你經(jīng)常在需要提交數(shù)據(jù)時(shí)查詢組件當(dāng)前的狀態(tài)或動(dòng)作,但這在 Flutter 中是怎么工作的呢?

在表單處理的實(shí)踐中,就像在 Flutter 中任何其他的地方一樣,要通過(guò)特定的 widgets。如果你有一個(gè) TextField 或是 TextFormField,你可以通過(guò) TextEditingController 來(lái)獲得用戶輸入:

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,來(lái)自 Flutter Cookbook 。

Text field 中的 placeholder 相當(dāng)于什么?

在 Flutter 中,你可以輕易地通過(guò)向 Text widget 的裝飾構(gòu)造器參數(shù)重傳遞 InputDecoration 來(lái)展示“小提示”,或是占位符文字:

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

我怎么展示驗(yàn)證錯(cuò)誤信息?

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

然而,你并不想在一開(kāi)始就顯示錯(cuò)誤信息。相反,當(dāng)用戶輸入了驗(yàn)證信息,更新?tīng)顟B(tài),并傳入一個(gè)新的 InputDecoration 對(duì)象:

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

和硬件、第三方服務(wù)以及平臺(tái)交互

我怎么和平臺(tái),以及平臺(tái)的原生代碼交互?

Flutter 的代碼并不直接在平臺(tái)之下運(yùn)行,相反,Dart 代碼構(gòu)建的 Flutter 應(yīng)用在設(shè)備上以原生的方式運(yùn)行,卻“側(cè)步躲開(kāi)了”平臺(tái)提供的 SDK。這意味著,例如,你在 Dart 中發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求,它就直接在 Dart 的上下文中運(yùn)行。你并不會(huì)用上平常在 iOS 或 Android 上使用的原生 API。你的 Flutter 程序仍然被原生平臺(tái)的 ViewController 管理作一個(gè) view,但是你并不會(huì)直接訪問(wèn) ViewController 自身,或是原生框架。

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

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

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

我怎么訪問(wèn) GPS 傳感器?

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

我怎么訪問(wèn)攝像頭?

image_picker 在訪問(wèn)攝像頭時(shí)非常常用。

我怎么登錄 Facebook?

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

我怎么使用 Firebase 特性?

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

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

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

如果有一些 Flutter 和社區(qū)插件遺漏的平臺(tái)相關(guān)的特性,可以根據(jù) developing packages and plugins 頁(yè)面構(gòu)建自己的插件。

Flutter 的插件結(jié)構(gòu),簡(jiǎn)要來(lái)說(shuō),就像 Android 中的 Event bus。你發(fā)送一個(gè)消息,并讓接受者處理并反饋結(jié)果給你。在這種情況下,接受者就是在 Android 或 iOS 上的原生代碼。

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

我怎么在 Flutter 中訪問(wèn) UserDefaults?

在 iOS 中,你可以使用屬性列表來(lái)存儲(chǔ)鍵值對(duì)的集合,即我們熟悉的 UserDefaults。

在 Flutter 中,可以使用 Shared Preferences plugin 來(lái)達(dá)到相似的功能。它包裹了 UserDefaluts 以及 Android 上等價(jià)的 SharedPreferences 的功能。

CoreData 相當(dāng)于 Flutter 中的什么?

在 iOS 中,你通過(guò) CoreData 來(lái)存儲(chǔ)結(jié)構(gòu)化的數(shù)據(jù)。這是一個(gè) SQL 數(shù)據(jù)庫(kù)的上層封裝,讓查詢和關(guān)聯(lián)模型變得更加簡(jiǎn)單。

在 Flutter 中,使用 SQFlite 插件來(lái)實(shí)現(xiàn)這個(gè)功能。

通知

我怎么推送通知?

在 iOS 中,你需要向蘋果開(kāi)發(fā)者平臺(tái)中注冊(cè)來(lái)允許推送通知。

在 Flutter 中,使用 firebase_messaging 插件來(lái)實(shí)現(xiàn)這一功能。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容