如何在Flutter中的WebView中显示加载指示器?

34

我希望在网页视图数据显示在屏幕上之前先显示“加载中”。如何实现?

以下是我的代码:

class WebDetailPage extends StatelessWidget {
  final String title;
  final String webUrl;

  final Completer<WebViewController> _controller =
      Completer<WebViewController>();

  WebDetailPage({
    @required this.title,
    @required this.webUrl,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colour.white,
        title: Text(title, style: TextStyle(color: Colour.midnightBlue)),
        leading: IconButton(
            icon: Icon(Icons.arrow_back, color: Colour.midnightBlue),
            onPressed: () => Navigator.of(context).pop()),
      ),
      body: Center(
        child: WebView(
          initialUrl: webUrl,
          javascriptMode: JavascriptMode.unrestricted,
          onWebViewCreated: (WebViewController webViewController) {
            _controller.complete(webViewController);
          },
        ),
      )
    );
  }
}

有人可以帮我解决这个问题吗?因为我已经搜寻和研究了很多,但仍然找不到解决办法。

13个回答

86

完整示例

class WebViewState extends State<WebViewScreen>{

  String title,url;
  bool isLoading=true;
  final _key = UniqueKey();
  
  WebViewState(String title,String url){
    this.title=title;
    this.url=url;
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: new AppBar(
          title: Text(this.title,style: TextStyle(fontWeight: FontWeight.w700)),centerTitle: true
      ),
      body: Stack(
        children: <Widget>[
          WebView(
            key: _key,
            initialUrl: this.url,
            javascriptMode: JavascriptMode.unrestricted,
            onPageFinished: (finish) {
              setState(() {
                isLoading = false;
              });
            },
          ),
          isLoading ? Center( child: CircularProgressIndicator(),)
                    : Stack(),
        ],
      ),
    );
  }

}

我只是使用Stack小部件,以便在webview上方设置加载指示器。当webview的onPageFinished被调用时,我会将isLoading=false变量值设置为false,并设置透明容器。


如果使用这种方式,WebView 就无法滚动。返回 Stack() 而不是 Container() 的答案在我的情况下有效。 - Varsha Prabhakar
1
调用setState会不会强制Web视图重新构建自身,从而导致性能问题? - Siddy Hacks
这仅在打开应用程序时添加指示器,而不是在应用程序内导航时添加。那么应用程序内的转换呢? - Daniil

12

完整示例

class WebViewState extends State<WebViewScreen>{

  String title,url;
  bool isLoading=true;
  final _key = UniqueKey();

  WebViewState(String title,String url){
    this.title=title;
    this.url=url;
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: new AppBar(
          title: Text(this.title,style: TextStyle(fontWeight: FontWeight.w700)),centerTitle: true
      ),
      body: Stack(
        children: <Widget>[
          WebView(
            key: _key,
            initialUrl: this.url,
            javascriptMode: JavascriptMode.unrestricted,
            onPageFinished: (finish) {
              setState(() {
                isLoading = false;
              });
            },
          ),
          isLoading ? Center( child: CircularProgressIndicator(),)
                    : Stack(),
        ],
      ),
    );
  }

}

1
这对我有用,因为Sanjayrajsinh的另一个答案使用了Container,这意味着您无法在WebView小部件中滚动网页。此解决方案在三元否定中使用了Stack,现在我可以在WebView中滚动网页。 - Munes

5

如果有人在使用webview_flutter版本4.0.2时遇到问题,我们必须使用WebViewControllersetNavigationDelegate并设置onPageFinished回调函数。

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class PrivacyPolicyPage extends StatefulWidget {
  static const routeName = "/privacy_policy";

  const PrivacyPolicyPage({super.key});

  @override
  State<PrivacyPolicyPage> createState() => _PrivacyPolicyPageState();
}

class _PrivacyPolicyPageState extends State<PrivacyPolicyPage> {
  late final WebViewController _controller;
  bool _loading = true;

