Flutter 入門指北(Part 13)之網(wǎng)絡(luò)

該文已授權(quán)公眾號(hào) 「碼個(gè)蛋」以清,轉(zhuǎn)載請(qǐng)指明出處

前面講完了常用的部件儿普,BLoC 模式,數(shù)據(jù)持久化等常用的掷倔,今天再介紹個(gè)重頭戲 —— 網(wǎng)絡(luò)請(qǐng)求

HttpClient

HttpClientdart 自帶的網(wǎng)絡(luò)請(qǐng)求方式眉孩,在 dart:io 包下。使用 HttpClient 作為請(qǐng)求分以下幾個(gè)步驟

  1. 創(chuàng)建 HttpClient 實(shí)例

    HttpClient client = HttpClient();
    
  2. 打開連接勒葱,并設(shè)置一些頭參數(shù)浪汪,請(qǐng)求參數(shù)等

    // 如果 url 中沒有查詢參數(shù)可直接創(chuàng)建
    Uri uri = Uri.parse('https://www.xxx.com');
    // 如果存在查詢參數(shù)則在 Uri 中添加
    Uri uri = Uri(scheme: 'https', host: 'www.xxx.com', queryParameters: {'a': 'AAA'});
    // 打開連接
    HttpClientRequest request = await client.getUrl(uri);
    request.headers.add('token', 'Bear ${'x' * 20}'); // 添加頭部 token 信息
    // 如果是 post 或者 put 請(qǐng)求,通過 `add` 添加請(qǐng)求體
    // 因?yàn)?`add` 方法需要傳入 `List<int>` 參數(shù)凛虽,可以通過 utf8.encode 進(jìn)行編碼
    request.add(utf8.encode('{"a": "aaa"}'));
    // 也可以通過添加流的方式進(jìn)行添加
    request.addStream(input);
    
  3. 連接服務(wù)器

    // 設(shè)置 request 后通過 request.close() 獲取一個(gè)響應(yīng)對(duì)象 HttpClientResponse死遭,
    // 包括響應(yīng)頭,響應(yīng)內(nèi)容等
    HttpClientResponse response = await request.close();
    
  4. 讀取服務(wù)器響應(yīng)內(nèi)容

    String responseBody = await response.transform(utf8.decoder).join();
    
  5. 關(guān)閉實(shí)例

    client.close();
    

例如我們要去請(qǐng)求 Bird.so 的首頁并顯示凯旋,我們可以這么實(shí)現(xiàn)

_httpClientRequest() async {
    HttpClient client;
    // try catch finally 用于捕獲請(qǐng)求過程中發(fā)生的異常呀潭,在 finally 中設(shè)置保證 client 能夠關(guān)閉
    try {
      client = HttpClient();
      HttpClientRequest request = await client.getUrl(Uri.parse(_BIRD_SO_URL));
      HttpClientResponse response = await request.close();
      String strResponse = await response.transform(utf8.decoder).join();
      setState(() => _netBack = strResponse);
    } catch (e) {
      print('${e.toString()}');
      setState(() => _netBack = 'Fail');
    } finally {
      client.close();
    }
  }

最后實(shí)現(xiàn)的效果

client.gif

很顯然,用 HttpClient 請(qǐng)求相對(duì)來說是個(gè)非常麻煩的過程至非,如果要涉及到文本上傳之類的钠署,那么就會(huì)更麻煩了,所以這邊引入一個(gè)網(wǎng)絡(luò)請(qǐng)求的插件 dio荒椭,寫本文的時(shí)候版本為 2.1.0

Dio

