Flutter作業#05 — 製作 about 頁面(2)

作業:打造吸睛的 About 頁面 進階版

- 加入 const
- 自訂 widget constructor
- 搭配自訂的資料型別
- 包含多個檔案
- 使用 collection for 顯示 List 的資料

這一週比較忙🙈 好險在上課前一小時把這週的作業做完啦!這次使用新學的語法做了跟上次的作業做成相同的頁面,看了最後的對比圖,覺得自己有慢慢在進步啦💪

實在太趕了 GitHub 連結下次再補~🙏

成品 gif

設置 enum files

  1. 角色資料 enum:
enum ShellieMay {
name('雪莉玫 ShellieMay'),
description(
'有了達菲陪伴米奇環遊世界後,米妮又特意為達菲製作新的朋友,於是邊有了雪梨玫,後來達菲更與雪梨玫成為了好朋友。她和達菲一樣頭又著和米奇一樣的形狀,擁有粉紅色毛髮,是達菲熊家族第二個出現的角色。'),
gender('女'),
characteristic('長長的睫毛,藍色眼睛,頭戴蝴蝶結,腳掌和屁股上有著米奇形狀的印記'),
personality('樂觀開朗'),
hobby('旅遊,打扮,認識新朋友'); // 這裡要記得加上分號!!

//定義屬性
final String inputText;

//構造函數
const ShellieMay(this.inputText);
}

2. 表格欄位的圖標 enum

enum IconSet {
gender(Icons.account_circle, '性別', Colors.red),
characteristic(Icons.stars, '特徵', Colors.orange),
personality(Icons.mood, '性格', Colors.green),
hobby(Icons.recommend, '愛好', Colors.blue);

//定義屬性
final IconData iconName;
final String iconTitle;
final Color iconColor;

//構造函數
const IconSet(this.iconName, this.iconTitle, this.iconColor);
}

其實原本的 enum 都是直接打在 main.dart 裡的,寫完之後選擇 enum 名稱,就會自動出現小燈泡選項 “Move ‘IconSet’ to file” :

選擇之後,就會自動整理成 .dart 檔案,並且在原本的 main.dart 中自動 import 啦!

各 widget 拆分

BarTitle()

class BarTitle extends StatelessWidget {
const BarTitle({
super.key,
});

@override
Widget build(BuildContext context) {
return Text(
ShellieMay.name.inputText, //這裡呼叫自定義的 enum ShellieMay
style: const TextStyle(
fontWeight: FontWeight.bold,
),
);
}
}

Background()

class Background extends StatelessWidget {
const Background({
super.key,
});

@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints.expand(), //使 child widget 對齊 parent widget 四邊
child: Opacity(
opacity: 0.24, //調整透明度
child: Image.asset(
'assets/background.jpg', //設置圖片的時候,如果模擬器上顯示"Asset not found" hot reload 可能跑不出來(會),要記得手動刷新~
fit: BoxFit.cover,
),
),
);
}
}

Avatar()

class Avatar extends StatelessWidget {
const Avatar({
super.key,
});

@override
Widget build(BuildContext context) {
return const SizedBox(
width: 320,
height: 320,
child: CircleAvatar( //自動生成圓形圖片
foregroundImage: AssetImage(
'assets/avatar/shellieMay.jpg',
),
),
);
}
}

Description()

class Description extends StatelessWidget {
const Description({
super.key,
});

@override
Widget build(BuildContext context) {
return Text(
ShellieMay.description.inputText, //呼叫自定義的 enum ShellieMay
style: const TextStyle(
fontSize: 18,
),
);
}
}

DetailsTable()

用這週新學了 Table Widget 來取代原本使用的 Divider() 和 SizedBox() :

//先設置兩組 List 以便後續生成時使用 for 迴圈依次呼叫。
List<IconSet> icons = [
IconSet.gender,
IconSet.characteristic,
IconSet.personality,
IconSet.hobby
];
List<ShellieMay> character = [
ShellieMay.gender,
ShellieMay.characteristic,
ShellieMay.personality,
ShellieMay.hobby
];


class DetailsTable extends StatelessWidget {
const DetailsTable({
super.key,
});

@override
Widget build(BuildContext context) {
return Table(
border: const TableBorder( //設置表格框線
horizontalInside: BorderSide( //僅設置表格內部水平線
color: Colors.grey,
width: 1.2,
),
),
columnWidths: { //設置兩欄的寬度比例
0: FractionColumnWidth(1 / 6),
1: FractionColumnWidth(5 / 6),
},
children: [
for (var i = 0; i <= 3; i++) //使用 for 迴圈重複四次生成 row 的 method。for 內部記得要用 “;” 分隔!!
buildTableRow( //自訂的 buildTableRow() *完整程式碼見下方~
iconSet: icons[i], //自訂參數傳入自訂的 enum IconSet
character: character[i], //自訂參數傳入自訂的 enum ShellieMay
)
],
);
}
}


//method: buildTableRow()
TableRow buildTableRow({
required IconSet iconSet, //設置必填的命名參數
required ShellieMay character,
}) {
return TableRow(
children: [
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle, //tableCell 內部垂直置中對齊
child: Padding(
padding: const EdgeInsets.all(4.0),
child: RowTitle( //自訂的 widget RowTitle() *完整程式碼見下方
rowIcon: iconSet.iconName, // iconSet 參數型別為自訂的 enum IconSet
rowName: iconSet.iconTitle,
iconColor: iconSet.iconColor,
),
),
),
TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
character.inputText, //character 參數型別為自訂的 enum ShellieMay
style: TextStyle(
fontSize: 18,
),
),
),
),
],
);
}


//widget: RowTitle()
class RowTitle extends StatelessWidget {
IconData rowIcon; //設置內部參數的各個型別
String rowName;
Color iconColor;

RowTitle({
required this.rowIcon, //設置必填的命名參數
required this.rowName,
required this.iconColor,
});

@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(
rowIcon,
size: 36,
color: iconColor,
),
Text(rowName),
],
);
}
}

Gallery()

class Gallery extends StatelessWidget {
const Gallery({
super.key,
});

@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: 48), //設置 container 底部與外的距離(取代原本的 SizedBox 手動設置間距)
child: SingleChildScrollView(
scrollDirection: Axis.horizontal, //設置橫向滑動的 scrollView
child: SizedBox(
width: 810,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ //使用 for 迴圈設置三次相同格式的圖片。
for (var i = 1; i <= 3; i++) setGalleryItem(i), //自訂的 method setGalleryItem() *完整程式碼見下方
],
),
),
),
);
}


//method: setGalleryItem()
SizedBox setGalleryItem(int serialNum) { //設置數字型別的參數
return SizedBox(
width: 260,
height: 260,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)), //使用 BoxDecoration 設置圓角
),
clipBehavior: Clip.hardEdge, //使 container 內的 child 也會被 clipped 成圓角
child: Image.asset(
'assets/gallery/shellieMay$serialNum.jpg',
),
),
);
}
}

完整頁面拼接

import 'package:about_shellie_may/icon_set.dart';
import 'package:about_shellie_may/shellie_may.dart';
import 'package:flutter/material.dart';

class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: BarTitle(),
),
body: Stack(
children: [
Background(),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 24,
),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: [
Wrap(
runSpacing: 16,
children: [
Avatar(),
Description(),
DetailsTable(),
Gallery(),
],
)
],
),
),
)
],
),
),
);
}
}

void main() {
runApp(const MainApp());
}

兩週作業比較

兩次作業的 method/ widget tree放一起看,清爽多啦~

--

--