給Android開發者的Flutter指南 (上) [翻譯]

horseLai發表於2019-08-20

官方英文原文: flutter.io/flutter-for…

提示:由於篇幅很長,所以分為上下兩篇給Android開發者的Flutter指南 (下)已經翻譯完成,感興趣的同學可以看看


一、對應於 View

Flutter中可以將Widget看成View,但是又不能當成是Andriod中的View,可以類比的理解。

View不同的是,Widget的壽命不同,它們是不可變的,直到它們需要改變時才會退出,而每當它們的狀態發生改變時,Flutter框架都會建立新的Widget例項,相比之下,View只會繪製一次直到下一次呼叫invalidate

FlutterWidget是很輕量的,部分原因歸咎於它們的不可變性,因為它們本身不是View檢視,且不會直接繪製任何東西,而是用於描述UI

1. 如何更新Widget

Android中,可以直接改變view以更新它們,但是在Flutter中,widget是不可變的,不能直接更新,而需要通過Widget state來更新。

這就是StatelessWidgetStatefulWidget的來源

  • StatelessWidget 一個沒有狀態資訊的Widget。當描述的使用者介面部分不依賴於物件中的配置資訊時,StatelessWidgets會很有用。這就類似於在Android中使用ImageVIew顯示logo,這個logo並不需要在執行期間做任何改變,對應的在Flutter中就使用StatelessWidget

  • StatefulWidget 如果你想要基於網路請求得到的資料動態改變UI,那麼就使用StatefulWidget,並且告訴Flutter框架,這個WidgetState(狀態)已經發生改變,可以更新Widget了。

值得注意的是,在Flutter核心中,StatelessWidgetStatefulWidget兩者的行為是相同的,它們都會重建每一幀,不同的是StatefulWidget包含了一個State物件,用於儲存跨幀資料,以及恢復幀資料。

如果你心存疑惑,那麼記住這個規則:如果因為使用者互動,控制元件需要改變的話,那麼他就是stateful,而如果控制元件需要響應改變,但是父容器控制元件並不需要自己響應改變,那麼這個父容器依然可以是stateless的。

以下示例展示瞭如何使用StatelessWidget,一個普遍的StatelessWidget就是Text控制元件,如果你檢視了Text的實現,你會發現是StatelessWidget的子類。

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
); 
複製程式碼

如你所見,Text控制元件並沒有與之關聯的狀態資訊,它只是渲染了構建它時傳入的的資料,然後沒了。但是如果你想要讓'I like Flutter!'可以動態改變,比方說響應FloatingActionButton的點選事件,那麼可以將Text包含在一個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),
      ),
    );
  }
}
複製程式碼

2. 如何佈局Widget

Android中,佈局寫在xml中,而在Flutter中,佈局就是控制元件樹(Widget tree),以下示例描述瞭如何佈局一個帶內邊距的控制元件。

@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),
      ),
    ),
  );
}
複製程式碼

可以檢視Flutter提供的 控制元件目錄

3. 如何新增和刪除佈局元件

Android中,可以呼叫父容器的addChildremoveChild動態新增和刪除子view,而在flutter中,因為控制元件都是不可變的,因此沒有直接與addChild行對應的功能,但是可以給父容器傳入一個返回控制元件的函式,然後通過一個boolean標記來控制子控制元件的建立。

例如,以下示例演示瞭如何在點選FloatingActionButton時在兩個控制元件間切換的功能:

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),
      ),
    );
  }
}
複製程式碼

4. 如何給控制元件新增動畫Android中,你可以通過xml來建立動畫,或者呼叫viewanimate()方法。而在Flutter中,則是將控制元件包裹在動畫控制元件內,然後使用動畫庫執行動畫。

Flutter中,使用AnimationController(它是個Animation<double>)可以暫停、定位、停止以及反轉動畫。它需要一個Ticker用於示意(signal)vsync在何時產生,然後在它所執行的幀上產生一個值在[0,1]之間的線性插值,你可以建立一個或多個Animation,然後將他們繫結到控制器上。

比方說,你可能使用一個CurvedAnimation來沿著插值曲線執行動畫,這種情況下,控制器就是動畫進度的“master”資源,而CurvedAnimation則用於計算、替換控制器預設線性動作的曲線,正如Flutter中的Widget一樣,動畫也是組合起來工作的。

當構建控制元件樹時,你為控制元件的動畫屬性指定了Animation ,比方說FadeTransition的不透明度(opacity),接著就是告訴控制器來執行動畫了。以下示例描述瞭如何編寫一個在點選FloatingActionButton時如何將控制元件淡化(fade)成logoFadeTransition

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() {
    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();
        },
      ),
    );
  }
}
複製程式碼

更多資訊,請檢視 Animation & Motion widgets Animations tutorialAnimations overview..

5. 如何使用Canvas畫圖?

Android中,你會通過CanvasDrawable來繪製圖形,Flutter中也有個類似的Canvas API,因為它們都基於底層的渲染引擎Skia,因此在使用FlutterCanvas畫圖操作對於Android開發者來說是件非常熟悉的事情。

Flutter中有兩個幫助繪圖的類:CustomPaintCustomPainter,其中後者用於實現你的繪圖邏輯。

學習如何在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;
}
複製程式碼

6. 如何自定義控制元件?

Android中,典型的方式就是繼承VIew,或者使用已有的View控制元件,然後複寫相關方法以實現期望的行為。而在flutter中,則是通過組合小控制元件的方式自定義View,而不是繼承它們,這在某種程度上跟通過ViewGroup實現自定義控制元件的方式很像,因為所有小元件都是已經存在的,你只是將他們組合起來以提供不一樣的行為,比如,只是自定義佈局邏輯。

