Wrap native ViewController/Fragment

This section provides a guide about how to create a bridge for a native ViewController (on iOS) and Fragment (on Android) for Flutter.

It uses AppointmentsFragment/AppointmentListViewController as an example, but you can adapt it to any ViewController/Fragment of the SDK.

Android

Create the Fragment wrapper

class NablaAppointmentsView(
    private val context: Context,
    id: Int,
    creationParams: Map<String?, Any?>?,
) : PlatformView {
    private val view: FragmentContainerView

    init {
        val fragment = AppointmentsFragment.newInstance()

        val viewId = Random().nextInt()
        val view = FragmentContainerView(context)
        view.setId(viewId)

        view.doOnAttach {
            val activity = it.context.getFragmentActivityOrThrow()
            activity.supportFragmentManager.findFragmentByTag("flutter_fragment")?.let { flutterFragment ->
                flutterFragment.childFragmentManager.commit {
                    replace(it.id, fragment)
                }
            }
        }

        this.view = view
    }

    override fun getView(): View = view

    override fun dispose() {}

    private fun Context.getFragmentActivityOrThrow(): FragmentActivity {
        if (this is FragmentActivity) {
            return this
        }

        var currentContext = this
        while (currentContext is ContextWrapper) {
            if (currentContext is FragmentActivity) {
                return currentContext
            }
            currentContext = currentContext.baseContext
        }

        throw IllegalStateException("Unable to find activity")
    }
}

class NablaAppointmentsViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context?, viewId: Int, args: Any?): PlatformView {
        val creationParams = args as Map<String?, Any?>?
        return NablaAppointmentsView(context!!, viewId, creationParams)
    }
}

Wire the wrapper

In the MainActivity file, add these lines to wire your newly created native view:

class MainActivity: FlutterFragmentActivity() {
    private val delegate = AppCompatDelegate.create(this, null)

    init {
        addOnContextAvailableListener {
            delegate.installViewFactory()
        }
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        // Your other wiring here...

        flutterEngine.platformViewsController
            .registry
            .registerViewFactory("nablaAppointments", NablaAppointmentsViewFactory())
    }
}

⚠️

If you activity is of type FlutterActivity, change it to FlutterFragmentActivity (io.flutter.embedding.android.FlutterFragmentActivity)

iOS

Create the ViewController wrapper

class NablaAppointmentsViewFactory : NSObject, FlutterPlatformViewFactory {
    private let messenger: FlutterBinaryMessenger
    private let rootViewController: UIViewController

    init(messenger: FlutterBinaryMessenger, rootViewController: UIViewController) {
        self.messenger = messenger
        self.rootViewController = rootViewController
        super.init()
    }

    func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
        return NablaAppointmentsNativeView(
            frame: frame,
            viewIdentifier: viewId,
            arguments: args,
            binaryMessenger: messenger,
            parentController: rootViewController
        )
    }
}

class NablaAppointmentsNativeView : NSObject, FlutterPlatformView {
    private var _viewController: UIViewController

    init(
        frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?,
        binaryMessenger messenger: FlutterBinaryMessenger,
        parentController: UIViewController
    ) {
        _viewController = UIViewController(nibName: nil, bundle: nil)
        super.init()

        _viewController = NablaSchedulingClient.shared.views.createAppointmentListViewController()
        parentController.addChild(_viewController)
        _viewController.view.frame = frame
        _viewController.didMove(toParent: parentController)
    }

    func view() -> UIView {
        return _viewController.view
    }
}

Wire the wrapper

In the AppDelegate file, add these lines to wire your newly created view:

weak var registrar = self.registrar(forPlugin: "nabla")

let factory = NablaAppointmentsViewFactory(messenger: registrar!.messenger(), rootViewController: flutterViewController)
registrar!.register(
    factory,
    withId: "nablaAppointments"
)

Flutter

Create a file named nablaappointmentswidget.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';

const String viewType = 'nablaAppointments';

Widget _buildAndroid(BuildContext context, Map<String, dynamic> creationParams) {
  return PlatformViewLink(
    viewType: viewType,
    surfaceFactory:
        (context, controller) {
      return AndroidViewSurface(
        controller: controller as AndroidViewController,
        gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
        hitTestBehavior: PlatformViewHitTestBehavior.opaque,
      );
    },
    onCreatePlatformView: (params) {
      return PlatformViewsService.initExpensiveAndroidView(
        id: params.id,
        viewType: viewType,
        layoutDirection: TextDirection.ltr,
        creationParams: creationParams,
        creationParamsCodec: const StandardMessageCodec(),
        onFocus: () {
          params.onFocusChanged(true);
        },
      )
        ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
        ..create();
    },
  );
}

Widget _buildIoS(BuildContext context, Map<String, dynamic> creationParams) {
  return UiKitView(
    viewType: viewType,
    layoutDirection: TextDirection.ltr,
    creationParams: creationParams,
    creationParamsCodec: const StandardMessageCodec(),
  );
}

class NablaAppointmentsWidget extends StatelessWidget {
  const NablaAppointmentsWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Pass parameters to the platform side.
    final Map<String, dynamic> params = <String, dynamic>{};

    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        return _buildAndroid(context, params);
      case TargetPlatform.iOS:
        return _buildIoS(context, params);
      default:
        throw UnsupportedError('Unsupported platform view');
    }
  }
}

You can now use the NablaAppointmentsWidget in your Flutter code.