Flutter | 定義一個通用的多功能網路請求 Widget

Flutter筆記發表於2019-10-07

首先道個歉,最近公司很忙,又趕上十一假期,所以鴿了將近半個月。

不過,後續還是會每週最少更新兩篇的!

那說起網路請求的控制元件,我們首先是不是會想起 FutureBuilder

FutureBuilder 給我們封裝好了網路請求中的各種狀態。

如果沒有了解過,那麼可以看我這篇文章:Flutter - FutureBuilder 非同步UI神器。

這篇文章是早期寫的,有些地方寫的有些問題,但不重要!主要了解一下 FutureBuilder 的狀態就可以了。

本篇文章中只是提供一種思路,歡迎一起探討,也歡迎不吝賜教!

效果如下。

首先是沒有開啟服務的情況:

Flutter | 定義一個通用的多功能網路請求 Widget

可以看到全部都是錯誤的資訊,

然後開啟服務:

Flutter | 定義一個通用的多功能網路請求 Widget

1. 先定義一個通用的網路請求

那既然是網路請求,那首先我們要定義一個通用的網路請求方法。

每一家後臺 API 的風格都不一樣,有的是 RSETful,有的是我們最熟悉的 GET、POST。

這裡就以 GET 為例,API 介面為 GitHub - 網易雲音樂 Node.js API service。

網路請求使用的是 Dio,先建立一個 NetUtils.dart

初始化程式碼:

static Dio _dio;

static void init() async {
  Directory tempDir = await getTemporaryDirectory();
  String tempPath = tempDir.path;
  CookieJar cj = PersistCookieJar(dir: tempPath);
  _dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:3000'))
    ..interceptors.add(CookieManager(cj))
    ..interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
}
複製程式碼

在 runApp 前面呼叫即完成初始化。

接著定義一個通用的網路請求:

static Future<Response> _get(
  BuildContext context,
  String url, {
    Map<String, dynamic> params,
  }) async {
  Loading.showLoading(context);
  try {
    return await _dio.get(url, queryParameters: params);
  } on DioError catch (e) {
    if (e.response is Map) {
      return Future.value(e.response);
    } else {
      return Future.error(0);
    }
  } finally {
    Loading.hideLoading(context);
  }
}
複製程式碼

這裡程式碼很簡單,方法需要傳入三個引數:

  1. context:用於 showLoading
  2. url:API 地址
  3. params:該網路請求的引數,可以為空

方法內部我們捕獲了 DioError,然後判斷介面是否還返回了正常的內容。

例如:狀態碼不為2xx,但是仍然返回了資料,這樣 Dio 是會丟擲 DioError 的,需要我們自己捕獲來處理。

如果返回了正常的資料,那我們還是返回回去,如果不是正常的資料,則直接丟擲 Future.error(0)

使用該通用方法:

/// 新碟上架
static Future<AlbumData> getAlbumData(
  BuildContext context, {
    Map<String, dynamic> params = const {
      'offset': 0,
      'limit': 10,
    },
  }) async {
  var response = await _get(context, '/top/album', params: params);
  return AlbumData.fromJson(response.data);
}
複製程式碼

我們就可以像這樣來使用剛才定義好的方法,也方便我們後續定義一個通用的 FutureBuilder

2. 確認網路請求控制元件所需要的功能

我們從最開始的圖中明顯能看出來的,其實是有三個功能:

  1. 請求資料並顯示 Loading
  2. 正常時返回正常資料,錯誤時返回錯誤 Widget
  3. 錯誤 Widget 可以點選重新請求

然鵝,細心的同學也發現問題了。

我們在網路請求中新增了一個 Loading,而且需要一個 BuildContext。

我們都知道,是不能在 initState() 方法中去使用這個 BuildContext 的。

所以,我們還要進行一個 第一幀回撥

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((call) {
    _request();
  });
}
複製程式碼

這樣就完成了我們的需求調研。

3. 編寫通用網路請求控制元件

說的是一個通用的網路請求控制元件,其實就是把 FutureBuilder 封裝一層。

請求資料並顯示 Loading

但是,這裡也有一個問題:

我們在最開始定義網路請求工具類的時候,每一個網路請求都是一個方法,而每個方法中都有或者沒有引數。

我們也知道,FutureBuilder 需要傳入一個 Future,那這可怎麼辦?

而且我們不能在使用該控制元件的時候呼叫網路請求方法,因為網路請求中封裝了一個 Loading,這個 Loading 需要 BuildContext

既然如此,那我們只能傳入方法(Function)了:

