flutter FutureBuilder

312 阅读3分钟

在 Flutter 中,你可能遇到过需要等待语句执行的情况。异步操作让程序在等待另一个操作完成的同时完成工作。一些常见的例子是:

  • 从 Internet 获取数据
  • 读取、创建或保存文件
  • 实例化复杂对象

在 Dart 中,为了执行异步操作,我们使用 Future 类、async 和 await 关键字。要在 widget 中使用 Future,我们使用 FutureBuilder widget。

Future 简介

Future 是 Dart 中 Future 类的一个实例。 Future 表示异步操作的结果,可以有两种状态:未完成或已完成。

我们使用 async 将函数声明为异步函数,其返回类型会自动转换为返回数据类型的 Future。在这里,添加 async 关键字后,返回类型从 String 更改为 Future< String >。使用 await 关键字,我们等待任何返回 Future 的事件的响应。在这里,http.get 函数返回一个 Future< Response >。

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

Future<String> getData() async {
  http.Response res = await http.get(Uri.parse("https://www.google.com");
  return res.body;
}

现在在我们要获取数据的地方,我们将使用以下代码:

Future<void> main() async {
  String data = await getData();
  print(data);
}

或者,我们也可以使用 .then 关键字,使用以下代码:

void main2(){
  getData().then((value){
    print(value);
  });
}

现在我们对 Futures 以及何时使用它们有了基本的了解。让我们看看如何在我们的应用程序中使用它们。

传统的异步方法

对于我们的示例,我们将发出 API 请求并查看代码的外观。我们将发出一个获取请求,在这种情况下不需要身份验证。

我们将使用的 API 是:

https://api.catboys.com/img

响应示例:

{
  "url": "https://cdn.catboys.com/images/image_73.jpg",
  "artist": "CoverDesign1",
  "artist_url": "https://www.deviantart.com/coverdesign1",
  "source_url": "https://www.deviantart.com/coverdesign1/art/Artiste-Iya-Chen-Render-396950935",
  "error": "none"
}
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

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

class _HomePageState extends State<HomePage> {
  // Variables to update user regarding the request status
  bool isLoading = false;
  bool errorOccured = false;
  String imageUrl = '';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.refresh),
        onPressed: getData,
      ),
      appBar: AppBar(
        title: const Text("Traditional Async"),
      ),
      body: Center(
          child: isLoading
              ? const CircularProgressIndicator()
              : errorOccured
                  ? const Text("Error Occured")
                  : Image.network(imageUrl)),
    );
  }

  Future<void> getData() async {
    // Set Fields
    errorOccured = false;
    isLoading = true;
    setState(() {});
    
    http.Response res;
    try {
      res = await http.get(Uri.parse("https://api.catboys.com/img"));
    } catch (e) {
      // Error Occured while making request
      errorOccured = true;
      isLoading = false;
      setState(() {});
      debugPrint(e.toString());
      return;
    }
    // Request Sucessfull
    Map<String, dynamic> data = json.decode(res.body);
    imageUrl = data['url'];
    isLoading = false;
    setState(() {});
  }
}

在代码中,可以观察到后端和前端是紧密耦合的。此外,我们可以看到我们必须一次又一次地调用 setState((){}) 来更新状态。我们可以在这里使用 ValueNotifier 来更新加载和错误状态,但是我们还必须管理 ValueNotifier。

为了处理所有这些加载、错误状态并减少样板代码,Flutter 有一个 FutureBuilder widget 。

Future Builder 简介

FutureBuilder 是一个基于任何 Future 函数的响应重建自身的 widget 。 FutureBuilder 订阅 Future 并在从 Future 接收到值时重建。它本质上是有状态的,即它在内部管理自己的状态,就像我们在有状态 widget 中所做的那样。

我们使用以下构造函数来创建 FutureBuilder:

FutureBuilder({
  Key? key,
  this.future,
  this.initialData,
  required this.builder,
})

我们将 future 值传递给 future 参数。 builder 接受一个函数,该函数有两个参数 BuildContext、AsyncSnapshot< T > 并返回一个 Widget。 initialData 参数用于将初始数据传递给 FutureBuilder。

实现如下所示:

FutureBuilder(
  future: getData(),
  builder: (BuildContext context, AsyncSnapshot snapshot) {
   return Container();
  },
);

AsyncSnapshot 包含一些关于连接、数据、错误等的重要数据。它的一些重要字段是:

  1. connectionState
  2. hasData
  3. hasError
  4. data
  5. error
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class FutureBuilderExample extends StatefulWidget {
  const FutureBuilderExample({Key? key}) : super(key: key);

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

class _FutureBuilderExampleState extends State<FutureBuilderExample> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.refresh),
        onPressed: () {
          setState(() {});
        },
      ),
      appBar: AppBar(title: const Text("Future Builder Example")),
      body: FutureBuilder(
        future: getData(),
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            // Future resolved check if it has error
            if (snapshot.hasError) {
              return _getErrorText();
            } else if (snapshot.hasData) {
              //NOTE: isEmpty can only be used on Maps and Strings
              if (snapshot.data!.isEmpty) {
                return const Center(
                  child: Text("Data Received, but is empty"),
                );
              }
              return _getImageWidget(snapshot);
            } else {
              return _getNoDataReceivedText();
            }
          } else if (snapshot.connectionState == ConnectionState.waiting) {
            // Future in progress
            return _getLoadingIndicator();
          } else {
            return Text("State: ${snapshot.connectionState}");
          }
        },
      ),
    );
  }

  Center _getImageWidget(AsyncSnapshot<dynamic> snapshot) {
    return Center(
      child: Image.network(snapshot.data),
    );
  }

  _getNoDataReceivedText() => const Center(child: Text("No Data Received"));

  _getLoadingIndicator() {
    return const Center(
      child: CircularProgressIndicator(),
    );
  }

  _getErrorText() {
    return const Center(
      child: Text("An Error Occured"),
    );
  }
}

Future<String> getData() async {
  http.Response res;
  try {
    res = await http.get(Uri.parse("https://api.catboys.com/img"));
  } catch (e) {
    return "";
  }
  Map<String, dynamic> data = json.decode(res.body);
  return data['url'];
}