給 Android 開發者的 Flutter 指南

轉載:https://zhuanlan.zhihu.com/p/67500100

這篇文檔旨在幫助 Android 開發者利用既有的 Android 知識來通過 Flutter 開發移動應用。如果你瞭解 Android 框架的基本知識,你就可以使用這篇文檔作爲 Flutter 開發的快速入門。

你的 Android 知識和技能對於 Flutter 開發是非常有用的,因爲 Flutter 依賴於 Android 操作系統的多種功能和配置。Flutter 是一種全新的構建移動界面的方式,但是它有一套和 Android(以及 iOS)進行非 UI 任務通信的插件系統。如果你是一名 Android 專家,你就不必重新學習所有知識才能使用 Flutter。

這篇文檔可以用作隨時查閱以及答疑解惑的專題手冊。

本文結構如下:
1.視圖 (Views)
2.如何在佈局中添加或刪除一個組件?
3.Intents
4.異步 UI
5.工程結構和資源文件
6.Activity 和 Fragment
7.佈局
8.手勢監聽和觸摸事件處理
9.Listviews 和 adapters
10.文字處理
11.表單輸入
12.Flutter 插件
13.主題(Themes)
14.數據庫和本地存儲
15.通知


一、視圖 (Views)

1.1 視圖 在 Flutter 中的對應概念是什麼?

響應式或者聲明式的編程和傳統的命令式風格有什麼不同呢?作爲對比,請查閱 聲明式 UI 介紹

Android 中的 View 是顯示在屏幕上的一切的基礎。按鈕、工具欄、輸入框以及一切內容都是 View。而 Flutter 中 View 的大致對應物是 Widget。Widget 並非完全對應於 Android 中的 View,但是在你熟悉 Flutter 的工作原理的過程中可以把它們看做“聲明和構建 UI 的方式”。

然而,Widget 和 View 還是有一些差異。首先,Widget 有着不一樣的生命週期:它們是不可變的,一旦需要變化則生命週期終止。任何時候 Widget 或它們的狀態變化時,Flutter 框架都會創建一個新的 Widget 樹的實例。對比來看,一個 Android View 只會繪製一次,除非調用 invalidate 纔會重繪。

Flutter 的 Widget 很輕量,部分原因在於它們的不可變性。因爲它們本身既非視圖,也不會直接繪製任何內容,而是 UI 及其底層創建真正視圖對象的語義的描述。

Flutter 支持 Material Components 庫。它提供實現了 Material Design 設計規範 的控件。 Meterial Design 是一套爲所有平臺優化(包括 iOS)的靈活的設計系統。

Flutter 非常靈活、有表達能力,它可以實現任何設計語言。例如,在 iOS 平臺上,你可以使用 Cupertino widgets 創建 Apple 的 iOS 設計語言 風格的界面。

1.2 如何更新 Widget

在 Android 中,你可以直接操作更新 View。然而在 Flutter 中,Widget 是不可變的,無法被直接更新,你需要操作 Widget 的狀態。

這就是有狀態 (Stateful) 和無狀態 (Stateless) Widget 概念的來源。StatelessWidget 如其字面意思—沒有狀態信息的 Widget。

StatelessWidget 用於你描述的用戶界面的一部分不依賴於除了對象中的配置信息以外的任何東西的場景。

例如在 Android 中,這就像顯示一個展示圖標的 ImageView。這個圖標在運行過程中不會改變,所以在 Flutter 中就使用 StatelessWidget

如果你想要根據 HTTP 請求返回的數據或者用戶的交互來動態地更新界面,那麼你就必須使用 StatefulWidget,並告訴 Flutter 框架 Widget 的狀態 (State) 更新了,以便 Flutter 可以更新這個 Widget。

這裏需要着重注意的是,無狀態和有狀態的 Widget 本質上是行爲一致的。它們每一幀都會重建,不同之處在於 StatefulWidget有一個跨幀存儲和恢復狀態數據的 State 對象。

如果你有疑問,那麼記住這條規則:如果一個 Widget 會變化(例如由於用戶交互),它是有狀態的。然而,如果一個 Widget 響應變化,它的父 Widget 只要本身不響應變化,就依然是無狀態的。

下面的例子展示瞭如何使用 StatelessWidgetText Widget 是一個普通的 StatelessWidget。 如果你查看 Text Widget 的實現,你會發現它繼承自 StatelessWidget

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

