Flutter學習指南:文件、存儲和網絡

Flutter學習指南
交互、手勢和動畫
UI佈局和控件
熟悉Dart語言
編寫第一個應用
開發環境搭建git

本篇文章咱們先學習 Flutter IO 相關的基礎知識,而後在 Flutter學習指南:交互、手勢和動畫 的基礎上,繼續開發一個 echo 客戶端。因爲平常開發中 HTTP 比 socket 更常見,咱們的 echo 客戶端將會使用 HTTP 協議跟服務端通訊。Echo 服務器也會使用 Dart 來實現。github

文件

爲了執行文件操做,咱們可使用 Dart 的 io 包:sql

import 'dart:io';
複製代碼

建立文件

在 Dart 裏,咱們經過類 File 來執行文件操做:shell

void foo() async {
  const filepath = "path to your file";
  var file = File(filepath);
  try {
    bool exists = await file.exists();
    if (!exists) {
      await file.create();
    }
  } catch (e) {
    print(e);
  }
}
複製代碼

相對於 CPU,IO 老是很慢的,因此大部分文件操做都返回一個 Future,並在出錯的時候拋出一個異常。若是你須要,也可使用同步版本,這些方法都帶一個後綴 Sync:數據庫

void foo() {
  const filepath = "path to your file";
  var file = File(filepath);
  try {
    bool exists = file.existsSync();
    if (!exists) {
      file.createSync();
    }
  } catch (e) {
    print(e);
  }
}
複製代碼

async 方法使得咱們能夠像寫同步方法同樣寫異步代碼,同步版本的 io 方法已經沒有太多使用的必要了(Dart 1 不支持 async 函數,因此同步版本的方法的存在是有必要的)。編程

寫文件

寫 String 時咱們可使用 writeAsString 和 writeAsBytes 方法:json

const filepath = "path to your file";
var file = File(filepath);
await file.writeAsString('Hello, Dart IO');
List<int> toBeWritten = [123];
await file.writeAsBytes(toBeWritten);
複製代碼

若是隻是爲了寫文件,還可使用 openWrite 打開一個 IOSink:後端

void foo() async {
  const filepath = "path to your file";
  var file = File(filepath);
  IOSink sink;
  try {
    sink = file.openWrite();
    // 默認的寫文件操做會覆蓋原有內容;若是要追究內容,用 append 模式
    // sink = file.openWrite(mode: FileMode.append);

    // write() 的參數是一個 Object,他會執行 obj.toString() 把轉換後
    // 的 String 寫入文件
    sink.write('Hello, Dart');
    //調用 flush 後纔會真的把數據寫出去
    await sink.flush();
  } catch (e) {
    print(e);
  } finally {
    sink?.close();
  }
}
複製代碼

讀文件

讀寫原始的 bytes 也是至關簡單的:數組

var msg = await file.readAsString();
List<int> content = await file.readAsBytes();
複製代碼

和寫文件相似,它還有一個 openRead 方法:服務器

// Stream 是 async 包裏的類
import 'dart:async';
// utf八、LineSplitter 屬於 convert 包
import 'dart:convert';
import 'dart:io';

void foo() async {
  const filepath = "path to your file";
  var file = File(filepath);
  try {
    Stream<List<int>> stream = file.openRead();
    var lines = stream
        // 把內容用 utf-8 解碼
        .transform(utf8.decoder)
        // 每次返回一行
        .transform(LineSplitter());
    await for (var line in lines) {
      print(line);
    }
  } catch (e) {
    print(e);
  }
}
複製代碼

最後須要注意的是,咱們讀寫 bytes 的時候,使用的對象是 List<int>,而一個 int 在 Dart 裏面有 64 位。Dart 一開始設計就是用於 Web,這部分的效率也就不那麼高了。

JSON

JSON 相關的 API 放在了 convert 包裏面:

import 'dart:convert';
複製代碼

把對象轉換爲 JSON

假設咱們有這樣一個對象:

class Point {
  int x;
  int y;
  String description;

  Point(this.x, this.y, this.description);
}
複製代碼

爲了把他轉換爲 JSON,咱們給他定義一個 toJson 方法(注意,不能改變他的方法簽名):

class Point {
  // ...

  // 注意,咱們的方法只有一個語句,這個語句定義了一個 map。
  // 使用這種語法的時候,Dart 會自動把這個 map 當作方法的返回值
  Map<Stringdynamic> toJson() => {
    'x': x,
    'y': y,
    'desc': description
  };
}
複製代碼