例如,你會怎樣實現一個在構造器中傳入標題的CustomButton呢?組合RaisedButtonText,而不是繼承RaisedButton

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(onPressed: () {}, child: Text(label));
  }
}
複製程式碼

然後就可以使用CustomButton了,只需要新增到任意Flutter控制元件中即可:

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}
複製程式碼

二、對應於 Intent

1. Flutter中與Intent相對應的是什麼?

Android中,Intent有兩個主要用途:用於activity間的跳轉、用於元件間的通訊。而在Flutter中,沒有Intent這個概念,雖然你依然可以通過本地整合(native integrations(使用外掛))來啟動Intent

Flutter也沒有與activityfragment直接對應的元件,而是使用NavigatorRoute來進行螢幕間的切換,這跟activity類似。

Route是應用螢幕和頁面的抽象,而Navigator則是一個管理Route的控制元件。可以粗略的將Route看成activity,但是它們含義不同。Navigator通過pushpop(可看成壓棧和出棧)Route來切換螢幕,Navigator工作原理可看成一個棧,push表示向前切換,pop表示返回。

Android中,需要在AndroidManifest.xml中宣告activity,而在Flutter中,你有以下頁面切換選擇:

  • 指定一個包含所有Route名字的MapMaterialApp
  • 直接切換到RouteWidgetApp

如下示例為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的名字直接pushNavigator的方式:

Navigator.of(context).pushNamed('/b');
複製程式碼

另一個使用Intent的使用場景是呼叫外部元件,比如相機、檔案選擇器,對於這種情況,你需要建立一個本地平臺的整合(native platform integration),或者使用已有的外掛;

關於如何構建本地平臺整合,請檢視 Developing Packages and Plugins..

2. Flutter中如何處理來自外部應用的Intent

Flutter可以通過直接訪問Android layer來處理來自AndroidIntent,或者請求共享資料。

在以下示例中,會註冊一個文字共享的Intent過濾器到執行我們Flutter程式碼的本地Activity,然後其他應用就能共享文字資料到我們的Flutter應用。

基本流程就是,我們先在Android本地層(Activity)中先處理這些共享資料,然後等待Flutter請求,而當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準備好處理時,它會通過平臺通道(platform channel)進行請求,接著從本地將資料傳送給它就行了。

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

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

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

複製程式碼

最後,當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. 對應於startActivityForResult的是啥?

Flutter中,Navigator用於處理Rote,也被用於獲取已壓棧Route的返回結果,等push()返回的Future執行結束就能拿到結果了:

Map coordinates = await Navigator.of(context).pushNamed('/location');
複製程式碼

然後,在定位功能的Route中,當使用者選擇完位置資訊後,就可以通過pop()將結果一同返回了:

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
複製程式碼

三、非同步 UI

1. 在Flutter中與runOnUiThread()相對應是什麼?

Dart有個單執行緒執行模型,支援Isolate(一種在其他執行緒執行Dart程式碼的方式)、事件迴圈(event loop)以及非同步程式設計。除非你自己建立一個Isolate,否則你的Dart程式碼都會執行在主UI執行緒,並且由事件迴圈驅動。Flutter中的事件迴圈跟Android主執行緒的Looper是等同的,也就是說,Looper都繫結在UI執行緒。

Dart擁有單執行緒執行模型,但是並不意味著你需要通過這種阻塞式的操作方式執行所有程式碼,這會造成UI被凍結(freeze)。不像Android,需要你在任意時刻都保持主執行緒無阻塞,在Flutter中,可以使用Dart語言提供的非同步特性,如async/await來執行非同步任務。

如下示例,你可以使用Dartasync/await來處理網路請求程式碼,而不在UI中處理:

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

一旦await等待完成了網路請求,就會呼叫setState()方法以更新UI,接著觸發控制元件子樹的重建並更新資料。如下示例描述瞭如何非同步載入資料,然後填充到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);
    });
  }
}
複製程式碼

關於更多後臺執行緒的資訊,以及FlutterAndroid在這一問題上的區別,將在下面描述。

2. 如何將工作任務轉移到後臺執行緒?

Android中,如果你想要訪問網路資料,那麼你需要切換到後臺執行緒中執行,以避免阻塞主執行緒而導致ANR,比如,你會使用AsynctaskLiveDataIntentServiceJobScheduler或者RxJava Scheduler進行後臺執行緒處理。

因為Flutter是個單執行緒模型,並且執行著事件迴圈(event loop,如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中則沒有與之對應的方式,因為await修飾的耗時任務函式,剩餘的工作都交給Dart的事件迴圈去處理了。

然而當你處理大量資料時,你的UI會掛提(hangs),因此在Flutter中需要使用Isolate來充分利用CPU的多核心優勢,以進行耗時任務,或者運算密集型任務。

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;
  }
}
複製程式碼

3. Flutter中對應於OkHttp的是啥?

Flutter中,可以使用http包進行網路請求。

http包中沒有任何與OkHttp相對應的特性,它對我們通常自己實現網路請求的方式進行了更進一步的抽象,使得網路請求更加簡單。

要使用http包,需要在pubspec.yaml中新增如下依賴:

dependencies:
  ...
  http: ^0.11.3+16
複製程式碼

如下建立非同步網路請求:

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. 如何顯示耗時任務的執行進度?

Android中,通常在後臺執行緒中執行耗時任務時,將進度顯示於ProgressBar,而在Flutter中則是使用ProgressIndicator控制元件。通過boolean標記位來控制何時開始渲染,然後在耗時任務開始之前更新它的狀態,並在任務結束時隱藏掉。

下面示例中,build函式分割成了三個不同的子函式,如果showLoadingDialog()返回true,則渲染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);
    });
  }
}

複製程式碼

相關文章