如上所示,這個 Text Widget 沒有相關聯的狀態信息,它只渲染傳入構造器的信息,僅此而已。

但是,假如你想要動態地改變“I Like Flutter”,例如當你點擊一個 FloatingActionButton 的時候,該怎麼辦呢?

爲了實現這個效果,將 Text Widget 嵌入一個 StatefulWidget 中,並在用戶點擊按鈕的時候更新它。

例如:

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> {
  // Default placeholder text
  String textToShow = "I Like Flutter";

  void _updateText() {
    setState(() {
      // update the text
      textToShow = "Flutter is Awesome!";
    });
  }

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

1.3 如何佈局 Widget?我的 XML 佈局文件在哪裏?

在 Android 中,你通過 XML 文件定義佈局,但是在 Flutter 中,你是通過一個 Widget 樹來定義佈局的。

以下示例展示瞭如何顯示一個帶有填充 (padding) 的簡單 Widget:

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

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

二、如何在佈局中添加或刪除一個組件?

在 Android 中,你通過調用父 View 的 addChild() 或 removeChild() 方法動態地添加或者刪除子 View。在 Flutter 中,由於 Widget 是不可變的,所以沒有 addChild() 的直接對應的方法。不過,你可以給返回一個 Widget 的父 Widget 傳入一個方法,並通過布爾標記值控制子 Widget 的創建。

例如,下面就是你可以如何在點擊一個 FloatingActionButton 的時候在兩個 Widget 之間切換。

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> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return MaterialButton(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),
      ),
    );
  }
}

2.1 Widget 如何實現動畫?

在 Android 中,你既可以通過 XML 文件定義動畫,也可以調用 View 對象的 animate() 方法。在 Flutter 裏,則使用動畫庫,通過將 Widget 嵌入一個動畫 Widget 的方式實現 Widget 的動畫效果。

Flutter 通過 Animation<double> 的子類 AnimationController 來暫停、播放、停止以及逆向播放動畫。它需要一個 Ticker 在垂直同步 (vsync) 的時候發出信號,並且在運行的時候創建一個介於 0 和 1 之間的線性插值。然後你就可以創建一個或多個 Animation,並將它們綁定到控制器上。

例如,你可以使用 CurvedAnimation 來實現一個曲線插值的動畫。在這種情況下,控制器決定了動畫進度,CurvedAnimation 計算用於替換控制器默認線性動畫的曲線值。和 Widget 一樣,Flutter 中的動畫效果也可以組合使用。

在構建 Widget 樹的時候,你需要將 Animation 對象賦值給某個 Widget 的動畫屬性,例如 FadeTransition 的不透明度屬性,並讓控制器開始動畫。

下面的例子展示瞭如何實現一個點擊 FloatingActionButton 的時候將一個 Widget 漸變爲一個圖標的 FadeTransition

import 'package:flutter/material.dart';

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

class FadeAppTest 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() {
    super.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();
        },
      ),
    );
  }
}

獲取更多內容,請查看動畫 Widget, 動畫指南,以及 動畫概覽

2.2 如何使用 Canvas 進行繪製?

在 Android 中,你可以使用 Canvas 和 Drawable 將圖片和形狀繪製到屏幕上。Flutter 也有一個類似於 Canvas 的 API,因爲它基於相同的底層渲染引擎 Skia。因此,在 Flutter 中用畫布 (canvas) 進行繪製對於 Android 開發者來說是一件非常熟悉的工作。

Flutter 有兩個幫助你用畫布 (canvas) 進行繪製的類:CustomPaint 和 CustomPainter,後者可以實現自定義的繪製算法。

如果想學習在 Flutter 中如何實現一個簽名功能,可以查看 Collin 在 StackOverflow 上的回答。

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: DemoApp()));

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => Scaffold(body: Signature());
}

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

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

2.3 如何創建自定義 Widget?

在 Android 中,一般通過繼承 View 類,或者使用已有的視圖類,再覆寫或實現可以達到特定效果的方法。

在 Flutter 中,通過 組合 更小的 Widget 來創建自定義 Widget(而不是繼承它們)。這和 Android 中實現一個自定義的 ViewGroup有些類似,所有的構建 UI 的模塊代碼都在手邊,不過由你提供不同的行爲—例如,自定義佈局 (layout) 邏輯。

