native_ui_builder 0.0.1-beta
native_ui_builder: ^0.0.1-beta copied to clipboard
A code generator for Flutter that transpiles Dart UI primitives into native SwiftUI and Jetpack Compose code.
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
}
}