如何在Flutter中使用进度指示器?

141

我是flutter的新手,想知道在我的布局中添加CircularProgressIndicator的更好方法。比如说,我的登录视图。这个视图有用户名、密码和登录按钮。我想创建一个叠加层(使用Opacity),当加载时显示像我在NativeScript中使用的进度指示器,但我对如何做以及是否是更好的方法有点困惑。例如,在NativeScript中,我将IndicatorActivity添加到主布局中,并将busy设置为true或false,这样它会在加载时覆盖所有视图组件。

编辑:

我已经成功实现了这个效果:

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _loading = false;

  void _onLoading() {
    setState(() {
      _loading = true;
      new Future.delayed(new Duration(seconds: 3), _login);
    });
  }


  Future _login() async{
    setState((){
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {


      var body = new Column(
          children: <Widget>[
            new Container(
              height: 40.0,
              padding: const EdgeInsets.all(10.0),
              margin: const EdgeInsets.fromLTRB(15.0, 150.0, 15.0, 0.0),
              decoration: new BoxDecoration(
                color: Colors.white,
              ),
              child: new TextField(
                decoration: new InputDecoration.collapsed(hintText: "username"),
              ),
            ),
            new Container(
              height: 40.0,
              padding: const EdgeInsets.all(10.0),
              margin: const EdgeInsets.all(15.0),
              decoration: new BoxDecoration(
                color: Colors.white,
              ),
              child: new TextField(
                decoration: new InputDecoration.collapsed(hintText: "password"),
              ),
            ),
          ],
        );


      var bodyProgress = new Container(
        child: new Stack(
          children: <Widget>[
            body,
            new Container(
              alignment: AlignmentDirectional.center,
              decoration: new BoxDecoration(
                color: Colors.white70,
              ),
              child: new Container(
                decoration: new BoxDecoration(
                  color: Colors.blue[200],
                  borderRadius: new BorderRadius.circular(10.0)
                ),
                width: 300.0,
                height: 200.0,
                alignment: AlignmentDirectional.center,
                child: new Column(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new Center(
                      child: new SizedBox(
                        height: 50.0,
                        width: 50.0,
                        child: new CircularProgressIndicator(
                          value: null,
                          strokeWidth: 7.0,
                        ),
                      ),
                    ),
                    new Container(
                      margin: const EdgeInsets.only(top: 25.0),
                      child: new Center(
                        child: new Text(
                          "loading.. wait...",
                          style: new TextStyle(
                            color: Colors.white
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      );

      return new Scaffold(
        appBar: new AppBar(
          title: new Text(widget.title),
        ),
        body: new Container(
          decoration: new BoxDecoration(
            color: Colors.blue[200]
          ),
          child: _loading ? bodyProgress : body
        ),
        floatingActionButton: new FloatingActionButton(
          onPressed: _onLoading,
          tooltip: 'Loading',
          child: new Icon(Icons.check),
        ),
      );
  }
}

应用程序屏幕截图结果

我还在适应状态的概念。这段代码在使用 Flutter 时符合预期吗?


2
如何在显示对话框时禁用返回按钮? - Quick learner
15个回答

132
在Flutter中,处理异步操作的方法有几种。一种懒惰的方法可以使用模态框。这将阻止用户输入,从而防止任何不必要的操作。这只需要对您的_onLoading进行少量修改,例如:
void _onLoading() {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (BuildContext context) {
      return Dialog(
        child: new Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            new CircularProgressIndicator(),
            new Text("Loading"),
          ],
        ),
      );
    },
  );
  new Future.delayed(new Duration(seconds: 3), () {
    Navigator.pop(context); //pop dialog
    _login();
  });
}

最理想的方法是使用FutureBuilder和一个有状态的widget。这就是你开始做的事情。

关键在于,你不需要在状态中设置一个boolean loading = false,而是直接使用Future<MyUser> user作为参数传递给FutureBuilder,它将在完成时提供一些信息,例如“hasData”或MyUser的实例。

这将导致类似于以下的结果:

@immutable
class MyUser {
  final String name;

  MyUser(this.name);
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Future<MyUser> user;

  void _logIn() {
    setState(() {
      user = new Future.delayed(const Duration(seconds: 3), () {
        return new MyUser("Toto");
      });
    });
  }

  Widget _buildForm(AsyncSnapshot<MyUser> snapshot) {
    var floatBtn = new RaisedButton(
      onPressed:
          snapshot.connectionState == ConnectionState.none ? _logIn : null,
      child: new Icon(Icons.save),
    );
    var action =
        snapshot.connectionState != ConnectionState.none && !snapshot.hasData
            ? new Stack(
                alignment: FractionalOffset.center,
                children: <Widget>[
                  floatBtn,
                  new CircularProgressIndicator(
                    backgroundColor: Colors.red,
                  ),
                ],
              )
            : floatBtn;

    return new ListView(
      padding: const EdgeInsets.all(15.0),
        children: <Widget>[
          new ListTile(
            title: new TextField(),
          ),
          new ListTile(
            title: new TextField(obscureText: true),
          ),
          new Center(child: action)
        ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return new FutureBuilder(
      future: user,
      builder: (context, AsyncSnapshot<MyUser> snapshot) {
        if (snapshot.hasData) {
          return new Scaffold(
            appBar: new AppBar(
              title: new Text("Hello ${snapshot.data.name}"),
            ),
          );
        } else {
          return new Scaffold(
            appBar: new AppBar(
              title: new Text("Connection"),
            ),
            body: _buildForm(snapshot),
          );
        }
      },
    );
  }
}

1
很酷,这两个例子在登录和其他情况下都会很有用。使用对话框处理进度比我的版本好看,而FutureBuilder比我的解决方案更优雅。谢谢你的帮助! - Ricardo Bocchi
@RicardoBocchi 是的 - Shady Aziza
我认为对话框在实际示例中不起作用,用户在 _login() 返回后将被重定向的方式令人困惑。你的第二个示例似乎更方便。非常完美。 - Shady Aziza
1
好的,对话框功能正常,需要很少修改原始代码。例如,他可以在对话框关闭后跟随Navigator.pushNamed("/home") - Rémi Rousselet
FutureBuilder 很棒,但通常将应用程序拆分为不同的视图会更好。我宁愿选择对于身份验证来说是 Dialog 选项而不是 FutureBuilder。我把它放在这里主要是因为作者想要实现“值得知道”的效果。 - Rémi Rousselet
显示剩余8条评论

57

对我而言,一个不错的方法是在进行登录过程时,在底部显示一个SnackBar,这是我的一个示例:

enter image description here

以下是设置SnackBar的步骤。

为你的Scaffold定义一个全局键。

final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
将它添加到你的Scaffoldkey属性中。
return new Scaffold(
      key: _scaffoldKey,
.......

我的登录按钮onPressed回调:

onPressed: () {
                  _scaffoldKey.currentState.showSnackBar(
                      new SnackBar(duration: new Duration(seconds: 4), content:
                      new Row(
                        children: <Widget>[
                          new CircularProgressIndicator(),
                          new Text("  Signing-In...")
                        ],
                      ),
                      ));
                  _handleSignIn()
                      .whenComplete(() =>
                      Navigator.of(context).pushNamed("/Home")
                  );
                }

这取决于您希望如何构建布局,我不确定您心中有什么想法。

编辑

您可能希望以这种方式设置,我使用了 Stack 实现了这个结果,并根据 onPressed 来显示或隐藏我的指示器

这里输入图片描述

class TestSignInView extends StatefulWidget {
  @override
  _TestSignInViewState createState() => new _TestSignInViewState();
}


class _TestSignInViewState extends State<TestSignInView> {
  bool _load = false;
  @override
  Widget build(BuildContext context) {
    Widget loadingIndicator =_load? new Container(
      color: Colors.grey[300],
      width: 70.0,
      height: 70.0,
      child: new Padding(padding: const EdgeInsets.all(5.0),child: new Center(child: new CircularProgressIndicator())),
    ):new Container();
    return new Scaffold(
      backgroundColor: Colors.white,
      body:  new Stack(children: <Widget>[new Padding(
        padding: const EdgeInsets.symmetric(vertical: 50.0, horizontal: 20.0),
        child: new ListView(

          children: <Widget>[
            new Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center
              ,children: <Widget>[
            new TextField(),
            new TextField(),

            new FlatButton(color:Colors.blue,child: new Text('Sign In'),
                onPressed: () {
              setState((){
                _load=true;
              });

                  //Navigator.of(context).push(new MaterialPageRoute(builder: (_)=>new HomeTest()));
                }
            ),

            ],),],
        ),),
        new Align(child: loadingIndicator,alignment: FractionalOffset.center,),

      ],));
  }

}

嗨,那就是我想做的,但我没有得到我需要的布局。Stack就是答案。关于StatefulWidget,当进度状态改变时,构建所有视图是否正确? - Ricardo Bocchi
在我的代码中,当 _loading 改变时,所有的视图都会被重建。是这样吗? - Ricardo Bocchi
是的,每次调用setState时整个树都会被重新构建。 - Shady Aziza
1
使用模态框可能更加简单直观。您只需在请求开始时推送加载对话框,完成后弹出即可。它还具有防止进一步用户输入的优点。 - Rémi Rousselet
2
好的,让我来做点什么。 - Rémi Rousselet
显示剩余4条评论

48
创建一个布尔类型变量isLoading,并将其设置为false。使用三目运算符,在用户点击登录按钮时将isLoading的状态设置为true。这样将会在登录按钮的位置得到一个圆形加载指示器。
 isLoading ? new PrimaryButton(
                      key: new Key('login'),
                      text: 'Login',
                      height: 44.0,
                      onPressed: setState((){isLoading = true;}))
                  : Center(
                      child: CircularProgressIndicator(),
                    ),

你可以查看屏幕截图,看看在点击登录之前它是什么样子。 这里输入图片描述

在点击登录之后 这里输入图片描述

同时,您可以运行登录过程并登录用户。如果用户凭据错误,则再次将isLoadingsetState设置为false,以便加载指示器变得不可见,并且登录按钮对用户可见。 顺便说一下,代码中使用的primaryButton是我自定义的按钮。你可以在button中使用同样的OnPressed


我从未遇到过需要处理双击的情况,因为在单击时它会变成加载指示器。根据您评论中的理解,我认为我们可以使用手势检测器包装自定义按钮,然后您可以在那里处理双击。 - Harsha pulikollu
三元运算符在哪里使用?你的例子看起来很聪明,但不确定如何实现它。 - Bikram Pahi
在构建方法中使用上述代码片段,您可以在需要(登录)按钮的位置添加它。当用户单击该按钮时,bool(isLoading)变为true,并显示圆形加载指示器而不是按钮。 - Harsha pulikollu
这正是我想要实现的,但我做了完全相同的事情,却没有起作用。是否有更新改变了这种做法? - Mouradif
我使用这种方法。它很棒,我只需要一种将其抽象化和可重用的方式。 - Haider Malik
显示剩余4条评论

32

步骤1:创建对话框

   showAlertDialog(BuildContext context){
      AlertDialog alert=AlertDialog(
        content: new Row(
            children: [
               CircularProgressIndicator(),
               Container(margin: EdgeInsets.only(left: 5),child:Text("Loading" )),
            ],),
      );
      showDialog(barrierDismissible: false,
        context:context,
        builder:(BuildContext context){
          return alert;
        },
      );
    }

步骤二:进行调用

showAlertDialog(context);
await firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
Navigator.pop(context);

示例对话和登录表单

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
class DynamicLayout extends StatefulWidget{
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return new MyWidget();
    }
  }
showAlertDialog(BuildContext context){
  AlertDialog alert=AlertDialog(
    content: new Row(
        children: [
           CircularProgressIndicator(),
           Container(margin: EdgeInsets.only(left: 5),child:Text("Loading" )),
        ],),
  );
  showDialog(barrierDismissible: false,
    context:context,
    builder:(BuildContext context){
      return alert;
    },
  );
}

  class MyWidget extends State<DynamicLayout>{
  Color color = Colors.indigoAccent;
  String title='app';
  GlobalKey<FormState> globalKey=GlobalKey<FormState>();
  String email,password;
  login() async{
   var currentState= globalKey.currentState;
   if(currentState.validate()){
        currentState.save();
        FirebaseAuth firebaseAuth=FirebaseAuth.instance;
        try {
          showAlertDialog(context);
          AuthResult authResult=await firebaseAuth.signInWithEmailAndPassword(
              email: email, password: password);
          FirebaseUser user=authResult.user;
          Navigator.pop(context);
        }catch(e){
          print(e);
        }
   }else{

   }
  }
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar:AppBar(
        title: Text("$title"),
        ) ,
          body: Container(child: Form(
            key: globalKey,
            child: Container(
              padding: EdgeInsets.all(10),
              child: Column(children: <Widget>[
              TextFormField(decoration: InputDecoration(icon: Icon(Icons.email),labelText: 'Email'),
              // ignore: missing_return
              validator:(val){
                if(val.isEmpty)
                  return 'Please Enter Your Email';
              },
              onSaved:(val){
                email=val;
              },
              ),
                TextFormField(decoration: InputDecoration(icon: Icon(Icons.lock),labelText: 'Password'),
             obscureText: true,
                  // ignore: missing_return
                  validator:(val){
                    if(val.isEmpty)
                      return 'Please Enter Your Password';
                  },
                  onSaved:(val){
                    password=val;
                  },
              ),
                RaisedButton(color: Colors.lightBlue,textColor: Colors.white,child: Text('Login'),
                  onPressed:login),
            ],)
              ,),)
         ),
    );
  }
}

来自Ui的示例

输入图像描述


4
请稍微补充一些背景信息到您的答案中。 - Vendetta
这有几个例子。首先需要检查关于那个的教程。请参考以下链接。 http://techandroidhub.com/flutter-progress-indicators-tutorial/ - Sawan Modi

31

1. 不需要插件 (Without plugin)

    class IndiSampleState extends State<ProgHudPage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Demo'),
        ),
        body: Center(
          child: RaisedButton(
            color: Colors.blueAccent,
            child: Text('Login'),
            onPressed: () async {
              showDialog(
                  context: context,
                  builder: (BuildContext context) {
                    return Center(child: CircularProgressIndicator(),);
                  });
              await loginAction();
              Navigator.pop(context);
            },
          ),
        ));
  }

  Future<bool> loginAction() async {
    //replace the below line of code with your login request
    await new Future.delayed(const Duration(seconds: 2));
    return true;
  }
}

2. 使用插件

使用这个插件progress_hud

在pubspec.yaml文件中添加依赖项

dev_dependencies:
  progress_hud: 

导入包

import 'package:progress_hud/progress_hud.dart';

以下是示例代码,用于显示和隐藏指示器

class ProgHudPage extends StatefulWidget {
  @override
  _ProgHudPageState createState() => _ProgHudPageState();
}

class _ProgHudPageState extends State<ProgHudPage> {
  ProgressHUD _progressHUD;
  @override
  void initState() {
    _progressHUD = new ProgressHUD(
      backgroundColor: Colors.black12,
      color: Colors.white,
      containerColor: Colors.blue,
      borderRadius: 5.0,
      loading: false,
      text: 'Loading...',
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('ProgressHUD Demo'),
        ),
        body: new Stack(
          children: <Widget>[
            _progressHUD,
            new Positioned(
                child: RaisedButton(
                  color: Colors.blueAccent,
                  child: Text('Login'),
                  onPressed: () async{
                    _progressHUD.state.show();
                    await loginAction();
                    _progressHUD.state.dismiss();
                  },
                ),
                bottom: 30.0,
                right: 10.0)
          ],
        ));
  }

  Future<bool> loginAction()async{
    //replace the below line of code with your login request
    await new Future.delayed(const Duration(seconds: 2));
    return true;
  }
}

13
请勿投票否定,有些人不想处理UI的细节,我就是其中之一,所以这个插件非常方便。 - Vladtn
3
API中的进度条已经足够,添加依赖会增加构建大小。Flutter的构建已经过于庞大。 - prashant0205
你真的应该将这个添加为开发依赖吗? - George
请查看最新的示例 https://pub.dartlang.org/packages/progress_hud#-example-tab- - Shyju M
1
在导航到下一个屏幕之前,请使用Navigator.pop(context); @MohammadMeshkani - Shyju M
显示剩余12条评论

13
我采用了以下方法,使用一个简单的模态进度指示器小部件来包装在异步调用期间想要使其模态的任何内容。
该软件包中的示例还解决了如何在进行异步调用以验证表单时处理表单验证的问题(有关此问题的详细信息,请参见flutter/issues/9688)。例如,在不离开表单的情况下,可以使用此异步表单验证方法来验证新用户名是否与数据库中的现有用户名相匹配。

https://pub.dartlang.org/packages/modal_progress_hud

这是该软件包提供的示例演示(附带源代码):

async form validation with modal progress indicator

这个例子可以适用于其他模态进度指示器行为(比如不同的动画、在模态窗口中添加额外文本等)。


4

这是使用堆栈的解决方案:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';

final themeColor = new Color(0xfff5a623);
final primaryColor = new Color(0xff203152);
final greyColor = new Color(0xffaeaeae);
final greyColor2 = new Color(0xffE8E8E8);

class LoadindScreen extends StatefulWidget {
  LoadindScreen({Key key, this.title}) : super(key: key);
  final String title;
  @override
  LoginScreenState createState() => new LoginScreenState();
}

class LoginScreenState extends State<LoadindScreen> {
  SharedPreferences prefs;

  bool isLoading = false;

  Future<Null> handleSignIn() async {
    setState(() {
      isLoading = true;
    });
    prefs = await SharedPreferences.getInstance();
    var isLoadingFuture = Future.delayed(const Duration(seconds: 3), () {
      return false;
    });
    isLoadingFuture.then((response) {
      setState(() {
        isLoading = response;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(
            widget.title,
            style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
          ),
          centerTitle: true,
        ),
        body: Stack(
          children: <Widget>[
            Center(
              child: FlatButton(
                  onPressed: handleSignIn,
                  child: Text(
                    'SIGN IN WITH GOOGLE',
                    style: TextStyle(fontSize: 16.0),
                  ),
                  color: Color(0xffdd4b39),
                  highlightColor: Color(0xffff7f7f),
                  splashColor: Colors.transparent,
                  textColor: Colors.white,
                  padding: EdgeInsets.fromLTRB(30.0, 15.0, 30.0, 15.0)),
            ),

            // Loading
            Positioned(
              child: isLoading
                  ? Container(
                      child: Center(
                        child: CircularProgressIndicator(
                          valueColor: AlwaysStoppedAnimation<Color>(themeColor),
                        ),
                      ),
                      color: Colors.white.withOpacity(0.8),
                    )
                  : Container(),
            ),
          ],
        ));
  }
}

3
你可以给进度指示器添加透明度并使其居中。
Future<Null> _submitDialog(BuildContext context) async {
  return await showDialog<Null>(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return SimpleDialog(
          elevation: 0.0,
          backgroundColor: Colors.transparent,
          children: <Widget>[
            Center(
              child: CircularProgressIndicator(),
            )
          ],
        );
      });
}

3
{
isloading? progressIos:Container()

progressIos(int i) {
    return Container(
        color: i == 1
            ? AppColors.liteBlack
            : i == 2 ? AppColors.darkBlack : i == 3 ? AppColors.pinkBtn : '',
        child: Center(child: CupertinoActivityIndicator()));
  }
}

1

居中显示:

Column(
    mainAxisAlignment: MainAxisAlignment.center,
    mainAxisSize: MainAxisSize.max,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
        Row(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.max,
            children: [CircularProgressIndicator()])
      ])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接