【譯】Dart/Flutter中的非同步程式設計

JarvanMo發表於2020-05-24

背景

好久沒有更文了,為避免讓廣大朋友產生我消失了的錯覺,所以就又水一文,能幫助一個是一個。 本來想自己寫寫文章,後來發現翻譯也不錯。這裡是英文原文

簡述

final myFuture = http.get("https://example.com");
複製程式碼

正如上面的程式碼,很多Dart非同步API都是返回的FutureFuture也是Dart非同步程式設計中最基礎的一個概念。總地來說,Dart中的Future和其他程式語言中的future或者promise大同小異。

本文將圍繞Future背後的概念以及如何使用Future展開。也會講Flutter中的FutureBuilder控制元件,這個控制元件會基於Future狀態來幫助你非同步地更新Flutter UI

得益於Dartasync-await這樣的特性,我們可能永遠不會直接使用Future這個API。但是我們也很難避免在Dart程式碼中遇到Future,畢竟我們可能要建立Future或者閱讀一些和Future有關的程式碼。

如何理解Future

我們可以簡單地將資料比喻成禮物。現在有個朋友要送給我們一個禮盒,當禮盒裝好的那一刻,這個過程就開始了。一段時間後我們需要開啟這個神祕盒子,裡面的禮物可能完好無損也可能是損壞了,也就是說當一個Future完成後,對應的結果可能是我們期望的資料,也可能是一個錯誤。

我們可以這個過程歸納成三種狀態:

  • 未完成:禮盒封裝好了。
  • 完成了並且得到了對應的值:禮盒開啟,並且我們的禮物(data)已經準備好了。
  • 完成了但發生了錯誤:禮盒開啟,但禮物卻損壞了(error)。

絕大部分情況下,我們無非都是圍繞著這三種狀態進行一些處理。當我們接收一個Future時,我們會一直等到我們開啟禮盒,然後我們才會決定如何處理,比如說可以正常接收到值時,我們應該如何處理,又或者說,當發生錯誤的時候我們又要怎麼做。我們可以經常看到1-2-3這樣的過程:

【譯】Dart/Flutter中的非同步程式設計

說到Future我們不得不提到Event-Loop(如下圖所示,也可以在上面的系列視訊中學習)。關於Future,我們需要知道,Future只是一個幫助我們可以更簡單使用Event-Loop的API。

Event-Loop示意圖

我們寫的Dart程式碼是由單一執行緒執行的。當我們的應用在執行時,這個執行緒一直在不停地執行啊執行,然後不停得從事件佇列(Event Queue)中拾取事件並對事件進行處理。

為了更好解釋Event-LoopFuture,我們看個簡單的例子。

假如說,我們要實現一個下載功能,當使用者點選了按鈕,程式會自動下載一下圖片。,我們用RaisedButton簡單實現一下:

RaisedButton(
  onPressed: () {
    final myFuture = http.get('https://my.image.url');
    myFuture.then((resp) {
      setImage(resp);
    });
  },
  child: Text('Click me!'),
)
複製程式碼

我們一起理理這個過程。 首先,觸發點選按鈕事件。Event Loop捕獲了點選事件,然後呼叫了點選監聽器(就是在RaisedButton建構函式中傳入的onPressed)。我們的onPressed使用了http庫進行了一次HTTP請求(http.get()),並且這個請求返回了一個Future(myFuture)。

現在我們已經得到了我們的禮盒--myFuture。現在禮盒已經裝好了。為了監聽開啟禮盒的回撥,我們要使用then()

一旦我們裝好了禮盒,我們就需要等待。也話在這個期間有其他的事件進入Event-Loop,使用者做了一些其他的事情,你的禮盒還放在那裡的同時,Event-Loop依然保持執行。

最終,圖片資料被下載下來,然後http庫會告訴我們“好極了,我已經拿了Future”。然後它把資料裝進禮盒中並把禮盒開啟,這時就觸發了我們的回撥。

現在,then()中的程式碼片斷就被執行了,並向使用者顯示圖片。

整個過程中,不管有什麼其他的任務正在進行或者有其他任務進入,我們的程式碼從來也沒有直接接觸Event-Loop。這個過程不需要關心有什麼其他任務在進行,或者有什麼其他事件進入。我們所要作的就是從http庫得到Future,然後告訴程式在Futuer完成後需要做什麼。

在現實的程式碼中,我們還要關注錯誤。我們稍後涉及到這點。


現在我們更近一步地瞭解一下FutureAPI,有些正是我們剛剛看到的。

第一個問題,我們怎麼得到一個Future例項?大部分情況下,我們不會直接建立Future。因為大部分常見的非同步程式設計任務已經有對應的庫了,這些庫可以為我們生成Future

