**Last updated**: 17 March 2025 # Flutter integration with Native SDKs ## Overview The integration leverages [Flutter platform channels](https://docs.flutter.dev/platform-integration/platform-channels) to configure and render our Native SDKs for [iOS](/products/checkout/ios) and [Android](/products/checkout/android). You create a form containing the Access Checkout UI components in the iOS and Android projects. This form is then rendered on the Flutter application using a `PlatformViewLink` for Android or a `UiKitView` for iOS. The communication between Native and Flutter is done via method channels. ## Get started To integrate our Checkout Native iOS and Android SDKs into Flutter you **must**: - add our Checkout SDKs to iOS and Android - implement platform-specific native views - create a method channel for communication between Native and Flutter - expose the Native views to Flutter - optionally implement validation and event listeners Full sample integration You can see an example demo of a Flutter integration with Native iOS and Android SDKs [in our `access-checkout-flutter-demo` GitHub repository](https://github.com/Worldpay/access-checkout-flutter-demo/tree/main/access_checkout_flutter_native_sdk_demo) ## On the Native side ### Add our iOS and Android Checkout SDK Firstly, you must add the iOS and Android dependencies manually to the Android and iOS projects within your Flutter application. #### Android: add SDK dependency You must add the Android Checkout SDK to your `android/app/build.gradle`. To ensure compatibility, the SDK also requires a minimum SDK version of `26`. build.gradle.kts (Kotlin) android/app/build.gradle.kts android { defaultConfig { minSdk = 26 } } dependencies { implementation("com.worldpay.access:access-checkout-android:4.0.0") } build.gradle (Gradle) android/app/build.gradle android { defaultConfig { minSdk 26 } } dependencies { implementation 'com.worldpay.access:access-checkout-android:4.0.0' } #### iOS: add SDK dependency You must add the iOS Checkout SDK to your `ios/Podfile`. Podfile ios/Podfile target 'Runner' do pod 'AccessCheckoutSDK' end ### Implement native views The Access Checkout SDK provides the UI elements, but to integrate them into Flutter you must make use of `PlatformViews`. #### Android: create custom view - `MainActivity.kt` - the entry point responsible for registering the ViewFactory and binding the ViewFactory to a platform channel. - `AccessCheckoutView.kt` - renders the Access Checkout UI, handles session generation and validation and communicates to Flutter via the Method Channel. - `AccessCheckoutViewFactory.kt` - creates and configures instances of `AccessCheckoutView`. - `access_checkout_layout.xml` - contains the UI layout of the Access Checkout fields (Pan, Expiry and CVC). MainActivity.kt android/app/src/main/kotlin/com/example/flutter/MainActivity.kt package com.example.access_checkout_flutter_native_sdk_demo import com.example.access_checkout_flutter_native_sdk_demo.AccessCheckoutViewFactory import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine class MainActivity : FlutterActivity() { private val METHOD_CHANNEL_NAME = "com.worldpay.flutter/accesscheckout" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val registry = flutterEngine .platformViewsController .registry registry .registerViewFactory( "com.worldpay.flutter/accesscheckout", AccessCheckoutViewFactory( flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL_NAME, this ) ) } } AccessCheckoutView.kt android/app/src/main/kotlin/com/example/flutter/AccessCheckoutView.kt package com.example.access_checkout_flutter_native_sdk_demo import android.content.Context import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.view.LayoutInflater import android.view.View import androidx.lifecycle.LifecycleOwner import com.worldpay.access.checkout.client.api.exception.AccessCheckoutException import com.worldpay.access.checkout.client.session.AccessCheckoutClient import com.worldpay.access.checkout.client.session.AccessCheckoutClientBuilder import com.worldpay.access.checkout.client.session.listener.SessionResponseListener import com.worldpay.access.checkout.client.session.model.CardDetails import com.worldpay.access.checkout.client.session.model.SessionType import com.worldpay.access.checkout.client.validation.AccessCheckoutValidationInitialiser import com.worldpay.access.checkout.client.validation.config.CardValidationConfig import com.worldpay.access.checkout.client.validation.listener.AccessCheckoutCardValidationListener import com.worldpay.access.checkout.client.validation.model.CardBrand import com.worldpay.access.checkout.ui.AccessCheckoutEditText import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.platform.PlatformView class AccessCheckoutView( private val lifecycleOwner: LifecycleOwner, private val methodChannel: MethodChannel, context: Context, creationParams: Map, ) : PlatformView { private val layout: View private val panInput: AccessCheckoutEditText private val expiryInput: AccessCheckoutEditText private val cvcInput: AccessCheckoutEditText private var baseUrl: String = creationParams["baseUrl"] as String private var checkoutId: String = creationParams["checkoutId"] as String private var useCardValidation: Boolean = creationParams["useCardValidation"] as? Boolean ?: false private var accessCheckoutClient: AccessCheckoutClient init { layout = LayoutInflater.from(context) .inflate(R.layout.access_checkout_layout, null) panInput = layout.findViewById(R.id.pan_input) expiryInput = layout.findViewById(R.id.expiry_date_input) cvcInput = layout.findViewById(R.id.cvc_input) if (useCardValidation) { initializeCardValidation() } accessCheckoutClient = AccessCheckoutClientBuilder() .baseUrl(baseUrl) .checkoutId(checkoutId) .lifecycleOwner(lifecycleOwner) .sessionResponseListener( object : SessionResponseListener { override fun onError(error: AccessCheckoutException) { methodChannel.invokeMethod("onSessionError", "Could not create session") } override fun onSuccess(sessionResponseMap: Map) { // Important: Flutter will not understand SessionType // therefore it needs to be converted into a JSON-serializable format val sessionData = sessionResponseMap.mapKeys { it.key.name } methodChannel.invokeMethod("onSessionGenerated", sessionData) } } ) .context(context) .build() methodChannel.setMethodCallHandler { call, result -> when (call.method) { "generateSession" -> generateSession() else -> result.notImplemented() } } } // private fun styleComponent() {} private fun initializeCardValidation() { val cardValidationConfig = CardValidationConfig.Builder() .baseUrl(baseUrl) .pan(panInput) .expiryDate(expiryInput) .cvc(cvcInput) .lifecycleOwner(lifecycleOwner) .enablePanFormatting() .validationListener(object : AccessCheckoutCardValidationListener { override fun onBrandChange(cardBrand: CardBrand?) { // TODO: Update the brand image using your SVG loader } override fun onCvcValidated(isValid: Boolean) { updateUIField(cvcInput, isValid) if (!isValid) { methodChannel.invokeMethod("onValidationUpdated", false) } } override fun onExpiryDateValidated(isValid: Boolean) { updateUIField(expiryInput, isValid) if (!isValid) { methodChannel.invokeMethod("onValidationUpdated", false) } } override fun onPanValidated(isValid: Boolean) { updateUIField(panInput, isValid) if (!isValid) { methodChannel.invokeMethod("onValidationUpdated", false) } } override fun onValidationSuccess() { methodChannel.invokeMethod("onValidationUpdated", true) } }) .build() AccessCheckoutValidationInitialiser.initialise(cardValidationConfig) } private fun updateUIField(field: AccessCheckoutEditText, isValid: Boolean) { val colour = if (isValid) Color.GREEN else Color.RED //Update Text color field.setTextColor(colour) //Update Border val border = GradientDrawable() border.setStroke(3, colour) field.background = border } private fun generateSession() { val cardDetails = CardDetails.Builder() .pan(panInput) .expiryDate(expiryInput) .cvc(cvcInput) .build() accessCheckoutClient.generateSessions(cardDetails, listOf(SessionType.CARD)) } override fun getView(): View = layout override fun dispose() {} } AccessCheckoutViewFactory.kt android/app/src/main/kotlin/com/example/flutter/AccessCheckoutViewFactory.kt package com.example.access_checkout_flutter_native_sdk_demo import android.content.Context import androidx.lifecycle.LifecycleOwner import com.example.access_checkout_flutter_native_sdk_demo.AccessCheckoutView import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.platform.PlatformView import io.flutter.plugin.platform.PlatformViewFactory class AccessCheckoutViewFactory( private val messenger: BinaryMessenger, private val channel: String, private val lifecycleOwner: LifecycleOwner ) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context, viewId: Int, args: Any?): PlatformView { val methodChannel = MethodChannel(messenger, channel) val creationParams: Map = args as Map return AccessCheckoutView(lifecycleOwner, methodChannel, context, creationParams) } } access_checkout_layout.xml android/app/src/main/res/layout/access_checkout_layout.xml #### iOS: create custom view - `AppDelegate.swift` - the entry point responsible for registering the ViewFactory and binding the ViewFactory to a platform channel. - `AccessCheckoutView.swift` - renders the Access Checkout UI, handles session generation and validation and communicates to Flutter via the Method Channel. - `AccessCheckoutViewFactory.swift` - creates and configures instances of `AccessCheckoutView`. - `AccessCheckoutViewController.swift` - manages the layout in storyboard and connects the UI elements. - `AccessCheckoutView.storyboard` - contains the UI layout of the Access Checkout fields (Pan, Expiry and CVC). AppDelegate.swift ios/Runner/AppDelegate.swift import Flutter import UIKit @main @objc class AppDelegate: FlutterAppDelegate { private var METHOD_CHANNEL_NAME = "com.worldpay.flutter/accesscheckout" override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) weak var pluginRegistrar = registrar(forPlugin: "com.worldpay.flutter/accesscheckout") let factory = AccessCheckoutViewFactory( messenger: pluginRegistrar!.messenger(), channel: METHOD_CHANNEL_NAME) pluginRegistrar!.register( factory, withId: "com.worldpay.flutter/accesscheckout" ) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } AccessCheckoutView.swift ios/Runner/AccessCheckoutView.swift import AccessCheckoutSDK import Flutter import UIKit class AccessCheckoutView: NSObject, FlutterPlatformView { @IBOutlet private var panInput: AccessCheckoutUITextField! @IBOutlet private var expiryInput: AccessCheckoutUITextField! @IBOutlet private var cvcInput: AccessCheckoutUITextField! private var accessCheckoutClient: AccessCheckoutClient private var methodChannel: FlutterMethodChannel private var _view: UIView private var baseUrl: String private var checkoutId: String private var useCardValidation: Bool private var controller: AccessCheckoutViewController init( methodChannel channel: FlutterMethodChannel, frame: CGRect, viewIdentifier viewId: Int64, binaryMessenger messenger: FlutterBinaryMessenger?, creationParams: [String: Any] ) { baseUrl = creationParams["baseUrl"] as? String ?? "" checkoutId = creationParams["checkoutId"] as? String ?? "" useCardValidation = creationParams["useCardValidation"] as? Bool ?? false methodChannel = channel accessCheckoutClient = try! AccessCheckoutClientBuilder() .accessBaseUrl(baseUrl) .checkoutId(checkoutId) .build() let storyboard = UIStoryboard(name: "AccessCheckoutView", bundle: Bundle.main) controller = storyboard.instantiateViewController(withIdentifier:"ViewController") as! AccessCheckoutViewController _view = controller.view! super.init() methodChannel.setMethodCallHandler({ [weak self] (call, result) in guard let self = self else {return} switch call.method{ case "generateSession": generateSession() default: result(FlutterMethodNotImplemented) } }) // iOS views can be created here referenceNativeView() if(useCardValidation){ initializeCardValidation() } } func initializeCardValidation() { let validationConfig = try! CardValidationConfig.builder() .pan(panInput) .expiryDate(expiryInput) .cvc(cvcInput) .accessBaseUrl(baseUrl) .validationDelegate(self) .enablePanFormatting() .build() AccessCheckoutValidationInitialiser().initialise(validationConfig) } func updateUIField(field: AccessCheckoutUITextField, isValid: Bool) { var colour = isValid ? UIColor.green : UIColor.red //Update Text & border color field.textColor = colour field.borderColor = colour } func generateSession() { let cardDetails = try! CardDetailsBuilder().pan(panInput) .expiryDate(expiryInput) .cvc(cvcInput) .build() do { try accessCheckoutClient.generateSessions( cardDetails: cardDetails, sessionTypes: [SessionType.card]) { result in DispatchQueue.main.async { switch result { case .failure(let error): print(error) self.methodChannel.invokeMethod("onSessionError", arguments: "Could not create session") case .success(let sessions): var sessionData: [String: String] = [:] for (key, value) in sessions { let keyName = String(describing: key).uppercased() sessionData[keyName] = value } self.methodChannel.invokeMethod("onSessionGenerated", arguments: sessionData) } } } } catch { self.methodChannel.invokeMethod("onSessionError", arguments: "Could not create session") } } func referenceNativeView() { panInput = controller.panInput expiryInput = controller.expiryInput cvcInput = controller.cvcInput } func view() -> UIView { return _view } } extension AccessCheckoutView: AccessCheckoutCardValidationDelegate { func cardBrandChanged(cardBrand: AccessCheckoutSDK.CardBrand?) { //TODO } func panValidChanged(isValid: Bool) { self.updateUIField(field: self.panInput, isValid: isValid) if(!isValid){ self.methodChannel.invokeMethod("onValidationUpdated", arguments:false) } } func expiryDateValidChanged(isValid: Bool) { self.updateUIField(field: self.expiryInput, isValid: isValid) if(!isValid){ self.methodChannel.invokeMethod("onValidationUpdated", arguments:false) } } func cvcValidChanged(isValid: Bool) { self.updateUIField(field: self.cvcInput, isValid: isValid) if(!isValid){ self.methodChannel.invokeMethod("onValidationUpdated", arguments:false) } } func validationSuccess() { self.methodChannel.invokeMethod("onValidationUpdated", arguments:true) } } AccessCheckoutViewFactory.swift ios/Runner/AccessCheckoutViewFactory.swift import Flutter import UIKit class AccessCheckoutViewFactory: NSObject, FlutterPlatformViewFactory { private var messenger: FlutterBinaryMessenger private var channel: String init(messenger: FlutterBinaryMessenger, channel: String) { self.messenger = messenger self.channel = channel super.init() } func create( withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any? ) -> FlutterPlatformView { let creationParams = args as? [String: Any] ?? [:] return AccessCheckoutView( methodChannel: FlutterMethodChannel(name: channel, binaryMessenger: messenger), frame: frame, viewIdentifier: viewId, binaryMessenger: messenger, creationParams: creationParams ) } /// Implementing this method is only necessary when the `arguments` in `createWithFrame` is not `nil`. public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { return FlutterStandardMessageCodec.sharedInstance() } } AccessCheckoutViewController.swift ios/Runner/AccessCheckoutViewController.swift import AccessCheckoutSDK import Flutter import UIKit class AccessCheckoutViewController: UIViewController { @IBOutlet weak var panInput: AccessCheckoutUITextField! @IBOutlet weak var cvcInput: AccessCheckoutUITextField! @IBOutlet weak var expiryInput: AccessCheckoutUITextField! } AccessCheckoutView.storyboard ios/Runner/AccessCheckoutView.storyboard ## On the Flutter side ### Set up method channels for communication To communicate with the native side you must create a `service` that handles the communication between Flutter and Native. In this class, you connect to our previously defined method channel `com.worldpay.flutter/accesscheckout` on the Native iOS and Android platforms. You must tell Flutter how to handle the callbacks issues when invoking the `generateSession` method on the native side. Optionally you can also define the logic for handling validation updates, in case you would like to handle states. Prerequisite The name of the method channel and methods must be the same between the Flutter and Native sides. We are using `com.worldpay.flutter/accesscheckout` as the method channel and `generateSession` as the method name, as an example. access_checkout_flutter.dart lib/service/access_checkout_flutter.dart import 'package:flutter/services.dart'; class AccessCheckoutFlutter { static const channel = MethodChannel('com.worldpay.flutter/accesscheckout'); static Future listenForValidationUpdates(Function(bool) onValidationUpdated) async { channel.setMethodCallHandler((call) async { if (call.method case "onValidationUpdated") { onValidationUpdated(call.arguments as bool); } }); } static Future generateSession( {required Function(Map) onSuccess, required Function(String) onError}) async { channel.setMethodCallHandler((call) async { switch (call.method) { case "onSessionGenerated": onSuccess(call.arguments); case "onSessionError": onError(call.arguments); } }); await channel.invokeMethod('generateSession'); } } ### Embed native views in Flutter To render our native views, you must handle the target platform and display a `PlatformViewLink` for Android or a `UiKitView` for iOS. We recommend to create a widget to handle this logic `lib/widgets/access_checkout_native_widget.dart`, so that this widget is the only one responsible for rendering the native views. access_checkout_native_widget.dart lib/widgets/access_checkout_native_widget.dart import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; class AccessCheckoutNativeWidget extends StatelessWidget { final String checkoutId; final String baseUrl; final bool useCardValidation; const AccessCheckoutNativeWidget({ super.key, required this.checkoutId, required this.baseUrl, required this.useCardValidation, }); static const StandardMessageCodec _decoder = StandardMessageCodec(); @override Widget build(BuildContext context) { const String viewType = "com.worldpay.flutter/accesscheckout"; final Map creationParams = { "baseUrl": baseUrl, "checkoutId": checkoutId, "useCardValidation": useCardValidation }; switch (defaultTargetPlatform) { case TargetPlatform.android: return PlatformViewLink( viewType: viewType, surfaceFactory: (context, controller) { return AndroidViewSurface( controller: controller as AndroidViewController, gestureRecognizers: const >{}, hitTestBehavior: PlatformViewHitTestBehavior.opaque, ); }, onCreatePlatformView: (params) { return PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: _decoder, onFocus: () { params.onFocusChanged(true); }, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..create(); }, ); case TargetPlatform.iOS: return UiKitView( viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); default: throw UnsupportedError("Unsupported platform view"); } } } ### Render the Flutter component You can now render our newly created widget that integrates our Native Checkout SDKs into Flutter. To achieve this, create another widget where you can orchestrate, render and handle some state for when a session is created. This widget will also be responsible for initializing and configuring the SDKs, using parameters such as `checkoutId`, `baseUrl`, `useCardValidation` Finally, you must display this `AccessCheckoutWidget` in our Flutter page and configure the parameters it requires. native_sdk_page.dart lib/screens/native_sdk_page.dart import 'package:access_checkout_flutter_native_sdk_demo/widgets/access_checkout.dart'; import 'package:flutter/material.dart'; class NativeSdkPage extends StatelessWidget { const NativeSdkPage({super.key}); @override Widget build(BuildContext context) { return const Center( child: AccessCheckoutWidget( // TODO: Replace the checkout id and base url with the values provided to you checkoutId: "00000000-0000-0000-0000-000000000000", baseUrl: "https://try.access.worldpay-bsh.securedataplatform.com", useCardValidation: true)); } } access_checkout.dart lib/widgets/access_checkout.dart import 'package:access_checkout_flutter_native_sdk_demo/service/access_checkout_flutter.dart'; import 'package:access_checkout_flutter_native_sdk_demo/widgets/access_checkout_native_widget.dart'; import 'package:flutter/material.dart'; class AccessCheckoutWidget extends StatefulWidget { final String checkoutId; final String baseUrl; final bool useCardValidation; const AccessCheckoutWidget({ super.key, required this.checkoutId, required this.baseUrl, required this.useCardValidation, }); @override AccessCheckoutWidgetState createState() => AccessCheckoutWidgetState(); } class AccessCheckoutWidgetState extends State { bool isSubmitButtonEnabled = false; String sessionToken = ""; late String checkoutId; late String baseUrl; late bool useCardValidation; @override void initState() { super.initState(); checkoutId = widget.checkoutId; baseUrl = widget.baseUrl; useCardValidation = widget.useCardValidation; if (useCardValidation) { AccessCheckoutFlutter.listenForValidationUpdates((isInputValid) { setState(() { isSubmitButtonEnabled = isInputValid; }); }); } else { setState(() { isSubmitButtonEnabled = true; }); } } @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( height: 150, child: AccessCheckoutNativeWidget( checkoutId: checkoutId, baseUrl: baseUrl, useCardValidation: useCardValidation)), Row(children: [ Expanded( flex: 1, child: OutlinedButton( onPressed: isSubmitButtonEnabled ? () => generateSession() : null, child: const Text('Submit'), ), ) ]), if (sessionToken != "") Text(sessionToken), ], ))); } Future generateSession() async { await AccessCheckoutFlutter.generateSession(onSuccess: (sessions) { setState(() { sessionToken = sessions["CARD"]!; }); showSnackBar(sessions["CARD"]!); }, onError: (error) { showSnackBar(error); }); } void showSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message), duration: const Duration(seconds: 5))); } }