Skip to content

Commit 5eaa5c9

Browse files
authored
[webview_flutter_wkwebview] Extended Web View API on iOS to add flexibility when working with local HTML content (#8787)
## Overview The `readAccessURLProvider` property has been added to the `WebKitWebViewControllerCreationParams` class. This function allows customization of the path to associated resources when loading a local HTML page using `loadFile` on iOS. ### What Is the Issue Fixes flutter/flutter#136479 The native `WKWebView` takes two arguments when loading local HTML pages: 1. **The file URL** – The local HTML file to be loaded. 2. **The read access URL** – A file or directory that `WKWebView` can access (e.g., .css, .js files). Currently, the Flutter implementation always sets the parent folder of the specified HTML file as the read access directory. This behavior is not configurable, which limits how local web content can be structured. For example, the following structure does not work because the .css and .js files are not in the parent directory of the HTML file: ``` /app_resources/ │── styles/ │ ├── main.css │ │── scripts/ │ ├── app.js │ │── pages/ ├── index.html (Loaded file) ``` With the existing behavior, `WKWebView` cannot `access styles/main.css` and `scripts/app.js` because the parent folder of `index.html` (`/pages/`) does not contain them. ### How This Resolves the Issue The `readAccessURLProvider` property has been added to the `WebKitWebViewControllerCreationParams` class. This property accepts a function that takes the path of the HTML file being loaded and returns the path to the associated resources. ![Screenshot at Mar 04 14-49-46](https://github.com/user-attachments/assets/be9ce41f-4a8c-4e91-b73c-041e22f59d29) Each time `loadFile` is called, the controller invokes this function and passes its result as the second argument to the underlying `WKWebView` implementation. By default, the function returns the parent directory of the specified HTML file, preserving the existing behavior when no custom provider is set. ### Impact on End Users 1. Developers can now explicitly specify the directory that `WKWebView` should allow access to when loading a local HTML page, providing greater flexibility in organizing assets. 2. The existing API signature remains unchanged, ensuring that the update does not require any modifications to existing codebases unless the new functionality is utilized.
1 parent f0b2726 commit 5eaa5c9

File tree

10 files changed

+354
-55
lines changed

10 files changed

+354
-55
lines changed

packages/webview_flutter/webview_flutter_android/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 4.9.0
2+
3+
* Adds support for `PlatformWebViewController.loadFileWithParams`.
4+
* Introduces `AndroidLoadFileParams`, a platform-specific extension of `LoadFileParams` for Android that adds support for `headers`.
5+
16
## 4.8.2
27

38
* Bumps gradle from 8.9.0 to 8.11.1.

packages/webview_flutter/webview_flutter_android/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies:
1717
# The example app is bundled with the plugin so we use a path dependency on
1818
# the parent directory to use the current plugin's version.
1919
path: ../
20-
webview_flutter_platform_interface: ^2.13.0
20+
webview_flutter_platform_interface: ^2.14.0
2121

2222
dev_dependencies:
2323
espresso: ^0.4.0

packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,41 @@ import 'android_webkit_constants.dart';
1818
import 'platform_views_service_proxy.dart';
1919
import 'weak_reference_utils.dart';
2020

21+
/// Object specifying parameters for loading a local file in a
22+
/// [AndroidWebViewController].
23+
@immutable
24+
base class AndroidLoadFileParams extends LoadFileParams {
25+
/// Constructs a [AndroidLoadFileParams], the subclass of a [LoadFileParams].
26+
AndroidLoadFileParams({
27+
required String absoluteFilePath,
28+
this.headers = const <String, String>{},
29+
}) : super(
30+
absoluteFilePath: absoluteFilePath.startsWith('file://')
31+
? absoluteFilePath
32+
: Uri.file(absoluteFilePath).toString(),
33+
);
34+
35+
/// Constructs a [AndroidLoadFileParams] using a [LoadFileParams].
36+
factory AndroidLoadFileParams.fromLoadFileParams(
37+
LoadFileParams params, {
38+
Map<String, String> headers = const <String, String>{},
39+
}) {
40+
return AndroidLoadFileParams(
41+
absoluteFilePath: params.absoluteFilePath,
42+
headers: headers,
43+
);
44+
}
45+
46+
/// Additional HTTP headers to be included when loading the local file.
47+
///
48+
/// If not provided at initialization time, doesn't add any additional headers.
49+
///
50+
/// On Android, WebView supports adding headers when loading local or remote
51+
/// content. This can be useful for scenarios like authentication,
52+
/// content-type overrides, or custom request context.
53+
final Map<String, String> headers;
54+
}
55+
2156
/// Object specifying creation parameters for creating a [AndroidWebViewController].
2257
///
2358
/// When adding additional fields make sure they can be null or have a default
@@ -386,12 +421,29 @@ class AndroidWebViewController extends PlatformWebViewController {
386421
Future<void> loadFile(
387422
String absoluteFilePath,
388423
) {
389-
final String url = absoluteFilePath.startsWith('file://')
390-
? absoluteFilePath
391-
: Uri.file(absoluteFilePath).toString();
424+
return loadFileWithParams(
425+
AndroidLoadFileParams(
426+
absoluteFilePath: absoluteFilePath,
427+
),
428+
);
429+
}
392430

393-
_webView.settings.setAllowFileAccess(true);
394-
return _webView.loadUrl(url, <String, String>{});
431+
@override
432+
Future<void> loadFileWithParams(
433+
LoadFileParams params,
434+
) async {
435+
switch (params) {
436+
case final AndroidLoadFileParams params:
437+
await Future.wait(<Future<void>>[
438+
_webView.settings.setAllowFileAccess(true),
439+
_webView.loadUrl(params.absoluteFilePath, params.headers),
440+
]);
441+
442+
default:
443+
await loadFileWithParams(
444+
AndroidLoadFileParams.fromLoadFileParams(params),
445+
);
446+
}
395447
}
396448

397449
@override

packages/webview_flutter/webview_flutter_android/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: webview_flutter_android
22
description: A Flutter plugin that provides a WebView widget on Android.
33
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
5-
version: 4.8.2
5+
version: 4.9.0
66

77
environment:
88
sdk: ^3.6.0
@@ -21,7 +21,7 @@ dependencies:
2121
flutter:
2222
sdk: flutter
2323
meta: ^1.10.0
24-
webview_flutter_platform_interface: ^2.13.0
24+
webview_flutter_platform_interface: ^2.14.0
2525

2626
dev_dependencies:
2727
build_runner: ^2.1.4

packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart

Lines changed: 194 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ void main() {
274274
));
275275
}
276276

