对自己写的flutter APP的简单记录。

怕自己忘了之前看过的flutter教程,所以趁周末写了这个APP,功能很简单,涉及到的都是基本知识,写的时候稍微花了点时间学习数据库和文件读写。界面不怎么漂亮,过渡动画之类的还没有了解过,flutter的坑要很久才能填上。

演示视频地址

背景及大体设计

因为很久之前用《单词城堡》APP背单词的时候,里面有一项内容要求用户把乱序单词排序,因为没有任何提示,做起来很吃力,于是在海科展当志愿者的时候趁着空闲时间写了一个python版的查乱序单词的脚本。脚本的实现思路很简单,就是对用户输入的字母进行全排列,对每种可能的情况在json文件中查找对应的汉语(json文件是在Github上找到的)。

之后一段时间都把脚本下载到手机上,用termux执行进行查找,但是这样每次查一个单词都要打开termux然后输入命令,用户体验不是很好,慢慢就有了直接写一个APP的想法,这也是我学习flutter的原因之一。之后在寒假用了两周的时间把基本教程看了一遍,开学后发现有一堆事情等着我去做,暂时没时间对flutter进一步研究,于是趁着周六的空闲用了一天的时间把APP写了出来。

有三个界面,分别是联网查询界面、本地json查询界面、收藏展示界面。为了简单直接使用一个BottomNavigationBar处理,不需要很复杂的路由。另外本地查询的结果带有收藏选项,勾选后能在收藏展示界面查看。

过程概述

首先创建项目,我使用的编辑器是VSCode,安装相关插件之后对flutter的支持很友好,因为电脑性能有限便用自己的手机进行相关调试。在VSCode中创建项目很简单,按下Ctrl+Shift+P打开命令面板,输入flutter之后选择Flutter: New Project,输入项目名后按下回车,之后选择项目位置,项目便会自动创建。

首先写出来具体的程序框架,实现三个空白的页面,之后再对每个页面进行补充。

联网查询

联网查询界面本来计划调用百度的API,但是发现dart有已经封装好的第三方库,任务得到了简化,。示例中有多种语言的相互翻译,我只简单实现了中译英,代码很简单:

String _inputWords, _res;   // 输入单词 翻译结果
  
// 异步获取单词翻译结果
void _getRes() async{
  final translator = GoogleTranslator();
  translator.baseUrl = "https://translate.google.cntranslate_a/single";
  var res = await translator.translate(_inputWords, to:'zh-cn');
  setState(() {
    _res = res;
  });
}

TextField获取用户输入,之后用一个按钮触发上面的异步过程就能获得翻译结果并对相应部件进行重绘。

本地查询

因为我已经有一个现成的json文件,只要把它读入再对用户的输入进行查询就能实现这个功能,用户输入、字典查询都很简单,对于我来说比较难的点是如何读取json资源。

首先要明白一点,打包到程序安装包中的json是assets,需要在项目当中对其进行指定,之后项目构建过程中会将其放到asset bundle存档当中,应用程序可以在运行时进行读取。

这要与用户设备中保存的文件进行区分,我在实现的时候搜索了很久flutter如何读取本地文件,用path_provider插件实现之后却发现自己根本在项目里面找不到对应的目录放json文件,白花了很多时间。对于每个设备当中的文件,一定要先进行创建之后才能读取。

接下来简单介绍一下我添加资源并进行读取的过程。

将资源放在项目当中的某个文件夹内(通常是assets文件夹),之后在pubspec.yaml文件当中添加文件及路径,例如:

flutter:
  assets:
    - assets/data.json

之后在程序当中使用DefalutAssetBundle.of()获取资源,我读入json的代码:

Future<Map> _loadFile() async{
    String tmp = await DefaultAssetBundle.of(context).loadString('assets/data.json');
    return json.decode(tmp);
}

重写initState()函数,在里面调用上面的异步过程便能将json文件加载到内存。几个相关链接:

  1. Flutter中文网:在Flutter中添加资源和图片
  2. Flutter中文网:文件读写
  3. Flutter官方:文件读写

最后简单提一下全排列的实现方式:将字母与包括自身在内的所有剩余字母交换顺序,然后递归调用,到达字符串末尾输出。

