unified_game_services
One Dart API for game services โ achievements, leaderboards, stats, cloud save, profiles and presence โ across many providers (Steam, Epic, GameJolt, and your own custom backends), with no dependency on Flutter.
Write your integration once and publish to several platforms at the same time. Pure Dart, so it runs in Flutter apps, CLIs, servers and game engines alike.
Features
- ๐ฎ Unified API: achievements, leaderboards, stats, cloud save, friends, presence.
- ๐ Pluggable providers โ target several at once.
- ๐งฉ Multi-provider fan-out: one
submitScorewrites to every capable provider. - ๐ ๏ธ Bring your own backend (Supabase, Firebase, RESTโฆ) as a custom provider.
- ๐ชถ Pure Dart โ no Flutter dependency.
- ๐งช Capability-gated, with a typed error hierarchy and an event stream.
Install
dependencies:
unified_game_services: ^0.1.0
# plus the provider(s) you need:
unified_game_services_steam: ^0.1.0
unified_game_services_gamejolt: ^0.1.0
This package re-exports the shared models, capabilities, events and exceptions,
so a single import is enough.
Provider setup differs. Pure-REST providers like GameJolt are zero-config (just credentials). Native providers like Steam need a one-time native-library setup that pub.flutter-io.cn cannot deliver for you (Valve's SDK can't be redistributed) โ run
dart run unified_game_services_steam:setupand see the Steam package README.
Usage
import 'package:unified_game_services/unified_game_services.dart';
import 'package:unified_game_services_steam/unified_game_services_steam.dart';
import 'package:unified_game_services_gamejolt/unified_game_services_gamejolt.dart';
final services = UnifiedGameServices(providers: [
SteamProvider(appId: 480),
GameJoltProvider(
gameId: 'โฆ', privateKey: 'โฆ', username: 'โฆ', userToken: 'โฆ',
),
]);
await services.signIn();
// Writes fan out to every provider that supports the capability:
if (services.supports(GameCapability.achievements)) {
await services.unlockAchievement('first_win'); // Steam + GameJolt
}
await services.submitScore(leaderboardId: 'global', score: 1500);
// Reads come from the primary provider (the first one), or pick with `from:`:
final achievements = await services.getAchievements();
final board = await services.getLeaderboard('global');
// Listen to events from all providers:
services.events.listen(print);
Single-provider mode: call UnifiedGameServices() with no arguments to use the
provider registered via UnifiedGameServicesPlatform.instance.
Capabilities
Each provider advertises what it supports; gate calls with supports:
if (services.supports(GameCapability.cloudSave)) {
await services.saveData('profile', bytes);
}
GameCapability covers achievements, leaderboards, stats, cloudSave,
friends, presence, multiplayer. A fan-out to a capability no provider
supports throws CapabilityNotSupportedException; partial failures throw
AggregateGameServiceException.
Provider-specific features
Some providers expose extras beyond the unified API. Reach them with the typed accessor:
final gj = services.provider<GameJoltProvider>();
await gj?.startSessionHeartbeat();
Custom providers
Back the API with your own database/service by extending
UnifiedGameServicesPlatform. See
example/custom_provider.dart for a complete
template (illustrated with Supabase).
Per-store builds (Steam-only, Epic-only, โฆ)
Want a Steam-only build and an Epic-only build โ and don't care about Google
Play in the Steam one? You don't need Flutter flavors. This is a federated
plugin: the facade only fans out to the providers you register at runtime, so
"Steam-only" just means "register only SteamProvider". A provider you never
construct never runs.
The one thing that genuinely differs per store is the native runtime lib.
Steam/Epic/GameCenter reach the OS over FFI, so the host ships a platform lib
(steam_api64.dll/libsteam_api.dylib + steam_appid.txt;
EOSSDK-Win64-Shipping.dll/libEOSSDK-Mac-Shipping.dylib). These are large,
license-gated, and not cross-redistributable โ that, not the Dart code, is the
real reason to split builds. Google Play is REST (no native lib), so leaving it
out of a Steam build costs nothing: just don't register it.
Strategies, cheapest first:
- Runtime switch โ one binary, pick the store via
--define:dart run --define=STORE=epic example/per_store_builds.dart. Simplest; unused Dart deps still ship. Fine for dev. const bool.fromEnvironmentguards โ one entry point, but the FFI providers are tree-shaken out of builds that don't opt in. Aconstflag makes the branch a compile-time constant; unset โ const-falseโ the compiler proves the provider unreachable and drops its wholedart:fficlosure:const kEnableSteam = bool.fromEnvironment('ENABLE_STEAM'); if (kEnableSteam) providers.add(SteamProvider(appId: 480)); // only referencedart compile exe โฆ --define=ENABLE_STEAM=trueincludes it; a plainflutter build appbundle(flag unset) removes it. Reference each FFI provider only inside its guard โ a single reference outside re-anchors the symbol and defeats the shake. This is the closest thing to "automatic per- platform" (Dart has no OS-conditional import), verified: toggling the flags changed a compiled binary by ~260 KB of FFI closure.- Entry points โ
bin/main_steam.dart+bin/main_epic.dart, each importing only its provider. Dart tree-shaking drops the other's code. Recommended for pure-Dart/CLI targets; package the matching native lib per build. - Flutter flavors / separate app packages โ real dependency + asset separation. Use when a Flutter app must bundle only one store's native lib + credentials per variant.
Examples: example/per_store_builds.dart
(strategy 1, doubles as a strategy-3 entry point) and
example/treeshake_per_store.dart
(strategy 2, single entry point + tree-shaking).
Steam/Epic are desktop-only
The FFI providers (Steam, Epic) run only on desktop (Windows/macOS/Linux) โ never mobile or web. You don't have to guard them manually:
- Web โ each FFI package exports an inert stub (
if (dart.library.io)), so importing it never breaksflutter build web/dart compile js; constructing it throwsUnsupportedError. - Mobile (Android/iOS) โ
dart:fficompiles, but the constructor throwsUnsupportedError(the native runtime lib isn't there). The native libs are never auto-bundled โ nothing ships to a mobile/web build automatically.
The stub/runtime guards keep bad builds from crashing cryptically, but a runtime
gate does not tree-shake โ the FFI Dart is still compiled into a mobile
bundle as dead code. Dart has no OS-conditional import to drop it automatically;
to exclude it, make the symbol unreachable at compile time with a
const bool.fromEnvironment guard (strategy 2 above โ the closest to automatic)
or a per-platform entry point that never imports Steam/Epic (strategy 3/4).
Respect third-party redistributables. The Steamworks / EOS runtime libs are license-gated and not cross-redistributable โ ship each only under your own Steam/Epic agreement, bundle it only in the matching desktop build, and never commit it to source control.
Flutter desktop flavors (Windows/macOS/Linux)
If your app is a Flutter desktop app wanting a Steam build and an Epic
build from one codebase, know that Flutter's --flavor flag is not first-class
on Windows/Linux the way it is on Android (Gradle product flavors) or
iOS/macOS (Xcode schemes + .xcconfig). Recommended setup per platform:
- macOS โ real flavor support: add an Xcode scheme +
.xcconfigper store (mirrors the iOS flavor recipe), then bundle the matching native lib (libsteam_api.dylib/libEOSSDK-Mac-Shipping.dylib) into that scheme'sResourcesvia an Xcode "Copy Files" build phase, gated per configuration. - Windows/Linux โ no native flavor mechanism. Use strategy 3 from above
instead: one
lib/main_steam.dart/lib/main_epic.dartentry point per store, built withflutter build windows --target=lib/main_steam.dart(same forlinux). Copy the matching native lib next to the built executable as a post-build step in your CI/release script (steam_api64.dll+steam_appid.txtalongsideRunner.exe;EOSSDK-Win64-Shipping.dlllikewise) โ Flutter's build output doesn't know about these, so this step is always manual regardless of flavor tooling. - CI matrix โ the cleanest way to produce all store variants is a CI job
matrix over
{store} ร {os}, each running the platform-appropriate build above and attaching the correct native lib as a release asset. Don't try to ship every store's native lib in a single "universal" build โ that's exactly the redistribution problem called out above.
Related packages
| Package | Role | Platforms |
|---|---|---|
unified_game_services |
This package โ the app-facing facade. | all |
unified_game_services_platform_interface |
Shared contract + models (for provider authors). | all |
unified_game_services_steam |
Steam provider (Steamworks via FFI). | desktop |
unified_game_services_epic |
Epic Online Services provider (EOS C SDK via FFI). | desktop |
unified_game_services_game_center |
Apple Game Center provider (GameKit via Objective-C FFI). | macOS, iOS |
unified_game_services_gamejolt |
GameJolt provider (REST). | all |
unified_game_services_playfab |
PlayFab provider (REST). | all |
unified_game_services_google_play_rest |
Google Play Games via REST โ cross-platform, achievements + leaderboards. | all |
unified_game_services_google_play_android |
Google Play Games via the on-device Play Games v2 Java SDK (package:jni) โ native write UI. |
Android |
unified_game_services_google_play |
Auto-selects native on Android, REST elsewhere (incl. web). Pure Dart. | all |
unified_game_services_google_play_flutter |
Flutter adapter for the above โ auto-resolves the Android Activity, zero wiring. |
all (Flutter apps) |
unified_game_services_xbox_pc |
Xbox on PC (GDK) โ research placeholder, not implemented yet, not published. | โ |
Picking a Google Play tier:
unified_game_services_google_play_restalone โ you don't need native write UI at all (e.g. a CLI, server, or an engine that only reads/writes over REST). No native dependency, works everywhere.unified_game_services_google_play_flutterโ you're a Flutter app and want zero wiring: it auto-resolves the AndroidActivityfor you (viajni_flutter) and falls back to REST off Android. The one Flutter-only package in the family โ pulls influtter+jni_flutter(needs the Flutter SDK onPATHformelos bootstrap, seeCLAUDE.md).unified_game_services_google_playdirectly โ same auto-selecting behavior (native on Android, REST elsewhere), but you supply theactivityResolver. Use this from any host, Flutter or not, that either (a) isn't Flutter and has its own way to obtain the AndroidActivityjobject, or (b) is Flutter but wants to avoid thejni_flutterdependency (and its Flutter-SDK-on-PATHbootstrap requirement) and resolve theActivitysome other way.unified_game_services_google_play_androiddirectly โ you want the native tier only, Android-exclusive, with no REST fallback at all (e.g. an Android-only game engine host).
Troubleshooting & further reading
Each provider's own README covers its setup gotchas in depth โ check there first if a provider misbehaves:
- Steam: native lib not found /
steam_appid.txtmissing โ seeunified_game_services_steam's README setup section; requires a running Steam client for most calls. - Epic:
EOS_InvalidRequest(1012) on sign-in โ missing Epic Account Services scopes in the Dev Portal; seeunified_game_services_epic's README "Host responsibilities" section. - Google Play:
401/token refresh loops โ checkaccess_type=offline+prompt=consenton the OAuth strategy; seeunified_game_services_google_play_rest's README auth section. - Any provider: catch
GameServiceExceptionsubtypes (NotSignedIn,SignInFailed,CapabilityNotSupported,Network,PlatformOperation) rather than a barecatch (e)โ they tell you which layer failed.
License
MIT โ see the LICENSE file.
This is an independent, unofficial library, not affiliated with or endorsed by
any platform vendor. Third-party credits, trademark notices, and SDK
redistribution terms are in the repository's NOTICE.md.
Libraries
- unified_game_services
- Unified, multi-platform game services API for Dart.