gemini_flutter_kit 0.1.0
gemini_flutter_kit: ^0.1.0 copied to clipboard
A production-ready toolkit for integrating Google Gemini AI into Flutter apps: streaming chat UI, rate limiting, retries, secure key storage, offline queueing, and usage tracking, out of the box.
import 'package:flutter/material.dart';
import 'package:gemini_flutter_kit/gemini_flutter_kit.dart';
void main() => runApp(const ExampleApp());
/// A full demo of `gemini_flutter_kit`: secure key entry, a streaming chat,
/// live usage tracking, and an offline-queue status banner.
class ExampleApp extends StatelessWidget {
/// Creates the demo app.
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'gemini_flutter_kit demo',
theme: ThemeData(
colorSchemeSeed: const Color(0xFF2563EB),
useMaterial3: true,
),
home: const _Gate(),
);
}
}
/// Decides whether to show the key-entry screen or the chat screen.
class _Gate extends StatefulWidget {
const _Gate();
@override
State<_Gate> createState() => _GateState();
}
class _GateState extends State<_Gate> {
final GeminiKeyManager _keys = GeminiKeyManager();
String? _apiKey;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
// Allow passing a key at build time: --dart-define=GEMINI_API_KEY=...
const fromEnv = String.fromEnvironment('GEMINI_API_KEY');
final key = fromEnv.isNotEmpty ? fromEnv : await _keys.getKey();
setState(() {
_apiKey = key;
_loading = false;
});
}
Future<void> _save(String key) async {
await _keys.saveKey(key);
setState(() => _apiKey = key);
}
Future<void> _signOut() async {
await _keys.clearKey();
setState(() => _apiKey = null);
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
final key = _apiKey;
if (key == null) return _ApiKeyScreen(onSubmit: _save);
return ChatScreen(apiKey: key, onSignOut: _signOut);
}
}
class _ApiKeyScreen extends StatefulWidget {
const _ApiKeyScreen({required this.onSubmit});
final ValueChanged<String> onSubmit;
@override
State<_ApiKeyScreen> createState() => _ApiKeyScreenState();
}
class _ApiKeyScreenState extends State<_ApiKeyScreen> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Enter Gemini API key')),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Get a free key from Google AI Studio '
'(aistudio.google.com/app/apikey) and paste it below. It is '
'stored securely in the platform keychain — never in code.',
),
const SizedBox(height: 16),
TextField(
controller: _controller,
obscureText: true,
decoration: const InputDecoration(
labelText: 'API key',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
final key = _controller.text.trim();
if (key.isNotEmpty) widget.onSubmit(key);
},
child: const Text('Continue'),
),
const SizedBox(height: 24),
Text(
GeminiDisclosureText.mediumDisclosure(appName: 'This demo'),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
}
}
/// The main chat screen with usage tracking and an offline banner.
class ChatScreen extends StatefulWidget {
/// Creates a [ChatScreen].
const ChatScreen({super.key, required this.apiKey, required this.onSignOut});
/// The Gemini API key to use.
final String apiKey;
/// Called when the user clears the stored key.
final VoidCallback onSignOut;
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
late final GeminiClient _client;
UsageSummary _usage = UsageSummary.empty;
@override
void initState() {
super.initState();
_client = GeminiClient(
apiKey: widget.apiKey,
offlineQueue: GeminiOfflineQueue(),
safetyGuard: GeminiSafetyGuard(
onReport: (id, reason) =>
debugPrint('Reported $id for: $reason'), // wire to your backend
),
);
_client.initialize();
_client.queuedResponses.listen((r) {
if (mounted && r.isSuccess) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Queued reply: ${r.text}')));
}
_refreshUsage();
});
_refreshUsage();
}
Future<void> _refreshUsage() async {
final usage = await _client.usageTracker.getSummary();
if (mounted) setState(() => _usage = usage);
}
@override
void dispose() {
_client.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Gemini Chat'),
actions: [
IconButton(
tooltip: 'Refresh usage',
icon: const Icon(Icons.refresh),
onPressed: _refreshUsage,
),
IconButton(
tooltip: 'Sign out',
icon: const Icon(Icons.logout),
onPressed: widget.onSignOut,
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(28),
child: _UsageBar(usage: _usage),
),
),
body: Column(
children: [
_OfflineBanner(queue: _client.offlineQueue!),
Expanded(
child: GeminiChatView(
client: _client,
theme: GeminiChatTheme.light,
onReportResponse: (id, reason) =>
debugPrint('UI reported $id: $reason'),
emptyState: const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text(
'Ask Gemini anything to get started.\n'
'Try: "Explain Flutter widgets simply."',
textAlign: TextAlign.center,
),
),
),
),
),
],
),
);
}
}
class _UsageBar extends StatelessWidget {
const _UsageBar({required this.usage});
final UsageSummary usage;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Text(
'Today: ${usage.tokensToday} tokens · ${usage.requestsToday} requests '
'| Month: ${usage.tokensThisMonth} tokens',
style: Theme.of(context).textTheme.bodySmall,
),
);
}
}
class _OfflineBanner extends StatelessWidget {
const _OfflineBanner({required this.queue});
final GeminiOfflineQueue queue;
@override
Widget build(BuildContext context) {
return StreamBuilder<QueueStatus>(
stream: queue.queueStatusStream,
builder: (context, snapshot) {
final status = snapshot.data;
if (status == null || (status.isOnline && status.pending == 0)) {
return const SizedBox.shrink();
}
return Container(
width: double.infinity,
color: status.isOnline
? Colors.blue.shade100
: Colors.orange.shade100,
padding: const EdgeInsets.all(8),
child: Text(
status.isOnline
? 'Reconnected — sending ${status.pending} queued message(s)…'
: 'You are offline. ${status.pending} message(s) queued.',
textAlign: TextAlign.center,
),
);
},
);
}
}