api_provider 2.0.0
api_provider: ^2.0.0 copied to clipboard
A thin, testable layer over Dio for Flutter apps - bearer-token auth, automatic token refresh with retry, secure token storage, and friendly error handling.
example/lib/main.dart
import 'package:api_provider/api_provider.dart';
import 'package:flutter/material.dart';
final ApiProvider api = ApiProvider(
baseUrl: 'https://jsonplaceholder.typicode.com/',
// Called on a 401. Swap in your refresh endpoint, e.g.
// final res = await api.post('auth/refresh', data: {...}, options: skipAuth());
// return res.data['token'];
// Returning null means "can't refresh": the user is signed out.
refreshTokenProvider: RefreshTokenProvider.from(() async => null),
onUnauthorized: () => debugPrint('Session expired - show the login screen'),
);
/// One class per feature, sharing the ApiProvider's client and token.
class PostsProvider extends BaseProvider {
PostsProvider() : super(httpClient: api.baseApiClient, name: 'Posts');
Future<List<dynamic>> latest() async {
// jsonplaceholder is public, so skipAuth(). Your own endpoints would not need it.
final res = await get<List<dynamic>>('posts',
queryParameters: {'_limit': 5}, options: skipAuth());
return res.data ?? const [];
}
}
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const PostsPage(),
);
}
class PostsPage extends StatefulWidget {
const PostsPage({super.key});
@override
State<PostsPage> createState() => _PostsPageState();
}
class _PostsPageState extends State<PostsPage> {
final _posts = PostsProvider();
List<dynamic> _items = const [];
String? _error;
bool _loading = false;
@override
void initState() {
super.initState();
api.hasAuthToken(); // restore a saved login; the app bar updates by itself
_load();
}
Future<void> _load() async {
setState(() {
_loading = true;
_error = null;
});
try {
_items = await _posts.latest();
} on DioException catch (e) {
_error = e.userMessage; // friendly text for timeouts, offline, 4xx/5xx...
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _toggleLogin() async {
if (api.isAuthenticated) {
await api.deleteAuthToken();
} else {
await api.persistToken(token: 'demo-token', isLocalSaved: true);
}
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('api_provider example'),
actions: [
// Rebuilds on sign-in, sign-out and token refresh.
ValueListenableBuilder<String?>(
valueListenable: api.authTokenListenable,
builder: (context, token, _) => TextButton(
onPressed: _toggleLogin,
child: Text(token != null ? 'Sign out' : 'Sign in')),
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text(_error!))
: RefreshIndicator(
onRefresh: _load,
child: ListView(
children: [
for (final p in _items)
ListTile(
title: Text('${p['title']}'),
subtitle: Text('${p['body']}'))
],
),
),
);
}