接下來咱們調用 json.encode 方法把對象轉換爲 JSON:

void main() {
  var point = Point(212'Some point');
  var pointJson = json.encode(point);
  print('pointJson = $pointJson');

  // List, Map 都是支持的
  var points = [point, point];
  var pointsJson = json.encode(points);
  print('pointsJson = $pointsJson');
}

// 執行後打印出:
// pointJson = {"x":2,"y":12,"desc":"Some point"}
// pointsJson = [{"x":2,"y":12,"desc":"Some point"},{"x":2,"y":12,"desc":"Some point"}]
複製代碼

把 JSON 轉換爲對象

首先,咱們給 Point 類再加多一個構造函數:

class Point {
  // ...

  Point.fromJson(Map<Stringdynamic> map)
      : x = map['x'], y = map['y'], description = map['desc'];

  // 爲了方便後面演示,也加入一個 toString
  @override
  String toString() {
    return "Point{x=$x, y=$y, desc=$description}";
  }
}
複製代碼

爲了解析 JSON 字符串,咱們能夠用 json.decode 方法:

dynamic obj = json.decode(jsonString);
複製代碼

返回一個 dynamic 的緣由在於,Dart 不知道傳進去的 JSON 是什麼。若是是一個 JSON 對象,返回值將是一個 Map;若是是 JSON 數組,則會返回 List<dynamic>:

void main() {
  var point = Point(212'Some point');
  var pointJson = json.encode(point);
  print('pointJson = $pointJson');
  var points = [point, point];
  var pointsJson = json.encode(points);
  print('pointsJson = $pointsJson');
  print('');

  var decoded = json.decode(pointJson);
  print('decoded.runtimeType = ${decoded.runtimeType}');
  var point2 = Point.fromJson(decoded);
  print('point2 = $point2');

  decoded = json.decode(pointsJson);
  print('decoded.runtimeType = ${decoded.runtimeType}');
  var points2 = <Point>[];
  for (var map in decoded) {
    points2.add(Point.fromJson(map));
  }
  print('points2 = $points2');
}
複製代碼

運行結果以下:

pointJson = {"x":2,"y":12,"desc":"Some point"}
pointsJson = [{"x":2,"y":12,"desc":"Some point"},{"x":2,"y":12,"desc":"Some point"}]

decoded.runtimeType = _InternalLinkedHashMap<String, dynamic>
point2 = Point{x=2, y=12, desc=Some point}
decoded.runtimeType = List<dynamic>
points2 = [Point{x=2, y=12, desc=Some point}, Point{x=2, y=12, desc=Some point}]
複製代碼

須要說明的是,咱們把 Map 轉化爲對象時使用時定義了一個構造函數,但這個是任意的,使用靜態方法、Dart 工廠方法等都是可行的。之因此限定 toJson 方法的原型,是由於 json.encode 只支持 Map、List、String、int 等內置類型。當它遇到不認識的類型時,若是沒有給它設置參數 toEncodable,就會調用對象的 toJson 方法(因此方法的原型不能改變)。

HTTP

爲了向服務器發送 HTTP 請求,咱們可使用 io 包裏面的 HttpClient。但它實在不是那麼好用,因而就有人弄出了一個 http 包。爲了使用 http 包,須要修改 pubspec.yaml:

# pubspec.yaml
dependencies:
  http: ^0.11.3+17
複製代碼

http 包的使用很是直接,爲了發出一個 GET,可使用 http.get 方法;對應的,還有 post、put 等。

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

Future<String> getMessage() async {
  try {
    final response = await http.get('http://www.xxx.com/yyy/zzz');
    if (response.statusCode == 200) {
      return response.body;
    }
  } catch (e) {
    print('getMessage: $e');
  }
  return null;
}
複製代碼

HTTP POST 的例子咱們在下面實現 echo 客戶端的時候再看。

使用 SQLite 數據庫

包 sqflite 可讓咱們使用 SQLite:

dependencies:
  sqflite: any
複製代碼

sqflite 的 API 跟 Android 的那些很是像,下面咱們直接用一個例子來演示:

import 'package:sqflite/sqflite.dart';

class Todo {
  static const columnId = 'id';
  static const columnTitle = 'title';
  static const columnContent = 'content';

  int id;
  String title;
  String content;

  Todo(this.title, this.content, [this.id]);

