Flutter 系列三:优化"书架"App,以正确的方式管理数据!

Posted 承香墨影

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 系列三:优化"书架"App,以正确的方式管理数据!相关的知识,希望对你有一定的参考价值。

承香墨影
只分享最有用的原创技术干货!
Flutter 系列三:优化"书架"App,以正确的方式管理数据!

Flutter 发布了一段时间了,今天接前几次分享的,基于 Flutter 开发的书架 App 继续扩展功能。

本系列是海外一位学生,在简单阅读 Flutter 文档之后所写,我想如果是你,你能做的更好。

还不了解的可以先看看之前的两篇文章:

— 承香墨影



作者 | Norbert
翻译 | 承香墨影

授权 承香墨影 翻译并发布

在这篇文章中,我们将讨论使用更好的方法来存储/缓存/加载数据。

在今天,大多数应用都依赖服务器来显示数据,并结合缓存、数据库和错误处理,这可能让逻辑变的非常混乱。而幸运的是,有一种设计模式使得访问数据变得非常方便和简单。

没有模式的时候:

Flutter 系列三:优化"书架"App,以正确的方式管理数据!

每个页面都需要显示数据,因此向网络请求加载数据。假设你还想对数据进行缓存,现在的流程是,页面首先需要检查缓存中的数据,然后再检查网络请求回来的数据。添加到数据层的所有内容,最终都会反映在页面的 UI 代码中。

代码紧密耦合,组织不良,问题出现在我们通过页面去管理数据。

采用 repository

Flutter 系列三:优化"书架"App,以正确的方式管理数据!

Repository 对于页面而言,是一个数据源。更具体的说,对于应用的其他部分来说,它是唯一的数据来源。请求数据的页面并不一定需要知道数据是从哪里加载出来的,这将允许所有与数据相关的操作都在一个地方完成。这样访问数据将变得更加容易,并且也有利于今后的重构和修改。


项目结构

Flutter 系列三:优化"书架"App,以正确的方式管理数据!
  • data 目录包含了所有与数据相关的类。

  • 我将页面和小部件,分别拆分到了 pages 和 widgets 目录下。

与之前一样,代码已被放在了 Github 上,建议阅读源码查看细节。

https://github.com/Norbert515/BookSearch/tree/v3

Repository

class Repository {
 static final Repository _repo = new Repository._internal();
 BookDatabase database;
 static Repository get() {
   return _repo;
 }
 Repository._internal() {
   database = BookDatabase.get();
 }
}

和 BookDatabase 一样,Repository 是一个单例类。

  Future updateBook(Book book) async {
   database.updateBook(book);
 }
 Future close() async {
   return database.close();
 }

这些调用不需要任何额外的逻辑,因此只需要简单的委托给数据库操作。

  /// Fetches the books from the Google Books Api with the query parameter being input.
 /// If a book also exists in the local storage (eg. a book with notes/ stars) that version of the book will be used instead
 Future<ParsedResponse<List<Book>>> getBooks(String input) async{
   //http request, catching error like no internet connection.
   //If no internet is available for example response is
    http.Response response = await http.get("https://www.googleapis.com/books/v1/volumes?q=$input")
        .catchError((resp) {});
    if(response == null) {
      return new ParsedResponse(NO_INTERNET, []);
    }
    //If there was an error return an empty list
    if(response.statusCode < 200 || response.statusCode >= 300) {
      return new ParsedResponse(response.statusCode, []);
    }
    // Decode and go to the items part where the necessary book information is
    List<dynamic> list = JSON.decode(response.body)['items'];
    Map<String, Book> networkBooks = {};
   for(dynamic jsonBook in list) {
     Book book = new Book(
         title: jsonBook["volumeInfo"]["title"],
         url: jsonBook["volumeInfo"]["imageLinks"]["smallThumbnail"],
         id: jsonBook["id"]
     );
     networkBooks[book.id] = book;
   }
   List<Book> databaseBook = await database.getBooks([]..addAll(networkBooks.keys));
   for(Book book in databaseBook) {
     networkBooks[book.id] = book;
   }
   return new ParsedResponse(response.statusCode, []..addAll(networkBooks.values));
 }

此方法,基于搜索查询返回所有的数据。这里返回的结果,来自互联网的数据以及本地数据库中存储的数据集合。

