承香墨影
只分享最有用的原创技术干货!
关注
Flutter 发布了一段时间了,今天接前几次分享的,基于 Flutter 开发的书架 App 继续扩展功能。
本系列是海外一位学生,在简单阅读 Flutter 文档之后所写,我想如果是你,你能做的更好。
还不了解的可以先看看之前的两篇文章:
Flutter:一小时从零构建一个简单的 App,以及你如何做到这一点!
Flutter:扩展上周的“书架” App,利用数据库来存储笔记和收藏!
— 承香墨影
作者 | Norbert
翻译 | 承香墨影
授权 承香墨影 翻译并发布
在这篇文章中,我们将讨论使用更好的方法来存储/缓存/加载数据。
在今天,大多数应用都依赖服务器来显示数据,并结合缓存、数据库和错误处理,这可能让逻辑变的非常混乱。而幸运的是,有一种设计模式使得访问数据变得非常方便和简单。
每个页面都需要显示数据,因此向网络请求加载数据。假设你还想对数据进行缓存,现在的流程是,页面首先需要检查缓存中的数据,然后再检查网络请求回来的数据。添加到数据层的所有内容,最终都会反映在页面的 UI 代码中。
代码紧密耦合,组织不良,问题出现在我们通过页面去管理数据。
Repository 对于页面而言,是一个数据源。更具体的说,对于应用的其他部分来说,它是唯一的数据来源。请求数据的页面并不一定需要知道数据是从哪里加载出来的,这将允许所有与数据相关的操作都在一个地方完成。这样访问数据将变得更加容易,并且也有利于今后的重构和修改。
data 目录包含了所有与数据相关的类。
我将页面和小部件,分别拆分到了 pages 和 widgets 目录下。
与之前一样,代码已被放在了 Github 上,建议阅读源码查看细节。
https://github.com/Norbert515/BookSearch/tree/v3
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
已经可重用了。我们继续构建一个现实所有已加收藏(星标)的书籍列表页面。
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"');
我继续保留了加载滚动控件,因为在某些场景下,书籍也将来自网络服务。
这样,集合页面就算是完成了。
至此,我们学会了:
数据抽象,以及如何使整体代码更具有可读性并且易于管理。
使用小部件,使他们更容易被重用和更易于调试。
今天在公众号后台回复成长『成长』,将会得到我整理的一些学习资料,也能回复『加群』,一起学习进步。
推荐阅读:
漫画:程序员,你能“管理”好你的产品经理吗?
利用 Kotlin 的特性,优化 Intent 的数据传递!
解决 Lottie 动画包含图片的问题!
Google 的 Flutter 学习资料!
远程控制智能电视,方案已开源!