Skip to content

Commit 5e09c54

Browse files
[webview_flutter_android] [webview_flutter_wkwebview] Adds support for PlatformNavigationDelegate.onUrlChange (#3653)
[webview_flutter_android] [webview_flutter_wkwebview] Adds support for `PlatformNavigationDelegate.onUrlChange`
1 parent 087d454 commit 5e09c54

25 files changed

+1371
-462
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.3.0
2+
3+
* Adds support for `PlatformNavigationDelegate.onUrlChange`.
4+
15
## 3.2.4
26

37
* Updates pigeon to fix warnings with clang 15.

example/integration_test/webview_flutter_test.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,81 @@ Future<void> main() async {
10021002
final String? currentUrl = await controller.currentUrl();
10031003
expect(currentUrl, secondaryUrl);
10041004
});
1005+
1006+
testWidgets('can receive url changes', (WidgetTester tester) async {
1007+
final Completer<void> pageLoaded = Completer<void>();
1008+
1009+
final PlatformNavigationDelegate navigationDelegate =
1010+
PlatformNavigationDelegate(
1011+
const PlatformNavigationDelegateCreationParams(),
1012+
)..setOnPageFinished((_) => pageLoaded.complete());
1013+
1014+
final PlatformWebViewController controller = PlatformWebViewController(
1015+
const PlatformWebViewControllerCreationParams(),
1016+
)
1017+
..setJavaScriptMode(JavaScriptMode.unrestricted)
1018+
..setPlatformNavigationDelegate(navigationDelegate)
1019+
..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded)));
1020+
1021+
await tester.pumpWidget(Builder(
1022+
builder: (BuildContext context) {
1023+
return PlatformWebViewWidget(
1024+
PlatformWebViewWidgetCreationParams(controller: controller),
1025+
).build(context);
1026+
},
1027+
));
1028+
1029+
await pageLoaded.future;
1030+
await navigationDelegate.setOnPageFinished((_) {});
1031+
1032+
final Completer<String> urlChangeCompleter = Completer<String>();
1033+
await navigationDelegate.setOnUrlChange((UrlChange change) {
1034+
urlChangeCompleter.complete(change.url);
1035+
});
1036+
1037+
await controller.runJavaScript('location.href = "$primaryUrl"');
1038+
1039+
await expectLater(urlChangeCompleter.future, completion(primaryUrl));
1040+
});
1041+
1042+
testWidgets('can receive updates to history state',
1043+
(WidgetTester tester) async {
1044+
final Completer<void> pageLoaded = Completer<void>();
1045+
1046+
final PlatformNavigationDelegate navigationDelegate =
1047+
PlatformNavigationDelegate(
1048+
const PlatformNavigationDelegateCreationParams(),
1049+
)..setOnPageFinished((_) => pageLoaded.complete());
1050+
1051+
final PlatformWebViewController controller = PlatformWebViewController(
1052+
const PlatformWebViewControllerCreationParams(),
1053+
)
1054+
..setJavaScriptMode(JavaScriptMode.unrestricted)
1055+
..setPlatformNavigationDelegate(navigationDelegate)
1056+
..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl)));
1057+
1058+
await tester.pumpWidget(Builder(
1059+
builder: (BuildContext context) {
1060+
return PlatformWebViewWidget(
1061+
PlatformWebViewWidgetCreationParams(controller: controller),
1062+
).build(context);
1063+
},
1064+
));
1065+
1066+
await pageLoaded.future;
1067+
await navigationDelegate.setOnPageFinished((_) {});
1068+
1069+
final Completer<String> urlChangeCompleter = Completer<String>();
1070+
await navigationDelegate.setOnUrlChange((UrlChange change) {
1071+
urlChangeCompleter.complete(change.url);
1072+
});
1073+
1074+
await controller.runJavaScript(
1075+
'window.history.pushState({}, "", "secondary.txt");',
1076+
);
1077+
1078+
await expectLater(urlChangeCompleter.future, completion(secondaryUrl));
1079+
});
10051080
});
10061081

