LoginSignup
4
3

More than 3 years have passed since last update.

FlutterでNavigatorを使わずにDialogを自作する。

Last updated at Posted at 2019-12-15

SnackBarの処理(Scaffold.of(context).showSnackBar)を参考にしてStreamも交えつつ、
ダイヤログを自作する記事になります。

この記事は仕組みについて簡単な解説になります。
上記のサンプルについてGitHubに公開しておりますので下記のURLを踏んでください。

尚、Blocの実装についてある程度経験がある前提で進みます。

1.Preliminary

Flutterのネイティブ機能にダイヤログ関連の処理(showDialog)が備わっており、
特に不自由なくアプリ作成を楽しんでいると思います。(この記事の意義とは)

なんやかんやの個人開発でDialog機能に下記のような物足りなさを感じて自作することになりました。

  • 画面を表示したタイミングでダイヤログを表示させたい
    (build段階でDialog表示の処理を入れてしまった。。)

  • Dialogが二重に表示されて辛い。

  • NavigatorされないDialogが欲しい。

2. AppProvider

Dialogの状態をアプリ全体で共有できるようにします。

void main() => runApp(AppProvider(child:MyApp()));
class AppProvider extends StatefulWidget {

  final Widget child;
  DialogBloc dialogBloc;

  AppProvider({
    Key key,
    @required this.child
  }):super(key: key){
    dialogBloc = DialogBloc();
  }

  static _AppProvider of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(_AppProvider);
  }

   // Dialogを表示
  static void showDialog(BuildContext context,BaseDialog dialog) {
    AppProvider.of(context).dialogBloc.showDialog(dialog: dialog);
  }

   // Dialogを非表示
  static void popDialog(BuildContext context) {
    AppProvider.of(context).dialogBloc.popDialog();
  }
   // 不要なDialogがBloc内にあるのかチェック
  static void inspectDialog(BuildContext context) {
    AppProvider.of(context).dialogBloc.inspectDialogs();
  }

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return AppProviderState();
  }

}

class AppProviderState extends State<AppProvider> {...}

class _AppProvider extends InheritedWidget {...}

3. DialogBloc

DialogBlocの主な役割は以下になります。

  • AppInjectorから引き渡されたDialogの構成要素をスタック配列として格納し、後入れを表示できるように制御する
  • 表示、非表示などDialogの制御要素が更新された際に、StreamController介してsubscribeしているWidgetに通知する。
class DialogBloc {

  List<DialogStateController> controllers;
  StreamController<List<DialogStateController>> statesStreamController;
  Stream get statesStream => statesStreamController.stream;
  Sink get statesSink => statesStreamController.sink;

  DialogBloc(){
    controllers = [];
    statesStreamController = StreamController<List<DialogStateController>>();
    statesSink.add(controllers);
  }


  void showDialog({BaseDialog dialog}) async {
    //後入れのDialogのみを表示できるように、先入れのDialogを非表示に更新
    if(controllers.length > 0) {
      controllers.last.state = DialogState.hide;
    }
    controllers.add(DialogStateController(dialog: dialog));
    statesSink.add(controllers);
  }

  void popDialog() async {
    if(controllers.length == 0) return;

    //後入れのDialogを削除に更新
    controllers.last.state = DialogState.dismiss;

    //先入れのDialogを表示に更新
    if( controllers.length >= 2) {
      controllers[controllers.length - 2].state = DialogState.show;
    }
    statesSink.add(controllers);
  }

  void inspectDialogs() async {
     //削除対象であれば削除
    controllers = controllers.where((dialog)=> dialog.state != DialogState.dismiss).toList();
    statesSink.add(controllers);
  }
}

Dialogの構成要素を格納

enum DialogType {
  center,
  bottom,
}

class BaseDialog {
  Widget child;
  Function outSideTapped;
  DialogType type;

  BaseDialog({
    this.child,
    this.outSideTapped,
    this.type = DialogType.center
  });
}

Dialogの制御要素を格納

enum DialogState {
  show,
  hide,
  dismiss,
}

class DialogStateController {
  BaseDialog dialog;
  DialogState state = DialogState.show;

