iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0

Generated from Stable Diffusion 3 Medium

幾乎所有的 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('登入'))
            ],
          ),
        ),
      ),
    );
  }
}

效果如下:

demo of login page

接著我們可以在設計一個登入後成功的頁面 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')],
          ),
        ),
      ),
    );
  }
}

demo of user page

使用 Navigator.push 進行路由

在實作 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('登入'))

建立完後看一下效果如何

demo of navigation push and pop

至於要傳送資料的話,我們可以很簡單的將變數傳入 UserPage() 。但是,這樣在畫面很多時,會不易管理,我們將延續昨天教學,以 InheritedWidget 的方式管理狀態。

首先,我們新增一個 User 物件,這個物件用來儲存用戶相關資訊,可以把他當成 Kotlin 中的 data class,或者 C 語言中的 struct。

class User {
  const User({required this.name});
  final String name;
}

接著,建立一個可以把 LoginPageUserPage 都包住的 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 資料,因此要將 MyAppStatelessWidget 改成 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}')
            ],
          ),
        ),
      ),
    );
  }
}

最終成品:

final demo of this project

後記:感覺很多東西都沒教,但希望大家能了解這篇文要講的觀念!有興趣的朋友記得按讚本文並追蹤此系列❤️


上一篇
Day-9 在 Flutter 中使用 InheritedWidget 進行狀態管理
下一篇
Day-11 在 Flutter 中使用 FutureBuilder 進行狀態管理
系列文
從零開始以Flutter打造跨平台聊天APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言