Day28 Networking & http

上一篇讲完如何处理已经得到的资讯数据,今天来看看我们是如何与Web 服务器进行通信的

首先添加依赖:pub.dev:http

...
dependencies:
  http: ^0.12.2
...
import 'package:http/http.dart' as http; //在.dart 引用

在专案上对 Android 的 AndroidManifest.xml,新增网路权限

<uses-permission android:name="android.permission.INTERNET" />

获取网路数据

接下来我们就可以进行网路请求了,我们来使用http.get()方法从JSONPlaceholder 上获取一个样本Album 数据当作范例

这个http.get()方法会返回一个包含ResponseFuture,此Response会包含成功从http 请求接收到的数据,接下来就要处理将http.Response 转换成一个自定义的Dart 对象

先创建一个 AlbumModel 类,此范例专案规模小,我们来用手动转换JSON当作范例

class Album {
  final int id;
  final String title;

  Album({this.id, this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      id: json['id'],
      title: json['title'],
    );
  }
}

现在,我们需要定义一个从服务器取得Album 数据的方法,fetchAlbum()函数并返回Future<Album>,为了实现这个目标,我们需要做以下几步:

  1. dart:convertlibrary 将Response 转换成一个 json Map
  2. 如果服务器返回了一个状态码为200的“OK” 的 Response,那麽就使用fromJson方法将 jsonMap转换成Album
  3. 如果服务器返回的Response不是我们预期的状态码为200的 Response,那麽就抛出异常。服务器若返回 404 Not Found错误,也同样要抛出异常,而不是返回一个null,在检查如下所示的snapshot值的时候,这一点相当重要
import 'dart:convert';

Future<Album> fetchAlbum() async {
  final response = await http.get('https://jsonplaceholder.typicode.com/albums/1');

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

我们新建一个StatefulWidget MyApp,在此控件上获取Response数据

...
class _MyAppState extends State<MyApp> { 
  Future<Album> futureAlbum;

  //覆写initState() 调用获取数据的方法fetch()
  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }

为何要在initState() 中调用fetchAlbum()

每当Flutter需要改变视图中的一些内容时(这个发生的频率非常高),就会调用build()方法。因此,如果你将数据请求置於build()内部,就会造成大量的无效调用,同时还会拖慢应用程序的速度

为了能够获取数据并在画面上显示,你可以使用FutureBuilderwidget。这个由Flutter提供的FutureBuilder组件可以让处理异步数据变的非常简单

此时,必须要有两个参数:

  1. 你想要处理的Future,在这个例子中就是刚刚建的futureAlbum,为调用fetchAlbum()返回的future
  2. 一个告诉Flutter渲染哪些内容的builder函数,同时这也依赖於Future的状态:loading、success或者是error
FutureBuilder<Album>(
  future: futureAlbum,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text(snapshot.data.title);
    } else if (snapshot.hasError) {
      return Text("${snapshot.error}");
    }

    // By default, show a loading spinner.
    return CircularProgressIndicator();
  },
);

需要注意的是:当snapshot 值不是null 时,snapshot.hasData将只返回true,这就是为什麽要在後端返回404状态码的时候要让fetchAlbum方法抛出异常。如果fetchAlbum返回null的话,spinner会显示不正常

完整程序码

main.dart

import 'dart:async';
import 'dart:convert';

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

Future<Album> fetchAlbum() async {
  final response =
      await http.get('https://jsonplaceholder.typicode.com/albums/1');

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

class Album {
  final int id;
  final String title;

  Album({this.id, this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      id: json['id'],
      title: json['title'],
    );
  }
}

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  MyApp({Key key}) : super(key: key);

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

class _MyAppState extends State<MyApp> {
  Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Fetch Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Album>(
            future: futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data.title);
              } else if (snapshot.hasError) {
                return Text("${snapshot.error}");
              }

              // By default, show a loading spinner.
              return CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

删除服务器上的数据

我们来使用http.delete()方法从JSONPlaceholder 上的Album 中删除指定 id的数据当作范例

Future<Response> deleteAlbum(String id) async {
  final http.Response response = await http.delete(
    'https://jsonplaceholder.typicode.com/albums/$id',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
  );
  
  return response;
}

http.delete()方法返回一个Future包含的Response,该deleteAlbum()方法采用一个id参数,该参数用於标识要从服务器删除的数据

我们在上面范例的FutureBuilder新增一个删除数据功能的按钮,当按下该按钮时,将调用该deleteAlbum()方法,我们传递的id是从Internet 所得到的数据的id,这意味着按下按钮将删除从网路上获取的相同数据

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Text('${snapshot.data?.title ?? 'Deleted'}'),
    RaisedButton(
      child: Text('Delete Data'),
      onPressed: () {
       setState(() {
        _futureAlbum = deleteAlbum(snapshot.data.id.toString());
      });
      },
    ),
  ],
);

