#Flutter 04: Final Project

Wen
海大 SwiftUI iOS / Flutter App 程式設計
15 min readJun 12, 2024

Flutter 這門課要在此畫下句點了,同時,也是時候要驗收期末專案惹 ~ !!!

那話不多說,先來看看我做了甚麼吧 !

下面是我的成果畫面展現 :

接著,下面是我的成果展示影片 !!!
1. https://youtu.be/s2ejvLm1DBs

2. https://youtu.be/ZcDkft2hjnA

3. https://youtu.be/IpglCtN1eYk

下面是我的Github 網址 :

然後,我要開始介紹我的code了!!!
其實我起初花了一個禮拜在想到底要做甚麼主題,後來是因為某天在看Netflix ,我覺得 Netflix 的版面是我喜歡的樣式,所以決定要做 Netflix App.

以下是我用到的技術 (我只放上課老師沒教的) :

註冊 & 登入 (我是使用 Firebase)

  
// 註冊

TextEditingController email = TextEditingController();
TextEditingController password = TextEditingController();

...

SizedBox(
...
child: TextField(
controller: email,
style: const TextStyle(fontSize: 16, color: Colors.white),
decoration: InputDecoration(
labelText: 'Email or phone number',
...
),
),
),
const SizedBox(height: 12),
SizedBox(
...
child: TextField(
style: const TextStyle(fontSize: 16, color: Colors.white),
controller: password,
obscureText: _hidePassword,
...
),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: (() => signUp()),
...
child: const Text(
'Sign Up',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
),
  // 註冊

signUp() async {
try {
UserCredential userCredential =
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: email.text,
password: password.text,
);

await userCredential.user!.sendEmailVerification();

...
}
  // 登入

bool _hidePassword = true;
bool _isEmailValid = false;
bool _isPasswordValid = false;
TextEditingController email = TextEditingController();
TextEditingController password = TextEditingController();

signIn() async {
try {
UserCredential userCredential =
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email.text,
password: password.text,
);

User? user = userCredential.user;
if (user != null && user.emailVerified) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const BottomNavigatorBar(),
),
);
} else {
...
}
}
  // 登入

SizedBox(
...
child: TextField(
controller: email,
onChanged: (value) {
setState(() {
_isEmailValid = value.length >= 5;
});
},
...
),
),
const SizedBox(height: 12),
SizedBox(
...
child: TextField(
style: const TextStyle(fontSize: 16, color: Colors.white),
controller: password,
onChanged: (value) {
setState(() {
_isPasswordValid = value.length >= 4;
});
},
obscureText: _hidePassword,
decoration: InputDecoration(
labelText: 'Password',
...
),
),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _isEmailValid && _isPasswordValid ? signIn : null,
...
child: Text(
'Sign In',
style: TextStyle(
color: (_isEmailValid && _isPasswordValid)
? Colors.white
: Colors.grey,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
),

隱藏 & 顯示密碼

  // 隱藏跟顯示密碼

bool _hidePassword = true;


TextField(
...
controller: password,
onChanged: (value) {
setState(() {
_isPasswordValid = value.length >= 4;
});
},
obscureText: _hidePassword,
...
suffixIcon: IconButton(
icon: Icon(
_hidePassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.white,
),
onPressed: () {
setState(() {
_hidePassword = !_hidePassword;
});
},
),
),
),

動畫 (我使用的是 Lottie)

import 'package:lottie/lottie.dart';
...

@override
Widget build(BuildContext context) {
return Center(
child: Lottie.asset("assets/netflix.json"),
);
}

CarouselSlider.builder

import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_final_project/common/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';

class Custom extends StatelessWidget {
final TopRated data;
const Custom({super.key, required this.data});

@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return SizedBox(
width: size.width,
height: (size.height * 0.33 < 300) ? 300 : size.height * 0.33,
child: CarouselSlider.builder(
itemCount: data.results.length,
itemBuilder: (BuildContext context, int index, int realIndex) {
var url = data.results[index].backdropPath.toString();

return GestureDetector(
child: Column(
children: [
...
CachedNetworkImage(
imageUrl: "$imageurl$url",
),
...
Text(
data.results[index].name,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
options: CarouselOptions(
height: (size.height * 0.33 < 300) ? 300 : size.height * 0.33,
aspectRatio: 16 / 9,
reverse: false,
autoPlay: true,
autoPlayInterval: const Duration(seconds: 3),
autoPlayAnimationDuration: const Duration(milliseconds: 800),
enlargeCenterPage: true,
scrollDirection: Axis.horizontal,
initialPage: 0,
),
),
);
}
}

Futurebuilder

import 'package:flutter/material.dart';
import 'package:flutter_final_project/common/utils.dart';
import 'package:flutter_final_project/models/upcoming.dart';
import 'package:flutter_final_project/screens/movie_detailed_screen.dart';

class MovieCard extends StatelessWidget {
final Future<Upcoming> future;
...

const MovieCard(
{super.key, required this.future, required this.headLineText});

@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
if (snapshot.hasData) {
var data = snapshot.data?.results;
...
}
}
);
}
}

ShowDialog


showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
...
title: const Text(
'Email Not Verified',
textAlign: TextAlign.center,
...
),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.65,
height: MediaQuery.of(context).size.height * 0.06,
child: const Text(
'Please verify your email address before logging in.',
...
),
),
actions: [
Center(
child: Column(
children: [
...
TextButton(
child: const Text(
'OK',
...
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
),
],
);
},
);
}
} on FirebaseAuthException catch (e) {
if (e.code == 'user-not-found') {
} else if (e.code == 'wrong-password') {
setState(() {});
} else if (e.code == 'invalid-email') {
} else {}
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
...
title: const Text(
'Incorrect password',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.65,
height: MediaQuery.of(context).size.height * 0.08,
child: Text(
'Sorry, that password is incorrect for the account with the email address ${email.text}.',
...
),
),
actions: [
Center(
child: Column(
children: [
...
TextButton(
child: const Text(
'Try Again',
...
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
),
],
);
},
).catchError(
(e) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('錯誤'),
content: const Text('註冊時發生錯誤,請稍後再試。'),
actions: [
TextButton(
child: const Text('確定'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
},
);

RefreshIndicator

RefreshIndicator(
onRefresh: () async {
await Future.delayed(
const Duration(seconds: 1),
);
_refreshData();
},
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 5),
itemCount: 3,
itemBuilder: (context, index) {
if (index == 0) {
return FutureBuilder(
future: topratedmovies,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Custom(data: snapshot.data!);
} else {
return const SizedBox.shrink();
}
},
);
}
...
},
),
),

心得 :

很開心能夠修 Flutter 這門課,我想未來我一定會繼續專研 Flutter 的 !!!

--

--