iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0
Mobile Development

從零開始以Flutter打造跨平台聊天APP系列 第 20

Day-20 實作(1) Flutter 建立註冊與登入畫面

  • 分享至 

  • xImage
  •  

Generated from Stable Diffusion 3 Medium

第 20 天了,是時候來刻 UI 了,首先,我們的程式一進入後,要先向 Shared preferences 檢查是否已經登入,如果是已登入者,直接渲染主畫面 MePage,若無則渲染登入頁面 LoginPage。在登入畫面中,以一個連結連到註冊頁面 SignUpPage。另外,為了將登入狀態保存好,實作一個 Me class 保存用戶資料以及綁定相關邏輯,狀態的部分則交由 MeDataLayer 進行保存。

參考程式碼:https://github.com/ksw2000/ironman-2024/tree/a2f53698e424af23ac9d6aba699ffd4686dd33b4/whisper

因為是 final project,所以程式碼會分次不同 commit 到 Github 上,參考程式碼僅是某次的版本

MyApp

加入一個 MeDataLayer 來儲存使用者「Me」的相關狀態。為了異步處理判斷初始化面應顯示「登入 LoginPage」亦或是「已登入後的聊天畫面 MePage」我們再插入一層 RoutingLoginOrMe

class _MyAPPState extends State<MyApp> {
  Me? me;

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MeDataLayer(
        user: me,
        setUser: (user) {
          setState(() {
            me = user;
          });
        },
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
            useMaterial3: true,
          ),
          home: const RoutingLoginOrMe(),
        ));
  }
}

RoutingLoginOrMe

我們可以使用一個 FutureBuilder 來檢查保持登入與否,這裡的部分因為尚未實作,所以我們暫時以 delay 代替。

class _RoutingLoginOrMeState extends State<RoutingLoginOrMe> {
  final Future<(bool, Me?)> _checkKeepLogin =
      Future.delayed(const Duration(seconds: 3), () {
    return (false, null);
  });

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: _checkKeepLogin,
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Scaffold(
                appBar: AppBar(
                  backgroundColor: Theme.of(context).colorScheme.inversePrimary,
                  title: const Text('登入 Whisper'),
                ),
                body: Center(
                    child: Column(
                  children: [
                    const Icon(
                      Icons.error_outline,
                      color: Colors.redAccent,
                    ),
                    const SizedBox(
                      height: 20,
                    ),
                    Text("${snapshot.error}")
                  ],
                )));
          } else if (snapshot.hasData) {
            if (snapshot.data!.$1 == false) {
              // 渲染登入畫面
              return const LoginPage();
            }
            return const MePage();
          }
          return Scaffold(
              appBar: AppBar(
                backgroundColor: Theme.of(context).colorScheme.inversePrimary,
                title: const Text('登入 Whisper'),
              ),
              body: const Center(
                child: CircularProgressIndicator(),
              ));
        });
  }
}

LoginPage

LoginPage 中除了渲染帳號密碼等元件外,還需要處理登入的動作!我們把登入者「Me」的資訊以「MeDataLayer」這個 InheritedWidget 做管理。另外,當我們成功能入後我們需要將 WidgetTree 的節點由 LoginPage 轉向 MePage,此時使用的是 Navigator.pushReplacement,此舉能避免使用者按下返回鍵後返回到登入頁面。

class _LoginPageState extends State<LoginPage> {
  final idCtrl = TextEditingController();
  final pwdCtrl = TextEditingController();
  bool _isLogging = false;

  @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('登入 Whisper'),
      ),
      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,
              ),
              _isLogging
                  ? const CircularProgressIndicator()
                  : ElevatedButton(
                      onPressed: () {
                        setState(() {
                          _isLogging = true;
                        });
                        Me.login(idCtrl.text, pwdCtrl.text).then((me) {
                          if (context.mounted) {
                            MeDataLayer.of(context).setUser(me);
                            Navigator.pushReplacement(context,
                                MaterialPageRoute(builder: (context) {
                              return const MePage();
                            }));
                          }
                        }).catchError((err) {
                          if (context.mounted) {
                            ScaffoldMessenger.of(context).showSnackBar(
                                SnackBar(content: Text("登入失敗:$err")));
                          }
                        }).whenComplete(() {
                          setState(() {
                            _isLogging = false;
                          });
                        });
                      },
                      child: const Text('登入')),
              const SizedBox(
                height: 20,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text("沒有帳戶嗎?"),
                  TextButton(
                      onPressed: () {
                        Navigator.push(context,
                            MaterialPageRoute(builder: (context) {
                          return const SignUpPage();
                        }));
                      },
                      child: const Text("馬上註冊"))
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

登入畫面

MePage

MePage 的部分我們一樣放個登出鈕控制登出,該按鈕未來會串接後端伺服器以及清空 Shared Preferences 內的登入資訊。

class _MePageState extends State<MePage> {
  bool _isLoggingOut = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('聊天主畫面'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Hello ${MeDataLayer.of(context).user?.name}'),
              const SizedBox(
                height: 20,
              ),
              _isLoggingOut
                  ? const CircularProgressIndicator()
                  : OutlinedButton(
                      onPressed: () async {
                        setState(() {
                          _isLoggingOut = true;
                        });
                        await Me.logout();
                        if (context.mounted) {
                          MeDataLayer.of(context).setUser(null);
                          Navigator.pushReplacement(context,
                              MaterialPageRoute(builder: (context) {
                            return const LoginPage();
                          }));
                        }
                        setState(() {
                          _isLoggingOut = false;
                        });
                      },
                      child: const Text("登出"))
            ],
          ),
        ),
      ),
    );
  }
}

Me Page

Me

class Me {
  const Me(
      {required this.uid,
      required this.id,
      required this.name,
      required this.token,
      required this.profile});
  final int uid;
  final String id;
  final String name;
  final String token;
  final String? profile;

  static Future<Me> login(String id, String password) async {
    // TODO 向伺服器發送帳密資訊以請求登入
    await Future.delayed(const Duration(seconds: 3));
    return Future.value(const Me(
        uid: 0, id: "kahou0522", name: "日野下花帆", token: "", profile: null));
  }

  static Future<void> logout() async {
    // TODO 清除 shared preference
    await Future.delayed(const Duration(seconds: 3));
    return;
  }
}

class MeDataLayer extends InheritedWidget {
  const MeDataLayer(
      {super.key,
      required this.user,
      required this.setUser,
      required super.child});

  final Function(Me? user) setUser;
  final Me? user;

  static MeDataLayer of(BuildContext context) {
    final res = context.dependOnInheritedWidgetOfExactType<MeDataLayer>();
    assert(res != null, 'No UserData found in context');
    return res!;
  }

  @override
  bool updateShouldNotify(MeDataLayer oldWidget) {
    return oldWidget.user != user;
  }
}

SignUpPage

註冊畫面主要都是 UI 的部分,程式碼很長這裡就不放了,這個畫面我們後續會再一併處理!

註冊頁面


上一篇
Day-19 在 Flutter 中使用 Widget Test 測試畫面
下一篇
Day-21 實作(2) Flutter 利用 ListView.buider 實現載入更多
系列文
從零開始以Flutter打造跨平台聊天APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言