TodoMVC 开发
组件分解
为降低代码耦合度并且提高开发效率,我们使用组件化的思想来进行开发。
以下是一个 todomvc 的界面,简单的,我们可以将其分解成 3 个独立功能的组件:
-
内容输入:一个输入框,供用户输入内容;
-
待办列表:可修改待办状态,删除待办;
-
底部菜单:
- "n item left"显示未完成待办数量
- All(全部)、Active(未完成待办)和 Completed(已完成待办)可对待办列表进行筛选
- "Clear completed"可删除所有已完成待办
状态管理
在上边我们已经对组件进行了拆解,如果是前端开发人员,应该已经注意到了上方拆解的各个组件之间需要共享数据的问题。在面向对象的编程中,子类可以继承父类的数据,以达到共享数据的效果,但是从逻辑上这三个组件明显是并列的,不存在父子关系。这里我们需要寻求类一种似于 Vue 中的 VueX/pinia,或者是 React 中 Redux 的解决方案。
在上一篇《Flutter 开发速成(二)------Dart、Flutter 基础》中已经提到组件间状态管理的解决方案:InheritedWidget ,或者是引用状态管理的包。
从待办的功能中我们可以分析出需要 2 个共享数据就够了:todoList(待办列表)和 currentStatus(当前展示的待办类型)。
我使用的是 InheritedWidget 来实现状态管理的功能。
在 lib 下新建一个 states 目录,这是一个状态管理功能的目录。在 states 下新建一个 todo_profile.dart 文件,TodoProfileState 类继承了 InheritedWidget。
在 TodoProfileState 的构造函数中,必须传入自定义的数据和方法。
TodoProfileState 中 todoList(待办列表)和 currentStatus(当前展示的待办类型)是自定义的数据,这两个数据需要通过传入方法修改,changeTodoList 和 changeCurrentStatus 就是分别用以修改这两个数据的方法。
TodoProfileState 完整代码:
dart
import 'package:flutter/material.dart';
class TodoProfileState extends InheritedWidget {
const TodoProfileState(
{super.key,
required this.todoList,
required this.changeTodoList,
required this.currentStatus,
required this.changeCurrentStatus,
required this.child})
: super(child: child);
final Widget child;
// 待办列表
final List todoList;
/// 修改待办列表
final Function changeTodoList;
// 当前列表类型
// 0:全部 1:进行中 2:已完成
final int currentStatus;
final Function changeCurrentStatus;
//定义一个便捷方法,方便子树中的widget获取共享数据
static TodoProfileState? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<TodoProfileState>();
}
//该回调决定当data发生变化时,是否通知子树中依赖data的Widget重新build
@override
bool updateShouldNotify(TodoProfileState oldWidget) {
return true;
}
}
TodoProfileState 类定义好之后,需要绑定其数据和方法。
在 main 方法中,这里实例化一个TodoProfileState
作为Scaffold
实例的 body,TodoProfileState
的 child 需要是一个SafeArea
实例,SafeArea
中用 Column 组件作为布局,将其他内容纵向排列。
scala
import 'package:flutter/material.dart';
import 'states/index.dart';
import './widgets/index.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'TodoMVC'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
List _todoList = [];
int _currentStatus = 0;
// 修改待办列表
void _changeTodoList(List todoList) {
setState(() {
_todoList = todoList;
});
}
// 修改当前待办类型
void _changeCurrentStatus(int currentStatus) {
setState(() {
_currentStatus = currentStatus;
});
}
// 自定义方法
Column _initListData() {
return Column(children: [
Container(
margin: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: InputForm(),
),
Expanded(child: Items()),
Footers()
]);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: TodoProfileState(
todoList: _todoList,
changeTodoList: _changeTodoList,
currentStatus: _currentStatus,
changeCurrentStatus: _changeCurrentStatus,
child: SafeArea(
child: _initListData(),
),
),
);
}
}
文本输入组件实现
lib 目录下新建一个 widgets 目录,用来管理自定义组件。
flutter 有基础的文本输入组件TextField
。
考虑到输入内容校验移动端操作的便利性,我加了一个Add
按钮,点击 Android 手机键盘的搜索或者点击Add
按钮将当前的输入内容加入待办列表。
Form
继承自StatefulWidget
对象,Form
的子孙元素必须是FormField
类型,文本输入使用 FormField 类型的组件TextFormField
。
ElevatedButton
即"漂浮"按钮。onPressed
点击按钮实现的方法。点击按钮进行表单校验,校验通过保存表单。
表单保存,触发TextFormField
组件的onSaved
方法,在这方法中调用TodoProfileState
类的方法,将内容加入待办列表。
scss
TodoProfileState.of(context)?.changeTodoList(_todoList)
完整代码:
scala
import 'package:flutter/material.dart';
import '../states/index.dart';
class InputForm extends StatefulWidget {
const InputForm({super.key});
@override
State<InputForm> createState() => _InputFormState();
}
class _InputFormState extends State<InputForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
List? _todoList;
@override
void didChangeDependencies() {
_todoList = TodoProfileState.of(context)?.todoList;
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Row(
children: <Widget>[
Expanded(
child: TextFormField(
decoration: const InputDecoration(
hintText: 'What needs to be done?',
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
onSaved: (newValue) {
_todoList?.add({"content": newValue, "isActive": true});
// 改变待办列表
TodoProfileState.of(context)?.changeTodoList(_todoList);
_formKey.currentState?.reset();
},
onFieldSubmitted: (value) {
//// 监听到Enter键被按下
if (value == null || value.isEmpty) {
return;
}
_todoList?.add({"content": value, "isActive": true});
// 改变待办列表
TodoProfileState.of(context)?.changeTodoList(_todoList);
_formKey.currentState?.reset();
},
),
),
SizedBox(width: 10),
ElevatedButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.all(2))),
onPressed: () {
// Validate will return true if the form is valid, or false if
// the form is invalid.
var _form = _formKey.currentState;
if (_form!.validate()) {
// 保存表单内容
_form.save();
// Process data.
}
},
child: const Text(
'Add',
// style: TextStyle(color: Colors.green),
),
)
],
),
);
}
}
待办列表组件实现
考虑到移动端需要上下滑动查看内容,使用ListView
组件实现可滑动列表;
列表项 ListTile 用 Container 包裹,通过 Container 的 decoration 属性画每项底部分割线;
ListTile 的 leading 为 Checkbox,给列表项前加多选框;
ListTile 的 trailing 为 IconButton,给列表项后加删除按钮。
less
import 'package:flutter/material.dart';
import '../states/index.dart';
class Items extends StatefulWidget {
const Items({super.key});
@override
State<Items> createState() => _ItemsState();
}
class _ItemsState extends State<Items> {
List? _todoList;
@override
void didChangeDependencies() {
var _currentStatus = TodoProfileState.of(context)!.currentStatus;
if (_currentStatus > 0) {
var isActive = _currentStatus == 1 ? true : false;
_todoList = TodoProfileState.of(context)
?.todoList
.where((item) => item['isActive'] == isActive)
.toList();
} else {
_todoList = TodoProfileState.of(context)?.todoList;
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
itemCount: _todoList?.length,
itemBuilder: (BuildContext context, int index) {
return Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xedededff), width: 1))),
child: ListTile(
title: Text(
_todoList?[index]['content'],
style: TextStyle(
// 完成和未完成任务的字体颜色不一样
color: _todoList?[index]['isActive']
? Colors.black
: Color(0xd9d9d9ff),
// 任务完成文字中间加划线
decoration: _todoList?[index]['isActive']
? null
: TextDecoration.lineThrough,
decorationColor: Color(0xd9d9d9ff), // 可选:设置划线颜色
decorationThickness: 2,
),
), //下划线),),
leading: Checkbox(
value: !_todoList?[index]['isActive'],
activeColor: Colors.green, //选中时的颜色
shape: CircleBorder(eccentricity: 0.5),
onChanged: (value) {
// print('value:$value------index:$index');
// 改变待办列表
_todoList?[index]["isActive"] = !value!;
TodoProfileState.of(context)?.changeTodoList(_todoList);
},
),
trailing: IconButton(
icon: const Icon(
Icons.close,
),
color: Colors.red,
onPressed: () {
// 删除待办列表某项
_todoList?.removeAt(index);
TodoProfileState.of(context)?.changeTodoList(_todoList);
},
),
));
},
);
}
}
底部菜单组件实现
底部菜单的"n item left"显示文本,其他的是一排的文本按钮。
TextButton
:flutter 的基本组件TextButton
即文本按钮,默认背景透明并不带阴影。
less
TextButton(
child: Text("normal"),
onPressed: () {},
)
这里为了方便自定义按钮的的交互样式,使用了Container
组件。Container
组件的构造函数没有处理点击事件的回调函数,所以在外边包了一层Listener
组件,在Listener
组件的onPointerDown
方法中处理点击事件。
less
Listener(
child: Container(
child: Text('Active'),
margin: EdgeInsets.fromLTRB(0, 0, 4, 0),
padding: EdgeInsets.fromLTRB(4, 0, 4, 0),
decoration: BoxDecoration(
border: Border.all(
color: _currentStatus == 1
? Color.fromRGBO(175, 47, 47, 0.2)
: Colors.white)),
),
onPointerDown: (PointerDownEvent event) {
// print('点击Active');
setState(() {
_currentStatus = 1;
});
TodoProfileState.of(context)?.changeCurrentStatus(1);
},
)
实现效果
源码:
此 TodoMVC demo 地址:github(github.com/ying-611/fl...)
将应用安装到 Android 手机
1、真机连接
使用 USB 数据线将手机连接到电脑,USB 连接方式需要选择"数据传输"
接下来就需要打开手机的开发者模型,最新的 Android 系统开发者选项隐藏起来了。
打开设置 ->关于手机,点击版本号 5-7 次就可以打开手机的开发者模式。
打开设置 ->系统与更新 ,就可以看到一个开发者人员选项
将USB调试
和"仅充电"模式下运行ADB调试
打开。
2、通过 AS 运行并安装应用
然后再打开 AS,AS 中打开项目,在设备选择中选择我们的手机,之间点击运行按钮,程序就会运行在我们的手机上。在手机上你会看到按钮应用的提示,根据提示安装即可。
程序安装完成会自已运行。