flutterwebview-flutter

How do I show loading screen until Javascript finished executing on the loaded page?


I am new to Flutter and trying to work on a problem that involves loading a webpage using webview_flutter plugin.

My goal is following

  1. Load the webpage.
  2. Once loaded, execute series of Javascript code, to remove the page elements not needed.
  3. Show the page after JavaScript execution is completed.

Until 1. and 2. are going on, I want to show a full page Loading ... indicator, so that user waits until the entire page is loaded and JS executions are complete.

My attempt includes using showDialog(), in addition to keeping a boolean state variable pageLoaded. However, when I run the application, the showDialog() works well until page content is loaded, however, the second dialog appears after that and stays on the main page forever.

I do not like this approach since I am making use of 2 loading screens as I do not know a better way to do this. Also, the second loading screen should go away once pageLoaded is set to true, however, that is not happening as well. This makes me feel that I am missing something fundamental here.

I recorded my attempt and uploaded on YouTube to share what I have right now.

My attempt is available on github at https://github.com/hhimanshu/webview_images_render_intercept/tree/h2/so.

The exact file that I am talking is main.dart, and I have pasted the content here as well

import 'dart:async';

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

void main() => runApp(const MaterialApp(home: PageLoadApp()));

class PageLoadApp extends StatefulWidget {
  const PageLoadApp({Key? key}) : super(key: key);

  @override
  State<PageLoadApp> createState() => _PageLoadAppState();
}

class _PageLoadAppState extends State<PageLoadApp> {
  bool pageLoaded = false;

  void onPageLoaded() {
    setState(() {
      pageLoaded = true;
    });
  }

  static Future<String> get _url async {
    await Future.delayed(const Duration(seconds: 10));
    return 'https://www.canada.ca/en/immigration-refugees-citizenship/corporate/publications-manuals/discover-canada/read-online/canadas-history.html';
  }

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

class WebViewWidget extends StatefulWidget {
  final String url;
  final bool pageLoaded;
  final Function onPageLoaded;

  const WebViewWidget(
      {required this.url,
      required this.onPageLoaded,
      required this.pageLoaded});

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

class _WebViewWidget extends State<WebViewWidget> {
  late WebView _webView;
  final Completer<WebViewController> _controller =
      Completer<WebViewController>();

  Iterable<Future<void>> cleanupLoadedHtmlPage(WebViewController controller) {
    const List<String> javascriptToExecute = [
      'document.getElementById("wb-lng").hidden = true;',
      'document.getElementById("wb-srch").hidden = true',
      'document.getElementsByClassName("gcweb-menu")[0].hidden = true',
      'document.getElementsByClassName("mwsalerts")[0].hidden = true',
      'document.getElementById("wb-bc").hidden = true',
      'document.getElementsByClassName("pagination")[0].hidden = true',
      'document.getElementsByClassName("pagedetails")[0].hidden = true',
      'document.getElementsByClassName("global-footer")[0].hidden = true'
    ];
    return javascriptToExecute.map((js) {
      print("removing $js");
      return controller.runJavascript(js);
    });
  }

  @override
  void initState() {
    super.initState();
    _webView = WebView(
      initialUrl: widget.url,
      javascriptMode: JavascriptMode.unrestricted,
      onWebViewCreated: (WebViewController webViewController) {
        _controller.complete(webViewController);
      },
      onProgress: (int progress) {
        print('WebView is loading (progress : $progress%)');
      },
      onPageStarted: (String url) {
        print('Page started loading: $url');
      },
      onPageFinished: (String url) async {
        if (!widget.pageLoaded) {
          print("Showing Alert dialog");
          showDialog(
              context: context,
              builder: (BuildContext context) {
                return const AlertDialog(
                  title: Text('Loading ...'),
                  backgroundColor: Colors.red,
                );
              });
        } else {
          Navigator.pop(context);
        }

        print('Page finished loading: $url');

        final controller = await _controller.future;
        var cleanupFuture = cleanupLoadedHtmlPage(controller);
        Future.wait(cleanupFuture).then((value) {
          print("Clean up done => $value");
          widget.onPageLoaded();
        });
      },
    );
  }

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

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

I am seeking help to achieve my goal and learn how to do the following