  @override
  void initState() {
    _controller = WebViewController.fromPlatformCreationParams(
        const PlatformWebViewControllerCreationParams())
      ..setNavigationDelegate(NavigationDelegate(
          onPageFinished: (_) => setState(() {
                _loading = false;
              })))
      ..setJavaScriptMode(JavaScriptMode.disabled)
      ..loadFlutterAsset("assets/privacy_policy.html");
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Privacy Policy')),
      body: Stack(children: [
        WebViewWidget(controller: _controller),
        if (_loading) const Center(child: CircularProgressIndicator())
      ]),
    );
  }
}


5

只需使用 Stack 和 Visibility Widget 即可。

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class MyWebView extends StatefulWidget {
final String url;
const MyWebView({Key? key, this.url = ''}) : super(key: key);

@override
State<MyWebView> createState() => _MyWebViewState();
}

class _MyWebViewState extends State<MyWebView> {
  bool isLoading = true;
  @override
  void initState() {
  super.initState();
  if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
  if (Platform.isIOS) WebView.platform = CupertinoWebView();
}

@override
Widget build(BuildContext context) {
return SafeArea(
  child: Scaffold(
    appBar: AppBar(
      backgroundColor: Colors.transparent,
      elevation: 0,
    ),
    body: Stack(
      children: [
        WebView(
          initialUrl: widget.url,
          onPageFinished: (finish) {
            setState(() {
              isLoading = false;
            });
          },
          javascriptMode: JavascriptMode.unrestricted,
        ),
        Visibility(
          visible: isLoading,
          child: const Center(
            child: CircularProgressIndicator(),
          ),
        )
      ],
    ),
    bottomNavigationBar: BottomAppBar(
      child: Row(),
      ),
    ),
  );
 }
}

5
您可以使用Future Builder轻松解决此问题。是的,您听到的是正确的。
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() => runApp(MaterialApp(home: MyApp()));

class MyApp extends StatelessWidget {

  static Future<String> get _url async {
    await Future.delayed(Duration(seconds: 1));
    return 'https://flutter.dev/';
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    body: Center(
      child:FutureBuilder(
        future: _url,
        builder: (BuildContext context, AsyncSnapshot snapshot) => snapshot.hasData
        ? WebViewWidget(url: snapshot.data,)
        : CircularProgressIndicator()),
  ),);
}

class WebViewWidget extends StatefulWidget {
  final String url;
  WebViewWidget({this.url});

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

class _WebViewWidget extends State<WebViewWidget> {
  WebView _webView;
  @override
  void initState() {
    super.initState();
     _webView = WebView(
      initialUrl: widget.url,
      javascriptMode: JavascriptMode.unrestricted,
    );
  }

  @override
  void dispose() {
    super.dispose();
    _webView = null;
  }

  @override
  Widget build(BuildContext context) => _webView;
}

4

Google Codelabs 示例

在我看来,这个解决方案是最简单且用户体验很棒的

欲了解更多信息,请查看监听页面加载事件的官方示例。

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({super.key});

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

1
非常感谢@genericUser。这种添加加载器的方式可以提高您的应用程序的用户体验。 - ali sampson

2
我们可以使用IndexedStack小部件根据索引来切换小部件。我们还利用webview的onPageStarted和onPageFinished属性。利用状态管理,当页面开始加载时,我们更改索引值,并在页面加载完成时也进行更改。请注意保留HTML标签。
num pos = 1;

在构建方法中

return Scaffold(
        body: IndexedStack(index: pos, children: <Widget>[
      WebView(
        initialUrl: 'http://pub.dev/',
        javascriptMode: JavascriptMode.unrestricted,
        onPageStarted: (value) {
          setState(() {
            pos = 1;
          });
        },
        onPageFinished: (value) {
          setState(() {
            pos = 0;
          });
        },
      ),
      Container(
        child: Center(child: CircularProgressIndicator()),
      ),
    ]));

不确定为什么,但这个解决方案似乎会给我带来非常大的性能问题。需要注意。 - Tom O

1

完整示例

使用Stack和Visibility Widget

