clickbait
clickbait

Reputation: 2998

Is there a way to load, render, and persist webpages (or WebViews) in memory, like browsers do, in a Flutter app?

I’m trying to make a Flutter app that lets the user open up preloaded webpages from a list. The home screen should have a list of online articles with image previews (client-side screenshots) of them, and clicking on a list item should open the webpage in a WebView or an InAppWebView for the user to read.

None of that works because I couldn’t figure out how to pre-render webpages in order to (1) take screenshots of them without displaying them and (2) make the pages show up instantly, without having to load, when the user wants to view them.

My attempted solution was to use a non-widget class to instantiate InAppWebView instances, take screenshot of their pages when they finishes loading, and store the InAppWebView widgets in a List to persist the widgets so they can be retrieved at any time. This method fails because InAppWebViews don’t load pages unless they’re in the widget tree and visible on the screen. The screenshots never get taken because the WebViews never load.

Bad solution attempt:

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:screenshot/screenshot.dart';

class WebView {
  static final Map globalInstancesPool = {};

  static void dumpPool() {
    globalInstancesPool.clear();
  }

  late final InAppWebView widget;

  Image? previewScreenshot;

  void _takeScreenshot(Function? callback) {
    ScreenshotController screenshotController = ScreenshotController();
    screenshotController
        .captureFromWidget(
            Positioned(top: 0, left: 0, right: 0, bottom: 0, child: widget))
        .then((Uint8List captureBytes) {
      previewScreenshot = Image.memory(captureBytes);
      if (callback == null) return;
      callback(previewScreenshot!);
    });
  }

  WebView({required String url, Function? onScreenshotReady}) {
    widget = InAppWebView(
      initialUrlRequest: URLRequest(url: Uri.tryParse(url)),
      initialOptions: InAppWebViewGroupOptions(
          crossPlatform: InAppWebViewOptions(
        cacheEnabled: true,
        clearCache: false,
      )),
      onLoadStop: (a, b) {
        _takeScreenshot(onScreenshotReady);
      },
    );
  }

  factory WebView.newOrExisting(
      {required String url, Function? onScreenshotReady}) {
    if (globalInstancesPool.containsKey(url)) {
      return globalInstancesPool[url]!;
    }
    WebView newWebViewInstance =
        WebView(url: url, onScreenshotReady: onScreenshotReady);
    globalInstancesPool[url] = newWebViewInstance;
    return newWebViewInstance;
  }
}

I tried inserting all the WebViews into Offstage widgets, but they don’t load. The closest I got to making it work is to just display the WebViews and size them very small (1x1 pixel). I want to avoid this technique, mainly for performance reasons.

I’ve read GitHub issues that ask about the same thing and haven’t found any working solutions other than the 1x1-pixel WebView trick:

https://github.com/pichillilorenzo/flutter_inappwebview/issues/1081

https://github.com/flutter/flutter/issues/64402

https://github.com/flutter/flutter/issues/51875

A better solution I’d like (but have no idea how to do) is to store the pre-rendered pages themselves without having a bunch of WebView widgets. So it’s just 1 WebView, and the pre-rendered pages get “injected” into the WebView. Thus far, I’ve yet to find any tutorials for doing this in Flutter.

This task may seem obscure but it’s just what browsers do:

All I want to do is preload multiple webpages and have them ready to use. Functionality that every major browser has. How do you accomplish this in Flutter?

Upvotes: 3

Views: 1746

Answers (1)

user11756851
user11756851

Reputation:

you could try by wraping the webview within a

    RepaintBoundary(key: key)

make sure to assign a unique key so you can call

    RenderObject? object = key.currentContext?.findRenderObject();

and then call

    RenderRepaintBoundary boundary = object as RenderRepaintBoundary;
    ui.Image image = await boundary.toImage();
    ByteData? byteData =
          await image.toByteData(format: ui.ImageByteFormat.png);

      var bytes = byteData!.buffer.asUint8List(); 

when the onPageFinished: (String url) {}, is called from the webview controller from the navigation delegate

controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..setBackgroundColor(const Color(0x00000000))
  ..setNavigationDelegate(
    NavigationDelegate(
      onProgress: onPageProgress,
      onPageStarted: (String url) {},
      onPageFinished: (String url) {},
      onWebResourceError: (WebResourceError error) async {
        await Future.delayed(Durations.d50);
        popAndShowError();
      },
      onNavigationRequest: (NavigationRequest request) {
        return NavigationDecision.navigate;
      },
    ),
  )
  ..loadRequest(Uri.parse(uri));

Upvotes: 1

Related Questions