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

承香墨影

只分享最有用的原创技术干货!

关注

Flutter 系列三:优化

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

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

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

Flutter:一小时从零构建一个简单的 App,以及你如何做到这一点!

Flutter:扩展上周的“书架” App,利用数据库来存储笔记和收藏!

— 承香墨影

作者 | Norbert

翻译 | 承香墨影

授权 承香墨影 翻译并发布

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

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

没有模式的时候:

Flutter 系列三:优化

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

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

采用 repository

Flutter 系列三:优化

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


项目结构

Flutter 系列三:优化
  • 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>> 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 list = JSON.decode(response.body)['items'];
    Map 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 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> getBooks(List 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 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 {
 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 createState() => new BookCardState();
}

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

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


新的页面

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

Flutter 系列三:优化

lass _CollectionPageState extends State {
 List _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 系列三:优化

推荐阅读:

  • 漫画:程序员,你能“管理”好你的产品经理吗?

  • 利用 Kotlin 的特性,优化 Intent 的数据传递!

  • 解决 Lottie 动画包含图片的问题!

  • Google 的 Flutter 学习资料!

  • 远程控制智能电视,方案已开源!

Flutter 系列三:优化

你可能感兴趣的:(Flutter 系列三:优化"书架"App,以正确的方式管理数据!)