Skip to content

d0lim/example-flutter-webview

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Example Flutter Webview

The codes in this repository are generated by GitHub Copilot Agent

Prompts

You are a Flutter coding agent. Your task is to build a hybrid mobile application using Flutter with InAppWebView that bridges native functionalities with a WebView-based frontend.

You must implement bi-directional communication between Dart and JavaScript, and support a set of key native features.

For each feature, follow the format:


Task: [Short title]

Instruction:

  • [Implementation step 1]
  • [Implementation step 2]
  • [Tips for integration with JS/native]

Expected Output (Flutter code snippet):

// Example Dart code

Examples:


Task: Message Bus (JS ↔ Dart)

Instruction:

  • Use addJavaScriptHandler with name native to receive messages from JS.
  • From JS, use window.flutter_inappwebview.callHandler('native', args) to send data.
  • From Dart, use evaluateJavascript() to send data to JS.
  • Add logs for debugging.

Expected Output:

InAppWebView(
  onWebViewCreated: (controller) {
    controller.addJavaScriptHandler(
      handlerName: 'native',
      callback: (args) {
        print("Received from JS: \$args");
        return { 'status': 'OK' };
      },
    );
    controller.evaluateJavascript(
      source: "window.postMessageFromFlutter = function(msg) { console.log(msg); }"
    );
  },
)

Task: Push Notifications → JS

Instruction:

  • Use firebase_messaging to listen for push notifications.
  • When message is received, encode it and send via JS CustomEvent.
  • Dispatch to WebView using evaluateJavascript.

Expected Output:

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  final payload = jsonEncode(message.data);
  webViewController.evaluateJavascript(
    source: "window.dispatchEvent(new CustomEvent('push', { detail: \$payload }));"
  );
});

Task: Inject Auth Token into WebView

Instruction:

  • Use flutter_secure_storage to retrieve token.
  • Inject token into headers when creating URLRequest.
  • On token expiration, notify JS to logout.

Expected Output:

final token = await FlutterSecureStorage().read(key: "auth_token");

InAppWebView(
  initialUrlRequest: URLRequest(
    url: Uri.parse("https://example.com"),
    headers: { "Authorization": "Bearer \$token" },
  ),
)

Task: Deep Linking

Instruction:

  • Use uni_links package to handle deep links.
  • Setup platform-specific configurations in iOS and Android.
  • Forward deep link data to WebView using JavaScript bridge.

Expected Output:

@override
void initState() {
  super.initState();
  initUniLinks();
}

Future<void> initUniLinks() async {
  // Handle app start from deep link
  Uri? initialLink = await getInitialUri();
  if (initialLink != null) {
    _handleDeepLink(initialLink);
  }

  // Handle app in foreground receiving deep link
  uriLinkStream.listen((Uri? uri) {
    if (uri != null) {
      _handleDeepLink(uri);
    }
  }, onError: (err) {
    print('Deep link error: $err');
  });
}

void _handleDeepLink(Uri uri) {
  final deepLinkData = jsonEncode({
    'path': uri.path,
    'queryParameters': uri.queryParameters,
  });

  if (webViewController != null) {
    webViewController!.evaluateJavascript(
      source: "window.dispatchEvent(new CustomEvent('deepLink', { detail: $deepLinkData }));"
    );
  } else {
    // Store for later when WebView is initialized
    _pendingDeepLink = deepLinkData;
  }
}

Task: File Upload Handler

Instruction:

  • Override WebView's file chooser with native file picker.
  • Use file_picker package for native file selection.
  • Support multiple file selection for different MIME types.

Expected Output:

