你值得拥有的一款diy flutter应用:todos

74 阅读2分钟

功能

  1. 添加todo item
  2. 删除todo item
  3. 搜索todo item
  4. 完成todo item 播放对应的音效,提示完成所花的时间。
  5. 菜单栏功能没实现,读者可以自己动手进行修改
  6. 背景图片,用户可以替换assets里面的照片
  7. 右上角的头像,用户可以替换assets里面的照片
  8. 增加toast功能,空item不允许添加

代码

配置文件

先看配置文件,主要是加入了一些额外的包,just_audio为了制作声音,toast是为了做提示。加入之后命令行输入flutter pub install。如果是vs code的话,会自动下载

name: m_todo_list
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.1+1

environment:
  sdk: '>=2.19.4 <4.0.0'

dependencies:

  flutter:
    sdk: flutter
    
  provider: ^6.0.0
  just_audio: ^0.9.33
  toast: ^0.3.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true
  assets:
    - assets/images/
    - assets/music/

UI

image.png 如图整个项目的ui可以分为四个部分:分别为AppBar,SearchBox,TodoItem list,和最下面的添加框。
这下面的代码就是主界面的布局了。

Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: tdBGColor,
      appBar: _buildAppBar(), // 1. appBar
      body: Stack( // 2.3.4
        children: [
          Container(
            padding: EdgeInsets.symmetric(horizontal: 15),
            child: Column(children: [
              searchBox(), // 2. searchbox
              Expanded( // 3. listview
                  child: ListView(
                children: [
                  Container(
                    margin: EdgeInsets.only(
                      top: 50,
                      bottom: 20,
                    ),
                    child: Text(
                      "All ToDos",
                      style: TextStyle(
                        fontSize: 30,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                  for (ToDo todoo in _foundToDo.reversed)
                    ToDoItem(
                      todo: todoo,
                      onToDoChanged: _handleToDoChange,
                      onDeleteItem: _deleteToDoItem,
                    )
                ],
              ))
            ]),
          ),
          Align( // 4. add item box
            alignment: Alignment.bottomCenter,
            child: Row(children: [
              Expanded(
                  child: Container(
                margin: EdgeInsets.only(
                  bottom: 20,
                  right: 20,
                  left: 20,
                ),
                padding: EdgeInsets.symmetric(
                  horizontal: 20,
                  vertical: 5,
                ),
                decoration: BoxDecoration(
                  color: Colors.white,
                  boxShadow: const [
                    BoxShadow(
                      color: Colors.grey,
                      offset: Offset(0.0, 0.0),
                      blurRadius: 10.0,
                      spreadRadius: 0.0,
                    )
                  ],
                  borderRadius: BorderRadius.circular(10),
                ),
                child: TextField(
                  controller: _todoController,
                  decoration: InputDecoration(
                    hintText: "Add a new todo item",
                    border: InputBorder.none,
                  ),
                  onSubmitted: (value) {
                    _addToDoItem(value);
                  },
                ),
              )),
              Container(
                margin: EdgeInsets.only(
                  bottom: 20,
                  right: 20,
                ),
                child: ElevatedButton(
                  onPressed: () {
                    _addToDoItem(_todoController.text);
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: tdBlue,
                    minimumSize: Size(60, 60),
                    elevation: 10,
                  ),
                  child: Text(
                    "+",
                    style: TextStyle(fontSize: 40),
                  ),
                ),
              )
            ]),
          )
        ],
      ),
    );
  }

AppBar

image.png 左边一个menu的Icon,右边一个图片,SizeBox包裹,borderRadius做成圆角。MainAxisAlignment.spaceBetween把两边分开。背景颜色在constants/colors.dart文件夹定义了。

AppBar _buildAppBar() {
  return AppBar(
    backgroundColor: tdBGColor,
    elevation: 0,
    title: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Icon(
          Icons.menu,
          color: tdBlack,
          size: 30,
        ),
        SizedBox(
            width: 50,
            height: 50,
            child: ClipRRect(
              borderRadius: BorderRadius.circular(20),
              child: Image.asset("assets/images/nana3.png"),
            ))
      ],
    ),
  );
}

SearchBox

image.png 这里定义了一个TextField,然后设置了输入框的prefixIcon同时去掉了border,添加提示文字hintText。这里加入了一个onChanged事件,这里后面会继续提到,现在只讲ui这部分。

