📮 [Flutter] 从设计到构建到发布:APP应用开发经验总结
前言
对《Flutter实战·第二版》经过一周左右的学习,决定入手开发小型项目,以对书中的知识进行巩固。刚好女朋友想要一个属于我们的记录代办、家务、菜谱的应用,于是就有了这个项目。
模型定义
应用模块分为四块(代办,家务,菜谱,收入)。考虑的研发成本,目前仅采用客户端进行数据的存储和获取,采用安卓轻量级存储类 SharedPreferences进行数据存储。

代码实现
模型
在 root_dir\jsons 新建储存数据。

模型定义
todo.json
{
"id": "id",
"name": "测试",
"description?": "描述",
"taskId?": "家务id",
"task?": "$houseWork",
"startTime?": "2011-01-26T19:06:43Z",
"endTime?": "2011-01-26T19:06:43Z",
"finishTime?": "2011-01-26T19:06:43Z",
"finished": false,
"deleted": false
}ecipe.json
{
"id": "id",
"name": "name",
"description?": "描述",
"banner?": "banner",
"steps": "做法",
"deleted": false
}income.json
{
"total": 0
}houseWork.json
{
"id": "id",
"name": "name",
"banner?": "封面",
"description?": "描述",
"worth": 0,
"deleted": false
}生成模型
在 pubspec.yaml 中添加json_serializable: ^1.0.0 json_model: ^6.7.1 依赖。
执行 flutter packages pub run json_model 将jsons 目录下的json 转化为 JSON Model 输出在 lib/model 目录。

