Skip to main content

native_ui_builder 0.0.1-beta copy "native_ui_builder: ^0.0.1-beta" to clipboard
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 to VStack / Column)
  • NativeHStack (translates to HStack / Row)
  • NativeText (translates to Text / Text)
  • NativeSpacer (translates to Spacer / Spacer)
  • NativeSwitch (translates to Toggle / Switch)
  • NativeButton (translates to Button / 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:

  1. SwiftUI Generation: It parses the AST of your build method and generates a SettingsCard.swift file.
  2. Jetpack Compose Generation: It generates a SettingsCard.kt file.
  3. The Bridge: It generates a Dart wrapper (settings_card.native.dart) that uses UiKitView (iOS) and AndroidView (Android) to embed these native views inside the Flutter app.

Setup #

  1. Add native_ui_builder to your dependencies.
  2. Add native_ui_builder_generator and build_runner to your dev_dependencies.
  3. 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)

  1. 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")
    }
    
  2. Move the Code: Copy the generated .kt file (e.g., SettingsCard.kt) into your Android source tree (android/app/src/main/kotlin/com/your/package/SettingsCard.kt). Important: Add package com.your.package to the top of the file!

  3. Register the View: In MainActivity.kt, create a PlatformViewFactory that returns a ComposeView. Crucially, you must attach the LifecycleOwner to 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)

  1. Move the Code: Open your iOS project in Xcode (ios/Runner.xcworkspace) and drag the generated .swift file into the Runner group.
  2. Register the View: In AppDelegate.swift, register a FlutterPlatformViewFactory that uses a UIHostingController to 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
    }
}
0
likes
130
points
119
downloads

Documentation

API reference

Publisher

verified publisherpinz.dev

Weekly Downloads

A code generator for Flutter that transpiles Dart UI primitives into native SwiftUI and Jetpack Compose code.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on native_ui_builder