277-
test('loadFile without file prefix', () async {
277+
test('Initializing WebView settings on controller creation', () async {
278278
final MockWebView mockWebView = MockWebView();
279279
final MockWebSettings mockWebSettings = MockWebSettings();
280280
createControllerWithMocks(
@@ -292,56 +292,209 @@ void main() {
292292
verify(mockWebSettings.setUseWideViewPort(false)).called(1);
293293
});
294294

295-
test('loadFile without file prefix', () async {
296-
final MockWebView mockWebView = MockWebView();
297-
final MockWebSettings mockWebSettings = MockWebSettings();
298-
final AndroidWebViewController controller = createControllerWithMocks(
299-
mockWebView: mockWebView,
300-
mockSettings: mockWebSettings,
301-
);
295+
group('loadFile', () {
296+
test('Without file prefix', () async {
297+
final MockWebView mockWebView = MockWebView();
298+
final MockWebSettings mockWebSettings = MockWebSettings();
299+
final AndroidWebViewController controller = createControllerWithMocks(
300+
mockWebView: mockWebView,
301+
mockSettings: mockWebSettings,
302+
);
302303

303-
await controller.loadFile('/path/to/file.html');
304+
await controller.loadFile('/path/to/file.html');
304305

305-
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
306-
verify(mockWebView.loadUrl(
307-
'file:///path/to/file.html',
308-
<String, String>{},
309-
)).called(1);
310-
});
306+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
307+
verify(mockWebView.loadUrl(
308+
'file:///path/to/file.html',
309+
<String, String>{},
310+
)).called(1);
311+
});
311312

312-
test('loadFile without file prefix and characters to be escaped', () async {
313-
final MockWebView mockWebView = MockWebView();
314-
final MockWebSettings mockWebSettings = MockWebSettings();
315-
final AndroidWebViewController controller = createControllerWithMocks(
316-
mockWebView: mockWebView,
317-
mockSettings: mockWebSettings,
318-
);
313+
test('Without file prefix and characters to be escaped', () async {
314+
final MockWebView mockWebView = MockWebView();
315+
final MockWebSettings mockWebSettings = MockWebSettings();
316+
final AndroidWebViewController controller = createControllerWithMocks(
317+
mockWebView: mockWebView,
318+
mockSettings: mockWebSettings,
319+
);
319320

320-
await controller.loadFile('/path/to/?_<_>_.html');
321+
await controller.loadFile('/path/to/?_<_>_.html');
321322

322-
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
323-
verify(mockWebView.loadUrl(
324-
'file:///path/to/%3F_%3C_%3E_.html',
325-
<String, String>{},
326-
)).called(1);
323+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
324+
verify(mockWebView.loadUrl(
325+
'file:///path/to/%3F_%3C_%3E_.html',
326+
<String, String>{},
327+
)).called(1);
328+
});
329+
330+
test('With file prefix', () async {
331+
final MockWebView mockWebView = MockWebView();
332+
final MockWebSettings mockWebSettings = MockWebSettings();
333+
final AndroidWebViewController controller = createControllerWithMocks(
334+
mockWebView: mockWebView,
335+
);
336+
337+
when(mockWebView.settings).thenReturn(mockWebSettings);
338+
339+
await controller.loadFile('file:///path/to/file.html');
340+
341+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
342+
verify(mockWebView.loadUrl(
343+
'file:///path/to/file.html',
344+
<String, String>{},
345+
)).called(1);
346+
});
327347
});
328348

329-
test('loadFile with file prefix', () async {
330-
final MockWebView mockWebView = MockWebView();
331-
final MockWebSettings mockWebSettings = MockWebSettings();
332-
final AndroidWebViewController controller = createControllerWithMocks(
333-
mockWebView: mockWebView,
334-
);
349+
group('loadFileWithParams', () {
350+
group('Using LoadFileParams model', () {
351+
test('Without file prefix', () async {
352+
final MockWebView mockWebView = MockWebView();
353+
final MockWebSettings mockWebSettings = MockWebSettings();
354+
final AndroidWebViewController controller = createControllerWithMocks(
355+
mockWebView: mockWebView,
356+
mockSettings: mockWebSettings,
357+
);
335358

336-
when(mockWebView.settings).thenReturn(mockWebSettings);
359+
await controller.loadFileWithParams(
360+
const LoadFileParams(absoluteFilePath: '/path/to/file.html'),
361+
);
337362

338-
await controller.loadFile('file:///path/to/file.html');
363+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
364+
verify(mockWebView.loadUrl(
365+
'file:///path/to/file.html',
366+
<String, String>{},
367+
)).called(1);
368+
});
339369

340-
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
341-
verify(mockWebView.loadUrl(
342-
'file:///path/to/file.html',
343-
<String, String>{},
344-
)).called(1);
370+
test('Without file prefix and characters to be escaped', () async {
371+
final MockWebView mockWebView = MockWebView();
372+
final MockWebSettings mockWebSettings = MockWebSettings();
373+
final AndroidWebViewController controller = createControllerWithMocks(
374+
mockWebView: mockWebView,
375+
mockSettings: mockWebSettings,
376+
);
377+
378+
await controller.loadFileWithParams(
379+
const LoadFileParams(absoluteFilePath: '/path/to/?_<_>_.html'),
380+
);
381+
382+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
383+
verify(mockWebView.loadUrl(
384+
'file:///path/to/%3F_%3C_%3E_.html',
385+
<String, String>{},
386+
)).called(1);
387+
});
388+
389+
test('With file prefix', () async {
390+
final MockWebView mockWebView = MockWebView();
391+
final MockWebSettings mockWebSettings = MockWebSettings();
392+
final AndroidWebViewController controller = createControllerWithMocks(
393+
mockWebView: mockWebView,
394+
mockSettings: mockWebSettings,
395+
);
396+
397+
await controller.loadFileWithParams(
398+
const LoadFileParams(absoluteFilePath: 'file:///path/to/file.html'),
399+
);
400+
401+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
402+
verify(mockWebView.loadUrl(
403+
'file:///path/to/file.html',
404+
<String, String>{},
405+
)).called(1);
406+
});
407+
});
408+
409+
group('Using WebKitLoadFileParams model', () {
410+
test('Without file prefix', () async {
411+
final MockWebView mockWebView = MockWebView();
412+
final MockWebSettings mockWebSettings = MockWebSettings();
413+
final AndroidWebViewController controller = createControllerWithMocks(
414+
mockWebView: mockWebView,
415+
mockSettings: mockWebSettings,
416+
);
417+
418+
await controller.loadFileWithParams(
419+
AndroidLoadFileParams(absoluteFilePath: '/path/to/file.html'),
420+
);
421+
422+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
423+
verify(mockWebView.loadUrl(
424+
'file:///path/to/file.html',
425+
<String, String>{},
426+
)).called(1);
427+
});
428+
429+
test('Without file prefix and characters to be escaped', () async {
430+
final MockWebView mockWebView = MockWebView();
431+
final MockWebSettings mockWebSettings = MockWebSettings();
432+
final AndroidWebViewController controller = createControllerWithMocks(
433+
mockWebView: mockWebView,
434+
mockSettings: mockWebSettings,
435+
);
436+
437+
await controller.loadFileWithParams(
438+
AndroidLoadFileParams(absoluteFilePath: '/path/to/?_<_>_.html'),
439+
);
440+
441+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
442+
verify(mockWebView.loadUrl(
443+
'file:///path/to/%3F_%3C_%3E_.html',
444+
<String, String>{},
445+
)).called(1);
446+
});
447+
448+
test('With file prefix', () async {
449+
final MockWebView mockWebView = MockWebView();
450+
final MockWebSettings mockWebSettings = MockWebSettings();
451+
final AndroidWebViewController controller = createControllerWithMocks(
452+
mockWebView: mockWebView,
453+
mockSettings: mockWebSettings,
454+
);
455+
456+
await controller.loadFileWithParams(
457+
AndroidLoadFileParams(
458+
absoluteFilePath: 'file:///path/to/file.html'),
459+
);
460+
461+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
462+
verify(mockWebView.loadUrl(
463+
'file:///path/to/file.html',
464+
<String, String>{},
465+
)).called(1);
466+
});
467+
468+
test('With additional headers', () async {
469+
final MockWebView mockWebView = MockWebView();
470+
final MockWebSettings mockWebSettings = MockWebSettings();
471+
final AndroidWebViewController controller = createControllerWithMocks(
472+
mockWebView: mockWebView,
473+
mockSettings: mockWebSettings,
474+
);
475+
476+
await controller.loadFileWithParams(
477+
AndroidLoadFileParams(
478+
absoluteFilePath: 'file:///path/to/file.html',
479+
headers: const <String, String>{
480+
'Authorization': 'Bearer test_token',
481+
'Cache-Control': 'no-cache',
482+
'X-Custom-Header': 'test-value',
483+
},
484+
),
485+
);
486+
487+
verify(mockWebSettings.setAllowFileAccess(true)).called(1);
488+
verify(mockWebView.loadUrl(
489+
'file:///path/to/file.html',
490+
const <String, String>{
491+
'Authorization': 'Bearer test_token',
492+
'Cache-Control': 'no-cache',
493+
'X-Custom-Header': 'test-value',
494+
},
495+
)).called(1);
496+
});
497+
});
345498
});
346499

347500
test('loadFlutterAsset when asset does not exist', () async {

packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 3.23.0
2+
3+
* Adds support for `PlatformWebViewController.loadFileWithParams`.
4+
* Introduces `WebKitLoadFileParams`, a platform-specific extension of `LoadFileParams` for iOS and macOS that adds support for `readAccessPath`.
5+
16
## 3.22.1
27

38
* Changes the handling of a Flutter method failure from throwing an assertion error to logging the

packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ dependencies:
1010
flutter:
1111
sdk: flutter
1212
path_provider: ^2.0.6
13-
webview_flutter_platform_interface: ^2.13.0
13+
webview_flutter_platform_interface: ^2.14.0
1414
webview_flutter_wkwebview:
1515
# When depending on this package from a real application you should use:
1616
# webview_flutter: ^x.y.z

0 commit comments

Comments
 (0)