在,每本书都会进行一次数据库查询。这对于庞大的数据库来说,非常的低效。但是,在这里它只涉及一个 SQL 查询:

  • 首先,将来自网络的所有书籍数据,都存储在一个 Map 中。

  • 然后按 Key 进行查询,返回本地的图书数据。

  • 这些从数据库中查询到的数据,包含一些用户的操作数据,因此我们可以简单的用本地数据库中的内容替换掉网络请求的内容。

  /// Get all books with ids, will return a list with all the books found
 Future<List<Book>> getBooks(List<String> ids) async{
   var db = await _getDb();
   // Building SELECT * FROM TABLE WHERE ID IN (id1, id2, ..., idn)
   var idsString = ids.map((it) => '"$it"').join(',');
   var result = await db.rawQuery('SELECT * FROM $tableName WHERE ${Book.db_id} IN ($idsString)');
   var books = [];
   for(Map<String, dynamic> item in result) {
     books.add(new Book.fromMap(item));
   }
   return books;
 }

将列表转换成 ("id0","id1","id2"…"idn")  并将它插入到 SQL 的查询条件中。

错误处理

你可能已经注意到了,ParsedResponse 将用来包装方法返回的数据。

/// A class similar to http.Response but instead of a String describing the body
/// it already contains the parsed Dart-Object
class ParsedResponse<T> {
 ParsedResponse(this.statusCode, this.body);
 final int statusCode;
 final T body;
 bool isOk() {
   return statusCode >= 200 && statusCode < 300;
 }
}

这个简介的小型泛型类中,包含了已经解析的数据和状态码。这使得我们能够返回一个随时可用的 Dart 对象以及附加的信息。例如服务器状态码。

  void _textChanged(String text) {
   if(text.isEmpty) {
     setState((){_isLoading = false;});
     _clearList();
     return;
   }
   setState((){_isLoading = true;});
   _clearList();
   Repository.get().getBooks(text)
   .then((books){
     setState(() {
       _isLoading = false;
       if(books.isOk()) {
         _items = books.body;
       } else {
         scaffoldKey.currentState.showSnackBar(new SnackBar(content: new Text("Something went wrong, check your internet connection")));
       }
     });
   });
 }

现在 ,searct_book 看起来更加简洁了。数据流如下:

  • 请求数据。

  • 当请求数据回来时,检查状态码并决定是显示结果还是错误信息。

尽可能的利用小部件

在,BookCard 管理的是它自己的点击事件,意思是当按下的时候,它会直接去数据库中更新对应的数据。但是此时有一个更好的方案。

class BookCard extends StatefulWidget {
 BookCard({
   this.book,
   @required this.onCardClick,
   @required this.onStarClick,
 });
 final Book book;
 final VoidCallback onCardClick;
 final VoidCallback onStarClick;
 @override
 State<StatefulWidget> createState() => new BookCardState();
}

现在,BookCard 点击事件监听器上会承载两个点击事件,一个用于点击星号,另一个用于点击卡片上其他的部分。这个 BookCard 不需要知道任何关于数据库的操作,这样它就解耦出来,可以用在应用程序的任何地方。

此外,现在逻辑更清晰了,一个简单的 Card 不应该知道数据库的存在,它的唯一功能就是现实数据和通知点击事件。


新的页面

我们现在将数据库集中访问,并且 BookCard 已经可重用了。我们继续构建一个现实所有已加收藏(星标)的书籍列表页面。

Flutter 系列三:优化"书架"App,以正确的方式管理数据!

lass _CollectionPageState extends State<CollectionPage> {
 List<Book> _items = new List();
 bool _isLoading = false;
 @override
 void initState() {
   super.initState();
   Repository.get().getFavoriteBooks()
     .then((books) {
     setState(() {
       _items = books;
     });
   });
 }

此外还有一个共有的方法 getFavoriteBooks() ,这个方法被委托给数据库,然后执行 rawQuery() 方法。

var result = await db.rawQuery('SELECT * FROM $tableName WHERE ${Book.db_star} = "1"');

我继续保留了加载滚动控件,因为在某些场景下,书籍也将来自网络服务。

这样,集合页面就算是完成了。

小结

至此,我们学会了:

  • 数据抽象,以及如何使整体代码更具有可读性并且易于管理。

  • 使用小部件,使他们更容易被重用和更易于调试。

推荐阅读:

以上是关于Flutter 系列三:优化"书架"App,以正确的方式管理数据!的主要内容,如果未能解决你的问题,请参考以下文章

全栈项目|小书架|小程序端-评论功能实现

性能优化方法论系列三性能优化的核心思想

性能优化方法论系列三性能优化的核心思想

性能优化方法论系列三性能优化的核心思想

优化 mongodb 中的查询

执行搜索引擎优化的链接属性要求