  Todo.fromMap(Map<Stringdynamic> map)
      : id = map[columnId], title = map[columnTitle], content = map[columnContent];

  Map<Stringdynamic> toMap() => {
    columnTitle: title,
    columnContent: content,
  };

  @override
  String toString() {
    return 'Todo{id=$id, title=$title, content=$content}';
  }
}

void foo() async {
  const table = 'Todo';
  // getDatabasesPath() 的 sqflite 提供的函數
  var path = await getDatabasesPath() + '/demo.db';
  // 使用 openDatabase 打開數據庫
  var database = await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        var sql ='''
            CREATE TABLE $table ('
            ${Todo.columnId} INTEGER PRIMARY KEY,'
            ${Todo.columnTitle} TEXT,'
            ${Todo.columnContent} TEXT'
            )
            '''
;
        // execute 方法能夠執行任意的 SQL
        await db.execute(sql);
      }
  );
  // 爲了讓每次運行的結果都同樣,先把數據清掉
  await database.delete(table);

  var todo1 = Todo('Flutter''Learn Flutter widgets.');
  var todo2 = Todo('Flutter''Learn how to to IO in Flutter.');

  // 插入數據
  await database.insert(table, todo1.toMap());
  await database.insert(table, todo2.toMap());

  List<Map> list = await database.query(table);
  // 從新賦值,這樣 todo.id 纔不會爲 0
  todo1 = Todo.fromMap(list[0]);
  todo2 = Todo.fromMap(list[1]);
  print('query: todo1 = $todo1');
  print('query: todo2 = $todo2');

  todo1.content += ' Come on!';
  todo2.content += ' I\'m tired';
  // 使用事務
  await database.transaction((txn) async {
    // 注意,這裏面只能用 txn。直接使用 database 將致使死鎖
    await txn.update(table, todo1.toMap(),
        // where 的參數裏,咱們可使用 ? 做爲佔位符,對應的值按順序放在 whereArgs

        // 注意,whereArgs 的參數類型是 List,這裏不能寫成 todo1.id.toString()。
        // 否則就變成了用 String 和 int 比較,這樣一來就匹配不到待更新的那一行了
        where: '${Todo.columnId} = ?', whereArgs: [todo1.id]);
    await txn.update(table, todo2.toMap(),
        where: '${Todo.columnId} = ?', whereArgs: [todo2.id]);
  });

  list = await database.query(table);
  for (var map in list) {
    var todo = Todo.fromMap(map);
    print('updated: todo = $todo');
  }

  // 最後,別忘了關閉數據庫
  await database.close();
}
複製代碼

運行結果以下:

