Native UI Builder
A code generator for Flutter that transpiles Dart UI primitives into native SwiftUI and Jetpack Compose code.
The Concept
Define a UI component in Dart using a specific set of primitives (NativeVStack, NativeText, etc.), and the package transpiles it into SwiftUI and Jetpack Compose, wrapping it all in a Flutter PlatformView automatically.
Available Primitives
NativeVStack(translates toVStack/Column)NativeHStack(translates toHStack/Row)NativeText(translates toText/Text)NativeSpacer(translates toSpacer/Spacer)NativeSwitch(translates toToggle/Switch)NativeButton(translates toButton/Button)
The Developer Experience
The developer writes a Dart class annotated with @NativeComponent.
import 'package:flutter/material.dart';
import 'package:native_ui_builder/native_ui_builder.dart';
part 'settings_card.native.dart';
@NativeComponent()
class SettingsCard extends NativeView {
final String title;
final bool isToggled;
final VoidCallback onToggle;
SettingsCard({
required this.title,
required this.isToggled,
required this.onToggle
});
@override
NativeWidget build() {
return NativeHStack(
padding: 16.0,
children: [
NativeText(title, style: NativeTextStyle.headline),
const NativeSpacer(),
NativeSwitch(
value: isToggled,
onChanged: (val) => onToggle(),
),
],
);
}
}
How the Package Works Under the Hood
When the developer runs dart run build_runner build:
- SwiftUI Generation: It parses the AST of your build method and generates a
SettingsCard.swiftfile. - Jetpack Compose Generation: It generates a
SettingsCard.ktfile. - The Bridge: It generates a Dart wrapper (
settings_card.native.dart) that usesUiKitView(iOS) andAndroidView(Android) to embed these native views inside the Flutter app.
Setup
- Add
native_ui_builderto yourdependencies. - Add
native_ui_builder_generatorandbuild_runnerto yourdev_dependencies. - Run
dart run build_runner build.
4. Manually Wiring Up the Generated Views
Since v0.0.1 generates the raw Jetpack Compose and SwiftUI files, you must manually register the PlatformView in the native projects so Flutter knows how to render them.
Android (Jetpack Compose)
-
Enable Compose: Since Kotlin 2.0, you must use the JetBrains Compose Compiler plugin.
In
android/settings.gradle.kts, add:plugins { // ... existing plugins id("org.jetbrains.kotlin.plugin.compose") version "2.4.0" apply false }In
android/app/build.gradle.kts, apply the plugin, enable compose features, and add the dependencies:plugins { // ... id("org.jetbrains.kotlin.plugin.compose") } android { // ... buildFeatures { compose = true } } dependencies { implementation("androidx.compose.ui:ui:1.6.0") implementation("androidx.compose.material3:material3:1.2.0") implementation("androidx.compose.ui:ui-tooling-preview:1.6.0") implementation("androidx.activity:activity-compose:1.8.2") } -
Move the Code: Copy the generated
.ktfile (e.g.,SettingsCard.kt) into your Android source tree (android/app/src/main/kotlin/com/your/package/SettingsCard.kt). Important: Addpackage com.your.packageto the top of the file! -
Register the View: In
MainActivity.kt, create aPlatformViewFactorythat returns aComposeView. Crucially, you must attach theLifecycleOwnerto the View tree before Compose can render inside a Flutter PlatformView.
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import io.flutter.embedding.android.FlutterFragmentActivity
// ...
class MainActivity: FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Setup Jetpack Compose ViewTree owners
val view = window.decorView
view.setViewTreeLifecycleOwner(this)
view.setViewTreeViewModelStoreOwner(this)
view.setViewTreeSavedStateRegistryOwner(this)
flutterEngine.platformViewsController.registry.registerViewFactory(
"com.native_ui_builder/SettingsCard", SettingsCardFactory(flutterEngine)
)
}
}
// See the example app for the full SettingsCardFactory and SettingsCardPlatformView implementation!
iOS (SwiftUI)
- Move the Code: Open your iOS project in Xcode (
ios/Runner.xcworkspace) and drag the generated.swiftfile into theRunnergroup. - Register the View: In
AppDelegate.swift, register aFlutterPlatformViewFactorythat uses aUIHostingControllerto render the SwiftUI view.
import Flutter
import UIKit
import SwiftUI
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
// Register custom PlatformView once per implicit engine
let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "NativeUIBuilderPlugin")!
let factory = SettingsCardFactory(messenger: registrar.messenger())
registrar.register(factory, withId: "com.native_ui_builder/SettingsCard")
}
}
// 1. The Factory
class SettingsCardFactory: NSObject, FlutterPlatformViewFactory {
private var messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
return SettingsCardPlatformView(frame: frame, viewIdentifier: viewId, arguments: args, binaryMessenger: messenger)
}
public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
}
class SettingsCardState: ObservableObject {
@Published var title: String
@Published var isToggled: Bool
init(title: String, isToggled: Bool) {
self.title = title
self.isToggled = isToggled
}
}
struct SettingsCardWrapper: View {
@ObservedObject var state: SettingsCardState
var onToggle: () -> Void
var body: some View {
let isToggledBinding = Binding<Bool>(
get: { state.isToggled },
set: { newValue in state.isToggled = newValue }
)
SettingsCard(title: state.title, isToggled: isToggledBinding, onToggle: onToggle)
}
}
// 2. The PlatformView wrapping SwiftUI
class SettingsCardPlatformView: NSObject, FlutterPlatformView {
private var _view: UIView
private var channel: FlutterMethodChannel
private var state: SettingsCardState
init(frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, binaryMessenger messenger: FlutterBinaryMessenger) {
let params = args as? [String: Any]
let initialTitle = params?["title"] as? String ?? ""
let initialToggled = params?["isToggled"] as? Bool ?? false
self.state = SettingsCardState(title: initialTitle, isToggled: initialToggled)
self.channel = FlutterMethodChannel(name: "com.native_ui_builder/SettingsCard\(viewId)", binaryMessenger: messenger)
self._view = UIView()
super.init()
self.channel.setMethodCallHandler { [weak self] call, result in
if call.method == "update" {
if let args = call.arguments as? [String: Any] {
if let newTitle = args["title"] as? String {
self?.state.title = newTitle
}
if let newToggled = args["isToggled"] as? Bool {
if self?.state.isToggled != newToggled {
self?.state.isToggled = newToggled
}
}
}
result(nil)
} else {
result(FlutterMethodNotImplemented)
}
}
let onToggle: () -> Void = { [weak self] in
guard let self = self else { return }
self.channel.invokeMethod("onToggle", arguments: nil)
}
let wrapper = SettingsCardWrapper(state: self.state, onToggle: onToggle)
let hostingController = UIHostingController(rootView: wrapper)
self._view = hostingController.view
}
func view() -> UIView {
return _view
}
}