用于Model 和 JSON之间相互转换。
全局变量
用于数据存储和初始化 。lib/Global/global_data.dart 。
class GlobalData {
static UserData userData = UserData()
..houseWorks = []
..incomes = Income()
..todos = []
..recipes = [];
static late SharedPreferences sharedPreferences;
// 初始化
static Future init() async {
WidgetsFlutterBinding.ensureInitialized();
sharedPreferences = await SharedPreferences.getInstance();
var houseWorksData = sharedPreferences.getString("houseWorks");
var todoData = sharedPreferences.getString("todos");
var recipeData = sharedPreferences.getString("recipes");
var incomeData = sharedPreferences.getString("incomes");
try {
if (houseWorksData != null) {
userData.houseWorks = (jsonDecode(houseWorksData) as List<dynamic>)
.map((element) => HouseWork.fromJson(element))
.toList();
}
if (todoData != null) {
userData.todos = (jsonDecode(todoData) as List<dynamic>)
.map((element) => Todo.fromJson(element))
.toList();
}
if (incomeData != null) {
userData.incomes = Income.fromJson(jsonDecode(incomeData));
}
if (recipeData != null) {
userData.recipes = (jsonDecode(recipeData) as List<dynamic>)
.map((element) => Recipe.fromJson(element))
.toList();
}
} catch (e) {
print(e);
}
}
// 保存数据
static saveUserData() {
sharedPreferences.setString("houseWorks", jsonEncode(userData.houseWorks));
sharedPreferences.setString("todos", jsonEncode(userData.todos));
sharedPreferences.setString("recipes", jsonEncode(userData.recipes));
sharedPreferences.setString(
"incomes", jsonEncode(userData.incomes.toJson()));
}
}共享状态
pubspec.yml 添加 provider: ^6.1.2 依赖(用于共享状态监听,当模块对应数据变更时,通知UI进行更新)。
global_data_notifier.dart
class GloabalDataNotifier extends ChangeNotifier {
UserData get userData {
return GlobalData.userData;
}
void notifyListeners() {
GlobalData.saveUserData();
super.notifyListeners();
}
}todo_data.dart
class TodoActions {
static String add = 'add';
static String delete = 'delete';
static String update = 'update';
static String finish = 'finish';
}
class TodoData extends GloabalDataNotifier {
late int length;
late int finishedLength;
late int total;
TodoData() {
length = todos.length ?? 0;
finishedLength = finishedList.length ?? 0;
total = length + finishedLength;
}
static Todo generateData(Todo? houseWork) {
if (houseWork != null) {
return houseWork;
} else {
return Todo()
..id = const Uuid().v8()
..finished = false
..deleted = false;
}
}
List<Todo> get todos {
return userData.todos.where((todo) {
return !todo.deleted && !todo.finished;
}).toList();
}
List<Todo> get finishedList {
return userData.todos.where((todo) {
return !todo.deleted && todo.finished;
}).toList();
}
double get progress {
return total == 0 ? .0 : finishedLength / total;
}
void updateLength(String action, [bool finished = false]) {
if (action == TodoActions.add) {
length++;
total++;
} else if (action == TodoActions.finish) {
length--;
finishedLength++;
} else if (action == TodoActions.delete) {
finished ? finishedLength-- : length--;
total--;
}
}
void add(Todo todo) {
updateLength(TodoActions.add);
userData.todos.add(todo);
notifyListeners();
}
void delete(Todo todo) {
final index = findIndex(todo);
if (index != -1) {
updateLength(TodoActions.delete, userData.todos[index].finished);
userData.todos[index].deleted = true;
notifyListeners();
}
}
void update(Todo todo) {
final index = findIndex(todo);
if (index != -1) {
userData.todos[index] = todo;
notifyListeners();
}
}
void finish(Todo todo) {
final index = findIndex(todo);
if (index != -1) {
updateLength(TodoActions.finish);
userData.todos[index]
..finished = true
..finishTime = DateFormat("yyyy-MM-dd HH:mm").format(DateTime.now());
notifyListeners();
}
}
int findIndex(Todo todo) {
return userData.todos.indexWhere((element) {
return element.id == todo.id;
});
}
}house_work.dart
class HouseWorkData extends GloabalDataNotifier {
List<HouseWork> get houseWorks {
return userData.houseWorks
.where((houseWork) => houseWork.deleted == false)
.toList();
}
get length {
return userData.houseWorks.length ?? 0;
}
static String getPrice(HouseWork houseWork) {
return "¥${houseWork.worth}";
}
static HouseWork generateData(HouseWork? houseWork) {
if (houseWork != null) {
return houseWork;
} else {
return HouseWork()
..id = const Uuid().v8()
..deleted = false;
}
}
add(HouseWork houseWork) {
userData.houseWorks.add(houseWork);
notifyListeners();
}
update(HouseWork houseWork) {
final index = findIndex(houseWork);
if (index != -1) {
userData.houseWorks[index] = houseWork;
notifyListeners();
}
}
delete(HouseWork houseWork) {
final index = findIndex(houseWork);
if (index != -1) {
userData.houseWorks[index].deleted = true;
notifyListeners();
}
}
int findIndex(HouseWork houseWork) {
return userData.houseWorks.indexWhere((element) {
return element.id == houseWork.id;
});
}
HouseWork find(String id) {
return userData.houseWorks.firstWhere((element) {
return element.id == id;
});
}
}recipe_data.dart
class RecipeData extends GloabalDataNotifier {
static Recipe generateData(Recipe? houseWork) {
if (houseWork != null) {
return houseWork;
} else {
return Recipe()
..id = const Uuid().v8()
..deleted = false;
}
}
List<Recipe> get recipes {
return userData.recipes.where((recipe) => recipe.deleted == false).toList();
}
set recipes(List<Recipe> recipe) {
userData.recipes = recipe;
notifyListeners();
}
get length {
return recipes.length ?? 0;
}
void add(Recipe recipe) {
userData.recipes.add(recipe);
notifyListeners();
}
void delete(Recipe recipe) {
final index = findIndex(recipe);
if (index != -1) {
userData.recipes[index].deleted = true;
}
notifyListeners();
}
void update(Recipe recipe) {
final index = findIndex(recipe);
if (index != -1) {
userData.recipes[index] = recipe;
notifyListeners();
}
}
int findIndex(Recipe recipe) {
return userData.recipes.indexWhere((element) {
return element.id == recipe.id;
});
}
}income.dart
class IncomeData extends GloabalDataNotifier {
Income get incomeData {
return userData.incomes;
}
set incomeData(Income income) {
userData.incomes = income;
notifyListeners();
}
get total {
return userData.incomes.total ?? 0;
}
void updateIncome(Todo todo) {
if (todo.task != null) {
incomeData.total += todo.task!.worth;
}
notifyListeners();
}
}根节点
main.dart
void main() {
// 等待SharedPreferencesc初始化
GlobalData.init().then((e) => runApp(const MyApp()));
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget build(BuildContext context) {
return MultiProvider( // 创建provider
providers: [
ChangeNotifierProvider(
create: (context) => HouseWorkData(),
),
ChangeNotifierProvider(
create: (context) => IncomeData(),
),
ChangeNotifierProvider(
create: (context) => RecipeData(),
),
ChangeNotifierProvider(
create: (context) => TodoData(),
),
],
child: MaterialApp(
theme: ThemeData(
useMaterial3: true,
),
locale: const Locale('zh', 'CN'),
home: const HomePage(),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate
],
routes: { // 创建provider创建路由
MenuConfig.houseWork.path: (context) => const HouseWorkPage(),
MenuConfig.recipe.path: (context) => const RecipePage(),
MenuConfig.income.path: (context) => const IncomePage()
},
));
}
}维护路由
menu_config.dart
class MenuItem {
MenuItem(
{required this.label,
this.icon,
required this.path,
this.build,
required this.name});
String label;
String path;
String name;
IconData? icon;
Widget Function()? build;
}
class MenuConfig {
static MenuItem todo = MenuItem(
name: 'todo',
label: '代办',
path: "/",
icon: IconFont.icon_1_todo,
build: () => const TodoPage());
static MenuItem recipe = MenuItem(
name: 'recipe',
label: '菜谱',
path: "recipe",
icon: IconFont.icon_repast_recipe,
build: () => const RecipePage());
static MenuItem houseWork = MenuItem(
name: 'housework',
label: '家务',
path: "housework",
icon: Icons.work_outline,
build: () => const HouseWorkPage());
static MenuItem income = MenuItem(
name: "income",
label: '收入',
path: "income",
icon: IconFont.icon_1_income_2,
build: () => const IncomePage());
static List<MenuItem> menus = [
MenuConfig.todo,
MenuConfig.recipe,
MenuConfig.houseWork,
MenuConfig.income
];
static renderMenu() {
return menus.map((menu) => menu.build!()).toList();
}
}引入Iconfont
下载iconfont.ttf

