移动大作业开发指南

移动大作业开发指南

项目一

由于DDL实在有点紧张,大家可以只实现网络请求或者只实现本地存储,不用二者兼得

使用Dio发送网络请求

首先需要新建一个Dio类的实例,在新建时我们可以指定各种参数,例如baseUrl和连接超时时间等:

1
2
3
final Dio _dio = Dio(
BaseOptions(baseUrl: "http://47.113.194.64:7000")
);

指定了baseUrl过后,每一次发请求时,Dio都会自动为你在前面加上baseUrl,不用每次都复制粘贴了,例如:

1
2
_dio.post("/auth/login", data: {"username": "baka", "password": "123456"});
// 如果不指定baseUrl你就只能这样写: _dio.post("http://47.113.194.64:7000/auth/login", data: {"username": "baka", "password": "123456"});

使用shared_preferences(以下简称sp)本地存储

由于sp能存储的数据类型有限,我们如果想把待办项信息存入本地该怎么办呢?答案是使用JSON,JSON是一种描述信息的字符串,它长得有点像Dart里面的Map:

1
2
3
4
5
6
7
8
9
10
11
// Map
final Map<String, dynamic> map = {
"title": "完成文档",
"ddl": "2024-10-26",
};

// 用JSON描述
final String jsonStr = '{
"title": "完成文档",
"ddl": "2024-10-26"
}';

注意我们在Map中的ddl后面写了尾随逗号,但JSON的格式是不允许尾随逗号的。

你可以看到JSON其实就是一个有着特定格式的字符串,并且其内容也是按照键值对的方式来存储数据的,JSON其实也是有限制的,它的键值对的值只能是以下几种类型:数字(整数和小数)、字符串、布尔值

在Dart中,有一个办法能将Map直接转化为JSON,那就是使用Dart自带的json对象,使用方法:

1
2
3
4
5
6
7
8
9
import 'dart:convert'; // 一定要import这个包!json对象是来自这个包里面的

final Map<String, dynamic> example = {
"name": "Tom",
"score": 100
};

String jsonStr = json.encode(example); // encode方法即可将Map转为JSON
Map<String, dynamic> map = json.decode(jsonStr); // decode方法即可将JSON恢复为Map!

就这样,你实现了JSON和Map的互相转换,接下来你就可以把你的待办项信息放在一个Map中,然后通过json.encode()转化为字符串了,为了方便维护代码,你完全可以把这几个互转的方法写在TodoItem这个类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import 'dart:convert';

import 'package:todo_list/utils/date_util.dart';

class ToDoItem{
ToDoItem({
required this.id,
required this.title,
required this.finished,
this.ddl
});

int id;
String title;
DateTime? ddl;
bool finished;

// 从Map构建TodoItem对象
ToDoItem.fromMap(Map map):this(
id: map["itemId"],
title: map["title"],
finished: map['finished'] is bool ? map["finished"] : (map["finished"] == 0 ? false : true),
ddl: map["ddl"] == null ? null :DateUtil.parse(map["ddl"])
);
// 从JSON字符串构建TodoItem对象
ToDoItem.fromString(String jsonStr): this.fromMap(json.decode(jsonStr));

// 将TodoItem转化为Map
Map<String, dynamic> toMap(){
return {
"itemId": id,
"title": title,
"finished": finished,
"ddl": ddl == null ? null : DateUtil.format(ddl!)
};
}

// 将TodoItem转化为JSON字符串
// 此处的@override可以不用加
@override
String toString(){
return json.encode(toMap());
}

}

封装sp工具类

为了方便你的编程,你完全可以把与sp相关的操作封装在一个类中,当需要对本地数据进行操作和读取的时候直接调用这个类就可以了,下面这个例子供你参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:todo_list/model/todo_item.dart';

class SPUtil{
static SharedPreferences? _prefs;
static Future<void> init() async {
// 为了防止初始化方法被多次调用,当检测到已有SP对象时就直接return
if(_prefs != null){
return;
}
// 在使用SharedPreferences.getInstance()方法之前一定要先调用这个
WidgetsFlutterBinding.ensureInitialized();
_prefs = await SharedPreferences.getInstance();
}

static SharedPreferences get prefs => _prefs!;

// 根据传进来的参数同步本地待办项
static void syncLocal(List<ToDoItem> newItems){
List<String> target = [];
for(ToDoItem i in newItems){
target.add(i.toString());
}
prefs.setStringList("todo_list", target);
}

static bool saveTodoItem(ToDoItem item){
List<String> current = prefs.getStringList("todo_list") ?? [];
List<ToDoItem> itemList = [];
// 解析成ToDoItem对象方便我们读取和修改其中的数据
for(String s in current){
itemList.add(ToDoItem.fromString(s));
}
bool isDuplicated = itemList.any((element) => element.title == item.title);
if(isDuplicated){
return false;
}
current.add(item.toString());
prefs.setStringList("todo_list", current);
return true;
}

static void toggleItem(ToDoItem item, bool status){
List<String> current = prefs.getStringList("todo_list") ?? [];
List<ToDoItem> itemList = [];
// 解析成ToDoItem对象方便我们读取和修改其中的数据
for(String s in current){
itemList.add(ToDoItem.fromString(s));
}
// 根据title找到目标
int targetIndex = itemList.indexWhere((element) => element.title == item.title);
if(targetIndex == -1){
return;
}
// 修改完成状态后回写数据
item.finished = status;
itemList[targetIndex].finished = status;
current = [];
debugPrint(itemList.toString());
for(ToDoItem i in itemList){
current.add(i.toString());
}
debugPrint(current.toString());
prefs.setStringList('todo_list', current);
}

static bool isLocal(){
return prefs.getBool("use_local") ?? false;
}

static void useLocal(){
prefs.setBool("use_local", true);
}

}

