Flutter – 如何创建一个简单但有效的进度条

为什么指示进展如此重要?

想象一下,我们有一个要导入数据库的大文件或一个需要一些时间才能完成的 API 请求。 我们不能只单击操作按钮并开始该过程,而不向用户指示某些东西在幕后工作。 这可能会产生错误和低质量软件的错觉,并给其他可行的解决方案带来负面评价。 在我们的帮助下,加载器和进度条出现了。 这里的区别在于,第一种类型用于短操作,无法测量工作进度(例如简单的 API 请求)或根本不需要它。 在这种情况下,我们可以使用任何类型的重复动画。


我们如何在没有不必要的样板代码的情况下实现简单的进度条?

让我们先问问自己,要实现这个目标,我们需要什么?

  1. 一个进度条可能具有的状态模型。 所以在这种情况下:
  • 初始化
  • 开始
  • 进行中
  • 完成的
  • 错误
  1. 一种解耦业务逻辑和UI的方式
  2. 一种用户取消当前正在进行的工作并清理资源或以某种优雅的方式完成的方法

 

Flutter - 如何创建一个简单但有效的进度条
flutter 状态交互模式

 


代码实现

让我们首先创建一个文件并将其命名为 state.model.dart(我将其放在 /lib/models/ 中)。 在该文件中,我们必须添加如下所示的代码。

abstract class IState {}

class InitState extends IState {}

class StartState extends IState {
  final String? msg;

  StartState([this.msg]);
}

class ProgressState extends IState {
  final String? msg;
  final double progress;

  ProgressState(this.progress, [this.msg]);
}

class FinishState extends IState {
  final String? msg;

  FinishState([this.msg]);
}

class ErrorState extends IState {
  final String msg;
  final ErrorType error;

  ErrorState(this.error, this.msg);

  @override
  String toString() => msg;
}

enum ErrorType {
  databaseConnection,
  serverConnection,
  // You can add other errors here
  unknown,
}

这里的想法是我们为所有类型的状态创建一个基类,以便我们可以轻松地将它用作加载器小部件中的参数(感谢上帝的多态性!)。 这也有助于我们为不同的状态设置不同类型的属性。 例如,InitState 不需要任何属性,但是对于 ProgressState,我们需要传递一个双精度值(介于 0 和 1 之间)来指示进度,也可能是一条消息。

现在让我们创建一个使用这些状态的小部件。 创建文件 progress.widget.dart (我把它放在 /lib/widgets/ 里面)

import 'package:flutter/material.dart';
import '../models/state.model.dart';

class ProgressWidget {
  static Future<void> show({
    required BuildContext context,
    required Stream<IState> stateStream,
    String? okText,
    String? cancelText,
    Function? onCancel,
  }) async {
    return showDialog(
      barrierDismissible: false,
      context: context,
      builder: (c) {
        return WillPopScope(
          onWillPop: () async => false,
          child: Material(
            color: Colors.transparent,
            child: Center(
              child: Container(
                padding: const EdgeInsets.all(10),
                decoration: BoxDecoration(
                  color: Colors.grey[100],
                  border: Border.all(color: Colors.black, width: 1),
                  borderRadius: BorderRadius.circular(10),
                ),
                child: StreamBuilder(
                  initialData: InitState,
                  stream: stateStream,
                  builder: (context, snapshot) {
                    Widget child = const SizedBox();
                    if (snapshot.hasError) {
                      child = _ErrorWidget(snapshot.error.toString(), cancelText);
                    } else {
                      switch (snapshot.connectionState) {
                        case ConnectionState.none:
                        case ConnectionState.waiting:
                        case ConnectionState.active:
                          child = _ProgressIndicatorWidget(_getProgress(snapshot.data), _getMsg(snapshot.data), cancelText, onCancel);
                          break;
                        case ConnectionState.done:
                          child = _SuccessWidget(_getMsg(snapshot.data), okText);
                          break;
                      }
                    }
                    return child;
                  },
                ),
              ),
            ),
          ),
        );
      },
    );
  }

  static double _getProgress(dynamic data) {
    if (data is ProgressState) {
      return data.progress;
    }
    return 0;
  }

  static String? _getMsg(dynamic data) {
    if (data is StartState || data is ProgressState || data is FinishState) {
      return data.msg;
    }
    return null;
  }
}

因此,我们正在创建一个带有静态方法“show”的类,该方法将用于在包含我们的进度小部件的屏幕上显示一个弹出对话框。我们需要一个可以在 showDialog Flutter 函数上传递的 BuildContext(上下文)参数,这样我们就可以在屏幕上创建一个覆盖。记得将 barrierDismissible: false 传递给 showDialog。我们不希望有人通过意外单击背景来取消正在进行的过程。此外,出于同样的原因,我们使用 WillPopScope 小部件。取消该过程的唯一方法必须是用户必须单击为其分配的按钮的方法。第二个参数是 Stream<IState> stateStream,我们将订阅并监听新的状态。String? okText 用于替换默认的ok按钮文本和String? cancelText 对默认取消按钮执行相同的操作。最后一个参数是 Fluntion?onCancel。该函数在用户按下取消按钮时执行。我们正在使用 StreamBuilder 小部件,因此我们可以根据工作的当前状态反应性地更新 UI。还有两个辅助方法(_getProgress 和 _getMsg)可以派上用场,仅从任何状态中获取我们感兴趣的数据。