InAppWebView(
  // ...other properties
  onReceivedHttpAuthRequest: (controller, challenge) async {
    // Handle HTTP authentication
  },
  onConsoleMessage: (controller, consoleMessage) {
    print("JS Console: ${consoleMessage.message}");
  },
  onCreateWindow: (controller, createWindowAction) async {
    // Handle new window creation
    return true;
  },
  onJsAlert: (controller, jsAlertRequest) async {
    // Handle JavaScript alerts
    return JsAlertResponse(handledByClient: true);
  },
  onFileChoose: (controller, params) async {
    List<String>? paths;
    List<PlatformFile>? files;

    try {
      if (params.allowMultiple) {
        final result = await FilePicker.platform.pickFiles(
          type: FileType.custom,
          allowedExtensions: _parseAcceptTypes(params.acceptTypes),
          allowMultiple: true,
        );
        files = result?.files;
        paths = files?.map((file) => file.path!).toList();
      } else {
        final result = await FilePicker.platform.pickFiles(
          type: FileType.custom,
          allowedExtensions: _parseAcceptTypes(params.acceptTypes),
        );
        files = result?.files;
        paths = files?.isNotEmpty == true ? [files!.first.path!] : null;
      }

      return FileChooserResponse(
        paths: paths,
        handledByClient: true,
      );
    } catch (e) {
      print("File picking error: $e");
      return FileChooserResponse(handledByClient: false);
    }
  },
)

List<String> _parseAcceptTypes(List<String>? acceptTypes) {
  if (acceptTypes == null || acceptTypes.isEmpty) {
    return [];
  }

  final extensions = <String>[];
  for (final type in acceptTypes) {
    if (type.startsWith('.')) {
      extensions.add(type.substring(1));
    } else if (type.contains('/')) {
      // Handle MIME types like 'image/*'
      final parts = type.split('/');
      if (parts.length == 2 && parts[1] == '*') {
        switch (parts[0]) {
          case 'image':
            extensions.addAll(['jpg', 'jpeg', 'png', 'gif', 'webp']);
            break;
          case 'video':
            extensions.addAll(['mp4', 'mov', 'avi', 'mkv']);
            break;
          case 'audio':
            extensions.addAll(['mp3', 'wav', 'ogg', 'aac']);
            break;
        }
      }
    }
  }

  return extensions;
}

Task: Geolocation Access

Instruction:

  • Use geolocator package to access device location.
  • Create JavaScript handler for location requests.
  • Handle permission requests and error states.

Expected Output:

// Add permission handlers in main app
Future<void> requestLocationPermission() async {
  LocationPermission permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
  }
}

// In WebView setup
void setupLocationBridge(InAppWebViewController controller) {
  controller.addJavaScriptHandler(
    handlerName: 'getLocation',
    callback: (args) async {
      try {
        // Check if high accuracy is requested
        bool highAccuracy = args.isNotEmpty && args[0] == true;

        final position = await Geolocator.getCurrentPosition(
          desiredAccuracy: highAccuracy ?
            LocationAccuracy.high : LocationAccuracy.medium
        );

        return {
          'status': 'OK',
          'latitude': position.latitude,
          'longitude': position.longitude,
          'accuracy': position.accuracy,
          'altitude': position.altitude,
          'speed': position.speed,
          'timestamp': position.timestamp?.millisecondsSinceEpoch,
        };
      } catch (e) {
        print("Location error: $e");
        return {
          'status': 'ERROR',
          'message': e.toString(),
        };
      }
    },
  );

  // Inject JS helper function
  controller.evaluateJavascript(source: '''
    window.getNativeLocation = function(highAccuracy = false) {
      return window.flutter_inappwebview.callHandler('getLocation', highAccuracy);
    };
  ''');
}

Task: Lifecycle Synchronization

Instruction:

  • Override app lifecycle methods.
  • Notify WebView of app state changes using JavaScript events.
  • Pause and resume WebView resources accordingly.

Expected Output:

class _WebViewScreenState extends State<WebViewScreen> with WidgetsBindingObserver {
  InAppWebViewController? _webViewController;
  AppLifecycleState? _lastState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    _lastState = state;

    if (_webViewController == null) return;