然后你需要在main函数中初始化这个类,你的main函数需要改写成一个async函数:

1
2
3
4
5
Future<void> main() async {
// 一定要await!
await SPUtil.init();
runApp(const MainApp());
}

从上面的代码中你也能看出来,使用sp来存储数据是极为不方便的,特别是当我们想对其中的某一个数据进行筛选和修改时,必须先把所有的数据都拿出来,改完后又将所有数据回写,你可以使用本地数据库而不是sp来解决代码冗余的问题。当然你也可以不将待办项以字符串列表的形式存入,你可以自己定义一个键的命名规则,然后单独存入每一个待办项,每次查找时根据你的命名规则找到指定的待办项,这样其他无关的待办项就不会被取出,可以提高效率,这个方法请自行实现,如果你不追求效率直接用上面的代码也可。

之所以不让大家使用本地数据库,是因为你还需要了解一些数据库的知识,这样只会让信息量更大,如果你有兴趣请参阅:使用SQlite本地数据库进行存储

项目二

如何定义类别

我们可以将不同的类别定义为不同的文件夹,将照片存储在文件夹中,文件夹的名字即为类别的名字,当按照类别拍照或者按照类别查看的时候直接操作相应的文件夹就可以了。

Flutter是一个跨平台框架,在不同的系统中,App储存数据的位置不同,所以你需要一个库来帮你处理这些事情,这就是path_provider库,安装方法:

1
2
dependencies:
path_provider: ^2.1.3

dependencies下加入path_provider即可

使用方法参见(包括了如何存入和读取):https://blog.csdn.net/idaretobe/article/details/132804197

请主要关注getApplicationDocumentsDirectory()方法的使用

下面有请ChatGPT为我们介绍如何将图片存入指定路径:

如果使用 image_picker 的相机拍照生成的照片,你可以按照相同的逻辑来处理。相机拍照后得到的照片会生成在临时目录中,你可以将其复制到应用的文档目录以便长期保存。

修改代码中的 ImageSource.gallery 为 ImageSource.camera 即可。以下是拍照并保存的完整代码示例:

1. 实现拍照并存储图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart'; *// 用于获取文件名*

class CameraExample extends StatefulWidget {
@override
_CameraExampleState createState() => _CameraExampleState();

}

class _CameraExampleState extends State<CameraExample> {
File? _imageFile;

// 拍照并保存到应用的目录
Future<void> _takePhotoAndSave() async {
final ImagePicker _picker = ImagePicker();
final XFile? photo = await _picker.pickImage(source: ImageSource.camera);

if (photo != null) {

// 获取应用的目录
Directory appDir = await getApplicationDocumentsDirectory();

// 获取文件名并拼接目标路径
String fileName = basename(photo.path);
String savedImagePath = '${appDir.path}/$fileName';

// 将照片复制到应用目录
File savedImage = await File(photo.path).copy(savedImagePath);
setState(() {
​ _imageFile = savedImage; *// 更新照片路径到状态*
});

print('照片已保存到: $savedImagePath');
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Camera Example')),
body: Center(
​ child: _imageFile != null
​ ? Image.file(_imageFile!) *// 显示拍摄的照片*
​ : Text('请拍照'),
),
floatingActionButton: FloatingActionButton(
​ onPressed: _takePhotoAndSave,
​ child: Icon(Icons.camera_alt),
),
);
}
}

解释:

​ 1._takePhotoAndSave 方法:

​ •调用 image_picker 中的 pickImage,使用 ImageSource.camera 选项打开相机。

​ •当拍照完成后,获取到照片的临时文件路径。

​ •使用 path_provider 获取应用的文档目录,并将照片文件复制到该目录下。

​ 2.照片的保存路径:拍照后,照片默认会保存到设备的临时目录中,文件可能会被系统清理。使getApplicationDocumentsDirectory()将文件复制到应用文档目录中,以便长期保存。

现在,当用户使用相机拍照后,照片会被保存到应用的文档目录中,确保不会被系统清除。