dio 是個(gè)非常強(qiáng)大的網(wǎng)絡(luò)請(qǐng)求庫(kù)谐鼎,他的方式類似 OkHttp,我們可以直接查看官方文檔趣惠,使用方式非常簡(jiǎn)單狸棍,創(chuàng)建一個(gè) Dio 實(shí)例身害,然后就可以通過 getpost 等方式發(fā)起請(qǐng)求隔缀,返回 Future<Response>题造,而且支持多個(gè)并發(fā)請(qǐng)求,可以設(shè)置返回響應(yīng)的類型猾瘸,監(jiān)聽上傳下載進(jìn)度等等界赔,看著就很給力。對(duì)于簡(jiǎn)單的方式牵触,這邊就不做太多介紹淮悼,主要講下攔截器,也是非常給力的一部分揽思。比如我們需要請(qǐng)求這么個(gè)接口 https://randomuser.me/api/

json.png

這個(gè)接口通過 get 請(qǐng)求袜腥,可以加入任意的查詢參數(shù)。比如我們需要實(shí)現(xiàn)一個(gè)請(qǐng)求加解密的過程钉汗,如果每次都在上傳參數(shù)或者返回請(qǐng)求的時(shí)候去加密羹令,解密的話,就做了非常多無用功了损痰,那么這時(shí)候攔截器就派上用場(chǎng)了福侈。先定義下加解密的規(guī)則,上傳的參數(shù)統(tǒng)一轉(zhuǎn)為小寫卢未,不存在大寫肪凛,請(qǐng)求回的數(shù)據(jù),不能含有 info 字段辽社∥扒剑看下如何實(shí)現(xiàn)

_dioRequest() async {
    BaseOptions options = BaseOptions(connectTimeout: 5000, receiveTimeout: 60000);
    Dio dio = Dio(options);
    
    dio.interceptors.add(InterceptorsWrapper(onRequest: (opt) {
      // 獲取查詢的參數(shù)
      Map params = opt.queryParameters;
      // 將所有的參數(shù)轉(zhuǎn)為小寫,因?yàn)椴樵儏?shù)通過 map 形式上傳
      params.forEach((key, value) => 
                       opt.queryParameters[key] = '$value'.toLowerCase());
      // 這邊還可以做些別的操作滴铅,例如需要 token 進(jìn)行用戶身份驗(yàn)證戳葵,則通過頭部進(jìn)行添加
      // opt.headers['authorization'] = 'token';
      // 在官網(wǎng)中,提供了 lock 和 unlock 的寫法汉匙,被 lock 后拱烁,接下來的請(qǐng)求會(huì)進(jìn)入隊(duì)列等待,
      // 直到 unlock 后才能繼續(xù)盹兢,可以用于幾個(gè)請(qǐng)求,后續(xù)的需要用到前面的返回值的情況使用
        
      // 返回修改后的 RequestOptions
      return opt;
    }, onResponse: (resp) {
      // 返回響應(yīng)體后守伸,將 info 字段的內(nèi)容切除绎秒,并將 json 拼接完成
      resp.data = '${'${resp.data}'.split(', info').first}}';
      return resp;
    }, onError: (error) {
      // 發(fā)生錯(cuò)誤時(shí)的回調(diào)
      return error;
    }));

    // 發(fā)送一個(gè)請(qǐng)求,可以查看下打印的結(jié)果
    Response response = await dio.get(_USER_ME_URL, queryParameters: {'a': 'AAA', 'b': 'BbBbBb'});
    print(response.data);
    print(response.request.headers);
    print(response.request.queryParameters);
    setState(() => _netBack = response.data.toString()); // 界面顯示 response.data
  }

看下最后的顯示信息

header.png
response.png

請(qǐng)求體的頭部成功加上了 authorization 參數(shù)尼摹,請(qǐng)求的參數(shù)全部變?yōu)樾懠郏祷氐男畔⒁舶?info 字段值去除剂娄。在很多時(shí)候,請(qǐng)求接口后玄呛,需要將 json 轉(zhuǎn)換成 pojo 類來處理阅懦,可以通過 json_serializable 這個(gè)三方插件實(shí)現(xiàn),這邊提供文章 Flutter Json自動(dòng)反序列化徘铝,當(dāng)然這種方式比較麻煩耳胎,這里推薦個(gè) Android Studio 下的插件 dart_json_format 直接搜索就可以,如果用的是 Vitual Code 或者別的不是 JetBrains 系列的惕它,這里有個(gè)轉(zhuǎn)換的網(wǎng)址 JsonToDart怕午。

