Flutter

블로그 프로젝트 - 로그인 폼 상태 관리

whs5758 2025. 9. 1. 09:11
LoginForm
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../../../_core/constants/size.dart';
import '../../../../widgets/custom_auth_text_form_field.dart';
import '../../../../widgets/custom_elavated_button.dart';
import '../../../../widgets/custom_text_button.dart';

class LoginForm extends ConsumerWidget {
  const LoginForm({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Form(
      child: Column(
        children: [
          CustomAuthTextFormField(
            title: "Username",
          ),
          const SizedBox(height: mediumGap),
          CustomAuthTextFormField(
            title: "Password",
            obscureText: true,
          ),
          const SizedBox(height: largeGap),
          CustomElevatedButton(
            text: "로그인",
            click: () {
              Navigator.popAndPushNamed(context, "/post/list");
            },
          ),
          CustomTextButton(
            text: "회원가입 페이지로 이동",
            click: () {
              Navigator.pushNamed(context, "/join");
            },
          ),
        ],
      ),
    );
  }
}

 

LoginBody
import 'package:flutter/material.dart';
import 'package:flutter_blog/_core/constants/size.dart';
import 'package:flutter_blog/ui/pages/auth/login_page/widgets/login_form.dart';
import 'package:flutter_blog/ui/widgets/custom_auth_text_form_field.dart';
import 'package:flutter_blog/ui/widgets/custom_elavated_button.dart';
import 'package:flutter_blog/ui/widgets/custom_logo.dart';
import 'package:flutter_blog/ui/widgets/custom_text_button.dart';

class LoginBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: ListView(
        children: [
          const CustomLogo("Blog"),
          LoginForm(),
        ],
      ),
    );
  }
}

 

login_form_notifier.dart

//
// 창고 데이터 설계
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';

import '../../_core/utils/validator_util.dart';

class LoginModel {
  // 각 필드 사용자 입력 값 관리 변수
  final String username;
  final String password;

  // 각 입력 필드 검증 에러 메세지 관리 변수
  final String usernameError;
  final String passwordError;

  LoginModel(
    this.username,
    this.password,
    this.usernameError,
    this.passwordError,
  );

  LoginModel copyWith(
      {String? username,
      String? password,
      String? usernameError,
      String? passwordError}) {
    return LoginModel(
      username ?? this.username,
      password ?? this.password,
      usernameError ?? this.usernameError,
      passwordError ?? this.passwordError,
    );
  }

  // 디버그 용
  @override
  String toString() {
    return 'LoginModel{username: $username, password: $password, usernameError: $usernameError, passwordError: $passwordError}';
  }
} // end of LoginModel class

// 창고 메뉴얼 설계
class LoginFormNotifier extends Notifier<LoginModel> {
  @override
  LoginModel build() {
    return LoginModel("", "", "", "");
  }

  // 사용자명 입력시 : 즉시 검증 + 상태 업데이트 기능 구현
  void username(String username) {
    final String error = validateUsername(username);
    Logger().d(error);

    state = state.copyWith(
      username: username,
      usernameError: error,
    );
  }

  // 비밀번호 입력시: 즉시 검증 + 상태 업데이트 기능 구현
  void password(String password) {
    String passwordError = validatePassword(password);
    if (passwordError.trim().isEmpty) {
      Logger().d(password);
    } else {
      Logger().d(passwordError);
    }
    state = state.copyWith(password: password, passwordError: passwordError);
  }

  bool validate() {
    final usernameError = validateUsername(state.username);
    final passwordError = validatePassword(state.password);
    return usernameError.isEmpty && passwordError.isEmpty;
  }
} // end of LoginFormNotifier

// 실제 창고를 메모리에 올리자 - 전역 변수로 관리
final loginFormProvider =
    NotifierProvider<LoginFormNotifier, LoginModel>(() => LoginFormNotifier());
// loginFormProvider --> LoginFormNotifier() --> LoginModel()
// ref.read(loginFormProvider);    --> LoginModel()
// ref.read(loginFormProvider.notifier); --> LoginFormNotifier()

 

LoginForm + 상태관리 연결 처리
import 'package:flutter/material.dart';
import 'package:flutter_blog/providers/form/login_form_notifier.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../../../_core/constants/size.dart';
import '../../../../widgets/custom_auth_text_form_field.dart';
import '../../../../widgets/custom_elavated_button.dart';
import '../../../../widgets/custom_text_button.dart';

class LoginForm extends ConsumerWidget {
  const LoginForm({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    LoginModel loginModel = ref.watch(loginFormProvider);
    LoginFormNotifier loginFormNotifier = ref.read(loginFormProvider.notifier);

    return Form(
      child: Column(
        children: [
          CustomAuthTextFormField(
            title: "Username",
            errorText: loginModel.usernameError,
            onChanged: (value) {
              loginFormNotifier.username(value);
            },
          ),
          const SizedBox(height: mediumGap),
          CustomAuthTextFormField(
            title: "Password",
            errorText: loginModel.passwordError,
            onChanged: (value) {
              loginFormNotifier.password(value);
            },
            obscureText: true,
          ),
          const SizedBox(height: largeGap),
          CustomElevatedButton(
            text: "로그인",
            click: () {
              // 검증
              bool isValid = loginFormNotifier.validate();
              if (isValid) {
                // 서버로 로그인 통신 요청
                // 서버로 로그인 요청 성공 한다면 페이지 이동 ...
                Navigator.popAndPushNamed(context, "/post/list");
              } else {
                ScaffoldMessenger.of(context)
                    .showSnackBar(SnackBar(content: Text("유효성 검사 실패")));
              }
            },
          ),
          CustomTextButton(
            text: "회원가입 페이지로 이동",
            click: () {
              Navigator.pushNamed(context, "/join");
            },
          ),
        ],
      ),
    );
  }
}