Custom Views for the Flutter Plugin
Register custom native views to use within Scenes.
Custom Views allow you to embed native iOS and Android views within Scenes, giving you full control over design and layout while leveraging Airship’s targeting and orchestration capabilities.
Requirements
To use Custom Views in Flutter, you must add native iOS and Android code to your Flutter project. Custom Views work by embedding Flutter widgets within native Airship custom view containers.
Implementation
Custom Views require native implementation on both iOS and Android platforms. For a complete working implementation, see this Flutter Custom Views example .
The basic pattern is:
- Set up Flutter routing to handle custom view routes (e.g.,
/custom/my-view) - Register custom views on iOS using
AirshipCustomViewManager - Register custom views on Android using
AirshipCustomViewManager
Flutter Routing
Configure your app’s routing to handle custom view paths:
MaterialApp(
onGenerateRoute: (settings) {
// Pattern match on full route paths
switch (settings.name) {
case '/custom/my-banner':
return MaterialPageRoute(
builder: (context) => Material(child: MyBannerWidget()),
);
case '/custom/my-product-card':
return MaterialPageRoute(
builder: (context) => Material(child: MyProductCard()),
);
}
// Handle other routes
return null;
},
home: MyHomePage(),
)iOS
Create AirshipPluginExtender.swift in your ios/Runner/ directory:
import Foundation
import AirshipFrameworkProxy
import ActivityKit
import Flutter
#if canImport(AirshipCore)
import AirshipCore
import AirshipAutomation
#else
import AirshipKit
#endif
@objc(AirshipPluginExtender)
public class AirshipPluginExtender: NSObject, AirshipPluginExtenderProtocol {
@MainActor
public static func onAirshipReady() {
AirshipCustomViewManager.shared.register(name: "example") { args in
FlutterCustomViewWrapper(viewName: "example", properties: args.properties)
}
}
}Create FlutterCustomView.swift in your ios/Runner/ directory:
import Flutter
import UIKit
import SwiftUI
import AirshipFrameworkProxy
#if canImport(AirshipCore)
import AirshipCore
import AirshipAutomation
#else
import AirshipKit
#endif
/// SwiftUI wrapper for Flutter custom view
@available(iOS 16.0, *)
public struct FlutterCustomView: View {
let viewName: String
let properties: AirshipJSON?
public var body: some View {
FlutterCustomViewRepresentable(viewName: viewName, properties: properties)
}
}
/// UIViewRepresentable bridge for SwiftUI
@available(iOS 16.0, *)
struct FlutterCustomViewRepresentable: UIViewRepresentable {
let viewName: String
let properties: AirshipJSON?
func makeUIView(context: Context) -> FlutterCustomViewContainer {
return FlutterCustomViewContainer(viewName: viewName, properties: properties)
}
func updateUIView(_ uiView: FlutterCustomViewContainer, context: Context) {
// No updates needed
}
}
/// Flutter custom view that embeds a Flutter widget
public class FlutterCustomViewContainer: UIView {
private let viewName: String
private let properties: AirshipJSON?
private var flutterEngine: FlutterEngine?
private var flutterViewController: FlutterViewController?
public init(viewName: String, properties: AirshipJSON?) {
self.viewName = viewName
self.properties = properties
super.init(frame: .zero)
setupView()
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
backgroundColor = .systemGray6
clipsToBounds = true
}
override public func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
if newWindow != nil {
embedFlutterView()
} else {
removeFlutterView()
}
}
override public func layoutSubviews() {
super.layoutSubviews()
flutterViewController?.view.frame = bounds
}
private func embedFlutterView() {
flutterEngine = FlutterEngine(name: "airship_custom_\(viewName)")
let result = flutterEngine?.run()
guard result == true else {
return
}
flutterViewController = FlutterViewController(
engine: flutterEngine!,
nibName: nil,
bundle: nil
)
guard let flutterViewController = flutterViewController else {
return
}
addSubview(flutterViewController.view)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
let route = "/custom/\(self.viewName)"
self.flutterViewController?.pushRoute(route)
}
}
private func removeFlutterView() {
flutterViewController?.view.removeFromSuperview()
flutterViewController = nil
flutterEngine?.destroyContext()
flutterEngine = nil
}
}For more details on iOS custom views, see the Apple Custom Views documentation.
Android
Create AirshipExtender.kt in your android/app/src/main/kotlin/ directory:
import android.content.Context
import android.view.View
import android.widget.FrameLayout
import io.flutter.embedding.android.FlutterView
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import com.urbanairship.android.layout.AirshipCustomViewManager
import com.urbanairship.android.layout.AirshipCustomViewHandler
import com.urbanairship.android.layout.AirshipCustomViewArguments
import android.util.Log
import io.flutter.embedding.android.FlutterTextureView
import androidx.annotation.Keep
import com.urbanairship.UAirship
import com.urbanairship.android.framework.proxy.AirshipPluginExtender
@Keep
class AirshipExtender : AirshipPluginExtender {
override fun onAirshipReady(context: Context, airship: UAirship) {
AirshipCustomViewManager.register("example", FlutterCustomViewHandler())
}
}
class FlutterCustomViewHandler : AirshipCustomViewHandler {
override fun onCreateView(context: Context, args: AirshipCustomViewArguments): View {
return FlutterCustomView(
context,
args.name,
args.properties
)
}
}
class FlutterCustomView(
context: Context,
private val viewName: String,
private val properties: com.urbanairship.json.JsonMap
) : FrameLayout(context) {
private var flutterEngine: FlutterEngine? = null
private var flutterView: FlutterView? = null
private var isEngineInitialized = false
companion object {
private const val TAG = "FlutterCustomView"
}
init {
setupView()
}
private fun setupView() {
setBackgroundColor(android.graphics.Color.BLACK)
if (layoutParams == null) {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
embedFlutterView()
}
private fun embedFlutterView() {
if (isEngineInitialized) {
return
}
try {
val route = "/custom/$viewName"
flutterEngine = FlutterEngine(context).apply {
navigationChannel.setInitialRoute(route)
dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
}
val renderSurface = FlutterTextureView(context)
flutterView = FlutterView(context, renderSurface)
val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(flutterView, params)
flutterView?.attachToFlutterEngine(flutterEngine!!)
flutterEngine?.lifecycleChannel?.appIsResumed()
isEngineInitialized = true
} catch (e: Exception) {
Log.e(TAG, "Failed to create Flutter view", e)
cleanup()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
cleanup()
}
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(visibility)
when (visibility) {
View.VISIBLE -> {
flutterEngine?.lifecycleChannel?.appIsResumed()
}
View.INVISIBLE, View.GONE -> {
flutterEngine?.lifecycleChannel?.appIsPaused()
}
}
}
private fun cleanup() {
try {
flutterEngine?.lifecycleChannel?.appIsPaused()
flutterView?.let { view ->
view.detachFromFlutterEngine()
removeView(view)
}
flutterView = null
flutterEngine?.destroy()
flutterEngine = null
isEngineInitialized = false
} catch (e: Exception) {
Log.e(TAG, "Error during cleanup", e)
}
}
}For more details on Android custom views, see the Android Custom Views documentation.
Using Custom Views
Once registered, Custom Views can be added to Scenes in the Airship dashboard:
- Create or edit a Scene
- Add the Custom View content element to a screen
- Enter the view name (e.g.,
my-custom-view) that matches the name you registered in your native code - Optionally add key-value pairs to pass custom properties to the view
The native view will be displayed within the Scene with the properties you configured.
Categories