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

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

相关推荐
liulian091612 小时前
Flutter for OpenHarmony 跨平台开发:单位转换功能实战指南
flutter
千码君201613 小时前
Trae:一些关于flutter和 go前后端开发构建的分享
android·flutter·gradle·android-studio·trae·vibe code
maaath15 小时前
【maaath】Flutter for OpenHarmony 手表配饰应用实战开发
flutter·华为·harmonyos
maaath15 小时前
【maaath】Flutter for OpenHarmony 跨平台计算器应用开发实践
flutter·华为·harmonyos
maaath20 小时前
【maaath】Flutter for OpenHarmony 闹钟时钟应用开发实战
flutter·华为·harmonyos
maaath21 小时前
【maaath】Flutter for OpenHarmony 短信管理应用实战
flutter·华为·harmonyos
maaath1 天前
【maaath】Flutter for OpenHarmony打造跨平台便签备忘录应用
flutter·华为·harmonyos
千码君20161 天前
flutter:与Android Studio模拟器的调试分享
android·flutter
xmdy58661 天前
Flutter+开源鸿蒙实战|智联邻里Day8 Lottie动画集成+url_launcher跳转拨号+个人中心完善+全局UI统一
flutter·开源·harmonyos
liulian09161 天前
Flutter for OpenHarmony 跨平台开发:颜色选择器功能实战指南
flutter