base.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import 'dart:async';
  2. import 'dart:ui';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter_webview_plugin/src/javascript_channel.dart';
  6. import 'javascript_message.dart';
  7. const _kChannel = 'flutter_webview_plugin';
  8. // TODO: more general state for iOS/android
  9. enum WebViewState { shouldStart, startLoad, finishLoad, abortLoad }
  10. // TODO: use an id by webview to be able to manage multiple webview
  11. /// Singleton class that communicate with a Webview Instance
  12. class FlutterWebviewPlugin {
  13. factory FlutterWebviewPlugin() {
  14. if(_instance == null) {
  15. const MethodChannel methodChannel = const MethodChannel(_kChannel);
  16. _instance = FlutterWebviewPlugin.private(methodChannel);
  17. }
  18. return _instance;
  19. }
  20. @visibleForTesting
  21. FlutterWebviewPlugin.private(this._channel) {
  22. _channel.setMethodCallHandler(_handleMessages);
  23. }
  24. static FlutterWebviewPlugin _instance;
  25. final MethodChannel _channel;
  26. final _onBack = StreamController<Null>.broadcast();
  27. final _onDestroy = StreamController<Null>.broadcast();
  28. final _onUrlChanged = StreamController<String>.broadcast();
  29. final _onStateChanged = StreamController<WebViewStateChanged>.broadcast();
  30. final _onScrollXChanged = StreamController<double>.broadcast();
  31. final _onScrollYChanged = StreamController<double>.broadcast();
  32. final _onProgressChanged = new StreamController<double>.broadcast();
  33. final _onHttpError = StreamController<WebViewHttpError>.broadcast();
  34. final _onPostMessage = StreamController<JavascriptMessage>.broadcast();
  35. final Map<String, JavascriptChannel> _javascriptChannels =
  36. // ignoring warning as min SDK version doesn't support collection literals yet
  37. // ignore: prefer_collection_literals
  38. Map<String, JavascriptChannel>();
  39. Future<Null> _handleMessages(MethodCall call) async {
  40. switch (call.method) {
  41. case 'onBack':
  42. _onBack.add(null);
  43. break;
  44. case 'onDestroy':
  45. _onDestroy.add(null);
  46. break;
  47. case 'onUrlChanged':
  48. _onUrlChanged.add(call.arguments['url']);
  49. break;
  50. case 'onScrollXChanged':
  51. _onScrollXChanged.add(call.arguments['xDirection']);
  52. break;
  53. case 'onScrollYChanged':
  54. _onScrollYChanged.add(call.arguments['yDirection']);
  55. break;
  56. case 'onProgressChanged':
  57. _onProgressChanged.add(call.arguments['progress']);
  58. break;
  59. case 'onState':
  60. _onStateChanged.add(
  61. WebViewStateChanged.fromMap(
  62. Map<String, dynamic>.from(call.arguments),
  63. ),
  64. );
  65. break;
  66. case 'onHttpError':
  67. _onHttpError.add(
  68. WebViewHttpError(call.arguments['code'], call.arguments['url']));
  69. break;
  70. case 'javascriptChannelMessage':
  71. _handleJavascriptChannelMessage(
  72. call.arguments['channel'], call.arguments['message']);
  73. break;
  74. }
  75. }
  76. /// Listening the OnDestroy LifeCycle Event for Android
  77. Stream<Null> get onDestroy => _onDestroy.stream;
  78. /// Listening the back key press Event for Android
  79. Stream<Null> get onBack => _onBack.stream;
  80. /// Listening url changed
  81. Stream<String> get onUrlChanged => _onUrlChanged.stream;
  82. /// Listening the onState Event for iOS WebView and Android
  83. /// content is Map for type: {shouldStart(iOS)|startLoad|finishLoad}
  84. /// more detail than other events
  85. Stream<WebViewStateChanged> get onStateChanged => _onStateChanged.stream;
  86. /// Listening web view loading progress estimation, value between 0.0 and 1.0
  87. Stream<double> get onProgressChanged => _onProgressChanged.stream;
  88. /// Listening web view y position scroll change
  89. Stream<double> get onScrollYChanged => _onScrollYChanged.stream;
  90. /// Listening web view x position scroll change
  91. Stream<double> get onScrollXChanged => _onScrollXChanged.stream;
  92. Stream<WebViewHttpError> get onHttpError => _onHttpError.stream;
  93. /// Start the Webview with [url]
  94. /// - [headers] specify additional HTTP headers
  95. /// - [withJavascript] enable Javascript or not for the Webview
  96. /// - [clearCache] clear the cache of the Webview
  97. /// - [clearCookies] clear all cookies of the Webview
  98. /// - [hidden] not show
  99. /// - [rect]: show in rect, fullscreen if null
  100. /// - [enableAppScheme]: false will enable all schemes, true only for httt/https/about
  101. /// android: Not implemented yet
  102. /// - [userAgent]: set the User-Agent of WebView
  103. /// - [withZoom]: enable zoom on webview
  104. /// - [withLocalStorage] enable localStorage API on Webview
  105. /// Currently Android only.
  106. /// It is always enabled in UIWebView of iOS and can not be disabled.
  107. /// - [withLocalUrl]: allow url as a local path
  108. /// Allow local files on iOs > 9.0
  109. /// - [localUrlScope]: allowed folder for local paths
  110. /// iOS only.
  111. /// If null and withLocalUrl is true, then it will use the url as the scope,
  112. /// allowing only itself to be read.
  113. /// - [scrollBar]: enable or disable scrollbar
  114. /// - [supportMultipleWindows] enable multiple windows support in Android
  115. /// - [invalidUrlRegex] is the regular expression of URLs that web view shouldn't load.
  116. /// For example, when webview is redirected to a specific URL, you want to intercept
  117. /// this process by stopping loading this URL and replacing webview by another screen.
  118. /// Android only settings:
  119. /// - [displayZoomControls]: display zoom controls on webview
  120. /// - [withOverviewMode]: enable overview mode for Android webview ( setLoadWithOverviewMode )
  121. /// - [useWideViewPort]: use wide viewport for Android webview ( setUseWideViewPort )
  122. Future<Null> launch(
  123. String url, {
  124. Map<String, String> headers,
  125. Set<JavascriptChannel> javascriptChannels,
  126. bool withJavascript,
  127. bool clearCache,
  128. bool clearCookies,
  129. bool hidden,
  130. bool enableAppScheme,
  131. Rect rect,
  132. String userAgent,
  133. bool withZoom,
  134. bool displayZoomControls,
  135. bool withLocalStorage,
  136. bool withLocalUrl,
  137. String localUrlScope,
  138. bool withOverviewMode,
  139. bool scrollBar,
  140. bool supportMultipleWindows,
  141. bool appCacheEnabled,
  142. bool allowFileURLs,
  143. bool useWideViewPort,
  144. String invalidUrlRegex,
  145. bool geolocationEnabled,
  146. bool debuggingEnabled,
  147. }) async {
  148. final args = <String, dynamic>{
  149. 'url': url,
  150. 'withJavascript': withJavascript ?? true,
  151. 'clearCache': clearCache ?? false,
  152. 'hidden': hidden ?? false,
  153. 'clearCookies': clearCookies ?? false,
  154. 'enableAppScheme': enableAppScheme ?? true,
  155. 'userAgent': userAgent,
  156. 'withZoom': withZoom ?? false,
  157. 'displayZoomControls': displayZoomControls ?? false,
  158. 'withLocalStorage': withLocalStorage ?? true,
  159. 'withLocalUrl': withLocalUrl ?? false,
  160. 'localUrlScope': localUrlScope,
  161. 'scrollBar': scrollBar ?? true,
  162. 'supportMultipleWindows': supportMultipleWindows ?? false,
  163. 'appCacheEnabled': appCacheEnabled ?? false,
  164. 'allowFileURLs': allowFileURLs ?? false,
  165. 'useWideViewPort': useWideViewPort ?? false,
  166. 'invalidUrlRegex': invalidUrlRegex,
  167. 'geolocationEnabled': geolocationEnabled ?? false,
  168. 'withOverviewMode': withOverviewMode ?? false,
  169. 'debuggingEnabled': debuggingEnabled ?? false,
  170. };
  171. if (headers != null) {
  172. args['headers'] = headers;
  173. }
  174. _assertJavascriptChannelNamesAreUnique(javascriptChannels);
  175. if (javascriptChannels != null) {
  176. javascriptChannels.forEach((channel) {
  177. _javascriptChannels[channel.name] = channel;
  178. });
  179. } else {
  180. if (_javascriptChannels.isNotEmpty) {
  181. _javascriptChannels.clear();
  182. }
  183. }
  184. args['javascriptChannelNames'] =
  185. _extractJavascriptChannelNames(javascriptChannels).toList();
  186. if (rect != null) {
  187. args['rect'] = {
  188. 'left': rect.left,
  189. 'top': rect.top,
  190. 'width': rect.width,
  191. 'height': rect.height,
  192. };
  193. }
  194. await _channel.invokeMethod('launch', args);
  195. }
  196. /// Execute Javascript inside webview
  197. Future<String> evalJavascript(String code) async {
  198. final res = await _channel.invokeMethod('eval', {'code': code});
  199. return res;
  200. }
  201. /// Close the Webview
  202. /// Will trigger the [onDestroy] event
  203. Future<Null> close() async {
  204. _javascriptChannels.clear();
  205. await _channel.invokeMethod('close');
  206. }
  207. /// Reloads the WebView.
  208. Future<Null> reload() async => await _channel.invokeMethod('reload');
  209. /// Navigates back on the Webview.
  210. Future<Null> goBack() async => await _channel.invokeMethod('back');
  211. /// Checks if webview can navigate back
  212. Future<bool> canGoBack() async => await _channel.invokeMethod('canGoBack');
  213. /// Checks if webview can navigate back
  214. Future<bool> canGoForward() async => await _channel.invokeMethod('canGoForward');
  215. /// Navigates forward on the Webview.
  216. Future<Null> goForward() async => await _channel.invokeMethod('forward');
  217. // Hides the webview
  218. Future<Null> hide() async => await _channel.invokeMethod('hide');
  219. // Shows the webview
  220. Future<Null> show() async => await _channel.invokeMethod('show');
  221. // Reload webview with a url
  222. Future<Null> reloadUrl(String url, {Map<String, String> headers}) async {
  223. final args = <String, dynamic>{'url': url};
  224. if (headers != null) {
  225. args['headers'] = headers;
  226. }
  227. await _channel.invokeMethod('reloadUrl', args);
  228. }
  229. // Clean cookies on WebView
  230. Future<Null> cleanCookies() async {
  231. // one liner to clear javascript cookies
  232. await evalJavascript('document.cookie.split(";").forEach(function(c) { document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); });');
  233. return await _channel.invokeMethod('cleanCookies');
  234. }
  235. // Stops current loading process
  236. Future<Null> stopLoading() async =>
  237. await _channel.invokeMethod('stopLoading');
  238. /// Close all Streams
  239. void dispose() {
  240. _onDestroy.close();
  241. _onUrlChanged.close();
  242. _onStateChanged.close();
  243. _onProgressChanged.close();
  244. _onScrollXChanged.close();
  245. _onScrollYChanged.close();
  246. _onHttpError.close();
  247. _onPostMessage.close();
  248. _instance = null;
  249. }
  250. Future<Map<String, String>> getCookies() async {
  251. final cookiesString = await evalJavascript('document.cookie');
  252. final cookies = <String, String>{};
  253. if (cookiesString?.isNotEmpty == true) {
  254. cookiesString.split(';').forEach((String cookie) {
  255. final split = cookie.split('=');
  256. cookies[split[0]] = split[1];
  257. });
  258. }
  259. return cookies;
  260. }
  261. /// resize webview
  262. Future<Null> resize(Rect rect) async {
  263. final args = {};
  264. args['rect'] = {
  265. 'left': rect.left,
  266. 'top': rect.top,
  267. 'width': rect.width,
  268. 'height': rect.height,
  269. };
  270. await _channel.invokeMethod('resize', args);
  271. }
  272. Set<String> _extractJavascriptChannelNames(Set<JavascriptChannel> channels) {
  273. final Set<String> channelNames = channels == null
  274. // ignore: prefer_collection_literals
  275. ? Set<String>()
  276. : channels.map((JavascriptChannel channel) => channel.name).toSet();
  277. return channelNames;
  278. }
  279. void _handleJavascriptChannelMessage(
  280. final String channelName, final String message) {
  281. _javascriptChannels[channelName]
  282. .onMessageReceived(JavascriptMessage(message));
  283. }
  284. void _assertJavascriptChannelNamesAreUnique(
  285. final Set<JavascriptChannel> channels) {
  286. if (channels == null || channels.isEmpty) {
  287. return;
  288. }
  289. assert(_extractJavascriptChannelNames(channels).length == channels.length);
  290. }
  291. }
  292. class WebViewStateChanged {
  293. WebViewStateChanged(this.type, this.url, this.navigationType);
  294. factory WebViewStateChanged.fromMap(Map<String, dynamic> map) {
  295. WebViewState t;
  296. switch (map['type']) {
  297. case 'shouldStart':
  298. t = WebViewState.shouldStart;
  299. break;
  300. case 'startLoad':
  301. t = WebViewState.startLoad;
  302. break;
  303. case 'finishLoad':
  304. t = WebViewState.finishLoad;
  305. break;
  306. case 'abortLoad':
  307. t = WebViewState.abortLoad;
  308. break;
  309. }
  310. return WebViewStateChanged(t, map['url'], map['navigationType']);
  311. }
  312. final WebViewState type;
  313. final String url;
  314. final int navigationType;
  315. }
  316. class WebViewHttpError {
  317. WebViewHttpError(this.code, this.url);
  318. final String url;
  319. final String code;
  320. }