API Keys
Loading preview
Starting the Flutter app
A Flutter API keys screen with reveal, copy, create and revoke actions.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
const _months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
class _Key {
final String name;
final String prefix;
final String secret;
final String created;
bool revealed;
_Key(
this.name,
this.prefix,
this.secret,
this.created, {
this.revealed = false,
});
String get value => '${prefix}_$secret';
String get masked => '$prefix${'•' * 16}';
}
class ApiKeys extends StatefulWidget {
/// Called with the name of a key after it's added to the list.
final ValueChanged<String>? onCreate;
/// Called with a key's name after it's removed from the list.
final ValueChanged<String>? onRevoke;
const ApiKeys({super.key, this.onCreate, this.onRevoke});
@override
State<ApiKeys> createState() => _ApiKeysState();
}
class _ApiKeysState extends State<ApiKeys> {
final _keys = [
_Key('Production', 'sk_live_9f2a', 'q7Lm2xVt8RbK4zNc', 'Jan 12, 2026'),
_Key('Staging', 'sk_test_4b71', 'Hd3Yw9PsG6jT1fUe', 'Mar 3, 2026'),
_Key('CI pipeline', 'sk_ci_0c58', 'Zr5Ao1EkM8vC2nXi', 'May 21, 2026'),
];
void _showMessage(String message) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
Future<void> _copy(_Key key) async {
await Clipboard.setData(ClipboardData(text: key.value));
if (!mounted) return;
_showMessage('${key.name} key copied');
}
/// Demo only: a real key is generated by your server, not on the device.
_Key _generateKey(String name) {
const chars = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
final random = Random.secure();
String pick(int length) => List.generate(
length,
(_) => chars[random.nextInt(chars.length)],
).join();
final now = DateTime.now();
return _Key(
name,
'sk_demo_${pick(4)}',
pick(16),
'${_months[now.month - 1]} ${now.day}, ${now.year}',
revealed: true,
);
}
Future<void> _create() async {
final name = await showDialog<String>(
context: context,
builder: (context) =>
_CreateKeyDialog(existing: _keys.map((k) => k.name).toSet()),
);
if (name == null || !mounted) return;
setState(() => _keys.add(_generateKey(name)));
widget.onCreate?.call(name);
_showMessage('$name key created');
}
Future<void> _revoke(_Key key) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Revoke ${key.name}?'),
content: const Text('Apps using this key will stop working.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Revoke'),
),
],
),
);
if (confirmed != true || !mounted) return;
setState(() => _keys.remove(key));
widget.onRevoke?.call(key.name);
_showMessage('${key.name} key revoked');
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('API Keys')),
floatingActionButton: FloatingActionButton.extended(
onPressed: _create,
icon: const Icon(Icons.add),
label: const Text('Create key'),
),
body: _keys.isEmpty
? const Center(child: Text('No API keys'))
: ListView.builder(
// Bottom padding keeps the last Revoke button clear of the FAB.
padding: const EdgeInsets.fromLTRB(16, 16, 16, 88),
itemCount: _keys.length,
itemBuilder: (context, i) {
final k = _keys[i];
return Card(
key: ValueKey(k),
margin: const EdgeInsets.symmetric(vertical: 6),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Wrap, so the date drops to its own line when both don't fit.
Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 12,
runSpacing: 4,
children: [
Text(
k.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'Created ${k.created}',
style: TextStyle(
fontSize: 12,
color: scheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
color: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Expanded(
child: Text(
k.revealed ? k.value : k.masked,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13,
),
),
),
IconButton(
tooltip: k.revealed ? 'Hide key' : 'Show key',
visualDensity: VisualDensity.compact,
onPressed: () =>
setState(() => k.revealed = !k.revealed),
icon: Icon(
k.revealed
? Icons.visibility_off
: Icons.visibility,
),
),
IconButton(
tooltip: 'Copy key',
visualDensity: VisualDensity.compact,
onPressed: () => _copy(k),
icon: const Icon(Icons.copy),
),
],
),
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () => _revoke(k),
style: TextButton.styleFrom(
foregroundColor: scheme.error,
),
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Revoke'),
),
),
],
),
),
);
},
),
);
}
}
class _CreateKeyDialog extends StatefulWidget {
final Set<String> existing;
const _CreateKeyDialog({required this.existing});
@override
State<_CreateKeyDialog> createState() => _CreateKeyDialogState();
}
class _CreateKeyDialogState extends State<_CreateKeyDialog> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
String? _validate(String? value) {
final name = value?.trim() ?? '';
if (name.isEmpty) return 'Enter a name';
final taken = widget.existing.any(
(k) => k.toLowerCase() == name.toLowerCase(),
);
return taken ? 'A key with this name exists' : null;
}
void _submit() {
if (!_formKey.currentState!.validate()) return;
Navigator.pop(context, _nameController.text.trim());
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Create API key'),
content: Form(
key: _formKey,
child: TextFormField(
controller: _nameController,
autofocus: true,
decoration: const InputDecoration(
labelText: 'Key name',
border: OutlineInputBorder(),
),
validator: _validate,
onFieldSubmitted: (_) => _submit(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(onPressed: _submit, child: const Text('Create')),
],
);
}
}
Add it to your project
Copies the Dart source into your app, so you own it. Flutter SDK only, no extra packages.
First time? Run dart pub global activate fluttertemplates_dev, or read the CLI guide.
How it works
Each _Key carries a mutable revealed flag toggled by the eye icon, plus a sample secret, so value gives the full key and masked the bulleted form. The copy button always copies value, even while hidden. Create key opens _CreateKeyDialog, which rejects blank and duplicate names, then _generateKey adds a revealed demo key. Revoke confirms before removing the entry. Both are in-memory, and onCreate and onRevoke report the key’s name afterwards.
Adapting it
- Keep only
prefixclient-side; fetch the secret once at creation, since real keys shouldn’t be re-readable. - Replace
_generateKeywith your API, which should create the secret and show it once. - Call your API from
onRevoke, or before_keys.removeif failures must keep the entry.