我们在deleteAlmum()方法中提出删除请求後,可以从deleteAlbum() 方法中返回一个Response,以通知我们的画面说数据已删除

Future<Album> deleteAlbum(String id) async {
  final http.Response response = await http.delete(
    'https://jsonplaceholder.typicode.com/albums/$id',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
  );

  if (response.statusCode == 200) {
    // If the server returned a 200 OK response,
    // then parse the JSON. After deleting,
    // you'll get an empty JSON `{}` response.
    // Don't return `null`, otherwise
    // `snapshot.hasData` will always return false
    // on `FutureBuilder`.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('Failed to delete album.');
  }
}

FutureBuilder()现在在收到Response 时进行重建。由於如果删除的请求成功,Response 的主体内容中将没有任何数据,因此该Album.fromJson()方法将创建具有Album默认值的对象实例

更新後的完整程序码:

main.dart

import 'dart:async';
import 'dart:convert';

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

Future<Album> fetchAlbum() async {
  final response =
      await http.get('https://jsonplaceholder.typicode.com/albums/1');

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response, then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response, then throw an exception.
    throw Exception('Failed to load album');
  }
}

Future<Album> deleteAlbum(String id) async {
  final http.Response response = await http.delete(
    'https://jsonplaceholder.typicode.com/albums/$id',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
  );

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON. After deleting,
    // you'll get an empty JSON `{}` response.
    // Don't return `null`, otherwise `snapshot.hasData`
    // will always return false on `FutureBuilder`.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a "200 OK response",
    // then throw an exception.
    throw Exception('Failed to delete album.');
  }
}

class Album {
  final int id;
  final String title;

  Album({this.id, this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      id: json['id'],
      title: json['title'],
    );
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  MyApp({Key key}) : super(key: key);

  @override
  _MyAppState createState() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyApp> {
  Future<Album> _futureAlbum;

  @override
  void initState() {
    super.initState();
    _futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Delete Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Delete Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Album>(
            future: _futureAlbum,
            builder: (context, snapshot) {
              // If the connection is done,
              // check for response data or an error.
              if (snapshot.connectionState == ConnectionState.done) {
                if (snapshot.hasData) {
                  return Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Text('${snapshot.data?.title ?? 'Deleted'}'),
                      RaisedButton(
                        child: Text('Delete Data'),
                        onPressed: () {
                          setState(() {
                            _futureAlbum =
                                deleteAlbum(snapshot.data.id.toString());
                          });
                        },
                      ),
                    ],
                  );
                } else if (snapshot.hasError) {
                  return Text("${snapshot.error}");
                }
              }

              // By default, show a loading spinner.
              return CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

更新网路数据

我们来使用http.put()方法从JSONPlaceholder 上的Album 中更新指定栏位的数据当作范例

Future<http.Response> updateAlbum(String title) {
  return http.put(
    'https://jsonplaceholder.typicode.com/albums/1',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );
}

http.put()方法返回一个Future包含的Response,该updateAlbum方法接受一个参数,该参数title被发送到服务器以更新Album

updateAlbum() 函数返回 Future<Album>,概念与上述例子相同:

  1. Map使用 dart:convert将Response 主体转换为JSON
  2. 如果服务器返回UPDATED状态码为200的Response,则使用工厂方法将JSONMap转换为AlbumfromJson()
  3. 如果服务器未返回UPDATED状态码为200的Response,则引发异常。(即使在404未找到服务器Response的情况下,也将引发异常,请勿返回null,这在检查取得的结果数据(snapshot)时很重要
Future<Album> updateAlbum(String title) async {
  final http.Response response = await http.put(
    'https://jsonplaceholder.typicode.com/albums',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );
  if (response.statusCode == 200) {
    // If the server did return a 200 UPDATED response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 UPDATED response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

给使用者自己更新标题栏位

创建一个TextField以输入标题,并创建一个RaisedButton 用来更新服务器上的数据。还定义一个TextEditingController以从读取用户输入TextField的值,以调用updateAlbum()方法

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    ...
    RaisedButton(
      child: Text('Update Data'),
      onPressed: () {
        setState(() {
          _futureAlbum = updateAlbum(_controller.text);
        });
      },
    ),
    ...
  ],
)

完整的程序码:

import 'dart:async';
import 'dart:convert';

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

Future<Album> fetchAlbum() async {
  final response =
      await http.get('https://jsonplaceholder.typicode.com/albums/1');

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response, then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response, then throw an exception.
    throw Exception('Failed to load album');
  }
}

Future<Album> deleteAlbum(String id) async {
  final http.Response response = await http.delete(
    'https://jsonplaceholder.typicode.com/albums/$id',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
  );

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON. After deleting,
    // you'll get an empty JSON `{}` response.
    // Don't return `null`, otherwise `snapshot.hasData`
    // will always return false on `FutureBuilder`.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a "200 OK response",
    // then throw an exception.
    throw Exception('Failed to delete album.');
  }
}

Future<Album> updateAlbum(String title) async {
  final http.Response response = await http.put(
    'https://jsonplaceholder.typicode.com/albums/1',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body));
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to update album.');
  }
}

class Album {
  final int id;
  final String title;

