使用gitpod開發flutter - 食譜app
成果畫面呈現
https://www.gitpod.io/docs/enterprise/introduction/getting-started/quickstart/flutter
https://github.com/gitpod-samples/template-flutter
點Open in Gitpod
新增screens folder建立home.dart
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: Text(
'Hello World!',
style: TextStyle(fontSize: 24),
),
),
);
}
}
main.dart加入home page
import 'package:flutter/material.dart';
import 'package:gitpod_flutter_quickstart/screens/home.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith(secondary: Colors.black),
),
home: HomePage(),
);
}
}
執行看看
新增utils folder建立class.dart
class Nutrients {
String name;
String weight;
double percent;
Nutrients({required this.name, required this.weight, required this.percent});
}
class Recipe {
String id, imageUrl, title;
List<String> steps;
List<String> ingredients;
List<Nutrients> nutrients;
Recipe(
{
required this.id,
required this.title,
required this.imageUrl,
required this.steps,
required this.ingredients,
required this.nutrients
}
);
}
建立data.dart
import 'package:gitpod_flutter_quickstart/utils/class.dart';
class Data {
static List<Recipe> recipes = [
Recipe(
id: '1',
title: '水餃',
imageUrl:
'https://images.unsplash.com/photo-1496116218417-1a781b1c416c?ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
nutrients: [
Nutrients(name: '卡路里', weight: '200', percent: 0.7),
Nutrients(name: '蛋白質', weight: '10gm', percent: 0.5),
Nutrients(name: '碳水化合物', weight: '50gm', percent: 0.9),
],
steps: [
'準備餡料:將絞肉、切碎的蔬菜、醬油和調味料混合均勻。',
'在餃子皮中央放一勺餡料,對折並捏緊封口。',
'選擇煮法:水煮、蒸或煎至熟透。'
'搭配醬料,趁熱享用。'
],
ingredients: [
'1/2 磅絞肉(豬肉或雞肉)',
'2 包水餃皮',
'切碎的高麗菜',
'2 湯匙 醬油'
]),
Recipe(
id: '2',
title: '卡布奇諾',
imageUrl:
'https://images.unsplash.com/photo-1444418185997-1145401101e0?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1391&q=80',
nutrients: [
Nutrients(name: '熱量', weight: '200 大卡', percent: 0.7),
Nutrients(name: '蛋白質', weight: '10 克', percent: 0.5),
Nutrients(name: '碳水化合物', weight: '50 克', percent: 0.9),
],
steps: [
'準備好所有食材。',
'將兩杯濃縮咖啡倒入卡布奇諾杯中。',
'打發牛奶,使其體積增加一倍。',
'將打發好的牛奶倒入濃縮咖啡中,形成均勻比例的泡沫、蒸汽牛奶和濃縮咖啡。',
'立即享用。'
],
ingredients: ['2 份 濃縮咖啡(雙倍濃縮)', '4 盎司 牛奶'],
),
Recipe(
id: '3',
title: '義大利麵',
imageUrl:
'https://images.unsplash.com/photo-1473093295043-cdd812d0e601?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
nutrients: [
Nutrients(name: '熱量', weight: '100 大卡', percent: 0.2),
Nutrients(name: '蛋白質', weight: '10 克', percent: 0.7),
Nutrients(name: '碳水化合物', weight: '50 克', percent: 0.6),
Nutrients(name: '脂肪', weight: '10 克', percent: 0.3),
],
steps: [
'準備好所有食材。',
'將水煮沸並加入適量鹽,然後加入義大利麵,煮至軟硬適中。',
'在平底鍋中加熱橄欖油,炒香大蒜和洋蔥。',
'加入番茄醬,慢燉 10 分鐘,調味。',
'瀝乾義大利麵,與醬汁混合均勻。',
'撒上起司或羅勒葉後即可享用。'
],
ingredients: [
'200 克 義大利麵',
'2 湯匙 橄欖油',
'1 顆 洋蔥(切碎)',
'2 瓣 大蒜(切碎)',
'1 杯 番茄醬',
'適量 鹽和胡椒',
'適量 起司或羅勒葉(可選)'
],
),
Recipe(
id: '4',
title: '披薩',
imageUrl:
'https://images.unsplash.com/photo-1506354666786-959d6d497f1a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80',
nutrients: [
Nutrients(name: '熱量', weight: '200 大卡', percent: 0.7),
Nutrients(name: '蛋白質', weight: '10 克', percent: 0.5),
Nutrients(name: '碳水化合物', weight: '50 克', percent: 0.9),
],
steps: [
'準備好所有食材。',
'將麵粉、酵母、水和鹽混合,揉成麵團,靜置發酵 1 小時。',
'將麵團擀成圓形,放在烤盤上。',
'在麵團上塗抹番茄醬,撒上起司和配料。',
'將披薩放入 220°C 預熱的烤箱,烘烤 15-20 分鐘。',
'取出披薩,切片後即可享用。'
],
ingredients: [
'2 杯 麵粉',
'1 湯匙 酵母',
'1/2 杯 水',
'1 茶匙 鹽',
'1/2 杯 番茄醬',
'1 杯 起司',
'適量 喜愛的配料(如蘑菇、臘腸、青椒)'
],
),
];
}
建立widget.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gitpod_flutter_quickstart/utils/class.dart';
// CircleIndicator 是一個 StatefulWidget,用來顯示圓形進度條並且加上動畫效果
class CircleIndicator extends StatefulWidget {
final double percent; // 圓形進度條的填充百分比 (範圍是 0.0 到 1.0)
final Nutrients nutrient; // 用來顯示營養素的物件,包含名稱和重量
// 構造函數,預設 percent 是 0.5
CircleIndicator({this.percent = 0.5, required this.nutrient});
@override
_CircleIndicatorState createState() => _CircleIndicatorState();
}
// CircleIndicator 的狀態管理類別
class _CircleIndicatorState extends State<CircleIndicator>
with SingleTickerProviderStateMixin {
double fraction = 0.0; // 用來保存當前的動畫進度值
late Animation<double> animation; // 動畫控制器,用來管理進度動畫
@override
void initState() {
super.initState();
// 初始化動畫控制器,設置動畫持續時間為 1000 毫秒
var controller = AnimationController(
duration: Duration(milliseconds: 1000), vsync: this);
// 設置動畫,從 0.0 開始到 widget.percent 的結束值
animation = Tween(begin: 0.0, end: widget.percent).animate(controller)
..addListener(() {
setState(() {
fraction = animation.value; // 隨著動畫進度更新 fraction
});
});
controller.forward(); // 啟動動畫
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: <Widget>[
// 顯示營養素的名稱和重量
Container(
width: 70,
height: 70,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
widget.nutrient.name, // 顯示營養素的名稱
style: TextStyle(color: Colors.white),
),
Text(
widget.nutrient.weight, // 顯示營養素的重量
style: TextStyle(color: Colors.white),
),
],
),
),
// 圓形進度條的外層容器,使用 CustomPaint 來繪製圓形
Container(
width: 70,
height: 70,
child: CustomPaint(
foregroundPainter: CirclePainter(fraction), // 傳入當前進度值來繪製圓形
),
),
],
),
);
}
}
// CirclePainter 用來繪製圓形進度條
class CirclePainter extends CustomPainter {
late Paint _paint; // 用來繪製圓形的畫筆
double _fraction; // 當前的進度值
CirclePainter(this._fraction) {
// 初始化畫筆
_paint = Paint()
..color = Color(0xff8DB646) // 設置圓形顏色
..strokeWidth = 5.0 // 設置圓形邊框寬度
..strokeCap = StrokeCap.round // 設置圓角
..style = PaintingStyle.stroke; // 只繪製邊框
}
@override
void paint(Canvas canvas, Size size) {
var rect = Offset(0.0, 0.0) & size; // 設定圓形範圍
// 使用畫筆繪製圓形,根據 _fraction 設置繪製的角度
canvas.drawArc(rect, -pi / 2, pi * 2 * _fraction, false, _paint);
}
@override
bool shouldRepaint(CirclePainter oldDelegate) {
return oldDelegate._fraction != _fraction; // 如果進度值有變化,則需要重繪
}
}
建立widget.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gitpod_flutter_quickstart/utils/class.dart';
// CircleIndicator 是一個 StatefulWidget,用來顯示圓形進度條並且加上動畫效果
class CircleIndicator extends StatefulWidget {
final double percent; // 圓形進度條的填充百分比 (範圍是 0.0 到 1.0)
final Nutrients nutrient; // 用來顯示營養素的物件,包含名稱和重量
// 構造函數,預設 percent 是 0.5
CircleIndicator({this.percent = 0.5, required this.nutrient});
@override
_CircleIndicatorState createState() => _CircleIndicatorState();
}
// CircleIndicator 的狀態管理類別
class _CircleIndicatorState extends State<CircleIndicator>
with SingleTickerProviderStateMixin {
double fraction = 0.0; // 用來保存當前的動畫進度值
late Animation<double> animation; // 動畫控制器,用來管理進度動畫
@override
void initState() {
super.initState();
// 初始化動畫控制器,設置動畫持續時間為 1000 毫秒
var controller = AnimationController(
duration: Duration(milliseconds: 1000), vsync: this);
// 設置動畫,從 0.0 開始到 widget.percent 的結束值
animation = Tween(begin: 0.0, end: widget.percent).animate(controller)
..addListener(() {
setState(() {
fraction = animation.value; // 隨著動畫進度更新 fraction
});
});
controller.forward(); // 啟動動畫
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: <Widget>[
// 顯示營養素的名稱和重量
Container(
width: 70,
height: 70,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
widget.nutrient.name, // 顯示營養素的名稱
style: TextStyle(color: Colors.white),
),
Text(
widget.nutrient.weight, // 顯示營養素的重量
style: TextStyle(color: Colors.white),
),
],
),
),
// 圓形進度條的外層容器,使用 CustomPaint 來繪製圓形
Container(
width: 70,
height: 70,
child: CustomPaint(
foregroundPainter: CirclePainter(fraction), // 傳入當前進度值來繪製圓形
),
),
],
),
);
}
}
// CirclePainter 用來繪製圓形進度條
class CirclePainter extends CustomPainter {
late Paint _paint; // 用來繪製圓形的畫筆
double _fraction; // 當前的進度值
CirclePainter(this._fraction) {
// 初始化畫筆
_paint = Paint()
..color = Color(0xff8DB646) // 設置圓形顏色
..strokeWidth = 5.0 // 設置圓形邊框寬度
..strokeCap = StrokeCap.round // 設置圓角
..style = PaintingStyle.stroke; // 只繪製邊框
}
@override
void paint(Canvas canvas, Size size) {
var rect = Offset(0.0, 0.0) & size; // 設定圓形範圍
// 使用畫筆繪製圓形,根據 _fraction 設置繪製的角度
canvas.drawArc(rect, -pi / 2, pi * 2 * _fraction, false, _paint);
}
@override
bool shouldRepaint(CirclePainter oldDelegate) {
return oldDelegate._fraction != _fraction; // 如果進度值有變化,則需要重繪
}
}
增加details.dart
import 'package:flutter/material.dart';
import 'package:gitpod_flutter_quickstart/utils/class.dart';
import 'package:gitpod_flutter_quickstart/utils/widgets.dart';
// 詳細頁面 (DetailsPage) 負責顯示單一食譜的詳細資訊。
class DetailsPage extends StatelessWidget {
final Recipe recipe; // 傳遞的食譜數據
DetailsPage({required this.recipe});
@override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0, // 設置 SliverAppBar 的展開高度
floating: false,
pinned: true, // 讓 AppBar 保持固定
title: Text(recipe.title), // 設置標題
flexibleSpace: FlexibleSpaceBar(
background: Hero(
tag: recipe.id, // 使用 Hero 動畫標籤
child: FadeInImage(
image: NetworkImage(recipe.imageUrl), // 加載食譜圖片
fit: BoxFit.cover,
placeholder: AssetImage('assets/images/loading.gif'), // 預設加載圖片
),
),
),
),
];
},
body: Container(
color: Theme.of(context).primaryColor,
padding: EdgeInsets.only(top: 8.0), // 設置上方內邊距
child: SingleChildScrollView(
physics: BouncingScrollPhysics(), // 設置滾動效果
child: Column(
children: <Widget>[
Text('Nutrition', // 營養標題
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20)),
NutritionWidget(
nutrients: recipe.nutrients,
),
Divider(color: Colors.white, endIndent: 40.0, indent: 40.0),
Text('Ingredients', // 成分標題
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20)),
IngredientsWidget(
ingredients: recipe.ingredients,
),
Divider(color: Colors.white, endIndent: 40.0, indent: 40.0),
Text('Steps', // 步驟標題
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20)),
RecipeSteps(
steps: recipe.steps,
)
],
),
),
),
),
);
}
}
// 顯示食譜步驟的元件
class RecipeSteps extends StatelessWidget {
final List<String> steps;
RecipeSteps({this.steps = const []});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: steps.length,
padding: const EdgeInsets.all(0.0),
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).accentColor,
child: Text('${index + 1}',
style: TextStyle(
color: Colors.black, fontWeight: FontWeight.bold)),
),
title: Text(steps[index],
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16)));
},
);
}
}
// 顯示成分列表的元件
class IngredientsWidget extends StatelessWidget {
final List<String>? ingredients;
IngredientsWidget({this.ingredients});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 50,
width: double.infinity,
child: ListView.builder(
itemCount: ingredients!.length,
shrinkWrap: true,
scrollDirection: Axis.horizontal,
physics: BouncingScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Chip(
backgroundColor: Theme.of(context).accentColor,
label: Text(ingredients![index],
style: TextStyle(
color: Colors.black, fontWeight: FontWeight.bold)),
),
);
},
),
);
}
}
// 顯示營養資訊的元件
class NutritionWidget extends StatelessWidget {
final List<Nutrients>? nutrients;
NutritionWidget({this.nutrients});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 86,
width: double.infinity,
child: Center(
child: ListView.builder(
itemCount: nutrients!.length,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return CircleIndicator(
percent: nutrients![index].percent,
nutrient: nutrients![index],
);
},
),
),
);
}
}
修改home.daart
import 'package:flutter/material.dart';
import 'package:gitpod_flutter_quickstart/screens/details.dart';
import 'package:gitpod_flutter_quickstart/utils/data.dart';
// HomePage 是一個無狀態 (Stateless) 小部件,負責顯示食譜的網格視圖。
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 使用 Container 作為網格視圖的背景容器
var body = Container(
color: Theme.of(context).primaryColor, // 設置背景顏色為主題顏色
child: GridView.builder(
shrinkWrap: false, // 允許 GridView 自適應內容大小
itemCount: Data.recipes.length, // 根據食譜數量設置項目數量
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), // 設置兩列網格
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.all(8.0), // 設置每個項目的間距
child: InkWell(
onTap: () {
// 點擊食譜卡片時,導航至詳細頁面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailsPage(
recipe: Data.recipes[index],
)));
},
child: Card(
color: Theme.of(context).accentColor, // 設置卡片背景色
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0), // 設置圓角
),
child: Container(
width: MediaQuery.of(context).size.width / 2, // 設置寬度為螢幕寬度的一半
height: 50, // 設置固定高度
child: Column(
children: <Widget>[
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8.0),
topRight: Radius.circular(8.0),
), // 設置圖片的圓角
child: Hero(
tag: Data.recipes[index].id, // 設置 Hero 動畫標籤
child: FadeInImage(
image:
NetworkImage(Data.recipes[index].imageUrl), // 從網路加載圖片
fit: BoxFit.cover, // 讓圖片填滿空間
placeholder:
AssetImage('assets/images/loading.gif'), // 設置載入中的佔位圖片
),
),
),
),
Padding(
padding: const EdgeInsets.all(4.0), // 設置文字的間距
child: Text(
Data.recipes[index].title, // 顯示食譜標題
style: TextStyle(
color: Theme.of(context).primaryColor, // 設置文字顏色
fontSize: 20, // 設置字體大小
fontWeight: FontWeight.bold), // 設置字體加粗
),
)
],
),
),
),
),
);
}),
);
return Scaffold(
appBar: AppBar(
centerTitle: true, // 設置標題置中
title: Text('Recipes'), // 設置 AppBar 標題
actions: <Widget>[
IconButton(
icon: Icon(Icons.search), // 搜索按鈕
onPressed: () {}, // 點擊事件 (目前未實作)
)
],
),
body: body, // 設置頁面主體內容
);
}
}
留言
發佈留言