10071082
testWidgets('launches with gestureNavigationEnabled on iOS',

example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1111
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
1212
8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */; };
13+
8F4FF94B29AC223F000A6586 /* FWFURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF94A29AC223F000A6586 /* FWFURLTests.m */; };
1314
8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */; };
1415
8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */; };
1516
8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */; };
@@ -78,6 +79,7 @@
7879
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7980
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
8081
8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewFlutterWKWebViewExternalAPITests.m; sourceTree = "<group>"; };
82+
8F4FF94A29AC223F000A6586 /* FWFURLTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFURLTests.m; sourceTree = "<group>"; };
8183
8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFInstanceManagerTests.m; sourceTree = "<group>"; };
8284
8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewHostApiTests.m; sourceTree = "<group>"; };
8385
8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFDataConvertersTests.m; sourceTree = "<group>"; };
@@ -162,6 +164,7 @@
162164
8FB79B8E2820BAB300C101D3 /* FWFScrollViewHostApiTests.m */,
163165
8FB79B902820BAC700C101D3 /* FWFUIViewHostApiTests.m */,
164166
8FB79B962821985200C101D3 /* FWFObjectHostApiTests.m */,
167+
8F4FF94A29AC223F000A6586 /* FWFURLTests.m */,
165168
);
166169
path = RunnerTests;
167170
sourceTree = "<group>";
@@ -463,6 +466,7 @@
463466
buildActionMask = 2147483647;
464467
files = (
465468
8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */,
469+
8F4FF94B29AC223F000A6586 /* FWFURLTests.m in Sources */,
466470
8FB79B852820A3A400C101D3 /* FWFUIDelegateHostApiTests.m in Sources */,
467471
8FB79B972821985200C101D3 /* FWFObjectHostApiTests.m in Sources */,
468472
8FB79B672820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m in Sources */,

example/ios/RunnerTests/FWFObjectHostApiTests.m

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,43 @@ - (void)testObserveValueForKeyPath {
139139
return value[0].value == FWFNSKeyValueChangeKeyEnumOldValue;
140140
}]
141141
changeValues:[OCMArg checkWithBlock:^BOOL(id value) {
142-
return [@"key" isEqual:value[0]];
142+
FWFObjectOrIdentifier *object = (FWFObjectOrIdentifier *)value[0];
143+
return !object.isIdentifier.boolValue &&
144+
[@"key" isEqual:object.value];
145+
}]
146+
completion:OCMOCK_ANY]);
147+
}
148+
149+
- (void)testObserveValueForKeyPathWithIdentifier {
150+
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
151+
152+
FWFObject *mockObject = [self mockObjectWithManager:instanceManager identifier:0];
153+
FWFObjectFlutterApiImpl *mockFlutterAPI = [self mockFlutterApiWithManager:instanceManager];
154+
155+
OCMStub([mockObject objectApi]).andReturn(mockFlutterAPI);
156+
157+
NSObject *object = [[NSObject alloc] init];
158+
[instanceManager addDartCreatedInstance:object withIdentifier:1];
159+
160+
NSObject *returnedObject = [[NSObject alloc] init];
161+
[instanceManager addDartCreatedInstance:returnedObject withIdentifier:2];
162+
163+
[mockObject observeValueForKeyPath:@"keyPath"
164+
ofObject:object
165+
change:@{NSKeyValueChangeOldKey : returnedObject}
166+
context:nil];
167+
OCMVerify([mockFlutterAPI
168+
observeValueForObjectWithIdentifier:@0
169+
keyPath:@"keyPath"
170+
objectIdentifier:@1
171+
changeKeys:[OCMArg checkWithBlock:^BOOL(
172+
NSArray<FWFNSKeyValueChangeKeyEnumData *>
173+
*value) {
174+
return value[0].value == FWFNSKeyValueChangeKeyEnumOldValue;
175+
}]
176+
changeValues:[OCMArg checkWithBlock:^BOOL(id value) {
177+
FWFObjectOrIdentifier *object = (FWFObjectOrIdentifier *)value[0];
178+
return object.isIdentifier.boolValue && [@(2) isEqual:object.value];
143179
}]
144180
completion:OCMOCK_ANY]);
145181
}