以上代碼查看 http_main.dart 文件

實(shí)踐一下下

不知道小伙還記得前面講的 BLoC 沒有,忘了可以查看 Flutter 狀態(tài)管理及 BLoC淹魄,這里結(jié)合 BLoCDio 實(shí)現(xiàn)界面和邏輯分離的小例子郁惜,接口使用前面提到的 https://randomuser.me/api/ 接口。網(wǎng)絡(luò)應(yīng)該是比較常用的甲锡,所以對(duì)其進(jìn)行一些封裝還是很有必要的兆蕉,這邊提供下我自己封裝的方法

import 'package:dio/dio.dart';

// 用于錯(cuò)誤信息回調(diào)
typedef ErrorCallback = void Function(String msg);

class HttpUtils {
  static const GET = 'get';
  static const POST = 'post';

  static Dio _dio;

  static HttpUtils _instance;

  Dio get hp => _dio;

  // dio 可以在 BaseOptions 中指定域名 baseUrl,
  // 后續(xù)接口就不需要再添加域名了
  // 如果請(qǐng)求的接口域名發(fā)生了變化缤沦,只要把全部 url 寫全虎韵,就會(huì)自動(dòng)使用新的域名
  HttpUtils._internal(String base) {
    // 生成一個(gè)單例,防止多次打開關(guān)閉造成開銷
    _dio = Dio(BaseOptions(baseUrl: base, connectTimeout: 10000, receiveTimeout: 10000));
  }

  factory HttpUtils(String base) {
    if (_instance == null) _instance = HttpUtils._internal(base);
    return _instance;
  }

  // 添加攔截器
  addInterceptor(List<InterceptorsWrapper> interceptors) {
    _dio.interceptors.clear();
    _dio.interceptors.addAll(interceptors);
  }

  Future<Response<T>> getRequest<T>(url, {Map params, ErrorCallback callback}) =>
      _request(url, GET, params: params, callback: callback);

  Future<Response<T>> postRequest<T>(url, {Map params, ErrorCallback callback}) =>
      _request(url, POST, params: params, callback: callback);

  Future<Response> download(url, path, {ProgressCallback receive, CancelToken token}) =>
      _dio.download(url, path, onReceiveProgress: receive, cancelToken: token);

  // T 可以指定返回的類型疚俱,String 或者 Map<String, dynamic>
  Future<Response<T>> _request<T>(
    url,
    String method, {
    Map params, // 上傳的參數(shù)
    Options opt,
    ErrorCallback callback, // 錯(cuò)誤回調(diào)
    ProgressCallback send, // 上傳進(jìn)度監(jiān)聽
    ProgressCallback receive, // 下載監(jiān)聽
    CancelToken token, // 用于取消的 token劝术,可以多個(gè)請(qǐng)求綁定一個(gè) token
  }) async {
    try {
      Response<T> rep;

      if (method == GET) {
        // 如果不是重新創(chuàng)建 Dio 實(shí)例,get 方法使用 queryParams 會(huì)出錯(cuò)呆奕,不懂原因养晋,使用拼接沒有問題
        if (params != null && params.isNotEmpty) {
          var sb = StringBuffer('?');
          params.forEach((key, value) {
            sb.write('$key=$value&');
          });
          // get 請(qǐng)求下拼接路徑
          url += sb.toString().substring(0, sb.length - 1);
        }
        rep = await _dio.get(url, options: opt, onReceiveProgress: receive, cancelToken: token);
      } else if (method == POST) {
        // post 參數(shù)放請(qǐng)求體
        rep = params == null
            ? await _dio.post(url, options: opt, cancelToken: token, onSendProgress: send, onReceiveProgress: receive)
            : await _dio.post(url,
                data: params, options: opt, cancelToken: token, onSendProgress: send, onReceiveProgress: receive);
      }

      // 如果 statusCode 不是 200 則錯(cuò)誤回調(diào),返回空的 Response
      if (rep.statusCode != 200 && callback != null) {
        callback('network error, and code is ${rep.statusCode}');
        return null;
      }
      return rep;
    } catch (e) {
      if (callback != null) {
        callback('network error, catch error: ${e.toString()}');
      }
      return null;
    }
  }
}

