Skip to content

Commit 4290d18

Browse files
[webview_flutter] Support for loading progress tracking (flutter#2151)
1 parent f125331 commit 4290d18

File tree

11 files changed

+202
-1
lines changed

11 files changed

+202
-1
lines changed

packages/webview_flutter/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.0.0-nullsafety.6
2+
3+
* Added support for progress tracking.
4+
15
## 2.0.0-nullsafety.5
26

37
* Add section to the wiki explaining how to use Material components.

packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) {
7272

7373
return true;
7474
}
75+
76+
@Override
77+
public void onProgressChanged(WebView view, int progress) {
78+
flutterWebViewClient.onLoadingProgress(progress);
79+
}
7580
}
7681

7782
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@@ -367,6 +372,9 @@ private void applySettings(Map<String, Object> settings) {
367372
webView.setWebContentsDebuggingEnabled(debuggingEnabled);
368373
}
369374
break;
375+
case "hasProgressTracking":
376+
flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key);
377+
break;
370378
case "gestureNavigationEnabled":
371379
break;
372380
case "userAgent":

packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class FlutterWebViewClient {
3030
private static final String TAG = "FlutterWebViewClient";
3131
private final MethodChannel methodChannel;
3232
private boolean hasNavigationDelegate;
33+
boolean hasProgressTracking;
3334

3435
FlutterWebViewClient(MethodChannel methodChannel) {
3536
this.methodChannel = methodChannel;
@@ -125,6 +126,14 @@ private void onPageFinished(WebView view, String url) {
125126
methodChannel.invokeMethod("onPageFinished", args);
126127
}
127128

129+
void onLoadingProgress(int progress) {
130+
if (hasProgressTracking) {
131+
Map<String, Object> args = new HashMap<>();
132+
args.put("progress", progress);
133+
methodChannel.invokeMethod("onProgress", args);
134+
}
135+
}
136+
128137
private void onWebResourceError(
129138
final int errorCode, final String description, final String failingUrl) {
130139
final Map<String, Object> args = new HashMap<>();

packages/webview_flutter/example/lib/main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class _WebViewExampleState extends State<WebViewExample> {
6262
onWebViewCreated: (WebViewController webViewController) {
6363
_controller.complete(webViewController);
6464
},
65+
onProgress: (int progress) {
66+
print("WebView is loading (progress : $progress%)");
67+
},
6568
javascriptChannels: <JavascriptChannel>{
6669
_toasterJavascriptChannel(context),
6770
},
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 <Flutter/Flutter.h>
6+
#import <Foundation/Foundation.h>
7+
#import <WebKit/WebKit.h>
8+
9+
NS_ASSUME_NONNULL_BEGIN
10+
11+
@interface FLTWKProgressionDelegate : NSObject
12+
13+
- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel;
14+
15+
- (void)stopObservingProgress:(WKWebView *)webView;
16+
17+
@end
18+
19+
NS_ASSUME_NONNULL_END
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 "FLTWKProgressionDelegate.h"
6+
7+
NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress";
8+
9+
@implementation FLTWKProgressionDelegate {
10+
FlutterMethodChannel *_methodChannel;
11+
}
12+
13+
- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel {
14+
self = [super init];
15+
if (self) {
16+
_methodChannel = channel;
17+
[webView addObserver:self
18+
forKeyPath:FLTWKEstimatedProgressKeyPath
19+
options:NSKeyValueObservingOptionNew
20+
context:nil];
21+
}
22+
return self;
23+
}
24+
25+
- (void)stopObservingProgress:(WKWebView *)webView {
26+
[webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath];
27+
}
28+
29+
- (void)observeValueForKeyPath:(NSString *)keyPath
30+
ofObject:(id)object
31+
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
32+
context:(void *)context {
33+
if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) {
34+
NSNumber *newValue =
35+
change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0
36+
int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100
37+
[_methodChannel invokeMethod:@"onProgress"
38+
arguments:@{@"progress" : [NSNumber numberWithInt:newValueAsInt]}];
39+
}
40+
}
41+
42+
@end

packages/webview_flutter/ios/Classes/FlutterWebView.m

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#import "FlutterWebView.h"
66
#import "FLTWKNavigationDelegate.h"
7+
#import "FLTWKProgressionDelegate.h"
78
#import "JavaScriptChannelHandler.h"
89

910
@implementation FLTWebViewFactory {
@@ -64,6 +65,7 @@ @implementation FLTWebViewController {
6465
// The set of registered JavaScript channel names.
6566
NSMutableSet* _javaScriptChannelNames;
6667
FLTWKNavigationDelegate* _navigationDelegate;
68+
FLTWKProgressionDelegate* _progressionDelegate;
6769
}
6870

6971
- (instancetype)initWithFrame:(CGRect)frame
@@ -119,6 +121,12 @@ - (instancetype)initWithFrame:(CGRect)frame
119121
return self;
120122
}
121123

124+
- (void)dealloc {
125+
if (_progressionDelegate != nil) {
126+
[_progressionDelegate stopObservingProgress:_webView];
127+
}
128+
}
129+
122130
- (UIView*)view {
123131
return _webView;
124132
}
@@ -323,6 +331,13 @@ - (NSString*)applySettings:(NSDictionary<NSString*, id>*)settings {
323331
} else if ([key isEqualToString:@"hasNavigationDelegate"]) {
324332
NSNumber* hasDartNavigationDelegate = settings[key];
325333
_navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue];
334+
} else if ([key isEqualToString:@"hasProgressTracking"]) {
335+
NSNumber* hasProgressTrackingValue = settings[key];
336+
bool hasProgressTracking = [hasProgressTrackingValue boolValue];
337+
if (hasProgressTracking) {
338+
_progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView
339+
channel:_channel];
340+
}
326341
} else if ([key isEqualToString:@"debuggingEnabled"]) {
327342
// no-op debugging is always enabled on iOS.
328343
} else if ([key isEqualToString:@"gestureNavigationEnabled"]) {

packages/webview_flutter/lib/platform_interface.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ abstract class WebViewPlatformCallbacksHandler {
3030
/// Invoked by [WebViewPlatformController] when a page has finished loading.
3131
void onPageFinished(String url);
3232

33+
/// Invoked by [WebViewPlatformController] when a page is loading.
34+
/// /// Only works when [WebSettings.hasProgressTracking] is set to `true`.
35+
void onProgress(int progress);
36+
3337
/// Report web resource loading error to the host application.
3438
void onWebResourceError(WebResourceError error);
3539
}
@@ -388,6 +392,7 @@ class WebSettings {
388392
WebSettings({
389393
this.javascriptMode,
390394
this.hasNavigationDelegate,
395+
this.hasProgressTracking,
391396
this.debuggingEnabled,
392397
this.gestureNavigationEnabled,
393398
this.allowsInlineMediaPlayback,
@@ -400,6 +405,10 @@ class WebSettings {
400405
/// Whether the [WebView] has a [NavigationDelegate] set.
401406
final bool? hasNavigationDelegate;
402407

408+
/// Whether the [WebView] should track page loading progress.
409+
/// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress.
410+
final bool? hasProgressTracking;
411+
403412
/// Whether to enable the platform's webview content debugging tools.
404413
///
405414
/// See also: [WebView.debuggingEnabled].
@@ -427,7 +436,7 @@ class WebSettings {
427436

428437
@override
429438
String toString() {
430-
return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)';
439+
return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)';
431440
}
432441
}
433442

packages/webview_flutter/lib/src/webview_method_channel.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController {
4040
case 'onPageFinished':
4141
_platformCallbacksHandler.onPageFinished(call.arguments['url']!);
4242
return null;
43+
case 'onProgress':
44+
_platformCallbacksHandler.onProgress(call.arguments['progress']);
45+
return null;
4346
case 'onPageStarted':
4447
_platformCallbacksHandler.onPageStarted(call.arguments['url']!);
4548
return null;
@@ -183,6 +186,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController {
183186

184187
_addIfNonNull('jsMode', settings!.javascriptMode?.index);
185188
_addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate);
189+
_addIfNonNull('hasProgressTracking', settings.hasProgressTracking);
186190
_addIfNonNull('debuggingEnabled', settings.debuggingEnabled);
187191
_addIfNonNull(
188192
'gestureNavigationEnabled', settings.gestureNavigationEnabled);

packages/webview_flutter/lib/webview_flutter.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ typedef void PageStartedCallback(String url);
142142
/// Signature for when a [WebView] has finished loading a page.
143143
typedef void PageFinishedCallback(String url);
144144

145+
/// Signature for when a [WebView] is loading a page.
146+
typedef void PageLoadingCallback(int progress);
147+
145148
/// Signature for when a [WebView] has failed to load a resource.
146149
typedef void WebResourceErrorCallback(WebResourceError error);
147150

@@ -217,6 +220,7 @@ class WebView extends StatefulWidget {
217220
this.gestureRecognizers,
218221
this.onPageStarted,
219222
this.onPageFinished,
223+
this.onProgress,
220224
this.onWebResourceError,
221225
this.debuggingEnabled = false,
222226
this.gestureNavigationEnabled = false,
@@ -357,6 +361,9 @@ class WebView extends StatefulWidget {
357361
/// [WebViewController.evaluateJavascript] can assume this.
358362
final PageFinishedCallback? onPageFinished;
359363

364+
/// Invoked when a page is loading.
365+
final PageLoadingCallback? onProgress;
366+
360367
/// Invoked when a web resource has failed to load.
361368
///
362369
/// This can be called for any resource (iframe, image, etc.), not just for
@@ -476,6 +483,7 @@ WebSettings _webSettingsFromWidget(WebView widget) {
476483
return WebSettings(
477484
javascriptMode: widget.javascriptMode,
478485
hasNavigationDelegate: widget.navigationDelegate != null,
486+
hasProgressTracking: widget.onProgress != null,
479487
debuggingEnabled: widget.debuggingEnabled,
480488
gestureNavigationEnabled: widget.gestureNavigationEnabled,
481489
allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback,
@@ -488,6 +496,7 @@ WebSettings _clearUnchangedWebSettings(
488496
WebSettings currentValue, WebSettings newValue) {
489497
assert(currentValue.javascriptMode != null);
490498
assert(currentValue.hasNavigationDelegate != null);
499+
assert(currentValue.hasProgressTracking != null);
491500
assert(currentValue.debuggingEnabled != null);
492501
assert(currentValue.userAgent != null);
493502
assert(newValue.javascriptMode != null);
@@ -497,6 +506,7 @@ WebSettings _clearUnchangedWebSettings(
497506

498507
JavascriptMode? javascriptMode;
499508
bool? hasNavigationDelegate;
509+
bool? hasProgressTracking;
500510
bool? debuggingEnabled;
501511
WebSetting<String?> userAgent = WebSetting.absent();
502512
if (currentValue.javascriptMode != newValue.javascriptMode) {
@@ -505,6 +515,9 @@ WebSettings _clearUnchangedWebSettings(
505515
if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) {
506516
hasNavigationDelegate = newValue.hasNavigationDelegate;
507517
}
518+
if (currentValue.hasProgressTracking != newValue.hasProgressTracking) {
519+
hasProgressTracking = newValue.hasProgressTracking;
520+
}
508521
if (currentValue.debuggingEnabled != newValue.debuggingEnabled) {
509522
debuggingEnabled = newValue.debuggingEnabled;
510523
}
@@ -515,6 +528,7 @@ WebSettings _clearUnchangedWebSettings(
515528
return WebSettings(
516529
javascriptMode: javascriptMode,
517530
hasNavigationDelegate: hasNavigationDelegate,
531+
hasProgressTracking: hasProgressTracking,
518532
debuggingEnabled: debuggingEnabled,
519533
userAgent: userAgent,
520534
);
@@ -571,6 +585,12 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler {
571585
}
572586

573587
@override
588+
void onProgress(int progress) {
589+
if (_widget.onProgress != null) {
590+
_widget.onProgress!(progress);
591+
}
592+
}
593+
574594
void onWebResourceError(WebResourceError error) {
575595
if (_widget.onWebResourceError != null) {
576596
_widget.onWebResourceError!(error);

packages/webview_flutter/test/webview_flutter_test.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,62 @@ void main() {
692692
});
693693
});
694694

695+
group('$PageLoadingCallback', () {
696+
testWidgets('onLoadingProgress is not null', (WidgetTester tester) async {
697+
int? loadingProgress;
698+
699+
await tester.pumpWidget(WebView(
700+
initialUrl: 'https://youtube.com',
701+
onProgress: (int progress) {
702+
loadingProgress = progress;
703+
},
704+
));
705+
706+
final FakePlatformWebView? platformWebView =
707+
fakePlatformViewsController.lastCreatedView;
708+
709+
platformWebView?.fakeOnProgressCallback(50);
710+
711+
expect(loadingProgress, 50);
712+
});
713+
714+
testWidgets('onLoadingProgress is null', (WidgetTester tester) async {
715+
await tester.pumpWidget(const WebView(
716+
initialUrl: 'https://youtube.com',
717+
onProgress: null,
718+
));
719+
720+
final FakePlatformWebView platformWebView =
721+
fakePlatformViewsController.lastCreatedView!;
722+
723+
// This is to test that it does not crash on a null callback.
724+
platformWebView.fakeOnProgressCallback(50);
725+
});
726+
727+
testWidgets('onLoadingProgress changed', (WidgetTester tester) async {
728+
int? loadingProgress;
729+
730+
await tester.pumpWidget(WebView(
731+
initialUrl: 'https://youtube.com',
732+
onProgress: (int progress) {},
733+
));
734+
735+
await tester.pumpWidget(WebView(
736+
initialUrl: 'https://youtube.com',
737+
onProgress: (int progress) {
738+
loadingProgress = progress;
739+
},
740+
));
741+
742+
final FakePlatformWebView platformWebView =
743+
fakePlatformViewsController.lastCreatedView!;
744+
745+
platformWebView.fakeOnProgressCallback(50);
746+
747+
expect(loadingProgress, 50);
748+
});
749+
});
750+
695751
group('navigationDelegate', () {
696752
testWidgets('hasNavigationDelegate', (WidgetTester tester) async {
697753
await tester.pumpWidget(const WebView(
@@ -1021,6 +1077,18 @@ class FakePlatformWebView {
10211077
);
10221078
}
10231079

1080+
void fakeOnProgressCallback(int progress) {
1081+
final StandardMethodCodec codec = const StandardMethodCodec();
1082+
1083+
final ByteData data = codec.encodeMethodCall(MethodCall(
1084+
'onProgress',
1085+
<dynamic, dynamic>{'progress': progress},
1086+
));
1087+
1088+
ServicesBinding.instance!.defaultBinaryMessenger
1089+
.handlePlatformMessage(channel.name, data, (ByteData? data) {});
1090+
}
1091+
10241092
void _loadUrl(String? url) {
10251093
history = history.sublist(0, currentPosition + 1);
10261094
history.add(url);

0 commit comments

Comments
 (0)