iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 28
0
自我挑戰組

從零開始的Flutter世界系列 第 28

Day28 Networking & http

  • 分享至 

  • xImage
  •  

上一篇講完如何處理已經得到的資訊數據,今天來看看我們是如何與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);
      },
    );
  }
}

上一篇
Day27 JSON and serialization
下一篇
Day29 Flutter Persistence
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言