Эта статья — перевод оригинальной статьи Aleksandr Denisov "Flutter for Apple TV"

Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

В марте 2021 года Flutter получил серьезное обновление, которое позволяет разработчикам создавать красивые, быстрые и портативные приложения для самых разных платформ. С Flutter 2.x вы можете использовать одну и ту же кодовую базу для отправки нативных приложений в мобильные операционные системы, такие как iOS и Android, в настольные операционные системы, такие как Windows, macOS и Linux, а также в браузеры, такие как Chrome, Firefox, Safari или Edge. Также команда Flutter дала немного информации о Flutter для встраиваемых устройств, но нигде официально не было описано, как с помощью Flutter можно разрабатывать приложения для операционных систем Smart TV.

Если попытаться поискать в StackOverflow или GitHub информацию по теме Flutter для TV, то можно найти разрозненные куски информации от сообщества, в основном об экспериментах с Android TV, и практически ничего о других платформах. Причина этого в том, что Android TV не является платформой Flutter, поддерживаемой для производственного использования, а Apple TV вообще не поддерживается.

Но тем не менее, если заглянуть в репозиторий Flutter на GitHub, можно найти массу вопросов, связанных с разработкой приложения для Smart TV, и даже не только Android TV, но и Apple TV. Можно сделать вывод, что, несмотря на отсутствие официальной поддержки, разработчики продолжают попытки создавать приложения для смарт-телевизоров.

Собственно, я не исключение. Перед моей командой стояла задача создать приложение сразу для нескольких платформ, включая Smart TV Platforms. Нам нужно было поддерживать семь платформ с одной и той же кодовой базой: iOS, Android, Web, Android TV, Apple TV, Fire TV и Tizen. Мы оказались разработчиками, которые пытались создать приложение не только для мобильных и веб-приложений, но и для ТВ-платформ.

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

Но первый вопрос, который должен возникнуть — «зачем», зачем вам вообще может понадобиться использовать Flutter для создания ТВ-приложений. У меня есть несколько ответов на этот вопрос. Во-первых, очень круто, если можно просто взять уже написанное Flutter-приложение и просто запустить его на телевизоре. Во-вторых, и это гораздо важнее, если вы планируете разработать приложение, которое будет поддерживать телевизионные платформы, Flutter позволит вам сэкономить много ресурсов.

На самом деле, чем больше платформ, использующих единую кодовую базу, вы поддерживаете, тем больше ресурсов вы экономите. Нет необходимости иметь отдельные команды разработчиков для ТВ-приложений, все может быть сделано одной командой. И это здорово! ????

В этой статье я подробно покажу, как запускать приложения Flutter для Apple TV, и в чем разница между Android TV и Apple TV при работе с этими платформами.

Так что всегда лучше не просто говорить слова, а показывать пример на реальном приложении. К сожалению, я не могу поделиться примерами из приложения, над которым мы работаем, так как это противоречит NDA, поэтому я подготовил пример простого приложения, которое работает и на мобильных устройствах, и в веб-браузерах, и на ТВ-устройствах. Это просто галерея фильмов и сериалов. Если коснуться или щелкнуть любой из фильмов, откроется страница с подробным описанием.

Есть результат запуска этого приложения на iPhone, планшете Android и в браузере Chrome.

Давайте попробуем запустить его на Android TV.

Собственно, с запуском Android TV все предельно просто, ведь Android TV — это та же ОС Android. Вы можете запустить эмулятор Android TV или подключить реальное устройство через Android Debug Bridge (adb) и нажать кнопку запуска. Приложение просто запускается, без каких-либо дополнительных действий.

Но нужно учитывать одну деталь, ТВ-лэйаут и мобильный лэйаут иногда имеют отличия, и надо иметь возможность их различать. Во Flutter есть класс Platform, который позволяет определить, где запускается приложение. Итак, у нас есть проблема: Platform.isAndroid будет true как для мобильных устройств, так и для телевизоров, потому что Android TV — это тоже Android. Вам необходимо предоставить дополнительную информацию о платформе во время сборки. Это можно сделать несколькими способами: сделать разные точки входа, например, как «main.dart» и «main_tv.dart». Но мне кажется, что проще всего использовать Flutter Environment Variables. В этом примере я создал переменную среды TV_MODE, чтобы предоставить компилятору разные значения: «ON» для ТВ и все остальное для мобильных устройств. Затем я реализовал класс MyPlatform и использовал его вместо Platform.

class MyPlatform {
  static const tvMode = String.fromEnvironment('TV_MODE');
  static bool get isTv => tvMode == 'ON';

  static bool get isIOS => !isTv && Platform.isIOS;
  static bool get isAndroid => !isTv && Platform.isAndroid;
  static bool get isTVOS => isTv && Platform.isIOS;
  static bool get isAndroidTV => isTv && Platform.isAndroid;
}

Итак, не забудьте указать TV_MODE=ON при запуске приложения на телевизоре, чтобы получить правильное поведение приложения.

flutter run --dart-define TV_MODE=ON

Итак, давайте проверим.

А как насчет Apple TV?