typedef ValueWidgetBuilder<T> = Widget Function(
  BuildContext context,
  T value,
);


final ValueWidgetBuilder<T> builder;
final Function futureFunc;
final Map<String, dynamic> params;

CustomFutureBuilder({
  @required this.futureFunc,
  @required this.builder,
  this.params,
});
複製程式碼

這樣,我們就可以在 第一幀回撥 中來呼叫該網路請求了,這樣一舉兩得:

既不用在使用該控制元件的時候呼叫方法,又避免了 Loading 使用 BuildContext 報錯的問題。

Future<T> _future;

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((call) {
    _request();
  });
}

void _request() {
  setState(() {
    if (widget.params == null)
      _future = widget.futureFunc(context);
    else
      _future = widget.futureFunc(context, params: widget.params);
  });
}
複製程式碼

首先我們定義了一個 Future,然後在 第一幀回撥 中初始化該 Future 就可以了。

正常時返回正常資料,錯誤時返回錯誤 Widget

這就需要我們封裝好的網路請求和 FutureBuilder 有一個互動了,

網路請求的邏輯如下:

Flutter | 定義一個通用的多功能網路請求 Widget

這樣正好就可以對應 FutureBuilder 的幾種狀態:

  1. 網路請求 -> ConnectionState.noneConnectionState.waiting
  2. 顯示Loading -> ConnectionState.active
  3. 請求結束 -> ConnectionState.done
  4. 是否有資料(無論對錯)-> snapshot.hasData
  5. 丟擲錯誤 -> snapshot.hasError

瞭解這些之後,我們就可以寫出程式碼:

  Widget build(BuildContext context) {
    return _future == null
        ? Container(
            alignment: Alignment.center,
            height: ScreenUtil().setWidth(200),
            child: CupertinoActivityIndicator(),
          )
        : FutureBuilder(
            future: _future,
            builder: (context, snapshot) {
              switch (snapshot.connectionState) {
                case ConnectionState.none:
                case ConnectionState.waiting:
                case ConnectionState.active:
                  return Container(
                    alignment: Alignment.center,
                    height: ScreenUtil().setWidth(200),
                    child: CupertinoActivityIndicator(),
                  );
                case ConnectionState.done:
                  if (snapshot.hasData) {
                    return widget.builder(context, snapshot.data);
                  } else if (snapshot.hasError) {
                    return NetErrorWidget(
                      callback: () {
                        _request();
                      },
                    );
                  }
              }
              return Container();
            },
          );
  }
複製程式碼

首先判斷 _future 是否為 null,如果為空,那麼則表示還沒有初始化該 Future,

個人建議這個時候返回自己定義好的載入中 Widget,因為後續在網路請求中的時候也返回該 Widget,這樣不會顯得亂。

然後在 ConnectionState.done 中判斷是否存在資料,如果有的話,就顯示傳進來的 Widget。

如果返回錯誤,則返回錯誤的 Widget。

錯誤 Widget 可以點選重新請求

這個邏輯其實很簡單,在我最開始說的文章中有講解一部分。

那就是什麼時候 FutureBuilder 會重新建立?

@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.future != widget.future) {
    if (_activeCallbackIdentity != null) {
      _unsubscribe();
      _snapshot = _snapshot.inState(ConnectionState.none);
    }
    _subscribe();
  }
}
複製程式碼

可以很清晰的看到,在兩次 Future 不一樣的情況下會重新走一遍流程。否則是不會走的。

而我們在上面也已經定義好了,因為傳進來的是 Function 和 Params,我們可以隨時重新建立該 Future:

void _request() {
  setState(() {
    if (widget.params == null)
      _future = widget.futureFunc(context);
    else
      _future = widget.futureFunc(context, params: widget.params);
  });
}
複製程式碼

錯誤 Widget 的點選事件寫成這個就 ok 了,這樣就重新建立了該 FutureBuilder,也就是重新請求了。

總結

程式碼的話,我就不傳上去了,因為這個只適用於一部分。

我這裡只是提供一種思路,個人覺得還是不錯的。

如果有什麼想法的話,歡迎一起探討,不吝賜教!

另我個人建立了一個「Flutter 交流群」,可以新增我個人微信 「17610912320」來入群。

推薦閱讀:

Flutter | 超簡單仿微信QQ側滑選單元件

Flutter | 一個超級酷炫的登入頁是怎樣煉成的

Flutter | WReorderList 一個可以指定兩個item互換位置的元件

Flutter | 如何實現一個水波紋擴散效果的 Widget

Flutter | 定義一個通用的多功能網路請求 Widget

相關文章