  Album({this.id, this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      id: json['id'],
      title: json['title'],
    );
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  MyApp({Key key}) : super(key: key);

  @override
  _MyAppState createState() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyApp> {
  final TextEditingController _controller = TextEditingController();
  Future<Album> _futureAlbum;

  @override
  void initState() {
    super.initState();
    _futureAlbum = fetchAlbum();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Update Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Update Data Example'),
        ),
        body: Container(
          alignment: Alignment.center,
          padding: const EdgeInsets.all(8.0),
          child: FutureBuilder<Album>(
            future: _futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                if (snapshot.hasData) {
                  return Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Text('${snapshot.data?.title ?? 'Deleted'}'),
                      TextField(
                        controller: _controller,
                        decoration: InputDecoration(hintText: 'Enter Title'),
                      ),
                      RaisedButton(
                        child: Text('Update Data'),
                        onPressed: () {
                          setState(() {
                            _futureAlbum = updateAlbum(_controller.text);
                          });
                        },
                      ),
                      RaisedButton(
                        child: Text('Delete Data'),
                        onPressed: () {
                          setState(() {
                            _futureAlbum =
                                deleteAlbum(snapshot.data.id.toString());
                          });
                        },
                      ),
                    ],
                  );
                } else if (snapshot.hasError) {
                  return Text("${snapshot.error}");
                }
              }

              return CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

发送数据给服务器

我们来使用http.post()方法将Album 标题发送给JSONPlaceholder

Future<http.Response> createAlbum(String title) {
  return http.post(
    'https://jsonplaceholder.typicode.com/albums',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );
}

createAlbum()方法采用一个参数title ,该参数发送到服务器以创建一个Album

做法都跟前面一样,差别在於CREATED成功的话,预期的状态码为201

完整的程序码:

import 'dart:async';
import 'dart:convert';

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

Future<Album> createAlbum(String title) async {
  final http.Response response = await http.post(
    'https://jsonplaceholder.typicode.com/albums',
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );

  if (response.statusCode == 201) {
    return Album.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('Failed to create album.');
  }
}

class Album {
  final int id;
  final String title;

  Album({this.id, this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      id: json['id'],
      title: json['title'],
    );
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  MyApp({Key key}) : super(key: key);

  @override
  _MyAppState createState() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyApp> {
  final TextEditingController _controller = TextEditingController();
  Future<Album> _futureAlbum;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Create Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Create Data Example'),
        ),
        body: Container(
          alignment: Alignment.center,
          padding: const EdgeInsets.all(8.0),
          child: (_futureAlbum == null)
              ? Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    TextField(
                      controller: _controller,
                      decoration: InputDecoration(hintText: 'Enter Title'),
                    ),
                    RaisedButton(
                      child: Text('Create Data'),
                      onPressed: () {
                        setState(() {
                          _futureAlbum = createAlbum(_controller.text);
                        });
                      },
                    ),
                  ],
                )
              : FutureBuilder<Album>(
                  future: _futureAlbum,
                  builder: (context, snapshot) {
                    if (snapshot.hasData) {
                      return Text(snapshot.data.title);
                    } else if (snapshot.hasError) {
                      return Text("${snapshot.error}");
                    }

                    return CircularProgressIndicator();
                  },
                ),
        ),
      ),
    );
  }
}

发起HTTP 认证授权请求

为了从众多的网络服务中获取数据,你需要提供相应的授权认证信息。当然了,解决这一问题的方法有很多,而最常见的方法或许就是使用AuthorizationHTTP header了

添加Authorization Headers:

http这个package提供了相当实用的方法来向请求中添加headers,你也可以使用dart:io来使用一些常见的HttpHeaders

