5
7

More than 1 year has passed since last update.

flutter_riverpodでアプリをMVVMにする

Last updated at Posted at 2021-12-13

はじめに

前回のログインアプリをflutter_riverpodを使用して、MVVM風にリファクタしてみました。
前回作成したアプリ(https://qiita.com/yufuku/items/24dac97e6052b2571386)

今回のゴールはview(ui)側で実装していた処理をviewModelに移行して、view側ではstateを持たないようにすることです。

最初のフォルダ構成とコードは下記になります。

└──lib
   ├── main.dart
   ├── requester
   │   └── requester.dart
   └── ui
       ├── hello.dart
       └── login.dart
lib/main.dart
import 'package:flutter/material.dart';
import 'package:authentication_frontend/ui/login.dart';

void main() {
  runApp(const LoginApp());
}
lib/ui/login.dart
import 'package:authentication_frontend/ui/hello.dart';
import 'package:authentication_frontend/requester/requester.dart';
import 'package:flutter/material.dart';

class LoginApp extends StatelessWidget {
  const LoginApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ログイン画面',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const LoginPage(title: 'Login Page'),
    );
  }
}

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _userNameTextController = TextEditingController();
  final _passwordNameTextController = TextEditingController();
  final FocusNode _userNamefocusNode = FocusNode();
  final FocusNode _passwordNameFocusNode = FocusNode();
  var _errorMessage = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_errorMessage, style: const TextStyle(color: Colors.red)),
            TextField(
              controller: _userNameTextController,
              decoration: const InputDecoration(
                filled: true,
                labelText: 'Username',
              ),
              focusNode: _userNamefocusNode,
            ),
            const SizedBox(height: 12.0),
            TextField(
              controller: _passwordNameTextController,
              decoration: const InputDecoration(
                filled: true,
                labelText: 'Password',
              ),
              focusNode: _passwordNameFocusNode,
              obscureText: true,
            ),
            ButtonBar(
              children: <Widget>[
                TextButton(
                  child: const Text('CANCEL'),
                  onPressed: () {
                    _userNameTextController.clear();
                    _passwordNameTextController.clear();
                  },
                ),
                ElevatedButton(
                    child: const Text('LOGIN'),
                    onPressed: () {
                      Requester()
                          .loginRequester(_userNameTextController.text,
                              _passwordNameTextController.text)
                          .then((_) {
                        setState(() {
                          _errorMessage = "";
                        });
                        Navigator.push(context,
                            MaterialPageRoute(builder: (context) => Hello()));
                      }).onError((error, stackTrace) {
                        debugPrint(error.toString());
                        _userNameTextController.clear();
                        _passwordNameTextController.clear();
                        setState(() {
                          _errorMessage = "ログインに失敗しました。ユーザー名かパスワードが間違っています。";
                        });
                      });
                    }),
                ElevatedButton(
                    child: const Text('SIGNUP'),
                    onPressed: () {
                      Requester()
                          .signUpRequester(_userNameTextController.text,
                              _passwordNameTextController.text)
                          .then((_) {
                        setState(() {
                          _errorMessage = "";
                        });
                        Navigator.push(context,
                            MaterialPageRoute(builder: (context) => Hello()));
                      }).onError((error, stackTrace) {
                        debugPrint(error.toString());
                        _userNameTextController.clear();
                        _passwordNameTextController.clear();
                        setState(() {
                          _errorMessage = "ユーザーの作成に失敗しました。既に登録済みのユーザーです。";
                        });
                      });
                    }),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

lib/ui/hello.dart
import 'package:authentication_frontend/requester/requester.dart';
import 'package:flutter/material.dart';

class Hello extends StatefulWidget {
  const Hello({Key? key}) : super(key: key);

  @override
  _HelloState createState() => _HelloState();
}

class _HelloState extends State<Hello> {
  var _message = "";

  @override
  void initState() {
    super.initState();
    Requester().helloRequester().then((value) {
      setState(() {
        _message = value;
      });
    }).onError((error, stackTrace) {
      showDialog(
          context: context,
          builder: (_) {
            return AlertDialog(
              title: Text("認証に失敗しました。再ログインをお願いします。"),
              actions: [
                TextButton(
                    onPressed: () => Navigator.pop(context), child: Text("OK")),
              ],
            );
          }).then((_) {
        Navigator.pop(context);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Hello"),
        centerTitle: true,
        automaticallyImplyLeading: false,
        actions: [
          TextButton(
            onPressed: () {
              Requester().logoutRequester().then((_) {
                Navigator.pop(context);
              });
            },
            child: const Text('LOGOUT',
                style: TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                    fontSize: 16.0)),
          ),
        ],
      ),
      body: Padding(
        padding: EdgeInsets.only(top: 100),
        child: Center(
          child: Column(
            children: [Text(_message, style: TextStyle(fontSize: 50.0))],
          ),
        ),
      ),
    );
  }
}

今回は上記のファイルを変更していきます。
また、今回は変更の対象ではないですが、apiのリクエスト部分の処理はrequester.dartにまとめています。

lib/requester/requester.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class TokenExipireException implements Exception {
  final String message;
  const TokenExipireException(this.message);

  @override
  String toString() => message;
}

class Requester {
  String uri = defaultTargetPlatform == TargetPlatform.android
      ? 'http://10.0.2.2:8080/v1/'
      : 'http://127.0.0.1:8080/v1/';

  Map<String, String> headers = {
    "Content-Type": "application/json",
  };
  final storage = new FlutterSecureStorage();

  Requester() {}

  Future<void> loginRequester(String name, String password) async {
    var loginUri = uri + "auth/login";

    var request = AuthRequest(name: name, password: password);

    final response = await http.post(Uri.parse(loginUri),
        body: json.encode(request.toJson()), headers: headers);

    if (response.statusCode == 200) {
      Map<String, dynamic> decoded = json.decode(response.body);
      var loginResponse = AuthResponse.fromJson(decoded);
      debugPrint(loginResponse.accessToken);
      await storage.write(key: "accessToken", value: loginResponse.accessToken);
      await storage.write(key: "refreshToken", value: loginResponse.refreshToken);
    } else {
      throw Exception("Login Error");
    }
  }

  Future<void> signUpRequester(String name, String password) async {
    var signUpUri = uri + "auth/signup";

    var request = AuthRequest(name: name, password: password);

    final response = await http.post(Uri.parse(signUpUri),
        body: json.encode(request.toJson()), headers: headers);

    if (response.statusCode == 200) {
      Map<String, dynamic> decoded = json.decode(response.body);
      var signUpResponse = AuthResponse.fromJson(decoded);
      await storage.write(
          key: "accessToken", value: signUpResponse.accessToken);
      await storage.write(
          key: "refreshToken", value: signUpResponse.refreshToken);
      debugPrint(signUpResponse.accessToken);
    } else {
      throw Exception("Sign UP Error");
    }
  }

  Future<String> helloRequester() async {
    var helloUri = uri + "hello";
    var accessToken = await storage.read(key: "accessToken");
    headers["Authorization"] = accessToken ?? "";

    debugPrint("send helloRequester");
    final response = await http.get(Uri.parse(helloUri), headers: headers);

    if (response.statusCode == 200) {
      Map<String, dynamic> decoded = json.decode(response.body);
      var helloResponse = HelloResponse.fromJson(decoded);
      return helloResponse.message;
    } else if (response.statusCode == 401 || response.statusCode == 404) {
      debugPrint("send refreshTokenRequester");
      await refreshTokenRequester();
      var value = await helloRequester();
      return value;
    } else {
      throw Exception("Hello Error");
    }
  }

  Future<void> refreshTokenRequester() async {
    var refreshTokenUri = uri + "auth/refresh_token";
    var refreshToken = await storage.read(key: "refreshToken");

    var request = RefreshTokenRequest(refreshToken: refreshToken ?? "");

    final response = await http.post(Uri.parse(refreshTokenUri),
        body: json.encode(request.toJson()), headers: headers);

    if (response.statusCode == 200) {
      Map<String, dynamic> decoded = json.decode(response.body);
      var refreshTokenResponse = AuthResponse.fromJson(decoded);
      await storage.write(
          key: "accessToken", value: refreshTokenResponse.accessToken);
      await storage.write(
          key: "refreshToken", value: refreshTokenResponse.refreshToken);
      debugPrint(refreshTokenResponse.accessToken);
    } else {
      throw Exception("Token Refresh Error");
    }
  }

  Future<void> logoutRequester() async {
    var logoutUri = uri + "auth/logout";
    var accessToken = await storage.read(key: "accessToken");
    headers["Authorization"] = accessToken ?? "";

    final response = await http.post(Uri.parse(logoutUri), headers: headers);

    if (response.statusCode == 201) {
      await storage.delete(key: "accessToken");
    }
  }


}

class AuthResponse {
  final String accessToken;
  final String refreshToken;

  AuthResponse.fromJson(Map<String, dynamic> json)
      : accessToken = json['access_token'],
        refreshToken = json['refresh_token'];
}

class AuthRequest {
  final String name;
  final String password;

  AuthRequest({
    this.name = "",
    this.password = "",
  });

  Map<String, dynamic> toJson() => {
        'password': password,
        'user_name': name,
      };
}

class HelloResponse {
  final message;

  HelloResponse.fromJson(Map<String, dynamic> json) : message = json['message'];
}

class RefreshTokenRequest {
  final String refreshToken;

  RefreshTokenRequest({
    this.refreshToken = "",
  });

  Map<String, dynamic> toJson() => {
        'refresh_token': refreshToken,
      };
}

環境

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.5.3, on macOS 11.2.3 20D91 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio (version 2020.3)
[✓] VS Code (version 1.62.3)
dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.4
  flutter_secure_storage: ^5.0.2
  flutter_riverpod: ^1.0.2

実装

下記の手順で、uiからstateと処理のロジックを分離していきます。
・flutter_riverpodの導入
・ProvierScopeとConsumerWighetの適用
・login.dartの変更
・hello.dartの変更

最終的なフォルダ構成は下記になります。

└─ lib
   ├── main.dart
   ├── requester
   │   └── requester.dart
   └── ui
       ├── hello
       │   ├── hello.dart
       │   └── hello_view_model.dart
       └── login
           ├── login.dart
           └── login_view_model.dart

flutter_riverpodの導入

まずは、flutter_riverpodをインストールします。
https://pub.dev/packages/flutter_riverpod/install

flutter pub add flutter_riverpod

provierstateの導入とConsumerWighetの適用

flutter_riverpodを使用するためにrunApp時にProviderScopeを適用します。

lib/main.dart
void main() {
  runApp(ProviderScope(child: const LoginApp()));
}

次に、それぞれのuiファイルのStatefulWidgetをConsumerWidgetに変更し、Stateをextendsしていたクラスでの処理を全てConsumerWidgetに移します。
Stateを管理するメソッドは(setStateやinitStateなど)使用できないので、削除します。
最後に、buildの引数にWighetRefを追加します。
uiに対して、これらの変更を加えたコードが下記になります。

lib/ui/login/login.dart
class LoginApp extends StatelessWidget {
  const LoginApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ログイン画面',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: LoginPage(title: 'Login Page'),
    );
  }
}

class LoginPage extends ConsumerWidget {
  LoginPage({Key? key, required this.title}) : super(key: key);

  final String title;

  final _userNameTextController = TextEditingController();
  final _passwordNameTextController = TextEditingController();
  final FocusNode _userNamefocusNode = FocusNode();
  final FocusNode _passwordNameFocusNode = FocusNode();
  var _errorMessage = "";

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_errorMessage, style: const TextStyle(color: Colors.red)),
            TextField(
              controller: _userNameTextController,
              decoration: const InputDecoration(
                filled: true,
                labelText: 'Username',
              ),
              focusNode: _userNamefocusNode,
            ),
            const SizedBox(height: 12.0),
            TextField(
              controller: _passwordNameTextController,
              decoration: const InputDecoration(
                filled: true,
                labelText: 'Password',
              ),
              focusNode: _passwordNameFocusNode,
              obscureText: true,
            ),
            ButtonBar(
              children: <Widget>[
                TextButton(
                  child: const Text('CANCEL'),
                  onPressed: () {
                    _userNameTextController.clear();
                    _passwordNameTextController.clear();
                  },
                ),
                ElevatedButton(
                    child: const Text('LOGIN'),
                    onPressed: () {
                      Requester()
                          .loginRequester(_userNameTextController.text,
                              _passwordNameTextController.text)
                          .then((_) {
                        Navigator.push(context,
                            MaterialPageRoute(builder: (context) => Hello()));
                      }).onError((error, stackTrace) {
                        debugPrint(error.toString());
                        _userNameTextController.clear();
                        _passwordNameTextController.clear();
                      });
                    }),
                ElevatedButton(
                    child: const Text('SIGNUP'),
                    onPressed: () {
                      Requester()
                          .signUpRequester(_userNameTextController.text,
                              _passwordNameTextController.text)
                          .then((_) {
                        Navigator.push(context,
                            MaterialPageRoute(builder: (context) => Hello()));
                      }).onError((error, stackTrace) {
                        debugPrint(error.toString());
                        _userNameTextController.clear();
                        _passwordNameTextController.clear();
                      });
                    }),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
lib/ui/hello/hello.dart
class Hello extends ConsumerWidget {
  Hello({Key? key}) : super(key: key);

  var _message = "";

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Hello"),
        centerTitle: true,
        automaticallyImplyLeading: false,
        actions: [
          TextButton(
            onPressed: () {
              Requester().logoutRequester().then((_) {
                Navigator.pop(context);
              });
            },
            child: const Text('LOGOUT',
                style: TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                    fontSize: 16.0)),
          ),
        ],
      ),
      body: Padding(
        padding: EdgeInsets.only(top: 100),
        child: Center(
          child: Column(
            children: [Text(_message, style: TextStyle(fontSize: 50.0))],
          ),
        ),
      ),
    );
  }
}

