iT邦幫忙

2021 iThome 鐵人賽

DAY 18
0
Modern Web

Flutter web 的奇妙冒險系列 第 18

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 裡的東西清空該如何做?

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


上一篇
Day 17 | Flutter的常用 widgets - Container、Row、Column
下一篇
Day 19 | 萬年範例-TodoList(2)
系列文
Flutter web 的奇妙冒險30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言