为异步Flutter应用程序选择正确的进度指示器

简介

你是否曾经在一个移动应用程序中填写并提交了一个表单,然后你看到一个动画或图形的弹出窗口,显示你的请求正在处理中?然后,又出现了另一个弹出窗口,通知你请求是否成功?

这是一个使用图形装饰器来向用户传达其操作状态的常见例子。在Flutter中,这些装饰器被称为进度指示器。

在这篇文章中,您将学习如何在异步应用程序中实现Flutter内置的进度指示器。我们将深入研究每一个指标,了解它们是如何工作的,以及你如何定制它们。然后,我们将建立两个应用程序,在进行下载和外部API请求时向用户显示进度指示器。

前提条件

  • 具有Dart和Flutter的工作知识
  • 在您的机器上安装Dart、Android和Flutter SDKs
  • 您选择的Flutter开发环境

对于这个演示,我将使用Android Studio作为我的开发环境。

什么是异步应用程序?

一个异步应用程序是由一个任务或一组任务组成的,这些任务被置于运动中,而程序的其他部分则继续执行之前的任务,直到完成。

理想情况下,你已经决定是否在你的程序中应用异步执行,因为你知道你想建立什么样的系统。确定这一点的一个有用的提示是,确定应该独立执行的具体任务和那些依赖于其他进程完成的任务。

Flutter进度指示器

顾名思义,进度指示器有助于传达用户请求的状态。需要进度指示器的行动的例子包括。

  • 下载文件
  • 上传文件
  • 提交表格
  • 在应用程序上加载一个页面

Flutter有一个抽象的ProgressIndicator 类,其具体的进度指示器部件--LinearProgressIndicatorCircularProgressIndicator --是其子类。

我们将看一下Flutter中的三个进度指示器。在我写这篇文章的时候,Flutter中有两个内置的指标,其余的是外部依赖,必须在你的Flutter项目中安装。

线性进度指示器

这是Flutter内置的第一个进度指示器,它是ProgressIndicator 抽象类的一个子类。它用于在一个水平条中传达任务的进度。

圆形进度指示器

这是Flutter内置的第二个进度指示器,它也是ProgressIndicator 抽象类的一个子类。CircularProgressIndicator() 转动以传达一个任务正在被处理。

扫描二维码关注公众号,回复: 13693850 查看本文章

一般来说,这些指标的持续时间可以是确定的或不确定的。

一个确定的进度指示器的作用是传达已经完成的任务的部分或百分比以及尚未执行的部分。

该指标的值随着任务执行中的每一点进展而变化。double 每个进度指示器都有一个value 属性,接受0.01.0 之间的数据类型,以设置指示器的起始点和终点。

Demo of a determinate circular progress indicator

上面的图片是一个用下面的代码片建立的确定的圆形进度指示器。

    dart

class DeterminateIndicator extends StatefulWidget {



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

    class _DeterminateIndicatorState extends State<DeterminateIndicator > {


      @override
      Widget build(BuildContext context) {

        return Scaffold(
          backgroundColor: Colors.black,
          body: Center(
            child: Padding(
              padding: const EdgeInsets.all(10.0),
              child: TweenAnimationBuilder(
                tween: Tween(begin: 0.0, end: 1.0),
                duration: Duration(seconds: 3),
                builder: (context, value, _) {
                  return SizedBox(
                    width: 100,
                    height: 100,
                    child: CircularProgressIndicator(
                      value: value as double,
                      backgroundColor: Colors.grey,
                      color: Colors.amber,
                      strokeWidth: 10,
                    ),
                  );
                }
              ),
            )
            )
          );

      }

    }

复制代码

该指标旋转了三秒钟,正如TweenAnimationBuilder() widget的持续时间所定义的。

一个不确定的进度指示器的作用是传达一个没有明确持续时间的任务的进度。换句话说,当我们不知道任务在完成之前需要多长时间时,就会使用这个指标。

通过将其value 属性设置为null ,可以使一个指标成为不确定的。

Demo of an indeterminate linear progress indicator

上面的图片是一个不确定的线性进度指示器,是用下面的代码片构建的。

    dart

    class IndeterminateIndicator extends StatefulWidget {

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

    class _IndeterminateIndicatorState extends State<IndeterminateIndicator > {

