p-mazhnik / flutter-embedding

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

Multiple embedded views on the same page #15

Open arnleal09 opened 6 days ago

arnleal09 commented 6 days ago

I am using the example of rendering Flutter widgets in a React web page with NextJs. When I add two components on the same page, only one of the views renders. Using print, I can see that the flutter ViewCollection does indeed contain both views, and the target has two different elements with distinct IDs. However, the second view is always the one that gets displayed. There is any limitation to these use? I am using flutter version 3.24.2

<FlutterView
                    key={´1´}
                    className={´1´}
                    flutterApp={flutterApp}
                    onClicksChange={setClicks}
                    onScreenChange={setScreen}
                    onTextChange={setText}
                    removeView={() => removeView(1)}
                    text={text}
                    clicks={clicks}
                    screen={screen}
                  /> 

other elements

<FlutterView
                    key={´2´}
                    className={´2´}
                    flutterApp={flutterApp}
                    onClicksChange={setClicks}
                    onScreenChange={setScreen2}
                    onTextChange={setText}
                    removeView={() => removeView(2)}
                    text={text}
                    clicks={clicks}
                    screen={screen2}
                  />
<!DOCTYPE html>
<html>

<head>
  <!--
    If you are serving your web app in a path other than the root, change the
    href value below to reflect the base path you are serving from.

    The path provided below has to start and end with a slash "/" in order for
    it to work correctly.

    For more details:
    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

    This is a placeholder for base href that will be replaced by the value of
    the `--base-href` argument provided to `flutter build`.
  -->
  <script id="googleMapScript"
    src="https://maps.googleapis.com/maps/api/js?key=xxx"></script>
</head>
<base href="$FLUTTER_BASE_HREF">

<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">

<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="app">
<link rel="apple-touch-icon" href="icons/Icon-192.png">

<!-- Favicon -->
<link rel="icon" type="icons" href="favicon.ico" />

<title>plataforma_revendedora_mobile</title>
<link rel="manifest" href="manifest.json">
<script>
  // The value below is injected by flutter build, do not touch.
  var serviceWorkerVersion = '{{flutter_service_worker_version}}';
</script>
<script src="flutter.js" defer></script>
<style>
  html,
  body {
    height: 100%;
    padding: 0;
    margin: 0;
  }
</style>
</head>

<body>

  <script>
    // Listen until Flutter tells us it's ready to rumble
    window.addEventListener('flutter-initialized', function (event) {
      const state = event.detail;
      window['_debugCounter'] = state;
      state.onClicksChanged(() => {
        console.log('New clicks value: ', state.getClicks());
      });
    });
    window.addEventListener('load', function (ev) {
      // Download main.dart.js
      {{flutter_js}}
      {{flutter_build_config}}

      _flutter.loader.load({
        serviceWorker: {
          serviceWorkerVersion: serviceWorkerVersion,
        },
        onEntrypointLoaded: async function (engineInitializer) {
          engineInitializer.initializeEngine({
            multiViewEnabled: true,
          }).then(function (appRunner) {
            appRunner.runApp().then(function (flutterApp) {
              flutterApp.addView({ hostElement: document.querySelector("body") });
            });
          });
        }
      });
    });
  </script>
</body>

</html>

I am making the flutter init in this way and passing the context to the root of all components. When i do navigation for another page the widgets render ok, the only problem is when are two in the same page.

import React, { createContext, useContext, useState, useEffect } from 'react';
declare var _flutter: any;

interface FlutterAppContextProps { flutterApp: any; }

const FlutterAppContext = createContext<FlutterAppContextProps | undefined>(undefined);

