3

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.

2
  • Is it buildable? There is no 'title' parameter in 'BottomNavigationBar'. When I test a your code by removing not provided code, it works well. Commented Sep 8, 2020 at 7:25
  • @KuKu Yeah it's buildable. I checked my post and fixed a markdown syntax error. But it seems title is the AppBar property. Commented Sep 8, 2020 at 7:35

1 Answer 1

1

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,
                );
              }
          }
        });
  }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.