舉例來說,你該如何創建一個在構造器接收標籤參數的 CustomButton?你要組合 RaisedButton 和一個標籤來創建自定義按鈕,而不是繼承RaisedButton

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

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

然後就像使用其它 Flutter Widget 一樣使用 CustomButton

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

三、Intents

3.1 Intent 在 Flutter 中的對應概念是什麼?

在 Android 中,Intent 主要有兩個使用場景:在 Activity 之前進行導航,以及組件間通信。 Flutter 卻沒有 intent 這樣的概念,但是你依然可以通過原生集成 插件) 來啓動 intent。

Flutter 實際上並沒有 Activity 和 Fragment 的對應概念。在 Flutter 中你需要使用 Navigator 和 Route 在同一個 Activity 內的不同界面間進行跳轉。

Route 是應用內屏幕和頁面的抽象,Navigator 是管理路徑 route 的工具。一個 route 對象大致對應於一個 Activity,但是它的含義是不一樣的。Navigator 可以通過對 route 進行壓棧和彈棧操作實現頁面 的跳轉。Navigator 的工作原理和棧相似,你可以將想要跳轉到的 route 壓棧 (push()),想要返回的時候將 route 彈棧 (pop())。

在 Android 中,在應用的 AndroidManifest.xml 文件中聲明 Activity。

在 Flutter 中,你有多種不同的方式在頁面間導航:

  • 定義一個 route 名字的 Map。(MaterialApp)
  • 直接導航到一個 route。(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'),
    },
  ));
}

通過將 route 名壓棧 (push) 到 Navigator 中來跳轉到這個 route。

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

Intent 的另一種常見的使用場景是調用外部的組件,例如相機或文件選擇器。對於這種情況,你需要創建 一個原生平臺集成(或者使用 已有的插件)。

想要學習如何創建一個原生平臺集成,請查看開發包和插件

3.2 在 Flutter 中應該如何處理從外部應用接收到的 intent?

Flutter 可以通過直接和 Android 層通信並請求分享的數據來處理接收到的 Android intent。

下面的例子中,運行 Flutter 代碼的原生 Activity 註冊了一個文本分享的 intent 過濾器,這樣其它應用就可以和 Flutter 應用分享文本了。

從以上流程可以得知,我們首先在 Android 原生層面(在我們的 Activity 中)處理分享的文本數據,然後 Flutter 再通過使用 MethodChannel 獲取這個數據。

首先,在 AndroidManifest.xml 中註冊 intent 過濾器:

<activity
  android:name=".MainActivity"
  android:launchMode="singleTop"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize">
  <!-- ... -->
  <intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
  </intent-filter>
</activity>

接着在 MainActivity 中處理 intent,提取出其它 intent 分享的文本並保存。當 Flutter 準備好處理的時候,它會使用一個平臺通道請求數據,數據便會從原生端發送過來:

package com.example.shared;

import android.content.Intent;
import android.os.Bundle;

import java.nio.ByteBuffer;

import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {

  private String sharedText;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType();

    if (Intent.ACTION_SEND.equals(action) && type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent); // Handle text being sent
      }
    }

    new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler(
      new MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall call, MethodChannel.Result result) {
          if (call.method.contentEquals("getSharedText")) {
            result.success(sharedText);
            sharedText = null;
          }
        }
      });
  }

  void handleSendText(Intent intent) {
    sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
  }
}

最後,當 Widget 渲染的時候,從 Flutter 這端請求數據:

import 'package:flutter/material.dart';
import 'package:flutter/services.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 Shared App Handler',
      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> {
  static const platform = const MethodChannel('app.channel.shared.data');
  String dataShared = "No data";

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text(dataShared)));
  }

  getSharedText() async {
    var sharedData = await platform.invokeMethod("getSharedText");
    if (sharedData != null) {
      setState(() {
        dataShared = sharedData;
      });
    }
  }
}

3.3 startActivityForResult() 的對應方法是什麼?

Navigator 類負責 Flutter 的導航,並用來接收被壓棧的 route 的返回值。這是通過在 push()後返回的 Future 上 await 來實現的。

例如,要打開一個讓用戶選擇位置的 route,你可以這樣做:

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

然後,在你的位置 route 內,一旦用戶選擇了位置,你就可以彈棧 (pop) 並返回結果:

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

四、異步 UI

4.1 runOnUiThread() 在 Flutter 中的對應方法是什麼?