Widget searchBox() {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 15),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(20),
      ),
      child: TextField(
          onChanged: (value) => _runFilter(value),
          decoration: InputDecoration(
            contentPadding: EdgeInsets.only(left: 10),
            prefixIcon: Icon(
              Icons.search,
              color: tdBlack,
              size: 20,
            ),
            prefixIconConstraints: BoxConstraints(
              maxHeight: 20,
              maxWidth: 25,
            ),
            border: InputBorder.none,
            hintText: "Search...",
          )),
    );
  }

listview

image.png

上边是个标题文字,下面输出的是我们定义的todoItems。

Expanded(
  child: ListView(
    children: [
      Container(
        margin: EdgeInsets.only(
          top: 50,
          bottom: 20,
        ),
        child: Text(
          "All ToDos",
          style: TextStyle(
            fontSize: 30,
            fontWeight: FontWeight.w500,
          ),
        ),
      ),
      for (ToDo todoo in _foundToDo.reversed)
        ToDoItem(
          todo: todoo,
          onToDoChanged: _handleToDoChange,
          onDeleteItem: _deleteToDoItem,
        )
    ],
))

ToDo的数据结构定义如下:

class ToDo {
  String id;
  String todoText;
  bool isDone;

  ToDo({
    required this.id,
    required this.todoText,
    this.isDone = false,
  });

  // 定义静态方法,可以直接通过class name 进行调用,即ToDo.todoList()
  static List<ToDo> todoList() {
    return [
      ToDo(id: '01', todoText: "Hello, Welcome to use this app.", isDone: true),
      ToDo(id: '02', todoText: "Have fun!", isDone: false),
    ];
  }
}

ToDoItem的Widget的定义如下,一个ListTitle,定义了leading和trailing,中间是展示的文本。

image.png

class ToDoItem extends StatelessWidget {
  final ToDo todo;
  final onToDoChanged;
  final onDeleteItem;

  const ToDoItem({
    Key? key,
    required this.todo,
    required this.onToDoChanged,
    required this.onDeleteItem,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(bottom: 20),
      child: ListTile(
        onTap: () {
          onToDoChanged(todo);
        },
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
        contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 5),
        tileColor: Colors.white,
        leading: Icon(
          todo.isDone ? Icons.check_box : Icons.check_box_outline_blank,
          color: tdBlue,
        ),
        title: Text(
          todo.todoText,
          style: TextStyle(
            fontSize: 16,
            color: tdBlack,
            decoration: todo.isDone ? TextDecoration.lineThrough : null,
          ),
        ),
        trailing: Container(
          padding: EdgeInsets.all(0),
          margin: EdgeInsets.symmetric(vertical: 12),
          height: 35,
          width: 35,
          decoration: BoxDecoration(
            color: tdRed,
            borderRadius: BorderRadius.circular(5),
          ),
          child: IconButton(
            color: Colors.white,
            iconSize: 18,
            icon: Icon(Icons.delete),
            onPressed: () {
              onDeleteItem(todo.id);
            },
          ),
        ),
      ),
    );
  }
}

add button

定义了一个container,左边一个textfield,右边一个Container(ElevatedButton)。

Align(
    alignment: Alignment.bottomCenter,
    child: Row(children: [
      Expanded(
          child: Container(
        margin: EdgeInsets.only(
          bottom: 20,
          right: 20,
          left: 20,
        ),
        padding: EdgeInsets.symmetric(
          horizontal: 20,
          vertical: 5,
        ),
        decoration: BoxDecoration(
          color: Colors.white,
          boxShadow: const [
            BoxShadow(
              color: Colors.grey,
              offset: Offset(0.0, 0.0),
              blurRadius: 10.0,
              spreadRadius: 0.0,
            )
          ],
          borderRadius: BorderRadius.circular(10),
        ),
        child: TextField(
          controller: _todoController,
          decoration: InputDecoration(
            hintText: "Add a new todo item",
            border: InputBorder.none,
          ),
          onSubmitted: (value) {
            _addToDoItem(value);
          },
        ),
      )),
      Container(
        margin: EdgeInsets.only(
          bottom: 20,
          right: 20,
        ),
        child: ElevatedButton(
          onPressed: () {
            _addToDoItem(_todoController.text);
          },
          style: ElevatedButton.styleFrom(
            backgroundColor: tdBlue,
            minimumSize: Size(60, 60),
            elevation: 10,
          ),
          child: Text(
            "+",
            style: TextStyle(fontSize: 40),
          ),
        ),
      )
    ]),
)

相关链接

本项目开源的Github
Tutorial video
Introduction of text field
Github Reference
Music library
Flutter music library
Turn all widgets be tappable.
Background image