比如說,網路請求返回了一個Future

final myFuture = http.get('http://example.com');
複製程式碼

得到一個shared preferences也返回一個Future

final myFuture = SharedPreferences.getInstance();
複製程式碼

當然了,我們也可以通過Future的構造方法建立Future

Future構造方法

最簡單的Future構造方法是Future(), 這個構造方法的引數是一個函式,並且返回一個和該函式返回值型別一樣的Future。過一會這個函式會非同步地執行,並且這個Future完成時會返回該函式的值。看一下Future()的例子:

void main() {
  final myFuture = Future(() {
    return 12;
  });
}
複製程式碼

讓我們加入一些列印語句,這樣會讓非同步部分更加明顯:

void main() {
  final myFuture = Future(() {
    print('Creating the future.'); // Prints second.
    return 12;
  });
  print('Done with main().'); // Prints first.
}
複製程式碼

如果我們在DartPad上執行這段程式碼,整個main函式會在傳給Future()構造方法的函式結束前結束。這是因為Future()的構造方法恰好先返回了一個未完成的Future。這意味著,“這是個盒子。你現在需要拿著他,然後過一會我會執行你的函式並且把一些資料裝進去”,我們再看一下之前程式碼的輸出結果:

Done with main().
Creating the future.
複製程式碼

另一個構造方法是Future.value(),它是用來處理你已經知道Future返回值的。這個構造方法在我們構建使用了快取的服務時很有用。有的時候我們已經持有我們需要地值了,所以我們可以直接返回:

final myFuture = Future.value(12);
複製程式碼

Future.value()還一個相對立構造方法,這個構造方法在完成時會返回一個錯誤。它是Future.error(),而它的工作原理也基本相同,但是這個構造方法會承載一個錯誤物件和一個可選的stacktrace

final myFuture = Future.error(ArgumentError.notNull('input'));
複製程式碼

最方便的構造方法可能是Future.delay()了。它和Future()一樣,只不過它先會等待一段指定的時間後,再執行傳入的函式然後再完成Future

Future.delay()的一種使用場景就是當我們在測試時需要mock網路服務。如果我們確保載入指示器可以正確顯示,那麼這個Future.delay()就有用武之地了:

final myFuture = Future.delayed(
  const Duration(seconds: 5),
  () => 12,
);
複製程式碼

使用Future

我們現在已經對Future有個基本地瞭解,現在我們要學習一下怎麼使用。正好我們之前所說,使用Future基本上就是圍繞著三種狀態:未完成完成了並且得到了對應的值完成了但發生了錯誤

下面的程式碼使用Future.delay()建立了一個Future,功能是在3s後Future完成並返回100。

void main() {
  Future.delayed(
    const Duration(seconds: 3),
    () => 100,
  );
  print('Waiting for a value...');
}
複製程式碼

當這段程式碼執行時,main()從上到下執行,建立一個Future並列印Waiting for a value...,這個時候Future還沒完成。再過三秒鐘它也不會完成。

為了使用Future返回的值,我們可以使用then()。我們可以通過then()註冊一個回撥,通過這個回撥我們可以獲取Future完成時的資料。我們給then()傳入一個函式,這個函式只有一個引數,引數的型別和Future的返回值型別一樣。一旦Future完成了返回並返回資料,這個函式就會被呼叫並將對應的資料傳遞過去:

void main() {
  Future.delayed(
    const Duration(seconds: 3),
    () => 100,
  ).then((value) {
    print('The value is $value.'); // Prints later, after 3 seconds.
  });
  print('Waiting for a value...'); // Prints first.
}
複製程式碼

讓我們看一下輸出日誌:

Waiting for a value... (3 seconds pass until callback executes)
The value is 100.
複製程式碼

除了執行我們的程式碼,then()本身也會返回自己的Future,與我們提供的函式的返回值一樣。所以如果我們需要進行一連串的非同步呼叫,我們可以選擇鏈式呼叫他們,儘管他們的返回型別不同:

_fetchNameForId(12)
    .then((name) => _fetchCountForName(name))
    .then((count) => print('The count is $count.'));
複製程式碼

回到我們第一個例子中,如果初始化的Future完成時沒有對應的資料會發生什麼--也就是說如果發生了錯誤什麼如何?而then()方法是需要一個值的。這時我們需要註冊另一個回撥來處理髮生錯誤的情況。

