6.2. Platform Views

Одна из ключевых особенностей Flutter — возможность интеграции с существующими платформами и их функциональностью, что стало возможным благодаря механизму, известному как Platform Views.

Platform Views — это способ внедрения оригинальных платформенных View, таких как виджеты iOS и Android, во Flutter-приложения. Это позволяет разработчикам использовать встроенные элементы пользовательского интерфейса, такие как карты, видеоплееры или другие специализированные компоненты, которые могут быть недоступны или избыточны для создания на чистом Flutter.

С помощью Platform Views разработчики могут улучшить пользовательский опыт, добавляя элементы, которые соответствуют стандартному стилю и поведению платформы, на которой работает приложение.

В этом параграфе мы научимся применять Platform View для iOS и Android: как встроить веб-браузер в приложение и показать в нём выбранную страницу. Также сравним разные способы отображения на Android и определим их

Для этого нам понадобится написать код на стороне Dart и платформенный код для Android на Kotlin и для iOS на Swift.

На стороне Dart

Для Android будем использовать виджет AndroidView, для iOS — UiKitView. В каждый виджет необходимо передать два параметра: строку viewType для идентификации соответствующего View на платформе и url через creationParams, чтобы открыть нужную страницу в WebView.

1import 'dart:io';
2
3import 'package:flutter/material.dart';
4import 'package:flutter/services.dart';
5
6class PlatformWebView extends StatelessWidget {
7  final String url;
8
9  const PlatformWebView({
10    required this.url,
11    super.key,
12  });
13
14  @override
15  Widget build(BuildContext context) {
16    if (Platform.isIOS) {
17      return UiKitView(
18        viewType: 'WebView',
19        layoutDirection: TextDirection.ltr,
20        creationParams: {'url': url},
21        creationParamsCodec: const StandardMessageCodec(),
22      );
23    }
24
25    if (Platform.isAndroid) {
26      return AndroidView(
27        viewType: 'WebView',
28        layoutDirection: TextDirection.ltr,
29        creationParams: {'url': url},
30        creationParamsCodec: const StandardMessageCodec(),
31      );
32    }
33
34    throw Exception('Unsupported platform');
35  }
36}

Потом добавляем наш виджет PlatformWebView на экран.

1import 'package:flutter/material.dart';
2
3import 'platform_web_view.dart';
4
5void main() {
6  runApp(const MyApp());
7}
8
9class MyApp extends StatelessWidget {
10  const MyApp({super.key});
11
12  @override
13  Widget build(BuildContext context) {
14    return MaterialApp(
15      title: 'Platform Views Demo',
16      home: Scaffold(
17        appBar: AppBar(
18          title: Text('Platform Views'),
19        ),
20        body: PlatformWebView(
21          url: 'https://www.google.com',
22        ),
23      ),
24    );
25  }
26}
27

Теперь мы должны заняться настройкой платформенной части.

Настраиваем платформенную часть на Android

Сначала нам нужно отнаследоваться от PlatformView и реализовать метод getView. Этот метод вернёт готовый компонент WebView из Android SDK, с помощью которого можно встроить браузер в приложение.

1package ru.yandex.platform_views
2
3import android.content.Context
4import android.view.View
5import android.webkit.WebView
6import io.flutter.plugin.platform.PlatformView
7
8class PlatformWebView(context: Context, id: Int, creationParams: Map<String, Any>) : PlatformView {
9    private val webView: WebView
10
11    init {
12  // Здесь мы получаем url из creationParams, которые передали из Dart
13        val url = creationParams["url"] as String
14
15        webView = WebView(context)
16        webView.loadUrl(url)
17    }
18
19    override fun getView(): View {
20        return webView
21    }
22
23    override fun dispose() {
24
25    }
26}

Далее нам нужно создать класс, реализующий PlatformViewFactory. Этот класс будет заниматься созданием экземпляров PlatformWebView.

1package ru.yandex.platform_views
2
3import android.content.Context
4import io.flutter.plugin.common.StandardMessageCodec
5import io.flutter.plugin.platform.PlatformView
6import io.flutter.plugin.platform.PlatformViewFactory
7
8class PlatformWebViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
9    override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
10        val creationParams = args as Map<String, Any>
11        return PlatformWebView(context, viewId, creationParams)
12    }
13}

Чтобы связать viewType из Dart-кода с соответствующей фабрикой, нужно зарегистрировать её в FlutterEngine. Важно использовать тот же viewType, который мы указали в Dart.

1package ru.yandex.platform_views
2
3import io.flutter.embedding.android.FlutterActivity
4import io.flutter.embedding.engine.FlutterEngine
5
6class MainActivity: FlutterActivity() {
7    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
8        super.configureFlutterEngine(flutterEngine)
9        val viewTypeId = "WebView" 
10        flutterEngine
11            .platformViewsController
12            .registry
13            .registerViewFactory(viewTypeId, PlatformWebViewFactory())
14    }
15}

