幾乎所有的 APP 都有多個畫面來呈現不同的資訊。比如在我們未來的聊天 APP 中,就得需要好幾個畫面,一個登入畫面、一個註冊畫面、...等。這個章節中,我們會提到如何在這些畫面中進行切換以及利用 InheritedWidget 傳遞信息。
這次的程式碼比較多,範例程式傳送門: https://github.com/ksw2000/ironman-2024/tree/master/flutter-practice/navigation
在開始前我們先設計一個簡單的登入頁面。
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const LoginPage(),
);
}
}
實作 LoginPage
這裡會用到 TextField
元件,這個元件的使用請各位參考官網:TextField class - material library - Dart API
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final idCtrl = TextEditingController();
final pwdCtrl = TextEditingController();
@override
void dispose() {
idCtrl.dispose();
pwdCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Login Page'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: idCtrl,
decoration: const InputDecoration(hintText: '帳號'),
),
const SizedBox(
height: 10,
),
TextField(
controller: pwdCtrl,
obscureText: true, // 密碼輸入框
decoration: const InputDecoration(hintText: '密碼'),
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: () {
debugPrint("pressed");
},
child: const Text('登入'))
],
),
),
),
);
}
}
效果如下:
接著我們可以在設計一個登入後成功的頁面 UserPage
class UserPage extends StatelessWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('User Page'),
),
body: const Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[Text('Hello')],
),
),
),
);
}
}
在實作 LoginPage 時,我們在按鈕的地方只設了個 debugPrint
ElevatedButton(
onPressed: () {
debugPrint("pressed");
},
child: const Text('登入'))
我們可以直接透過這個按鈕來進行畫面切換,方法很簡單,就是呼叫 Navigator.push
Future<T?> push<T extends Object?>(BuildContext context, Route<T> route)
其中第一個 context
的參數,填入我們 build()
時所帶的 context。第二個參數需要一個 Route 物件。Flutter 提供兩個,一個是 Material 風格的 MaterialPageRoute
,另一個是 iOS 風格的 CupertinoPageRoute
。因為我們一開始選用 Material 風格,就 Material 到底吧🤗
ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const UserPage();
}));
},
child: const Text('登入'))
建立完後看一下效果如何
至於要傳送資料的話,我們可以很簡單的將變數傳入 UserPage()
。但是,這樣在畫面很多時,會不易管理,我們將延續昨天教學,以 InheritedWidget
的方式管理狀態。
首先,我們新增一個 User
物件,這個物件用來儲存用戶相關資訊,可以把他當成 Kotlin 中的 data class,或者 C 語言中的 struct。
class User {
const User({required this.name});
final String name;
}
接著,建立一個可以把 LoginPage 和 UserPage 都包住的 InheritedWidget
UserDataLayer 用來存放資料狀態。
class UserDataLayer extends InheritedWidget {
const UserDataLayer(
{super.key,
required this.user,
required this.setUser,
required super.child});
final Function(User user) setUser;
final User? user;
static UserDataLayer of(BuildContext context) {
final res = context.dependOnInheritedWidgetOfExactType<UserDataLayer>();
assert(res != null, 'No UserData found in context');
return res!;
}
@override
bool updateShouldNotify(UserDataLayer oldWidget) {
return oldWidget.user != user;
}
}
要注意的是,UserPage 是在 Navigator.push
後加入路由的,這個部分是由最一開始的 MaterialApp
控制的,因此我們要讓 InheritedWidget
包住 MaterialApp
。由於我們必需更新 user
資料,因此要將 MyApp
從 StatelessWidget
改成 StatefulWidget
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAPPState();
}
class _MyAPPState extends State<MyApp> {
User? user;
@override
Widget build(BuildContext context) {
return UserDataLayer(
user: user,
setUser: (user) {
setState(() {
this.user = user;
});
},
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const LoginPage()),
);
}
}
接著更新 LoginPage 中的按鈕使得 _MyAPPState
中的 user
能更新
ElevatedButton(
onPressed: () {
// 呼叫 setUser,使 user 資料更新
UserDataLayer.of(context).setUser(User(name: idCtrl.text));
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const UserPage();
}));
},
child: const Text('登入'))
最後處理 UserPage 的部分,除了顯示 hello 之外,連同顯示用戶名稱。為了簡化,我們先別管用戶的 id 還是名稱😂
class UserPage extends StatelessWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('User Page'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 從 UserDataLayer 取得 user 資料
Text('Hello ${UserDataLayer.of(context).user?.name}')
],
),
),
),
);
}
}
最終成品:
後記:感覺很多東西都沒教,但希望大家能了解這篇文要講的觀念!有興趣的朋友記得按讚本文並追蹤此系列❤️