Day 18 | 万年范例-TodoList

没错又是万年的demo作品- TodoList

今天我们先来做最最最阳春的TodoList,只先做简单的输入框及新增功能,其他功能我们之後再慢慢加上去。

https://ithelp.ithome.com.tw/upload/images/20211001/20112906sxcLoRaDha.png

从上图来看我们可以得知至少有两个大widget一个是输入框及todo卡片,首先我们可以先来实作todo卡片的样式。

实作todo卡片的样式

以目前的todo卡片来说我们可以得知至少会有两个参数来表示序号及内容,为了方便跟页面的其他layout区隔当然是另外做成一个widget。

我们先新增一个档案todo_card.dart,然後在里面宣告一个 widget TodoCard ,那为什麽是使用 StatelessWidget 呢?依照目前的功能来说,我的内容应该会是从外部传入的所以我这个TodoCard 理论上是不需要有内部状态的。

class TodoCard extends StatelessWidget {
  const TodoCard({required this.todoContent, required this.index, Key? key})
      : super(key: key);
  final String todoContent;
  final int index;

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

首先宣告两个会从外面传进来的变数 todoContentindex 来表示待办事项的内容及序号。

宣告完 non-nullable的参数後记得要放进去 constructor里,而且因为是non-nullable 所以记得也要加上required

const TodoCard({required this.todoContent, required this.index, Key? key}) : super(key: key);

然後回到 main.dart 使用:


TodoCard(todoContent: '测试测试测试测试测试测试', index: 0)
TodoCard(todoContent: '测试测试测试测试测试测试测试测试测试测试测试测试', index: 1)
//...看你想放几个

好我们可以开始实作这张卡片的样式了,从图来看应该会有几个需求

  1. 有border
  2. 固定宽度
  3. 靠左垂直置中
  4. 最小高度
  5. 里面有padding
  6. 每张卡片之间都有间距。

所以我们先将原本的 Container 加上宽度、alignment及border

宽度的参数很好理解就是 width ,但为什麽没有border这个参数呢?跟昨天的颜色一样这些都会是要在 decoration 这个参数里设定。

child: Container(
        width: 300,
				alignment: Alignment.centerLeft
        decoration: BoxDecoration(
          border: Border.all(),
          borderRadius: BorderRadius.circular(8),
        ),
    ),

那我们就用 BoxDecoration 这个class来实作我们 Container 的样式,这里会看到有 border

borderRadius 这两个参数分别就是控制border 本身的样式及这个Container 圆角弯曲程度。

Border.all() 就是直接对四边设定一样的参数,Border 的其他constructor 可以再查阅官方文件,那Border.all() 里面有几个可以调的参数 colorwidthstyle 但我这边就先用预设样式就好,所以就不另外传入其他参数了。

alignment 就直接加上Alignment.centerLeft 表示靠左垂置中

最小高度会是在 constraints 这个参数设定,所以只要使用 BoxConstraints(minHeight: 48) 即可。

里面有padding就会是在 padding 里设置那就会是用 EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0)

所以目前会是长这样:

Container(
    alignment: Alignment.centerLeft,
    constraints: const BoxConstraints(minHeight: 48),
    width: 300,
    padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0),
    decoration: BoxDecoration(
      border: Border.all(),
      borderRadius: BorderRadius.circular(8),
    ),
  ),

那我们先将资料灌进去看看

Container(
        alignment: Alignment.centerLeft,
        constraints: const BoxConstraints(minHeight: 48),
        width: 300,
        padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0),
        child: Text('#$index: $todoContent'),
        decoration: BoxDecoration(
          border: Border.all(),
          borderRadius: BorderRadius.circular(8),
        ),
      ),

好那剩下最後一个:每张卡片之间都有间距

其实也很简单就是用 Padding 包住这个 Container 就好。

@override
Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Container(
        alignment: Alignment.centerLeft,
        constraints: const BoxConstraints(minHeight: 48),
        width: 300,
        padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0),
        child: Text('#$index: $todoContent'),
        decoration: BoxDecoration(
          border: Border.all(),
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    );
  }

输入功能

这边就用 TextField 来实作,我们就直接这样使用:

SizedBox(
    width: 300,
    child: TextField(
      decoration: const InputDecoration(labelText: '待办事项'),
    ),
  ),

相信这些参数不用再多加解释了。

那我们该如何利用 TextField 来达成这个功能呢?没错这时候就需要状态了。目前我们能知道todoList不只一个,所以我这边选择用 List 来实作。

在原本的放计数器状态的那边我们改成先宣告一个 List<String> _todoList 及接下来我们要来改变状态用的 _handleAddNewTodo

class _MyHomePageState extends State<MyHomePage>{
 List<String> _todoList = ['123']; 
	void _handleAddNewTodo(String input) {
   
  }
	// 以下省略...
}

那接下来就是将这个state跟 TextField 串接上。

TextField 中有一个参数是 onSubmitted 所以我们就可以这样写,onSubmitted 是指我们在键盘按下enter後会执行的行为。

SizedBox(
    width: 300,
    child: TextField(
      decoration: const InputDecoration(labelText: '待办事项'),
      onSubmitted: (input) {
        _handleAddNewTodo(input);
      },
    ),
  ),

那接下来就是实作 _handleAddNewTodo 内部的行为

void _handleAddNewTodo(String input) {
    setState(() {
      _todoList = [..._todoList, input];
    });
  }

这里会看到这个操作: _todoList = [..._todoList, input];

... 是separate operator 意思,它可以将 List 摊平,大概会像是:

final a = [1,2,3]
final b = [4]
final c = [...a,...b]  // [1,2,3,4]

所以回来看这个操作意思则是将原本的_todoList 摊平後加上新的input。

下一步我要将这个State与我们的 TodoCard 结合,

..._todoList.asMap().entries.map(
        (entity) =>
            TodoCard(todoContent: entity.value, index: entity.key),
      ),

这里有一个小重点:因为 List.map 不会有index这个值,所以这里是用 asMap 来达成在迭代时拥有index这件事。 List.asMap 会把List变成Map,而key则是原本的index。


至此我们就可以按enter新增卡片。

但还是有几个问题,如果我一直新增最後会发现超出边界导致画面有错误,以及我想要按下enter,TextField 里的东西清空该如何做?

就留到明天再为大家解答吧。


<<:  [Day16] 团队管理:Role & Responsibility

>>:  Swift纯Code之旅 Day21. 「ViewController好乱(3) - MVC下的Button动作」

如何在 WordPress 放上 Google AdSense 广告 - 为网站增加被动收入

当我们经营 WordPress 一段时间之後,许多朋友透过 Google 搜寻或是 FB 或其他社群...

Day 11 漏洞分析 - Vulnerability Analysis (nikto)

经过连续十天的收集情报,体验了各式工具,可以发现前面介绍的大部分工具都是单纯的收集情报,少部分则可以...

Day2

rules of operator precedence 简单的小概念就是运算子(operator)...

DAY5 绘制介面

上一篇我们完成了wireframe的绘制,这次我们要将草稿跟库拉皮卡一样,没有办法下船更具现化一点,...

27 | 【区块组合套件介绍】Stackable

我们之前介绍的 WordPress 原生区块有时会遇上不足够的情况,因为功能都偏向基础和简单。部分...