Dart 有一個單線程執行的模型,同時也支持 Isolate(在另一個線程運行 Dart 代碼的方法), 它是一個事件循環和異步編程方式。除非你創建一個 Isolate,否則你的 Dart 代碼會運行在主 UI 線程,並被一個事件循環所驅動。Flutter 的事件循環對應於 Android 裏的主 Looper—也即綁定到主線程上的 Looper

Dart 的單線程模型並不意味着你需要以會導致 UI 凍結的阻塞操作的方式來運行所有代碼。不同於 Android 中 需要你時刻保持主線程空閒,在 Flutter 中,可以使用 Dart 語言提供的異步工具,例如 async/await 來 執行異步任務。如果你使用過 C# 或者 Javascript 中的 async/await 範式,或者 Kotlin 中的協程,你應該對它比較熟悉。

例如,你可以通過使用 async/await 來運行網絡代碼而且不會導致 UI 掛起,同時讓 Dart 來處理背後的繁重細節:

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

一旦用 await 修飾的網絡操作完成,再調用 setState() 更新 UI,這會觸發 Widget 子樹的重建並更新數據。

下面的例子展示了異步加載數據並將之展示在 ListView 內:

import 'dart:convert';

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

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

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

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

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

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

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

    loadData();
  }

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

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

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

參考下一節內容獲取更多關於後臺任務以及 Flutter 與 Android 的差異的信息。

4.2 如何將任務轉移到後臺線程?

在 Android 中,當你想要訪問一個網絡資源卻又不想阻塞主線程並避免 ANR 的時候,你一般會將任務放到一個後臺線程中運行。例如,你可以使用一個 AsyncTask、一個 LiveData、一個 IntentService、 一個 JobScheduler 任務或者通過 RxJava 的管道用調度器將任務切換到後臺線程中。

由於 Flutter 是單線程並且運行一個事件循環(類似 Node.js),你無須擔心線程的管理以及後臺線程的創建。如果你在執行和 I/O 綁定的任務,例如存儲訪問或者網絡請求,那麼你可以安全地使用 async/await, 並無後顧之憂。再例如,你需要執行消耗 CPU 的計算密集型工作,那麼你可以將其轉移到一個 Isolate 上以避免阻塞事件循環,就像你在 Android 中會將任何任務放到主線程之外一樣。

對於和 I/O 綁定的任務,將方法聲明爲 async 方法,並在方法內 await 一個長時間運行的任務:

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

這就是你一般應該如何執行網絡和數據庫操作,它們都屬於 I/O 操作。

在 Android 中,當你繼承 AsyncTask 的時候,你一般會覆寫三個方法,onPreExecute()doInBackground() 和onPostExecute()。Flutter 中沒有對應的 API,你只需要 await 一個耗時方法調用, Dart 的事件循環就會幫你處理剩下的事情。

然而,有時候你可能需要處理大量的數據並掛起你的 UI。在 Flutter 中,可以通過使用 Isolate來利用多核處理器的優勢執行耗時或計算密集的任務。

Isolate 是獨立執行的線程,不會和主執行內存堆分享內存。這意味着你無法訪問主線程的變量,或者調用 setState() 更新 UI。不同於 Android 中的線程,Isolate 如其名所示,它們無法分享內存(例如通過靜態變量的形式)。

下面的例子展示了一個簡單的 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;
  }
}

4.3 OkHttp 在 Flutter 中的對應物是什麼?

Flutter中使用流行的 http 包 進行網絡請求是很簡單的。

雖然 http 包沒有 OkHttp 中的所有功能,但是它抽象了很多通常你會自己實現的網絡功能,這使其本身在執行網絡請求時簡單易用。

如果要使用 http 包,需要在 pubspec.yaml 文件中添加依賴:

dependencies:
  ...
  http: ^0.11.3+16

如果要發起一個網絡請求,在異步 (async) 方法 http.get() 上調用 await 即可:

import 'dart:convert';

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

4.4 如何爲耗時任務顯示進度?

在 Android 中你通常會在後臺執行一個耗時任務的時候顯示一個 ProgressBar 在界面上。

在 Flutter 中,我們使用 ProgressIndicator Widget。通過代碼邏輯使用一個布爾標記值控制進度條的渲染。

在下面的例子中,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);
    });
  }
}

五、工程結構和資源文件

5.1 在哪裏放置分辨率相關的圖片文件?

