Skip to content

Commit

Permalink
[webview_flutter] Support for loading progress tracking (flutter#2151)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremie-movify authored and adsonpleal committed Feb 26, 2021
1 parent 4fe6c45 commit 169d4ab
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 1 deletion.
4 changes: 4 additions & 0 deletions packages/webview_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.0.0-nullsafety.6

* Added support for progress tracking.

## 2.0.0-nullsafety.5

* Add section to the wiki explaining how to use Material components.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) {

return true;
}

@Override
public void onProgressChanged(WebView view, int progress) {
flutterWebViewClient.onLoadingProgress(progress);
}
}

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
Expand Down Expand Up @@ -367,6 +372,9 @@ private void applySettings(Map<String, Object> settings) {
webView.setWebContentsDebuggingEnabled(debuggingEnabled);
}
break;
case "hasProgressTracking":
flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key);
break;
case "gestureNavigationEnabled":
break;
case "userAgent":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class FlutterWebViewClient {
private static final String TAG = "FlutterWebViewClient";
private final MethodChannel methodChannel;
private boolean hasNavigationDelegate;
boolean hasProgressTracking;

FlutterWebViewClient(MethodChannel methodChannel) {
this.methodChannel = methodChannel;
Expand Down Expand Up @@ -125,6 +126,14 @@ private void onPageFinished(WebView view, String url) {
methodChannel.invokeMethod("onPageFinished", args);
}

void onLoadingProgress(int progress) {
if (hasProgressTracking) {
Map<String, Object> args = new HashMap<>();
args.put("progress", progress);
methodChannel.invokeMethod("onProgress", args);
}
}

private void onWebResourceError(
final int errorCode, final String description, final String failingUrl) {
final Map<String, Object> args = new HashMap<>();
Expand Down
3 changes: 3 additions & 0 deletions packages/webview_flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class _WebViewExampleState extends State<WebViewExample> {
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
onProgress: (int progress) {
print("WebView is loading (progress : $progress%)");
},
javascriptChannels: <JavascriptChannel>{
_toasterJavascriptChannel(context),
},
Expand Down
19 changes: 19 additions & 0 deletions packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Flutter/Flutter.h>
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface FLTWKProgressionDelegate : NSObject

- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel;

- (void)stopObservingProgress:(WKWebView *)webView;

@end

NS_ASSUME_NONNULL_END
42 changes: 42 additions & 0 deletions packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "FLTWKProgressionDelegate.h"

NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress";

@implementation FLTWKProgressionDelegate {
FlutterMethodChannel *_methodChannel;
}

- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel {
self = [super init];
if (self) {
_methodChannel = channel;
[webView addObserver:self
forKeyPath:FLTWKEstimatedProgressKeyPath
options:NSKeyValueObservingOptionNew
context:nil];
}
return self;
}

- (void)stopObservingProgress:(WKWebView *)webView {
[webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) {
NSNumber *newValue =
change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0
int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100
[_methodChannel invokeMethod:@"onProgress"
arguments:@{@"progress" : [NSNumber numberWithInt:newValueAsInt]}];
}
}

@end
15 changes: 15 additions & 0 deletions packages/webview_flutter/ios/Classes/FlutterWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#import "FlutterWebView.h"
#import "FLTWKNavigationDelegate.h"
#import "FLTWKProgressionDelegate.h"
#import "JavaScriptChannelHandler.h"

@implementation FLTWebViewFactory {
Expand Down Expand Up @@ -64,6 +65,7 @@ @implementation FLTWebViewController {
// The set of registered JavaScript channel names.
NSMutableSet* _javaScriptChannelNames;
FLTWKNavigationDelegate* _navigationDelegate;
FLTWKProgressionDelegate* _progressionDelegate;
}

- (instancetype)initWithFrame:(CGRect)frame
Expand Down Expand Up @@ -119,6 +121,12 @@ - (instancetype)initWithFrame:(CGRect)frame
return self;
}

- (void)dealloc {
if (_progressionDelegate != nil) {
[_progressionDelegate stopObservingProgress:_webView];
}
}

- (UIView*)view {
return _webView;
}
Expand Down Expand Up @@ -323,6 +331,13 @@ - (NSString*)applySettings:(NSDictionary<NSString*, id>*)settings {
} else if ([key isEqualToString:@"hasNavigationDelegate"]) {
NSNumber* hasDartNavigationDelegate = settings[key];
_navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue];
} else if ([key isEqualToString:@"hasProgressTracking"]) {
NSNumber* hasProgressTrackingValue = settings[key];
bool hasProgressTracking = [hasProgressTrackingValue boolValue];
if (hasProgressTracking) {
_progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView
channel:_channel];
}
} else if ([key isEqualToString:@"debuggingEnabled"]) {
// no-op debugging is always enabled on iOS.
} else if ([key isEqualToString:@"gestureNavigationEnabled"]) {
Expand Down
11 changes: 10 additions & 1 deletion packages/webview_flutter/lib/platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ abstract class WebViewPlatformCallbacksHandler {
/// Invoked by [WebViewPlatformController] when a page has finished loading.
void onPageFinished(String url);

/// Invoked by [WebViewPlatformController] when a page is loading.
/// /// Only works when [WebSettings.hasProgressTracking] is set to `true`.
void onProgress(int progress);

/// Report web resource loading error to the host application.
void onWebResourceError(WebResourceError error);
}
Expand Down Expand Up @@ -388,6 +392,7 @@ class WebSettings {
WebSettings({
this.javascriptMode,
this.hasNavigationDelegate,
this.hasProgressTracking,
this.debuggingEnabled,
this.gestureNavigationEnabled,
this.allowsInlineMediaPlayback,
Expand All @@ -400,6 +405,10 @@ class WebSettings {
/// Whether the [WebView] has a [NavigationDelegate] set.
final bool? hasNavigationDelegate;

/// Whether the [WebView] should track page loading progress.
/// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress.
final bool? hasProgressTracking;

/// Whether to enable the platform's webview content debugging tools.
///
/// See also: [WebView.debuggingEnabled].
Expand Down Expand Up @@ -427,7 +436,7 @@ class WebSettings {

@override
String toString() {
return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)';
return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)';
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/webview_flutter/lib/src/webview_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController {
case 'onPageFinished':
_platformCallbacksHandler.onPageFinished(call.arguments['url']!);
return null;
case 'onProgress':
_platformCallbacksHandler.onProgress(call.arguments['progress']);
return null;
case 'onPageStarted':
_platformCallbacksHandler.onPageStarted(call.arguments['url']!);
return null;
Expand Down Expand Up @@ -183,6 +186,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController {

_addIfNonNull('jsMode', settings!.javascriptMode?.index);
_addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate);
_addIfNonNull('hasProgressTracking', settings.hasProgressTracking);
_addIfNonNull('debuggingEnabled', settings.debuggingEnabled);
_addIfNonNull(
'gestureNavigationEnabled', settings.gestureNavigationEnabled);
Expand Down
20 changes: 20 additions & 0 deletions packages/webview_flutter/lib/webview_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ typedef void PageStartedCallback(String url);
/// Signature for when a [WebView] has finished loading a page.
typedef void PageFinishedCallback(String url);

/// Signature for when a [WebView] is loading a page.
typedef void PageLoadingCallback(int progress);

/// Signature for when a [WebView] has failed to load a resource.
typedef void WebResourceErrorCallback(WebResourceError error);

Expand Down Expand Up @@ -217,6 +220,7 @@ class WebView extends StatefulWidget {
this.gestureRecognizers,
this.onPageStarted,
this.onPageFinished,
this.onProgress,
this.onWebResourceError,
this.debuggingEnabled = false,
this.gestureNavigationEnabled = false,
Expand Down Expand Up @@ -357,6 +361,9 @@ class WebView extends StatefulWidget {
/// [WebViewController.evaluateJavascript] can assume this.
final PageFinishedCallback? onPageFinished;

/// Invoked when a page is loading.
final PageLoadingCallback? onProgress;

/// Invoked when a web resource has failed to load.
///
/// This can be called for any resource (iframe, image, etc.), not just for
Expand Down Expand Up @@ -476,6 +483,7 @@ WebSettings _webSettingsFromWidget(WebView widget) {
return WebSettings(
javascriptMode: widget.javascriptMode,
hasNavigationDelegate: widget.navigationDelegate != null,
hasProgressTracking: widget.onProgress != null,
debuggingEnabled: widget.debuggingEnabled,
gestureNavigationEnabled: widget.gestureNavigationEnabled,
allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback,
Expand All @@ -488,6 +496,7 @@ WebSettings _clearUnchangedWebSettings(
WebSettings currentValue, WebSettings newValue) {
assert(currentValue.javascriptMode != null);
assert(currentValue.hasNavigationDelegate != null);
assert(currentValue.hasProgressTracking != null);
assert(currentValue.debuggingEnabled != null);
assert(currentValue.userAgent != null);
assert(newValue.javascriptMode != null);
Expand All @@ -497,6 +506,7 @@ WebSettings _clearUnchangedWebSettings(

JavascriptMode? javascriptMode;
bool? hasNavigationDelegate;
bool? hasProgressTracking;
bool? debuggingEnabled;
WebSetting<String?> userAgent = WebSetting.absent();
if (currentValue.javascriptMode != newValue.javascriptMode) {
Expand All @@ -505,6 +515,9 @@ WebSettings _clearUnchangedWebSettings(
if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) {
hasNavigationDelegate = newValue.hasNavigationDelegate;
}
if (currentValue.hasProgressTracking != newValue.hasProgressTracking) {
hasProgressTracking = newValue.hasProgressTracking;
}
if (currentValue.debuggingEnabled != newValue.debuggingEnabled) {
debuggingEnabled = newValue.debuggingEnabled;
}
Expand All @@ -515,6 +528,7 @@ WebSettings _clearUnchangedWebSettings(
return WebSettings(
javascriptMode: javascriptMode,
hasNavigationDelegate: hasNavigationDelegate,
hasProgressTracking: hasProgressTracking,
debuggingEnabled: debuggingEnabled,
userAgent: userAgent,
);
Expand Down Expand Up @@ -571,6 +585,12 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler {
}

@override
void onProgress(int progress) {
if (_widget.onProgress != null) {
_widget.onProgress!(progress);
}
}

void onWebResourceError(WebResourceError error) {
if (_widget.onWebResourceError != null) {
_widget.onWebResourceError!(error);
Expand Down
68 changes: 68 additions & 0 deletions packages/webview_flutter/test/webview_flutter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,62 @@ void main() {
});
});

group('$PageLoadingCallback', () {
testWidgets('onLoadingProgress is not null', (WidgetTester tester) async {
int? loadingProgress;

await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onProgress: (int progress) {
loadingProgress = progress;
},
));

final FakePlatformWebView? platformWebView =
fakePlatformViewsController.lastCreatedView;

platformWebView?.fakeOnProgressCallback(50);

expect(loadingProgress, 50);
});

testWidgets('onLoadingProgress is null', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
onProgress: null,
));

final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView!;

// This is to test that it does not crash on a null callback.
platformWebView.fakeOnProgressCallback(50);
});

testWidgets('onLoadingProgress changed', (WidgetTester tester) async {
int? loadingProgress;

await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onProgress: (int progress) {},
));

await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onProgress: (int progress) {
loadingProgress = progress;
},
));

final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView!;

platformWebView.fakeOnProgressCallback(50);

expect(loadingProgress, 50);
});
});

group('navigationDelegate', () {
testWidgets('hasNavigationDelegate', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
Expand Down Expand Up @@ -1021,6 +1077,18 @@ class FakePlatformWebView {
);
}

void fakeOnProgressCallback(int progress) {
final StandardMethodCodec codec = const StandardMethodCodec();

final ByteData data = codec.encodeMethodCall(MethodCall(
'onProgress',
<dynamic, dynamic>{'progress': progress},
));

ServicesBinding.instance!.defaultBinaryMessenger
.handlePlatformMessage(channel.name, data, (ByteData? data) {});
}

void _loadUrl(String? url) {
history = history.sublist(0, currentPosition + 1);
history.add(url);
Expand Down

0 comments on commit 169d4ab

Please sign in to comment.