实验三 数据存储与读取
一、 实验目的
1.学习并掌握移动应用开发中三种常规数据存储形式。
2.学习并完成移动应用页面之间切换时数据的传递。
3.结合已学数据结构和不同形式数据存储与访问完成复杂高级应用功能。
4.理解在复杂移动应用开发过程中业务逻辑和界面展示层分离的意义。
二、 知识要点
1.文件存储
文件读写是操作系统基本的数据操作功能,由于 Android 和 iOS 在沙盒机制以及安全权限设定不同,其在文件存储空间和存储路径的设定不同。
2.轻量级数据存储
在移动开发过程中,开发人员会把常规的用户或者系统配置等轻量级的数据存储起来,用于随时加载到内存中或者变更配置。在安卓平台上系统提供了轻量级的存储类SharedPreferences,用以完成常用配置等信息的存储与访问;在 iOS 平台上系统提供NSUserDefaults 用来存储用户一些偏好设置等小型数据信息,系统会为用户 APP 对应 的执行沙盒环境下的目录(Libray/Preferences)下创建 plist 文件,以键值对的形式存储数据。
3.数据库存储
对于重复数据或者结构化数据(例如联系人信息),将数据存储到数据库是理想的选择。无论是 Android 系统还是 iOS 系统都引入了轻型的 SQLite 数据库,SQLite 设计目标就是嵌入式的,资源占用较低等特性;由于智能手机普及,SQLite 也已成为世界上应用最广泛的数据库引擎。
4.其它形式存储
网络存储
Android:ContentProvider(数据共享)、KeyStore(密钥库)
iOS:plist(属性列表)、NSKeyedArchiver(对象归档)、Keychain(钥匙 串)
5.Flutter 软件包
Flutter 支持使用由其他开发者贡献给 Flutter 和 Dart 生态系统的共享软件包。这使您可以快速构建应用程序,而无需从头开始开发所有应用程序。现有的软件包支持许多使用场景,例如,网络请求(http),自定义导航/路由处理(fluro), 集成设备 API(如 url_launcher&battery) 以及使用第三方平台 SDK(如 Firebase))。
三、 实验内容
1.课堂实验
a) 登录页面使用“轻量级数据存储”形式存储输入的账户和密码,下次打开时自动载入账户和密码且允许点击登录。
b) 文件页面使用本地磁盘的文件系统,载入非隐藏的文件夹和文件(安卓载入存储卡根目录,苹果载入Library目录);支持多级目录的前进和返回。
c) 搜索页面使用数据库存储历史记录,支持单条记录的删除以及清空所有历史记录,点击搜索历史再次根据关键词搜索文件。
2.课后练习
a) 根据关键词(忽略大小写)递归搜索当前路径下子目录的文件或文件夹,展示搜索结果条目数以及搜索结果。
b) 点击搜索结果的文件夹支持跳转子目录继续浏览,允许根据浏览子目录的深度逐级返回,但不允许返回搜索结果子目录的父级目录。
c) 递归子目录耗时较长,设计“正在搜索”页面。
d) 搜索结果为零时,设计提醒搜索结果为零页面。
四、实验结果
1)、课堂实验
1、代码
//将用户名和密码进行本地存储
void _saveUser() async{
final SharedPreferences prefs = await _prefs;
prefs.setString("username", _username);
prefs.setString("password", _password);
}
//获取本地存储中的用户名和密码
Future _getUser() async{
final SharedPreferences prefs = await _prefs;
String loc_username = prefs.get("username") ?? null;
String loc_password = prefs.get("password") ?? null;
return {"loc_username":loc_username,"loc_password":loc_password};
}
根据路径将内存文件转换成DiskFile列表
@override
Future<List<DiskFile>> list(String dir,
{String order = 'name', int start = 0,int limit = 1000}) async{
var diskFiles = List<DiskFile>();
if (null == dir) return [];
Directory directory = Directory(dir);
int count = 0;
var _diskFilesComplete = Completer();
var listOfFiles = directory.list(followLinks: false);
listOfFiles.listen(((file) {
var fileName = PathUtils.basename(file.path);
if(file.path == null ||
(fileName.substring(0,1) == '.' &&
!AppConfig.instance.showAllFiles) ||
count++ < start) return ;
if(count > (start + limit)){
_diskFilesComplete.isCompleted ? null : _diskFilesComplete.complete('');
return;
}
diskFiles.add(SystemFile.fromSystem(file));
}),
onDone: () => _diskFilesComplete.isCompleted
? null
: _diskFilesComplete.complete(''),
onError:(e) => _diskFilesComplete.completeError(e),
);
await _diskFilesComplete.future;
FileStore.sortFiles(diskFiles,order);
return diskFiles;
}
返回父目录
Future<bool> _onBackParentDir() {
/// 从根目录返回,且允许pop时,执行pop
if(widget.rootPath.compareTo(_currPath) == 0 && widget.allowPop){
Navigator.pop(context);
return Future.value(false);
}
_currPath = PathUtils.dirname(_currPath);
_requestFiles();
return Future.value(false);
}
2、结果截图
登录页面使用“轻量级数据存储”形式存储输入的账户和密码,下次打开时自动载入账户和密码且允许点击登录。
文件页面使用本地磁盘的文件系统,载入非隐藏的文件夹和文件(安卓载入存储卡根目录,苹果载入Library目录);支持多级目录的前进和返回。
搜索页面使用数据库存储历史记录,支持单条记录的删除以及清空所有历史记录,点击搜索历史再次根据关键词搜索文件。
2)、课后练习
1、代码
数据库操作类(基于单例模式)
/// 基于单例模式
class DbHelper{
factory DbHelper() => _getInstance();
static DbHelper get instance => _getInstance();
static DbHelper _instance;
DbHelper._internal();
static DbHelper _getInstance(){
if (_instance == null){
_instance = new DbHelper._internal();
}
return _instance;
}
Database _db;
Future<Database> get db async {
if(_db == null) _db = await _initDb();
return _db;
}
_initDb() async{
String databasesPath = await getDatabasesPath();
String path = PathUtils.join(databasesPath,Constant.dbName);
return await openDatabase(
path,
version: Constant.dbVersion,
onCreate: _onCreate,);
}
void _onCreate(Database db,int version) =>
db.execute('create table ${Constant.searchHistoryTable}'
'("id" INTEGER primary key autoincrement,'
' "keyword" text unique, "time" INTEGER)');
void close() async {
if (_db != null) await _db.close();
}
Future<SearchHistory> insert(SearchHistory searchHistory) async{
var __db = await db;
try{
searchHistory.id = await __db.insert(Constant.searchHistoryTable, searchHistory.toMap());
}catch(e){
print(e);
}
return searchHistory;
}
Future<SearchHistory> query(int id)async{
var __db = await db;
List<Map> maps = await __db.query(Constant.searchHistoryTable,
where: 'id = ?',whereArgs: [id]);
if (maps.length > 0){
return SearchHistory.fromMap(maps.first);
}
return null;
}
Future<List<SearchHistory>> search(String key) async{
var __db = await db;
List<Map> maps = await __db.query(Constant.searchHistoryTable,
where: 'keyword like %?%',whereArgs: [key]);
var list = List<SearchHistory>();
maps.forEach((value) {
list.add(SearchHistory.fromMap(value));
});
return list;
}
Future<List<SearchHistory>> queryAll() async{
var __db = await db;
List<Map> maps = await __db.query(Constant.searchHistoryTable,);
var list = List<SearchHistory>();
maps.forEach((value) {
list.add(SearchHistory.fromMap(value));
});
return list;
}
Future<int> deleteAll() async{
var __db = await db;
return await __db.delete(Constant.searchHistoryTable);
}
Future<int> delete(int id) async{
var __db = await db;
return await __db.delete(Constant.searchHistoryTable,where: 'id = ?',whereArgs: [id]);
}
}
自定义的搜索历史组件
// 描述搜索组件事件
enum SearchHistoryEvent{delete,search}
//定义与父组件交互的回调函数
typedef OnSearchHistoryEventCallback = void Function(SearchHistoryEvent event,SearchHistory history);
//自定义历史记录组件
class SearchHistoryWidget extends StatefulWidget {
var searchKeyWord;
OnSearchHistoryEventCallback eventCallback;
//实现删除组件的功能,key是必要的
SearchHistoryWidget(Key key,{this.searchKeyWord,@required this.eventCallback}) : super(key:key);
@override
_SearchHistoryWidgetState createState() => _SearchHistoryWidgetState();
}
class _SearchHistoryWidgetState extends State<SearchHistoryWidget> {
_SearchHistoryWidgetState();
@override
Widget build(BuildContext context) {
return Container(
child: InkWell(
onTap: (){
widget.eventCallback(SearchHistoryEvent.search,SearchHistory(widget.searchKeyWord));
},
child: Row(
children: [
Container(
child: IconButton(
icon: Icon(Icons.restore),
onPressed: (){},
),
),
Expanded(
child: Text(widget.searchKeyWord),
),
Container(
child: IconButton(
icon: Icon(Icons.delete),
onPressed: (){
widget.eventCallback(SearchHistoryEvent.delete,SearchHistory(widget.searchKeyWord));
},
),
),
],
),
),
);
}
}
点击搜索结果事件
// 点击文件
void _onForwardDir(DiskFile file){
if(0 == file.isDir) return;// 是文件 不进行操作
print(file.path);
Navigator.push(context, MaterialPageRoute(
builder: (context)=>FilesPage(
rootPath: file.path,
allowPop: true, // 允许返回搜索界面
),
));
}
执行文件搜索
// 文件搜索
void _searchFile(value){
print("搜索文件"+value);
setState(() {
_searchState = SearchState.loading;
});
widget.fileStore.search(value).then((files){
setState(() {
_diskFiles = files;
_searchState = SearchState.done;
});
},onError: (e){
setState(() {
_searchState = SearchState.fail;
});
});
}
搜索文件
@override
Future<List<DiskFile>> search(String key,
{String dir = "/",int recursion = 0,int page = 1,int num = 1000}) async {
Directory directory = Directory(dir);
var diskFiles = List<DiskFile>();
int count = 0;
int start = (page - 1) * num;
var listOfFiles = directory.list(recursive: recursion == 1 ? true : false);
var _diskFilesComplete = Completer();
listOfFiles.listen(((file) {
var fileName = PathUtils.basename(file.path);
print(fileName);
if(file.path == null ||
(fileName.substring(0,1) == '.' &&
!AppConfig.instance.showAllFiles) ||
!fileName.toLowerCase().contains(key.toLowerCase()) ||
count++ < start) return ;
if(count > (start + num)){
_diskFilesComplete.isCompleted
? null
: _diskFilesComplete.complete('');
return;
}
diskFiles.add(SystemFile.fromSystem(file));
}),
onDone: () => _diskFilesComplete.isCompleted
? null
: _diskFilesComplete.complete(''),
onError:(e) => _diskFilesComplete.completeError(e),
);
await _diskFilesComplete.future;
return diskFiles;
}
文件排序
static void sortFiles(List<DiskFile> files,String order){
switch(order){
case "name":
print("根据名字排序");
files.sort((a,b){
return a.serverFilename.compareTo(b.serverFilename);
});
break;
case "time":
print("根据修改时间排序");
files.sort((a,b){
return a.serverCtime.compareTo(b.serverCtime);
});
break;
case "size":
print("根据文件大小排序");
files.sort((a,b){
return a.size.compareTo(b.size);
});
break;
}
}
2、结果截图
根据关键词(忽略大小写)递归搜索当前路径下子目录的文件或文件夹,展示搜索结果条目数以及搜索结果。
搜索 a
点击搜索结果的文件夹支持跳转子目录继续浏览,允许根据浏览子目录的深度逐级返回,但不允许返回搜索结果子目录的父级目录。
点击搜索结果中的sdcard
递归子目录耗时较长,设计“正在搜索”页面。
搜索结果为零时,设计提醒搜索结果为零页面。