Irbisa · Cheatsheet

Platform Channels

MethodChannel, EventChannel, plugin development, FFI

Senior Developer · 24 questions

  1. 01

    How do platform channels work (MethodChannel, EventChannel, BasicMessageChannel), and are the calls synchronous?

    A platform channel is a named, asynchronous message pipe between Dart and the host platform: Dart encodes a message with a codec, the engine hands it to the native handler registered under the same name, and the reply comes back as a Future.

    const channel = MethodChannel('com.example.app/battery'); Future<int?> batteryLevel() async {  try {    return await channel.invokeMethod<int>('getBatteryLevel');  } on PlatformException catch (e) {…
  2. 02

    What is EventChannel and when do you use it instead of MethodChannel?

    EventChannel carries a continuous stream of events from native to Dart.

    class BatteryState {  static const _channel = EventChannel('com.example/battery_state');   // created once: every listener shares one native onListen  static final Stream<int> percent =      _channel.receiveBroadcastStream().map((e) => e as int);…
  3. 03

    How should you handle errors across a platform channel?

    The native side answers with an error instead of a value (result.error(code, message, details)), and Dart receives a PlatformException whose code it can branch on.

    class CameraService {  static const _channel = MethodChannel('com.example/camera');   Future<String> takePicture() async {    try {      final path = await _channel…
  4. 04

    Dart throws a type error casting a channel result to Map<String, dynamic>, and Kotlin crashes casting a timestamp to Int. What is going on?

    Both crashes assert a type the standard codec never promised: Dart receives Map<Object?, Object?>, and Kotlin receives Integer or Long depending on the value, so read values as Object? and Number and convert them yourself.

    const channel = MethodChannel('com.example/profile'); // wrong: _TypeError, the platform returned Map<Object?, Object?>final bad = await channel.invokeMethod('load') as Map<String, dynamic>; // right: the typed helpers cast for you…
  5. 05

    Which types can cross a platform channel, and how do you send your own objects?

    The standard codec carries null, bool, int, double, String, typed data such as Uint8List and Float64List, and List and Map of those.

    class Tile {  const Tile(this.z, this.x, this.y, this.bytes);  final int z, x, y;  final Uint8List bytes;}…
  6. 06

    What is Pigeon and why use it over hand-written MethodChannel code?

    Pigeon is a code generator: you describe the interface once in Dart, and it writes matching, typed Dart, Kotlin and Swift (or C++, Objective-C) that talk over an ordinary channel.

    // pigeons/device.dart: the single source of truthimport 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions(  dartOut: 'lib/src/device.g.dart',  kotlinOut: 'android/src/main/kotlin/com/example/Device.g.kt',…
  7. 07

    How do you request runtime permissions such as camera or location in a Flutter app?

    Declare the permission in each platform's config, then request it at runtime at the moment the feature needs it (usually with permission_handler), and handle granted, denied and permanently denied as different cases.

    Future<bool> ensureCameraPermission() async {  final status = await Permission.camera.request(); // returns at once if granted  if (status.isGranted || status.isLimited) return true;   if (status.isPermanentlyDenied) {    // the system dialog will not appear again: explain, then send to Settings…
  8. 08

    You add a plugin, run the app, and every call throws MissingPluginException. How do you work out why?

    It means the message reached the platform and nobody there had registered a handler for that channel name, so the platform answered "not implemented".

  9. 09

    Some Dart calls to this Android handler never complete, and others crash the app. What is wrong?

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {    when (call.method) {        "readFile" -> scope.launch(Dispatchers.IO) {            val path = call.argument<String>("path")!!            result.success(File(path).readText())        }        "clearCache" -> {            cacheDir.deleteRecursively()            result.success(null)            result.success(true)        }        "ping" -> if (connected) result.success("pong")    }}

    Every call must get exactly one reply on every path, and this handler breaks that rule four ways: a failure path with no reply, a double reply, a conditional reply and no fallback branch.

  10. 10

    Find the bug: the inbox badge stops updating as soon as a chat is opened, and after the chat is closed no screen receives pushes at all.

    class PushListener {  PushListener(this.onMessage) {    _channel.setMethodCallHandler((call) async {      if (call.method == 'onMessage') onMessage(call.arguments as String);    });  }   static const _channel = MethodChannel('com.example/push');  final void Function(String text) onMessage;   void dispose() => _channel.setMethodCallHandler(null);} // InboxBadge and ChatScreen each create one in initState// and call dispose() in their own dispose()

    A channel name has exactly one Dart handler: each setMethodCallHandler replaces the previous one, so the chat's listener silently evicts the badge's, and the chat's dispose() then sets it to null, leaving nobody to receive pushes.

  11. 11

    Each time this screen rebuilds, the native log shows onCancel then onListen, and readings pause. Why?

    class HeartRate extends StatelessWidget {  const HeartRate({super.key, required this.unit});  final String unit;   static const _channel = EventChannel('com.example/heart_rate');   @override  Widget build(BuildContext context) {    return StreamBuilder<int>(      stream: _channel.receiveBroadcastStream().cast<int>(),      builder: (context, snap) => Text('${snap.data ?? '--'} $unit'),    );  }}

    build creates a new stream on every rebuild.

  12. 12

    Find the bug: after the user leaves the run screen, the location indicator stays on and the battery keeps draining.

    class _RunMapState extends State<RunMap> {  static const _location = EventChannel('com.example/location');  late final StreamSubscription<Object?> _sub;  Map<Object?, Object?>? _last;   @override  void initState() {    super.initState();    _sub = _location.receiveBroadcastStream().listen((event) {      setState(() => _last = event as Map<Object?, Object?>);    });  }   @override  Widget build(BuildContext context) => Text('${_last?['lat']}, ${_last?['lng']}');}

    The subscription is never cancelled: there is no dispose, so after the screen closes the stream still has a listener, native never receives onCancel, and GPS keeps running; and every new fix calls setState on a disposed State.

  13. 13

    An accelerometer EventChannel at 200 Hz makes the UI hitch, and the values lag further behind the longer the screen is open. Where is the problem?

    Lag that keeps growing means something accumulates: events queue faster than the UI isolate drains them, or your code does work per event that grows with time.

  14. 14

    How do you build a Flutter plugin from scratch, and when do you split it into a federated plugin?

    Run flutter create --template=plugin --platforms=android,ios for a Dart API, native code and an example/ app; split into a federated plugin only when platforms are owned or released separately.

    # my_plugin/pubspec.yaml: the app-facing package of a federated pluginname: my_pluginversion: 1.0.0 flutter:  plugin:…
  15. 15

    Which threads do platform channel calls run on, and how do you keep a slow native handler from freezing the UI?

    For Dart a channel call is always asynchronous, but the native handler runs on the platform main thread by default.

    class HashPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {    private var channel: MethodChannel? = null     override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {        val messenger = binding.binaryMessenger        // calls arrive on a background thread, in order, instead of the main thread…
  16. 16

    A feature runs a C++ filter on every camera frame and also shows the system photo permission dialog. What goes over dart:ffi and what over a channel?

    The filter goes over FFI and the permission over a channel.

  17. 17

    Native decodes a 12-megapixel photo and returns raw pixels over a MethodChannel; the app stutters and sometimes runs out of memory. What do you change?

    Stop moving pixels across the boundary.

    // wrong: pixels over the channel, copied on both sidesfinal pixels = await channel.invokeMethod<Uint8List>('decodePhoto', path);ui.decodeImageFromPixels(pixels!, 4000, 3000, ui.PixelFormat.rgba8888, (image) { /* ... */ }); // ~48 MB in flight // right: keep the bytes native and hand Dart a path to an encoded filefinal jpegPath = await channel.invokeMethod<String>('exportJpeg', path);…
  18. 18

    How do you call platform code from a background isolate?

    A channel needs a BinaryMessenger, and only the root isolate has one.

    import 'dart:isolate';import 'package:flutter/services.dart'; Future<String> hashViaPlatform(String path) async {  final token = RootIsolateToken.instance!;    // read on the root isolate…
  19. 19

    A designer wants a native map inside a Flutter screen. How do platform views work, and what do they cost?

    A platform view embeds a real native view (a map, a WebView, an ad) in the widget tree through AndroidView or UiKitView.

    Widget nativeMap() {  switch (defaultTargetPlatform) {    case TargetPlatform.android:      return const AndroidView(        viewType: 'com.example/map',        creationParams: {'zoom': 12},…
  20. 20

    Your Android plugin needs the Activity to request a permission. How do you get it without leaking it?

    The Activity arrives through the ActivityAware callbacks, and the rule that stops the leak is simple: hold it only between attach and detach.

    class PermissionPlugin : FlutterPlugin, ActivityAware, MethodCallHandler,    PluginRegistry.RequestPermissionsResultListener {    private var binding: ActivityPluginBinding? = null   // only between attach and detach    private var pending: MethodChannel.Result? = null    // onAttachedToEngine / onDetachedFromEngine: create and clear the MethodChannel    override fun onAttachedToActivity(b: ActivityPluginBinding) {…
  21. 21

    How do you run Dart code in the background on Android and iOS (a periodic sync, a background fetch, location tracking)?

    Flutter has no background scheduler of its own: the OS wakes the app through its own API (WorkManager on Android, BGTaskScheduler on iOS), and a plugin starts a headless Flutter engine that runs a top-level Dart entry point.

    @pragma('vm:entry-point')void callbackDispatcher() {  Workmanager().executeTask((task, inputData) async {    DartPluginRegistrant.ensureInitialized();    // a fresh isolate: nothing from the UI isolate exists here    final db = await AppDatabase.open();…
  22. 22

    How do you add Flutter to an existing native app, and why do you pre-warm and cache a FlutterEngine?

    You build the Flutter part as a module that the native app consumes as a library, and show it through FlutterActivity, FlutterFragment or FlutterViewController.

    class App : Application() {    override fun onCreate() {        super.onCreate()        // start once, early: the first Flutter screen then opens without a blank delay        val engine = FlutterEngine(this)        engine.navigationChannel.setInitialRoute("/checkout")…
  23. 23

    A cold start from a notification tap loses the payload: native calls invokeMethod and Dart never hears it. Why, and what do you ship instead?

    At that moment no Dart handler exists on the channel yet, so the message goes into the engine's channel buffer, which holds one message per channel by default and drops the rest with a warning.

    class Push {  static const _channel = MethodChannel('com.example/push');   // pull: survives the cold start because native kept the payload  static Future<PushPayload?> initial() async {    final map = await _channel.invokeMapMethod<String, Object?>('getInitialMessage');…
  24. 24

    A C library reports progress by calling your callback from its own worker thread. How do you deliver that into Dart without crashing the process?

    Use NativeCallable.listener.

    // C: typedef void (*progress_cb)(int32_t percent);//    void start_job(const char* path, progress_cb cb);   // cb runs on a worker threadtypedef _ProgressC = Void Function(Int32);typedef _StartJobC = Void Function(Pointer<Utf8>, Pointer<NativeFunction<_ProgressC>>);typedef _StartJob = void Function(Pointer<Utf8>, Pointer<NativeFunction<_ProgressC>>);…