Skip to content

Latest commit

 

History

History
94 lines (78 loc) · 8.32 KB

README.md

File metadata and controls

94 lines (78 loc) · 8.32 KB

salon_admin_web

  • サロンアプリの管理Webアプリ
  • Flutter Web で作ってます

フォルダ構成

フォルダ名 説明
config 設定まわり。mainから呼ばれるAppもここに含まれる。
domain データ構造。Entityとも呼ぶ。
exception 例外クラスの集合体。
extension 拡張クラスの集合体。
presentation StatelessWidgetのページ(View)と、ChangeNotifier(ViewModel)のセットの集合体。
repository データの取得や操作を担う。

使用しているパッケージの補足

パッケージ名 補足
charts_flutter グラフ描画
flutter_admin_scaffold サイドバーをつけて管理画面っぽくするやつ。サロンメンバーのすさ作成。
flutter_bootstrap Bootstrapのcontainer(col-md-4とか)が使える。
flutter_bootstrap_widgets Bootstrap3風にしてくれる、管理WebをWebっぽくするためにいたるところで使ってます。サロンメンバーのすさ作成。
flutter_web_data_table テーブルをもっと簡単に使えるようにする、一覧画面で使ってます。サロンメンバーのすさ作成。
flutter_web_router ルーティングを簡単に使えるようにする、/items/view/{itemId}みたいにワイルドカードをあつかえます。サロンメンバーのすさ作成。

アーキテクチャ

  • MVVM + Repository

アーキテクチャ

  • 詳細

アーキテクチャ詳細

【解説】 BaseModel extends ChangeNotifier

  • ViewModelに相当する部分のうち、共通処理をあつめたBaseModelを作り、各ページのViewModelはBaseModelを継承しています。
  • BaseModelが担う共通処理
    • ローディングのフラグ管理
    • リクエスト情報の保持
    • 各種ダイアログの表示
    • 画面遷移処理
    • セッション管理
  • context依存問題
    • BaseModelのコンストラクターはcontextを引数にとります。
    • ViewModelがcontextを持つことで画面遷移とポップアップ表示のトリガーをViewModelが担うことが出来て、View側にロジックを一切書かなくすることが出来る。
    • 本来は、ViewModelのViewへの依存性を低くし、ViewModelの単体テストを容易にできるようにすることが望ましい。
    • そのためにはViewModelはcontextは持たない方がよいが、実装の難易度が上がってしまうので現状のままとしている。
    • ViewModelのcontext非依存のアイデアとしては、streamやlistener等を使ってViewModelからViewにイベントを発行し、View側で画面遷移とポップアップ表示をすることが考えられる。

【解説】 ルーティング

  • ルーティングの細かい実装は flutter_web_router packageに閉じ込めました。ここではルーティングの考え方を説明します。
  • Flutter Web では、事前に名前付きパスを定義して、Navigator.of(context).pushNamed('/dashboard');のようにパスを与えて画面遷移します。
  • 普通にMaterialApp().routes で各画面のパスを固定で定義し、例えばお知らせ詳細画面を開くときにお知らせ情報(Notice)をコンストラクターで与えて表示すると、ブラウザ再読込したときにお知らせ情報が無くなって正しく表示されない問題が起きます。
  • この問題を解決するために、お知らせ詳細画面を表示する場合は /notices/view/{noticeId}、お知らせ編集画面を表示する場合は/notices/edit/{noticeId}のように、パスにnoticeIdを含めてしまえばブラウザ再読込みされても同じ画面を表示できます。
  • flutter_web_router を使って以下のような処理を行っています。
    • MaterialApp().onGenerateRoute でパスを定義する際は、/notices/view/{noticeId}のようにワイルドカード付きで定義しておく
    • 画面遷移するときは/notices/view/{noticeId}に合致するように、例えば/notices/view/ZU47spXSBzU2ZGnVOlAmのようなパスを生成してpushする
    • MaterialApp().onGenerateRouteでリクエストされたパスを解決する際、/notices/view/ZU47spXSBzU2ZGnVOlAm/notices/view/{noticeId}に合致すると判断し、適切な画面を呼び出す。その際 noticeId=ZU47spXSBzU2ZGnVOlAmという情報も画面に引き渡す。
    • 画面表示する際、お知らせ情報を取得してきて表示する。

【解説】 ログイン状態の管理

  • Flutter Web では、画面表時直後に限り、ログイン中であっても FirebaseAuth.instance.currentUserNULLを返すケースが存在します。
  • これを解決するために、画面表時直後に強制的にユーザのログイン状態が確定するまで待つ処理を挟んでいます。

main.dart

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // ユーザのログイン状態が確定するまで待つ
  await Firebase.initializeApp();
  await firebase.auth(firebase.app()).onAuthStateChanged.first;

  runApp(AdminApp());
}
  • 未ログインならログイン画面へ、ログイン済みならダッシュボード画面へ遷移する制御はflutter_web_router を使って以下のような処理を行っています。
    • WebRouter().addFilter() で、リクエストが来たときにログイン状態に応じた処理を追加する
      • 未ログインならログイン画面にリダイレクトする
      • ログイン済みならリクエスト通りの処理をする
      • 具体的には、 config/router_factory.dartLoginVerificationFilter を参照ください。

【解説】 セッション管理

  • PHPやRubyなどのサーバーアプリケーションでよく使われるセッションをFlutterWebでも使えるようにしました。

    • セッションとは、ユーザに紐付けた永続データです。実体はCookieを使っています。ユーザが入力した情報を一時的に保持し、画面遷移した先で取り出したりして使います。
  • 永続データの保存には shared_preferences パッケージを使っています。

  • repository/session_repository.dart がセッションを管理するレポジトリクラスです。パス毎にセッションデータを分離する仕組みをいれています。例えば、テーブル一覧表示のソート条件などをセッションで管理することで、画面遷移してもソート条件を保持することができますが、メンバー一覧画面とお知らせ一覧画面では別々に管理をする必要があります。レポジトリ内でパス毎にセッションデータを分離することで、利用側(ViewModel)は他の一覧画面のことを意識せずにセッションを使うことが出来ます。

    • お知らせ一覧画面( /notices )でソート条件をセッションに保持するとき setSession('sortColumnName', 'userId'); と実行します。すると、SharedPreferences には notices.sortColumnName = 'userId' と、パスをprefixにして保存します。
    • メンバー一覧画面( /members )でソート条件をセッションに保持するとき setSession('sortColumnName', 'userId'); と実行します。すると、SharedPreferences には members.sortColumnName = 'userId' と、パスをprefixにして保存します。
    • 画面側からは同じように関数を実行しますが、セッションレポジトリでパス情報をprefixに付加することで別々に保存することができます。