答案是使用[catchError()](https://api.dart.dev/stable/2.8.2/dart-async/Future/catchError.html)。它和then()一樣,只不過catchError()傳遞的不是資料,當Future執行過程中如果發生了錯誤,這個方法會被呼叫。就像then()一樣,catchError()本身也返回一個自己的Future,所以我們可以構建一個then()catchError()的呼叫鏈,這兩個方法可以相互待。

筆記:如果你在程式碼中使用了async-await,我們就不必使用then()catchError()了。因為我們可以使用await直接獲取對應的值,可以用try-catch-finally來處理錯誤。更詳細的資料,看一下Dart官方文件中關於非同步支援的章節吧

下面的例子展示瞭如何用catchError()處理Futuer中的錯誤:

void main() {
  Future.delayed(
    Duration(seconds: 3),
    () => throw 'Error!', // Complete with an error.
  ).then((value) {
    print(value);
  }).catchError((err) {
    print('Caught $err'); // Handle the error.
  });
  print('Waiting for a value...');
}
複製程式碼

我們甚至可以給catchError()一個測試函式,這個函式可以在回撥呼叫前對錯誤進行測試。通過這種方式,我們可以擁有多個catchError()函式,每個函式檢查一種不同型別的錯誤。下面的例子展示瞭如何使用一個檢測函式對錯誤進行測試,catchError()中的引數test是可選的。

void main() {
  Future.delayed(
    Duration(seconds: 3),
    () => throw 'Error!',
  ).then((value) {
    print(value);
  }).catchError((err) {
    print('Caught $err');
  }, test: (err) { // Optional test parameter.
    return err is String;
  });
  print('Waiting for a value...');
}
複製程式碼

現在文章你已經看到這裡了,希望你已經理解了Future的三種狀態,已經理解了三種狀態是怎麼在程式碼中體現的。在上面的例子中分三塊:

  1. 第一塊建立了一個未完成的Future
  2. Future執行完成並返回了對應資料,呼叫了then()裡的回撥。
  3. Future執行完成了但發生錯誤,呼叫了catchError()裡的回撥。

還有另一個方法你也可能想使用:whenComplete()。正如方法名字一樣,這個方法會在Future完成時被呼叫,不管Future是返回了資料還是丟擲了錯誤。

這有點像try-catch-finally中的finally。無論是程式碼是正確執行了,還是出現了錯誤,它都會被執行。

在Flutter中使用Future

我們剛剛一直在說如何建立Future,還有如何使用Future中的資料。現在我們要說說如何在Flutter中實踐。

假如說我們一個網路服務,這個網路服務會返回JSON資料,我們想要展示這些資料。我們可以當然使用StatefulWidget,然後通過建立Future,根據Future的執行情況,然後呼叫setState(),所有的這些都要我們手動控制。

當然了我們也可以使用FutureBuilder。這是一個Flutter SDK中的一個控制元件。我們傳一個Future和一個builder函式,當Future完成時,它會自動重構它的子控制元件。

FutureBuilder會呼叫builder函式,這個函式有兩個引數,一個是context,另一個是snapshot,它是當前Future的狀態。

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Use a FutureBuilder.
    return FutureBuilder<String>(
      future: _fetchNetworkData(),
      builder: (context, snapshot) {},
    );
  }
}
複製程式碼

我們可以通過檢查snapshot來看看Future是否發生了錯誤:

 return FutureBuilder<String>(
      future: _fetchNetworkData(5),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          // Future completed with an error.
          return Text(
            'There was an error',
          );
        }
        throw UnimplementedError("Case not handled yet");
      },
    );
複製程式碼

我們也可以通過檢查hasData檢視是否返回了資料:

return FutureBuilder<String>(
      future: _fetchNetworkData(5),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          // Future completed with an error.
          return Text(
            'There was an error',
          );
        } else if (snapshot.hasData) {
          // Future completed with a value.
          return Text(
            json.decode(snapshot.data)['field'],
          );
        }
        throw UnimplementedError("Case not handled yet");
      },
    );
複製程式碼

如果hasErrorhasData都不是true,那麼我們就知道Future還在執行中,我們還需要繼續等待,這時我們也可以輸出一些其他的資訊:

    return FutureBuilder<String>(
      future: _fetchNetworkData(5),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          // Future completed with an error.
          return Text(
            'There was an error',
          );
        } else if (snapshot.hasData) {
          // Future completed with a value.
          return Text(
            json.decode(snapshot.data)['field'],
          );
        } else {
          // Uncompleted.
          return Text(
            'No value yet!',
          );
        }
      },
    );
複製程式碼

即使在Flutter程式碼中我們依然可以看到三種狀態是如何體現的。

總結

本文闡述了Future是如何呈現的以及怎麼樣使用FutureFutureBuilder相關API建立Future,並且使用Future中的資料。

如果你想學習更多關於如何使用Future,可以通過執行示例程式碼和互動練習來測試你對Future的理解--去codelab上練一下futures,aysnc,await吧。

相關文章