void helper(String str, int pos){
    if(pos == str.length){
        print(str);
        return ;
    }
    else{
        for(int i = pos, i < str.length; i++){
            if(str[pos] == str[i] && pos != i)
                continue;   // 与非自身的相同字母交换无意义
            var tmp = str[i];
            str[i] = str[pos];
            str[pos] = tmp;
            helper(str, pos+1, end);
            tmp = str[i];
            str[i] = str[pos];
            str[pos] = tmp;
        }
    }
}

收藏展示(SQLite数据库)

这个界面的功能是对已经收藏过的数据进行展示,数据的保存方式可以是json或者使用数据库,因为搜索界面已经实现过文件读写了(虽然最后没用上),所以最终我使用了数据库保存数据。介绍之前先放两个链接:

  1. Flutter官方:用SQLite保存数据
  2. 简书sirai:Flutter数据库Sqflite

这里强推官方的教程,只要有一定的数据库基础,按照官方教程一步步来会很清楚的了解具体的使用方法。官方教程的唯一遗憾是没有给出具体的调用方式,像我一样的初学者可能没办法直接想到合适的解决方法。在实现过程中我找到了上面简书的文章,里面讲到了通过封装一个SqlManager类来实现部件中相关功能的调用。下面是我的具体实现,数据库很简单,只有一个表,表中只有单词、翻译两列内容。下面是我的实现代码:

import 'package:sqflite/sqflite.dart';
import 'dart:async';
import 'package:path/path.dart';

class MyWord {
  final String word;
  final String translation;

  MyWord({this.word, this.translation});

  Map<String, dynamic> toMap() {
    return {
      'word': word,
      'translation': translation,
    };
  }
}

class SqlManager {
  static Database database;

  SqlManager(){
    print('初始化');
    init();
  }
  void init() async {
    database = await openDatabase(
      join(await getDatabasesPath(), "english_words.db"),
      onCreate: (db, version) {
        return db.execute(
            "CREATE TABLE words(word TEXT PRIMARY KEY, translation TEXT)");
      },
      version: 1,
    );
  }
  
  Future<Database> getDatabase() async{
    if(database == null){
      await init();
    }
    return database;
  }

  // 插入单词
  Future<void> insertWord(MyWord word) async {
    print('插入');
    final Database db = await getDatabase();
    await db.insert('words', word.toMap(),
        conflictAlgorithm: ConflictAlgorithm.replace);
  }

  // 获取数据库内容
  Future<List<MyWord>> words() async {
    final Database db = await getDatabase();
    final List<Map<String, dynamic>> maps = await db.query('words');
    return List.generate(maps.length, (index) {
      return MyWord(
          word: maps[index]['word'],
          translation: maps[index]['translation']);
    });
  }

  // 检索是否已经收藏
  Future<bool> haveStore(String word) async{
    final db = await getDatabase();
    final List<Map<String, dynamic>> maps = await db.query("words",where: "word=?", whereArgs: [word]);
    if(maps == null || maps.length == 0)
      return false;
    else
      return true;
  }

  // 更新记录
  Future<void> updateWord(MyWord record) async {
    final db = await getDatabase();
    await db
        .update('words', record.toMap(), where: "word = ?", whereArgs: [record.word]);
  }

  // 删除单词
  Future<void> deleteWordBys(String word) async{
    final db = await getDatabase();
    int res = await db.delete('words', where: "word=?",whereArgs: [word]);
    print(res);
  }
  
  // 删除表
  Future<void> deleteWords()async{
    final db = await getDatabase();
    int res =await db.delete('words');
    print('删除$res');
  }
}

简单概括一下,想要使用sqlite数据库,需要sqflite、async、path三个库。需要对每个表的数据类进行封装,至少写出构造函数和字典转化函数(插入数据的参数格式为字典),之后封装一个SqlManager类,把自己想要实现的功能都准备好,在部件中创建该类的示例便能进行相关调用。

这里对于数据库的初始化稍微有些不明白,在我的实现当中,有两个部件内部定义了数据库管理类的示例,但是他们能够使用相同的数据库,没有任何冲突。这可能和创建数据库时onCreate参数或者sqflite的内部实现有关系。

简单总结

在学习flutter的过程中,我首次接触到了异步的概念,当时只是做了简单的了解,但是在实现这个简单的APP的过程中,我的算法没有花费多少时间,大部分时间都在考虑异步的过程该怎么实现。感觉对Future的理解还远远不够,之后如果还有机会需要加强这方面的理解。

总之在这个领域只是个初学者,这个简单的APP也只是对前面所学知识的简单总结,还有很多深入工作要做。