维护 assets/fonts/iconfont.ttf

pubspec.yaml 引入iconfont资源
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
fonts:
- family: IconFont
fonts:
- asset: assets/fonts/iconfont.ttf维护Iconfont 类,icon_font.dart 。IconData中的 iconCode 可以在 demo_index.html 中查看

class IconFont {
static const String _family = 'IconFont';
static const IconData icon_kongshuju = IconData(0xe63a, fontFamily: _family);
static const IconData icon_income_one = IconData(0xe6b0, fontFamily: _family);
static const IconData icon_1_todo = IconData(0xe600, fontFamily: _family);
static const IconData icon_1_income_2 = IconData(0xe601, fontFamily: _family);
static const IconData icon_income = IconData(0xe906, fontFamily: _family);
static const IconData icon_repast_recipe =
IconData(0xe649, fontFamily: _family);
}之后通过IconFont.${icon} 调用Iconfont图标。
通用组件
empty.dart 。空数据展示。

class EmptyWiget extends StatelessWidget {
const EmptyWiget({super.key});
Widget build(BuildContext context) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
IconFont.icon_kongshuju,
size: 60.0,
),
SizedBox(
height: 20,
),
Text("暂无数据")
],
),
);
}
}hw_card.dart
class HwCardWiget extends StatefulWidget {
const HwCardWiget(
{super.key,
required this.title,
this.description = "",
this.detail = "",
this.disable = false,
this.actions,
this.onDelete,
this.onEdit,
this.onFinished,
this.finished = false});
final String title;
final String description;
final String detail;
// 下拉按钮选项 支持删除 编辑 完成
final List<HwCardActionItem>? actions;
// 删除回调
final Function? onDelete;
// 编辑回调
final Function? onEdit;
// 完成回调
final Function? onFinished;
final bool disable;
final bool finished;
State<HwCardWiget> createState() => _HwCardWigetState();
}
class _HwCardWigetState extends State<HwCardWiget> {
// controler
ExpansionTileController expansionTileController = ExpansionTileController();
late TextStyle textStyle;
void initState() {
super.initState();
textStyle = TextStyle(
decoration:
widget.finished ? TextDecoration.lineThrough : TextDecoration.none // 如果是完成状态 则生成删除线
);
}
// 控制展开收起
void toggleExpand() {
if (widget.disable) return;
if (expansionTileController.isExpanded) {
expansionTileController.collapse();
} else {
expansionTileController.expand();
}
}
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
color: const Color.fromRGBO(254, 247, 255, 1),
child: InkWell( // 点击产生水波纹效果
onTap: toggleExpand,
child: Theme(
data: ThemeData().copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
controller: expansionTileController,
enabled: false,
expandedAlignment: Alignment.centerLeft,
childrenPadding:
const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 16.0),
leading: ClipOval(
child: Container(
width: 30,
height: 30,
alignment: Alignment.center,
color: const Color.fromRGBO(103, 80, 164, 1),
child: Text(
widget.title[0],
style: const TextStyle(color: Colors.white),
),
)),
trailing: PopupMenuButton(
// 下拉按钮
enabled: widget.actions?.isNotEmpty ?? false,
itemBuilder: (context) => widget.actions != null
? widget.actions!
.map(
(action) => PopupMenuItem(
value: action.key,
child: Text(action.label),
),
)
.toList()
: [],
onSelected: (value) {
if (value == HwCardActions.edit.key) {
if (widget.onEdit != null) {
widget.onEdit!();
}
} else if (value == HwCardActions.delete.key) {
if (widget.onDelete != null) {
widget.onDelete!();
}
} else if (value == HwCardActions.compelete.key) {
if (widget.onFinished != null) {
widget.onFinished!();
}
}
},
),
title: Text(widget.title,
style: textStyle.copyWith(
color: const Color.fromARGB(255, 1, 1, 1),
fontSize: 16)),
subtitle: widget.description.isNotEmpty
? Text(
widget.description,
style: textStyle.copyWith(
color: const Color.fromARGB(200, 73, 69, 79)),
)
: null,
children: [Text(widget.detail, style: textStyle)],
))));
}
}
message.dart 全局生成SnackBar 进行消息提示。
import 'package:flutter/material.dart';
SnackBar createMessage(String content) {
return SnackBar(content: Text(content));
}界面
home_page.dart 首页,配置侧边栏导航,渲染界面。
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final List<Widget> _menus = MenuConfig.renderMenu();
int _selectedIndex = 0;
Widget build(BuildContext context) {
final HouseWorkData houseWorkData = Provider.of<HouseWorkData>(context);
final navigationData = {
MenuConfig.todo.name: Provider.of<TodoData>(context).length,
MenuConfig.recipe.name: Provider.of<RecipeData>(context).length,
MenuConfig.houseWork.name: Provider.of<HouseWorkData>(context).length,
MenuConfig.income.name: Provider.of<IncomeData>(context).total,
};
String renderCount(MenuItem item) {
return "${item.name == MenuConfig.income.name ? "¥" : " "}${navigationData[item.name].toString()}";
}
return Scaffold(
appBar: AppBar(
title: Text(MenuConfig.menus[_selectedIndex].label),
),
drawer: NavigationDrawer(
selectedIndex: _selectedIndex,
children: [
Container(
padding: const EdgeInsets.fromLTRB(30, 40, 30, 40),
child: const Text("Menu"),
),
...MenuConfig.menus.map((menu) => NavigationDrawerDestination(
icon: Icon(menu.icon),
label: Expanded(
child: Container(
padding: const EdgeInsets.only(right: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [Text(menu.label), Text(renderCount(menu))],
),
),
)))
],
onDestinationSelected: (index) {
Navigator.of(context).pop();
setState(() {
_selectedIndex = index;
});
},
),
body: Container(
padding: EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 16.0),
child: _menus[_selectedIndex],
),
);
}
} todo_page.dart
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
State<TodoPage> createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late double progress = .0;
void initState() {
_animationController = AnimationController(
vsync: this, //注意State类需要混入SingleTickerProviderStateMixin(提供动画帧计时/触发器)
duration: Duration(microseconds: 1000),
);
_animationController.addListener(() => setState(() => {}));
super.initState();
}
void dispose() {
super.dispose();
_animationController.dispose();
}
void startAnimate(TodoData todo) {
// build时判断是否需要更新进度条
if (progress != todo.progress) {
progress = todo.progress;
_animationController.animateTo(todo.progress);
}
}
Widget build(BuildContext context) {
TodoData todoModel = Provider.of<TodoData>(context);
startAnimate(todoModel);
return Scaffold(
body: todoModel.todos.isNotEmpty || todoModel.finishedList.isNotEmpty
? ListView(children: [
Container(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 20.0),
child: LinearProgressIndicator(
value: _animationController.value,
borderRadius: BorderRadius.all(Radius.circular(100.0)),
),
),
...renderTodos(),
Container(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: const Divider(height: 1.0)),
...renderFnished()
])
: const EmptyWiget(),
floatingActionButton: FloatingActionButton(
onPressed: () {
openTodoFromDialog(context);
},
child: const Icon(
Icons.add,
),
),
);
}
renderTodos() {
TodoData todoModel = Provider.of<TodoData>(context);
IncomeData incomeData = Provider.of<IncomeData>(context);
return todoModel.todos.map((todo) => HwCardWiget(
title: todo.name,
description: renderRemainDate(todo.endTime),
detail: renderDetail(todo),
actions: [
HwCardActions.compelete,
HwCardActions.edit,
HwCardActions.delete,
],
onDelete: () {
todoModel.delete(todo);
},
onEdit: () {
openTodoFromDialog(context, todo, true);
},
onFinished: () {
todoModel.finish(todo);
incomeData.updateIncome(todo);
},
));
}
String renderRemainDate(String? endTime) {
if (endTime == null) return "";
var end = DateTime.parse(endTime);
var now = DateTime.now();
var remain = end.difference(now);
var remainDay = remain.inDays.abs();
var remainHours = remain.inHours.abs() % 24;
return "${remain.isNegative ? "超时" : "剩余"} ${remainDay == 0 ? "" : "${remainDay}天 "}${remainDay == 0 && remainHours == 0 ? "" : "${remainHours}分钟 "}${remain.inMinutes % 60} 分钟";
}
String renderTime(String? startTime, String? endTime) {
return "$startTime - $endTime";
}
String renderDetail(Todo todo) {
var result = "";
if (todo.description != null && todo.description != "") {
result += "描述:\n${todo.description}\n";
}
if (todo.task != null) {
result += "家务:\n${todo.task!.name}\n价值:\n¥${todo.task!.worth}\n";
}
result += "时间:\n${renderTime(todo.startTime, todo.endTime)}\n";
return result;
}
renderFnished() {
TodoData todoModel = Provider.of<TodoData>(context);
return todoModel.finishedList.map((todo) => HwCardWiget(
title: todo.name,
description: "完成时间: ${todo.finishTime}",
detail: renderDetail(todo),
finished: true));
}
}recipe_page.dart
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
Widget build(BuildContext context) {
final houseModel = Provider.of<RecipeData>(context);
return Scaffold(
body: houseModel.recipes.isEmpty
? const EmptyWiget()
: ListView(
children: houseModel.recipes
.map((recipe) => HwCardWiget(
title: recipe.name,
detail: "做法:\n${recipe.steps}",
actions: [
HwCardActions.edit,
HwCardActions.delete,
],
onDelete: () {
Provider.of<RecipeData>(context, listen: false)
.delete(recipe);
},
onEdit: () {
openRecipeFromDialog(context, recipe, true);
}))
.toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
openRecipeFromDialog(context);
},
child: const Icon(
Icons.add,
),
),
);
}
}
house_work.dart
class HouseWorkPage extends StatelessWidget {
const HouseWorkPage({super.key});
Widget build(BuildContext context) {
// 订阅 HouseWorkData HouseWorkData 数据变更 重新build Widget
final houseModel = Provider.of<HouseWorkData>(context);
return Scaffold(
body: houseModel.houseWorks.isEmpty
? EmptyWiget()
: ListView(
children: houseModel.houseWorks
.map((houseWork) => HwCardWiget(
title: houseWork.name,
detail: "价值 ${HouseWorkData.getPrice(houseWork)}",
actions: [
HwCardActions.edit,
HwCardActions.delete,
],
onDelete: () {
Provider.of<HouseWorkData>(context, listen: false)
.delete(houseWork);
},
onEdit: () {
openHouseWorkFromDialog(context, houseWork, true);
}))
.toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 打开新增弹窗
openHouseWorkFromDialog(context);
},
child: const Icon(
Icons.add,
),
),
);
}
}income_page.dart
class IncomePage extends StatelessWidget {
const IncomePage({super.key});
Widget build(BuildContext context) {
final incomeData = Provider.of<IncomeData>(context).incomeData;
return Scaffold(
body: Column(
children: [
CalendarDatePicker(
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now(),
onDateChanged: (value) {},
),
SizedBox(height: 20),
Icon(
IconFont.icon_income_one,
size: 40.0,
),
SizedBox(height: 10),
Text(
"总收入 ¥${incomeData.total}",
)
],
),
);
}
}表单
todo_form.dart
class TodoForm extends StatefulWidget {
const TodoForm({super.key, this.isEdit = false, this.data});
final bool isEdit;
final Todo? data;
State<TodoForm> createState() => _TodoFormState();
}
class _TodoFormState extends State<TodoForm> {
/* ---- 表单 ---- */
// 代办名
final TextEditingController _nameInputControl = TextEditingController();
// 任务
final TextEditingController _taskInputControl = TextEditingController();
// 开始时间
final TextEditingController _startTimeControl = TextEditingController();
// 开始时间
final TextEditingController _endTimeControl = TextEditingController();
// 开始时间
final TextEditingController _descriptionControl = TextEditingController();
final GlobalKey _formKey = GlobalKey<FormState>();
void initState() {
super.initState();
if (widget.isEdit && widget.data != null) {
_nameInputControl.value = TextEditingValue(text: widget.data!.name);
_taskInputControl.value =
TextEditingValue(text: widget.data?.taskId ?? '');
_startTimeControl.value =
TextEditingValue(text: widget.data?.startTime ?? '');
_endTimeControl.value =
TextEditingValue(text: widget.data?.endTime ?? '');
_descriptionControl.value =
TextEditingValue(text: widget.data?.description ?? '');
}
}
Widget build(BuildContext context) {
final List<HouseWork> houseWorks =
Provider.of<HouseWorkData>(context).houseWorks;
return Container(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
IconButton(onPressed: _closeDialog, icon: const Icon(Icons.close)),
Text(widget.isEdit ? "编辑代办" : "新增代办"),
TextButton(onPressed: _validateHandler, child: const Text("保存"))
]),
Container(
padding: const EdgeInsets.all(8.0),
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
controller: _nameInputControl,
decoration: const InputDecoration(
label: Text("名称"),
hintText: "请输入名称",
border: OutlineInputBorder(),
helperText: "(必填) 任务名称"),
validator: (value) =>
validatorUtilnotEmpty(value, "请输入名称"),
),
const SizedBox(
height: 20.0,
),
SelectFormField(
type: SelectFormFieldType.dropdown,
controller: _taskInputControl,
textInputAction: TextInputAction.none,
enabled: houseWorks.isEmpty != true,
decoration: InputDecoration(
label: const Text("家务"),
border: const OutlineInputBorder(),
helperText:
houseWorks.isEmpty ? "请先添加家务" : "(可选) 家务"),
items: houseWorks
.map<Map<String, dynamic>>((houseWork) => ({
'label': houseWork.name,
'value': houseWork.id
}))
.toList()),
const SizedBox(
height: 20.0,
),
TextFormField(
controller: _descriptionControl,
maxLines: 6,
decoration: const InputDecoration(
label: Text("描述"),
hintText: "请输入描述",
border: OutlineInputBorder(),
helperText: "(可选) 任务描述"),
),
const SizedBox(
height: 20.0,
),
TextFormField(
controller: _startTimeControl,
onTap: () {
_showTimePicker(context);
},
readOnly: true,
textInputAction: TextInputAction.none,
decoration: const InputDecoration(
label: Text("开始时间"),
hintText: "请选择开始时间",
border: OutlineInputBorder(),
helperText: "(必填) 开始时间"),
validator: (value) =>
validatorUtilnotEmpty(value, "请选择开始时间"),
),
const SizedBox(
height: 20.0,
),
TextFormField(
readOnly: true,
// readOnly: true,
controller: _endTimeControl,
textInputAction: TextInputAction.none,
enabled: _startTimeControl.text.isEmpty != true,
onTap: () {
_showTimePicker(context, true);
},
decoration: const InputDecoration(
label: Text("结束时间"),
hintText: "请选择结束时间",
border: OutlineInputBorder(),
helperText: "(必填) 结束时间"),
validator: (value) =>
validatorUtilnotEmpty(value, "请选择结束时间"),
),
],
)),
),
],
),
);
}
// 关闭弹窗
_closeDialog() {
Navigator.of(context).pop();
}
_validateHandler() {
if ((_formKey.currentState as FormState).validate()) {
Todo todo = TodoData.generateData(widget.data)
..name = _nameInputControl.text
..startTime = _startTimeControl.text
..taskId = _taskInputControl.text
..endTime = _endTimeControl.text
..description = _descriptionControl.text;
if (todo.taskId != null && todo.taskId != "") {
todo.task = Provider.of<HouseWorkData>(context, listen: false)
.find(todo.taskId!);
}
if (widget.isEdit) {
Provider.of<TodoData>(context, listen: false).update(todo);
} else {
Provider.of<TodoData>(context, listen: false).add(todo);
}
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("保存成功"), duration: Duration(milliseconds: 300)));
_closeDialog();
}
}
// 选择时间选项
_showTimePicker(BuildContext context, [bool isEnd = false]) async {
final firstDate = isEnd
? DateTime.parse(_startTimeControl.text)
: DateTime(DateTime.now().year - 10);
final lastDate = isEnd
? DateTime(firstDate.year + 10)
: DateTime(DateTime.now().year + 10);
// 打开日期选择弹窗
final date = await showDatePicker(
context: context, firstDate: firstDate, lastDate: lastDate);
if (date == null) return;
// 打开时间选择弹窗
final time =
await showTimePicker(context: context, initialTime: TimeOfDay.now());
if (time == null) return;
setState(() {
if (isEnd) {
_endTimeControl.text = DateFormat('yyyy-MM-dd HH:mm')
.format(date.copyWith(hour: time.hour, minute: time.minute));
} else {
_startTimeControl.text = DateFormat('yyyy-MM-dd HH:mm')
.format(date.copyWith(hour: time.hour, minute: time.minute));
}
});
}
}
Future openTodoFromDialog(BuildContext context,
[Todo? data, bool isEdit = false]) {
return showDialog(
context: context,
builder: (context) {
return Dialog.fullscreen(
child: TodoForm(data: data, isEdit: isEdit),
);
});
}





