flutter: bottomNavigationBar cannot switch between html pages using WebView

347

You can copy paste run full code below
I simulate this case with the full code below
Step 1: Use AutomaticKeepAliveClientMixin

class _WebViewKeepAlive extends State<WebViewKeepAlive>
    with AutomaticKeepAliveClientMixin {
    
    @override
  bool get wantKeepAlive => true;
  
  @override
  Widget build(BuildContext context) {
    super.build(context);

Step 2: Do not direct put function in future attribute, future: LocalLoader().loadLocal(htmlFile), Please use below way

Future<String> _future;

 @override
  void initState() {
    _future = _getUrl(widget.url);
    super.initState();
  }
 
return FutureBuilder(
        future: _future, 

Step 3: I use PageView in this case

working demo

enter image description here

full code

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyPortalPage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyPortalPageState extends State<MyPortalPage> {
  int _currentIndex = 0;
  PageController _pageController = PageController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SizedBox.expand(
        child: PageView(
          controller: _pageController,
          children: <Widget>[
            Page1(),
            WebViewKeepAlive(url: "https://flutter.dev/"),
            WebViewKeepAlive(url: "https://stackoverflow.com/"),
            Center(child: Text("Settings")),
          ],
          onPageChanged: (int index) {
            print("onPageChanged");
            setState(() {
              _currentIndex = index;
            });
          },
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        selectedItemColor: Colors.amber[800],
        unselectedItemColor: Colors.blue,
        onTap: (index) {
          print("onItemSelected");
          setState(() => _currentIndex = index);
          _pageController.jumpToPage(index);
        },
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.apps),
            label: 'Challenges',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.people),
            label: 'Users',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.message),
            label: 'Messages',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'Settings',
          ),
        ],
      ),
    );
  }
}

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

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

class _Page1State extends State<Page1> with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView.builder(itemBuilder: (context, index) {
      return ListTile(
        title: Text('Lorem Ipsum'),
        subtitle: Text('$index'),
      );
    });
  }

  @override
  bool get wantKeepAlive => true;
}

class WebViewKeepAlive extends StatefulWidget {
  final String url;
  WebViewKeepAlive({Key key, this.url}) : super(key: key);

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

class _WebViewKeepAlive extends State<WebViewKeepAlive>
    with AutomaticKeepAliveClientMixin {

  Future<String> _future;

  @override
  bool get wantKeepAlive => true;

  Future<String> _getUrl(String url) async {
    await Future.delayed(Duration(seconds: 1), () {});
    return Future.value(url);
  }

  @override
  void initState() {
    _future = _getUrl(widget.url);
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return FutureBuilder(
        future: _future,
        builder: (context, AsyncSnapshot<String> snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.none:
              return Text('none');
            case ConnectionState.waiting:
              return Center(child: CircularProgressIndicator());
            case ConnectionState.active:
              return Text('');
            case ConnectionState.done:
              if (snapshot.hasError) {
                return Text(
                  '${snapshot.error}',
                  style: TextStyle(color: Colors.red),
                );
              } else {
                return WebView(
                  initialUrl: snapshot.data,
                  javascriptMode: JavascriptMode.unrestricted,
                );
              }
          }
        });
  }
}
Share:
347
kakyo
Author by

kakyo

The more you learn, the more you hesitate.

Updated on December 24, 2022