example/ios/RunnerTests/FWFURLTests.m

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2013 The Flutter 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;
6+
@import XCTest;
7+
@import webview_flutter_wkwebview;
8+
9+
#import <OCMock/OCMock.h>
10+
11+
@interface FWFURLTests : XCTestCase
12+
@end
13+
14+
@implementation FWFURLTests
15+
- (void)testAbsoluteString {
16+
NSURL *mockUrl = OCMClassMock([NSURL class]);
17+
OCMStub([mockUrl absoluteString]).andReturn(@"https://www.google.com");
18+
19+
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
20+
[instanceManager addDartCreatedInstance:mockUrl withIdentifier:0];
21+
22+
FWFURLHostApiImpl *hostApi = [[FWFURLHostApiImpl alloc]
23+
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
24+
instanceManager:instanceManager];
25+
26+
FlutterError *error;
27+
XCTAssertEqualObjects([hostApi absoluteStringForNSURLWithIdentifier:@(0) error:&error],
28+
@"https://www.google.com");
29+
XCTAssertNil(error);
30+
}
31+
32+
- (void)testFlutterApiCreate {
33+
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
34+
FWFURLFlutterApiImpl *flutterApi = [[FWFURLFlutterApiImpl alloc]
35+
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
36+
instanceManager:instanceManager];
37+
38+
flutterApi.api = OCMClassMock([FWFNSUrlFlutterApi class]);
39+
40+
NSURL *url = [[NSURL alloc] initWithString:@"https://www.google.com"];
41+
[flutterApi create:url
42+
completion:^(FlutterError *error){
43+
}];
44+
45+
long identifier = [instanceManager identifierWithStrongReferenceForInstance:url];
46+
OCMVerify([flutterApi.api createWithIdentifier:@(identifier) completion:OCMOCK_ANY]);
47+
}
48+
@end