Future<http.Response> fetchAlbum() {
  return http.get(
    'https://jsonplaceholder.typicode.com/albums/1',
    // Send authorization headers to the backend.
    headers: {HttpHeaders.authorizationHeader: "Basic your_api_token_here"},
  );
}

完整程序码:

import 'dart:async';
import 'dart:convert';
import 'dart:io';

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

Future<Album> fetchAlbum() async {
  final response = await http.get(
    'https://jsonplaceholder.typicode.com/albums/1',
    headers: {HttpHeaders.authorizationHeader: "Basic your_api_token_here"},
  );
  final responseJson = jsonDecode(response.body);

  return Album.fromJson(responseJson);
}

class Album {
  final int userId;
  final int id;
  final String title;

  Album({this.userId, this.id, this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

发起WebSockets 请求

除了普通的HTTP请求,你还可以通过WebSockets来连接服务器, WebSockets可以以非轮询的方式与服务器进行双向通信

在这里,你可以连接一个 由websocket.org提供的测试服务器。该服务器只会返回你发送的信息

  1. 连接WebSocket 服务器

    web_socket_channel 这个package 提供了连接WebSocket 服务器所需的一些工具

    该包提供的WebSocketChannel不仅可以让你监听到来自服务器的消息还可以让你向服务器推送消息

    首先添加依赖:pub.dev:http

    ...
    dependencies:
      web_socket_channel: ^1.1.0
    ...
    

    在Flutter中,只用一行代码就可以创建一个连接到服务器的WebSocketChannel

    final channel = IOWebSocketChannel.connect('ws://echo.websocket.org');
    
  2. 监听来自服务器的资讯

    建立了连接之後,你就可以监听来自服务器的消息了

    当你向测试服务器发送一条消息之後,它会将同样的消息发送回来

    此范例,我们用StreamBuilder组件来监听新消息,并使用Text组件来展示它们

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

    运作方式:

    WebSocketChannel提供了一个来自服务器的Stream类消息

    这个Stream类是dart:async包的基本组成部分,它提供了一个从数据源监听异步事件的方法。和Future不一样的是,Future只能返回一个单独的异步响应,而Stream类可以随着时间的推移传递很多事

    StreamBuilderwidget会和Stream建立起连接,并且每当它接收到一个使用给定builder()函数的事件时,就会通知Flutter去rebuild

  3. 向服务器发送数据

    要向服务器发送数据,可以使用WebSocketChannel提供的sink下的add()方法来发送信息

    WebSocketChannel提供了一个StreamSink来向服务器推送消息。

    这个StreamSink类提供了一个可以向数据源添加同步或者异步事件的通用方法

  4. 关闭WebSocket 连接

    当你使用完WebSocket之後,记得关闭这个连接。要关闭这个WebSocket连接,只需要关闭sink

    channel.sink.close();
    

完整的范例程序码:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final title = 'WebSocket Demo';
    return MaterialApp(
      title: title,
      home: MyHomePage(
        title: title,
        channel: IOWebSocketChannel.connect('ws://echo.websocket.org'),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;
  final WebSocketChannel channel;

  MyHomePage({Key key, @required this.title, @required this.channel})
      : super(key: key);

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

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Form(
              child: TextFormField(
                controller: _controller,
                decoration: InputDecoration(labelText: 'Send a message'),
              ),
            ),
            StreamBuilder(
              stream: widget.channel.stream,
              builder: (context, snapshot) {
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 24.0),
                  child: Text(snapshot.hasData ? '${snapshot.data}' : ''),
                );
              },
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _sendMessage,
        tooltip: 'Send message',
        child: Icon(Icons.send),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      widget.channel.sink.add(_controller.text);
    }
  }

  @override
  void dispose() {
    widget.channel.sink.close();
    super.dispose();
  }
}

在背景处理 JSON 数据

Dart 通常只会在单线程中处理它们的工作,并且在大多数情况中,基本不会出现像动画卡顿以及性能不足这种问题,但是,当你需要进行一个非常复杂的计算时,例如解析一个巨大的JSON 文档。如果这项工作耗时超过了16 毫秒,那麽你的用户就会感受到掉帧。

为了避免掉帧,像上面那样消耗性能的计算就应该放在後台处理。在Android平台上,这意味着你需要在不同的线程中进行调度工作。而在Flutter中,你可以使用一个单独的Isolate

在这个例子中,你将会使用http.get()方法通过 JSONPlaceholder REST API获取到一个包含5000张图片对象的超大JSON文档

Future<http.Response> fetchPhotos(http.Client client) async {
  return client.get('https://jsonplaceholder.typicode.com/photos');
}

在这个例子中你需要给方法添加了一个http.Client参数。这将使得该方法测试起来更容易同时也可以在不同环境中使用

接下来需要解析并将json 转换成一列图片

首先创建一个PhotoModel 类:

class Photo {
  final int id;
  final String title;
  final String thumbnailUrl;