Comments

  • kakyo
    kakyo 11 months

    Goal

    I use bottomNavigationBar to switch between a few pages. Two of the pages use WebView to show local html files.

    Problem

    I found that when I switch between these pages, the pages with pure flutter widgets can load perfectly. But the HTML pages will only show the initially loaded page on switching.

    For example, If I have two navigator buttons leading to Page1 and Page2, then at runtime, if I tapped Page1 button first, then tapped Page2 button, then the WebView would still show Page1 instead of Page2. This is wrong.

    Code

    Here is the HTML page code I use

    class LocalLoader {
      Future<String> loadLocal(String filename) async {
        return await rootBundle.loadString('assets/doc/$filename');
      }
    }
    class HtmlPage extends StatelessWidget {
      final String htmlFile;
      HtmlPage({
        @required this.htmlFile
      });
      @override
      Widget build(BuildContext context) {
        return Container(
          child: FutureBuilder<String>(
            future: LocalLoader().loadLocal(htmlFile),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return WebView(
                  initialUrl: Uri.dataFromString(snapshot.data,
                      mimeType: 'text/html',
                      // CAUTION
                      // - required for non-ascii chars
                      encoding: Encoding.getByName("UTF-8")
                  ).toString(),
                  javascriptMode: JavascriptMode.unrestricted,
                );
              } else if (snapshot.hasError) {
                return Text("${snapshot.error}");
              } else {
                print('undefined behaviour');
              }
              return CircularProgressIndicator();
            },
          ),);
      }
    }
    

    Then with my bottomNavigationBar, I handle the tap event:

    class MyFlutterView extends StatefulWidget {
      @override
      _MyFlutterViewState createState() => _MyFlutterViewState();
    }
    class _MyFlutterViewState extends State<MyFlutterView> {
      final Keys keys = Keys();
      int _iSelectedDrawerItem = 3; // self
      int _iSelectedNavItem = 0;
      static List<Widget> _widgetOptions = <Widget>[
        MyFlutterPlaceholder(title: 'Index 0: MyFlutter'),
        MyPage(htmlFile: 'page1.html'),
        MyPage(htmlFile: 'page2.html'),
      ];
    
      void _onItemTapped(int index) {
        setState(() {
          _iSelectedNavItem = index;
        });
      }
      @override
      Widget build(BuildContext context) {
        final deviceSize = MediaQuery.of(context).size;
        final appBar = AppBar(
          backgroundColor: WidgetColors.menubar,
          title: Text('MyFlutter'),
        );
        return Scaffold(
          appBar: appBar,
          endDrawer: NavDrawer(
            keys: keys,
            iSelectedDrawerItem: _iSelectedDrawerItem,
          ),
          body: Container(
            decoration: BoxDecoration(
                gradient: WidgetColors.canvas,
            ),
            child: _widgetOptions.elementAt(_iSelectedNavItem),
          ),
          bottomNavigationBar: BottomNavigationBar(
              currentIndex : _iSelectedNavItem,
              type: BottomNavigationBarType.fixed,
              backgroundColor: WidgetColors.menubar,
              fixedColor: WidgetColors.myColor,
              // selectedItemColor: WidgetColors.myColor,
              unselectedItemColor: Colors.white,
              selectedIconTheme: IconThemeData(color: WidgetColors.myColor),
              // unselectedIconTheme: IconThemeData(color: Colors.white),
              items: [
                BottomNavigationBarItem(
                  label: 'MyFlutter',
                  icon: Icon(Icons.build)
                ),
                BottomNavigationBarItem(
                  label: 'Page1-HTML',
                  icon: Icon(Icons.help,),
                ),
                BottomNavigationBarItem(
                  label: 'Page2-HTML',
                  icon: Icon(Icons.info_outline_rounded),
                ),
              ],
              onTap: _onItemTapped),
        );
      }
    }
    
    

    I've also tried StatefulWidgets but the problem persists.

    Workaround

    The only workaround I have right now is to derive from the HtmlPage class for every single page I have, like this:

    class Page1 extends HtmlPage {
      Page1() : super(htmlFile: 'page1.html');
    }
    
    class Page2 extends HtmlPage {
      Page2() : super(htmlFile: 'page2.html');
    }
    

    After this, the HTML pages will switch and load as expected.

    Question

    How should I fix this? Should I work on loading the HTML file more explicitly? I thought setState would handle the loading for me automatically and this certainly applies to the pure flutter widget page (MyFlutterPlaceholder class in the code above).

    Also, I ensured that the url loading was called every time I switch page through the nav bar.