Flutter 开发速成(三)——开发一个 TodoMVC 应用

TodoMVC 开发

组件分解

为降低代码耦合度并且提高开发效率,我们使用组件化的思想来进行开发。

以下是一个 todomvc 的界面,简单的,我们可以将其分解成 3 个独立功能的组件:

  1. 内容输入:一个输入框,供用户输入内容;

  2. 待办列表:可修改待办状态,删除待办;

  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 中打开项目,在设备选择中选择我们的手机,之间点击运行按钮,程序就会运行在我们的手机上。在手机上你会看到按钮应用的提示,根据提示安装即可。

程序安装完成会自已运行。

相关推荐
程序猿阿伟13 分钟前
《React Native与Flutter:社交应用中用户行为分析与埋点统计的深度剖析》
flutter·react native·react.js
肥肥呀呀呀10 小时前
在Flutter上如何实现按钮的拖拽效果
前端·javascript·flutter
WDeLiang21 小时前
Flutter - UIKit开发相关指南 - 导航
flutter·ios·dart
程序猿阿伟1 天前
《Flutter社交应用暗黑奥秘:模式适配与色彩的艺术》
前端·flutter
融云1 天前
集成指南:如何采用融云 Flutter IMKit 实现双端丝滑社交体验
flutter
EndingCoder2 天前
跨平台移动开发框架React Native和Flutter性能对比
flutter·react native·react.js
Double Point2 天前
`RotationTransition` 是 Flutter 中的一个动画组件,用于实现旋转动画效果
flutter
亚洲小炫风2 天前
flutter 项目工程文件夹组织结构
flutter·flutter工程结构
Double Point2 天前
Flutter 中 vsync
flutter
Double Point2 天前
ScaleTransition 是 Flutter 中的一个动画组件,用于实现缩放动画效果。
flutter