在 StreamBuilder 中,我们根据它们的状态使用三个不同的小部件:_ProgressIndicatorWidget_ErrorWidget 和 _SuccessWidget。前两个几乎相同,这就是我构建另一个可以扩展的小部件 (_ProgressWidget) 的原因。我们可以在下面看到它们的代码(我将它们作为 progress.widget.dart 文件的一部分)。

class _ProgressIndicatorWidget extends StatelessWidget {
  final String? msg;
  final double progress;
  final String? cancelText;
  final Function? callback;

  const _ProgressIndicatorWidget(this.progress, this.msg, this.cancelText, this.callback);

  @override
  Widget build(BuildContext context) {
    return _ProgressWidget(
      progress: progress,
      color: Colors.blue[600]!,
      msg: msg,
      callback: callback,
      btnText: cancelText ?? "Cancel",
    );
  }
}

class _SuccessWidget extends StatelessWidget {
  final String? msg;
  final String? okText;

  const _SuccessWidget(this.msg, this.okText);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      mainAxisSize: MainAxisSize.min,
      children: [
        if (msg != null)
          Container(
            margin: const EdgeInsets.symmetric(vertical: 10),
            child: Text(msg!),
          ),
        OutlinedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text(okText ?? "Ok"),
        ),
      ],
    );
  }
}

class _ErrorWidget extends StatelessWidget {
  final String error;
  final String? cancelText;

  const _ErrorWidget(this.error, this.cancelText);

  @override
  Widget build(BuildContext context) {
    return _ProgressWidget(
      progress: 1,
      color: Colors.red[900]!,
      msg: error,
      btnText: cancelText ?? "Cancel",
    );
  }
}

class _ProgressWidget extends StatelessWidget {
  static const double _progressBarSize = 100;
  final double progress;
  final Color color;
  final String? msg;
  final String? btnText;
  final Function? callback;

  const _ProgressWidget({
    required this.progress,
    required this.color,
    this.msg,
    this.btnText,
    this.callback,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      mainAxisSize: MainAxisSize.min,
      children: [
        SizedBox(
          width: _progressBarSize,
          height: _progressBarSize,
          child: Stack(
            children: [
              if (progress >= 0)
                SizedBox(
                  width: _progressBarSize,
                  height: _progressBarSize,
                  child: TweenAnimationBuilder<double>(
                    tween: Tween<double>(begin: 0.0, end: progress),
                    duration: const Duration(milliseconds: 50),
                    builder: (c, v, _) => CircularProgressIndicator(
                      value: v,
                      color: color,
                      backgroundColor: Colors.white,
                    ),
                  ),
                ),
              if (progress >= 0)
                Center(
                  child: Text((progress * 100).toStringAsFixed(0) + "%"),
                ),
            ],
          ),
        ),
        if (msg != null)
          Container(
            margin: const EdgeInsets.symmetric(vertical: 10),
            child: Text(msg!),
          ),
        if (btnText != null)
          OutlinedButton(
            onPressed: () {
              Navigator.pop(context);
              callback?.call();
            },
            child: Text(btnText!),
          ),
      ],
    );
  }
}
view raw

将所有东西粘合在一起

我们定义了 UI 部分以及所有可能的状态。 唯一剩下的是我们可以测试的服务。 让我们创建一个模拟工作事件的服务。

import '../models/state.model.dart';

class SimulatingService {
  SimulatingService();

  Stream<IState> importData() async* {
    yield StartState("Start state");
    await Future.delayed(const Duration(milliseconds: 1000));
    for (int i = 0; i < 50; i++) {
      await Future.delayed(const Duration(milliseconds: 100));
      yield ProgressState(i / 50, "Progress state");
    }
    await clean();
    yield FinishState("Finish state");
  }

  Future<void> clean() async {
    print("Clean some recources");
  }
}

现在我们已经拥有了所有部分,让我们将它们放在一起以在示例应用程序中工作。

import 'package:flutter/material.dart';
import './services/simulating.service.dart';
import './widgets/progress.widget.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ProgressBar demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(),
    );
  }
}

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

  void _loadSimulatedService(BuildContext context) {
    final simService = SimulatingService();
    ProgressWidget.show(context: context, stateStream: simService.importData(), onCancel: simService.clean);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ProgressBar Demo Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text('ProgressBar test'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _loadSimulatedService(context),
        child: const Icon(Icons.play_arrow),
      ),
    );
  }
}

Flutter - 如何创建一个简单但有效的进度条

Flutter - 如何创建一个简单但有效的进度条

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Flutter – 如何创建一个简单但有效的进度条