На этом платформенная реализация для Android закончена, теперь реализуем такой же функционал для iOS.

Настраиваем платформенную часть на iOS

Создаём класс, наследующийся от FlutterPlatformView и возвращающий нативный UiView, только здесь для отображения браузера используем WKWebView из iOS SDK WebKit.

1import Flutter
2import UIKit
3import WebKit
4
5class PlatformWebView: NSObject, FlutterPlatformView {
6    private var webView: WKWebView
7
8    init(
9        frame: CGRect,
10        viewIdentifier viewId: Int64,
11        arguments args: Any?,
12        binaryMessenger messenger: FlutterBinaryMessenger?
13    ) {
14  // так же получаем url из creationParams
15        let argsDictionary = args as! [String: Any]
16        let url = URL(string: argsDictionary["url"] as! String)
17        let request = URLRequest(url: url!)
18
19        webView = WKWebView()
20        webView.load(request)
21
22        super.init()
23    }
24
25    func view() -> UIView {
26        return webView
27    }
28}

Создаём фабрику, которая будет заниматься инстанцированием PlatformWebView.

1import Flutter
2import UIKit
3
4class PlatformWebViewFactory: NSObject, FlutterPlatformViewFactory {
5    private var messenger: FlutterBinaryMessenger
6
7    init(messenger: FlutterBinaryMessenger) {
8        self.messenger = messenger
9        super.init()
10    }
11
12    func create(
13        withFrame frame: CGRect,
14        viewIdentifier viewId: Int64,
15        arguments args: Any?
16    ) -> FlutterPlatformView {
17        return PlatformWebView(
18            frame: frame,
19            viewIdentifier: viewId,
20            arguments: args,
21            binaryMessenger: messenger
22        )
23    }
24
25    public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
26          return FlutterStandardMessageCodec.sharedInstance()
27    }
28}

И так же регистрируем её в engine.

1import Flutter
2import UIKit
3
4@main
5@objc class AppDelegate: FlutterAppDelegate {
6  override func application(
7    _ application: UIApplication,
8    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9  ) -> Bool {
10    GeneratedPluginRegistrant.register(with: self)
11      
12    guard let pluginRegistrar = self.registrar(forPlugin: "plugin-name") else { return false }
13  
14    let factory = PlatformWebViewFactory(messenger: pluginRegistrar.messenger())
15    pluginRegistrar.register(factory, withId: "WebView")
16      
17    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
18  }
19}

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

Различные способы рендеринга на Android

Мы разобрали подключение Platform View через AndroidView на Android и через UiKitView на iOS. На iOS других способов отображения нет, но на Android существует три способа рендеринга:

  • Texture Layer Hybrid Composition

  • Hybrid Composition

  • Virtual Display

Texture Layer Hybrid Composition — это самый продвинутый режим, который появился в Flutter 3.0 и используется по умолчанию. Он объединяет преимущества двух других режимов. Но чтобы понять, как он работает, стоит рассмотреть особенности его предшественников.

Virtual Display

Работает с помощью рендеринга Platform View в VirtualDisplay, который находится вне иерархии View. Сначала Platform View рендерится в него, а после содержимое связывается с текстурой Flutter.

Flutter 6.2

Как показано выше, нативный контент сначала рендерится в память, затем Flutter Engine получает отрисованные кадры через textureId и отображает их. А виджет AndroidView предоставляет координаты и размеры виджета.

Проблемы с Virtual Display

Этот режим хорошо интегрируется в систему рисования Flutter, так как использует текстуру — стандартный Flutter-виджет. Однако при использовании Virtual Display возникает ряд проблем с совместимостью, в том числе с вводом текста и специальными возможностями (accessibility).

Основная проблема этого подхода в том, что Android воспринимает View внутри VirtualDisplay как изолированный элемент на отдельном невидимом экране, никак не связанный с основной иерархией отображаемых View.

Большая часть внутренней функциональности Android зависит от прохождения иерархии View и запроса информации о View в его текущей иерархии и окне. Поскольку View находится в виртуальном дисплее вместо реальной иерархии, система получает некорректную информацию о его положении и свойствах, из-за чего внутренняя функциональность может ломаться.

Как пример одной из проблем — в WebView не работают диалоговые окна «Копировать» и «Поделиться», которые должны возникать по лонгтапу, также есть полный список открытых Issue. Впрочем, этот режим хорошо работает, например, для отображения Google Maps, Яндекс Карт или для отображения виджетов внутри скроллящегося списка.

Пример

Для того чтобы переключиться на Virtual Display, вместо класса AndroidView нужно использовать PlatformViewLink. Также в onCreatePlatformView нужно возвращать PlatformViewsService.initAndroidView, в этом случае если Flutter версия меньше 3, или версия Android на устройстве < 23, то будет использоваться Virtual Display.

