gemini_flutter_kit 0.1.0 copy "gemini_flutter_kit: ^0.1.0" to clipboard
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.

example/lib/main.dart

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,
          ),
        );
      },
    );
  }
}
0
likes
150
points
37
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#gemini #ai #llm #chat #generative-ai

License

MIT (license)

Dependencies

connectivity_plus, flutter, flutter_secure_storage, google_generative_ai, gpt_markdown, shared_preferences

More

Packages that depend on gemini_flutter_kit