封裝后就可以愉快的調(diào)用了梁钾,如果有別的請(qǐng)求方式后期可以繼續(xù)擴(kuò)展绳泉。繼續(xù)看代碼,創(chuàng)建一個(gè) application.dart 文件姆泻,用于存放全局參數(shù)

class Application {
  static HttpUtils http;
}

并在 main() 方法中進(jìn)行初始化,接下來就可以直接使用

void main() {
  Application.http = HttpUtils('https://randomuser.me');
  
  runApp(DemoApp());

  // 透明狀態(tài)欄
  if (Platform.isAndroid) {
      SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.transparent));
  }
}

看下最后的實(shí)現(xiàn)效果吧拇勃,剛進(jìn)入沒有數(shù)據(jù)則通過轉(zhuǎn)圈圈提示四苇,加載完數(shù)據(jù)后,點(diǎn)擊頭像更換下個(gè)

bloc_http.gif

實(shí)現(xiàn) BLoC 需要有一個(gè)管理類

class UserBloc extends BaseBloc {
  RandomUserModel _user;

  RandomUserModel get user => _user;

  BehaviorSubject<RandomUserModel> _controller = BehaviorSubject();

  Observable<RandomUserModel> get stream => Observable(_controller.stream);

  // 網(wǎng)絡(luò)請(qǐng)求獲取新的數(shù)據(jù)方咆,并更新
  updateUserInfo() {
    Application.http.getRequest('/api').then((response) {
      // RandomUserModel 就是接口返回的 json 轉(zhuǎn)成的 model 類
      RandomUserModel model = RandomUserModel.fromMap(response.data);
      _user = model;
      // add 到 controller 通知修改
      _controller.add(model);
    });
  }

  @override
  void dispose() {
    _controller?.close(); // 及時(shí)銷毀
  }
}

設(shè)置好管理類后月腋,就可以來編寫界面了,界面也比較簡(jiǎn)單

class UserPageDemo extends StatelessWidget {
  // 將首字母大寫
  String _upperFirst(String content) {
    assert(content != null && content.isNotEmpty);
    return '${content.substring(0, 1).toUpperCase()}${content.substring(1)}';
  }

  // 地址信息通用部件
  Widget _userLocation(String info) => Padding(
      padding: const EdgeInsets.only(top: 4.0),
      child: Text(info, style: TextStyle(color: Colors.white, fontSize: 16.0)));