  Photo({this.id, this.title, this.thumbnailUrl});

  factory Photo.fromJson(Map<String, dynamic> json) {
    return Photo(
      id: json['id'] as int,
      title: json['title'] as String,
      thumbnailUrl: json['thumbnailUrl'] as String,
    );
  }
}

现在,为了让fetchPhotos()方法可以返回一个 Future<List<Photo>>,来将Response 转换成一列图片,我们需要以下两点更新:

  1. 创建一个可以将响应体转换成List<Photo>的方法:parsePhotos()
  2. fetchPhotos()方法中使用parsePhotos()方法
// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();

  return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response =
      await client.get('https://jsonplaceholder.typicode.com/photos');

  return parsePhotos(response.body);
}

将部分工作移交到单独的isolate中:

如果你在一台很慢的手机上运行fetchPhotos()函数,你或许会注意到应用会有点卡顿,因为它需要解析并转换json。显然这并不好,所以你要避免它

通过Flutter提供的compute()方法将解析和转换的工作移交到一个後台isolate中。这个compute()函数可以在後台isolate中运行复杂的函数并返回结果。在这里,我们就需要将parsePhotos()方法放入後台

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response =
      await client.get('https://jsonplaceholder.typicode.com/photos');

  // Use the compute function to run parsePhotos in a separate isolate.
  return compute(parsePhotos, response.body);
}

使用Isolates 需要注意的地方:

Isolates通过来回传递消息来交流。这些消息可以是任何值,它们可以是nullnumbooldouble或者String,哪怕是像这个例子中的List<Photo>这样简单对像都没问题。

当你试图传递更复杂的对象时,你可能会遇到错误,例如在isolates之间的Future或者http.Response

完整范例程序码:

import 'dart:async';
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response =
      await client.get('https://jsonplaceholder.typicode.com/photos');

  // Use the compute function to run parsePhotos in a separate isolate.
  return compute(parsePhotos, response.body);
}

// A function that converts a response body into a List<Photo>.
List<Photo> parsePhotos(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();

  return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}

class Photo {
  final int albumId;
  final int id;
  final String title;
  final String url;
  final String thumbnailUrl;

  Photo({this.albumId, this.id, this.title, this.url, this.thumbnailUrl});

  factory Photo.fromJson(Map<String, dynamic> json) {
    return Photo(
      albumId: json['albumId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
      url: json['url'] as String,
      thumbnailUrl: json['thumbnailUrl'] as String,
    );
  }
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final appTitle = 'Isolate Demo';

    return MaterialApp(
      title: appTitle,
      home: MyHomePage(title: appTitle),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: FutureBuilder<List<Photo>>(
        future: fetchPhotos(http.Client()),
        builder: (context, snapshot) {
          if (snapshot.hasError) print(snapshot.error);

          return snapshot.hasData
              ? PhotosList(photos: snapshot.data)
              : Center(child: CircularProgressIndicator());
        },
      ),
    );
  }
}

class PhotosList extends StatelessWidget {
  final List<Photo> photos;

  PhotosList({Key key, this.photos}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
      ),
      itemCount: photos.length,
      itemBuilder: (context, index) {
        return Image.network(photos[index].thumbnailUrl);
      },
    );
  }
}

<<:  Day28 资料流重新导向I

>>:  Day 28 | AI & 深度学习的应用

Unity与Photon的新手相遇旅途 | Day11-敌人攻击

今天的内容为该如何简单制作出一个自动攻击的敌人 ...

Day 12 - 三朵云的入门(云端基础证照)

图片来源 接下来继续谈到Q2的目标是Azure Certificate, 云端相关证照, 因为我非...

Day23 ( 高级 ) 仙女棒 ( 光迹效果 )

仙女棒 ( 光迹效果 ) 教学原文参考:仙女棒 ( 光迹效果 ) 这篇文章会介绍,如何在 Scrat...

Day 29:Google Map 自订资讯视窗

本篇文章同步发表在 HKT 线上教室 部落格,线上影音教学课程已上架至 Udemy 和 Youtu...

Day 24「小步快跑」Service 与单元测试(上)

笔者前阵子蛮喜欢路跑的,但跑了很久,成绩却一直没有明显进步,为此感到因扰。後来有一天,一位朋友跟我说...