Flutterでログイン画面を作成したので、メモしておきます。
状態管理は、公式ドキュメントで利用しているproviderパッケージを利用します。
作成したもの
- ログイン正常の動作
- ログインエラーの動作
レポジトリ
ディレクトリ構成
/path/to/directory/lib
|--main.dart
|--repository
| |--auth_repository.dart
|--ui
| |--index_page.dart
| |--login_model.dart
| |--login_page.dart
パッケージ追加
pubspec.yamlに以下パッケージを追加し、パッケージをインポート(dart pub get
)します。
/pubspec.yaml
dependencies:
flutter:
sdk: flutter
provider: ^5.0.0 # <--- 追加
font_awesome_flutter: ^9.0.0-nullsafety # <--- パスワードマスク用のアイコン利用のため
実装
- 認証レポジトリ追加
実際は、認証API(Firebase Authenticationなど)などをコールしますが、サンプル実装のため、必ず成功(true)を返します。
lib/repository/auth_repository.dart
class AuthRepository {
Future<bool> auth() {
return Future.value(true);
}
}
- 状態クラス追加
ChangeNotifierを継承して、状態クラスを作成します。
id, password, showPassword(=パスワード表示・非表示)などのフィールドと、認証や、パスワード表示切り替えなどの処理を持ちます。
lib/ui/login_model.dart
class LoginModel extends ChangeNotifier {
final AuthRepository repository;
String id = '';
String password = '';
String message = '';
bool showPassword = false; //パスワードを平文で表示する
LoginModel(this.repository);
void setMessage(String value) { //エラーメッセージ設定
message = value;
notifyListeners();
}
void togglePasswordVisible() { //パスワード表示切り替え
showPassword = !showPassword;
notifyListeners();
}
String? emptyValidator(String? value) { //必須入力チェック
if (value == null || value.isEmpty) {
return '入力してください';
}
return null;
}
Future<bool> auth() async {
print("id: $id, password: $password");
var results = await repository.auth();
return results;
}
}
- 画面(Widget)実装
ChangeNotifierProviderを利用して、状態クラスを子供のWidgetで利用できるようにします。
lib/ui/login_page.dart
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LoginModel(
AuthRepository(),
),
child: LoginApp(),
);
}
}
状態クラスは、以下メソッドを通じて利用します。
* context.read
(変更監視なし)
* context.watch
(変更監視あり)
lib/ui/login_page.dart
class LoginApp extends StatelessWidget {
final _formKey = GlobalKey<FormState>(); // 入力フォーム
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: const InputDecoration(
labelText: 'UserId',
hintText: 'ユーザIDを入力してください',
),
validator: context.read<LoginModel>().emptyValidator, // 入力チェック
onSaved: (value) => context.read<LoginModel>().id = value!, // save() 時に同期
),
TextFormField(
obscureText: !context.watch<LoginModel>().showPassword, // パスワード表示状態を監視(watch)
decoration: InputDecoration(
labelText: 'Password',
hintText: 'パスワードを入力してください',
suffixIcon: IconButton(
icon: Icon(context.watch<LoginModel>().showPassword // パスワード表示状態を監視(watch)
? FontAwesomeIcons.solidEye
: FontAwesomeIcons.solidEyeSlash),
onPressed: () =>
context.read<LoginModel>().togglePasswordVisible(), // パスワード表示・非表示をトグルする
),
),
validator: context.read<LoginModel>().emptyValidator, // 入力チェック
onSaved: (value) =>
context.read<LoginModel>().password = value!,
),
Container( // エラー文言表示エリア
margin: EdgeInsets.fromLTRB(0, 16, 0, 8),
child: Text(
context.watch<LoginModel>().message,
style: TextStyle(
fontSize: 16,
color: Colors.red,
),
),
),
Container(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ElevatedButton(
onPressed: () async { // ログインボタンアクション
context.read<LoginModel>().setMessage(''); // エラーメッセージを空に
if (_formKey.currentState!.validate()) { // 入力チェック
_formKey.currentState!.save(); // 入力チェックOK -> フォームの値を同期する
var response =
await context.read<LoginModel>().auth();
print('auth response = $response');
if (response) {
Navigator.push( // 画面遷移
context,
MaterialPageRoute(
builder: (context) => IndexPage(),
),
);
ScaffoldMessenger.of(context).showSnackBar( // SnackBar表示
SnackBar(
content: Text('ログインしました'),
),
);
} else {
context
.read<LoginModel>()
.setMessage('パスワードが誤っています'); // エラーメッセージセット
}
}
},
child: const Text('ログイン'),
),
),
)
],
),
),
),
),
);
}
}
終わりに
状態管理を切り出すことで、Widgetのコードが見通しがよくなりました。
riverpodやflutter_hooksあたりも今後試していきたいと思います。
次回は、今回作成した画面を元に、Widgetのテストを記載したいと思います。