EigenInteractive Flutter
The Flutter half of EigenInteractive: a server-authoritative engine for turn-based multiplayer games.
eigen_client is the pure Dart protocol, domain, clock, and live-session
runtime. eigen_flutter adds provider-neutral Flutter integration and reusable
presentation without owning a root app. The complete first-party product lives
in eigen_shell, and Firebase is an optional sibling adapter in
eigen_firebase. A game supplies a small GameModule containing its
client-side rules and presentation.
Start a game
The recommended flow creates the Cloudflare Worker and Flutter app together:
pnpm create eigen-game my-game
# or
npm create eigen-game@latest my-game
The scaffold installs published npm/pub.dev dependencies; it does not clone the engine repositories. For an existing app or separate Worker/app repositories, follow the manual setup guide.
Add to an app
dependencies:
eigen_flutter: ^0.9.0
eigen_shell: ^0.1.0 # omit when embedding beneath your own app root
eigen_firebase: ^0.2.0 # only when the app chooses Firebase
firebase_core: ^4.9.0
Import the framework through its public barrel:
import 'package:eigen_flutter/eigen_flutter.dart';
import 'package:eigen_firebase/eigen_firebase.dart';
import 'package:eigen_shell/eigen_shell.dart';
Do not depend on eigen_api directly or deep-import core/, features/, or
shared/. The barrel is the supported game-facing API.
On Android, the Firebase adapter's local-notification dependency requires
core-library desugaring in the application module. create-eigen-game
configures it automatically. For a hand-created app, add this to
android/app/build.gradle.kts:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
Boot the app with your module, branding, Worker origin, and generated Firebase configuration. The standard app targets Android and web, so the public Web Push key is required deployment configuration; it belongs to the same Firebase project used for authentication:
const apiBaseUrl = String.fromEnvironment('API_BASE_URL');
const googleWebClientId = String.fromEnvironment('GOOGLE_WEB_CLIENT_ID');
const firebaseVapidKey = String.fromEnvironment('FIREBASE_VAPID_KEY');
const appHost = String.fromEnvironment('APP_HOST');
Future<void> main() => runEigenShell(
module: const MyGameModule(),
config: AppConfig(
branding: const Branding(appName: 'My Game', seedColor: Colors.indigo),
engine: EngineConfig(
apiBaseUrl: apiBaseUrl,
appHost: appHost.isEmpty ? null : appHost,
),
),
initializeAdapter: () => initializeEigenFirebase(
firebaseOptions: DefaultFirebaseOptions.currentPlatform,
firebase: FirebaseAdapterConfig(
googleWebClientId: googleWebClientId,
vapidKey: firebaseVapidKey,
),
onBackgroundMessage: onBackgroundMessage,
telemetry: FirebaseTelemetryPolicy.releaseOnly(),
),
);
Without Firebase, omit eigen_firebase and call runEigenShell without an
initializer. To embed Eigen into an existing application, omit eigen_shell
too and install EigenFlutterScope beneath your own MaterialApp or
WidgetsApp. Auth, notification, and analytics ports default to
unavailable/no-op implementations; another adapter can override them through
package:eigen_flutter/adapters.dart.
These are public build-time values. Scaffolded apps keep them in
app-config.json and use the same command option for Android and web:
flutter run --dart-define-from-file=app-config.json
Missing or malformed required values are reported together before their
services start. Keep actual secrets on the Worker. Telemetry collection is off
unless the app passes an explicit policy such as releaseOnly().
Connect a game app to Firebase with the package executable:
dart run eigen_firebase:configure_firebase
It runs FlutterFire for Android and web, then derives
web/firebase-config.js for the messaging service worker from the Web app
FlutterFire selected. The public VAPID key remains in app-config.json because
it is not part of Firebase's app SDK configuration.
The game boundary
The authoritative TypeScript module declares state, observation, action,
and config once. It emits game-contract.json; the development-only
eigen_codegen package turns that artifact into immutable Dart payloads, a
typed rules base, and fixture copies:
flutter pub add --dev eigen_codegen
dart run eigen_codegen:generate_payloads \
--contract ../server/game-contract.json \
--output lib/game/generated/payloads.dart \
--fixtures-output test/fixtures
Your handwritten Dart code then:
- extends the generated
V<N>RulesBase; - implements client-side legality and optional optimistic preview;
- renders the game from
GameContentContext; - registers one rules unit per server
schemaVersion; - declares the version-independent creation and rules UI.
The server remains authoritative. Shared fixtures run against both languages so payload and behavior drift fails in tests.
Example
example/ is a complete Rock–Paper–Scissors client. It deliberately
uses simultaneous hidden commitments to demonstrate per-seat observations and
the valid “do not predict this move” path:
cd example
flutter pub get
flutter test
flutter build web --release
The package treats Android and web as supported targets. The generated scaffold includes the browser Firebase Messaging service worker, Firebase Auth's web popup flow, cross-origin Worker setup, and a release web build in CI. See Deploy the web app.
Documentation
- Quickstart
- The TypeScript + Dart game contract
- Payload generation
- Rendering a game
- Testing both halves
- Deploy the web app
- Dart API reference
- Versions and compatibility
Working on the framework
- CONTRIBUTING.md: local setup, generation, validation, changelog entries, and pull requests.
- MAINTAINERS.md: pub.dev setup, releases, version tags, and failure recovery.
Libraries
- eigen_flutter
- Reusable Flutter integration for EigenInteractive game applications.
- testing/twin_fixtures
- Twin-drift fixture runner: the Dart half of the shared JSON fixtures that keep a version unit's TS and Dart GameRules twins in sync.