Несмотря на то, что эмулятор Apple TV или реальное устройство также можно выбрать в качестве целевого устройства в Android Studio, приложение Flutter не может быть запущено на нем.

Позвольте мне объяснить, почему.

Если посмотреть на структуру проекта Flutter, то можно увидеть разные папки для приложений ios и android. Итак, в папке «android» находится Android-проект, а в папке «ios» — готовый проект XCode для iOS-приложения, но у Apple TV другая операционная система — tvOS. И как видите, папки tvOS в проекте нет, а Flutter может работать только на iOS.

На самом деле, вы можете попробовать открыть этот проект в XCode и отредактировать его, я имею в виду, изменить цель развертывания на tvOS, изменить раскадровку и так далее, чтобы сделать его проектом tvOS. Но даже если вы это сделаете, flutter-приложение все равно не запустится.

Дело в том, что tvOS — это не iOS, это другая операционная система, довольно близкая к iOS по своей структуре и имеющая то же ядро — Darwin. Но отличия есть, как минимум в API, часть iOS API просто отсутствует в tvOS, поэтому Flutter Engine будет падать при сборке tvOS приложения, пытаясь найти несуществующие методы.

Но есть решение. Движок Flutter имеет полностью открытый исходный код, а это означает, что потенциально его можно адаптировать для создания проектов для tvOS. Существует подробное руководство о том, как получить исходный код движка Flutter и настроить среду для разработки, а также руководства о том, как компилировать и отлаживать обновленный движок.

Можно попробовать внести необходимые изменения: вырезать использование методов, отсутствующих в tvOS, и отредактировать вызовы методов, сигнатуры которых отличаются. Но вам нужно иметь некоторый опыт работы с C и Objective-C.

Это был бы интересный опыт, но на самом деле он не нужен. Есть энтузиасты, которые уже внесли эти изменения и открыли исходный код обновленного Flutter Engine. Собственно, я один из них. Но вам все равно нужно скомпилировать движок локально, следуя упомянутому выше руководству, или вы можете взглянуть на подробное руководство, созданное специально для подхода Flutter для Apple TV, которое также может помочь.

Наконец, вы можете просто скачать уже собранный мной кастомный Flutter Engine (поддерживаемая версия Flutter — 2.10.3), но будьте осторожны, файл архива тяжелый, около 6,5 ГБ.

Вы можете проверить, как это работает на моем примере, выполнив следующие шаги:

  • Склонировать проект

  • Скачать движок

  • Измените значение FLUTTER_LOCAL_ENGINE на путь к папке скачанного движка в scripts/run_apple_tv.sh

  • Выбрать нужный тип движка (debug_sim, debug или release) в scripts/run_apple_tv.sh

  • Выполните sh scripts/run_apple_tv.sh в окне терминала (когда скрипт завершится, откроется XCode с проектом AppleTV)

  • Выберите устройство и нажмите кнопку «Run» в открытом проекте XCode.

Вуаля! Приложение должно запуститься на устройстве Apple Tv или эмуляторе, в зависимости от вашего выбора.

Итак, мы разобрались, как запускать приложения Flutter на Apple TV. Но просто запустить приложение недостаточно. Есть много разных вопросов. Например, как организовать взаимодействие с пользователем? Нельзя тапнуть пальцем по экрану телевизора, и даже не сделать клик мышкой, как в браузере, приходится взаимодействовать с телевизором с помощью пульта дистанционного управления.

В случае с Android TV все достаточно просто: к устройству подключается Remote Control Unit как необработанная клавиатура, а события нажатия кнопок на пульте обрабатываются как нажатия клавиш на клавиатуре. Кнопки вверх, вниз, влево и вправо аналогичны клавишам со стрелками на клавиатуре. Во Flutter уже есть механизм, который обрабатывает события клавиатуры. Это виджет Focus. Виджет управляет FocusNode, чтобы разрешить передачу фокуса клавиатуры виджету и его дочерним элементам. Обычно используется для навигации между виджетами с помощью клавиатуры, например в браузере. Вам просто нужно реализовать подсветку сфокусированных виджетов.

В случае с Apple TV разница большая. Стандартный пульт Apple TV вообще не имеет кнопок со стрелками, только тачпад. И нужно поддерживать не только тапы и клики по тачпаду, но и свайпы.

Но для поддержки Apple TV вам нужно использовать собственный движок Flutter, а это значит, что вы можете вносить изменения там. Таким образом, для получения событий сенсорной панели на стороне Flutter просто необходимо добавить код в Engine, который предоставляет информацию о касаниях, щелчках и пролистываниях в приложение Flutter через Flutter Channels.

На самом деле, в кастомном движке Flutter с открытым исходным кодом, который вы скомпилировали (или скачали) ранее, эта функциональность уже реализована. Нажатия слева, справа, вверх и вниз автоматически преобразуются в необработанные события клавиатуры как нажатие клавиш со стрелками, единственное, что вам остается сделать, это обрабатывать пролистывания и щелчки в вашем приложении Flutter.

