沒錯又是萬年的demo作品- TodoList
今天我們先來做最最最陽春的TodoList,只先做簡單的輸入框及新增功能,其他功能我們之後再慢慢加上去。
從上圖來看我們可以得知至少有兩個大widget一個是輸入框及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();
}
}
首先宣告兩個會從外面傳進來的變數 todoContent
及 index
來表示待辦事項的內容及序號。
宣告完 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)
//...看你想放幾個
好我們可以開始實作這張卡片的樣式了,從圖來看應該會有幾個需求
所以我們先將原本的 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()
裡面有幾個可以調的參數 color
、 width
、 style
但我這邊就先用預設樣式就好,所以就不另外傳入其他參數了。
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
裡的東西清空該如何做?
就留到明天再為大家解答吧。