Saltar al contenido principal

Agregar Flutter a cualquier aplicación web

Aprende las diferentes formas de incrustar vistas de Flutter en el contenido web.

Las vistas de Flutter y el contenido web se pueden componer para producir una aplicación web de diferentes maneras. Elige una de las siguientes opciones según tu caso de uso:

Modo de página completa

#

En el modo de página completa, la aplicación web de Flutter toma el control de toda la ventana del navegador y cubre su viewport por completo al renderizar.

Este es el modo de incrustación predeterminado para los nuevos proyectos web de Flutter, y no se necesita ninguna configuración adicional.

html
<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <script src="flutter_bootstrap.js" defer></script>
  </body>
</html>

Cuando Flutter web se inicia sin hacer referencia a multiViewEnabled o a un hostElement, utiliza el modo de página completa.

Para obtener más información sobre el archivo flutter_bootstrap.js, consulta Personalizar la inicialización de la aplicación.

Incrustación con iframe

#

Se recomienda el modo de página completa al incrustar una aplicación web de Flutter a través de un iframe. La página que incrusta el iframe puede cambiar su tamaño y posicionarlo según sea necesario, y Flutter lo llenará por completo.

html
<iframe src="https://url-to-your-flutter/index.html"></iframe>

Para obtener más información sobre los pros y los contras de un iframe, consulta los documentos del elemento Inline Frame en MDN.

Modo incrustado

#

Las aplicaciones web de Flutter también pueden renderizar contenido en un número arbitrario de elementos (comúnmente divs) de otra aplicación web; esto se denomina "modo incrustado" (o "multivista").

En este modo:

  • Una aplicación web de Flutter puede iniciarse, pero no se renderiza hasta que se agrega la primera "vista" con addView.
  • La aplicación host puede agregar o eliminar vistas de la aplicación web incrustada de Flutter.
  • La aplicación de Flutter recibe una notificación cuando se agregan o eliminan vistas, para que pueda ajustar sus widgets en consecuencia.

Habilitar el modo multivista

#

Habilita el modo multivista configurando multiViewEnabled: true en el método initializeEngine como se muestra:

flutter_bootstrap.js
js
{{flutter_js}}
{{flutter_build_config}}

_flutter.loader.load({
  onEntrypointLoaded: async function onEntrypointLoaded(engineInitializer) {
    let engine = await engineInitializer.initializeEngine({
      multiViewEnabled: true, // Enables embedded mode.
    });
    let app = await engine.runApp();
    // Make this `app` object available to your JS app.
  }
});

Gestionar vistas de Flutter desde JS

#

Para agregar o eliminar vistas, usa el objeto app devuelto por el método runApp:

js
// Adding a view...
let viewId = app.addView({
  hostElement: document.querySelector('#some-element'),
});

// Removing viewId...
let viewConfig = app.removeView(viewId);

Manejar cambios de vista desde Dart

#

Las adiciones y eliminaciones de vistas se exponen a Flutter a través del método didChangeMetrics de la clase WidgetsBinding.

La lista completa de vistas adjuntas a tu aplicación Flutter está disponible a través del iterable WidgetsBinding.instance.platformDispatcher.views. Estas vistas son de tipo FlutterView.

Para renderizar contenido en cada FlutterView, tu aplicación Flutter necesita crear un widget View. Los widgets View se pueden agrupar bajo un widget ViewCollection.

El siguiente ejemplo, del Multi View Playground, encapsula lo anterior en un widget MultiViewApp que se puede usar como el widget raíz para tu aplicación. Se ejecuta una función WidgetBuilder para cada FlutterView:

multi_view_app.dart
dart
import 'dart:ui' show FlutterView;
import 'package:flutter/widgets.dart';

/// Calls [viewBuilder] for every view added to the app to obtain the widget to
/// render into that view. The current view can be looked up with [View.of].
class MultiViewApp extends StatefulWidget {
  const MultiViewApp({super.key, required this.viewBuilder});

  final WidgetBuilder viewBuilder;

  @override
  State<MultiViewApp> createState() => _MultiViewAppState();
}

class _MultiViewAppState extends State<MultiViewApp> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _updateViews();
  }

  @override
  void didUpdateWidget(MultiViewApp oldWidget) {
    super.didUpdateWidget(oldWidget);
    // Need to re-evaluate the viewBuilder callback for all views.
    _views.clear();
    _updateViews();
  }

  @override
  void didChangeMetrics() {
    _updateViews();
  }

  Map<Object, Widget> _views = <Object, Widget>{};

  void _updateViews() {
    final Map<Object, Widget> newViews = <Object, Widget>{};
    for (final FlutterView view in WidgetsBinding.instance.platformDispatcher.views) {
      final Widget viewWidget = _views[view.viewId] ?? _createViewWidget(view);
      newViews[view.viewId] = viewWidget;
    }
    setState(() {
      _views = newViews;
    });
  }

  Widget _createViewWidget(FlutterView view) {
    return View(
      view: view,
      child: Builder(
        builder: widget.viewBuilder,
      ),
    );
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ViewCollection(views: _views.values.toList(growable: false));
  }
}