query: todo1 = Todo{id=1, title=Flutter, content=Learn Flutter widgets}
query: todo2 = Todo{id=2, title=Flutter, content=Learn how to to IO in Flutter}
updated: todo = Todo{id=1, title=Flutter, content=Learn Flutter widgets. Come on!}
updated: todo = Todo{id=2, title=Flutter, content=Learn how to to IO in Flutter. I'm tired}
複製代碼

有 Android 經驗的讀者會發現,使用 Dart 編寫數據庫相關代碼的時候舒服不少。若是讀者對數據庫不太熟悉,能夠參考《SQL必知必會》。本篇的主要知識點到這裏的就講完了,做爲練習,下面咱們就一塊兒來實現 echo 客戶端的後端。

echo 客戶端

HTTP 服務端

在開始以前,你能夠在 GitHub 上找到上篇文章的代碼,咱們將在它的基礎上進行開發。

git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout ux-basic
複製代碼

服務端架構

首先咱們來看看服務端的架構(說是架構,但其實很是的簡單,或者說很簡陋):

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

class HttpEchoServer {

  final int port;
  HttpServer httpServer;
  // 在 Dart 裏面,函數也是 first class object,因此咱們能夠直接把
  // 函數放到 Map 裏面
  Map<Stringvoid Function(HttpRequest)> routes;

  HttpEchoServer(this.port) {
    _initRoutes();
  }

  void _initRoutes() {
    routes = {
      // 咱們只支持 path 爲 '/history' 和 '/echo' 的請求。
      // history 用於獲取歷史記錄;
      // echo 則提供 echo 服務。
      '/history': _history,
      '/echo': _echo,
    };
  }

  // 返回一個 Future,這樣客戶端就可以在 start 完成後作一些事
  Future start() async {
    // 1. 建立一個 HttpServer
    httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
    // 2. 開始監聽客戶請求
    return httpServer.listen((request) {
      final path = request.uri.path;
      final handler = routes[path];
      if (handler != null) {
        handler(request);
      } else {
        // 給客戶返回一個 404
        request.response.statusCode = HttpStatus.notFound;
        request.response.close();
      }
    });
  }

  void _history(HttpRequest request) {
    // ...
  }

  void _echo(HttpRequest request) async {
    // ...
  }

  void close() async {
    var server = httpServer;
    httpServer = null;
    await server?.close();
  }
}
複製代碼

在服務端框架裏,咱們把支持的全部路徑都加到 routes 裏面,當收到客戶請求的時候,只須要直接從 routes 裏取出對應的處理函數,把請求分發給他就能夠了。若是讀者對服務端編程沒有太大興趣或不太瞭解,這部分能夠不用太關注。

將對象序列化爲 JSON

爲了把 Message 對象序列化爲 JSON,這裏咱們對 Message 作一些小修改:

class Message {
  final String msg;
  final int timestamp;

  Message(this.msg, this.timestamp);
  Message.create(String msg)
      : msg = msg, timestamp = DateTime.now().millisecondsSinceEpoch;

  Map<Stringdynamic> toJson() => {
    "msg""$msg",
    "timestamp": timestamp
  };

  @override
  String toString() {
    return 'Message{msg: $msg, timestamp: $timestamp}';
  }
}
複製代碼

這裏咱們加入一個 toJson 方法。下面是服務端的 _echo 方法:

class HttpEchoServer {
  static const GET = 'GET';
  static const POST = 'POST';

  const List<Message> messages = [];

  // ...

  _unsupportedMethod(HttpRequest request) {
    request.response.statusCode = HttpStatus.methodNotAllowed;
    request.response.close();
  }

  void _echo(HttpRequest request) async {
    if (request.method != POST) {
      _unsupportedMethod(request);
      return;
    }

    // 獲取從客戶端 POST 請求的 body,更多的知識,參考
    // https://www.dartlang.org/tutorials/dart-vm/httpserver
    String body = await request.transform(utf8.decoder).join();
    if (body != null) {
      var message = Message.create(body);
      messages.add(message);
      request.response.statusCode = HttpStatus.ok;
      // json 是 convert 包裏的對象,encode 方法還有第二個參數 toEncodable。當遇到對象不是
      // Dart 的內置對象時,若是提供這個參數,就會調用它對對象進行序列化;這裏咱們沒有提供,
      // 因此 encode 方法會調用對象的 toJson 方法,這個方法在前面咱們已經定義了
      var data = json.encode(message);
      // 把響應寫回給客戶端
      request.response.write(data);
    } else {
      request.response.statusCode = HttpStatus.badRequest;
    }
    request.response.close();
  }
}
複製代碼

HTTP 客戶端

咱們的 echo 服務器使用了 dart:io 包裏面 HttpServer 來開發。對應的,咱們也可使用這個包裏的 HttpRequest 來執行 HTTP 請求,但這裏咱們並不打算這麼作。第三方庫 http 提供了更簡單易用的接口。

首先把依賴添加到 pubspec 裏:

# pubspec.yaml
dependencies:
  # ...

  http: ^0.11.3+17
複製代碼

客戶端實現以下:

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

class HttpEchoClient {
  final int port;
  final String host;

  HttpEchoClient(this.port): host = 'http://localhost:$port';

  Future<Message> send(String msg) async {
    // http.post 用來執行一個 HTTP POST 請求。
    // 它的 body 參數是一個 dynamic,能夠支持不一樣類型的 body,這裏咱們
    // 只是直接把客戶輸入的消息發給服務端就能夠了。因爲 msg 是一個 String,
    // post 方法會自動設置 HTTP 的 Content-Type 爲 text/plain
    final response = await http.post(host + '/echo', body: msg);
    if (response.statusCode == 200) {
      Map<Stringdynamic> msgJson = json.decode(response.body);
      // Dart 並不知道咱們的 Message 長什麼樣,咱們須要本身經過
      // Map<String, dynamic> 來構造對象
      var message = Message.fromJson(msgJson);
      return message;
    } else {
      return null;
    }
  }
}

class Message {
  final String msg;
  final int timestamp;

  Message.fromJson(Map<Stringdynamic> json)
    : msg = json['msg'], timestamp = json['timestamp'];

  // ...
}
複製代碼

如今,讓咱們把他們和上一節的 UI 結合到一塊兒。首先啓動服務器,而後建立客戶端:

HttpEchoServer _server;
HttpEchoClient _client;

class _MessageListState extends State<MessageList{
  final List<Message> messages = [];

  @override
  void initState() {
    super.initState();

    const port = 6060;
    _server = HttpEchoServer(port);
    // initState 不是一個 async 函數,這裏咱們不能直接 await _server.start(),
    // future.then(...) 跟 await 是等價的
    _server.start().then((_) {
      // 等服務器啓動後才建立客戶端
      _client = HttpEchoClient(port);
    });
  }

  // ...
}
複製代碼
class MessageListScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // ...

      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          final result = await Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => AddMessageScreen())
          );
          // 如下是修改了的地方
          if (_client == nullreturn;
          // 如今,咱們不是直接構造一個 Message,而是經過 _client 把消息
          // 發送給服務器
          var msg = await _client.send(result);
          if (msg != null) {
            messageListKey.currentState.addMessage(msg);
          } else {
            debugPrint('fail to send $result');
          }
        },
        // ...
      )
    );
  }
}
複製代碼