export const useFlutterApp = () => {
    const context = useContext(FlutterAppContext);
    if (!context) { throw new Error('useFlutterApp must be used within a FlutterAppProvider'); } return context;
};
export const FlutterAppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
    const [flutterApp, setFlutterApp] = useState(null);
    useEffect(() => {
        const initializeFlutter = async () => {
            try {
                console.log('setup Flutter engine initializer...');
                const engineInitializer = await new Promise<any>((resolve, reject) => {
                    if (_flutter && _flutter.loader) {
                        console.log('flutter loaded value');
                        console.log(_flutter);
                        _flutter.loader.loadEntrypoint({ entrypointUrl: 'https://url/main.dart.js', onEntrypointLoaded: resolve, });
                    }
                    else { reject(new Error('_flutter.loader not available')); }
                });
                const appRunner = await engineInitializer?.initializeEngine({ assetBase: 'https://url/', multiViewEnabled: true, });
                console.log('Flutter initialized:', appRunner);
                const flutterAppInstance = await appRunner.runApp();
                setFlutterApp(flutterAppInstance); console.log('Flutter initialized:', flutterAppInstance);
            } catch (error) { console.error('Error initializing Flutter:', error); }
        };
        initializeFlutter();
    }, []);
    return (<FlutterAppContext.Provider value={{ flutterApp }}> {children} </FlutterAppContext.Provider>);
};
p-mazhnik commented 6 days ago

You don't actually use the index.html file you attached in the NextJs app, correct? I see flutter initialization in the file.

Also, do you have the complete code I can play with? It would be nice to have a minimal reproducible sample.

There shouldn't be limitations in displaying multiple views at the same time, but of course it could be possible that you've found a bug or something is wrong with your setup. I also have a deployed version of the react app from the repo where multiple views are displayed correctly: https://p-mazhnik.github.io/flutter-embedding/react/

arnleal09 commented 6 days ago

@p-mazhnik Hi, thanks for the quick reply. In the Next.js document, I call the flutter.js file. Unfortunately, I can’t share the full code because it’s not my property. I see that in your deployment it works fine, so could it be related to the use of the Next.js framework?

On the React side, I removed the ViewWrapper, but I don’t think that should be causing the problem, as at least one view is being rendered. I’m using simple views like a circular loading indicator or a container with a color, but it’s always the second view that gets displayed. If I change the second widget call to a container, it shows, but adding another view does the same only the last one is rendered. The build is in release mode, and I see that the example uses profile mode. I tried that as well, but I’m getting the same result.

class MyDocument extends Document {
  render() {
    return (
      <Html dir="ltr" lang={countryLanguage}>
        <Head>
          <link rel="preconnect" href="https://fonts.googleapis.com/" />
          <link rel="icon" href={`/${THEME}/favicon.ico`} sizes="32x32" type="image/x-icon" />
          <link rel="apple-touch-icon" href={`/${THEME}/favicon.ico`} />

          <script src="https://url/flutter.js" defer></script>
export default function mainPage({ Component, pageProps }) {

  return (
                  <FlutterAppProvider>
                    <Component {...pageProps} />
                  </FlutterAppProvider>    
  );
}
export default function LayoutMain({ flutterApp }: TypeProps) {
  const [screen, setScreen] = useState('container')
  const [screen2, setScreen2] = useState('loading')
  const [clicks, setClicks] = useState(0)
  const [text, setText] = useState('')

  return (
    <Box className={style.layout} data-testid="LayoutMain">
      <Box as="section" className={style.section}>
        <Box className={style.container}>

          <FlutterView
            key={'1'}
            className={'loading1'}
            flutterApp={flutterApp}
            onClicksChange={setClicks}
            onScreenChange={setScreen}
            onTextChange={setText}
            removeView={() => { }}
            text={text}
            clicks={clicks}
            screen={screen}
          />
        </Box>
      </Box>

      <Box className={style.container}>
          <FlutterView
            key={'2'}
            className={'loading2'}
            flutterApp={flutterApp}
            onClicksChange={setClicks}
            onScreenChange={setScreen2}
            onTextChange={setText}
            removeView={() => { }}
            text={text}
            clicks={clicks}
            screen={screen2}
          />
        </Box>
    </Box>
  )
}

unnamed

debug2
p-mazhnik commented 4 days ago

so could it be related to the use of the Next.js framework?

I don't think so, it is still React

I can’t share the full code because it’s not my property

In such case, you should provide a minimal reproducible sample, which you can create using a fresh Next JS app.

I just tried multiple FlutterView components in a fresh NextJS app. Everything is working as expected: https://github.com/p-mazhnik/flutter-embedding/tree/nextjs cd nextjs-flutter && yarn run prebuild && yarn run dev