    switch (state) {
      case AppLifecycleState.resumed:
        _webViewController!.evaluateJavascript(
          source: "window.dispatchEvent(new CustomEvent('appLifecycle', { detail: { state: 'resumed' } }));"
        );
        _webViewController!.resume();
        break;
      case AppLifecycleState.paused:
        _webViewController!.evaluateJavascript(
          source: "window.dispatchEvent(new CustomEvent('appLifecycle', { detail: { state: 'paused' } }));"
        );
        _webViewController!.pause();
        break;
      case AppLifecycleState.inactive:
        _webViewController!.evaluateJavascript(
          source: "window.dispatchEvent(new CustomEvent('appLifecycle', { detail: { state: 'inactive' } }));"
        );
        break;
      case AppLifecycleState.detached:
        _webViewController!.evaluateJavascript(
          source: "window.dispatchEvent(new CustomEvent('appLifecycle', { detail: { state: 'detached' } }));"
        );
        break;
    }
  }
}

Task: Clipboard Access

Instruction:

  • Use flutter_clipboard package to access device clipboard.
  • Create JavaScript handlers for read/write operations.
  • Handle permissions and error states.

Expected Output:

void setupClipboardBridge(InAppWebViewController controller) {
  // Handler to read from clipboard
  controller.addJavaScriptHandler(
    handlerName: 'readClipboard',
    callback: (args) async {
      try {
        final data = await Clipboard.getData(Clipboard.kTextPlain);
        return {
          'status': 'OK',
          'text': data?.text ?? '',
        };
      } catch (e) {
        print("Clipboard read error: $e");
        return {
          'status': 'ERROR',
          'message': e.toString(),
        };
      }
    },
  );

  // Handler to write to clipboard
  controller.addJavaScriptHandler(
    handlerName: 'writeClipboard',
    callback: (args) async {
      try {
        if (args.isEmpty || args[0] == null) {
          return {
            'status': 'ERROR',
            'message': 'No text provided',
          };
        }

        await Clipboard.setData(ClipboardData(text: args[0].toString()));
        return {
          'status': 'OK',
        };
      } catch (e) {
        print("Clipboard write error: $e");
        return {
          'status': 'ERROR',
          'message': e.toString(),
        };
      }
    },
  );

  // Inject JS helper functions
  controller.evaluateJavascript(source: '''
    window.nativeClipboard = {
      readText: function() {
        return window.flutter_inappwebview.callHandler('readClipboard');
      },
      writeText: function(text) {
        return window.flutter_inappwebview.callHandler('writeClipboard', text);
      }
    };
  ''');
}

Task: Error Logging and Crash Reporting

Instruction:

  • Use firebase_crashlytics for error tracking.
  • Add JavaScript console interceptor for web errors.
  • Implement global error boundary in Dart.

Expected Output:

// Setup in main.dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp();

  // Pass all uncaught errors to Crashlytics
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;

  runZonedGuarded<Future<void>>(() async {
    runApp(MyApp());
  }, (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
  });
}

// In WebView class
void setupErrorLogging(InAppWebViewController controller) {
  // Log console messages to Crashlytics
  controller.onConsoleMessage = (controller, consoleMessage) {
    print("WebView Console: ${consoleMessage.message}");

    // Log errors to Crashlytics
    if (consoleMessage.messageLevel == ConsoleMessageLevel.ERROR) {
      FirebaseCrashlytics.instance.recordError(
        "WebView JS Error: ${consoleMessage.message}",
        StackTrace.current,
        reason: "JavaScript Console Error",
      );
    }
  };

  // Inject JS error handler
  controller.evaluateJavascript(source: '''
    window.addEventListener('error', function(event) {
      window.flutter_inappwebview.callHandler(
        'jsError',
        {
          message: event.message,
          source: event.filename,
          lineno: event.lineno,
          colno: event.colno,
          error: event.error ? event.error.toString() : null
        }
      );

      // Don't prevent default handling
      return false;
    });
  ''');

  // Handle JS errors in Dart
  controller.addJavaScriptHandler(
    handlerName: 'jsError',
    callback: (args) {
      if (args.isNotEmpty && args[0] is Map) {
        final errorData = args[0] as Map;
        FirebaseCrashlytics.instance.recordError(
          "JavaScript Error: ${errorData['message']}",
          StackTrace.current,
          reason: "JS Error at ${errorData['source']}:${errorData['lineno']}:${errorData['colno']}",
        );
      }
      return {'logged': true};
    },
  );
}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published