Flutter dialog (1) - showDialog的讲解

在应用开发中,或多或少都会遇到需要弹框的问题, 比如:需要用户确认,需要输入一些信息等等的问题,这就要用到 dialog 相关的概念了

而在 flutter 中,所有可以看见的都是 Widget,dialog 也不例外

不过和 android 或 iOS 中不同的一点是,Flutter 中 dialog 不是一个单独的类,而是一个可以由你自定义的 Widget

写在前面

首先为了方便,我定义了一个简单的方法用于构建按钮

  Widget buildButton(
    String text,
    Function onPressed, {
    Color color = Colors.white,
  }) {
    return FlatButton(
      color: color,
      child: Text(text),
      onPressed: onPressed,
    );
  }

showDialog

Flutter dialog (1) - showDialog的讲解
dialog 的方法签名是这样的

其中 context 和 builder 是必传项

builder 需要返回一个 Widget,这个 Widget 会被作为 dialog 展示在页面上

比如我简单的写了一个这个方法

    showDialog(
      context: context,
      builder: (ctx) {
        return Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              buildButton("返回1", () {}),
              buildButton("返回2", () {}),
            ],
          ),
        );
      },
    );

当我调用这个方法时,会得到这样的样式
Flutter dialog (1) - showDialog的讲解

这个就是最简单的方法,然后点击外部,dialog 会消失

添加关闭时的返回值

接着我给按钮添加具体的事件

修改代码为以下的样子

_showDialog() async {
    var result = await showDialog(
      context: context,
      builder: (ctx) {
        return Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              buildButton("返回1", () => Navigator.of(context).pop(1)),
              buildButton("返回2", () => Navigator.pop(context, 2)),
            ],
          ),
        );
      },
    );

    print("result = $result");
  }

然后分别点击 1 2 和外部让 dialog 消失
会得到以下的结果

Flutter dialog (1) - showDialog的讲解

不过这个只能让 dialog 显示固定的内容,如果你的 dialog 有内容变化,则使用这个方式就不行了,哪怕是调用 setState 也不会发生变化,这个是因为外部 State 的状态变化不会影响到 dialog 的内容,因为 dialog 是附着至 app 根部的,而不是附着于页面

Flutter dialog (1) - showDialog的讲解

结合 StatefulWidget 使用

所以我们 dialog 中也可以使用 StatefulWidget,如同一个页面一样,只是这个页面可能不是全屏的

我定义了一个简单的 CounterWidget


class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  var _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Material(
            child: Container(
              width: 100,
              height: 100,
              child: Text(
                _counter.toString(),
                style: TextStyle(fontSize: 40),
              ),
              alignment: Alignment.center,
            ),
            color: Colors.white,
          ),
          buildButton("+1", () => setState(() => _counter++)),
          buildButton("-1", () => setState(() => _counter--)),
        ],
      ),
    );
  }
}

并且调用

showDialog(context: context, builder: (ctx) => CounterWidget());

Flutter dialog (1) - showDialog的讲解

这里可以看到,一个带状态的控件也是可以被展示在 dialog 中的

结合 StatefulBuilder

在 flutter 中有一个类,叫 StatefulBuilder

这个类的 builder 构造中会给一个 state,这个 state 是一个方法,返回 void,传入参数是一个方法,听起来很绕

大概是这样用

var statefulBuilder = StatefulBuilder(
    builder: (ctx, state) {
        state(() {});
        return Container();
    },
);

看起来和 setState 很像

这里我模拟一个 progress 的变化,不过这个进度是由外部传入的

_showDialogWithStatefulBuilder() {
    var progress = 0.0;
    StateSetter ss;
    Timer.periodic(Duration(milliseconds: 300), (timer) {
      progress += 0.1;
      if (ss != null) {
        ss(() {});
      }
      if (progress >= 1) {
        timer.cancel();
        ss = null;
      }
    });
    var sb = StatefulBuilder(
      builder: (ctx, state) {
        ss = state;
        return Center(
          child: Container(
            height: 40,
            child: LinearProgressIndicator(
              backgroundColor: Colors.white,
              value: progress,
            ),
          ),
        );
      },
    );
    showDialog(context: context, builder: (ctx) => sb);
  }

Flutter dialog (1) - showDialog的讲解

这里只是简单的演示一个用法,实际应用中,进度条应该是可以多处复用的,应该使用 StatefulWidget 进行复用,而不是简易的使用 StatefulBuilder 来做这件事情,并且,应该在构建时传入 stream 并且监听 stream 为宜,而不应该使用这种 Timer 的形式

StatefulBuilder 应该用于弹出布局很特殊不太可能复用于其他地方的情况

使用 iOS 风格

有的同学可能要问了,你这演示都是 MD 风格的,我需要的是苹果风格的, 怎么办?

在 flutter 中,如果你需要 iOS 风格的,只需要使用 Cupertino 组件即可

void showCupertinoDialog() {
    var dialog = CupertinoAlertDialog(
      content: Text(
        "你好,我是你苹果爸爸的界面",
        style: TextStyle(fontSize: 20),
      ),
      actions: <Widget>[
        CupertinoButton(
          child: Text("取消"),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
        CupertinoButton(
          child: Text("确定"),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ],
    );

    showDialog(context: context, builder: (_) => dialog);
  }

Flutter dialog (1) - showDialog的讲解

带输入框的 dialog

 showHasInputDialog() {
    var widget = Center(
      child: Container(
        height: 40,
        width: double.infinity,
        child: Material(
          child: TextField(),
        ),
      ),
    );
    showDialog(context: context, builder: (_) => widget);
  }

Flutter dialog (1) - showDialog的讲解

根据软键盘自动变化位置

之前的输入框有一些问题,如果你的弹窗在底部,则弹出的输入框可能会被挡住

这里需要另一个方法来实现

import 'package:flutter/material.dart';
import 'dart:ui' as ui;

class InputDialog extends StatefulWidget {
  @override
  _InputDialogState createState() => _InputDialogState();
}

class _InputDialogState extends State<InputDialog> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    if (this.mounted) setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    var mediaQueryData = MediaQueryData.fromWindow(ui.window);
    return AnimatedContainer(
      color: Colors.transparent,
      duration: const Duration(milliseconds: 300),
      padding: EdgeInsets.only(bottom: mediaQueryData.viewInsets.bottom),
      child: Material(child: TextField()),
      alignment: Alignment.center,
    );
  }
}

定义一个 dialog 类,然后监听窗口的变化

然后在变化的时候动态的修改 padding,以达到输入框永远在界面中心的目的

Flutter dialog (1) - showDialog的讲解

后记

完整代码 github

第一篇主要讲了 showDialog 方法的一些使用方法和建议

以上