login.dartの変更

ここから、各uiのState管理やロジック部分をviewModelに移します。
まずは、login.dartをlogin_view_model.dartに切り出していきます。
login画面ではログイン処理に失敗した際にエラーメッセージ(errorMessage)を表示しています。
まず、これをlogin_view_model.dartに移します。

/lib/ui/login/login_view_model.dart
StateProvider<String> errorMessageProvider = StateProvider((ref) => '');

次に、errorMessageProviderの変更をui側で検知できるように、ConsumerWighet(LoginPage)のbuild内でwatchしておく。

lib/ui/login/login.dart
class LoginPage extends ConsumerWidget {
・・・
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final errorMessage = ref.watch(errorMessageProvider);
・・・

ref.watch(errorMessageProvider)を追加しておくことで、errorMessageProviderが変更すると、ui側に検知されるようになります。

次に、ログインとサインアップのapiリクエストもviewModelを経由するように変更します。

/lib/ui/login/login_view_model.dart
StateProvider<String> errorMessageProvider = StateProvider((ref) => '');

Future<void> loginRequest(WidgetRef ref, String name, String password) async {
  return Requester()
      .loginRequester(name, password)
      .onError((error, stackTrace) {
    ref.watch(errorMessageProvider.notifier).state = "ログインに失敗しました";
    throw Exception(error.toString());
  });
}

Future<void> signUpRequest(WidgetRef ref, String name, String password) async {
  return Requester()
      .signUpRequester(name, password)
      .onError((error, stackTrace) {
    ref.watch(errorMessageProvider.notifier).state = "サインアップに失敗しました";
    throw Exception(error.toString());
  });
}

それぞれの、request処理はui側から、ユーザー名とパスワードをもらってRequesterの処理を呼び出すだけです。
エラーになった際には、errorMessageProviderの値をref.watch(errorMessageProvider.notifier).stateで値を書き換えます。
こうすることで、ui側で変更が検知されてuiの更新が行われます。
最後に、このviewModelのrequest処理をui側で呼び出します。

ui部分の最終的なコードは下記になります。

lib/ui/login/login.dart
class LoginPage extends ConsumerWidget {
  LoginPage({Key? key, required this.title}) : super(key: key);

