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

承香墨影

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

关注

上周,分享了一遍 Flutter 的入门文章,从零搭建一个简单的 App。今天继续分享它的续集,在原有 App 的基础之上,扩展出更多的功能,Flutter 让一切都变得简单,希望你能喜欢!

— 承香墨影

作者 | Norbert

翻译 | 承香墨影

授权 承香墨影 翻译并发布

在上一篇关于 Flutter 的文章《Flutter:一小时从零构建一个简单的 App,以及你如何做到这一点!》中,我谈到了我是如何在一个小时内,利用 Flutter 构建 App 的,该 App 已经完成。它的目的是通过列表展示网络上的图书,这些功能现在已经实现了,但是它们可以更好。

我想把本文写成一个系列,在整个系列中,我将继续改进应用程序并添加令人兴奋的新功能,并最终将其发布到 Google Play 商店和 Apple 的 App Store 上。


在这篇文字中,我们将介绍如何使用数据库来存储笔记和喜欢的书籍。

我添加了几个类,如果你想了解细节,建议直接在 Github 上阅读源码( https://github.com/Norbert515/BookSearch)。

  • Book.dart 是一个包含标题、网址、ID、收藏(加星)和笔记的 Model 类。

  • utils.dart 包含渐变的过渡动画,以及元素的转场动画。

  • 还有一些其他的类,之后会解释。

我还将 BookCard 放入了一个小部件中(main.dart 内)。

该数据库将仅用来存储被加了星号和笔记的数据,这样搜索所有书籍的时候,不会弄乱数据库。


在第一部分中,我们只有一个类,这是我刻意为之的,因为代码并不是很多。而且把所有的代码都放在一个地方,对于那样的一个简单 App 来说,是很好的。

但是随着项目开始增长,必须要调整结构了。

我做了一些结构上的改进,例如:

  • Book 类越来越大,所以我将它抽成了一个单独的类。

  • 使用 BookCard 来扩展 StatefulWidget。

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

它需要通过 Book 对象来初始化。

class BookCardState extends State<BookCard> {
 Book bookState;
 @override
 void initState() {
   super.initState();
   bookState = widget.book;

在 BookCardState 中,持有一个变量来存储 Book 对象,这是因为数据库中可能包含同一本书所提供的更多信息,并且用户可以在任何时间更新这些信息。

数据库

在 pubspec.yaml 文件中,添加了两个依赖项: Sqfilte 、 path_provider。

sqflite: any
path_provider: "^0.3.1"

代码如下:

class BookDatabase {
 static final BookDatabase _bookDatabase = new BookDatabase._internal();
 final String tableName = "Books";
 Database db;
 static BookDatabase get() {
   return _bookDatabase;
 }
 BookDatabase._internal();
 Future init() async {
   // Get a location using path_provider
   Directory documentsDirectory = await getApplicationDocumentsDirectory();
   String path = join(documentsDirectory.path, "demo.db");
   db = await openDatabase(path, version: 1,
       onCreate: (Database db, int version) async {
         // When creating the db, create the table
         await db.execute(
             "CREATE TABLE $tableName ("
                 "${Book.db_id} STRING PRIMARY KEY,"
                 "${Book.db_title} TEXT,"
                 "${Book.db_url} TEXT,"
                 "${Book.db_star} BIT,"
                 "${Book.db_notes} TEXT"
                 ")");
       });
 }

数据库是一个单例模式,可以使用静态的 BookDatabase.get() 方法,获取到它。init() 方法用来将数据库正确的初始化(利用 path_provider),并创建必要的 SQLite 表。

  /// Get a book by its id, if there is not entry for that ID, returns null.
 Future<Book> getBook(String id) async{
   var result = await db.rawQuery('SELECT * FROM $tableName WHERE ${Book.db_id} = "$id"');
   if(result.length == 0)return null;
   return new Book.fromMap(result[0]);
 }

这里使用 db.rawQuery() 来查询数据,它是一个标准的 SQL 查询语句,在查询完成之后会返回一个 Map 集合。查询结果可能包含很多条数据,因为可能存在多个与搜索条件相匹配的查询结果,这些数据被存储在 Map<String,dynamic> 中,我们必须自己去解析它。

  /// Inserts or replaces the book.
 Future updateBook(Book book) async {
   await db.inTransaction(() async {
     await db.rawInsert(
         'INSERT OR REPLACE INTO '
             '$tableName(${Book.db_id}, ${Book.db_title}, ${Book.db_url}, ${Book.db_star}, ${Book.db_notes})'
             ' VALUES("${book.id}", "${book.title}", "${book.url}", ${book.starred? 1:0}, "${book.notes}")');
   });
 }

最后它将书籍数据,插入或者更新到数据库当中。

就是这样,很容易不是吗?

现在,数据库的功能已经封装完成,我们还需要在我们的项目中使用它。这在 Flutter 中,一切都变得简单。

  @override
 void initState() {
   super.initState();
   bookState = widget.book;
   BookDatabase.get().getBook(widget.book.id)
       .then((book){
         if (book == null) return;
         setState((){
           bookState = book;
         });
     });
}

在获取到数据之后,我们遍历每条来自网络的书籍,是否同时也存在于数据库中,将匹配命中的数据用本地存储的书的内容替换掉。

child: new IconButton(
 icon: bookState.starred? new Icon(Icons.star): new Icon(Icons.star_border),
 color: Colors.black,
 onPressed: (){
   setState(() {
     bookState.starred = !bookState.starred;
   });
   BookDatabase.get().updateBook(bookState);
 },
),

当用户按下星号图标的时候,用新的信息更新数据库,并将图标做一个简单的动画,给用户一个反馈。

  @override
 void initState() {
   super.initState();
   _textController = new TextEditingController(text: widget.book.notes);
   subject.stream.debounce(new Duration(milliseconds: 400)).listen((text){
     widget.book.notes = text;
     BookDatabase.get().updateBook(widget.book);
   });
 }

_BookNotesPageState 中,用户输入了内容之后,需要保存这些注释的笔记内容。那么有几种场景来触发它,可能有一个按钮来触发保存,也可以在离开页面的时候自动保存。

我这里使用了与我上一篇文章中相同的技术,一个 RxDart,在用户停止输入后 400 毫秒后,将信息存储到本地数据库中。

过度动画

class FadeRoute<T> extends MaterialPageRoute<T> {
 FadeRoute({ WidgetBuilder builder, RouteSettings settings })
     : super(builder: builder, settings: settings);
 @override
 Widget buildTransitions(BuildContext context,
     Animation<double> animation,
     Animation<double> secondaryAnimation,
     Widget child) {
   if (settings.isInitialRoute)
     return child;
   // Fades between routes. (If you don't want any animation,
   // just return child.)
   return new FadeTransition(opacity: animation, child: child);
 }
}

我做了一个自己的页面路由,在页面之间,实现淡入淡出的动画。

      onTap: (){
       Navigator.of(context).push(
           new FadeRoute(
             builder: (BuildContext context) => new BookNotesPage(bookState),
             settings: new RouteSettings(name: '/notes', isInitialRoute: false),
           ));
     },

当用户点击一张书籍卡片的时候会被调用。使用的是 FadeTransition 动画。BookNotesPage 是用来显示当前是书籍的页面。

 new Hero(
  child: new Image.network(bookState.url),
  tag: bookState.id,
):

最后,在每个页面上,Image 都被包装在一个 Hero 里面,它自己来负责元素动画。

小结

就是这样,现在该应用程序还可以支持收藏书籍,甚至我们可以写下自己的读书笔记。

如果你喜欢本文,记得点赞让我知道你喜欢它,有任何问题可以在留言区留言。

今天在公众号后台回复成长『成长』,将会得到我整理的一些学习资料,也能回复『加群』,一起学习进步。

推荐阅读:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值