大功告成,在作了這麼多工做之後,咱們的應用如今是真正的 echo 客戶端了,雖然看起來跟以前沒什麼兩樣。接下來,咱們就作一些跟以前不同的——把歷史記錄保存下來。

歷史記錄存儲、恢復

獲取應用的存儲路徑

爲了得到應用的文件存儲路徑,咱們引入多一個庫:

# pubspec.yaml
dependencies:
  # ...

  path_provider: ^0.4.1
複製代碼

經過它咱們能夠拿到應用的 file、cache 和 external storage 的路徑:

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

class HttpEchoServer {
  String historyFilepath;

  Future start() async {
    historyFilepath = await _historyPath();

    // ...
  }

  Future<String> _historyPath() async {
    // 獲取應用私有的文件目錄
    final directory = await path_provider.getApplicationDocumentsDirectory();
    return directory.path + '/messages.json';
  }
}
複製代碼

保存歷史記錄

class HttpEchoServer {

  void _echo(HttpRequest request) async {
    // ...

    // 原諒我,爲了簡單,咱們就多存幾回吧
    _storeMessages();
  }

  Future<bool> _storeMessages() async {
    try {
      // json.encode 支持 List、Map
      final data = json.encode(messages);
      // File 是 dart:io 裏的類
      final file = File(historyFilepath);
      final exists = await file.exists();
      if (!exists) {
        await file.create();
      }
      file.writeAsString(data);
      return true;
    // 雖然文件操做方法都是異步的,咱們仍然能夠經過這種方式 catch 到
    // 他們拋出的異常
    } catch (e) {
      print('_storeMessages: $e');
      return false;
    }
  }
}
複製代碼

加載歷史記錄

class HttpEchoServer {

  // ...

  Future start() async {
    historyFilepath = await _historyPath();
    // 在啓動服務器前先加載歷史記錄
    await _loadMessages();
    httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
    // ...
  }

  Future _loadMessages() async {
    try {
      var file = File(historyFilepath);
      var exists = await file.exists();
      if (!exists) return;

      var content = await file.readAsString();
      var list = json.decode(content);
      for (var msg in list) {
        var message = Message.fromJson(msg);
        messages.add(message);
      }
    } catch (e) {
      print('_loadMessages: $e');
    }
  }
}
複製代碼

如今,咱們來實現 _history 函數:

class HttpEchoServer {
  // ...

  void _history(HttpRequest request) {
    if (request.method != GET) {
      _unsupportedMethod(request);
      return;
    }

    String historyData = json.encode(messages);
    request.response.write(historyData);
    request.response.close();
  }
}
複製代碼

_history 的實現很直接,咱們只是把 messages 全都返回給客戶端。

接下來是客戶端部分:

class HttpEchoClient {

  // ...

  Future<List<Message>> getHistory() async {
    try {
      // http 包的 get 方法用來執行 HTTP GET 請求
      final response = await http.get(host + '/history');
      if (response.statusCode == 200) {
        return _decodeHistory(response.body);
      }
    } catch (e) {
      print('getHistory: $e');
    }
    return null;
  }

  List<Message> _decodeHistory(String response) {
    // JSON 數組 decode 出來是一個 <Map<String, dynamic>>[]
    var messages = json.decode(response);
    var list = <Message>[];
    for (var msgJson in messages) {
      list.add(Message.fromJson(msgJson));
    }
    return list;
  }
}