雖然 Android 區分對待資源文件 (resources) 和資產文件 (assets),但是 Flutter 應用只有資產文件 (assets)。 所有原本在 Android 中應該放在 res/drawable-* 文件夾中的資源文件,在 Flutter 中都放在一個 assets 文件夾中。

Flutter 遵循一個簡單的類似 iOS 的密度相關的格式。文件可以是一倍 (1.0x)、兩倍 (2.0x)、三倍 (3.0x) 或其它的任意倍數。Flutter 沒有 dp 單位,但是有邏輯像素尺寸,基本和設備無關的像素尺寸是一樣的。名稱爲 devicePixelRatio 的尺寸表示在單一邏輯像素標準下設備物理像素的比例。

和 Android 的密度分類的對照表如下:

Android 密度修飾符

| Flutter 像素比例 — | — ldpi | 0.75x mdpi | 1.0x hdpi | 1.5x xhdpi | 2.0x xxhdpi3.0x xxxhdpi | 4.0x

文件放置於任意文件夾中—Flutter 沒有預先定義好的文件夾結構。你在 pubspec.yaml 文件中定義文件(包括位置信息),Flutter 負責找到它們。

需要注意的是,在 Flutter 1.0 beta 2 之前,在 Flutter 中定義的文件不能被原生端訪問,反之亦然,原生端定義的資產文件 (assets) 和資源文件 (resources) 也無法被 Flutter 訪問,因爲它們是放置於不同的文件夾中的。

至於 Flutter beta 2,文件是放置於原生端的 asset 文件夾中,所以可以被原生端的 AssetManager 訪問:

val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")

然而對於 Flutter beta 2,Flutter 依然無法訪問原生資源文件(resources),也無法訪問原生資產文件(assets)。

如果你要向 Flutter 項目中添加一個新的叫 my_icon.png 的圖片資源,並且將其放入我們隨便起名的叫做 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");
}

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

Flutter 當下並沒有一個特定的管理字符串的資源管理系統。目前來講,最好的辦法是將字符串作爲靜態域存放在類中,並通過類訪問它們。例如:

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

接着在你們的代碼中,你可以這樣訪問你的字符串:

Text(Strings.welcomeMessage)

Flutter 在 Android 上提供無障礙的基本支持,但是這個功能當下仍在開發。

我們鼓勵 Flutter 開發者使用 intl 包進行國際化和本地化。

5.3 Gradle 文件的對應物是什麼?我該如何添加依賴?

在 Android 中,你在 Gradle 構建腳本中添加依賴。Flutter 使用 Dart 自己的構建系統以及 Pub 包管理器。 構建工具會將原生 Android 和 iOS 殼應用的構建代理給對應的構建系統。

雖然在你的 Flutter 項目的 android 文件夾下有 Gradle 文件,但是它們只用於給對應平臺的集成添加原生依賴。一般來說,在 pubspec.yaml 文件中定義在 Flutter 裏使用的外部依賴。Pub 是查找 Flutter 包的好地方。

六、Activity 和 Fragment

6.1 Activity 和 Fragment 在 Flutter 中的對應概念是什麼?

在 Android 中,一個 Activity 代表用戶可以完成的一件獨立任務。一個 Fragment 代表一個 行爲或者用戶界面的一部分。Fragment 用於模塊化你的代碼,爲大屏組合複雜的用戶界面,並適配應用的界面。在 Flutter 中,這兩個概念都對應於 Widget

如果要學習更多的關於 Activity 和 Fragment 創建界面的內容,請閱讀社區貢獻的 Medium 文章,給 Android 開發者的 Flutter 指南:如何在 Flutter 中設計一個 Activity 界面

就如在 Intents 部分所提,Flutter 中的界面 都是以 Widget 表示的,因爲 Flutter 中一切皆爲 Widget。你使用 Navigator 在表示不同屏幕或頁面,或者僅僅是相同數據的不同狀態和渲染的各個 Route 之間進行導航。

6.2 如何監聽 Android Activity 的生命週期事件?

在 Android 中,你可以覆寫 Actvity 的生命週期方法來監聽其生命週期,也可以在 Application 上 註冊 ActivityLifecycleCallbacks。在 Flutter 中,這兩種方法都沒有,但是你可以通過綁定 WidgetsBinding 觀察者並監聽 didChangeAppLifecycleState() 的變化事件來監聽生命週期。