example/lib/main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ Page resource error:
124124
}
125125
debugPrint('allowing navigation to ${request.url}');
126126
return NavigationDecision.navigate;
127+
})
128+
..setOnUrlChange((UrlChange change) {
129+
debugPrint('url change to ${change.url}');
127130
}),
128131
)
129132
..addJavaScriptChannel(JavaScriptChannelParams(

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.0.0
13+
webview_flutter_platform_interface: ^2.1.0
1414
webview_flutter_wkwebview:
1515
# When depending on this package from a real application you should use:
1616
# webview_flutter: ^x.y.z

ios/Classes/FLTWebViewFlutterPlugin.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import "FWFScrollViewHostApi.h"
1414
#import "FWFUIDelegateHostApi.h"
1515
#import "FWFUIViewHostApi.h"
16+
#import "FWFURLHostApi.h"
1617
#import "FWFUserContentControllerHostApi.h"
1718
#import "FWFWebViewConfigurationHostApi.h"
1819
#import "FWFWebViewHostApi.h"
@@ -100,6 +101,9 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
100101
FWFWKWebViewHostApiSetup(registrar.messenger, [[FWFWebViewHostApiImpl alloc]
101102
initWithBinaryMessenger:registrar.messenger
102103
instanceManager:instanceManager]);
104+
FWFNSUrlHostApiSetup(registrar.messenger,
105+
[[FWFURLHostApiImpl alloc] initWithBinaryMessenger:registrar.messenger
106+
instanceManager:instanceManager]);
103107

104108
FWFWebViewFactory *webviewFactory = [[FWFWebViewFactory alloc] initWithManager:instanceManager];
105109
[registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"];

ios/Classes/FWFGeneratedWebKitApis.h

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) {
159159
@class FWFNSErrorData;
160160
@class FWFWKScriptMessageData;
161161
@class FWFNSHttpCookieData;
162+
@class FWFObjectOrIdentifier;
162163

163164
@interface FWFNSKeyValueObservingOptionsEnumData : NSObject
164165
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
@@ -300,6 +301,19 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) {
300301
@property(nonatomic, strong) NSArray<id> *propertyValues;
301302
@end
302303

304+
/// An object that can represent either a value supported by
305+
/// `StandardMessageCodec`, a data class in this pigeon file, or an identifier
306+
/// of an object stored in an `InstanceManager`.
307+
@interface FWFObjectOrIdentifier : NSObject
308+
/// `init` unavailable to enforce nonnull fields, see the `make` class method.
309+
- (instancetype)init NS_UNAVAILABLE;
310+
+ (instancetype)makeWithValue:(id)value isIdentifier:(NSNumber *)isIdentifier;
311+
@property(nonatomic, strong) id value;
312+
/// Whether value is an int that is used to retrieve an instance stored in an
313+
/// `InstanceManager`.
314+
@property(nonatomic, strong) NSNumber *isIdentifier;
315+
@end
316+
303317
/// The codec used by FWFWKWebsiteDataStoreHostApi.
304318
NSObject<FlutterMessageCodec> *FWFWKWebsiteDataStoreHostApiGetCodec(void);
305319

@@ -583,7 +597,7 @@ NSObject<FlutterMessageCodec> *FWFNSObjectFlutterApiGetCodec(void);
583597
keyPath:(NSString *)keyPath
584598
objectIdentifier:(NSNumber *)objectIdentifier
585599
changeKeys:(NSArray<FWFNSKeyValueChangeKeyEnumData *> *)changeKeys
586-
changeValues:(NSArray<id> *)changeValues
600+
changeValues:(NSArray<FWFObjectOrIdentifier *> *)changeValues
587601
completion:(void (^)(FlutterError *_Nullable))completion;
588602
- (void)disposeObjectWithIdentifier:(NSNumber *)identifier
589603
completion:(void (^)(FlutterError *_Nullable))completion;
@@ -702,4 +716,39 @@ NSObject<FlutterMessageCodec> *FWFWKHttpCookieStoreHostApiGetCodec(void);
702716
extern void FWFWKHttpCookieStoreHostApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
703717
NSObject<FWFWKHttpCookieStoreHostApi> *_Nullable api);
704718

719+
/// The codec used by FWFNSUrlHostApi.
720+
NSObject<FlutterMessageCodec> *FWFNSUrlHostApiGetCodec(void);
721+
722+
/// Host API for `NSUrl`.
723+
///
724+
/// This class may handle instantiating and adding native object instances that
725+
/// are attached to a Dart instance or method calls on the associated native
726+
/// class or an instance of the class.
727+
///
728+
/// See https://developer.apple.com/documentation/foundation/nsurl?language=objc.
729+
@protocol FWFNSUrlHostApi
730+
- (nullable NSString *)absoluteStringForNSURLWithIdentifier:(NSNumber *)identifier
731+
error:
732+
(FlutterError *_Nullable *_Nonnull)error;
733+
@end
734+
735+
extern void FWFNSUrlHostApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
736+
NSObject<FWFNSUrlHostApi> *_Nullable api);
737+
738+
/// The codec used by FWFNSUrlFlutterApi.
739+
NSObject<FlutterMessageCodec> *FWFNSUrlFlutterApiGetCodec(void);
740+
741+
/// Flutter API for `NSUrl`.
742+
///
743+
/// This class may handle instantiating and adding Dart instances that are
744+
/// attached to a native instance or receiving callback methods from an
745+
/// overridden native class.
746+
///
747+
/// See https://developer.apple.com/documentation/foundation/nsurl?language=objc.
748+
@interface FWFNSUrlFlutterApi : NSObject
749+
- (instancetype)initWithBinaryMessenger:(id<FlutterBinaryMessenger>)binaryMessenger;
750+
- (void)createWithIdentifier:(NSNumber *)identifier
751+
completion:(void (^)(FlutterError *_Nullable))completion;
752+
@end
753+
705754
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)