1PlatformViewLink(
2  viewType: viewType,
3  surfaceFactory: (context, controller) {
4    return AndroidViewSurface(
5      controller: controller as AndroidViewController,
6      gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
7      hitTestBehavior: PlatformViewHitTestBehavior.opaque,
8    );
9  },
10  onCreatePlatformView: (params) {
11    return PlatformViewsService.initAndroidView(
12      id: params.id,
13      viewType: viewType,
14      layoutDirection: TextDirection.ltr,
15      creationParams: creationParams,
16      creationParamsCodec: const StandardMessageCodec(),
17      onFocus: () {
18        params.onFocusChanged(true);
19      },
20    )
21      ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
22      ..create();
23  },
24)

Hybrid composition

Virtual Display хорошо рендерится, но иногда могут возникать проблемы, связанные с вводом текста, и для такого случая существует другой режим — Hybrid Composition.

В этом режиме Platform View добавляется напрямую в иерархию платформенных Android-компонентов как обычный View-элемент. В результате дерево виджетов Flutter разделяется на два разных Android View — одно под Platform View, а другое над ним. Рендеринг в таком случае осуществляется в три этапа — сначала рендерится всё, что находится под Platform View, потом Platform View, а потом то, что над ней.

Если посмотреть на иерархию View через Android Studio в Layout Inspector, то можно увидеть, что нативная WebView находится прямо в нативной иерархии View. А контент Flutter-виджетов отображается с помощью отдельного FlutterSurfaceView.

Flutter 6.2

Проблемы с Hybrid Composition

Из-за того что View физически находится в иерархии View на своём месте, то здесь не возникает проблем, связанных с вводом. Но возникают другие: так как композиция View начинает выполняться на платформенном потоке вместо raster thread, это может влиять на производительность рендеринга остальных Flutter-виджетов. Также до Android 10 кадры отображения Flutter копируются дважды GPU → CPU → GPU, что ещё больше влияет на производительность.

Несмотря на недостатки, этот режим хорошо подходит, например, для отображения WebView или других View, которые занимают весь экран и требуют работы с вводом текста.

Пример

Чтобы переключиться на Hybrid composition, вместо класса AndroidView нужно использовать PlatformViewLink. Также в onCreatePlatformView нужно возвращать PlatformViewsService.initExpensiveAndroidView, в этом случае всегда будет использоваться Hybrid composition.

1PlatformViewLink(
2  viewType: viewType,
3  surfaceFactory: (context, controller) {
4    return AndroidViewSurface(
5      controller: controller as AndroidViewController,
6      gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
7      hitTestBehavior: PlatformViewHitTestBehavior.opaque,
8    );
9  },
10  onCreatePlatformView: (params) {
11    return PlatformViewsService.initExpensiveAndroidView(
12      id: params.id,
13      viewType: viewType,
14      layoutDirection: TextDirection.ltr,
15      creationParams: creationParams,
16      creationParamsCodec: const StandardMessageCodec(),
17      onFocus: () {
18        params.onFocusChanged(true);
19      },
20    )
21      ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
22      ..create();
23  },
24)

Texture Layer Hybrid Composition

Как и в случае с Hybrid Composition, изображение размещается на экране в нужном месте. Однако, как и в случае с Virtual Display, для рендеринга используется текстура, которая при этом заполняется путём перенаправления операций отрисовки (draw).

То есть когда нативная View отрисовывается на Сanvas, она отрисовывается не непосредственно на экране, а на нативной текстуре, которая связана с Flutter-текстурой. В результате такой подход позволяет избежать рендеринга в несколько этапов и не меняет работу с потоками, как в Hybrid Composition.

В большинстве случаев этот подход сочетает в себе лучшие аспекты Virtual Display и Hybrid Composition, и по возможности ему следует отдавать предпочтение. Однако существует особенность: если Platform View представляет собой SurfaceView(предоставляет область для рисования в отдельном потоке) или содержит его, этот режим не будет работать корректно и SurfaceView будет отображаться в неправильном месте и/или с неправильным z-индексом.

По умолчанию для рендеринга используется этот режим, но он может переключаться либо на Virtual Display, либо на Hybrid Composition, если в приложении используется Surface View или версия Android < 23 (менее 1% от всех устройств в мире). Подробнее о выборе режима можно прочитать здесь.

Способ рендеринга iOS

В отличие от Android, на iOS существует только один способ отображения Platform View с помощью UiKitView, рендеринг в этом случае работает так же, как Hybrid Composition на Android.


Итак, в этом параграфе мы научились применять Platform View: показали, как встроить веб-браузер в приложение, разобрали различные способы отображения для Android. Также немного погрузились в механизмы, используемые при отображении Platform View, их преимущества и недостатки и объяснили, какой механизм лучше выбрать для конкретных задач.

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

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

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E
Предыдущий параграф6.1. Channels
Следующий параграф6.3. FFI