可以被觀察的生命週期事件有:

  • inactive — 應用處於非活躍狀態並且不接收用戶輸入。這個事件只適用於 iOS,Android 上沒有對應的事件
  • paused — 應用當前對用戶不可見,無法響應用戶輸入,並運行在後臺。這個事件對應於 Android 中的 onPause()
  • resumed — 應用對用戶可見並且可以響應用戶的輸入。這個事件對應於 Android 中的 onPostResume()
  • suspending — 應用暫時被掛起。這個事件對應於 Android 中的 onStop;iOS 上由於沒有對應的事件, 因此不會觸發此事件

想要了解這些狀態含義的更多細節,請查看 AppLifecycleStatus 文檔

你可能已經注意到,只有一小部分的 Activity 生命週期事件是可用的;雖然 FlutterActivity在內部捕獲了幾乎所有的 Activity 生命週期事件並將它們發送給 Flutter 引擎,但是它們大部分都向你屏蔽了。 Flutter 爲你管理引擎的啓動和停止,在大部分情況下幾乎沒有理由要在 Flutter 一端監聽 Activity 的生命週期。 如果你需要通過監聽生命週期來獲取或釋放原生的資源,你無論如何都應該在原生一端做這件事。

下面的例子展示瞭如何監聽容器 Activity 的生命週期狀態:

import 'package:flutter/widgets.dart';

class LifecycleWatcher extends StatefulWidget {
  @override
  _LifecycleWatcherState createState() => _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
  AppLifecycleState _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _lastLifecycleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecycleState == null)
      return Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);

    return Text('The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
        textDirection: TextDirection.ltr);
  }
}

void main() {
  runApp(Center(child: LifecycleWatcher()));
}

七、佈局

7.1 LinearLayout 的對應概念是什麼?

在 Android 中,LinearLayout 用於線性佈局你的控件—水平或者垂直。 在 Flutter 中,使用 Row 或者 Column Widget 來實現相同的效果。

如果你注意看的話,會發現下面的兩段代碼除了 Row 和 Column Widget 以外是一模一樣的。它們的孩子是一樣的,而這個特性可以被充分利用來開發包含有相同的孩子但是會隨時間改變的複雜佈局。

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

 

@override
Widget build(BuildContext context) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Column One'),
      Text('Column Two'),
      Text('Column Three'),
      Text('Column Four'),
    ],
  );
}

如果想學習更多的構建線性佈局的內容,請閱讀社區貢獻的 Medium 文章 給 Android 開發者的 Flutter 指南:如何在 Flutter 中設計線性佈局?

7.2 RelativeLayout 的對應概念是什麼?

RelativeLayout 通過 Widget 的相互位置對它們進行佈局。在 Flutter 中, 有幾種實現相同效果的方法。

你可以通過組合使用 Column、Row 和 Stack Widget 實現 RelativeLayout 的效果。你還可以在 Widget 構造器內聲明孩子相對父親的佈局規則。

Collin 在 StackOverflow 上的回答是一個在 Flutter 中構建相對佈局的好例子。

7.3 ScrollView 的對應概念是什麼?

在 Android 中,使用 ScrollView 佈局控件—如果用戶的設備屏幕比應用的內容區域小,用戶可以滑動內容。

在 Flutter 中,實現這個功能的最簡單的方法是使用 ListView Widget。從 Android 的角度看,這樣做可能是殺雞用牛刀了,但是 Flutter 中 ListView Widget 既是一個 ScrollView,也是一個 Android 中的 ListView。

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

7.4 在 Flutter 中如何處理屏幕旋轉?

FlutterView 會處理配置的變化,前提條件是在 AndroidManifest.xml 文件中聲明瞭:

android:configChanges="orientation|screenSize"

八、手勢監聽和觸摸事件處理

8.1 Flutter 中如何爲一個 Widget 添加點擊監聽器?

在 Android 中,你可以通過調用 setOnClickListener 方法在按鈕這樣的 View 上添加點擊監聽器。

在 Flutter 中有兩種添加觸摸監聽器的方法:

如果 Widget 支持事件監聽,那麼向它傳入一個方法並在方法中處理事件。例如,RaisedButton 有 一個 onPressed 參數:
@override
Widget build(BuildContext context) {
  return RaisedButton(
      onPressed: () {
        print("click");
      },
      child: Text("Button"));
}

如果 Widget 不支持事件監聽,將 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");
        },
      ),
    ));
  }
}