  final String title;

  final _userNameTextController = TextEditingController();
  final _passwordNameTextController = TextEditingController();
  final FocusNode _userNamefocusNode = FocusNode();
  final FocusNode _passwordNameFocusNode = FocusNode();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final errorMessage = ref.watch(errorMessageProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(errorMessage, style: const TextStyle(color: Colors.red)),
            TextField(
              controller: _userNameTextController,
              decoration: const InputDecoration(
                filled: true,
                labelText: 'Username',
              ),
              focusNode: _userNamefocusNode,
            ),
            const SizedBox(height: 12.0),
            TextField(
              controller: _passwordNameTextController,
              decoration: const InputDecoration(
                filled: true,
                labelText: 'Password',
              ),
              focusNode: _passwordNameFocusNode,
              obscureText: true,
            ),
            ButtonBar(
              children: <Widget>[
                TextButton(
                  child: const Text('CANCEL'),
                  onPressed: () {
                    _userNameTextController.clear();
                    _passwordNameTextController.clear();
                  },
                ),
                ElevatedButton(
                    child: const Text('LOGIN'),
                    onPressed: () {
                      loginRequest(ref, _userNameTextController.text,
                              _passwordNameTextController.text)
                          .then((_) {
                        Navigator.push(context,
                            MaterialPageRoute(builder: (context) => Hello()));
                      }).onError((error, stackTrace) {
                        _userNameTextController.clear();
                        _passwordNameTextController.clear();
                      });
                    }),
                ElevatedButton(
                    child: const Text('SIGNUP'),
                    onPressed: () {
                      signUpRequest(ref, _userNameTextController.text,
                              _passwordNameTextController.text)
                          .then((_) {
                        Navigator.push(context,
                            MaterialPageRoute(builder: (context) => Hello()));
                      }).onError((error, stackTrace) {
                        _userNameTextController.clear();
                        _passwordNameTextController.clear();
                      });
                    }),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

request内でerrorMessageProviderの更新をui側に検知するために、requestを呼び出す際に、ui側のWidgetRefを渡しています。

ログインページのエラー時の挙動はこのようになります。
ezgif.com-gif-maker (3).gif

hello.dartの変更

次に、hello.dartのロジックをviewModelを移行させます。
まず、この画面では画面が描画せれたタイミングでapiにリクエストし、返ってきた値を表示していました。
なので、viewModelでapiリクエストを実行しレスポンスが返ってきたタイミングでuiに通知してあげます。
それを実現するために、FutureProviderを使用します。

lib/ui/hello/hello_view_model.dart
FutureProvider<String> helloRequestProvider =
    FutureProvider((ref) async {
  var message = await Requester().helloRequester();

  return message;
});

この、helloRequestProviderをCosumerWighetのbuildでwatchすることで画面が描画されるタイミングでhelloRequestProvider内の処理が呼び出されます。
そして、helloRequestProviderの処理が完了した時のui側の処理をwhenで書きます。

lib/ui/hello/hello.dart
class Hello extends ConsumerWidget {
  Hello({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final messageProvider = ref.watch(helloRequestProvider);
・・・
    body: Padding(
        padding: EdgeInsets.only(top: 100),
        // messageProviderでのapiリクエスト完了後の処理
        child: messageProvider.when(
          data: (message) => Center(
            child: Column(
              children: [Text(message, style: TextStyle(fontSize: 50.0))],
            ),
          ),
          error: (error, stackTrace) {
              showDialog(
                  context: context,
                  builder: (_) {
                    return AlertDialog(
                      title: Text("認証に失敗しました。再ログインをお願いします。"),
                      actions: [
                        TextButton(
                            onPressed: () => Navigator.pop(context),
                            child: Text("OK")),
                      ],
                    );
                  }).then((_) {
                Navigator.pop(context);
              });
          },
          loading: () => AspectRatio(
            aspectRatio: 0.01,
            child: const CircularProgressIndicator(),
          ),

whenの処理では、リクエスト成功時はレスポンスのメッセージを画面に描画しています。
エラー時にはダイアログを出して、ログイン画面に戻していますNavigator.pop(context)
しかし、これだとshowDialogでエラーになってしまいます。

エラーメッセージ
This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets.

これは、Wighetのbuild時に状態変更をできないという内容です。
これを回避するために、showDialogをFutureで括ります。

lib/ui/hello/hello.dart
Future(() {
  showDialog(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: Text("認証に失敗しました。再ログインをお願いします。"),
          actions: [
            TextButton(
                onPressed: () => Navigator.pop(context),
                child: Text("OK")),
          ],
        );
      }).then((_) {
    Navigator.pop(context);
  });
});

これで、画面の描画時にリクエストを送りレスポンスの内容によって、uiを変更することができました。

しかし、このままだと初回にhello画面に遷移した際はリクエストが送られますが、2回目以降はリクエストされません(ログアウト→ログインした際など)。
画面が呼び出されるたびに毎回リクエストをして欲しいので、hello画面でNavigator.pop(context)が呼ばれて画面を閉じる際にmessageProviderを解放するようにします。
それを実現するために、FutureProviderの代わりにAutoDisposeFutureProviderを使用します。
こうすることで、画面が閉じるタイミングでui側のbuild時にhelloRequestProviderをwatchしていたmessageProviderが解放されます。
なので、画面再描画時もmessageProviderが再作成され、毎回処理が走るようになります。

これを踏まえた最終的なコードは下記になります。

lib/ui/hello/hello_view_model.dart
AutoDisposeFutureProvider<String> helloRequestProvider =
    FutureProvider.autoDispose((ref) async {
  var message = await Requester().helloRequester();

  return message;
});

Future<void> logoutRequest() async {
  return Requester().logoutRequester();
}
lib/ui/hello/hello.dart

class Hello extends ConsumerWidget {
  Hello({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    debugPrint("build");
    final messageProvider = ref.watch(helloRequestProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text("Hello"),
        centerTitle: true,
        automaticallyImplyLeading: false,
        actions: [
          TextButton(
            onPressed: () {
              logoutRequest().then((_) {
                Navigator.pop(context);
              });
            },
            child: const Text('LOGOUT',
                style: TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                    fontSize: 16.0)),
          ),
        ],
      ),
      body: Padding(
        padding: EdgeInsets.only(top: 100),
        child: messageProvider.when(
          data: (message) => Center(
            child: Column(
              children: [Text(message, style: TextStyle(fontSize: 50.0))],
            ),
          ),
          error: (error, stackTrace) {
            Future(() {
              showDialog(
                  context: context,
                  builder: (_) {
                    return AlertDialog(
                      title: Text("認証に失敗しました。再ログインをお願いします。"),
                      actions: [
                        TextButton(
                            onPressed: () => Navigator.pop(context),
                            child: Text("OK")),
                      ],
                    );
                  }).then((_) {
                Navigator.pop(context);
              });
            });
          },
          loading: () => AspectRatio(
            aspectRatio: 0.01,
            child: const CircularProgressIndicator(),
          ),
        ),
      ),
    );
  }
}

ログアウト処理についてもリクエストをviewModel側に移しましたが大した変更ではないので、説明は省略します。

最終的な画面が下記になります。
ezgif.com-gif-maker (5).gif

おわりに

今回は、flutter_riverpodで画面側の状態管理をviewからviewModelに移すことで、MVVMな設計でログインアプリを書き換えてみました。
今回のコードは下記になります。
https://github.com/fu-yuta/authentication_frontend/tree/mvvm

リファクタしただけだと、あまり利点が感じられなかったので、次はテストとか作りたいです。

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7