Flutter网络请求(http及websocket封装)

HTTP请求

http请求我使用的是http包,简单的示例如下:

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

var url = 'https://example.com/whatsit/create';
var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'});
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');

print(await http.read('https://example.com/foobar.txt'));

自定义请求

在实际项目中我们经常需要全局的注入一些header,比如认证信息、国际化信息等,这就需要我们自定义一下我们的http请求,示例如下:

class UserAgentClient extends http.BaseClient {
  final String userAgent;
  final http.Client _inner;

  UserAgentClient(this.userAgent, this._inner);

  Future<http.StreamedResponse> send(http.BaseRequest request) {
    request.headers['user-agent'] = userAgent;
    return _inner.send(request);
  }
}

返回拦截

在请求返回后通常需要一些全局的拦截器,根据不同的错误码来弹出不同的提醒,在flutter中绘制widget需要获取上下文(context),使用navigatorKey来获取全局上下文。

GlobalKey<NavigatorState> navigatorKey = GlobalKey();
MaterialApp(
    ...
    navigatorKey: navigatorKey,
    ...
)

通过全局上下文来显示全局弹出提醒

_showErrorMessage(message) {
    OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
      return new Positioned(
          top: MediaQuery.of(context).size.height * 0.2,
          child: new Material(
            child: new Container(
              width: MediaQuery.of(context).size.width,
              alignment: Alignment.center,
              child: new Center(
                child: new Card(
                  child: new Padding(
                    padding: EdgeInsets.all(8),
                    child: new Text(message),
                  ),
                  color: Colors.grey,
                ),
              ),
            ),
          ));
    });
    navigatorKey.currentState.overlay.insert(overlayEntry);
    new Future.delayed(Duration(seconds: 2)).then((value) {
      overlayEntry.remove();
    });
}

文件上传

简单示例

var request = http.MultipartRequest(
    'POST',
    Uri.parse("https://example.com/upload"),
);
Map<String, String> headers = {
    "Content-type": "multipart/form-data"
};
request.files.add(
    http.MultipartFile(
        'file', file.readAsBytes().asStream(), file.lengthSync(),
        filename: file.path.split("/").last),
);
request.headers.addAll(headers);
var res = await request.send();
var result = await http.Response.fromStream(res);

websocket

https://flutter.dev/docs/cookbook/networking/web-sockets

final channel = IOWebSocketChannel.connect('ws://echo.websocket.org');

...

    StreamBuilder(
        stream: widget.channel.stream,
        builder: (context, snapshot) {
            return Text(snapshot.hasData ? '${snapshot.data}' : '');
        },
    );

...

封装请求基类

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:web_socket_channel/io.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

GlobalKey<NavigatorState> navigatorKey = GlobalKey();

var innerClient = http.Client();
String token = 'token'

class MyHttpClient extends http.BaseClient {
  final http.Client _inner = innerClient;
  final String baseUrl = 'https://example.com/';

  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    request.headers['Content-Type'] = 'application/json';
    request.headers['Authorization'] = "Token $token";
    return _inner.send(request);
  }
}

var httpClient = MyHttpClient()

class MyApi {
  _showErrorMessage(message) {
    OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
      return new Positioned(
          top: MediaQuery.of(context).size.height * 0.7,
          child: new Material(
            child: new Container(
              width: MediaQuery.of(context).size.width,
              alignment: Alignment.center,
              child: new Center(
                child: new Card(
                  child: new Padding(
                    padding: EdgeInsets.all(8),
                    child: new Text(message),
                  ),
                  color: Colors.grey,
                ),
              ),
            ),
          ));
    });
    navigatorKey.currentState.overlay.insert(overlayEntry);
    new Future.delayed(Duration(seconds: 2)).then((value) {
      overlayEntry.remove();
    });
  }

  _processResponse(http.Response response) {
    switch (response.statusCode) {
      case 200:
      case 201:
        {
          return json.decode(response.body);
        }
        break;
      case 400:
        {
          _showErrorMessage(response.body);
        }
        break;
      case 401:
        {
          navigatorKey.currentState.pushNamed('/login');
        }
        break;
    case 403:
        {
          _showErrorMessage('权限不足');
        }
        break;
      case 404:
        {
          navigatorKey.currentState.pop();
        }
        break;
      case 500:
        {
          _showErrorMessage('服务器错误');
        }
        break;
      default:
        {
          _showErrorMessage('未知错误');
        }
        break;
    }
    return null;
  }

  _convertObj(Map data) {
    List pList = [];
    data.forEach((k, v) {
      pList.add('$k=$v');
    });
    return pList.join('&');
  }

  postJson(url, data) async {
    var response = await httpClient.post('${httpClient.baseUrl}$url',
        body: json.encode(data));
    return _processResponse(response);
  }

  getJson(url, {Map data}) async {
    String _url = '${httpClient.baseUrl}$url';
    if (data != null && data.isNotEmpty) {
      _url += '?' + _convertObj(data);
    }
    var response = await httpClient.get(_url);
    return _processResponse(response);
  }

  uploadFile(String url, File file) async {
    var request = http.MultipartRequest(
      'POST',
      Uri.parse("${httpClient.baseUrl}$url"),
    );
    Map<String, String> headers = {
      "Authorization": "Token ${httpClient.token}",
      "Content-type": "multipart/form-data"
    };
    request.files.add(
      http.MultipartFile(
          'file', file.readAsBytes().asStream(), file.lengthSync(),
          filename: file.path.split("/").last),
    );
    request.headers.addAll(headers);
    var res = await request.send();
    return _processResponse(await http.Response.fromStream(res));
  }

  IOWebSocketChannel wsConnect(url) {
    List<String> uList = httpClient.baseUrl.split('://');
    String _host = uList[1];
    String _protocol = uList[0];
    Map<String, String> headers = {
      "Authorization": "Token ${httpClient.token}",
    };
    String protocol = _protocol == 'https' ? 'wss' : 'ws';
    return IOWebSocketChannel.connect('$protocol://$_host$url',
        headers: headers);
  }
}

然后实现继承MyApi的业务类的具体请求即可,业务类无需考虑各种全局拦截,只需实现具体业务即可

class DemoApi extends MyApi {
  login(data) async => await postJson('/login/', data);
  userInfo(id) async => await getJson('/user/$id/');
}