8.2 如何處理 Widget 上的其它手勢?

使用 GestureDetector 可以監聽非常多的手勢,例如:

  • Tap
    • onTapDown - 一個可能產生點擊事件的指針觸摸到屏幕的特定位置。
    • onTapUp - 一個產生了點擊事件的指針停止觸摸屏幕的特定位置。
    • onTap - A tap has occurred.
    • onTap - 一個點擊事件已經發生。
    • onTapCancel - 之前觸發了 onTapDown 事件的指針不會產生點擊事件。

 

  • Double tap
    • onDoubleTap - 用戶在屏幕同一位置連續快速地點擊兩次。

 

  • Long press
    • onLongPress - 指針在屏幕的同一位置保持了一段較長時間的觸摸狀態。

 

  • Vertical drag
    • onVerticalDragStart - 指針已經觸摸屏幕並可能開始垂直移動。
    • onVerticalDragUpdate - 觸摸屏幕的指針在垂直方向移動了更多的距離。
    • onVerticalDragEnd - 之前和屏幕接觸並垂直移動的指針不再繼續和屏幕接觸,並且在和屏幕停止接觸的時候以一定的速度移動。

 

  • Horizontal drag
    • onHorizontalDragStart - 指針已經觸摸屏幕並可能開始水平移動。
    • onHorizontalDragUpdate - 觸摸屏幕的指針在水平方向移動了更多的距離。
    • onHorizontalDragEnd - 之前和屏幕接觸並水平移動的指針不再繼續和屏幕接觸,並且在和屏幕停止 接觸的時候以一定的速度移動。

 

下面的例子展示了一個實現了雙擊旋轉 Flutter 標誌的 GestureDetector

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

九、Listviews 和 adapters

9.1 ListView 在 Flutter 中的對應概念是什麼?

Flutter 中 ListView 的對應概念仍然是…ListView!

使用 Android 的 ListView 時,創建一個 adapter 並將其傳給 ListView,ListView 渲染 adapter 返回的每一行內容。然後,你需要確保回收了每一行視圖,否則,你會遇到各種奇怪的 界面和內存問題。

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

9.2 如何知道點擊了哪個列表項?

在 Android 中,ListView 有一個可以幫助你定位哪個列表項被點擊了的方法 onItemClickListener。在 Flutter 中,則使用傳入 Widget 的觸摸監聽。

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

9.3 如何動態更新 ListView?

在 Android 中,你需要更新 adapter 並調用 notifyDataSetChanged

在 Flutter 中,如果你準備在 setState() 裏更新一組 Widget,你很快會發現你的數據並沒有 更新到界面上。這是因爲當 setState() 被調用的時候,Flutter 渲染引擎會查看 Widget 樹 是否有任何更改。當引擎檢查到 ListView,他會執行 == 檢查,並判斷兩個 ListView 是一樣的。沒有任何更改,所以也就不需要更新。

更新 ListView 的一個簡單方法是,在 setState() 裏創建一個新的 List,並將數據從舊列表拷貝到新列表。雖然這個方法很簡單,就如下面例子所示,但是並不推薦在大數據集的時候使用。

import 'package:flutter/material.dart';

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

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

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

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

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

  @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。這個方法非常適用於 動態列表或者擁有大量數據的列表。這基本上就是 Android 裏的 RecyclerView,會爲你自動回收列表項:

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 = <Widget>[];

  @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 方法和 Android adapter 裏的 getView 方法類似;它通過位置返回你期望在這個位置渲染的列表項。

最後也是最重要的一條,需要注意 onTap() 方法不再重建列表項,但是會執行 .add 操作。

十、文字處理

10.1 如何爲 Text Widget 設置自定義字體?

在 Android SDK 中(從 Android O 開始),你可以創建一個字體資源文件並將其傳給 TextView 的 FontFamily 參數。

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

10.2 如何更改 Text Widget 的樣式?

除了字體,你還可以自定義 Text Widget 的其它樣式元素。Text Widget 的樣式參數接收一個 TextStyle 對象,你可以在這個對象裏自定義很多參數,例如:

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

十一、表單輸入

如果需要更多使用表單的信息,請查看 Flutter Cookbook 中的 檢索一個文本字段的值

11.1 Input 的“提示”(“hint”)的對應概念是什麼?

在 Flutter 中,你可以簡單地通過向 Text Widget 構造器的 decoration 參數傳入一個 InputDecoration 對象來爲輸入框展示一個“提示”或佔位文本。

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

