How we achieved 225x faster API calls with intelligent caching and zero memory leaks
As Flutter developers, we've all been there: wrestling with Dio interceptors, implementing cache logic from scratch, managing multiple API endpoints, and dealing with authentication token refresh flows. After building dozens of Flutter apps, I decided enough was enough β it was time to create a solution that handles all of this elegantly.
Today, I'm excited to share ApiClientPlus, a production-ready HTTP client that makes API management in Flutter actually enjoyable.
π― The Problem: API Management is Hard
Before we dive into the solution, let's talk about what's broken. Here are the pain points I've encountered in nearly every Flutter project:
1. Cache Management is Tedious
// The old way: Manual cache hell
final cachedData = await _storage.get(key);
if (cachedData != null && !_isExpired(cachedData)) {
return cachedData;
}
try {
final response = await dio.get(url);
await _storage.set(key, response);
return response;
} catch (e) {
if (cachedData != null) {
return cachedData; // Fallback
}
rethrow;
}
This is boilerplate hell. And we're only scratching the surface β what about different cache strategies? TTL management? Cache invalidation?
2. Multiple APIs = Multiple Headaches
// Managing different API clients manually
final authDio = Dio(BaseOptions(baseUrl: 'https://auth.api.com'));
final publicDio = Dio(BaseOptions(baseUrl: 'https://public.api.com'));
final cdnDio = Dio(BaseOptions(baseUrl: 'https://cdn.api.com'));
// Which one do I use? How do I switch? Where's my token?
3. Authentication Token Refresh
// The token refresh dance
dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
final newToken = await refreshToken();
// Retry request...
// Handle edge cases...
// Hope nothing breaks...
}
},
));
Sound familiar? There had to be a better way.
π The Solution: ApiClientPlus
After months of refinement and battle-testing in production apps, ApiClientPlus emerged as a comprehensive solution that handles all of this and more.
Key Features at a Glance
β
6 intelligent cache strategies with automatic expiration
β
Multi-API management with seamless switching
β
Token authentication with auto-refresh
β
12ms average overhead (blazing fast!)
β
Zero memory leaks (tested extensively)
β
Production-ready with comprehensive error handling
π‘ How It Works
1. One-Time Setup (It's Actually Simple)
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ApiClientPlus().initialize(
configs: [
ApiConfig(
name: 'api',
baseUrl: 'https://api.example.com',
requiresAuth: true,
),
],
defaultDomain: 'api',
cacheConfig: CacheConfig(
enableCache: true,
defaultTtl: Duration(minutes: 10),
),
tokenGetter: () async => await getToken(),
);
runApp(MyApp());
}
That's it. One initialization, and you're done.
2. Making API Calls (The Easy Part)
// Simple GET with intelligent caching
final response = await ApiClientService.get(
'/users',
useCache: true,
cacheStrategy: ApiClientCacheStrategy.cacheFirst,
);
// POST with automatic authentication
final user = await ApiClientService.post(
'/users',
data: {'name': 'John Doe'},
);
Notice what's not there? No manual cache checks. No token handling. No boilerplate. Just clean, readable code.
π¨ The Magic: Cache Strategies
This is where ApiClientPlus really shines. Instead of implementing cache logic from scratch every time, choose from six battle-tested strategies:
1. Cache First (Default Smart Choice)
final data = await ApiClientService.get(
'/dashboard',
cacheStrategy: ApiClientCacheStrategy.cacheFirst,
);
How it works: Checks cache first, falls back to network if needed.
Best for: General-purpose API calls, user profiles, app settings.
2. Network First (Fresh Data Priority)
final posts = await ApiClientService.get(
'/posts',
cacheStrategy: ApiClientCacheStrategy.networkFirst,
);
How it works: Tries network first, uses cache as fallback.
Best for: Feeds, news, content that changes frequently.
3. Stale While Revalidate (Performance King π)
This is my favorite. Here's why:
final products = await ApiClientService.get(
'/products',
cacheStrategy: ApiClientCacheStrategy.staleWhileRevalidate,
);
How it works: Returns cached data instantly, refreshes in background.
Result: Your users see data in 2ms instead of 450ms, while fresh data loads silently.
Real-world impact:
- β‘ Instant perceived performance
- π― Happy users (no loading spinners!)
- π Always fresh data (updates in background)
4. Cache Then Network (Progressive Enhancement)
final dashboard = await ApiClientService.get(
'/dashboard',
cacheStrategy: ApiClientCacheStrategy.cacheThenNetwork,
onCachedResponse: (cached) {
// Show cached data immediately
updateUI(cached.data);
},
);
// Fresh data arrives, UI updates again
Best for: Complex dashboards, progressive loading UX.
5. Cache Only (Offline Mode)
final saved = await ApiClientService.get(
'/saved-articles',
cacheStrategy: ApiClientCacheStrategy.cacheOnly,
);
Best for: Offline-first apps, saved content.
6. Network Only (Real-Time Data)
final livePrice = await ApiClientService.get(
'/stock-price',
cacheStrategy: ApiClientCacheStrategy.networkOnly,
);
Best for: Live data, real-time updates, financial apps.
π The Numbers: Performance That Matters
We didn't just build this β we obsessively tested it. Here's what we found:
Benchmark Results
Operation Time Result
βββββββββββββββββββββββββββββββββββββββββββββ
API Call Overhead ~12ms β
Excellent
Cache Retrieval ~2ms β‘ Instant
Route Matching ~600ΞΌs π Blazing
Plugin Init ~13ms ποΈ Quick
Memory Leaks 0 growth π‘οΈ Solid
Real-World Cache Impact
π First Request (Network): 450ms
β‘ Second Request (Cache): 2ms
Result: 225x FASTER! π
This isn't theoretical β this is what happens in production, on real devices, with real users.
Load Testing
We pushed it hard:
- β 100 sequential calls: 1.1 seconds (11ms avg)
- β 50 domain switches: 549ms (10.98ms avg)
- β 1000 route matches: 627ms (0.6ms avg)
- β Zero memory leaks after 20+ create/dispose cycles
π’ Managing Multiple APIs (Finally Easy!)
One of my favorite features is multi-API management. Here's how it works:
await ApiClientPlus().initialize(
configs: [
ApiConfig(
name: 'auth',
baseUrl: 'https://auth.company.com',
requiresAuth: true,
),
ApiConfig(
name: 'public',
baseUrl: 'https://api.company.com',
requiresAuth: false,
),
ApiConfig(
name: 'cdn',
baseUrl: 'https://cdn.company.com',
),
],
defaultDomain: 'auth',
);
Now switching between APIs is trivial:
// User authentication
final profile = await ApiClientService.get(
'/me',
domainName: 'auth',
);
// Public content
final posts = await ApiClientService.get(
'/posts',
domainName: 'public',
);
// CDN assets
final image = await ApiClientService.get(
'/avatar.png',
domainName: 'cdn',
);
Each API can have its own configuration, timeouts, headers, and authentication requirements. No more juggling multiple Dio instances!
π Authentication Made Simple
Token management and refresh flows are notoriously tricky. ApiClientPlus handles it elegantly:
await ApiClientPlus().initialize(
configs: [...],
tokenGetter: () async {
// Your token retrieval logic
return await SecureStorage.read('auth_token');
},
onTokenInvalid: () async {
// Handle expired tokens
await SecureStorage.delete('auth_token');
Navigator.pushReplacementNamed(context, '/login');
},
);
Now when a 401 occurs:
-
onTokenInvalidis called automatically - You handle the logout/refresh logic
- User is redirected appropriately
No interceptor spaghetti code required.
π Debugging: Know What's Happening
Development without good logging is like flying blind. ApiClientPlus gives you comprehensive visibility:
LogConfig(
showLog: true,
showCacheLog: true,
logLevel: "DEBUG",
prettyJson: true,
isColored: true,
)
Sample output:
INFO πΎ Cache Strategy: cache_first for /users
INFO πΎ β
CACHE HIT: /users (age: 2m 34s)
INFO π₯ Response: 2ms | 200 | from_cache: true
INFO π Cache Strategy: network_first for /posts
INFO π Making network request...
INFO π₯ Response: 156ms | 200 | from_cache: false
INFO πΎ β
Cached: /posts (ttl: 10m)
You know exactly what's happening, when, and why. No more guessing.
π― Real-World Example
Let's build a simple app that fetches and displays posts:
import 'package:flutter/material.dart';
import 'package:api_client_plus/api_client_plus.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ApiClientPlus().initialize(
configs: [
ApiConfig(
name: 'dummyjson',
baseUrl: 'https://dummyjson.com',
),
],
defaultDomain: 'dummyjson',
cacheConfig: CacheConfig(
enableCache: true,
defaultTtl: Duration(minutes: 5),
),
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: PostsScreen(),
);
}
}
class PostsScreen extends StatefulWidget {
@override
_PostsScreenState createState() => _PostsScreenState();
}
class _PostsScreenState extends State<PostsScreen> {
List<dynamic> posts = [];
bool loading = true;
@override
void initState() {
super.initState();
fetchPosts();
}
Future<void> fetchPosts() async {
try {
final response = await ApiClientService.get(
'/posts',
useCache: true,
cacheStrategy: ApiClientCacheStrategy.staleWhileRevalidate,
);
setState(() {
posts = response.data['posts'];
loading = false;
});
} catch (e) {
setState(() => loading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
@override
Widget build(BuildContext context) {
if (loading) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(title: Text('Posts')),
body: RefreshIndicator(
onRefresh: fetchPosts,
child: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post['title']),
subtitle: Text(post['body']),
);
},
),
),
);
}
}
With staleWhileRevalidate, this app:
- Shows cached posts instantly on subsequent opens
- Refreshes data in the background
- Provides smooth pull-to-refresh
- Handles errors gracefully
All in ~80 lines of code!
π οΈ Advanced Features
Custom Headers & Query Parameters
final response = await ApiClientService.get(
'/users',
headers: {
'X-Custom-Header': 'value',
'X-Request-ID': generateUUID(),
},
query: {
'page': 1,
'limit': 20,
'sort': 'desc',
},
);
Timeouts & Retries
ApiConfig(
name: 'api',
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 30),
receiveTimeout: Duration(seconds: 30),
maxRetries: 3, // Auto-retry failed requests
)
Cache Fallback on Errors
CacheConfig(
hitCacheOnNetworkFailure: true,
hitCacheOnErrorCodes: [500, 502, 503, 401, 403],
)
When these error codes occur, ApiClientPlus automatically falls back to cached data if available. Your app stays functional even when the backend is down!
π Lessons Learned
Building ApiClientPlus taught me several valuable lessons:
1. Performance Matters More Than You Think
Users notice the difference between 450ms and 2ms. That's the difference between a "loading" state and instant content. Intelligent caching isn't just a nice-to-have β it's essential for great UX.
2. Flexibility > Opinionated
I initially built this with one cache strategy in mind. But different screens need different strategies. A news feed needs fresh data. A user profile can be stale. Settings can be cached aggressively. Six strategies emerged from real needs.
3. Testing Prevents Disasters
The extensive performance tests caught memory leaks early. The route matching tests prevented production bugs. If you're building infrastructure code like this, test obsessively.
4. Developer Experience is King
The best library is one developers actually want to use. Clean APIs, good docs, and helpful error messages matter more than features.
π Getting Started
Ready to try it? Installation is simple:
dependencies:
api_client_plus: ^1.0.0
Then run:
flutter pub get
Check out the full documentation and GitHub repo for more examples and guides.
π€ Community & Contribution
ApiClientPlus is open source and built for the Flutter community. If you:
- π Find a bug
- π‘ Have a feature idea
- π Want to improve docs
- β Just want to show support
Head over to GitHub and get involved!
π― What's Next?
We're not stopping here. The roadmap includes:
- GraphQL support for modern APIs
- Request/response transformers for data mapping
- Offline queue for failed requests
- Analytics integration for monitoring
- Mock mode for testing
Have ideas? Open an issue on GitHub!
π Final Thoughts
Building apps shouldn't mean fighting with infrastructure. API management, caching, authentication β these are solved problems. ApiClientPlus lets you focus on what matters: building great features for your users.
The 225x performance improvement isn't magic. It's just smart caching, done right.
Give it a try in your next Flutter project. I think you'll love it.
π Resources
- Package: pub.dev/packages/api_client_plus
- Source: GitHub Repository
- Issues: Report Bugs
If this article helped you, please give it a π₯ and follow for more Flutter content!
Have questions? Drop them in the comments below. I read and respond to every one.
Tags: #Flutter #Dart #MobileDevelopment #API #Performance #OpenSource #Cache #HTTP
Top comments (0)