Irbisa · Шпаргалка

Платформенные каналы

MethodChannel, EventChannel, разработка плагинов, FFI

Senior-разработчик · 24 вопроса

  1. 01

    Как работают платформенные каналы (MethodChannel, EventChannel, BasicMessageChannel) и синхронны ли вызовы?

    Платформенный канал работает как именованная асинхронная труба для сообщений между Dart и платформой: Dart кодирует сообщение кодеком, движок передаёт его нативному обработчику, зарегистрированному на то же имя, а ответ возвращается как 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

    Что такое EventChannel и когда он нужен вместо MethodChannel?

    EventChannel передаёт непрерывный поток событий из нативного кода в 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

    Как правильно обрабатывать ошибки через платформенный канал?

    Нативная сторона отвечает ошибкой вместо значения (result.error(code, message, details)), и Dart получает PlatformException, по code которого можно ветвиться.

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

    Dart падает с ошибкой типа при касте результата канала к Map<String, dynamic>, а Kotlin падает при касте таймстемпа к Int. Что происходит?

    Оба падения утверждают тип, которого стандартный кодек не обещал: Dart получает Map<Object?, Object?>, а Kotlin получает Integer или Long в зависимости от значения, поэтому читайте значения как Object? и Number и приводите их сами.

    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

    Какие типы можно передавать через платформенный канал и как отправить собственный объект?

    Стандартный кодек умеет null, bool, int, double, String, типизированные данные вроде Uint8List и Float64List, а также List и Map из них.

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

    Что такое Pigeon и зачем он нужен вместо рукописного кода на MethodChannel?

    Pigeon работает как генератор кода: вы один раз описываете интерфейс на Dart, а он пишет согласованный типизированный код на Dart, Kotlin и Swift (или C++, Objective-C), который общается через обычный канал.

    // 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

    Как запросить runtime-разрешения, например камеру или геолокацию, во Flutter-приложении?

    Объявите разрешение в конфиге каждой платформы, затем запрашивайте его в рантайме в момент, когда оно нужно фиче (обычно через permission_handler), и обрабатывайте «выдано», «отказано» и «отказано навсегда» как разные случаи.

    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

    Вы добавили плагин, запустили приложение, и каждый вызов бросает MissingPluginException. Как найти причину?

    Это значит, что сообщение дошло до платформы, но там никто не зарегистрировал обработчик на это имя канала, и платформа ответила «не реализовано».

  9. 09

    Часть вызовов из Dart к этому Android-обработчику никогда не завершается, а другие роняют приложение. Что не так?

    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")    }}

    Каждый вызов должен получить ровно один ответ на каждом пути, а этот обработчик нарушает правило четырьмя способами: путь ошибки без ответа, двойной ответ, условный ответ и отсутствие запасной ветки.

  10. 10

    Найдите баг: бейдж входящих перестаёт обновляться, как только открывают чат, а после закрытия чата пуши не получает вообще ни один экран.

    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()

    У имени канала ровно один Dart-обработчик: каждый setMethodCallHandler заменяет предыдущий, поэтому слушатель чата молча вытесняет слушатель бейджа, а dispose() чата затем ставит null, и пуши больше никто не получает.

  11. 11

    При каждой перестройке этого экрана нативный лог показывает onCancel, затем onListen, и показания прерываются. Почему?

    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 создаёт новый стрим при каждой перестройке.

  12. 12

    Найдите баг: после того как пользователь уходит с экрана пробежки, индикатор геолокации не гаснет и батарея продолжает садиться.

    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']}');}

    Подписка никогда не отменяется: dispose нет, поэтому после закрытия экрана у стрима остаётся слушатель, нативная сторона не получает onCancel, и GPS продолжает работать, а каждая новая точка вызывает setState у уже уничтоженного State.

  13. 13

    EventChannel акселерометра на 200 Гц вызывает подтормаживания UI, а значения отстают тем сильнее, чем дольше открыт экран. Где проблема?

    Отставание, которое только растёт, означает, что что-то копится: события встают в очередь быстрее, чем UI-изолят их разбирает, или ваш код делает на каждое событие работу, растущую со временем.

  14. 14

    Как написать Flutter-плагин с нуля и когда делить его на federated-плагин?

    Запустите flutter create --template=plugin --platforms=android,ios, и получите Dart API, нативный код и приложение example/; делите на federated-плагин, только когда платформы ведут или выпускают отдельно.

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

    На каких потоках выполняются вызовы платформенных каналов и как не дать медленному нативному обработчику заморозить UI?

    Для Dart вызов канала всегда асинхронный, но нативный обработчик по умолчанию выполняется на главном потоке платформы.

    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

    Фича применяет C++-фильтр к каждому кадру камеры и показывает системный диалог доступа к фото. Что идёт через dart:ffi, а что через канал?

    Фильтр идёт через FFI, разрешение идёт через канал.

  17. 17

    Нативный код декодирует фото на 12 мегапикселей и возвращает сырые пиксели через MethodChannel; приложение тормозит и иногда падает по памяти. Что менять?

    Перестаньте гонять пиксели через границу.

    // 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

    Как вызвать платформенный код из фонового изолята?

    Каналу нужен BinaryMessenger, и он есть только у корневого изолята.

    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

    Дизайнер хочет нативную карту внутри Flutter-экрана. Как работают platform views и чего они стоят?

    Platform view встраивает настоящий нативный view (карту, WebView, рекламу) в дерево виджетов через AndroidView или UiKitView.

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

    Android-плагину нужна Activity, чтобы запросить разрешение. Как получить её без утечки?

    Activity приходит через колбэки ActivityAware, и правило против утечки простое: держать её можно только между attach и 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

    Как выполнять Dart-код в фоне на Android и iOS (периодическую синхронизацию, background fetch, отслеживание геолокации)?

    Собственного фонового планировщика у Flutter нет: ОС будит приложение через свой API (WorkManager на Android, BGTaskScheduler на iOS), а плагин запускает headless-движок Flutter, который выполняет top-level точку входа на Dart.

    @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

    Как встроить Flutter в существующее нативное приложение и зачем заранее прогревать и кешировать FlutterEngine?

    Flutter-часть собирается как модуль, который нативное приложение подключает как библиотеку, и показывается через FlutterActivity, FlutterFragment или 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

    При холодном старте по тапу на уведомление теряется payload: нативный код вызывает invokeMethod, а Dart его не слышит. Почему и что сделать вместо этого?

    В этот момент Dart-обработчика на канале ещё нет, поэтому сообщение попадает в буфер каналов движка, а он по умолчанию хранит одно сообщение на канал и остальные выбрасывает с предупреждением.

    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

    C-библиотека сообщает о прогрессе, вызывая ваш колбэк со своего рабочего потока. Как доставить это в Dart, не уронив процесс?

    Используйте 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>>);…