11.2 如何顯示驗證錯誤的信息?

就像上面實現“提示”功能一樣,像 Text Widget 構造方法的 decoration 參數傳入一個 InputDecoration 對象。

然而,你並不想一開始就顯示錯誤信息。相反,當用戶輸入了無效的信息後,更新狀態並傳入一個新的 InputDecoration 對象。

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> {
  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(emailRegexp);

    return regExp.hasMatch(em);
  }
}

十二、Flutter 插件

12.1 如何使用 GPS 傳感器?

使用 geolocator 社區插件。

12.2 如何使用相機?

image_picker 插件被常用於相機功能的使用。

12.3 如何使用 Facebook 登錄?

使用 flutter_facebook_login 社區插件實現 Facebook 登錄功能。

12.4 如何使用 Firebase 的功能?

官方插件 提供了 Firebase 的大多數功能。 這些插件都是由 Flutter 團隊維護的官方集成插件:

你可以在 Pub(https://pub.dev/flutter) 網站上查找一些官方插件沒有直接支持的功能的第三方 Firebase 插件。

12.5 如何創建自己的自定義原生集成插件?

如果有 Flutter 官方或社區第三方插件沒有涵蓋的平臺特定的功能,你可以根據 開發包和插件 頁面 創建自己的插件。

Flutter 的插件架構,簡而言之,和 Android 中的事件總線的使用非常相似:你發送一個消息,並讓接受者處理並返回一個結果給你。在這種情況下,接受者是運行在 Android 或 iOS 原生端的代碼。

12.6 如何在 Flutter 應用中使用 NDK?

如果你在現有的 Android 應用中使用 NDK,並且希望你的 Flutter 應用可以利用你的 native 庫,這可以通過創建一個自定義插件實現。

你的自定義插件首先和你的 Android 應用通信,Android 應用會通過 JNI 調用 native 方法。一旦有返回值,就可以向 Flutter 發送回一個消息並渲染結果。

暫時還不支持從 Flutter 中直接調用 native 代碼。

十三、主題(Themes)

13.1 如何對應用使用主題?

Flutter 提供開箱即用的優美的 Material Design 實現,可以滿足你通常需要的各種樣式和主題的需求。 不同於 Android 中你在 XML 文件中定義主題並在 AndroidManifest.xml 中將其賦值給你的應用, Flutter 中是在頂層 Widget 上聲明主題。

爲了在應用中利用好 Material 組件,你可以在應用中聲明一個頂層 Widget MeterialApp 作爲入口。 MaterialApp 是一個包裝了一系列 Widget 的爲你給予便利的 Widget,而這些 Widget 通常是實現 Material Design 的應用所必須的。它基於 WidgetsApp 並添加了 Material 相關的功能。

你也可以使用 WidgetApp 作爲應用的 Widget,它會提供一些相同的功能,但是不如 MaterialApp 提供的功能豐富。

如果要自定義任意子組件的顏色或者樣式,給 MaterialApp Widget 傳入一個 ThemeData 對象即可。 例如,在下面的代碼中,主色調設置爲藍色,文本選中顏色設置爲紅色。

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

十四、數據庫和本地存儲

14.1 如何使用 Shared Preferences?

在 Android 中,你可以使用 SharedPreferences API 來存儲少量的鍵值對。

在 Flutter 中,使用 Shared_Preferences 插件 實現此功能。 這個插件同時包裝了 Shared Preferences 和 NSUserDefaults(iOS 平臺對應 API)的功能。

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: RaisedButton(
            onPressed: _incrementCounter,
            child: Text('Increment Counter'),
          ),
        ),
      ),
    ),
  );
}

_incrementCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  print('Pressed $counter times.');
  prefs.setInt('counter', counter);
}

14.2 在 Flutter 中如何使用 SQLite?

在 Android 中,你會使用 SQLite 來存儲可以通過 SQL 進行查詢的結構化數據。

在 Flutter 中,使用 SQFlite 插件實現此功能。

十五、通知

15.1 如何設置推送通知?

在 Android 中,你會使用 Firebase Cloud Messaging 來爲應用設置推送通知。

在 Flutter 中,則使用 Firebase_Messaging 插件實現此功能。想要獲得更多關於使用 Firebase Cloud Messaging API 的信息,請查閱 firebase_messaging 插件文檔。

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