      @override
      Widget build(BuildContext context) {

        return Scaffold(
          backgroundColor: Colors.black,
          body: Center(
            child: Padding(
              padding: const EdgeInsets.all(10.0),
              child: SizedBox(
                     child: LinearProgressIndicator(
                      backgroundColor: Colors.grey,
                      color: Colors.amber,
                      minHeight: 10,
                    ),
              ),
            )
            )
          );

      }

    }

复制代码

Flutter Spinkit包

flutter_spinkit一个外部包,它包括一个可以在你的应用程序中实例化的动画指标集合。

要在你的项目中安装这个包,请在你的pubspec.yaml 文件中添加下面的依赖性。

dependencies:
  flutter_spinkit: ^5.1.0

复制代码

或者,你可以简单地在你的终端运行以下命令。

console

$ flutter pub add flutter_spinkit

复制代码

下面是这个软件包中的一些指标的预览。

Flutter Spinkit's progress indicator library

您可以随时参考flutter_spinkit文档,从其他可能更适合您的应用程序主题的可用选项中选择。

进度指示器的合适用例

当在您的应用程序中应用一个进度指示器时,您首先要考虑的是您是否能获得任务的终点或衡量其进度。这使你能够决定是否应该选择一个确定的或不确定的进度指标。

例如,你可以测量任务的进度,从而应用确定的进度指标的情况包括。

  • 上传一个文件
  • 下载一个文件
  • 执行一个倒计时

然而,当你不能测量任务的进度时,不确定的指标是你最好的选择。这种情况的例子包括。

  • 加载一个应用程序
  • 通过HTTP连接发送数据
  • 请求一个 API 的服务

flutter_spinkit 包所提供的指标通常被归类为加载指标。因此,当您需要一个不确定的进度指标时,最适合使用它。

实现确定的进度指示器

让我们继续演示一个确定的指标如何工作。我们将通过建立一个应用程序来实现这一点,该应用程序可以通过点击一个按钮从互联网上下载一个文件。

你将通过循环进度指示器传达下载的进度。我们正在下载的文件的大小是可以得到的,所以我们将通过计算已经下载了多少字节来衡量其进度。

本演示所需的依赖性是。

  • path_provider,为我们存储下载的文件提供目录访问。
  • http,用于通过互联网请求下载文件。
dart

class DeterminateIndicator extends StatefulWidget {

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

class _DeterminateIndicatorState extends State<DeterminateIndicator> {

  File? imageFile;
  double downloadProgress = 0;

  Future downloadImage() async {
    final url =      'https://images.unsplash.com/photo-1593134257782-e89567b7718a?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=375&q=80';

    final request = Request('GET', Uri.parse(url));
    final response = await Client().send(request);
    final contentLength = response.contentLength;
    final fileDirectory = await getApplicationDocumentsDirectory();
    final filePath = '${fileDirectory.path}/image.jfif';

    imageFile = File(filePath);
    final bytes = <int>[];
    response.stream.listen(
          (streamedBytes) {
        bytes.addAll(streamedBytes);

        setState(() {
          downloadProgress = bytes.length / contentLength!;
        });
      },
      onDone: () async {
        setState(() {
          downloadProgress = 1;
        });
      },
      cancelOnError: true,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        title: Text('Determinate progress indicator'),
        centerTitle: true,
      ),
      body: Container(
        alignment: Alignment.center,
        padding: EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            downloadProgress == 1 ? Container(
              width: 250,
                height: 250,
                child: Image.file(imageFile!)
            ) : Text('Download in progress'),
            SizedBox(height: 30),

            SizedBox(
              width: 100,
              height: 100,
              child: Stack(
                fit: StackFit.expand,
                children: [
                  CircularProgressIndicator(
                    value: downloadProgress,
                    valueColor: AlwaysStoppedAnimation(Colors.blueAccent),
                    strokeWidth: 10,
                    backgroundColor: Colors.white,
                  ),
                  Center(
                      child: downloadProgress == 1
                          ?
                      Text(
                        'Done',
                        style: TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.bold,
                            fontSize: 20
                        ),
                      )
                          :
                      Text(
                        '${(downloadProgress * 100).toStringAsFixed(0)}%',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          color: Colors.white,
                          fontSize: 24,
                        ),
                      )
                  ),
                ],
              ),
            ),

            const SizedBox(height: 32),
            Container(
              width: 200,
              height: 40,
              child: RaisedButton(
                onPressed: downloadImage,
                color: Theme
                    .of(context)
                    .primaryColor,
                child: Row(
                    children: <Widget>[
                      Text(
                        'Download image',
                        style: TextStyle(
                            color: Colors.white,
                            fontSize: 16
                        ),
                      ),
                      SizedBox(width: 10),
                      Icon(
                        Icons.download,
                        color: Colors.white,
                      )
                    ]
                ),
              ),
            )
          ],
        ),
      ),
    );
  }

}

复制代码

在上面的代码中,我们向图片的URL发送了一个HTTP请求。你可以用你选择的图像URL来替换这个URL。来自HTTP请求的响应内容被读成字节。

响应中的每一个流式字节都用downloadProgress 变量来测量,小部件在其值的每一个变化中都被重新构建。