  1. When the app is loaded, fetch the URL content
  2. When content is downloaded, run series of JavaScript code.
  3. When completed, show the web page with page elements removed from step 2.

Additionally, while step 1. and 2. are running a user should see a Loading ... indicator which cannot be dismissed. The indicator goes away automatically at step 3.

Please help me understand how to achieve this.

Thank you in advance


Solution

  • You can create a global variable isLoading and use the help of ValueNotifier and ValueListenableBuilder to check if the value is loading to show a CircularProgressIndicator() if it is. I've refactored your code.

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:webview_flutter/webview_flutter.dart';
    
    void main() => runApp(const MaterialApp(home: PageLoadApp()));
    final isLoading = ValueNotifier<bool>(true);
    
    class PageLoadApp extends StatefulWidget {
      const PageLoadApp({Key? key}) : super(key: key);
    
      @override
      State<PageLoadApp> createState() => _PageLoadAppState();
    }
    
    class _PageLoadAppState extends State<PageLoadApp> {
      static String get _url {
        // await Future.delayed(const Duration(seconds: 10));
        return 'https://www.canada.ca/en/immigration-refugees-citizenship/corporate/publications-manuals/discover-canada/read-online/canadas-history.html';
      }
    
      @override
      Widget build(BuildContext context) => Scaffold(
              body: Scaffold(
            body: ValueListenableBuilder<bool>(
                valueListenable: isLoading,
                builder: (context, value, _) {
                  return Scaffold(
                    body: Stack(
                      children: <Widget>[
                        WebViewWidget(
                          url: _url,
                        ),
                        (value == true
                            ? Scaffold(
                                body: Center(
                                  child: CircularProgressIndicator(),
                                ),
                              )
                            : Container()),
                      ],
                    ),
                  );
                  ;
                }),
          ));
    }
    
    class WebViewWidget extends StatefulWidget {
      final String url;
    
      const WebViewWidget({
        required this.url,
      });
    
      @override
      _WebViewWidget createState() => _WebViewWidget();
    }
    
    class _WebViewWidget extends State<WebViewWidget> {
      late WebView _webView;
      final Completer<WebViewController> _controller =
          Completer<WebViewController>();
    
      Iterable<Future<void>> cleanupLoadedHtmlPage(WebViewController controller) {
        const List<String> javascriptToExecute = [
          'document.getElementById("wb-lng").hidden = true;',
          'document.getElementById("wb-srch").hidden = true',
          'document.getElementsByClassName("gcweb-menu")[0].hidden = true',
          'document.getElementsByClassName("mwsalerts")[0].hidden = true',
          'document.getElementById("wb-bc").hidden = true',
          'document.getElementsByClassName("pagination")[0].hidden = true',
          'document.getElementsByClassName("pagedetails")[0].hidden = true',
          'document.getElementsByClassName("global-footer")[0].hidden = true'
        ];
        return javascriptToExecute.map((js) {
          print("removing $js");
          return controller.runJavascript(js);
        });
      }
    
      @override
      void initState() {
        super.initState();
        _webView = WebView(
          initialUrl: widget.url,
          javascriptMode: JavascriptMode.unrestricted,
          onWebViewCreated: (WebViewController webViewController) {
            _controller.complete(webViewController);
          },
          onProgress: (int progress) {},
          onPageStarted: (String url) {
            isLoading.value = true;
            print('Page started loading: $url');
          },
          onPageFinished: (String url) async {
            // Navigator.pop(context);
    
            print('Page finished loading: $url');
    
            final controller = await _controller.future;
            var cleanupFuture = cleanupLoadedHtmlPage(controller);
            Future.wait(cleanupFuture).then((value) {
              print("Clean up done => $value");
            });
            // for demo purposes, wait for 3 seconds
            await Future.delayed(Duration(seconds: 3));
            isLoading.value = false;
          },
        );
      }
    
      @override
      void dispose() {
        super.dispose();
        //_webView = null;
      }
    
      @override
      Widget build(BuildContext context) => _webView;
    }