Кастомный движок предоставляет PlatformChannel flutter/gamepadtouchevent, который дает возможность обрабатывать события тачпада. Вы можете создать свой собственный контроллер сенсорной панели для управления фокусом. Канал предоставляет три аргумента: «x», «y» и «type». Первые два — это координаты вашего пальца на тачпаде, а третий — тип взаимодействия.

Он может принимать значения: «started», «move», «ended», «loc», «click_s» и «click_e», что означает:

  • started: палец находится на тачпаде, и запускается свайп;

  • move: свайп в процессе;

  • ended: свайп закончен, палец отпустил тачпад;

  • click_s: палец находится на тачпаде, щелчок запущен;

  • click_e: палец отпустил тачпад, щелчок закончился;

  • loc: просто координаты пальца, не зависящие от взаимодействия;

Вот пример того, как можно реализовать простой контроллер тачпада. Там ловится движение пальца на 400 пикселей в какую-то сторону и вызывается метод triggerKey.

static const channel = BasicMessageChannel<dynamic>('flutter/gamepadtouchevent', JSONMessageCodec());

void init() {
  channel.setMessageHandler(_onMessage);
}

Future<void> _onMessage(dynamic arguments) async {
  num x = arguments['x'];
  num y = arguments['y'];
  String type = arguments['type'];
  late LogicalKeyboardKey key;

  if (type == 'started') {
    swipeStartX = x;
    swipeStartY = y;
    isMoving = true;
  } else if (type == 'move') {
    if (isMoving) {
      var moveX = swipeStartX - x;
      var moveY = swipeStartY - y;

      // need to move min distance in any direction
      // the 400px needs tweaking and might needs to be variable based on location of the widget on screen and duration/time of the movement to make it smoother
      if ((moveX.abs() >= 400) || (moveY.abs() >= 400)) {
        // determine direction horizontal or vertical
        if (moveX.abs() >= moveY.abs()) {
          if (moveX >= 0) {
            key = LogicalKeyboardKey.arrowLeft;
          } else {
            key = LogicalKeyboardKey.arrowRight;
          }
        } else {
          if (moveY >= 0) {
            key = LogicalKeyboardKey.arrowUp;
          } else {
            key = LogicalKeyboardKey.arrowDown;
          }
        }
        triggerKey(key);
        // reset start point (direction could change based on next cooordinates received)
        swipeStartX = x;
        swipeStartY = y;
      }
    }
  } else if (type == 'ended') {
    isMoving = false;
  } else if (type == 'click_s') {
    unawaited(
      simulateKeyEvent(
        PhysicalKeyboardKey.enter,
        isDown: true,
      ),
    );
  } else if (type == 'click_e') {
    unawaited(
      simulateKeyEvent(
        PhysicalKeyboardKey.enter,
        isDown: false,
      ),
    );
  }
}

TriggerKey — это метод, который запускает FocusManager для перемещения основного фокуса в нужном направлении.

void triggerKey(LogicalKeyboardKey key) {
  if (LogicalKeyboardKey.arrowLeft == key) {
    FocusManager.instance.primaryFocus!.focusInDirection(TraversalDirection.left);
  } else if (LogicalKeyboardKey.arrowRight == key) {
    FocusManager.instance.primaryFocus!.focusInDirection(TraversalDirection.right);
  } else if (LogicalKeyboardKey.arrowUp == key) {
    FocusManager.instance.primaryFocus!.focusInDirection(TraversalDirection.up);
  } else if (LogicalKeyboardKey.arrowDown == key) {
    FocusManager.instance.primaryFocus!.focusInDirection(TraversalDirection.down);
  }
}

Еще одна вещь, о которой нельзя забывать, это то, что кастомный движок не поддерживает нажатие кнопки «назад» RCU из коробки, вы должны поддерживать это самостоятельно. Вы должны обработать это событие и отправить событие сообщения popRoute на канал навигационной системы.

void init() {
  HardwareKeyboard.instance.addHandler(handleKeyMessage);
}

bool handleKeyMessage(KeyEvent message) {
  if (LogicalKeyboardKey.goBack == message.logicalKey) {
    final message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
    ServicesBinding.instance!.defaultBinaryMessenger
        .handlePlatformMessage(SystemChannels.navigation.name, message, (_) {});
    return true;
  }
  return false;
}

Это были самые основные моменты, касающиеся поддержки RCU для Apple TV.

Вот и все, поздравляю! Вы запустили свое первое приложение Flutter для AppleTV и теперь понимаете, что это возможно!

Так что разработка ТВ-приложений с помощью Flutter не является чем-то нереальным. Мы рассмотрели AndroidTV и AppleTV, но есть и другие ТВ-платформы, такие как Tizen, FireTV, WebOS, RokuTV и так далее. Я уверен, что Flutter можно использовать и для разработки приложений для них, и мы обязательно обсудим это в следующих статьях.

Комментарии (1)


  1. denisovich
    13.05.2022 19:23

    Спасиб за перевод. Проверь плиз некоторые места повнимательнее, иногда например click переведен как щелчок, touchpad как сенсорная панель и тп. Звучит странно :)