class _MessageListState extends State<MessageList{
  final List<Message> messages = [];

  @override
  void initState() {
    super.initState();

    const port = 6060;
    _server = HttpEchoServer(port);
    _server.start().then((_) {
      // 咱們等服務器啓動後才建立客戶端
      _client = HttpEchoClient(port);
      // 建立客戶端後立刻拉取歷史記錄
      _client.getHistory().then((list) {
        setState(() {
          messages.addAll(list);
        });
      });
    });
  }

  // ...
}
複製代碼

生命週期

最後須要作的是,在 APP 退出後關閉服務器。這就要求咱們可以收到應用生命週期變化的通知。爲了達到這個目的,Flutter 爲咱們提供了 WidgetsBinding 類(雖然沒有 Android 的 Lifecycle 那麼好用就是啦)。

// 爲了使用 WidgetsBinding,咱們繼承 WidgetsBindingObserver 而後覆蓋相應的方法
class _MessageListState extends State<MessageListwith WidgetsBindingObserver {

  // ...

  @override
  void initState() {
    // ...
    _server.start().then((_) {
      // ...

      // 註冊生命週期回調
      WidgetsBinding.instance.addObserver(this);
    });
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      var server = _server;
      _server = null;
      server?.close();
    }
  }
}
複製代碼

如今,咱們的應用是這個樣子的:

flutter-echo-demo
flutter-echo-demo

全部的代碼能夠在 GitHub 上找到:

git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout io-basic
複製代碼

使用 SQLite 數據庫

前面的實現中咱們把 echo 服務器的數據存放在了文件裏。這一節咱們改一改,把數據存到 SQLite 中。

別忘了添加依賴:

dependencies:
  sqflite: any
複製代碼

初始化數據庫

import 'package:sqflite/sqflite.dart';

class HttpEchoServer {
  // ...

  static const tableName = 'History';
  // 這部分常量最好是放到 Message 的定義裏。爲了方便閱讀,就暫且放這裏吧
  static const columnId = 'id';
  static const columnMsg = 'msg';
  static const columnTimestamp = 'timestamp';

  Database database;

  Future start() async {
    await _initDatabase();

    // ...
  }

  Future _initDatabase() async {
    var path = await getDatabasesPath() + '/history.db';
    database = await openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        var sql = '''
            CREATE TABLE $tableName (
            $columnId INTEGER PRIMARY KEY,
            $columnMsg TEXT,
            $columnTimestamp INTEGER
            )
            '''
;
        await db.execute(sql);
      }
    );
  }
}
複製代碼

加載歷史記錄

加載歷史記錄的相關代碼在 _loadMessages 方法中,這裏咱們修改原有的實現,讓它從數據庫加載數據:

class HttpEchoServer {
  // ...

  Future _loadMessages() async {
    var list = await database.query(
      tableName,
      columns: [columnMsg, columnTimestamp],
      orderBy: columnId,
    );
    for (var item in list) {
      // fromJson 也適用於使用數據庫的場景
      var message = Message.fromJson(item);
      messages.add(message);
    }
  }
}
複製代碼

實際上改成使用數據庫來存儲後,咱們並不須要把全部的消息都存放在內存中(也就是這裏的 _loadMessage 是沒必要要的)。客戶請求歷史記錄時,咱們再按需從數據庫讀取數據便可。爲了不修改到程序的邏輯,這裏仍是繼續保持一份數據在內存中。有興趣的讀者能夠對程序做出相應的修改。

保存記錄

記錄的保存很簡單,一行代碼就能夠搞定了:

void _echo(HttpRequest request) async {
  // ...

  _storeMessage(message);
}

void _storeMessage(Message msg) {
  database.insert(tableName, msg.toJson());
}
複製代碼

使用 JSON 的版本,咱們每次都須要把全部的數據都保存一遍。對數據庫來講,只要把收到的這一條信息存進去便可。讀者也應該可以感覺到,就咱們的需求來講,使用 SQLite 的版本實現起來更簡單,也更高效。

關閉數據庫

close 方法也要作相應的修改:

void close() async {
  // ...

  var db = database;
  database = null;
  db?.close();
}
複製代碼

這部分代碼能夠查看 tag echo-db:

git clone https://github.com/Jekton/flutter_demo.git
cd flutter_demo
git checkout echo-db
複製代碼



編程·思惟·職場
歡迎掃碼關注

相關文章
相關標籤/搜索