api_client_dio 0.1.0
api_client_dio: ^0.1.0 copied to clipboard
A simple, opinionated Dio wrapper for Flutter. Handles base URL, token injection, 401 refresh/redirect, logging, and timeouts out of the box. Framework-agnostic.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:api_client_dio/api_client_dio.dart';
/// Example TokenProvider using in-memory storage.
/// Replace with SharedPreferences / FlutterSecureStorage in real apps.
class InMemoryTokenProvider implements TokenProvider {
String? _accessToken;
String? _refreshToken;
@override
Future<String?> get accessToken async => _accessToken;
@override
Future<String?> get refreshToken async => _refreshToken;
@override
Future<void> onUnauthorized() async {
_accessToken = null;
_refreshToken = null;
print('[Auth] Token expired — user needs to login');
}
@override
Future<void> onTokenRefreshed({
required String accessToken,
String? refreshToken,
}) async {
_accessToken = accessToken;
_refreshToken = refreshToken;
print('[Auth] Token refreshed successfully');
}
void login(String token, {String? refresh}) {
_accessToken = token;
_refreshToken = refresh;
}
void logout() {
_accessToken = null;
_refreshToken = null;
}
}
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late final InMemoryTokenProvider _tokenProvider;
late final ApiClient _client;
String _status = 'Not started';
final _baseUrlController = TextEditingController(
text: 'https://jsonplaceholder.typicode.com',
);
final _emailController = TextEditingController(text: 'eve.holt@reqres.in');
final _passwordController = TextEditingController(text: 'cityslicka');
@override
void initState() {
super.initState();
_tokenProvider = InMemoryTokenProvider();
_client = ApiClient(
baseUrl: _baseUrlController.text,
tokenProvider: _tokenProvider,
enableLogging: true,
);
}
@override
void dispose() {
_baseUrlController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'API Client Demo',
theme: ThemeData(primarySwatch: Colors.teal),
home: Scaffold(
appBar: AppBar(title: const Text('API Client Demo')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _baseUrlController,
decoration: const InputDecoration(
labelText: 'Base URL',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _testGet,
icon: const Icon(Icons.download),
label: const Text('GET /posts/1'),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _testPost,
icon: const Icon(Icons.upload),
label: const Text('POST /posts'),
),
const SizedBox(height: 16),
const Text('Login Demo (reqres.in)',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: _testLogin,
icon: const Icon(Icons.login),
label: const Text('POST /api/login'),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_status,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
],
),
),
),
);
}
Future<void> _testGet() async {
try {
_setStatus('Loading...');
final response = await _client.get('/posts/1');
_setStatus('✅ GET /posts/1 → ${response.statusCode}\n'
'${response.data}');
} catch (e) {
_setStatus('❌ Error: $e');
}
}
Future<void> _testPost() async {
try {
_setStatus('Loading...');
final response = await _client.post('/posts', data: {
'title': 'API Client Dio',
'body': 'Testing the package',
'userId': 1,
});
_setStatus('✅ POST /posts → ${response.statusCode}\n'
'${response.data}');
} catch (e) {
_setStatus('❌ Error: $e');
}
}
Future<void> _testLogin() async {
try {
_client.updateBaseUrl('https://reqres.in');
_setStatus('Logging in...');
final response = await _client.post('/api/login', data: {
'email': _emailController.text,
'password': _passwordController.text,
});
final token = response.data['token'] as String?;
if (token != null) {
_tokenProvider.login(token);
_setStatus('✅ Login success!\nToken: $token');
} else {
_setStatus('⚠️ No token in response:\n${response.data}');
}
_client.updateBaseUrl(_baseUrlController.text);
} catch (e) {
_setStatus('❌ Login error: $e');
_client.updateBaseUrl(_baseUrlController.text);
}
}
void _setStatus(String msg) {
setState(() => _status = msg);
}
}