最后,一旦下载过程完成,我们就在屏幕上显示下载的图像,并将downloadProgress 的值定义为等于1。下面,你可以在我们的示例应用程序中看到最终结果。

The final example of a determinate circular progress indicator implemented in our app

实现一个不确定的进度指示器

对于这个演示部分,我们将建立一个简单的应用程序,向GitHub Rest API发出HTTP请求。 [https://api.github.com/users/olu-damilare](https://api.github.com/users/olu-damilare).然后,我们将继续在屏幕上渲染从该请求获得的一些数据。

由于我们不知道这个请求可能需要多长时间,我们必须实现一个不确定的进度指示器,以传达请求目前正在处理。

构建这个应用程序所需的外部依赖性是。

dart
class IndeterminateIndicator extends StatefulWidget {

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

class _IndeterminateIndicatorState extends State<IndeterminateIndicator> {

  String? name;
  String? username;
  String? publicRepos;
  String? publicGists;
  String? followers;
  String? following;
  bool isLoading = false;

  Future<void> fetchData() async{
    setState(() {
      isLoading = true;
    });

    try {
      Response response = await get(
          Uri.parse('https://api.github.com/users/olu-damilare'));
      Map data = jsonDecode(response.body);

      setState(() {
        name = data['name'];
        username = data['login'];
        publicRepos = data['public_repos'].toString();
        publicGists = data['public_gists'].toString();
        followers = data['followers'].toString();
        following = data['following'].toString();
        isLoading = false;
      });

    }catch(e){
      print('caught error: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.grey[900],
        appBar: AppBar(
        title: Text('Indeterminate progress indicator'),
        backgroundColor: Colors.grey[850],
        centerTitle: true,
        elevation: 0.0,
    ),
        body: isLoading ?
        Center(
            child: SizedBox(
              height: 200,
              width: 200,
              child: SpinKitCircle(
                itemBuilder: (BuildContext context, int index) {
                  return DecoratedBox(
                    decoration: BoxDecoration(
                      color: Colors.amber,
                    ),
                  );
                },
              ),
            )
        )
        :
        Padding(
        padding: EdgeInsets.all(60),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[

          Row(
            children: [
              buildParam('NAME:'),
              SizedBox(width: 15.0),
              name == null ? Text('') : buildData(name!),
            ],
          ),
          SizedBox(height: 20.0),
            Row(
              children: [
                buildParam('USERNAME:'),
                SizedBox(width: 15.0),
                name == null ? Text('') : buildData('@${username}'),
              ],
            ),
            SizedBox(height: 20.0),
            Row(
              children: [
                buildParam('PUBLIC REPOS:'),
                SizedBox(width: 15.0),
                name == null ? Text('') : buildData(publicRepos!),
              ],
            ),

          SizedBox(height: 20.0),
            Row(
              children: [
                buildParam('PUBLIC GISTS:'),
                SizedBox(width: 15.0),
                name == null ? Text('') : buildData(publicGists!),
              ],
            ),
            SizedBox(height: 20.0),
            Row(
              children: [
                buildParam('FOLLOWERS:'),
                SizedBox(width: 15.0),
                name == null ? Text('') : buildData(followers!),
              ],
            ),

            SizedBox(height: 20.0),
            Row(
              children: [
                buildParam('FOLLOWING:'),
                SizedBox(width: 15.0),
                name == null ? Text('') : buildData(following!),
              ],
            ),

            Padding(
              padding: const EdgeInsets.only(top: 50.0, left: 30),
              child: RaisedButton(
                color: Colors.amber,
                onPressed: fetchData,
                child: Text(
                    'Fetch data',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 20
                  ),
                ),
              ),
            )
          ]
          ),
          ),
          );
      }

      Widget buildParam(String param){
        return Text(
          param,
          style: TextStyle(
            fontSize: 15.0,
            fontWeight: FontWeight.bold,
            color: Colors.grey,
          ),
        );
      }

      Widget buildData(String data){
        return Text(
          data,
          style: TextStyle(
            fontSize: 20.0,
            fontWeight: FontWeight.bold,
            color: Colors.amber[400],
          ),
        );
      }
}

复制代码

The final example of an indeterminate circular progress indicator implemented in our app

最后的想法

进度指示器为您的应用程序贡献的用户体验是无价的。你不想让你的用户在每次执行动作时都怀疑你的应用程序是否出现了故障,而对他们的请求的状态又没有适当的指示。

适当地选择指标也会影响你的应用程序的用户体验,我希望我能够指导你为你的异步Flutter应用程序选择和实现正确的进度指标。

The post Choosing theright progress indicators for async Flutter appsappeared first onLogRocket Blog.

猜你喜欢

转载自juejin.im/post/7067855082322133028