p-mazhnik / flutter-embedding

Contains examples of embedding Flutter in React apps.
https://p-mazhnik.github.io/flutter-embedding/expo/
21 stars 4 forks source link

How can I dispose the current instance of the app? Need something like engineInitializer?.stopEngine #14

Closed yudhishthir-siterecon closed 1 month ago

yudhishthir-siterecon commented 1 month ago

I have successfully embedded a Flutter app in React (web) but I'm facing an issue of multiple instances of the flutter app.

When the wrapper (FlutterView) unmounts, I want the flutter app to be disposed. A new app is instantiated each time the wrapper React component re-mounts, while the previous instance remains. I need some function which I can call in the cleanup function of this react component, tried making _flutter null but that caused different set of issues.

Here's the current code, the main issue is I keep seeing logs even after the FlutterView is unmounted: Hello from Flutter! Timer ticked at 2024-07-11 21:20:11.069

main.dart:

import 'dart:async';
import 'package:flutter/material.dart';

import 'pages/counter.dart';
import 'pages/dash.dart';
import 'pages/text.dart';

import 'src/js_interop.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final ValueNotifier<DemoScreen> _screen =
      ValueNotifier<DemoScreen>(DemoScreen.counter);
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);
  final ValueNotifier<String> _text = ValueNotifier<String>('');

  late final DemoAppStateManager _state;

  @override
  void initState() {
    super.initState();
    _state = DemoAppStateManager(
      screen: _screen,
      counter: _counter,
      text: _text,
    );
    final export = createJSInteropWrapper(_state);

    // Emit this through the root object of the flutter app :)
    broadcastAppEvent('flutter-initialized', export);

    // Print something infinitely
    Timer.periodic(Duration(seconds: 1), (timer) {
      print('Hello from Flutter! Timer ticked at ${DateTime.now()}');
    });
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'React 🤝 Flutter',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: ValueListenableBuilder<DemoScreen>(
        valueListenable: _screen,
        builder: (context, value, child) => demoScreenRouter(value),
      ),
    );
  }

  Widget demoScreenRouter(DemoScreen which) {
    switch (which) {
      case DemoScreen.counter:
        return CounterDemo(counter: _counter);
      case DemoScreen.text:
        return TextFieldDemo(text: _text);
      case DemoScreen.dash:
        return DashDemo(text: _text);
    }
  }
}

FlutterView.tsx:

import React, { useEffect, useRef, memo } from 'react'
import CircularProgress from '@mui/material/CircularProgress'
import Box from '@mui/material/Box'
import { Button } from '@mui/material'

// The global _flutter namespace
declare var _flutter: any

const divStyle: React.CSSProperties = {
  height: '100%',
  width: '100%',
}

interface FlutterViewProps {
  assetBase?: string;
  src?: string;
  onClicksChange?: (clicks: number) => void;
  onScreenChange?: (screen: string) => void;
  onTextChange?: (text: string) => void;

  text: string;
  screen: string;
  clicks: number;
}

export const FlutterView: React.FC<FlutterViewProps> = memo(({
  assetBase = '',
  src = 'main.dart.js',
  onClicksChange,
  onScreenChange,
  onTextChange,
  text,
  screen,
  clicks,
}) => {
  const flutterState = useRef<any>(null)
  const ref = useRef<HTMLDivElement>(null)

  const onFlutterAppLoaded = (state: any) => {
    flutterState.current = state
    // listen to state changes
    state.onClicksChanged(onClicksChange)
    state.onTextChanged(onTextChange)
    state.onScreenChanged(onScreenChange)
    // set initial values
    state.setText(text)
    state.setScreen(screen)
    state.setClicks(clicks)
  }

  useEffect(() => {
    const target = ref.current
    let isRendered = true
    const initFlutterApp = async () => {
      if (!isRendered) return
      const engineInitializer = await new Promise<any>((resolve) => {
        console.log('setup Flutter engine initializer...')
        _flutter.loader.loadEntrypoint({
          entrypointUrl: src,
          onEntrypointLoaded: resolve,
        })
      })
      if (!isRendered) return

      console.log('initialize Flutter engine...')
      const appRunner = await engineInitializer?.initializeEngine({
        hostElement: target,
        assetBase: assetBase,
      })
      if (!isRendered) return

      console.log('run Flutter engine...')
      await appRunner?.runApp()
    }
    initFlutterApp()

    const eventListener = (event: Event) => {
      let state = (event as CustomEvent).detail
      onFlutterAppLoaded(state)
    }

    target?.addEventListener('flutter-initialized', eventListener, {
      once: true,
    })

    return () => {
      console.log(">>> cleaning up")
      isRendered = false
      target?.removeEventListener('flutter-initialized', eventListener)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  useEffect(() => {
    flutterState.current?.setText(text)
  }, [text])
  useEffect(() => {
    flutterState.current?.setScreen(screen)
  }, [screen])
  useEffect(() => {
    flutterState.current?.setClicks(clicks)
  }, [clicks])

  return (
    <div>
      <div
        ref={ref}
        style={{height: '500px', width: '300px'}}
      >
        <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
          <CircularProgress />
        </Box>
      </div>
    </div>
  )
})
p-mazhnik commented 1 month ago

Hi! Unfortunately, cleanup is not properly supported in the current Flutter version. Flutter web team added proper support for cleanup in the new multi-view scenario, but this scenario is not fully stable yet (although the functionality is already available in the latest Flutter version 3.22).

I have a PR where this new scenario is used for react app: https://github.com/p-mazhnik/flutter-embedding/pull/10, and I listed there some remaining pieces from the Flutter that are required to land the PR. The changes are also deployed here: https://p-mazhnik.github.io/flutter-embedding/react-multiview/

If I understood correctly, you want something like this: https://github.com/p-mazhnik/flutter-embedding/blob/fbabd30d97af170ae727ce4df287580fc74d74c6/cra-flutter/src/App/FlutterView/FlutterView.tsx#L66-L72 https://github.com/p-mazhnik/flutter-embedding/pull/10/files#diff-ea063f4ae88d7bc3ee95cd1ee2d4991a80c23bd36f01e4691193635a8a1e1d97R71

To summarize, you can try to update your code as in the PR, and see if it solves your issues. But keep in mind that some functionality might be broken