unified_game_services_google_play_android 0.2.0
unified_game_services_google_play_android: ^0.2.0 copied to clipboard
Native Google Play Games provider via the Play Games v2 Java SDK over package:jni (pure Dart, no Flutter). Android only; fires native achievement/leaderboard UI.
import 'dart:async';
import 'dart:ui' show PlatformDispatcher;
import 'package:flutter/material.dart';
import 'package:jni/jni.dart' show JObject;
import 'package:jni_flutter/jni_flutter.dart';
import 'package:unified_game_services_google_play_android/unified_game_services_google_play_android.dart';
import 'package:unified_game_services_platform_interface/unified_game_services_platform_interface.dart';
/// Minimal Flutter host for [GooglePlayAndroidProvider].
///
/// The provider package itself is pure Dart (no Flutter dependency). This app
/// only exists to satisfy the provider's two host requirements on a real
/// device:
/// 1. a running ART VM with `package:jni` bound to it (Flutter on Android
/// gives us this for free), and
/// 2. the current `Activity` jobject — obtained here with
/// `Jni.getCurrentActivity()` and passed into the provider.
///
/// The demo exercises the write path — the calls that fire the native Play
/// Games toasts/overlay: unlock / increment / reveal / submitScore — and the
/// read path: sign-in state, current player, achievements and scores.
///
/// Before it works on-device you must, in `android/`:
/// - set the real Play Games app id string resource (see AndroidManifest +
/// `app/build.gradle.kts`), and
/// - replace the placeholder achievement / leaderboard ids below with ids
/// from your Play Console game.
void main() => runApp(const DemoApp());
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'GPG Android (native) demo',
theme: ThemeData(colorSchemeSeed: Colors.green, useMaterial3: true),
home: const HomePage(),
);
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// Replace with ids from your Play Console game.
// Achievements created in the Play Console for this example.
static const _normalAchievementId = 'CgkI_4DWwZYFEAIQAQ';
static const _hiddenAchievementId = 'CgkI_4DWwZYFEAIQAg';
static const _incrementalAchievementId = 'CgkI_4DWwZYFEAIQAw'; // 10 steps
static const _leaderboardId = 'CgkI_REPLACE_ME_lb';
GooglePlayAndroidProvider? _provider;
StreamSubscription<GameServiceEvent>? _eventsSub;
final List<String> _log = [];
@override
void initState() {
super.initState();
_init();
}
void _init() {
try {
// The host Activity jobject — the one piece the provider can't get on
// its own. jni_flutter resolves it from the running Flutter engine. We
// pass a *resolver* (not the activity itself): it's volatile and must be
// fetched fresh, synchronously, before each native call.
JObject resolveActivity() =>
androidActivity(PlatformDispatcher.instance.engineId!)!;
// Probe once so misconfiguration (not on Android) fails clearly here.
if (androidActivity(PlatformDispatcher.instance.engineId!) == null) {
_append('init failed: no Android Activity (run on Android)');
return;
}
final provider = GooglePlayAndroidProvider(
activityResolver: resolveActivity,
);
_eventsSub = provider.events.listen(
(e) => _append('event: ${e.runtimeType}'),
);
_provider = provider;
_append('provider ready (caps: ${provider.capabilities})');
} catch (e) {
_append('init failed: $e');
}
}
void _append(String line) {
if (!mounted) return;
setState(() => _log.insert(0, line));
}
/// Runs [action], logging success or the mapped [GameServiceException].
Future<void> _run(String label, Future<void> Function() action) async {
final p = _provider;
if (p == null) {
_append('$label skipped — provider not initialized');
return;
}
try {
await action();
_append('$label ✓');
} on GameServiceException catch (e) {
_append('$label ✗ $e');
} catch (e) {
_append('$label ✗ $e');
}
}
@override
void dispose() {
_eventsSub?.cancel();
_provider?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final p = _provider;
return Scaffold(
appBar: AppBar(title: const Text('GPG native write demo')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton(
onPressed: p == null ? null : () => _run('signIn', p.signIn),
child: const Text('Sign in'),
),
FilledButton(
onPressed: p == null
? null
: () => _run(
'unlock',
() => p.unlockAchievement(_normalAchievementId),
),
child: const Text('Unlock normal'),
),
FilledButton(
onPressed: p == null
? null
: () => _run(
'increment',
() => p.incrementAchievement(
_incrementalAchievementId,
1,
),
),
child: const Text('Increment +1 (of 10)'),
),
FilledButton(
onPressed: p == null
? null
: () => _run(
'reveal',
() => p.revealAchievement(_hiddenAchievementId),
),
child: const Text('Reveal hidden'),
),
FilledButton(
onPressed: p == null
? null
: () => _run(
'submitScore',
() => p.submitScore(
leaderboardId: _leaderboardId,
score: 1234,
),
),
child: const Text('Submit score 1234'),
),
OutlinedButton(
onPressed: p == null
? null
: () => _run('isSignedIn', () async {
_append('isSignedIn = ${await p.isSignedIn()}');
}),
child: const Text('Is signed in?'),
),
OutlinedButton(
onPressed: p == null
? null
: () => _run('getCurrentPlayer', () async {
final me = await p.getCurrentPlayer();
_append('player = ${me?.id} "${me?.displayName}"');
}),
child: const Text('Current player'),
),
OutlinedButton(
onPressed: p == null
? null
: () => _run('getAchievements', () async {
final list = await p.getAchievements();
for (final a in list) {
_append(
' ${a.id} "${a.title}" unlocked=${a.isUnlocked} '
'hidden=${a.isHidden} '
'steps=${a.currentSteps}/${a.totalSteps} '
'at=${a.unlockedAt}',
);
}
_append('achievements: ${list.length}');
}),
child: const Text('Load achievements'),
),
OutlinedButton(
onPressed: p == null
? null
: () => _run('getLeaderboard', () async {
final board = await p.getLeaderboard(_leaderboardId);
for (final e in board.entries) {
_append(
' #${e.rank} ${e.player.displayName} '
'${e.displayScore}',
);
}
_append('leaderboard: ${board.entries.length}');
}),
child: const Text('Load leaderboard'),
),
OutlinedButton(
onPressed: p == null
? null
: () => _run('getPlayerScore', () async {
final e = await p.getPlayerScore(_leaderboardId);
_append(
e == null
? 'player score: none'
: 'player score: #${e.rank} ${e.displayScore}',
);
}),
child: const Text('My score'),
),
OutlinedButton(
onPressed: p == null
? null
: () => _run('signOut', p.signOut),
child: const Text('Sign out'),
),
FilledButton(
onPressed: p == null
? null
: () => _run('achievementsUi', p.showAchievementsUI),
child: const Text('Show achievements UI'),
),
FilledButton(
onPressed: p == null
? null
: () => _run('leaderboardsUi', p.showLeaderboardsUI),
child: const Text('Show leaderboards UI'),
),
],
),
),
const Divider(height: 1),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: _log.length,
itemBuilder: (_, i) => Text(
_log[i],
style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
),
),
),
],
),
);
}
}