Para obtener más información, consulta el mixin WidgetsBinding en los documentos de la API, o el repositorio de Multi View Playground que se utilizó durante el desarrollo.

Reemplazar runApp por runWidget en Dart

#

La función runApp de Flutter asume que hay al menos una vista disponible para renderizar (la implicitView); sin embargo, en el modo multivista de Flutter web, la implicitView ya no existe, por lo que runApp comenzará a fallar con errores de Unexpected null value.

En el modo multivista, tu main.dart debe llamar a la función runWidget en su lugar. No requiere una implicitView y solo se renderizará en las vistas que se hayan agregado explícitamente a tu aplicación.

El siguiente ejemplo utiliza la MultiViewApp descrita anteriormente para renderizar copias del widget MyApp() en cada FlutterView disponible:

main.dart
dart
void main() {
  runWidget(
    MultiViewApp(
      viewBuilder: (BuildContext context) => const MyApp(),
    ),
  );
}

Identificar vistas

#

Cada FlutterView tiene un identificador asignado por Flutter al adjuntarse. Este viewId se puede usar para identificar de manera única cada vista, recuperar su configuración inicial o decidir qué renderizar en ella.

El viewId del FlutterView renderizado se puede recuperar de su BuildContext de la siguiente manera:

dart
class SomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Retrieve the `viewId` where this Widget is being built:
    final int viewId = View.of(context).viewId;
    // ...

De manera similar, desde el método viewBuilder de la MultiViewApp, el viewId se puede recuperar así:

dart
MultiViewApp(
  viewBuilder: (BuildContext context) {
    // Retrieve the `viewId` where this Widget is being built:
    final int viewId = View.of(context).viewId;
    // Decide what to render based on `viewId`...
  },
)

Lee más sobre el constructor View.of.

Configuración inicial de la vista

#

Las vistas de Flutter pueden recibir cualquier dato de inicialización desde JS al iniciarse. Los valores se pasan a través de la propiedad initialData del método addView como se muestra:

js
// Adding a view with initial data...
let viewId = app.addView({
  hostElement: someElement,
  initialData: {
    greeting: 'Hello, world!',
    randomValue: Math.floor(Math.random() * 100),
  }
});

En Dart, el initialData está disponible como un objeto JSAny, accesible a través de la propiedad de nivel superior views en la biblioteca dart:ui_web. Se accede a los datos a través del viewId de la vista actual, como se muestra:

dart
final initialData = ui_web.views.getInitialData(viewId) as YourJsInteropType;

Para aprender a definir la clase YourJsInteropType para mapear el objeto initialData pasado desde JS para que sea seguro con respecto a los tipos en tu programa Dart, consulta: Interoperabilidad JS en dart.dev.

Restricciones de vista

#

De forma predeterminada, una vista web incrustada de Flutter considera el tamaño de su hostElement como una propiedad inmutable y limita estrictamente su diseño al espacio disponible.

En la web, es común que el tamaño intrínseco de un elemento afecte el diseño de la página (como las etiquetas img o p que pueden reajustar el contenido a su alrededor).

Al agregar una vista a Flutter web, puedes configurarla con restricciones que informen a Flutter sobre cómo debe distribuirse la vista:

js
// Adding a view with initial data...
let viewId = app.addView({
  hostElement: someElement,
  viewConstraints: {
    maxWidth: 320,
    minHeight: 0,
    maxHeight: Infinity,
  }
});

Las restricciones de la vista pasadas desde JS deben ser compatibles con el estilo CSS del hostElement donde se incrusta Flutter. Por ejemplo, Flutter no intentará "corregir" constantes contradictorias como pasar max-height: 100px en CSS, pero maxHeight: Infinity a Flutter.

Para obtener más información, consulta la clase ViewConstraints, y Entender las restricciones.

Elemento personalizado (hostElement)

#

Puedes incrustar una aplicación web de Flutter de vista única en cualquier elemento HTML de tu página web.

Para indicarle a Flutter web en qué elemento renderizar, pasa un objeto con un campo config a la función _flutter.loader.load que especifique un HTMLElement como el hostElement.

js
_flutter.loader.load({
  config: {
    hostElement: document.getElementById('flutter_host'),
  }
});

Para obtener más información sobre otras opciones de configuración, consulta Personalizar la inicialización de la aplicación web.