endpoint_lens 0.1.0
endpoint_lens: ^0.1.0 copied to clipboard
A Flutter developer tool that reveals the API endpoints connected to UI elements using long press inspection.
example/lib/main.dart
import 'package:endpoint_lens/endpoint_lens.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'example_api.dart';
/// Example client, created in [main] after [ApiLens.init].
late ExampleApi exampleApi;
void main() {
ApiLens.init(
enabled: const bool.fromEnvironment('API_LENS', defaultValue: true),
);
exampleApi = ExampleApi();
runApp(const ExampleApp());
}
/// Host application for the three sample screens.
class ExampleApp extends StatelessWidget {
/// Creates the example app.
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
const seed = Color(0xFF0B6E4F);
return MaterialApp(
title: 'API Lens example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: seed),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: seed,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const ExampleShell(),
);
}
}
class ExampleShell extends StatefulWidget {
const ExampleShell({super.key});
@override
State<ExampleShell> createState() => _ExampleShellState();
}
class _ExampleShellState extends State<ExampleShell> {
int _index = 0;
static const _pages = <Widget>[
LoginScreen(),
ProductsScreen(),
ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Scaffold(
body: IndexedStack(index: _index, children: _pages),
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (index) => setState(() => _index = index),
destinations: const <Widget>[
NavigationDestination(icon: Icon(Icons.login), label: 'Login'),
NavigationDestination(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Products',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
label: 'Profile',
),
],
),
),
const ApiLensInspectorButton(bottom: 120),
],
);
}
}
/// Sign-in form backed by `POST /login`.
class LoginScreen extends StatefulWidget {
/// Creates the login screen.
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _email = TextEditingController(
text: 'demo.user@example.com',
);
final TextEditingController _password = TextEditingController();
String? _message;
bool _busy = false;
@override
void dispose() {
_email.dispose();
_password.dispose();
super.dispose();
}
Future<void> _submit() async {
setState(() {
_busy = true;
_message = null;
});
try {
await exampleApi.login(
email: _email.text.trim(),
password: _password.text,
);
if (!mounted) return;
setState(
() => _message = 'Signed in. Long press this card to see POST /login.',
);
} on DioException catch (error) {
if (!mounted) return;
final code = error.response?.statusCode;
setState(() => _message = 'Failed${code == null ? '' : ' ($code)'}.');
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: ListView(
padding: const EdgeInsets.all(20),
children: <Widget>[
const Text(
'Any password except "wrong" succeeds. The password is redacted in API Lens.',
),
const SizedBox(height: 16),
ApiLensWidget(
feature: 'login',
label: 'Login',
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: <Widget>[
TextField(
controller: _email,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 12),
TextField(
controller: _password,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
onSubmitted: (_) => _submit(),
),
const SizedBox(height: 16),
FilledButton(
onPressed: _busy ? null : _submit,
child: Text(_busy ? 'Signing in…' : 'Sign in'),
),
if (_message != null) ...<Widget>[
const SizedBox(height: 12),
Text(_message!),
],
],
),
),
),
),
],
),
);
}
}
/// Catalog backed by `GET /products`.
class ProductsScreen extends StatefulWidget {
/// Creates the products screen.
const ProductsScreen({super.key});
@override
State<ProductsScreen> createState() => _ProductsScreenState();
}
class _ProductsScreenState extends State<ProductsScreen> {
late Future<List<DemoProduct>> _products = exampleApi.loadProducts();
void _reload() {
setState(() => _products = exampleApi.loadProducts());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: <Widget>[
IconButton(
tooltip: 'Reload products',
onPressed: _reload,
icon: const Icon(Icons.refresh),
),
],
),
body: FutureBuilder<List<DemoProduct>>(
future: _products,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Center(child: Text('Could not load products.'));
}
final products = snapshot.data ?? const <DemoProduct>[];
return ApiLensWidget(
feature: 'products',
label: 'Products',
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: products.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final product = products[index];
return Card(
child: ListTile(
title: Text(product.name),
subtitle: const Text('GET /products'),
trailing: Text('\$${product.price}'),
),
);
},
),
);
},
),
);
}
}
/// Profile backed by the user, posts, and avatar endpoints.
class ProfileScreen extends StatefulWidget {
/// Creates the profile screen.
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
late Future<DemoProfile> _profile = exampleApi.loadProfile();
void _reload() {
setState(() => _profile = exampleApi.loadProfile());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
actions: <Widget>[
IconButton(
tooltip: 'Reload profile',
onPressed: _reload,
icon: const Icon(Icons.refresh),
),
],
),
body: FutureBuilder<DemoProfile>(
future: _profile,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Center(child: Text('Could not load the profile.'));
}
final profile = snapshot.data!;
return ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
ApiLensWidget(
feature: 'profile',
label: 'User Profile',
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
profile.name,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 4),
Text(profile.email),
const SizedBox(height: 16),
Text(
'Posts',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
for (final post in profile.posts)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(post.title),
),
],
),
),
),
),
],
);
},
),
);
}
}