preload flutter

This commit is contained in:
2026-03-08 09:36:43 +01:00
parent addd78d057
commit c39dcb6164
2 changed files with 142 additions and 84 deletions

View File

@@ -28,60 +28,52 @@ declare global {
}
}
type FlutterViewProps = {
state: FlutterTimelineState;
onEvent: (event: FlutterEvent) => void;
className?: string;
height?: number;
};
// ---------------------------------------------------------------------------
// Module-level singleton state
// ---------------------------------------------------------------------------
export function FlutterView({
state,
onEvent,
className,
height,
}: FlutterViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [status, setStatus] = useState<"loading" | "ready" | "error">(
"loading"
);
const stateRef = useRef(state);
stateRef.current = state;
let flutterContainer: HTMLDivElement | null = null;
let engineReady = false;
let initPromise: Promise<void> | null = null;
const onEventRef = useRef(onEvent);
onEventRef.current = onEvent;
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let pollingInterval: ReturnType<typeof setInterval> | undefined;
let pollingTimeout: ReturnType<typeof setTimeout> | undefined;
// Module-level refs so bridge closures always read latest values
let currentStateRef: FlutterTimelineState | null = null;
let currentOnEventRef: ((event: FlutterEvent) => void) | null = null;
function ensureBridge() {
if (window.__zendegi__) return;
window.__zendegi__ = {
getState: () => JSON.stringify(stateRef.current),
getState: () => JSON.stringify(currentStateRef),
onEvent: (json: string) => {
const event = JSON.parse(json) as FlutterEvent;
onEventRef.current(event);
currentOnEventRef?.(event);
},
};
}
const init = async () => {
export function preloadFlutter() {
initFlutter();
}
function initFlutter(): Promise<void> {
if (initPromise) return initPromise;
initPromise = (async () => {
ensureBridge();
// Load flutter.js if not already loaded
if (!window._flutter) {
await new Promise<void>((resolve, reject) => {
if (document.querySelector('script[src="/flutter/flutter.js"]')) {
// Script tag exists but hasn't finished — wait for _flutter to appear
pollingTimeout = setTimeout(() => {
clearInterval(pollingInterval);
const timeout = setTimeout(() => {
clearInterval(interval);
reject(
new Error("Timed out waiting for flutter.js to initialize")
new Error("Timed out waiting for flutter.js to initialize"),
);
}, 10_000);
pollingInterval = setInterval(() => {
const interval = setInterval(() => {
if (window._flutter) {
clearInterval(pollingInterval);
clearTimeout(pollingTimeout);
clearInterval(interval);
clearTimeout(timeout);
resolve();
}
}, 50);
@@ -105,7 +97,13 @@ export function FlutterView({
window._flutter!.buildConfig = await res.json();
}
try {
// Create the persistent container once
if (!flutterContainer) {
flutterContainer = document.createElement("div");
flutterContainer.style.width = "100%";
flutterContainer.style.height = "100%";
}
await window._flutter!.loader.load({
config: {
entrypointBaseUrl: "/flutter/",
@@ -113,27 +111,81 @@ export function FlutterView({
},
onEntrypointLoaded: async (engineInitializer) => {
const appRunner = await engineInitializer.initializeEngine({
hostElement: container,
hostElement: flutterContainer!,
assetBase: "/flutter/",
});
appRunner.runApp();
setStatus("ready");
},
});
} catch (error) {
throw new Error(
`Flutter engine initialization failed: ${error instanceof Error ? error.message : error}`
);
}
};
init().catch(() => setStatus("error"));
engineReady = true;
})().catch((error) => {
// Reset all module state so next mount retries
flutterContainer = null;
engineReady = false;
initPromise = null;
throw error;
});
return initPromise;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
type FlutterViewProps = {
state: FlutterTimelineState;
onEvent: (event: FlutterEvent) => void;
className?: string;
height?: number;
};
export function FlutterView({
state,
onEvent,
className,
height,
}: FlutterViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [status, setStatus] = useState<"loading" | "ready" | "error">(
engineReady ? "ready" : "loading",
);
// Keep module-level refs in sync
currentStateRef = state;
currentOnEventRef = onEvent;
useEffect(() => {
const wrapper = containerRef.current;
if (!wrapper) return;
if (engineReady && flutterContainer) {
// Engine already running — reparent immediately, no loading flash
wrapper.appendChild(flutterContainer);
setStatus("ready");
// Push latest state so Flutter renders the correct timeline
window.__zendegi__?.updateState?.(JSON.stringify(currentStateRef));
} else {
// First mount — initialize engine
setStatus("loading");
initFlutter()
.then(() => {
// Guard: component may have unmounted while awaiting
if (!containerRef.current || !flutterContainer) return;
wrapper.appendChild(flutterContainer);
setStatus("ready");
// Push current state after engine is ready
window.__zendegi__?.updateState?.(JSON.stringify(currentStateRef));
})
.catch(() => setStatus("error"));
}
return () => {
clearInterval(pollingInterval);
clearTimeout(pollingTimeout);
container.replaceChildren();
delete (window as Partial<Window>).__zendegi__;
// Detach but don't destroy the Flutter container
if (flutterContainer?.parentElement === wrapper) {
wrapper.removeChild(flutterContainer);
}
};
}, []);

View File

@@ -1,6 +1,8 @@
import { useEffect } from "react";
import { createFileRoute, Link } from "@tanstack/react-router";
import { useSuspenseQuery } from "@tanstack/react-query";
import { timelinesQueryOptions } from "@/functions/get-timelines";
import { preloadFlutter } from "@/components/flutter-view";
export const Route = createFileRoute("/_default/timelines")({
loader: async ({ context }) => {
@@ -11,6 +13,10 @@ export const Route = createFileRoute("/_default/timelines")({
function RouteComponent() {
const timelinesQuery = useSuspenseQuery(timelinesQueryOptions());
useEffect(() => {
preloadFlutter();
}, []);
return (
<section className="container mx-auto max-w-3xl px-4 py-2">
<h1 className="text-4xl font-serif font-bold mb-6">Timelines</h1>