  bool showLoader = true;
    Stack(
       children: [
         Container(
           height: _updatedHeight,
           child: GestureDetector(
             onHorizontalDragUpdate: (updateDetails) {},
             child: Container(
               margin: EdgeInsets.only(right: 16,left: 16),
               width: MediaQuery.of(context).size.width,
               child: WebView(
                 initialUrl: "url",
                javascriptMode: JavascriptMode.unrestricted,
                 onWebViewCreated: (WebViewController webViewController) {
                   _completerController.complete(webViewController);
                   mainWebController = webViewController;
                 },
                 onProgress: (int progress) {
                   if(progress == 100){
                     setState(() {
                       showLoader = false;
                     });
                   }
                 },
                 onPageStarted: (String url) {
                //   print("WebView :: onPageStarted :: $url");
                 },
                 onPageFinished: (url) async {
                   double documentElementHeight = double.parse(
                       await mainWebController.runJavascriptReturningResult("document.documentElement.scrollHeight;"));
                      // print("WebView :: onPageFinished :: documentElementHeight = $documentElementHeight");
                       setState(() {
                         _updatedHeight = documentElementHeight; 
                       });
                      });
                    },
                 navigationDelegate: getNavigationDelegate,
               ),
             ),
           ),
         ),
        Visibility(
         visible: showLoader,
          child:Padding(
            padding: const EdgeInsets.only(top: 50),
            child: Container(
             width: MediaQuery.of(context).size.width,
             child: Center(
               child: AWProgressIndicatorWidget(),
             ),
         ),
          )
        )
       ],
     )

0

您可以使用 BLOC、Stream 和无状态 Widget


import 'dart:async';

import 'package:rxdart/subjects.dart';

class LoadingWebPageBloc  {
//Controllers
  final BehaviorSubject<bool> _loadingWebPageController = BehaviorSubject<bool>.seeded(true);

  //Sinks
  Function(bool) get changeLoadingWebPage => _loadingWebPageController.sink.add;

  //Streams
  Stream<bool> get loadingWebPageStream => _loadingWebPageController.stream.asBroadcastStream();

  @override
  void dispose() {
    _loadingWebPageController.close();
    super.dispose();
  }
}

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class CustomWebPagePreview extends StatelessWidget {
  final String url;
  CustomWebPagePreview({@required this.url});

  final LoadingWebPageBloc loadingWebPageBloc = LoadingWebPageBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: appBar,
        body: Container(
          child: Stack(
            children: <Widget>[
              WebView(
                initialUrl: url,
                javascriptMode: JavascriptMode.unrestricted,
                onPageStarted: (value) {
                  loadingWebPageBloc.changeloading(true);
                },
                onPageFinished: (value) {
                  loadingWebPageBloc.changeloading(false);
                },
              ),
              StreamBuilder<bool>(
                stream: loadingWebPageBloc.loading,
                initialData: true,
                builder: (context, snap) {
                  if (snap.hasData && snap.data == true) {
                    return Center(
                      child: CircularProgressIndicator(),
                    );
                  }
                  return SizedBox();
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

使用 context.listen 比 StreamBuilder 更容易。 - genericUser

0
对于希望拥有整洁代码的开发者,可以尝试以下方法:
main.dart 文件中:
import 'package:flutter/material.dart';
import 'package:ProjectName/src/app.dart'; //replace with your project name
 
void main() => runApp(MyApp());

关于 app.dart

import 'package:flutter/material.dart';
import 'package:ProjectName/src/webview_container.dart'; //replace with your project name

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
    home: Scaffold(
        body: WebViewClass('https://www.testurl.com', 'test') //replace with your url
       )
      );
  }
}

on webview_container.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewClass extends StatefulWidget {
  final url;
  final title;
  WebViewClass(this.url, this.title);


  @override
  createState() => WebViewState(this.url, this.title);
}
class WebViewState extends State<WebViewClass>{

  var _url;
  var _title;

  int position = 1 ;

  final key = UniqueKey();
  WebViewState(this._url, this._title);

  doneLoading(String A) {
    setState(() {
      position = 0;
    });
  }

  startLoading(String A){
    setState(() {
      position = 1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
    child: Scaffold(
      body: IndexedStack(
      index: position,
      children: <Widget>[

      WebView(
        zoomEnabled: false, //I have disabled zoom functionality on the app
        initialUrl: _url,
        javascriptMode: JavascriptMode.unrestricted,
        key: key ,
        onPageFinished: doneLoading,
        onPageStarted: startLoading,
        ),

       Container(
        color: Colors.white,
        child: Center(
          child: CircularProgressIndicator()),
        ),
        
      ])
  ),
    );
  }
}

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