  DialogStateController({
    @required this.dialog,
  });

  bool get isShow {
    return state == DialogState.show;
  }
}

4. DialogScreen

DialogScreenが画面の上位レイヤーに表示されるようにします。

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Stack(
        children: <Widget>[
          MyHomePage(),
          DialogScreen(
            dialogBloc: AppProvider.of(context).dialogBloc,
          )
        ],
      ),
    );
  }
}

DialogScreenの仕組みは簡単で、DialogBlocの制御要素をSubscribeし、通知された際に画面を再描画します。

class DialogScreen extends StatefulWidget{
  final DialogBloc dialogBloc;
  DialogScreen({this.dialogBloc});
  @override
  State<StatefulWidget> createState() {
    return DialogScreenState();
  }
}

class DialogScreenState extends State<DialogScreen> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {

    // DilogBlocの制御情報(DialogStateController)をSubscribe
    return StreamBuilder<List<DialogStateController>>(
      stream: widget.dialogBloc.statesStream,
      builder: (context, snapshot) {
        if(!snapshot.hasData || snapshot.data.length == 0) {
          return Align(
              alignment: Alignment.topLeft,
              child: SizedBox(height: 0,width: 0)
          );
        }
        List<Widget> widgets = [];

        // Dialogの背景
        widgets.add(Positioned(
          top: 0,
          left: 0,
          bottom: 0,
          right: 0,
          child: GestureDetector(
            onTap: snapshot.data.last.dialog.outSideTapped ?? (){
              AppProvider.popDialog(context);
            },
            child:
            FadeAnimationWrap(
              isShow: widget.dialogBloc.controllers.length > 0,
              child: ClipRect(
                  child: BackdropFilter(
                    filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),
                    child: Container(
                      color: Color.fromARGB(40, 0, 0, 0),
                    ),
                  )
              ),
            ),
          ),
        ),);

        // Dialogの構成情報より画面を生成
        snapshot.data.forEach(
                (controller){
              widgets.add(
                      (){
                    switch(controller.dialog.type){
                      case DialogType.bottom:
                        return modalDialog(controller);
                      default :
                        return centerDialog(controller);
                    }
                  }()
              );
            }
        );
        return
          Container(
            child: Stack(
                children: widgets
            ),
          );
      },
    );
  }

  Widget centerDialog(DialogStateController controller){
    return  Align(
        alignment: Alignment.center,
        child: FadeAnimationWrap(
            isShow: controller.isShow,
            child: Container(
              child: controller.dialog.child,
              padding: EdgeInsets.fromLTRB(24, 0, 24, 0),
            ),
            animateStateListener: (state){
              switch(state){
                case AnimationStatus.dismissed:
                  AppProvider.inspectDialog(context);
                  break;
                default: break;
              }
            }
        )
    );
  }

  Widget modalDialog(DialogStateController controller){...}

5. Implement Custom Dialog

こんな感じに実装できます。

class MyHomePage extends StatefulWidget {...}

class _MyHomePageState extends State<MyHomePage> {

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            child:Scaffold(
              backgroundColor: Colors.yellow,
              body:  Center(
                  child:Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      FlatButton(
                        onPressed: (){
                          showScreenDialog();
                        },
                        child: Text(
                            'Show Dialog',
                            style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.blueAccent)
                        ),

                      ),
                    ],
                  )
              ),
            )
        )
      ],
    );
  }

  void showScreenDialog() {
    AppProvider.showDialog(
        context,
        BaseDialog(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                DialogContentsWarp(
                    child: Center(
                      child: Text(
                        "Hello World \n No Border",
                        style: Theme.of(context).textTheme.title,
                      ),
                    )
                ),
                SizedBox(
                  height: 24,
                ),
                DialogButton(
                  isBottomCornerRound: true,
                  isTopCornerRound: true,
                  txt: "Close",
                  onPressed: (){
                    AppProvider.popDialog(context);
                  },
                ),
              ],
            )
        )
    );
  }
}

6. Summary

仕組みについての簡単な説明は以上になります。
最初に紹介したSample実装例はGitHubにありますので参考になればと思います。

4
3
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
4
3