Extension packages
Patrol can be extended with optional companion packages (for example, SDK-specific packages such as accessibility, payments, biometrics, etc.) without adding SDK logic to core patrol.
This page explains the recommended pattern for building such a package.
Architecture in one minute
- Core Patrol starts the native automation server on
:8081. - Core Patrol discovers extension implementations at startup:
- Android via
ServiceLoader(SPI file inMETA-INF/services/...) - iOS via an explicit registry populated at module load time (
+loadcallsPatrolRegisterServerExtensionClass)
- Android via
- Extension package registers extra POST endpoints (for example
/mySdkScan) on the same server. - Dart API in the extension package calls those endpoints over HTTP (same host/port as
$.platform.mobile.*).
The only core dependency from extension Dart code is patrolNativeServerUri.
What belongs where
| Area | Core patrol | Extension package |
|---|---|---|
| Route discovery and mounting | ✅ | ❌ |
| SDK-specific routes and handlers | ❌ | ✅ |
| Third-party SDK dependencies | ❌ | ✅ |
Package-owned schema (schema/*.dart) | ❌ | ✅ |
$.mySdk Dart extension API | ❌ | ✅ |
Step-by-step: create a new package
1) Create Flutter plugin package
Create a package with Flutter plugin structure (for example patrol_my_sdk).
In pubspec.yaml, keep it as a test-time dependency in consumer apps:
dev_dependencies:
patrol: ^<version>
patrol_my_sdk: ^<version>2) Define package schema
Create a package-local schema file, for example schema/my_sdk.dart.
class MySdkScanRequest {
late bool upload;
}
class MySdkScanResponse {
late int violations;
}
abstract class MySdkAutomator<IOSServer, AndroidServer, DartClient> {
MySdkScanResponse mySdkScan(MySdkScanRequest request);
}Use unique endpoint prefixes (mySdk*) to avoid route collisions.
3) Generate contracts/clients (recommended)
Run patrol_gen for the package schema to produce:
- Dart client (
DartClientenabled) - Android server contracts/routes (
AndroidServerenabled) - iOS server contracts/routes (
IOSServerenabled)
You can hand-write the HTTP client in a POC, but production packages should prefer codegen so endpoint names and payloads stay synchronized.
4) Build Dart API ($.mySdk)
Expose an extension on PatrolIntegrationTester:
extension PatrolMySdkExtension on PatrolIntegrationTester {
MySdkAutomator get mySdk => MySdkAutomator(this);
}Inside your package automator, initialize client with:
patrolNativeServerUriThis ensures the same host/port resolution as core Patrol (PATROL_HOST / PATROL_TEST_SERVER_PORT).
5) Implement Android native extension
Implement Patrol's extension interface and return routes:
class MySdkServerExtension : PatrolServerExtension {
override val name: String = "patrol_my_sdk"
override fun routes(): RoutingHttpHandler = routes(
"mySdkScan" bind POST to { req ->
// parse JSON, call SDK, return response
Response(OK).body(...)
},
)
}Register with SPI file:
com.example.patrol_my_sdk.MySdkServerExtension6) Implement iOS native extension
Create class conforming to PatrolServerExtension and register endpoints via PatrolRouteRegistrar.
@objc(MySdkServerExtension)
public class MySdkServerExtension: NSObject, PatrolServerExtension {
public override required init() { super.init() }
public var name: String { "patrol_my_sdk" }
public func register(on registrar: PatrolRouteRegistrar) {
registrar.post("mySdkScan") { body in
// decode request, call SDK, encode response
return Data()
}
}
}Register the class at module load time so Patrol can discover it without scanning the full Objective-C runtime (which is unsafe in large UITest binaries):
#import <Foundation/Foundation.h>
#import <patrol/PatrolExtensionRegistry.h>
@interface MySdkExtensionLoader : NSObject
@end
@implementation MySdkExtensionLoader
+ (void)load {
Class extensionClass = NSClassFromString(@"MySdkServerExtension");
if (extensionClass == Nil) {
return;
}
PatrolRegisterServerExtensionClass(extensionClass);
}
@endDo not call registerExtensionClass: via performSelector: — Swift class methods are
not reliably invokable that way from ObjC.
Patrol invokes extension route handlers on the HTTP server's worker threads, not on the
main thread. If your handler calls XCTest or XCUI APIs (for example XCUIApplication()),
dispatch that work to the main thread first — core Patrol's IOSAutomator does the same
via DispatchQueue.main.sync. Calling XCTest from a background thread crashes with
Must be called on the main thread.
7) Consumer app wiring
Android
The extension must be available on the instrumentation (androidTest) classpath:
dependencies {
androidTestImplementation(project(":patrol_my_sdk"))
}In some repository layouts (for example local monorepo path deps), you may also need to map Gradle project path in android/settings.gradle(.kts).
iOS
Use the standard Patrol iOS setup:
RunnerUITeststarget configuredFlutterGeneratedPluginSwiftPackagelinked toRunnerUITests(SPM), orinherit! :completeforRunnerUITestsin Podfile (CocoaPods)
Then the extension plugin is pulled into the UITest target with the rest of Flutter plugins.
For a full reference implementation, see patrol_axe.