  @override
  Widget build(BuildContext context) {
    UserBloc _bloc = BlocProvider.of<UserBloc>(context);
    _bloc.updateUserInfo();

    return Scaffold(
      // StreamBuilder 接受更新數(shù)據(jù)的 stream
      body: StreamBuilder(
          builder: (_, AsyncSnapshot<RandomUserModel> snapshot) => Container(
                alignment: Alignment.center,
                decoration: BoxDecoration(
                    gradient: LinearGradient(
                        begin: Alignment.topCenter,
                        end: Alignment.bottomCenter,
                        colors: [Colors.blue[600], Colors.blue[400]])),
                child: !snapshot.hasData
                    ? CupertinoActivityIndicator(radius: 12.0)
                    : Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
                        InkWell( // 用于切換數(shù)據(jù)
                            child: ClipOval( // 圓形頭像
                              child: FadeInImage.assetNetwork(
                                  placeholder: 'images/ava_default.png', image: snapshot.data.results[0].picture.large),
                            ),
                            onTap: () => _bloc.updateUserInfo()), // 更新數(shù)據(jù)
                        Padding(
                          padding: const EdgeInsets.only(top: 20.0),
                          child: Text(
                              '${_upperFirst(snapshot.data.results[0].name.first)} ${_upperFirst(snapshot.data.results[0].name.last)}',
                              style: TextStyle(color: Colors.white, fontSize: 24.0)),
                        ),
                        Text('${snapshot.data.results[0].email}',
                            style: TextStyle(color: Colors.white, fontSize: 18.0)),
                        _userLocation('${snapshot.data.results[0].location.street}'),
                        _userLocation('${_upperFirst(snapshot.data.results[0].location.city)}'),
                        _userLocation('${_upperFirst(snapshot.data.results[0].location.state)}'),
                      ]),
              ),
          initialData: _bloc.user, // 注入初始值
          stream: _bloc.stream), // 注入更新 stream
    );
  }
}

以上代碼查看 bloc_network 包下的所有文件

當(dāng)然了,福利是不可少的榆骚,但是需要你到項(xiàng)目中自己去找片拍。差不多入門的部分就講到這了,接下來考慮加個(gè)實(shí)戰(zhàn)妓肢,總之先等等吧捌省,我找個(gè)好的題材接口來寫。

最后代碼的地址還是要的:

  1. 文章中涉及的代碼:demos

  2. 基于郭神 cool weather 接口的一個(gè)項(xiàng)目碉钠,實(shí)現(xiàn) BLoC 模式纲缓,實(shí)現(xiàn)狀態(tài)管理:flutter_weather

  3. 一個(gè)課程(當(dāng)時(shí)買了想看下代碼規(guī)范的,代碼更新會(huì)比較慢放钦,雖然是跟著課上的一些寫代碼色徘,但是還是做了自己的修改,很多地方看著不舒服操禀,然后就改成自己的實(shí)現(xiàn)方式了):flutter_shop

如果對(duì)你有幫助的話褂策,記得給個(gè) Star,先謝過颓屑,你的認(rèn)可就是支持我繼續(xù)寫下去的動(dòng)力~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末斤寂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子揪惦,更是在濱河造成了極大的恐慌遍搞,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件器腋,死亡現(xiàn)場(chǎng)離奇詭異溪猿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)纫塌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門诊县,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人措左,你說我怎么就攤上這事依痊。” “怎么了怎披?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵胸嘁,是天一觀的道長(zhǎng)桦沉。 經(jīng)常有香客問我因俐,道長(zhǎng)脱盲,這世上最難降的妖魔是什么锹淌? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮识啦,結(jié)果婚禮上晰甚,老公的妹妹穿的比我還像新娘卿啡。我一直安慰自己,他們只是感情好指蚁,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著自晰,像睡著了一般凝化。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酬荞,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天搓劫,我揣著相機(jī)與錄音,去河邊找鬼混巧。 笑死枪向,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的咧党。 我是一名探鬼主播秘蛔,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼傍衡!你這毒婦竟也來了深员?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤蛙埂,失蹤者是張志新(化名)和其女友劉穎倦畅,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绣的,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叠赐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屡江。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芭概。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖盼理,靈堂內(nèi)的尸體忽然破棺而出谈山,到底是詐尸還是另有隱情,我是刑警寧澤宏怔,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布奏路,位于F島的核電站,受9級(jí)特大地震影響臊诊,放射性物質(zhì)發(fā)生泄漏鸽粉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一抓艳、第九天 我趴在偏房一處隱蔽的房頂上張望触机。 院中可真熱鬧,春花似錦、人聲如沸儡首。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔬胯。三九已至对供,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氛濒,已是汗流浹背产场。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舞竿,地道東北人京景。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像骗奖,于是被迫代替她去往敵國(guó)和親确徙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容