├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── FlutterWebviewPlugin.h │ ├── WebviewJavaScriptChannelHandler.h │ └── WebviewJavaScriptChannelHandler.m ├── .gitignore └── flutter_webview_plugin.podspec ├── example ├── ios │ ├── Flutter │ │ ├── flutter_assets │ │ │ ├── AssetManifest.json │ │ │ ├── FontManifest.json │ │ │ └── fonts │ │ │ │ └── MaterialIcons-Regular.ttf │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── AppDelegate.h │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── main.m │ │ ├── GeneratedPluginRegistrant.h │ │ ├── GeneratedPluginRegistrant.m │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ └── AppDelegate.m │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── Runner.xcscmblueprint │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ ├── .gitignore │ └── Podfile ├── android │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── app │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ └── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── java │ │ │ │ ├── com │ │ │ │ │ └── yourcompany │ │ │ │ │ │ └── flutter_webview_plugin_example │ │ │ │ │ │ └── MainActivity.java │ │ │ │ └── io │ │ │ │ │ └── flutter │ │ │ │ │ └── plugins │ │ │ │ │ └── GeneratedPluginRegistrant.java │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── .gitignore │ ├── settings.gradle │ ├── build.gradle │ ├── gradlew.bat │ └── gradlew ├── .gitignore ├── README.md ├── pubspec.yaml ├── android.iml ├── flutter_webview_plugin_example.iml └── lib │ └── main.dart ├── android ├── settings.gradle ├── gradle.properties ├── src │ ├── test │ │ ├── resources │ │ │ └── mockito-extensions │ │ │ │ └── org.mockito.plugins.MockMaker │ │ └── java │ │ │ └── com │ │ │ └── flutter_webview_plugin │ │ │ └── FlutterWebviewPluginTest.java │ └── main │ │ ├── res │ │ └── xml │ │ │ └── filepaths.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── flutter_webview_plugin │ │ ├── ObservableWebView.java │ │ ├── JavaScriptChannel.java │ │ ├── BrowserClient.java │ │ ├── FlutterWebviewPlugin.java │ │ └── WebviewManager.java ├── build.gradle └── flutter_webview_plugin.iml ├── android_test.sh ├── lib ├── flutter_webview_plugin.dart └── src │ ├── javascript_message.dart │ ├── javascript_channel.dart │ ├── webview_scaffold.dart │ └── base.dart ├── .gitignore ├── .github ├── workflows │ ├── flutter_ut.yml │ └── android_ut.yml └── ISSUE_TEMPLATE │ ├── SUPPORT.md │ ├── feature_request.md │ └── BUG.md ├── pubspec.yaml ├── scripts └── before_build_apks.sh ├── LICENSE ├── test └── flutter_webview_plugin_test.dart ├── travis.yml.bak ├── flutter_webview_plugin.iml ├── CHANGELOG.md ├── analysis_options.yaml └── README.md /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/AssetManifest.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_webview_plugin' 2 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | -------------------------------------------------------------------------------- /android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /android_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd example/android 3 | ./gradlew test -DflutterPath=$FLUTTER_HOME 4 | -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/FontManifest.json: -------------------------------------------------------------------------------- 1 | [{"fonts":[{"asset":"fonts/MaterialIcons-Regular.ttf"}],"family":"MaterialIcons"}] -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 3 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea 4 | .packages 5 | .pub/ 6 | build/ 7 | ios/.generated/ 8 | packages 9 | pubspec.lock 10 | .flutter-plugins 11 | .flutter-plugins-dependencies -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_assets/fonts/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Flutter/flutter_assets/fonts/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | PluginRegistry.java 10 | 11 | .project 12 | .settings 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /lib/flutter_webview_plugin.dart: -------------------------------------------------------------------------------- 1 | library flutter_webview_plugin; 2 | 3 | export 'src/base.dart'; 4 | export 'src/javascript_channel.dart'; 5 | export 'src/javascript_message.dart'; 6 | export 'src/webview_scaffold.dart'; 7 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercommunity/flutter_webview_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_webview_plugin_example 2 | 3 | Demonstrates how to use the flutter_webview_plugin plugin. 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](http://flutter.io/). 9 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, 8 | NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ios/Classes/FlutterWebviewPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | static FlutterMethodChannel *channel; 5 | 6 | @interface FlutterWebviewPlugin : NSObject 7 | @property (nonatomic, retain) UIViewController *viewController; 8 | @property (nonatomic, retain) WKWebView *webview; 9 | @end 10 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_webview_plugin_example 2 | description: Demonstrates how to use the flutter_webview_plugin plugin. 3 | version: 1.0.0+1 4 | publish_to: 'none' 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | flutter_webview_plugin: 13 | path: ../ 14 | 15 | flutter: 16 | uses-material-design: true 17 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | 13 | *.pbxuser 14 | *.mode1v3 15 | *.mode2v3 16 | *.perspectivev3 17 | 18 | !default.pbxuser 19 | !default.mode1v3 20 | !default.mode2v3 21 | !default.perspectivev3 22 | 23 | xcuserdata 24 | 25 | *.moved-aside 26 | 27 | *.pyc 28 | *sync/ 29 | Icon? 30 | .tags* 31 | 32 | -------------------------------------------------------------------------------- /lib/src/javascript_message.dart: -------------------------------------------------------------------------------- 1 | /// A message that was sent by JavaScript code running in a [WebView]. 2 | 3 | class JavascriptMessage { 4 | /// Constructs a JavaScript message object. 5 | /// 6 | /// The `message` parameter must not be null. 7 | const JavascriptMessage(this.message) : assert(message != null); 8 | 9 | /// The contents of the message that was sent by the JavaScript code. 10 | final String message; 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea 4 | .vscode 5 | .packages 6 | .pub/ 7 | .gradle/ 8 | build/ 9 | ios/.generated/ 10 | packages 11 | pubspec.lock 12 | .dart_tool/ 13 | 14 | example/ios/Podfile.lock 15 | **/Flutter/App.framework/ 16 | **/Flutter/Flutter.framework/ 17 | **/Flutter/Generated.xcconfig/ 18 | **/Flutter/flutter_assets/ 19 | example/ios/Flutter/flutter_export_environment.sh 20 | android/.project 21 | android/.settings/ 22 | -------------------------------------------------------------------------------- /example/ios/Runner/GeneratedPluginRegistrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #ifndef GeneratedPluginRegistrant_h 6 | #define GeneratedPluginRegistrant_h 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface GeneratedPluginRegistrant : NSObject 13 | + (void)registerWithRegistry:(NSObject*)registry; 14 | @end 15 | 16 | NS_ASSUME_NONNULL_END 17 | #endif /* GeneratedPluginRegistrant_h */ 18 | -------------------------------------------------------------------------------- /.github/workflows/flutter_ut.yml: -------------------------------------------------------------------------------- 1 | name: Flutter Unit Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Linux Flutter Unit Tests 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-java@v1 13 | with: 14 | java-version: '12.x' 15 | - uses: subosito/flutter-action@v1 16 | with: 17 | flutter-version: '2.0.4' 18 | - run: flutter doctor 19 | - run: flutter pub get 20 | - run: flutter test 21 | -------------------------------------------------------------------------------- /.github/workflows/android_ut.yml: -------------------------------------------------------------------------------- 1 | name: Android Unit Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Linux Android Unit Tests 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-java@v1 13 | with: 14 | java-version: '12.x' 15 | - uses: subosito/flutter-action@v1 16 | with: 17 | flutter-version: '2.0.4' 18 | - run: flutter doctor 19 | - run: flutter pub get 20 | - run: sh android_test.sh 21 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/yourcompany/flutter_webview_plugin_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.yourcompany.flutter_webview_plugin_example; 2 | 3 | import android.os.Bundle; 4 | 5 | import io.flutter.app.FlutterActivity; 6 | import io.flutter.plugins.GeneratedPluginRegistrant; 7 | 8 | public class MainActivity extends FlutterActivity { 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | GeneratedPluginRegistrant.registerWith(this); 13 | } 14 | } -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withInputStream { stream -> plugins.load(stream) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /android/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /example/ios/Runner/GeneratedPluginRegistrant.m: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #import "GeneratedPluginRegistrant.h" 6 | 7 | #if __has_include() 8 | #import 9 | #else 10 | @import flutter_webview_plugin; 11 | #endif 12 | 13 | @implementation GeneratedPluginRegistrant 14 | 15 | + (void)registerWithRegistry:(NSObject*)registry { 16 | [FlutterWebviewPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterWebviewPlugin"]]; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /ios/Classes/WebviewJavaScriptChannelHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #import 6 | #import 7 | 8 | NS_ASSUME_NONNULL_BEGIN 9 | 10 | @interface FLTCommunityJavaScriptChannel : NSObject 11 | 12 | - (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel 13 | javaScriptChannelName:(NSString*)javaScriptChannelName; 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /example/android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | PluginRegistry.h 13 | PluginRegistry.m 14 | 15 | *.pbxuser 16 | *.mode1v3 17 | *.mode2v3 18 | *.perspectivev3 19 | 20 | !default.pbxuser 21 | !default.mode1v3 22 | !default.mode2v3 23 | !default.perspectivev3 24 | 25 | xcuserdata 26 | 27 | *.moved-aside 28 | 29 | *.pyc 30 | *sync/ 31 | Icon? 32 | .tags* 33 | 34 | /Flutter/app.flx 35 | /Flutter/app.zip 36 | /Flutter/App.framework 37 | /Flutter/Flutter.framework 38 | /Flutter/Generated.xcconfig 39 | /ServiceDefinitions.json 40 | 41 | Pods/ 42 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/flutter_webview_plugin_example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ios/flutter_webview_plugin.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'flutter_webview_plugin' 6 | s.version = '0.0.1' 7 | s.summary = 'A new flutter plugin project.' 8 | s.description = <<-DESC 9 | A new flutter plugin project. 10 | DESC 11 | s.homepage = 'https://github.com/dart-flitter/flutter_webview_plugin' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Your Company' => 'email@example.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | 19 | s.ios.deployment_target = '8.0' 20 | end 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I want help writing my application 3 | about: You have a question for how to achieve a particular effect, or you need help 4 | with using a particular API. 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | 11 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | maven { 6 | url 'https://maven.google.com/' 7 | } 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.5.3' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | maven { 20 | url 'https://maven.google.com/' 21 | } 22 | } 23 | } 24 | 25 | rootProject.buildDir = '../build' 26 | subprojects { 27 | project.buildDir = "${rootProject.buildDir}/${project.name}" 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | 35 | wrapper { 36 | gradleVersion = '4.4' 37 | distributionUrl = distributionUrl.replace("bin", "all") 38 | } 39 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins; 2 | 3 | import io.flutter.plugin.common.PluginRegistry; 4 | import com.flutter_webview_plugin.FlutterWebviewPlugin; 5 | 6 | /** 7 | * Generated file. Do not edit. 8 | */ 9 | public final class GeneratedPluginRegistrant { 10 | public static void registerWith(PluginRegistry registry) { 11 | if (alreadyRegisteredWith(registry)) { 12 | return; 13 | } 14 | FlutterWebviewPlugin.registerWith(registry.registrarFor("com.flutter_webview_plugin.FlutterWebviewPlugin")); 15 | } 16 | 17 | private static boolean alreadyRegisteredWith(PluginRegistry registry) { 18 | final String key = GeneratedPluginRegistrant.class.getCanonicalName(); 19 | if (registry.hasPlugin(key)) { 20 | return true; 21 | } 22 | registry.registrarFor(key); 23 | return false; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_webview_plugin 2 | description: Plugin that allow Flutter to communicate with a native Webview. 3 | authors: 4 | - Hadrien Lejard 5 | - Toufik Zitouni 6 | - Pedia 7 | - Simon Lightfoot 8 | - Rafal Wachol 9 | - Misir Jafarov 10 | homepage: https://github.com/dart-flitter/flutter_webview_plugin 11 | version: 0.4.0 12 | 13 | 14 | environment: 15 | sdk: '>=2.12.0 <3.0.0' 16 | flutter: ">=2.0.0" 17 | 18 | flutter: 19 | plugin: 20 | platforms: 21 | android: 22 | package: com.flutter_webview_plugin 23 | pluginClass: FlutterWebviewPlugin 24 | ios: 25 | pluginClass: FlutterWebviewPlugin 26 | 27 | dependencies: 28 | flutter: 29 | sdk: flutter 30 | 31 | dev_dependencies: 32 | mocktail: ^0.1.1 33 | flutter_test: 34 | sdk: flutter 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | MinimumOSVersion 28 | 8.0 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | if ENV['FLUTTER_FRAMEWORK_DIR'] == nil 5 | abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework') 6 | end 7 | 8 | target 'Runner' do 9 | use_frameworks! 10 | 11 | # Pods for Runner 12 | 13 | # Flutter Pods 14 | pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR'] 15 | 16 | if File.exists? '../.flutter-plugins' 17 | flutter_root = File.expand_path('..') 18 | File.foreach('../.flutter-plugins') { |line| 19 | plugin = line.split(pattern='=') 20 | if plugin.length == 2 21 | name = plugin[0].strip() 22 | path = plugin[1].strip() 23 | resolved_path = File.expand_path("#{path}/ios", flutter_root) 24 | pod name, :path => resolved_path 25 | else 26 | puts "Invalid plugin specification: #{line}" 27 | end 28 | } 29 | end 30 | end 31 | 32 | post_install do |installer| 33 | installer.pods_project.targets.each do |target| 34 | target.build_configurations.each do |config| 35 | config.build_settings['ENABLE_BITCODE'] = 'NO' 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /android/src/test/java/com/flutter_webview_plugin/FlutterWebviewPluginTest.java: -------------------------------------------------------------------------------- 1 | package com.flutter_webview_plugin; 2 | 3 | import android.app.Activity; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | import org.mockito.Spy; 10 | 11 | import io.flutter.plugin.common.ErrorLogResult; 12 | import io.flutter.plugin.common.MethodCall; 13 | import io.flutter.plugin.common.MethodChannel; 14 | 15 | import static org.mockito.Mockito.verify; 16 | 17 | public class FlutterWebviewPluginTest { 18 | 19 | @Mock 20 | Activity mockActivity; 21 | 22 | MethodCall mockMethodCall; 23 | MethodChannel.Result mockResult; 24 | 25 | @Spy 26 | FlutterWebviewPlugin flutterWebviewPlugin = new FlutterWebviewPlugin(mockActivity, mockActivity); 27 | 28 | @Before 29 | public void setUp() { 30 | MockitoAnnotations.initMocks(this); 31 | } 32 | 33 | @Test 34 | public void shouldInvokeClose() { 35 | mockMethodCall = new MethodCall("close", null); 36 | mockResult = new ErrorLogResult(""); 37 | flutterWebviewPlugin.onMethodCall(mockMethodCall, mockResult); 38 | verify(flutterWebviewPlugin).close(mockMethodCall, mockResult); 39 | 40 | } 41 | } -------------------------------------------------------------------------------- /scripts/before_build_apks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wget https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip 3 | mkdir android-sdk 4 | unzip -qq sdk-tools-linux-3859397.zip -d android-sdk 5 | export ANDROID_HOME=`pwd`/android-sdk 6 | export PATH=`pwd`/android-sdk/tools/bin:$PATH 7 | mkdir -p /home/travis/.android # silence sdkmanager warning 8 | echo 'count=0' > /home/travis/.android/repositories.cfg # silence sdkmanager warning 9 | # suppressing output of sdkmanager to keep log under 4MB (travis limit) 10 | echo y | sdkmanager "tools" >/dev/null 11 | echo y | sdkmanager "platform-tools" >/dev/null 12 | echo y | sdkmanager "build-tools;28.0.3" >/dev/null 13 | echo y | sdkmanager "platforms;android-28" >/dev/null 14 | echo y | sdkmanager "extras;android;m2repository" >/dev/null 15 | echo y | sdkmanager "extras;google;m2repository" >/dev/null 16 | echo y | sdkmanager "patcher;v4" >/dev/null 17 | sdkmanager --list 18 | wget https://services.gradle.org/distributions/gradle-4.10.2-all.zip 19 | unzip -qq gradle-4.10.2-all.zip 20 | export GRADLE_HOME=$PWD/gradle-4.10.2 21 | export PATH=$GRADLE_HOME/bin:$PATH 22 | gradle -v 23 | git clone --depth=1 https://github.com/flutter/flutter.git 24 | export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH 25 | flutter doctor 26 | pub global activate flutter_plugin_tools -------------------------------------------------------------------------------- /lib/src/javascript_channel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 2 | 3 | final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$'); 4 | 5 | /// A named channel for receiving messaged from JavaScript code running inside a web view. 6 | class JavascriptChannel { 7 | /// Constructs a Javascript channel. 8 | /// 9 | /// The parameters `name` and `onMessageReceived` must not be null. 10 | JavascriptChannel({ 11 | required this.name, 12 | required this.onMessageReceived, 13 | }) : assert(_validChannelNames.hasMatch(name)); 14 | 15 | /// The channel's name. 16 | /// 17 | /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to 18 | /// the Javascript window object's property named `name`. 19 | /// 20 | /// The name must start with a letter or underscore(_), followed by any combination of those 21 | /// characters plus digits. 22 | /// 23 | /// Note that any JavaScript existing `window` property with this name will be overriden. 24 | /// 25 | /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. 26 | final String name; 27 | 28 | /// A callback that's invoked when a message is received through the channel. 29 | final JavascriptMessageHandler onMessageReceived; 30 | } 31 | 32 | /// Callback type for handling messages sent from Javascript running in a web view. 33 | typedef void JavascriptMessageHandler(JavascriptMessage message); 34 | -------------------------------------------------------------------------------- /ios/Classes/WebviewJavaScriptChannelHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #import "WebviewJavaScriptChannelHandler.h" 6 | 7 | @implementation FLTCommunityJavaScriptChannel { 8 | FlutterMethodChannel* _methodChannel; 9 | NSString* _javaScriptChannelName; 10 | } 11 | 12 | - (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel 13 | javaScriptChannelName:(NSString*)javaScriptChannelName { 14 | self = [super init]; 15 | NSAssert(methodChannel != nil, @"methodChannel must not be null."); 16 | NSAssert(javaScriptChannelName != nil, @"javaScriptChannelName must not be null."); 17 | if (self) { 18 | _methodChannel = methodChannel; 19 | _javaScriptChannelName = javaScriptChannelName; 20 | } 21 | return self; 22 | } 23 | 24 | - (void)userContentController:(WKUserContentController*)userContentController 25 | didReceiveScriptMessage:(WKScriptMessage*)message { 26 | NSAssert(_methodChannel != nil, @"Can't send a message to an unitialized JavaScript channel."); 27 | NSAssert(_javaScriptChannelName != nil, 28 | @"Can't send a message to an unitialized JavaScript channel."); 29 | NSDictionary* arguments = @{ 30 | @"channel" : _javaScriptChannelName, 31 | @"message" : [NSString stringWithFormat:@"%@", message.body] 32 | }; 33 | [_methodChannel invokeMethod:@"javascriptChannelMessage" arguments:arguments]; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new idea for Flutter webview plugin. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 22 | 23 | ## Use case 24 | 25 | 35 | 36 | ## Proposal 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Hadrien Lejard. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Hadrien Lejard nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutter_webview_plugin/ObservableWebView.java: -------------------------------------------------------------------------------- 1 | package com.flutter_webview_plugin; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.webkit.WebView; 6 | 7 | public class ObservableWebView extends WebView { 8 | private OnScrollChangedCallback mOnScrollChangedCallback; 9 | 10 | public ObservableWebView(final Context context) 11 | { 12 | super(context); 13 | } 14 | 15 | public ObservableWebView(final Context context, final AttributeSet attrs) 16 | { 17 | super(context, attrs); 18 | } 19 | 20 | public ObservableWebView(final Context context, final AttributeSet attrs, final int defStyle) 21 | { 22 | super(context, attrs, defStyle); 23 | } 24 | 25 | @Override 26 | protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) 27 | { 28 | super.onScrollChanged(l, t, oldl, oldt); 29 | if(mOnScrollChangedCallback != null) mOnScrollChangedCallback.onScroll(l, t, oldl, oldt); 30 | } 31 | 32 | public OnScrollChangedCallback getOnScrollChangedCallback() 33 | { 34 | return mOnScrollChangedCallback; 35 | } 36 | 37 | public void setOnScrollChangedCallback(final OnScrollChangedCallback onScrollChangedCallback) 38 | { 39 | mOnScrollChangedCallback = onScrollChangedCallback; 40 | } 41 | 42 | /** 43 | * Impliment in the activity/fragment/view that you want to listen to the webview 44 | */ 45 | public static interface OnScrollChangedCallback 46 | { 47 | public void onScroll(int l, int t, int oldl, int oldt); 48 | } 49 | } -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withInputStream { stream -> 5 | localProperties.load(stream) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | android { 18 | compileSdkVersion 28 19 | 20 | lintOptions { 21 | disable 'InvalidPackage' 22 | } 23 | 24 | defaultConfig { 25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 26 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 27 | applicationId "com.yourcompany.flutter_webview_plugin_example" 28 | minSdkVersion 16 29 | } 30 | 31 | buildTypes { 32 | release { 33 | // TODO: Add your own signing config for the release build. 34 | // Signing with the debug keys for now, so `flutter run --release` works. 35 | signingConfig signingConfigs.debug 36 | } 37 | } 38 | } 39 | 40 | flutter { 41 | source '../..' 42 | } 43 | 44 | dependencies { 45 | testImplementation 'junit:junit:4.12' 46 | 47 | androidTestImplementation 'androidx.annotation:annotation:1.1.0' 48 | androidTestImplementation 'androidx.test:runner:1.2.0' 49 | androidTestImplementation 'androidx.test:rules:1.2.0' 50 | } 51 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_webview_plugin_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | NSAppTransportSecurity 45 | 46 | NSAllowsArbitraryLoadsInWebContent 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/Runner.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "089954AE9FE84C565F8189CE7B1E08C8949FC40C", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "B9BC852104FA5F3F91D8B12EB80D37232CB3B40D" : 9223372036854775807, 8 | "089954AE9FE84C565F8189CE7B1E08C8949FC40C" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "9839F02E-853D-4357-B537-73CD5272EC87", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "B9BC852104FA5F3F91D8B12EB80D37232CB3B40D" : "..\/..\/flutter", 13 | "089954AE9FE84C565F8189CE7B1E08C8949FC40C" : "flutter_webview_plugin\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Runner", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "example\/ios\/Runner.xcworkspace", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/dart-flitter\/flutter_webview_plugin.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "089954AE9FE84C565F8189CE7B1E08C8949FC40C" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/flutter\/flutter.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "B9BC852104FA5F3F91D8B12EB80D37232CB3B40D" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 12 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I have found out bug in plugin. 3 | about: You are writing an Flutter application with this webview plugin but the application is crashing 4 | or throws an exception, a plugin is buggy, or something looks wrong. 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | 11 | 23 | 24 | ## System info 25 | 26 | Issue occurs on: iOS / Android / both 27 | Plugin version: xxx 28 | Flutter doctor output: 29 | 30 | ``` 31 | paste it here... 32 | ``` 33 | 34 | ## Steps to Reproduce 35 | 36 | 45 | 46 | 1. ... 47 | 2. ... 48 | 3. ... 49 | 50 | ## Logs 51 | 52 | 58 | 59 | ``` 60 | ``` 61 | 62 | 66 | 67 | ``` 68 | ``` 69 | 70 | 71 | 72 | ``` 73 | ``` -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate { 5 | GeneratedPluginRegistrant *plugins; 6 | } 7 | 8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 9 | [GeneratedPluginRegistrant registerWithRegistry:self]; 10 | // Override point for customization after application launch. 11 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 12 | return YES; 13 | } 14 | 15 | - (void)applicationWillResignActive:(UIApplication *)application { 16 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 17 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 18 | } 19 | 20 | - (void)applicationDidEnterBackground:(UIApplication *)application { 21 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 22 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 23 | } 24 | 25 | - (void)applicationWillEnterForeground:(UIApplication *)application { 26 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 27 | } 28 | 29 | - (void)applicationDidBecomeActive:(UIApplication *)application { 30 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 31 | } 32 | 33 | - (void)applicationWillTerminate:(UIApplication *)application { 34 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutter_webview_plugin/JavaScriptChannel.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | package com.flutter_webview_plugin; 6 | 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | import android.webkit.JavascriptInterface; 10 | 11 | import java.util.HashMap; 12 | 13 | import io.flutter.plugin.common.MethodChannel; 14 | 15 | /** 16 | * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets 17 | * up. 18 | * 19 | *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method 20 | * channel to the Dart code. 21 | */ 22 | class JavaScriptChannel { 23 | private final MethodChannel methodChannel; 24 | private final String javaScriptChannelName; 25 | private final Handler platformThreadHandler; 26 | 27 | /** 28 | * @param methodChannel the Flutter WebView method channel to which JS messages are sent 29 | * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method 30 | * channel with each message to let the Dart code know which JavaScript channel the message 31 | * was sent through 32 | */ 33 | JavaScriptChannel( 34 | MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { 35 | this.methodChannel = methodChannel; 36 | this.javaScriptChannelName = javaScriptChannelName; 37 | this.platformThreadHandler = platformThreadHandler; 38 | } 39 | 40 | // Suppressing unused warning as this is invoked from JavaScript. 41 | @SuppressWarnings("unused") 42 | @JavascriptInterface 43 | public void postMessage(final String message) { 44 | Runnable postMessageRunnable = 45 | new Runnable() { 46 | @Override 47 | public void run() { 48 | HashMap arguments = new HashMap<>(); 49 | arguments.put("channel", javaScriptChannelName); 50 | arguments.put("message", message); 51 | methodChannel.invokeMethod("javascriptChannelMessage", arguments); 52 | } 53 | }; 54 | if (platformThreadHandler.getLooper() == Looper.myLooper()) { 55 | postMessageRunnable.run(); 56 | } else { 57 | platformThreadHandler.post(postMessageRunnable); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.flutter_webview_plugin' 2 | version '1.0-SNAPSHOT' 3 | 4 | def ANDROIDX_WARNING = "flutterPluginsAndroidXWarning"; 5 | gradle.buildFinished { buildResult -> 6 | if (buildResult.failure && !rootProject.ext.has(ANDROIDX_WARNING)) { 7 | println ' *********************************************************' 8 | println 'WARNING: This version of flutter_webview_plugin will break your Android build if it or its dependencies aren\'t compatible with AndroidX.' 9 | println ' See https://goo.gl/CP92wY for more information on the problem and how to fix it.' 10 | println ' This warning prints for all Android build failures. The real root cause of the error may be unrelated.' 11 | println ' *********************************************************' 12 | rootProject.ext.set(ANDROIDX_WARNING, true); 13 | } 14 | } 15 | 16 | buildscript { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | 22 | dependencies { 23 | classpath 'com.android.tools.build:gradle:3.5.3' 24 | } 25 | } 26 | 27 | allprojects { 28 | repositories { 29 | jcenter() 30 | google() 31 | } 32 | } 33 | 34 | apply plugin: 'com.android.library' 35 | 36 | android { 37 | compileSdkVersion 28 38 | 39 | defaultConfig { 40 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 41 | // NOTE(jeffmikels): When targetSdkVersion or minSdkVersion is not set or < 4, gradle adds 42 | // additional scary permissions such as WRITE_EXTERNAL_STORAGE and READ_PHONE_STATE. 43 | minSdkVersion 16 44 | } 45 | lintOptions { 46 | disable 'InvalidPackage' 47 | } 48 | 49 | testOptions { 50 | unitTests { 51 | includeAndroidResources = true 52 | } 53 | } 54 | } 55 | dependencies { 56 | implementation 'androidx.appcompat:appcompat:1.1.0' 57 | 58 | testImplementation 'junit:junit:4.12' 59 | testImplementation 'androidx.test:core:1.2.0' 60 | 61 | // When running unit tests for project, gradle needs to have flutter.jar in path 62 | // since there's no FLUTTER_HOME variable, we need to pass flutterPath from console with command: 63 | // ./gradlew test -DflutterPath=/Users/rafal.wachol/Utils/flutter 64 | // 65 | // while develop you can set path to this jar explicitly so IDE won't complain 66 | if(System.getProperty('flutterPath')) { 67 | testImplementation files(System.getProperty('flutterPath') + '/bin/cache/artifacts/engine/android-x64/flutter.jar') 68 | } 69 | 70 | testImplementation 'org.mockito:mockito-inline:2.28.2' 71 | } 72 | -------------------------------------------------------------------------------- /test/flutter_webview_plugin_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 4 | import 'package:mocktail/mocktail.dart'; 5 | 6 | class MockMethodChannel extends Mock implements MethodChannel {} 7 | 8 | void main() { 9 | group('Method channel invoke', () { 10 | group('returns null', () { 11 | late MockMethodChannel methodChannel; 12 | late FlutterWebviewPlugin webview; 13 | 14 | setUp(() { 15 | methodChannel = MockMethodChannel(); 16 | webview = new FlutterWebviewPlugin.private(methodChannel); 17 | when(() => methodChannel.invokeMethod(any(), any())) 18 | .thenAnswer((_) => Future.value(null)); 19 | }); 20 | 21 | test('Should invoke close', () async { 22 | webview.close(); 23 | verify(() => methodChannel.invokeMethod('close')).called(1); 24 | }); 25 | test('Should invoke reload', () async { 26 | when(() => methodChannel.invokeMethod(any())) 27 | .thenAnswer((_) => Future.value(null)); 28 | 29 | webview.reload(); 30 | verify(() => methodChannel.invokeMethod('reload')).called(1); 31 | }); 32 | test('Should invoke goBack', () async { 33 | webview.goBack(); 34 | verify(() => methodChannel.invokeMethod('back')).called(1); 35 | }); 36 | test('Should invoke goForward', () async { 37 | webview.goForward(); 38 | verify(() => methodChannel.invokeMethod('forward')).called(1); 39 | }); 40 | test('Should invoke hide', () async { 41 | webview.hide(); 42 | verify(() => methodChannel.invokeMethod('hide')).called(1); 43 | }); 44 | test('Should invoke show', () async { 45 | webview.show(); 46 | verify(() => methodChannel.invokeMethod('show')).called(1); 47 | }); 48 | }); 49 | 50 | group('returns bool', () { 51 | late MockMethodChannel methodChannel; 52 | late FlutterWebviewPlugin webview; 53 | 54 | setUp(() { 55 | methodChannel = MockMethodChannel(); 56 | webview = new FlutterWebviewPlugin.private(methodChannel); 57 | when(() => methodChannel.invokeMethod(any(), any())) 58 | .thenAnswer((_) => Future.value(true)); 59 | }); 60 | 61 | test('Should invoke canGoBack', () async { 62 | webview.canGoBack(); 63 | verify(() => methodChannel.invokeMethod('canGoBack')).called(1); 64 | }); 65 | test('Should invoke canGoForward', () async { 66 | webview.canGoForward(); 67 | verify(() => methodChannel.invokeMethod('canGoForward')).called(1); 68 | }); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /travis.yml.bak: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | # # Job 1) Run analyzer 4 | # - os: linux 5 | # env: 6 | # - SHARD=Analyze 7 | # sudo: false 8 | # addons: 9 | # apt: 10 | # # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 11 | # sources: 12 | # - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 13 | # packages: 14 | # - libstdc++6 15 | # - fonts-droid 16 | # before_script: 17 | # - git clone https://github.com/flutter/flutter.git 18 | # - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH 19 | # - flutter doctor 20 | # - pub global activate flutter_plugin_tools 21 | # script: 22 | # - ./script/incremental_build.sh analyze 23 | # Job 2) Run dart unit tests 24 | - os: linux 25 | env: 26 | - SHARD=Format+Test 27 | jdk: oraclejdk8 28 | sudo: false 29 | addons: 30 | apt: 31 | # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 32 | sources: 33 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 34 | - llvm-toolchain-precise # for clang-format-5.0 35 | packages: 36 | - libstdc++6 37 | - fonts-droid 38 | - clang-format-5.0 39 | before_script: 40 | - git clone --depth=1 https://github.com/flutter/flutter.git 41 | - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH 42 | - flutter doctor 43 | script: 44 | - flutter test 45 | # Job 3.1) Build example APKs and run Java tests, shard 1/2 46 | - os: linux 47 | env: 48 | - SHARD="Build example apks 1/2" 49 | - PLUGIN_SHARDING="--shardIndex 0 --shardCount 2" 50 | jdk: oraclejdk8 51 | sudo: false 52 | addons: 53 | apt: 54 | # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 55 | sources: 56 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 57 | packages: 58 | - lib32stdc++6 # https://github.com/flutter/flutter/issues/6207 59 | - libstdc++6 60 | - fonts-droid 61 | before_script: 62 | - ./scripts/before_build_apks.sh 63 | - export ANDROID_HOME=`pwd`/android-sdk 64 | - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH 65 | - export FLUTTER_HOME=`pwd`/flutter 66 | script: 67 | - cd example/android 68 | - ./gradlew test -DflutterPath=$FLUTTER_HOME 69 | # # Job 3.2) Build example APKs and run Java tests, shard 2/2 70 | # - os: linux 71 | # env: 72 | # - SHARD="Build example apks 2/2" 73 | # - PLUGIN_SHARDING="--shardIndex 1 --shardCount 2" 74 | # jdk: oraclejdk8 75 | # sudo: false 76 | # addons: 77 | # apt: 78 | # # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 79 | # sources: 80 | # - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 81 | # packages: 82 | # - lib32stdc++6 # https://github.com/flutter/flutter/issues/6207 83 | # - libstdc++6 84 | # - fonts-droid 85 | # before_script: 86 | # - ./script/before_build_apks.sh 87 | # - export ANDROID_HOME=`pwd`/android-sdk 88 | # - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH 89 | # script: 90 | # - ./script/incremental_build.sh build-examples --apk 91 | # - ./script/incremental_build.sh java-test # must come after apk build 92 | 93 | 94 | cache: 95 | directories: 96 | - $HOME/.pub-cache -------------------------------------------------------------------------------- /flutter_webview_plugin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutter_webview_plugin/BrowserClient.java: -------------------------------------------------------------------------------- 1 | package com.flutter_webview_plugin; 2 | 3 | import android.annotation.TargetApi; 4 | import android.graphics.Bitmap; 5 | import android.os.Build; 6 | import android.webkit.WebResourceRequest; 7 | import android.webkit.WebResourceResponse; 8 | import android.webkit.WebView; 9 | import android.webkit.WebViewClient; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * Created by lejard_h on 20/12/2017. 18 | */ 19 | 20 | public class BrowserClient extends WebViewClient { 21 | private Pattern invalidUrlPattern = null; 22 | 23 | public BrowserClient() { 24 | this(null); 25 | } 26 | 27 | public BrowserClient(String invalidUrlRegex) { 28 | super(); 29 | if (invalidUrlRegex != null) { 30 | invalidUrlPattern = Pattern.compile(invalidUrlRegex); 31 | } 32 | } 33 | 34 | public void updateInvalidUrlRegex(String invalidUrlRegex) { 35 | if (invalidUrlRegex != null) { 36 | invalidUrlPattern = Pattern.compile(invalidUrlRegex); 37 | } else { 38 | invalidUrlPattern = null; 39 | } 40 | } 41 | 42 | @Override 43 | public void onPageStarted(WebView view, String url, Bitmap favicon) { 44 | super.onPageStarted(view, url, favicon); 45 | Map data = new HashMap<>(); 46 | data.put("url", url); 47 | data.put("type", "startLoad"); 48 | FlutterWebviewPlugin.channel.invokeMethod("onState", data); 49 | } 50 | 51 | @Override 52 | public void onPageFinished(WebView view, String url) { 53 | super.onPageFinished(view, url); 54 | Map data = new HashMap<>(); 55 | data.put("url", url); 56 | 57 | FlutterWebviewPlugin.channel.invokeMethod("onUrlChanged", data); 58 | 59 | data.put("type", "finishLoad"); 60 | FlutterWebviewPlugin.channel.invokeMethod("onState", data); 61 | 62 | } 63 | 64 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 65 | @Override 66 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 67 | // returning true causes the current WebView to abort loading the URL, 68 | // while returning false causes the WebView to continue loading the URL as usual. 69 | String url = request.getUrl().toString(); 70 | boolean isInvalid = checkInvalidUrl(url); 71 | Map data = new HashMap<>(); 72 | data.put("url", url); 73 | data.put("type", isInvalid ? "abortLoad" : "shouldStart"); 74 | 75 | FlutterWebviewPlugin.channel.invokeMethod("onState", data); 76 | return isInvalid; 77 | } 78 | 79 | @Override 80 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 81 | // returning true causes the current WebView to abort loading the URL, 82 | // while returning false causes the WebView to continue loading the URL as usual. 83 | boolean isInvalid = checkInvalidUrl(url); 84 | Map data = new HashMap<>(); 85 | data.put("url", url); 86 | data.put("type", isInvalid ? "abortLoad" : "shouldStart"); 87 | 88 | FlutterWebviewPlugin.channel.invokeMethod("onState", data); 89 | return isInvalid; 90 | } 91 | 92 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 93 | @Override 94 | public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { 95 | super.onReceivedHttpError(view, request, errorResponse); 96 | Map data = new HashMap<>(); 97 | data.put("url", request.getUrl().toString()); 98 | data.put("code", Integer.toString(errorResponse.getStatusCode())); 99 | FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data); 100 | } 101 | 102 | @Override 103 | public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { 104 | super.onReceivedError(view, errorCode, description, failingUrl); 105 | Map data = new HashMap<>(); 106 | data.put("url", failingUrl); 107 | data.put("code", Integer.toString(errorCode)); 108 | FlutterWebviewPlugin.channel.invokeMethod("onHttpError", data); 109 | } 110 | 111 | private boolean checkInvalidUrl(String url) { 112 | if (invalidUrlPattern == null) { 113 | return false; 114 | } else { 115 | Matcher matcher = invalidUrlPattern.matcher(url); 116 | return matcher.lookingAt(); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.0 2 | - migrated to null safety 3 | 4 | # 0.3.10+1 5 | - fixed android build 6 | 7 | # 0.3.10 8 | - add mediaPlaybackRequiresUserGesture parameter 9 | - Add ignore ssl error parameter 10 | 11 | # 0.3.9+1 12 | 13 | - Fixed error methods on iOS 14 | 15 | # 0.3.9 16 | 17 | - Fixed error methods on iOS 18 | - fixed build 19 | - fixed ios clean cookies 20 | - 4 Make plugin work in headless mode when extending FlutterApplication 21 | - added canGoBack and canGoForward methods 22 | 23 | # 0.3.8 24 | 25 | - Fix iOS local URL support (fixes #114) 26 | - bugfix: Added google() repository to allprojects to satisfy androidx build rules 27 | - fixed min sdk for android 28 | 29 | # 0.3.7 30 | 31 | - Added reloading url with headers 32 | - Added support for reloading url with headers 33 | 34 | # 0.3.6 35 | 36 | - Allow web contents debugging in Chrome 37 | - Android: allow geolocation and file chooser simultaneously 38 | - Add min sdk requirement and descriptions 39 | - fix bug android webview httperror exception 40 | - Exposes displayZoomControls, withOverviewMode and useWideViewPort settings for Android WebView 41 | 42 | # 0.3.5 43 | 44 | - Ability to choose from camera or gallery when using 45 | - Support for webview’s estimated loading progress #255 46 | - Fix back button handler to be compatible with the WillPopScope widget 47 | 48 | # 0.3.4 49 | 50 | - WebView always hidden on iOS 51 | 52 | # 0.3.3 53 | 54 | - BREAKING CHANGE - AndroidX support 55 | 56 | # 0.3.2 57 | 58 | - enable Javascript in iOS, support abort loading specific URLs 59 | - add resizeToAvoidBottomInset to WebviewScaffold; #301 60 | 61 | # 0.3.1 62 | 63 | - Add support for geolocation Android 64 | - fix No269: Can't load target="_blank" links on iOS 65 | - fix: reloadUrl will not return Future 66 | - Fix height of keyboard 67 | - Fix Hide/Show WebView 68 | - hotfix widget back to initialChild after webview is tapped on Android 69 | 70 | # 0.3.0 71 | 72 | - Fixes rect capture issue. Ensures WebView remains in the correct place on screen even when keyboard appears. 73 | - Fixed iOS crash issue with Flutter `>= 0.10.2`. 74 | - Added new `clearCookies` feature. 75 | - Added support for `hidden` and `initialChild` feature to show page loading view. 76 | - Added supportMultipleWindows: enables Multiple Window Support on Android. 77 | - Added appCacheEnabled: enables Application Caches API on Android. 78 | - Added allowFileURLs: allows `file://` local file URLs. 79 | - iOS Now supports: `reload`, `goBack`, and `goForward`. 80 | - iOS Bug fix `didFailNavigation` #77 81 | - Updated Android `compileSdkVersion` to `27` matching offical Flutter plugins. 82 | - Fixed Android `reloadUrl` so settings are not cleared. 83 | - Enabled compatible `Mixed Content Mode` on Android. 84 | 85 | # 0.2.1 86 | 87 | - Added webview scrolling listener 88 | - Added stopLoading() method 89 | 90 | # 0.2.0 91 | 92 | - update sdk 93 | - prevent negative webview height in scaffold 94 | - handle type error in getCookies 95 | - Support file upload via WebView on Android 96 | - fix WebviewScaffold crash on iOS 97 | - Scrollbar functionality to Web view 98 | - Add support of HTTP errors 99 | - Add headers when loading url 100 | 101 | # 0.1.6 102 | 103 | - fix onStateChanged 104 | - Taking safe areas into account for bottom bars 105 | - iOS 106 | + withLocalUrl option for iOS > 9.0 107 | - Android 108 | + add reload, goBack and foForward function 109 | 110 | # 0.1.5 111 | 112 | - iOS use WKWebView instead of UIWebView 113 | 114 | # 0.1.4 115 | 116 | - support localstorage for ANDROID 117 | 118 | # 0.1.3 119 | 120 | - support zoom in webview 121 | 122 | # 0.1.2 123 | 124 | - support bottomNavigationBar and persistentFooterButtons on webview scaffold 125 | 126 | # 0.1.1 127 | - support back button navigation for Android 128 | + if cannot go back, it will trigger onDestroy 129 | - support preview dart2 130 | 131 | # 0.1.0+1 132 | 133 | - fix Android close webview 134 | 135 | # 0.1.0 136 | 137 | - iOS && Android: 138 | - get cookies 139 | - eval javascript 140 | - user agent setting 141 | - state change event 142 | - embed in rectangle or fullscreen if null 143 | - hidden webview 144 | 145 | - Android 146 | - adding Activity in manifest is not needed anymore 147 | 148 | - Add `WebviewScaffold` 149 | 150 | # 0.0.9 151 | 152 | - Android: remove the need to use FlutterActivity as base activity 153 | 154 | # 0.0.5 155 | 156 | - fix "onDestroy" event for iOS [#4](https://github.com/dart-flitter/flutter_webview_plugin/issues/4) 157 | - fix fullscreen mode for iOS [#5](https://github.com/dart-flitter/flutter_webview_plugin/issues/5) 158 | 159 | # 0.0.4 160 | 161 | - IOS implementation 162 | - Update to last version of Flutter 163 | 164 | # 0.0.3 165 | 166 | - Documentation 167 | 168 | # 0.0.2 169 | 170 | - Initial version for Android 171 | -------------------------------------------------------------------------------- /example/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | 3 | linter: 4 | rules: 5 | # these rules are documented on and in the same order as 6 | # the Dart Lint rules page to make maintenance easier 7 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 8 | - always_declare_return_types 9 | - always_put_control_body_on_new_line 10 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 11 | - always_require_non_null_named_parameters 12 | # - always_specify_types 13 | - annotate_overrides 14 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 15 | - avoid_as 16 | # - avoid_bool_literals_in_conditional_expressions # not yet tested 17 | # - avoid_catches_without_on_clauses # we do this commonly 18 | # - avoid_catching_errors # we do this commonly 19 | - avoid_classes_with_only_static_members 20 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 21 | - avoid_empty_else 22 | - avoid_field_initializers_in_const_classes 23 | # - avoid_function_literals_in_foreach_calls 24 | - avoid_init_to_null 25 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 26 | - avoid_null_checks_in_equality_operators 27 | # - avoid_positional_boolean_parameters # not yet tested 28 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 29 | - avoid_relative_lib_imports 30 | - avoid_renaming_method_parameters 31 | - avoid_return_types_on_setters 32 | # - avoid_returning_null # we do this commonly 33 | # - avoid_returning_this # https://github.com/dart-lang/linter/issues/842 34 | # - avoid_setters_without_getters # not yet tested 35 | # - avoid_single_cascade_in_expression_statements # not yet tested 36 | - avoid_slow_async_io 37 | # - avoid_types_as_parameter_names # https://github.com/dart-lang/linter/pull/954/files 38 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 39 | # - avoid_unused_constructor_parameters # https://github.com/dart-lang/linter/pull/847 40 | - await_only_futures 41 | - camel_case_types 42 | - cancel_subscriptions 43 | # - cascade_invocations # not yet tested 44 | # - close_sinks # https://github.com/flutter/flutter/issues/5789 45 | # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153 46 | # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 47 | - control_flow_in_finally 48 | - directives_ordering 49 | - empty_catches 50 | - empty_constructor_bodies 51 | - empty_statements 52 | - hash_and_equals 53 | - implementation_imports 54 | # - invariant_booleans # https://github.com/flutter/flutter/issues/5790 55 | - iterable_contains_unrelated_type 56 | # - join_return_with_assignment # not yet tested 57 | - library_names 58 | - library_prefixes 59 | - list_remove_unrelated_type 60 | # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791 61 | - no_adjacent_strings_in_list 62 | - no_duplicate_case_values 63 | - non_constant_identifier_names 64 | # - omit_local_variable_types # opposite of always_specify_types 65 | # - one_member_abstracts # too many false positives 66 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 67 | - overridden_fields 68 | - package_api_docs 69 | - package_names 70 | - package_prefixed_library_names 71 | # - parameter_assignments # we do this commonly 72 | - prefer_adjacent_string_concatenation 73 | - prefer_asserts_in_initializer_lists 74 | - prefer_bool_in_asserts 75 | - prefer_collection_literals 76 | - prefer_conditional_assignment 77 | - prefer_const_constructors 78 | - prefer_const_constructors_in_immutables 79 | - prefer_const_declarations 80 | - prefer_const_literals_to_create_immutables 81 | # - prefer_constructors_over_static_methods # not yet tested 82 | - prefer_contains 83 | - prefer_equal_for_default_values 84 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 85 | - prefer_final_fields 86 | - prefer_final_locals 87 | # - prefer_foreach 88 | # - prefer_function_declarations_over_variables # not yet tested 89 | - prefer_initializing_formals 90 | # - prefer_interpolation_to_compose_strings # not yet tested 91 | # - prefer_iterable_whereType # https://github.com/dart-lang/sdk/issues/32463 92 | - prefer_is_empty 93 | - prefer_is_not_empty 94 | - prefer_single_quotes 95 | - prefer_typing_uninitialized_variables 96 | - recursive_getters 97 | - slash_for_doc_comments 98 | - sort_constructors_first 99 | - sort_unnamed_constructors_first 100 | - super_goes_last 101 | - test_types_in_equals 102 | - throw_in_finally 103 | # - type_annotate_public_apis # subset of always_specify_types 104 | - type_init_formals 105 | # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 106 | - unnecessary_brace_in_string_interps 107 | - unnecessary_getters_setters 108 | # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 109 | - unnecessary_null_aware_assignments 110 | - unnecessary_null_in_if_null_operators 111 | - unnecessary_overrides 112 | - unnecessary_parenthesis 113 | # - unnecessary_statements # not yet tested 114 | - unnecessary_this 115 | - unrelated_type_equality_checks 116 | - use_rethrow_when_possible 117 | # - use_setters_to_change_properties # not yet tested 118 | # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 119 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 120 | - valid_regexps 121 | # - void_checks # not yet tested -------------------------------------------------------------------------------- /lib/src/webview_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/rendering.dart'; 6 | import 'package:flutter_webview_plugin/src/javascript_channel.dart'; 7 | 8 | import 'base.dart'; 9 | 10 | class WebviewScaffold extends StatefulWidget { 11 | const WebviewScaffold({ 12 | Key? key, 13 | this.appBar, 14 | required this.url, 15 | this.headers, 16 | this.javascriptChannels = const {}, 17 | this.withJavascript = true, 18 | this.clearCache = false, 19 | this.clearCookies = false, 20 | this.mediaPlaybackRequiresUserGesture = true, 21 | this.enableAppScheme = true, 22 | this.userAgent, 23 | this.primary = true, 24 | this.persistentFooterButtons, 25 | this.bottomNavigationBar, 26 | this.withZoom = false, 27 | this.displayZoomControls = false, 28 | this.withLocalStorage = true, 29 | this.withLocalUrl = false, 30 | this.localUrlScope, 31 | this.withOverviewMode = false, 32 | this.useWideViewPort = false, 33 | this.scrollBar = true, 34 | this.supportMultipleWindows = false, 35 | this.appCacheEnabled = false, 36 | this.hidden = false, 37 | this.initialChild, 38 | this.allowFileURLs = false, 39 | this.resizeToAvoidBottomInset = false, 40 | this.invalidUrlRegex, 41 | this.geolocationEnabled = false, 42 | this.debuggingEnabled = false, 43 | this.ignoreSSLErrors = false, 44 | }) : super(key: key); 45 | 46 | final PreferredSizeWidget? appBar; 47 | final String url; 48 | final Map? headers; 49 | final Set javascriptChannels; 50 | final bool withJavascript; 51 | final bool clearCache; 52 | final bool clearCookies; 53 | final bool mediaPlaybackRequiresUserGesture; 54 | final bool enableAppScheme; 55 | final String? userAgent; 56 | final bool primary; 57 | final List? persistentFooterButtons; 58 | final Widget? bottomNavigationBar; 59 | final bool withZoom; 60 | final bool displayZoomControls; 61 | final bool withLocalStorage; 62 | final bool withLocalUrl; 63 | final String? localUrlScope; 64 | final bool scrollBar; 65 | final bool supportMultipleWindows; 66 | final bool appCacheEnabled; 67 | final bool hidden; 68 | final Widget? initialChild; 69 | final bool allowFileURLs; 70 | final bool resizeToAvoidBottomInset; 71 | final String? invalidUrlRegex; 72 | final bool geolocationEnabled; 73 | final bool withOverviewMode; 74 | final bool useWideViewPort; 75 | final bool debuggingEnabled; 76 | final bool ignoreSSLErrors; 77 | 78 | @override 79 | _WebviewScaffoldState createState() => _WebviewScaffoldState(); 80 | } 81 | 82 | class _WebviewScaffoldState extends State { 83 | final webviewReference = FlutterWebviewPlugin(); 84 | Rect? _rect; 85 | Timer? _resizeTimer; 86 | StreamSubscription? _onStateChanged; 87 | 88 | StreamSubscription? _onBack; 89 | 90 | @override 91 | void initState() { 92 | super.initState(); 93 | webviewReference.close(); 94 | 95 | _onBack = webviewReference.onBack.listen((_) async { 96 | if (!mounted) { 97 | return; 98 | } 99 | 100 | // The willPop/pop pair here is equivalent to Navigator.maybePop(), 101 | // which is what's called from the flutter back button handler. 102 | final pop = await _topMostRoute.willPop(); 103 | if (pop == RoutePopDisposition.pop) { 104 | // Close the webview if it's on the route at the top of the stack. 105 | final isOnTopMostRoute = _topMostRoute == ModalRoute.of(context); 106 | if (isOnTopMostRoute) { 107 | webviewReference.close(); 108 | } 109 | Navigator.pop(context); 110 | } 111 | }); 112 | 113 | if (widget.hidden) { 114 | _onStateChanged = 115 | webviewReference.onStateChanged.listen((WebViewStateChanged state) { 116 | if (state.type == WebViewState.finishLoad) { 117 | webviewReference.show(); 118 | } 119 | }); 120 | } 121 | } 122 | 123 | /// Equivalent to [Navigator.of(context)._history.last]. 124 | Route get _topMostRoute { 125 | Route? topMost; 126 | Navigator.popUntil(context, (route) { 127 | topMost = route; 128 | return true; 129 | }); 130 | return topMost!; 131 | } 132 | 133 | @override 134 | void dispose() { 135 | super.dispose(); 136 | _onBack?.cancel(); 137 | _resizeTimer?.cancel(); 138 | webviewReference.close(); 139 | if (widget.hidden) { 140 | _onStateChanged?.cancel(); 141 | } 142 | webviewReference.dispose(); 143 | } 144 | 145 | @override 146 | Widget build(BuildContext context) { 147 | return Scaffold( 148 | appBar: widget.appBar, 149 | resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset, 150 | persistentFooterButtons: widget.persistentFooterButtons, 151 | bottomNavigationBar: widget.bottomNavigationBar, 152 | body: _WebviewPlaceholder( 153 | onRectChanged: (Rect value) { 154 | if (_rect == null) { 155 | _rect = value; 156 | webviewReference.launch( 157 | widget.url, 158 | headers: widget.headers, 159 | javascriptChannels: widget.javascriptChannels, 160 | withJavascript: widget.withJavascript, 161 | clearCache: widget.clearCache, 162 | clearCookies: widget.clearCookies, 163 | mediaPlaybackRequiresUserGesture: 164 | widget.mediaPlaybackRequiresUserGesture, 165 | hidden: widget.hidden, 166 | enableAppScheme: widget.enableAppScheme, 167 | userAgent: widget.userAgent, 168 | rect: _rect, 169 | withZoom: widget.withZoom, 170 | displayZoomControls: widget.displayZoomControls, 171 | withLocalStorage: widget.withLocalStorage, 172 | withLocalUrl: widget.withLocalUrl, 173 | localUrlScope: widget.localUrlScope, 174 | withOverviewMode: widget.withOverviewMode, 175 | useWideViewPort: widget.useWideViewPort, 176 | scrollBar: widget.scrollBar, 177 | supportMultipleWindows: widget.supportMultipleWindows, 178 | appCacheEnabled: widget.appCacheEnabled, 179 | allowFileURLs: widget.allowFileURLs, 180 | invalidUrlRegex: widget.invalidUrlRegex, 181 | geolocationEnabled: widget.geolocationEnabled, 182 | debuggingEnabled: widget.debuggingEnabled, 183 | ignoreSSLErrors: widget.ignoreSSLErrors, 184 | ); 185 | } else { 186 | if (_rect != value) { 187 | _rect = value; 188 | _resizeTimer?.cancel(); 189 | _resizeTimer = Timer(const Duration(milliseconds: 250), () { 190 | // avoid resizing to fast when build is called multiple time 191 | webviewReference.resize(_rect!); 192 | }); 193 | } 194 | } 195 | }, 196 | child: widget.initialChild ?? 197 | const Center(child: CircularProgressIndicator()), 198 | ), 199 | ); 200 | } 201 | } 202 | 203 | class _WebviewPlaceholder extends SingleChildRenderObjectWidget { 204 | const _WebviewPlaceholder({ 205 | Key? key, 206 | required this.onRectChanged, 207 | Widget? child, 208 | }) : super(key: key, child: child); 209 | 210 | final ValueChanged onRectChanged; 211 | 212 | @override 213 | RenderObject createRenderObject(BuildContext context) { 214 | return _WebviewPlaceholderRender( 215 | onRectChanged: onRectChanged, 216 | ); 217 | } 218 | 219 | @override 220 | void updateRenderObject( 221 | BuildContext context, _WebviewPlaceholderRender renderObject) { 222 | renderObject..onRectChanged = onRectChanged; 223 | } 224 | } 225 | 226 | class _WebviewPlaceholderRender extends RenderProxyBox { 227 | _WebviewPlaceholderRender({ 228 | RenderBox? child, 229 | ValueChanged? onRectChanged, 230 | }) : _callback = onRectChanged, 231 | super(child); 232 | 233 | ValueChanged? _callback; 234 | Rect? _rect; 235 | 236 | Rect get rect => _rect!; 237 | 238 | set onRectChanged(ValueChanged callback) { 239 | if (callback != _callback) { 240 | _callback = callback; 241 | notifyRect(); 242 | } 243 | } 244 | 245 | void notifyRect() { 246 | if (_callback != null && _rect != null) { 247 | _callback!(_rect!); 248 | } 249 | } 250 | 251 | @override 252 | void paint(PaintingContext context, Offset offset) { 253 | super.paint(context, offset); 254 | final rect = offset & size; 255 | if (_rect != rect) { 256 | _rect = rect; 257 | notifyRect(); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Flutter Community: flutter_webview_plugin](https://fluttercommunity.dev/_github/header/flutter_webview_plugin)](https://github.com/fluttercommunity/community) 2 | 3 | # NOTICE 4 | > We are working closely with the Flutter Team to integrate all the Community Plugin features in the [Official WebView Plugin](https://pub.dev/packages/webview_flutter). We will try our best to resolve PRs and Bugfixes, but our priority right now is to merge our two code-bases. Once the merge is complete we will deprecate the Community Plugin in favor of the Official one. 5 | > 6 | > Thank you for all your support, hopefully you'll also show it for Official Plugin too. 7 | > 8 | > Keep Fluttering! 9 | 10 | # Flutter WebView Plugin 11 | 12 | [![pub package](https://img.shields.io/pub/v/flutter_webview_plugin.svg)](https://pub.dartlang.org/packages/flutter_webview_plugin) 13 | 14 | Plugin that allows Flutter to communicate with a native WebView. 15 | 16 | **_Warning:_** 17 | The webview is not integrated in the widget tree, it is a native view on top of the flutter view. 18 | You won't be able see snackbars, dialogs, or other flutter widgets that would overlap with the region of the screen taken up by the webview. 19 | 20 | The getSafeAcceptedType() function is available only for minimum SDK of 21. 21 | eval() function only supports SDK of 19 or greater for evaluating Javascript. 22 | 23 | ## Getting Started 24 | 25 | For help getting started with Flutter, view our online [documentation](http://flutter.io/). 26 | 27 | #### iOS 28 | 29 | In order for plugin to work correctly, you need to add new key to `ios/Runner/Info.plist` 30 | 31 | ```xml 32 | NSAppTransportSecurity 33 | 34 | NSAllowsArbitraryLoads 35 | 36 | NSAllowsArbitraryLoadsInWebContent 37 | 38 | 39 | ``` 40 | 41 | `NSAllowsArbitraryLoadsInWebContent` is for iOS 10+ and `NSAllowsArbitraryLoads` for iOS 9. 42 | 43 | 44 | ### How it works 45 | 46 | #### Launch WebView Fullscreen with Flutter navigation 47 | 48 | ```dart 49 | new MaterialApp( 50 | routes: { 51 | "/": (_) => new WebviewScaffold( 52 | url: "https://www.google.com", 53 | appBar: new AppBar( 54 | title: new Text("Widget webview"), 55 | ), 56 | ), 57 | }, 58 | ); 59 | ``` 60 | 61 | Optional parameters `hidden` and `initialChild` are available so that you can show something else while waiting for the page to load. 62 | If you set `hidden` to true it will show a default CircularProgressIndicator. If you additionally specify a Widget for initialChild 63 | you can have it display whatever you like till page-load. 64 | 65 | e.g. The following will show a read screen with the text 'waiting.....'. 66 | ```dart 67 | return new MaterialApp( 68 | title: 'Flutter WebView Demo', 69 | theme: new ThemeData( 70 | primarySwatch: Colors.blue, 71 | ), 72 | routes: { 73 | '/': (_) => const MyHomePage(title: 'Flutter WebView Demo'), 74 | '/widget': (_) => new WebviewScaffold( 75 | url: selectedUrl, 76 | appBar: new AppBar( 77 | title: const Text('Widget webview'), 78 | ), 79 | withZoom: true, 80 | withLocalStorage: true, 81 | hidden: true, 82 | initialChild: Container( 83 | color: Colors.redAccent, 84 | child: const Center( 85 | child: Text('Waiting.....'), 86 | ), 87 | ), 88 | ), 89 | }, 90 | ); 91 | ``` 92 | 93 | `FlutterWebviewPlugin` provide a singleton instance linked to one unique webview, 94 | so you can take control of the webview from anywhere in the app 95 | 96 | listen for events 97 | 98 | ```dart 99 | final flutterWebviewPlugin = new FlutterWebviewPlugin(); 100 | 101 | flutterWebviewPlugin.onUrlChanged.listen((String url) { 102 | 103 | }); 104 | ``` 105 | 106 | #### Listen for scroll event in webview 107 | 108 | ```dart 109 | final flutterWebviewPlugin = new FlutterWebviewPlugin(); 110 | flutterWebviewPlugin.onScrollYChanged.listen((double offsetY) { // latest offset value in vertical scroll 111 | // compare vertical scroll changes here with old value 112 | }); 113 | 114 | flutterWebviewPlugin.onScrollXChanged.listen((double offsetX) { // latest offset value in horizontal scroll 115 | // compare horizontal scroll changes here with old value 116 | }); 117 | 118 | ```` 119 | 120 | Note: Do note there is a slight difference is scroll distance between ios and android. Android scroll value difference tends to be larger than ios devices. 121 | 122 | 123 | #### Hidden WebView 124 | 125 | ```dart 126 | final flutterWebviewPlugin = new FlutterWebviewPlugin(); 127 | 128 | flutterWebviewPlugin.launch(url, hidden: true); 129 | ``` 130 | 131 | #### Close launched WebView 132 | 133 | ```dart 134 | flutterWebviewPlugin.close(); 135 | ``` 136 | 137 | #### Webview inside custom Rectangle 138 | 139 | ```dart 140 | final flutterWebviewPlugin = new FlutterWebviewPlugin(); 141 | 142 | flutterWebviewPlugin.launch(url, 143 | fullScreen: false, 144 | rect: new Rect.fromLTWH( 145 | 0.0, 146 | 0.0, 147 | MediaQuery.of(context).size.width, 148 | 300.0, 149 | ), 150 | ); 151 | ``` 152 | 153 | #### Injecting custom code into the webview 154 | Use `flutterWebviewPlugin.evalJavaScript(String code)`. This function must be run after the page has finished loading (i.e. listen to `onStateChanged` for events where state is `finishLoad`). 155 | 156 | If you have a large amount of JavaScript to embed, use an asset file. Add the asset file to `pubspec.yaml`, then call the function like: 157 | 158 | ```dart 159 | Future loadJS(String name) async { 160 | var givenJS = rootBundle.loadString('assets/$name.js'); 161 | return givenJS.then((String js) { 162 | flutterWebViewPlugin.onStateChanged.listen((viewState) async { 163 | if (viewState.type == WebViewState.finishLoad) { 164 | flutterWebViewPlugin.evalJavascript(js); 165 | } 166 | }); 167 | }); 168 | } 169 | ``` 170 | 171 | ### Accessing local files in the file system 172 | Set the `withLocalUrl` option to true in the launch function or in the Webview scaffold to enable support for local URLs. 173 | 174 | Note that, on iOS, the `localUrlScope` option also needs to be set to a path to a directory. All files inside this folder (or subfolder) will be allowed access. If ommited, only the local file being opened will have access allowed, resulting in no subresources being loaded. This option is ignored on Android. 175 | 176 | ### Ignoring SSL Errors 177 | 178 | Set the `ignoreSSLErrors` option to true to display content from servers with certificates usually not trusted by the Webview like self-signed certificates. 179 | 180 | **_Warning:_** Don't use this in production. 181 | 182 | Note that on iOS, you need to add new key to `ios/Runner/Info.plist` 183 | 184 | ```xml 185 | NSAppTransportSecurity 186 | 187 | NSAllowsArbitraryLoads 188 | 189 | NSAllowsArbitraryLoadsInWebContent 190 | 191 | 192 | ``` 193 | 194 | `NSAllowsArbitraryLoadsInWebContent` is for iOS 10+ and `NSAllowsArbitraryLoads` for iOS 9. 195 | Otherwise you'll still not be able to display content from pages with untrusted certificates. 196 | 197 | You can test your ignorance if ssl certificates is working e.g. through https://self-signed.badssl.com/ 198 | 199 | 200 | 201 | 202 | ### Webview Events 203 | 204 | - `Stream` onDestroy 205 | - `Stream` onUrlChanged 206 | - `Stream` onStateChanged 207 | - `Stream` onScrollXChanged 208 | - `Stream` onScrollYChanged 209 | - `Stream` onError 210 | 211 | **_Don't forget to dispose webview_** 212 | `flutterWebviewPlugin.dispose()` 213 | 214 | ### Webview Functions 215 | 216 | ```dart 217 | Future launch(String url, { 218 | Map headers: null, 219 | Set javascriptChannels: null, 220 | bool withJavascript: true, 221 | bool clearCache: false, 222 | bool clearCookies: false, 223 | bool hidden: false, 224 | bool enableAppScheme: true, 225 | Rect rect: null, 226 | String userAgent: null, 227 | bool withZoom: false, 228 | bool displayZoomControls: false, 229 | bool withLocalStorage: true, 230 | bool withLocalUrl: true, 231 | String localUrlScope: null, 232 | bool withOverviewMode: false, 233 | bool scrollBar: true, 234 | bool supportMultipleWindows: false, 235 | bool appCacheEnabled: false, 236 | bool allowFileURLs: false, 237 | bool useWideViewPort: false, 238 | String invalidUrlRegex: null, 239 | bool geolocationEnabled: false, 240 | bool debuggingEnabled: false, 241 | bool ignoreSSLErrors: false, 242 | }); 243 | ``` 244 | 245 | ```dart 246 | Future evalJavascript(String code); 247 | ``` 248 | 249 | ```dart 250 | Future> getCookies(); 251 | ``` 252 | 253 | ```dart 254 | Future cleanCookies(); 255 | ``` 256 | 257 | ```dart 258 | Future resize(Rect rect); 259 | ``` 260 | 261 | ```dart 262 | Future show(); 263 | ``` 264 | 265 | ```dart 266 | Future hide(); 267 | ``` 268 | 269 | ```dart 270 | Future reloadUrl(String url); 271 | ``` 272 | 273 | ```dart 274 | Future close(); 275 | ``` 276 | 277 | ```dart 278 | Future reload(); 279 | ``` 280 | 281 | ```dart 282 | Future goBack(); 283 | ``` 284 | 285 | ```dart 286 | Future goForward(); 287 | ``` 288 | 289 | ```dart 290 | Future stopLoading(); 291 | ``` 292 | 293 | ```dart 294 | Future canGoBack(); 295 | ``` 296 | 297 | ```dart 298 | Future canGoForward(); 299 | ``` 300 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 6 | 7 | const kAndroidUserAgent = 8 | 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Mobile Safari/537.36'; 9 | 10 | String selectedUrl = 'https://flutter.io'; 11 | 12 | // ignore: prefer_collection_literals 13 | final Set jsChannels = [ 14 | JavascriptChannel( 15 | name: 'Print', 16 | onMessageReceived: (JavascriptMessage message) { 17 | print(message.message); 18 | }), 19 | ].toSet(); 20 | 21 | void main() { 22 | WidgetsFlutterBinding.ensureInitialized(); 23 | runApp(MyApp()); 24 | } 25 | 26 | class MyApp extends StatelessWidget { 27 | final flutterWebViewPlugin = FlutterWebviewPlugin(); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return MaterialApp( 32 | title: 'Flutter WebView Demo', 33 | theme: ThemeData( 34 | primarySwatch: Colors.blue, 35 | ), 36 | routes: { 37 | '/': (_) => const MyHomePage(title: 'Flutter WebView Demo'), 38 | '/widget': (_) { 39 | return WebviewScaffold( 40 | url: selectedUrl, 41 | javascriptChannels: jsChannels, 42 | mediaPlaybackRequiresUserGesture: false, 43 | appBar: AppBar( 44 | title: const Text('Widget WebView'), 45 | ), 46 | withZoom: true, 47 | withLocalStorage: true, 48 | hidden: true, 49 | initialChild: Container( 50 | color: Colors.redAccent, 51 | child: const Center( 52 | child: Text('Waiting.....'), 53 | ), 54 | ), 55 | bottomNavigationBar: BottomAppBar( 56 | child: Row( 57 | children: [ 58 | IconButton( 59 | icon: const Icon(Icons.arrow_back_ios), 60 | onPressed: () { 61 | flutterWebViewPlugin.goBack(); 62 | }, 63 | ), 64 | IconButton( 65 | icon: const Icon(Icons.arrow_forward_ios), 66 | onPressed: () { 67 | flutterWebViewPlugin.goForward(); 68 | }, 69 | ), 70 | IconButton( 71 | icon: const Icon(Icons.autorenew), 72 | onPressed: () { 73 | flutterWebViewPlugin.reload(); 74 | }, 75 | ), 76 | ], 77 | ), 78 | ), 79 | ); 80 | }, 81 | }, 82 | ); 83 | } 84 | } 85 | 86 | class MyHomePage extends StatefulWidget { 87 | const MyHomePage({Key? key, required this.title}) : super(key: key); 88 | 89 | final String title; 90 | 91 | @override 92 | _MyHomePageState createState() => _MyHomePageState(); 93 | } 94 | 95 | class _MyHomePageState extends State { 96 | // Instance of WebView plugin 97 | final flutterWebViewPlugin = FlutterWebviewPlugin(); 98 | 99 | // On destroy stream 100 | late StreamSubscription _onDestroy; 101 | 102 | // On urlChanged stream 103 | late StreamSubscription _onUrlChanged; 104 | 105 | // On urlChanged stream 106 | late StreamSubscription _onStateChanged; 107 | 108 | late StreamSubscription _onHttpError; 109 | 110 | late StreamSubscription _onProgressChanged; 111 | 112 | late StreamSubscription _onScrollYChanged; 113 | 114 | late StreamSubscription _onScrollXChanged; 115 | 116 | final _urlCtrl = TextEditingController(text: selectedUrl); 117 | 118 | final _codeCtrl = TextEditingController(text: 'window.navigator.userAgent'); 119 | 120 | final _scaffoldKey = GlobalKey(); 121 | 122 | final _history = []; 123 | 124 | @override 125 | void initState() { 126 | super.initState(); 127 | 128 | flutterWebViewPlugin.close(); 129 | 130 | _urlCtrl.addListener(() { 131 | selectedUrl = _urlCtrl.text; 132 | }); 133 | 134 | // Add a listener to on destroy WebView, so you can make came actions. 135 | _onDestroy = flutterWebViewPlugin.onDestroy.listen((_) { 136 | if (mounted) { 137 | // Actions like show a info toast. 138 | ScaffoldMessenger.of(context) 139 | .showSnackBar(const SnackBar(content: Text('Webview Destroyed'))); 140 | } 141 | }); 142 | 143 | // Add a listener to on url changed 144 | _onUrlChanged = flutterWebViewPlugin.onUrlChanged.listen((String url) { 145 | if (mounted) { 146 | setState(() { 147 | _history.add('onUrlChanged: $url'); 148 | }); 149 | } 150 | }); 151 | 152 | _onProgressChanged = 153 | flutterWebViewPlugin.onProgressChanged.listen((double progress) { 154 | if (mounted) { 155 | setState(() { 156 | _history.add('onProgressChanged: $progress'); 157 | }); 158 | } 159 | }); 160 | 161 | _onScrollYChanged = 162 | flutterWebViewPlugin.onScrollYChanged.listen((double y) { 163 | if (mounted) { 164 | setState(() { 165 | _history.add('Scroll in Y Direction: $y'); 166 | }); 167 | } 168 | }); 169 | 170 | _onScrollXChanged = 171 | flutterWebViewPlugin.onScrollXChanged.listen((double x) { 172 | if (mounted) { 173 | setState(() { 174 | _history.add('Scroll in X Direction: $x'); 175 | }); 176 | } 177 | }); 178 | 179 | _onStateChanged = 180 | flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) { 181 | if (mounted) { 182 | setState(() { 183 | _history.add('onStateChanged: ${state.type} ${state.url}'); 184 | }); 185 | } 186 | }); 187 | 188 | _onHttpError = 189 | flutterWebViewPlugin.onHttpError.listen((WebViewHttpError error) { 190 | if (mounted) { 191 | setState(() { 192 | _history.add('onHttpError: ${error.code} ${error.url}'); 193 | }); 194 | } 195 | }); 196 | } 197 | 198 | @override 199 | void dispose() { 200 | // Every listener should be canceled, the same should be done with this stream. 201 | _onDestroy.cancel(); 202 | _onUrlChanged.cancel(); 203 | _onStateChanged.cancel(); 204 | _onHttpError.cancel(); 205 | _onProgressChanged.cancel(); 206 | _onScrollXChanged.cancel(); 207 | _onScrollYChanged.cancel(); 208 | 209 | flutterWebViewPlugin.dispose(); 210 | 211 | super.dispose(); 212 | } 213 | 214 | @override 215 | Widget build(BuildContext context) { 216 | return Scaffold( 217 | key: _scaffoldKey, 218 | appBar: AppBar( 219 | title: const Text('Plugin example app'), 220 | ), 221 | body: SingleChildScrollView( 222 | child: Column( 223 | mainAxisAlignment: MainAxisAlignment.center, 224 | children: [ 225 | Container( 226 | padding: const EdgeInsets.all(24.0), 227 | child: TextField(controller: _urlCtrl), 228 | ), 229 | ElevatedButton( 230 | onPressed: () { 231 | flutterWebViewPlugin.launch( 232 | selectedUrl, 233 | rect: Rect.fromLTWH( 234 | 0.0, 0.0, MediaQuery.of(context).size.width, 300.0), 235 | userAgent: kAndroidUserAgent, 236 | invalidUrlRegex: 237 | r'^(https).+(twitter)', // prevent redirecting to twitter when user click on its icon in flutter website 238 | ); 239 | }, 240 | child: const Text('Open Webview (rect)'), 241 | ), 242 | ElevatedButton( 243 | onPressed: () { 244 | flutterWebViewPlugin.launch(selectedUrl, hidden: true); 245 | }, 246 | child: const Text('Open "hidden" Webview'), 247 | ), 248 | ElevatedButton( 249 | onPressed: () { 250 | flutterWebViewPlugin.launch(selectedUrl); 251 | }, 252 | child: const Text('Open Fullscreen Webview'), 253 | ), 254 | ElevatedButton( 255 | onPressed: () { 256 | Navigator.of(context).pushNamed('/widget'); 257 | }, 258 | child: const Text('Open widget webview'), 259 | ), 260 | Container( 261 | padding: const EdgeInsets.all(24.0), 262 | child: TextField(controller: _codeCtrl), 263 | ), 264 | ElevatedButton( 265 | onPressed: () { 266 | final future = 267 | flutterWebViewPlugin.evalJavascript(_codeCtrl.text); 268 | future.then((String? result) { 269 | setState(() { 270 | _history.add('eval: $result'); 271 | }); 272 | }); 273 | }, 274 | child: const Text('Eval some javascript'), 275 | ), 276 | ElevatedButton( 277 | onPressed: () { 278 | final future = flutterWebViewPlugin 279 | .evalJavascript('alert("Hello World");'); 280 | future.then((String? result) { 281 | setState(() { 282 | _history.add('eval: $result'); 283 | }); 284 | }); 285 | }, 286 | child: const Text('Eval javascript alert()'), 287 | ), 288 | ElevatedButton( 289 | onPressed: () { 290 | setState(() { 291 | _history.clear(); 292 | }); 293 | flutterWebViewPlugin.close(); 294 | }, 295 | child: const Text('Close'), 296 | ), 297 | ElevatedButton( 298 | onPressed: () { 299 | flutterWebViewPlugin.getCookies().then((m) { 300 | setState(() { 301 | _history.add('cookies: $m'); 302 | }); 303 | }); 304 | }, 305 | child: const Text('Cookies'), 306 | ), 307 | Text(_history.join('\n')) 308 | ], 309 | ), 310 | ), 311 | ); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutter_webview_plugin/FlutterWebviewPlugin.java: -------------------------------------------------------------------------------- 1 | package com.flutter_webview_plugin; 2 | 3 | 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.graphics.Point; 8 | import android.view.Display; 9 | import android.webkit.WebStorage; 10 | import android.widget.FrameLayout; 11 | import android.webkit.CookieManager; 12 | import android.webkit.ValueCallback; 13 | import android.os.Build; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | import io.flutter.plugin.common.MethodCall; 20 | import io.flutter.plugin.common.MethodChannel; 21 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 22 | import io.flutter.plugin.common.PluginRegistry; 23 | 24 | /** 25 | * FlutterWebviewPlugin 26 | */ 27 | public class FlutterWebviewPlugin implements MethodCallHandler, PluginRegistry.ActivityResultListener { 28 | private Activity activity; 29 | private WebviewManager webViewManager; 30 | private Context context; 31 | static MethodChannel channel; 32 | private static final String CHANNEL_NAME = "flutter_webview_plugin"; 33 | private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; 34 | 35 | public static void registerWith(PluginRegistry.Registrar registrar) { 36 | if (registrar.activity() != null) { 37 | channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); 38 | final FlutterWebviewPlugin instance = new FlutterWebviewPlugin(registrar.activity(), registrar.activeContext()); 39 | registrar.addActivityResultListener(instance); 40 | channel.setMethodCallHandler(instance); 41 | } 42 | } 43 | 44 | FlutterWebviewPlugin(Activity activity, Context context) { 45 | this.activity = activity; 46 | this.context = context; 47 | } 48 | 49 | @Override 50 | public void onMethodCall(MethodCall call, MethodChannel.Result result) { 51 | switch (call.method) { 52 | case "launch": 53 | openUrl(call, result); 54 | break; 55 | case "close": 56 | close(call, result); 57 | break; 58 | case "eval": 59 | eval(call, result); 60 | break; 61 | case "resize": 62 | resize(call, result); 63 | break; 64 | case "reload": 65 | reload(call, result); 66 | break; 67 | case "back": 68 | back(call, result); 69 | break; 70 | case "forward": 71 | forward(call, result); 72 | break; 73 | case "hide": 74 | hide(call, result); 75 | break; 76 | case "show": 77 | show(call, result); 78 | break; 79 | case "reloadUrl": 80 | reloadUrl(call, result); 81 | break; 82 | case "stopLoading": 83 | stopLoading(call, result); 84 | break; 85 | case "cleanCookies": 86 | cleanCookies(call, result); 87 | break; 88 | case "canGoBack": 89 | canGoBack(result); 90 | break; 91 | case "canGoForward": 92 | canGoForward(result); 93 | break; 94 | case "cleanCache": 95 | cleanCache(result); 96 | break; 97 | default: 98 | result.notImplemented(); 99 | break; 100 | } 101 | } 102 | 103 | private void cleanCache(MethodChannel.Result result) { 104 | webViewManager.cleanCache(); 105 | WebStorage.getInstance().deleteAllData(); 106 | result.success(null); 107 | } 108 | 109 | void openUrl(MethodCall call, MethodChannel.Result result) { 110 | boolean hidden = call.argument("hidden"); 111 | String url = call.argument("url"); 112 | String userAgent = call.argument("userAgent"); 113 | boolean withJavascript = call.argument("withJavascript"); 114 | boolean clearCache = call.argument("clearCache"); 115 | boolean clearCookies = call.argument("clearCookies"); 116 | boolean mediaPlaybackRequiresUserGesture = call.argument("mediaPlaybackRequiresUserGesture"); 117 | boolean withZoom = call.argument("withZoom"); 118 | boolean displayZoomControls = call.argument("displayZoomControls"); 119 | boolean withLocalStorage = call.argument("withLocalStorage"); 120 | boolean withOverviewMode = call.argument("withOverviewMode"); 121 | boolean supportMultipleWindows = call.argument("supportMultipleWindows"); 122 | boolean appCacheEnabled = call.argument("appCacheEnabled"); 123 | Map headers = call.argument("headers"); 124 | boolean scrollBar = call.argument("scrollBar"); 125 | boolean allowFileURLs = call.argument("allowFileURLs"); 126 | boolean useWideViewPort = call.argument("useWideViewPort"); 127 | String invalidUrlRegex = call.argument("invalidUrlRegex"); 128 | boolean geolocationEnabled = call.argument("geolocationEnabled"); 129 | boolean debuggingEnabled = call.argument("debuggingEnabled"); 130 | boolean ignoreSSLErrors = call.argument("ignoreSSLErrors"); 131 | 132 | if (webViewManager == null || webViewManager.closed == true) { 133 | Map arguments = (Map) call.arguments; 134 | List channelNames = new ArrayList(); 135 | if (arguments.containsKey(JS_CHANNEL_NAMES_FIELD)) { 136 | channelNames = (List) arguments.get(JS_CHANNEL_NAMES_FIELD); 137 | } 138 | webViewManager = new WebviewManager(activity, context, channelNames); 139 | } 140 | 141 | FrameLayout.LayoutParams params = buildLayoutParams(call); 142 | 143 | activity.addContentView(webViewManager.webView, params); 144 | 145 | webViewManager.openUrl(withJavascript, 146 | clearCache, 147 | hidden, 148 | clearCookies, 149 | mediaPlaybackRequiresUserGesture, 150 | userAgent, 151 | url, 152 | headers, 153 | withZoom, 154 | displayZoomControls, 155 | withLocalStorage, 156 | withOverviewMode, 157 | scrollBar, 158 | supportMultipleWindows, 159 | appCacheEnabled, 160 | allowFileURLs, 161 | useWideViewPort, 162 | invalidUrlRegex, 163 | geolocationEnabled, 164 | debuggingEnabled, 165 | ignoreSSLErrors 166 | ); 167 | result.success(null); 168 | } 169 | 170 | private FrameLayout.LayoutParams buildLayoutParams(MethodCall call) { 171 | Map rc = call.argument("rect"); 172 | FrameLayout.LayoutParams params; 173 | if (rc != null) { 174 | params = new FrameLayout.LayoutParams( 175 | dp2px(activity, rc.get("width").intValue()), dp2px(activity, rc.get("height").intValue())); 176 | params.setMargins(dp2px(activity, rc.get("left").intValue()), dp2px(activity, rc.get("top").intValue()), 177 | 0, 0); 178 | } else { 179 | Display display = activity.getWindowManager().getDefaultDisplay(); 180 | Point size = new Point(); 181 | display.getSize(size); 182 | int width = size.x; 183 | int height = size.y; 184 | params = new FrameLayout.LayoutParams(width, height); 185 | } 186 | 187 | return params; 188 | } 189 | 190 | private void stopLoading(MethodCall call, MethodChannel.Result result) { 191 | if (webViewManager != null) { 192 | webViewManager.stopLoading(call, result); 193 | } 194 | result.success(null); 195 | } 196 | 197 | void close(MethodCall call, MethodChannel.Result result) { 198 | if (webViewManager != null) { 199 | webViewManager.close(call, result); 200 | webViewManager = null; 201 | } 202 | } 203 | 204 | /** 205 | * Checks if can navigate back 206 | * 207 | * @param result 208 | */ 209 | private void canGoBack(MethodChannel.Result result) { 210 | if (webViewManager != null) { 211 | result.success(webViewManager.canGoBack()); 212 | } else { 213 | result.error("Webview is null", null, null); 214 | } 215 | } 216 | 217 | /** 218 | * Navigates back on the Webview. 219 | */ 220 | private void back(MethodCall call, MethodChannel.Result result) { 221 | if (webViewManager != null) { 222 | webViewManager.back(call, result); 223 | } 224 | result.success(null); 225 | } 226 | 227 | /** 228 | * Checks if can navigate forward 229 | * @param result 230 | */ 231 | private void canGoForward(MethodChannel.Result result) { 232 | if (webViewManager != null) { 233 | result.success(webViewManager.canGoForward()); 234 | } else { 235 | result.error("Webview is null", null, null); 236 | } 237 | } 238 | 239 | /** 240 | * Navigates forward on the Webview. 241 | */ 242 | private void forward(MethodCall call, MethodChannel.Result result) { 243 | if (webViewManager != null) { 244 | webViewManager.forward(call, result); 245 | } 246 | result.success(null); 247 | } 248 | 249 | /** 250 | * Reloads the Webview. 251 | */ 252 | private void reload(MethodCall call, MethodChannel.Result result) { 253 | if (webViewManager != null) { 254 | webViewManager.reload(call, result); 255 | } 256 | result.success(null); 257 | } 258 | 259 | private void reloadUrl(MethodCall call, MethodChannel.Result result) { 260 | if (webViewManager != null) { 261 | String url = call.argument("url"); 262 | Map headers = call.argument("headers"); 263 | if (headers != null) { 264 | webViewManager.reloadUrl(url, headers); 265 | } else { 266 | webViewManager.reloadUrl(url); 267 | } 268 | 269 | } 270 | result.success(null); 271 | } 272 | 273 | private void eval(MethodCall call, final MethodChannel.Result result) { 274 | if (webViewManager != null) { 275 | webViewManager.eval(call, result); 276 | } 277 | } 278 | 279 | private void resize(MethodCall call, final MethodChannel.Result result) { 280 | if (webViewManager != null) { 281 | FrameLayout.LayoutParams params = buildLayoutParams(call); 282 | webViewManager.resize(params); 283 | } 284 | result.success(null); 285 | } 286 | 287 | private void hide(MethodCall call, final MethodChannel.Result result) { 288 | if (webViewManager != null) { 289 | webViewManager.hide(call, result); 290 | } 291 | result.success(null); 292 | } 293 | 294 | private void show(MethodCall call, final MethodChannel.Result result) { 295 | if (webViewManager != null) { 296 | webViewManager.show(call, result); 297 | } 298 | result.success(null); 299 | } 300 | 301 | private void cleanCookies(MethodCall call, final MethodChannel.Result result) { 302 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 303 | CookieManager.getInstance().removeAllCookies(new ValueCallback() { 304 | @Override 305 | public void onReceiveValue(Boolean aBoolean) { 306 | 307 | } 308 | }); 309 | } else { 310 | CookieManager.getInstance().removeAllCookie(); 311 | } 312 | result.success(null); 313 | } 314 | 315 | private int dp2px(Context context, float dp) { 316 | final float scale = context.getResources().getDisplayMetrics().density; 317 | return (int) (dp * scale + 0.5f); 318 | } 319 | 320 | @Override 321 | public boolean onActivityResult(int i, int i1, Intent intent) { 322 | if (webViewManager != null && webViewManager.resultHandler != null) { 323 | return webViewManager.resultHandler.handleResult(i, i1, intent); 324 | } 325 | return false; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /lib/src/base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_webview_plugin/src/javascript_channel.dart'; 7 | 8 | import 'javascript_message.dart'; 9 | 10 | const _kChannel = 'flutter_webview_plugin'; 11 | 12 | // TODO: more general state for iOS/android 13 | enum WebViewState { shouldStart, startLoad, finishLoad, abortLoad } 14 | 15 | // TODO: use an id by webview to be able to manage multiple webview 16 | 17 | /// Singleton class that communicate with a Webview Instance 18 | class FlutterWebviewPlugin { 19 | factory FlutterWebviewPlugin() { 20 | if (_instance == null) { 21 | const MethodChannel methodChannel = const MethodChannel(_kChannel); 22 | _instance = FlutterWebviewPlugin.private(methodChannel); 23 | } 24 | return _instance!; 25 | } 26 | 27 | @visibleForTesting 28 | FlutterWebviewPlugin.private(this._channel) { 29 | _channel.setMethodCallHandler(_handleMessages); 30 | } 31 | 32 | static FlutterWebviewPlugin? _instance; 33 | 34 | final MethodChannel _channel; 35 | 36 | final _onBack = StreamController.broadcast(); 37 | final _onDestroy = StreamController.broadcast(); 38 | final _onUrlChanged = StreamController.broadcast(); 39 | final _onStateChanged = StreamController.broadcast(); 40 | final _onScrollXChanged = StreamController.broadcast(); 41 | final _onScrollYChanged = StreamController.broadcast(); 42 | final _onProgressChanged = new StreamController.broadcast(); 43 | final _onHttpError = StreamController.broadcast(); 44 | final _onPostMessage = StreamController.broadcast(); 45 | 46 | final Map _javascriptChannels = 47 | {}; 48 | 49 | Future _handleMessages(MethodCall call) async { 50 | switch (call.method) { 51 | case 'onBack': 52 | _onBack.add(null); 53 | break; 54 | case 'onDestroy': 55 | _onDestroy.add(null); 56 | break; 57 | case 'onUrlChanged': 58 | _onUrlChanged.add(call.arguments['url']); 59 | break; 60 | case 'onScrollXChanged': 61 | _onScrollXChanged.add(call.arguments['xDirection']); 62 | break; 63 | case 'onScrollYChanged': 64 | _onScrollYChanged.add(call.arguments['yDirection']); 65 | break; 66 | case 'onProgressChanged': 67 | _onProgressChanged.add(call.arguments['progress']); 68 | break; 69 | case 'onState': 70 | _onStateChanged.add( 71 | WebViewStateChanged.fromMap( 72 | Map.from(call.arguments), 73 | ), 74 | ); 75 | break; 76 | case 'onHttpError': 77 | _onHttpError.add( 78 | WebViewHttpError(call.arguments['code'], call.arguments['url'])); 79 | break; 80 | case 'javascriptChannelMessage': 81 | _handleJavascriptChannelMessage( 82 | call.arguments['channel'], call.arguments['message']); 83 | break; 84 | } 85 | } 86 | 87 | /// Listening the OnDestroy LifeCycle Event for Android 88 | Stream get onDestroy => _onDestroy.stream; 89 | 90 | /// Listening the back key press Event for Android 91 | Stream get onBack => _onBack.stream; 92 | 93 | /// Listening url changed 94 | Stream get onUrlChanged => _onUrlChanged.stream; 95 | 96 | /// Listening the onState Event for iOS WebView and Android 97 | /// content is Map for type: {shouldStart(iOS)|startLoad|finishLoad} 98 | /// more detail than other events 99 | Stream get onStateChanged => _onStateChanged.stream; 100 | 101 | /// Listening web view loading progress estimation, value between 0.0 and 1.0 102 | Stream get onProgressChanged => _onProgressChanged.stream; 103 | 104 | /// Listening web view y position scroll change 105 | Stream get onScrollYChanged => _onScrollYChanged.stream; 106 | 107 | /// Listening web view x position scroll change 108 | Stream get onScrollXChanged => _onScrollXChanged.stream; 109 | 110 | Stream get onHttpError => _onHttpError.stream; 111 | 112 | /// Start the Webview with [url] 113 | /// - [headers] specify additional HTTP headers 114 | /// - [withJavascript] enable Javascript or not for the Webview 115 | /// - [clearCache] clear the cache of the Webview 116 | /// - [clearCookies] clear all cookies of the Webview 117 | /// - [hidden] not show 118 | /// - [rect]: show in rect, fullscreen if null 119 | /// - [enableAppScheme]: false will enable all schemes, true only for httt/https/about 120 | /// android: Not implemented yet 121 | /// - [userAgent]: set the User-Agent of WebView 122 | /// - [withZoom]: enable zoom on webview 123 | /// - [withLocalStorage] enable localStorage API on Webview 124 | /// Currently Android only. 125 | /// It is always enabled in UIWebView of iOS and can not be disabled. 126 | /// - [withLocalUrl]: allow url as a local path 127 | /// Allow local files on iOs > 9.0 128 | /// - [localUrlScope]: allowed folder for local paths 129 | /// iOS only. 130 | /// If null and withLocalUrl is true, then it will use the url as the scope, 131 | /// allowing only itself to be read. 132 | /// - [scrollBar]: enable or disable scrollbar 133 | /// - [supportMultipleWindows] enable multiple windows support in Android 134 | /// - [invalidUrlRegex] is the regular expression of URLs that web view shouldn't load. 135 | /// For example, when webview is redirected to a specific URL, you want to intercept 136 | /// this process by stopping loading this URL and replacing webview by another screen. 137 | /// Android only settings: 138 | /// - [displayZoomControls]: display zoom controls on webview 139 | /// - [withOverviewMode]: enable overview mode for Android webview ( setLoadWithOverviewMode ) 140 | /// - [useWideViewPort]: use wide viewport for Android webview ( setUseWideViewPort ) 141 | /// - [ignoreSSLErrors]: use to bypass Android/iOS SSL checks e.g. for self-signed certificates 142 | Future launch( 143 | String url, { 144 | Map? headers, 145 | Set javascriptChannels = const {}, 146 | bool withJavascript = true, 147 | bool clearCache = false, 148 | bool clearCookies = false, 149 | bool mediaPlaybackRequiresUserGesture = true, 150 | bool hidden = false, 151 | bool enableAppScheme = true, 152 | Rect? rect, 153 | String? userAgent, 154 | bool withZoom = false, 155 | bool displayZoomControls = false, 156 | bool withLocalStorage = true, 157 | bool withLocalUrl = false, 158 | String? localUrlScope, 159 | bool withOverviewMode = false, 160 | bool scrollBar = true, 161 | bool supportMultipleWindows = false, 162 | bool appCacheEnabled = false, 163 | bool allowFileURLs = false, 164 | bool useWideViewPort = false, 165 | String? invalidUrlRegex, 166 | bool geolocationEnabled = false, 167 | bool debuggingEnabled = false, 168 | bool ignoreSSLErrors = false, 169 | }) async { 170 | final args = { 171 | 'url': url, 172 | 'withJavascript': withJavascript, 173 | 'clearCache': clearCache, 174 | 'hidden': hidden, 175 | 'clearCookies': clearCookies, 176 | 'mediaPlaybackRequiresUserGesture': mediaPlaybackRequiresUserGesture, 177 | 'enableAppScheme': enableAppScheme, 178 | 'userAgent': userAgent, 179 | 'withZoom': withZoom, 180 | 'displayZoomControls': displayZoomControls, 181 | 'withLocalStorage': withLocalStorage, 182 | 'withLocalUrl': withLocalUrl, 183 | 'localUrlScope': localUrlScope, 184 | 'scrollBar': scrollBar, 185 | 'supportMultipleWindows': supportMultipleWindows, 186 | 'appCacheEnabled': appCacheEnabled, 187 | 'allowFileURLs': allowFileURLs, 188 | 'useWideViewPort': useWideViewPort, 189 | 'invalidUrlRegex': invalidUrlRegex, 190 | 'geolocationEnabled': geolocationEnabled, 191 | 'withOverviewMode': withOverviewMode, 192 | 'debuggingEnabled': debuggingEnabled, 193 | 'ignoreSSLErrors': ignoreSSLErrors, 194 | }; 195 | 196 | if (headers != null) { 197 | args['headers'] = headers; 198 | } 199 | 200 | _assertJavascriptChannelNamesAreUnique(javascriptChannels); 201 | 202 | javascriptChannels.forEach((channel) { 203 | _javascriptChannels[channel.name] = channel; 204 | }); 205 | 206 | args['javascriptChannelNames'] = 207 | _extractJavascriptChannelNames(javascriptChannels).toList(); 208 | 209 | if (rect != null) { 210 | args['rect'] = { 211 | 'left': rect.left, 212 | 'top': rect.top, 213 | 'width': rect.width, 214 | 'height': rect.height, 215 | }; 216 | } 217 | await _channel.invokeMethod('launch', args); 218 | } 219 | 220 | /// Execute Javascript inside webview 221 | Future evalJavascript(String code) async { 222 | final res = await _channel.invokeMethod('eval', {'code': code}); 223 | return res; 224 | } 225 | 226 | /// Close the Webview 227 | /// Will trigger the [onDestroy] event 228 | Future close() async { 229 | _javascriptChannels.clear(); 230 | await _channel.invokeMethod('close'); 231 | } 232 | 233 | /// Reloads the WebView. 234 | Future reload() async => await _channel.invokeMethod('reload'); 235 | 236 | /// Navigates back on the Webview. 237 | Future goBack() async => await _channel.invokeMethod('back'); 238 | 239 | /// Checks if webview can navigate back 240 | Future canGoBack() async => await _channel.invokeMethod('canGoBack'); 241 | 242 | /// Checks if webview can navigate back 243 | Future canGoForward() async => 244 | await _channel.invokeMethod('canGoForward'); 245 | 246 | /// Navigates forward on the Webview. 247 | Future goForward() async => await _channel.invokeMethod('forward'); 248 | 249 | // Hides the webview 250 | Future hide() async => await _channel.invokeMethod('hide'); 251 | 252 | // Shows the webview 253 | Future show() async => await _channel.invokeMethod('show'); 254 | 255 | // Clears browser cache 256 | Future clearCache() async => await _channel.invokeMethod('cleanCache'); 257 | 258 | // Reload webview with a url 259 | Future reloadUrl(String url, {Map? headers}) async { 260 | final args = {'url': url}; 261 | if (headers != null) { 262 | args['headers'] = headers; 263 | } 264 | await _channel.invokeMethod('reloadUrl', args); 265 | } 266 | 267 | // Clean cookies on WebView 268 | Future cleanCookies() async { 269 | // one liner to clear javascript cookies 270 | await evalJavascript( 271 | 'document.cookie.split(";").forEach(function(c) { document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); });'); 272 | return await _channel.invokeMethod('cleanCookies'); 273 | } 274 | 275 | // Stops current loading process 276 | Future stopLoading() async => 277 | await _channel.invokeMethod('stopLoading'); 278 | 279 | /// Close all Streams 280 | void dispose() { 281 | _onDestroy.close(); 282 | _onUrlChanged.close(); 283 | _onStateChanged.close(); 284 | _onProgressChanged.close(); 285 | _onScrollXChanged.close(); 286 | _onScrollYChanged.close(); 287 | _onHttpError.close(); 288 | _onPostMessage.close(); 289 | _instance = null; 290 | } 291 | 292 | Future> getCookies() async { 293 | final cookiesString = await evalJavascript('document.cookie'); 294 | final cookies = {}; 295 | 296 | if (cookiesString?.isNotEmpty == true) { 297 | cookiesString!.split(';').forEach((String cookie) { 298 | final split = cookie.split('='); 299 | cookies[split[0]] = split[1]; 300 | }); 301 | } 302 | 303 | return cookies; 304 | } 305 | 306 | /// resize webview 307 | Future resize(Rect rect) async { 308 | final args = {}; 309 | args['rect'] = { 310 | 'left': rect.left, 311 | 'top': rect.top, 312 | 'width': rect.width, 313 | 'height': rect.height, 314 | }; 315 | await _channel.invokeMethod('resize', args); 316 | } 317 | 318 | Set _extractJavascriptChannelNames(Set channels) { 319 | final Set channelNames = 320 | channels.map((JavascriptChannel channel) => channel.name).toSet(); 321 | return channelNames; 322 | } 323 | 324 | void _handleJavascriptChannelMessage( 325 | final String channelName, final String message) { 326 | if (_javascriptChannels.containsKey(channelName)) 327 | _javascriptChannels[channelName]! 328 | .onMessageReceived(JavascriptMessage(message)); 329 | else 330 | print('Channel "$channelName" is not exstis'); 331 | } 332 | 333 | void _assertJavascriptChannelNamesAreUnique( 334 | final Set? channels) { 335 | if (channels == null || channels.isEmpty) { 336 | return; 337 | } 338 | 339 | assert(_extractJavascriptChannelNames(channels).length == channels.length); 340 | } 341 | } 342 | 343 | class WebViewStateChanged { 344 | WebViewStateChanged(this.type, this.url, this.navigationType); 345 | 346 | factory WebViewStateChanged.fromMap(Map map) { 347 | WebViewState t; 348 | switch (map['type']) { 349 | case 'shouldStart': 350 | t = WebViewState.shouldStart; 351 | break; 352 | case 'startLoad': 353 | t = WebViewState.startLoad; 354 | break; 355 | case 'finishLoad': 356 | t = WebViewState.finishLoad; 357 | break; 358 | case 'abortLoad': 359 | t = WebViewState.abortLoad; 360 | break; 361 | default: 362 | throw UnimplementedError( 363 | 'WebViewState type "${map['type']}" is not supported.'); 364 | } 365 | return WebViewStateChanged(t, map['url'], map['navigationType']); 366 | } 367 | 368 | final WebViewState type; 369 | final String url; 370 | final int navigationType; 371 | } 372 | 373 | class WebViewHttpError { 374 | WebViewHttpError(this.code, this.url); 375 | 376 | final String url; 377 | final String code; 378 | } 379 | -------------------------------------------------------------------------------- /android/flutter_webview_plugin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutter_webview_plugin/WebviewManager.java: -------------------------------------------------------------------------------- 1 | package com.flutter_webview_plugin; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.annotation.TargetApi; 6 | import android.app.Activity; 7 | import android.content.Context; 8 | import android.net.http.SslError; 9 | import android.os.Build; 10 | import android.os.Handler; 11 | import android.view.KeyEvent; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.webkit.CookieManager; 15 | import android.webkit.GeolocationPermissions; 16 | import android.webkit.SslErrorHandler; 17 | import android.webkit.ValueCallback; 18 | import android.webkit.WebChromeClient; 19 | import android.webkit.WebSettings; 20 | import android.webkit.WebView; 21 | import android.widget.FrameLayout; 22 | import android.provider.MediaStore; 23 | 24 | import androidx.core.content.FileProvider; 25 | 26 | import android.database.Cursor; 27 | import android.provider.OpenableColumns; 28 | 29 | import java.util.List; 30 | import java.util.ArrayList; 31 | import java.util.HashMap; 32 | import java.util.Map; 33 | import java.io.File; 34 | import java.util.Date; 35 | import java.io.IOException; 36 | import java.text.SimpleDateFormat; 37 | 38 | import io.flutter.plugin.common.MethodCall; 39 | import io.flutter.plugin.common.MethodChannel; 40 | 41 | import static android.app.Activity.RESULT_OK; 42 | 43 | /** 44 | * Created by lejard_h on 20/12/2017. 45 | */ 46 | 47 | class WebviewManager { 48 | 49 | private ValueCallback mUploadMessage; 50 | private ValueCallback mUploadMessageArray; 51 | private final static int FILECHOOSER_RESULTCODE = 1; 52 | private Uri fileUri; 53 | private Uri videoUri; 54 | 55 | private long getFileSize(Uri fileUri) { 56 | Cursor returnCursor = context.getContentResolver().query(fileUri, null, null, null, null); 57 | returnCursor.moveToFirst(); 58 | int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE); 59 | return returnCursor.getLong(sizeIndex); 60 | } 61 | 62 | @TargetApi(7) 63 | class ResultHandler { 64 | public boolean handleResult(int requestCode, int resultCode, Intent intent) { 65 | boolean handled = false; 66 | if (Build.VERSION.SDK_INT >= 21) { 67 | if (requestCode == FILECHOOSER_RESULTCODE) { 68 | Uri[] results = null; 69 | if (resultCode == Activity.RESULT_OK) { 70 | if (fileUri != null && getFileSize(fileUri) > 0) { 71 | results = new Uri[]{fileUri}; 72 | } else if (videoUri != null && getFileSize(videoUri) > 0) { 73 | results = new Uri[]{videoUri}; 74 | } else if (intent != null) { 75 | results = getSelectedFiles(intent); 76 | } 77 | } 78 | if (mUploadMessageArray != null) { 79 | mUploadMessageArray.onReceiveValue(results); 80 | mUploadMessageArray = null; 81 | } 82 | handled = true; 83 | } 84 | } else { 85 | if (requestCode == FILECHOOSER_RESULTCODE) { 86 | Uri result = null; 87 | if (resultCode == RESULT_OK && intent != null) { 88 | result = intent.getData(); 89 | } 90 | if (mUploadMessage != null) { 91 | mUploadMessage.onReceiveValue(result); 92 | mUploadMessage = null; 93 | } 94 | handled = true; 95 | } 96 | } 97 | return handled; 98 | } 99 | } 100 | 101 | private Uri[] getSelectedFiles(Intent data) { 102 | // we have one files selected 103 | if (data.getData() != null) { 104 | String dataString = data.getDataString(); 105 | if (dataString != null) { 106 | return new Uri[]{Uri.parse(dataString)}; 107 | } 108 | } 109 | // we have multiple files selected 110 | if (data.getClipData() != null) { 111 | final int numSelectedFiles = data.getClipData().getItemCount(); 112 | Uri[] result = new Uri[numSelectedFiles]; 113 | for (int i = 0; i < numSelectedFiles; i++) { 114 | result[i] = data.getClipData().getItemAt(i).getUri(); 115 | } 116 | return result; 117 | } 118 | return null; 119 | } 120 | 121 | private final Handler platformThreadHandler; 122 | boolean closed = false; 123 | WebView webView; 124 | Activity activity; 125 | BrowserClient webViewClient; 126 | ResultHandler resultHandler; 127 | Context context; 128 | private boolean ignoreSSLErrors = false; 129 | 130 | WebviewManager(final Activity activity, final Context context, final List channelNames) { 131 | this.webView = new ObservableWebView(activity); 132 | this.activity = activity; 133 | this.context = context; 134 | this.resultHandler = new ResultHandler(); 135 | this.platformThreadHandler = new Handler(context.getMainLooper()); 136 | webViewClient = new BrowserClient() { 137 | @Override 138 | public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { 139 | if (ignoreSSLErrors){ 140 | handler.proceed(); 141 | }else { 142 | super.onReceivedSslError(view, handler, error); 143 | } 144 | } 145 | }; 146 | webView.setOnKeyListener(new View.OnKeyListener() { 147 | @Override 148 | public boolean onKey(View v, int keyCode, KeyEvent event) { 149 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 150 | switch (keyCode) { 151 | case KeyEvent.KEYCODE_BACK: 152 | if (webView.canGoBack()) { 153 | webView.goBack(); 154 | } else { 155 | FlutterWebviewPlugin.channel.invokeMethod("onBack", null); 156 | } 157 | return true; 158 | } 159 | } 160 | 161 | return false; 162 | } 163 | }); 164 | 165 | ((ObservableWebView) webView).setOnScrollChangedCallback(new ObservableWebView.OnScrollChangedCallback() { 166 | public void onScroll(int x, int y, int oldx, int oldy) { 167 | Map yDirection = new HashMap<>(); 168 | yDirection.put("yDirection", (double) y); 169 | FlutterWebviewPlugin.channel.invokeMethod("onScrollYChanged", yDirection); 170 | Map xDirection = new HashMap<>(); 171 | xDirection.put("xDirection", (double) x); 172 | FlutterWebviewPlugin.channel.invokeMethod("onScrollXChanged", xDirection); 173 | } 174 | }); 175 | 176 | webView.setWebViewClient(webViewClient); 177 | webView.setWebChromeClient(new WebChromeClient() { 178 | //The undocumented magic method override 179 | //Eclipse will swear at you if you try to put @Override here 180 | // For Android 3.0+ 181 | public void openFileChooser(ValueCallback uploadMsg) { 182 | 183 | mUploadMessage = uploadMsg; 184 | Intent i = new Intent(Intent.ACTION_GET_CONTENT); 185 | i.addCategory(Intent.CATEGORY_OPENABLE); 186 | i.setType("image/*"); 187 | activity.startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE); 188 | 189 | } 190 | 191 | // For Android 3.0+ 192 | public void openFileChooser(ValueCallback uploadMsg, String acceptType) { 193 | mUploadMessage = uploadMsg; 194 | Intent i = new Intent(Intent.ACTION_GET_CONTENT); 195 | i.addCategory(Intent.CATEGORY_OPENABLE); 196 | i.setType("*/*"); 197 | activity.startActivityForResult( 198 | Intent.createChooser(i, "File Browser"), 199 | FILECHOOSER_RESULTCODE); 200 | } 201 | 202 | //For Android 4.1 203 | public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) { 204 | mUploadMessage = uploadMsg; 205 | Intent i = new Intent(Intent.ACTION_GET_CONTENT); 206 | i.addCategory(Intent.CATEGORY_OPENABLE); 207 | i.setType("image/*"); 208 | activity.startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE); 209 | 210 | } 211 | 212 | //For Android 5.0+ 213 | public boolean onShowFileChooser( 214 | WebView webView, ValueCallback filePathCallback, 215 | FileChooserParams fileChooserParams) { 216 | if (mUploadMessageArray != null) { 217 | mUploadMessageArray.onReceiveValue(null); 218 | } 219 | mUploadMessageArray = filePathCallback; 220 | 221 | final String[] acceptTypes = getSafeAcceptedTypes(fileChooserParams); 222 | List intentList = new ArrayList(); 223 | fileUri = null; 224 | videoUri = null; 225 | if (acceptsImages(acceptTypes)) { 226 | Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 227 | fileUri = getOutputFilename(MediaStore.ACTION_IMAGE_CAPTURE); 228 | takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); 229 | intentList.add(takePhotoIntent); 230 | } 231 | if (acceptsVideo(acceptTypes)) { 232 | Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 233 | videoUri = getOutputFilename(MediaStore.ACTION_VIDEO_CAPTURE); 234 | takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri); 235 | intentList.add(takeVideoIntent); 236 | } 237 | Intent contentSelectionIntent; 238 | if (Build.VERSION.SDK_INT >= 21) { 239 | final boolean allowMultiple = fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE; 240 | contentSelectionIntent = fileChooserParams.createIntent(); 241 | contentSelectionIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); 242 | } else { 243 | contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); 244 | contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); 245 | contentSelectionIntent.setType("*/*"); 246 | } 247 | Intent[] intentArray = intentList.toArray(new Intent[intentList.size()]); 248 | 249 | Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); 250 | chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); 251 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); 252 | activity.startActivityForResult(chooserIntent, FILECHOOSER_RESULTCODE); 253 | return true; 254 | } 255 | 256 | 257 | @Override 258 | public void onProgressChanged(WebView view, int progress) { 259 | Map args = new HashMap<>(); 260 | args.put("progress", progress / 100.0); 261 | FlutterWebviewPlugin.channel.invokeMethod("onProgressChanged", args); 262 | } 263 | 264 | public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { 265 | callback.invoke(origin, true, false); 266 | } 267 | }); 268 | registerJavaScriptChannelNames(channelNames); 269 | } 270 | 271 | private Uri getOutputFilename(String intentType) { 272 | String prefix = ""; 273 | String suffix = ""; 274 | 275 | if (intentType == MediaStore.ACTION_IMAGE_CAPTURE) { 276 | prefix = "image-"; 277 | suffix = ".jpg"; 278 | } else if (intentType == MediaStore.ACTION_VIDEO_CAPTURE) { 279 | prefix = "video-"; 280 | suffix = ".mp4"; 281 | } 282 | 283 | String packageName = context.getPackageName(); 284 | File capturedFile = null; 285 | try { 286 | capturedFile = createCapturedFile(prefix, suffix); 287 | } catch (IOException e) { 288 | e.printStackTrace(); 289 | } 290 | return FileProvider.getUriForFile(context, packageName + ".fileprovider", capturedFile); 291 | } 292 | 293 | private File createCapturedFile(String prefix, String suffix) throws IOException { 294 | String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); 295 | String imageFileName = prefix + "_" + timeStamp; 296 | File storageDir = context.getExternalFilesDir(null); 297 | return File.createTempFile(imageFileName, suffix, storageDir); 298 | } 299 | 300 | private Boolean acceptsImages(String[] types) { 301 | return isArrayEmpty(types) || arrayContainsString(types, "image"); 302 | } 303 | 304 | private Boolean acceptsVideo(String[] types) { 305 | return isArrayEmpty(types) || arrayContainsString(types, "video"); 306 | } 307 | 308 | private Boolean arrayContainsString(String[] array, String pattern) { 309 | for (String content : array) { 310 | if (content.contains(pattern)) { 311 | return true; 312 | } 313 | } 314 | return false; 315 | } 316 | 317 | private Boolean isArrayEmpty(String[] arr) { 318 | // when our array returned from getAcceptTypes() has no values set from the 319 | // webview 320 | // i.e. , without any "accept" attr 321 | // will be an array with one empty string element, afaik 322 | return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0); 323 | } 324 | 325 | private String[] getSafeAcceptedTypes(WebChromeClient.FileChooserParams params) { 326 | 327 | // the getAcceptTypes() is available only in api 21+ 328 | // for lower level, we ignore it 329 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 330 | return params.getAcceptTypes(); 331 | } 332 | 333 | final String[] EMPTY = {}; 334 | return EMPTY; 335 | } 336 | 337 | private void clearCookies() { 338 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 339 | CookieManager.getInstance().removeAllCookies(new ValueCallback() { 340 | @Override 341 | public void onReceiveValue(Boolean aBoolean) { 342 | 343 | } 344 | }); 345 | } else { 346 | CookieManager.getInstance().removeAllCookie(); 347 | } 348 | } 349 | 350 | private void clearCache() { 351 | webView.clearCache(true); 352 | webView.clearFormData(); 353 | } 354 | 355 | private void registerJavaScriptChannelNames(List channelNames) { 356 | for (String channelName : channelNames) { 357 | webView.addJavascriptInterface( 358 | new JavaScriptChannel(FlutterWebviewPlugin.channel, channelName, platformThreadHandler), channelName); 359 | } 360 | } 361 | 362 | void openUrl( 363 | boolean withJavascript, 364 | boolean clearCache, 365 | boolean hidden, 366 | boolean clearCookies, 367 | boolean mediaPlaybackRequiresUserGesture, 368 | String userAgent, 369 | String url, 370 | Map headers, 371 | boolean withZoom, 372 | boolean displayZoomControls, 373 | boolean withLocalStorage, 374 | boolean withOverviewMode, 375 | boolean scrollBar, 376 | boolean supportMultipleWindows, 377 | boolean appCacheEnabled, 378 | boolean allowFileURLs, 379 | boolean useWideViewPort, 380 | String invalidUrlRegex, 381 | boolean geolocationEnabled, 382 | boolean debuggingEnabled, 383 | boolean ignoreSSLErrors 384 | ) { 385 | webView.getSettings().setJavaScriptEnabled(withJavascript); 386 | webView.getSettings().setBuiltInZoomControls(withZoom); 387 | webView.getSettings().setSupportZoom(withZoom); 388 | webView.getSettings().setDisplayZoomControls(displayZoomControls); 389 | webView.getSettings().setDomStorageEnabled(withLocalStorage); 390 | webView.getSettings().setLoadWithOverviewMode(withOverviewMode); 391 | webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(supportMultipleWindows); 392 | 393 | webView.getSettings().setSupportMultipleWindows(supportMultipleWindows); 394 | 395 | webView.getSettings().setAppCacheEnabled(appCacheEnabled); 396 | 397 | webView.getSettings().setAllowFileAccessFromFileURLs(allowFileURLs); 398 | webView.getSettings().setAllowUniversalAccessFromFileURLs(allowFileURLs); 399 | 400 | webView.getSettings().setUseWideViewPort(useWideViewPort); 401 | 402 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 403 | webView.getSettings().setMediaPlaybackRequiresUserGesture(mediaPlaybackRequiresUserGesture); 404 | } 405 | 406 | // Handle debugging 407 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 408 | webView.setWebContentsDebuggingEnabled(debuggingEnabled); 409 | } 410 | //ignore SSL errors 411 | this.ignoreSSLErrors = ignoreSSLErrors; 412 | 413 | webViewClient.updateInvalidUrlRegex(invalidUrlRegex); 414 | 415 | if (geolocationEnabled) { 416 | webView.getSettings().setGeolocationEnabled(true); 417 | } 418 | 419 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 420 | webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); 421 | } 422 | 423 | if (clearCache) { 424 | clearCache(); 425 | } 426 | 427 | if (hidden) { 428 | webView.setVisibility(View.GONE); 429 | } 430 | 431 | if (clearCookies) { 432 | clearCookies(); 433 | } 434 | 435 | if (userAgent != null) { 436 | webView.getSettings().setUserAgentString(userAgent); 437 | } 438 | 439 | if (!scrollBar) { 440 | webView.setVerticalScrollBarEnabled(false); 441 | } 442 | 443 | if (headers != null) { 444 | webView.loadUrl(url, headers); 445 | } else { 446 | webView.loadUrl(url); 447 | } 448 | } 449 | 450 | void reloadUrl(String url) { 451 | webView.loadUrl(url); 452 | } 453 | 454 | void reloadUrl(String url, Map headers) { 455 | webView.loadUrl(url, headers); 456 | } 457 | 458 | void close(MethodCall call, MethodChannel.Result result) { 459 | if (webView != null) { 460 | ViewGroup vg = (ViewGroup) (webView.getParent()); 461 | vg.removeView(webView); 462 | } 463 | webView = null; 464 | if (result != null) { 465 | result.success(null); 466 | } 467 | 468 | closed = true; 469 | FlutterWebviewPlugin.channel.invokeMethod("onDestroy", null); 470 | } 471 | 472 | void close() { 473 | close(null, null); 474 | } 475 | 476 | @TargetApi(Build.VERSION_CODES.KITKAT) 477 | void eval(MethodCall call, final MethodChannel.Result result) { 478 | String code = call.argument("code"); 479 | 480 | webView.evaluateJavascript(code, new ValueCallback() { 481 | @Override 482 | public void onReceiveValue(String value) { 483 | result.success(value); 484 | } 485 | }); 486 | } 487 | 488 | /** 489 | * Reloads the Webview. 490 | */ 491 | void reload(MethodCall call, MethodChannel.Result result) { 492 | if (webView != null) { 493 | webView.reload(); 494 | } 495 | } 496 | 497 | /** 498 | * Navigates back on the Webview. 499 | */ 500 | void back(MethodCall call, MethodChannel.Result result) { 501 | if (webView != null && webView.canGoBack()) { 502 | webView.goBack(); 503 | } 504 | } 505 | 506 | /** 507 | * Navigates forward on the Webview. 508 | */ 509 | void forward(MethodCall call, MethodChannel.Result result) { 510 | if (webView != null && webView.canGoForward()) { 511 | webView.goForward(); 512 | } 513 | } 514 | 515 | void resize(FrameLayout.LayoutParams params) { 516 | webView.setLayoutParams(params); 517 | } 518 | 519 | /** 520 | * Checks if going back on the Webview is possible. 521 | */ 522 | boolean canGoBack() { 523 | return webView.canGoBack(); 524 | } 525 | 526 | /** 527 | * Checks if going forward on the Webview is possible. 528 | */ 529 | boolean canGoForward() { 530 | return webView.canGoForward(); 531 | } 532 | 533 | /** 534 | * Clears cache 535 | */ 536 | void cleanCache(){ 537 | webView.clearCache(true); 538 | } 539 | 540 | void hide(MethodCall call, MethodChannel.Result result) { 541 | if (webView != null) { 542 | webView.setVisibility(View.GONE); 543 | } 544 | } 545 | 546 | void show(MethodCall call, MethodChannel.Result result) { 547 | if (webView != null) { 548 | webView.setVisibility(View.VISIBLE); 549 | } 550 | } 551 | 552 | void stopLoading(MethodCall call, MethodChannel.Result result) { 553 | if (webView != null) { 554 | webView.stopLoading(); 555 | } 556 | } 557 | } 558 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 11 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 12 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 13 | 945777F11EF64758001C8557 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 945777F01EF64758001C8557 /* GeneratedPluginRegistrant.m */; }; 14 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 15 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 16 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 17 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 18 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 19 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 20 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 21 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 22 | C1AB67DD965258C508BF745D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3CDD044DB4E60255722586 /* Pods_Runner.framework */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXCopyFilesBuildPhase section */ 26 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 27 | isa = PBXCopyFilesBuildPhase; 28 | buildActionMask = 2147483647; 29 | dstPath = ""; 30 | dstSubfolderSpec = 10; 31 | files = ( 32 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 33 | ); 34 | name = "Embed Frameworks"; 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXCopyFilesBuildPhase section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 24E8D05517AF08B6C698879C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 41 | 2E218F99AE7B8D5BDA44909D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 42 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 43 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 44 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 45 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 46 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 47 | 945777EF1EF64758001C8557 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 48 | 945777F01EF64758001C8557 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 49 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 50 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 51 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 52 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 54 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 55 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 57 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | 9A3CDD044DB4E60255722586 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 67 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 68 | C1AB67DD965258C508BF745D /* Pods_Runner.framework in Frameworks */, 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | /* End PBXFrameworksBuildPhase section */ 73 | 74 | /* Begin PBXGroup section */ 75 | 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 24E8D05517AF08B6C698879C /* Pods-Runner.debug.xcconfig */, 79 | 2E218F99AE7B8D5BDA44909D /* Pods-Runner.release.xcconfig */, 80 | ); 81 | name = Pods; 82 | sourceTree = ""; 83 | }; 84 | 9740EEB11CF90186004384FC /* Flutter */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 3B80C3931E831B6300D905FE /* App.framework */, 88 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 89 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 90 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 91 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 92 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 93 | ); 94 | name = Flutter; 95 | sourceTree = ""; 96 | }; 97 | 97C146E51CF9000F007C117D = { 98 | isa = PBXGroup; 99 | children = ( 100 | 9740EEB11CF90186004384FC /* Flutter */, 101 | 97C146F01CF9000F007C117D /* Runner */, 102 | 97C146EF1CF9000F007C117D /* Products */, 103 | 840012C8B5EDBCF56B0E4AC1 /* Pods */, 104 | CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, 105 | ); 106 | sourceTree = ""; 107 | }; 108 | 97C146EF1CF9000F007C117D /* Products */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 97C146EE1CF9000F007C117D /* Runner.app */, 112 | ); 113 | name = Products; 114 | sourceTree = ""; 115 | }; 116 | 97C146F01CF9000F007C117D /* Runner */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 945777EF1EF64758001C8557 /* GeneratedPluginRegistrant.h */, 120 | 945777F01EF64758001C8557 /* GeneratedPluginRegistrant.m */, 121 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 122 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 123 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 124 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 125 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 126 | 97C147021CF9000F007C117D /* Info.plist */, 127 | 97C146F11CF9000F007C117D /* Supporting Files */, 128 | ); 129 | path = Runner; 130 | sourceTree = ""; 131 | }; 132 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 97C146F21CF9000F007C117D /* main.m */, 136 | ); 137 | name = "Supporting Files"; 138 | sourceTree = ""; 139 | }; 140 | CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 9A3CDD044DB4E60255722586 /* Pods_Runner.framework */, 144 | ); 145 | name = Frameworks; 146 | sourceTree = ""; 147 | }; 148 | /* End PBXGroup section */ 149 | 150 | /* Begin PBXNativeTarget section */ 151 | 97C146ED1CF9000F007C117D /* Runner */ = { 152 | isa = PBXNativeTarget; 153 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 154 | buildPhases = ( 155 | AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, 156 | 9740EEB61CF901F6004384FC /* Run Script */, 157 | 97C146EA1CF9000F007C117D /* Sources */, 158 | 97C146EB1CF9000F007C117D /* Frameworks */, 159 | 97C146EC1CF9000F007C117D /* Resources */, 160 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 161 | 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, 162 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 163 | ); 164 | buildRules = ( 165 | ); 166 | dependencies = ( 167 | ); 168 | name = Runner; 169 | productName = Runner; 170 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 171 | productType = "com.apple.product-type.application"; 172 | }; 173 | /* End PBXNativeTarget section */ 174 | 175 | /* Begin PBXProject section */ 176 | 97C146E61CF9000F007C117D /* Project object */ = { 177 | isa = PBXProject; 178 | attributes = { 179 | LastUpgradeCheck = 0830; 180 | ORGANIZATIONNAME = "The Chromium Authors"; 181 | TargetAttributes = { 182 | 97C146ED1CF9000F007C117D = { 183 | CreatedOnToolsVersion = 7.3.1; 184 | }; 185 | }; 186 | }; 187 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 188 | compatibilityVersion = "Xcode 3.2"; 189 | developmentRegion = English; 190 | hasScannedForEncodings = 0; 191 | knownRegions = ( 192 | English, 193 | en, 194 | Base, 195 | ); 196 | mainGroup = 97C146E51CF9000F007C117D; 197 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 198 | projectDirPath = ""; 199 | projectRoot = ""; 200 | targets = ( 201 | 97C146ED1CF9000F007C117D /* Runner */, 202 | ); 203 | }; 204 | /* End PBXProject section */ 205 | 206 | /* Begin PBXResourcesBuildPhase section */ 207 | 97C146EC1CF9000F007C117D /* Resources */ = { 208 | isa = PBXResourcesBuildPhase; 209 | buildActionMask = 2147483647; 210 | files = ( 211 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 212 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 213 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 214 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 215 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 216 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | /* End PBXResourcesBuildPhase section */ 221 | 222 | /* Begin PBXShellScriptBuildPhase section */ 223 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 224 | isa = PBXShellScriptBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | ); 228 | inputPaths = ( 229 | ); 230 | name = "Thin Binary"; 231 | outputPaths = ( 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | shellPath = /bin/sh; 235 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 236 | }; 237 | 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { 238 | isa = PBXShellScriptBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | ); 242 | inputPaths = ( 243 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", 244 | "${PODS_ROOT}/../../../../../flutter/bin/cache/artifacts/engine/ios/Flutter.framework", 245 | "${BUILT_PRODUCTS_DIR}/flutter_webview_plugin/flutter_webview_plugin.framework", 246 | ); 247 | name = "[CP] Embed Pods Frameworks"; 248 | outputPaths = ( 249 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", 250 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_webview_plugin.framework", 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | shellPath = /bin/sh; 254 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 255 | showEnvVarsInLog = 0; 256 | }; 257 | 9740EEB61CF901F6004384FC /* Run Script */ = { 258 | isa = PBXShellScriptBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | ); 262 | inputPaths = ( 263 | ); 264 | name = "Run Script"; 265 | outputPaths = ( 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | shellPath = /bin/sh; 269 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 270 | }; 271 | AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { 272 | isa = PBXShellScriptBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | ); 276 | inputPaths = ( 277 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 278 | "${PODS_ROOT}/Manifest.lock", 279 | ); 280 | name = "[CP] Check Pods Manifest.lock"; 281 | outputPaths = ( 282 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | shellPath = /bin/sh; 286 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 287 | showEnvVarsInLog = 0; 288 | }; 289 | /* End PBXShellScriptBuildPhase section */ 290 | 291 | /* Begin PBXSourcesBuildPhase section */ 292 | 97C146EA1CF9000F007C117D /* Sources */ = { 293 | isa = PBXSourcesBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 297 | 97C146F31CF9000F007C117D /* main.m in Sources */, 298 | 945777F11EF64758001C8557 /* GeneratedPluginRegistrant.m in Sources */, 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | /* End PBXSourcesBuildPhase section */ 303 | 304 | /* Begin PBXVariantGroup section */ 305 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 306 | isa = PBXVariantGroup; 307 | children = ( 308 | 97C146FB1CF9000F007C117D /* Base */, 309 | ); 310 | name = Main.storyboard; 311 | sourceTree = ""; 312 | }; 313 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 314 | isa = PBXVariantGroup; 315 | children = ( 316 | 97C147001CF9000F007C117D /* Base */, 317 | ); 318 | name = LaunchScreen.storyboard; 319 | sourceTree = ""; 320 | }; 321 | /* End PBXVariantGroup section */ 322 | 323 | /* Begin XCBuildConfiguration section */ 324 | 97C147031CF9000F007C117D /* Debug */ = { 325 | isa = XCBuildConfiguration; 326 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 327 | buildSettings = { 328 | ALWAYS_SEARCH_USER_PATHS = NO; 329 | CLANG_ANALYZER_NONNULL = YES; 330 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 331 | CLANG_CXX_LIBRARY = "libc++"; 332 | CLANG_ENABLE_MODULES = YES; 333 | CLANG_ENABLE_OBJC_ARC = YES; 334 | CLANG_WARN_BOOL_CONVERSION = YES; 335 | CLANG_WARN_CONSTANT_CONVERSION = YES; 336 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 337 | CLANG_WARN_EMPTY_BODY = YES; 338 | CLANG_WARN_ENUM_CONVERSION = YES; 339 | CLANG_WARN_INFINITE_RECURSION = YES; 340 | CLANG_WARN_INT_CONVERSION = YES; 341 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 342 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 343 | CLANG_WARN_UNREACHABLE_CODE = YES; 344 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 345 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 346 | COPY_PHASE_STRIP = NO; 347 | DEBUG_INFORMATION_FORMAT = dwarf; 348 | ENABLE_STRICT_OBJC_MSGSEND = YES; 349 | ENABLE_TESTABILITY = YES; 350 | GCC_C_LANGUAGE_STANDARD = gnu99; 351 | GCC_DYNAMIC_NO_PIC = NO; 352 | GCC_NO_COMMON_BLOCKS = YES; 353 | GCC_OPTIMIZATION_LEVEL = 0; 354 | GCC_PREPROCESSOR_DEFINITIONS = ( 355 | "DEBUG=1", 356 | "$(inherited)", 357 | ); 358 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 359 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 360 | GCC_WARN_UNDECLARED_SELECTOR = YES; 361 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 362 | GCC_WARN_UNUSED_FUNCTION = YES; 363 | GCC_WARN_UNUSED_VARIABLE = YES; 364 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 365 | MTL_ENABLE_DEBUG_INFO = YES; 366 | ONLY_ACTIVE_ARCH = YES; 367 | SDKROOT = iphoneos; 368 | TARGETED_DEVICE_FAMILY = "1,2"; 369 | }; 370 | name = Debug; 371 | }; 372 | 97C147041CF9000F007C117D /* Release */ = { 373 | isa = XCBuildConfiguration; 374 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 375 | buildSettings = { 376 | ALWAYS_SEARCH_USER_PATHS = NO; 377 | CLANG_ANALYZER_NONNULL = YES; 378 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 379 | CLANG_CXX_LIBRARY = "libc++"; 380 | CLANG_ENABLE_MODULES = YES; 381 | CLANG_ENABLE_OBJC_ARC = YES; 382 | CLANG_WARN_BOOL_CONVERSION = YES; 383 | CLANG_WARN_CONSTANT_CONVERSION = YES; 384 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 385 | CLANG_WARN_EMPTY_BODY = YES; 386 | CLANG_WARN_ENUM_CONVERSION = YES; 387 | CLANG_WARN_INFINITE_RECURSION = YES; 388 | CLANG_WARN_INT_CONVERSION = YES; 389 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 390 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 391 | CLANG_WARN_UNREACHABLE_CODE = YES; 392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 393 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 394 | COPY_PHASE_STRIP = NO; 395 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 396 | ENABLE_NS_ASSERTIONS = NO; 397 | ENABLE_STRICT_OBJC_MSGSEND = YES; 398 | GCC_C_LANGUAGE_STANDARD = gnu99; 399 | GCC_NO_COMMON_BLOCKS = YES; 400 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 401 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 402 | GCC_WARN_UNDECLARED_SELECTOR = YES; 403 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 404 | GCC_WARN_UNUSED_FUNCTION = YES; 405 | GCC_WARN_UNUSED_VARIABLE = YES; 406 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 407 | MTL_ENABLE_DEBUG_INFO = NO; 408 | SDKROOT = iphoneos; 409 | TARGETED_DEVICE_FAMILY = "1,2"; 410 | VALIDATE_PRODUCT = YES; 411 | }; 412 | name = Release; 413 | }; 414 | 97C147061CF9000F007C117D /* Debug */ = { 415 | isa = XCBuildConfiguration; 416 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 417 | buildSettings = { 418 | ARCHS = arm64; 419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 420 | ENABLE_BITCODE = NO; 421 | FRAMEWORK_SEARCH_PATHS = ( 422 | "$(inherited)", 423 | "$(PROJECT_DIR)/Flutter", 424 | ); 425 | INFOPLIST_FILE = Runner/Info.plist; 426 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 427 | LIBRARY_SEARCH_PATHS = ( 428 | "$(inherited)", 429 | "$(PROJECT_DIR)/Flutter", 430 | ); 431 | PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.flutterWebviewPluginExample; 432 | PRODUCT_NAME = "$(TARGET_NAME)"; 433 | }; 434 | name = Debug; 435 | }; 436 | 97C147071CF9000F007C117D /* Release */ = { 437 | isa = XCBuildConfiguration; 438 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 439 | buildSettings = { 440 | ARCHS = arm64; 441 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 442 | ENABLE_BITCODE = NO; 443 | FRAMEWORK_SEARCH_PATHS = ( 444 | "$(inherited)", 445 | "$(PROJECT_DIR)/Flutter", 446 | ); 447 | INFOPLIST_FILE = Runner/Info.plist; 448 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 449 | LIBRARY_SEARCH_PATHS = ( 450 | "$(inherited)", 451 | "$(PROJECT_DIR)/Flutter", 452 | ); 453 | PRODUCT_BUNDLE_IDENTIFIER = com.yourcompany.flutterWebviewPluginExample; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | }; 456 | name = Release; 457 | }; 458 | /* End XCBuildConfiguration section */ 459 | 460 | /* Begin XCConfigurationList section */ 461 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 462 | isa = XCConfigurationList; 463 | buildConfigurations = ( 464 | 97C147031CF9000F007C117D /* Debug */, 465 | 97C147041CF9000F007C117D /* Release */, 466 | ); 467 | defaultConfigurationIsVisible = 0; 468 | defaultConfigurationName = Release; 469 | }; 470 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 471 | isa = XCConfigurationList; 472 | buildConfigurations = ( 473 | 97C147061CF9000F007C117D /* Debug */, 474 | 97C147071CF9000F007C117D /* Release */, 475 | ); 476 | defaultConfigurationIsVisible = 0; 477 | defaultConfigurationName = Release; 478 | }; 479 | /* End XCConfigurationList section */ 480 | }; 481 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 482 | } 483 | --------------------------------------------------------------------------------