cached_builder 1.0.1
cached_builder: ^1.0.1 copied to clipboard
A smart FutureBuilder replacement with built-in caching, retry, and stale-while-revalidate strategies. Integrated with connectivity_plus_wrapper for smart network awareness.
import 'package:flutter/material.dart';
import 'package:cached_builder/cached_builder.dart';
import 'package:connectivity_plus_wrapper/connectivity_plus_wrapper.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late ConnectivityWrapper connectivityWrapper;
bool _isInitialized = false;
int _testScenario = 0;
@override
void initState() {
super.initState();
_initialize();
}
Future<void> _initialize() async {
connectivityWrapper = ConnectivityWrapper();
setState(() => _isInitialized = true);
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return const MaterialApp(
home: Scaffold(
body: Center(child: CircularProgressIndicator()),
),
);
}
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Cached Builder Test'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => setState(() {}),
),
PopupMenuButton<int>(
onSelected: (value) => setState(() => _testScenario = value),
itemBuilder: (context) => [
const PopupMenuItem(value: 0, child: Text('Normal Data')),
const PopupMenuItem(value: 1, child: Text('Slow Network (3s)')),
const PopupMenuItem(value: 2, child: Text('Error Simulation')),
const PopupMenuItem(value: 3, child: Text('Empty Data')),
],
),
],
),
body: _buildTestBody(),
),
);
}
Widget _buildTestBody() {
return Column(
children: [
// Test Controls
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
const Text('Test Scenarios:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('Current: ${_getScenarioName(_testScenario)}'),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildTestButton(0, 'Normal'),
_buildTestButton(1, 'Slow'),
_buildTestButton(2, 'Error'),
_buildTestButton(3, 'Empty'),
],
),
],
),
),
),
// CachedBuilder Test
Expanded(
child: _buildCachedBuilderTest(),
),
],
);
}
Widget _buildTestButton(int scenario, String label) {
return ElevatedButton(
onPressed: () => setState(() => _testScenario = scenario),
style: ElevatedButton.styleFrom(
backgroundColor: _testScenario == scenario ? Colors.blue : null,
),
child: Text(label),
);
}
Widget _buildCachedBuilderTest() {
switch (_testScenario) {
case 0:
return _buildNormalTest();
case 1:
return _buildSlowTest();
case 2:
return _buildErrorTest();
case 3:
return _buildEmptyTest();
default:
return _buildNormalTest();
}
}
Widget _buildNormalTest() {
return CachedBuilder<List<Map<String, dynamic>>>(
k: 'normal-test',
future: _fetchNormalData,
connectivityWrapper: connectivityWrapper,
builder: (posts, isFromCache) {
return Column(
children: [
if (isFromCache)
Container(
padding: const EdgeInsets.all(8),
color: Colors.green[100],
child: Row(
children: [
const Icon(Icons.cached, size: 16, color: Colors.green),
const SizedBox(width: 8),
const Text('✅ Showing CACHED Data'),
const Spacer(),
Text('Items: ${posts.length}'),
],
),
),
Expanded(
child: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text('${post['title']} ${isFromCache ? '(Cached)' : ''}'),
subtitle: Text(post['body']),
leading: Icon(isFromCache ? Icons.cached : Icons.new_releases),
);
},
),
),
],
);
},
loading: const Center(child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading normal data...'),
],
)),
);
}
Widget _buildSlowTest() {
return CachedBuilder<List<Map<String, dynamic>>>(
k: 'slow-test',
future: _fetchSlowData,
connectivityWrapper: connectivityWrapper,
builder: (posts, isFromCache) {
return Column(
children: [
if (isFromCache)
Container(
padding: const EdgeInsets.all(8),
color: Colors.orange[100],
child: const Row(
children: [
Icon(Icons.timer, size: 16, color: Colors.orange),
SizedBox(width: 8),
Text('⏳ Showing cached slow data'),
],
),
),
Expanded(
child: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text('${post['title']} (Slow)'),
subtitle: Text(post['body']),
leading: const Icon(Icons.timer),
);
},
),
),
],
);
},
loading: const Center(child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading slow data (3 seconds)...'),
],
)),
);
}
Widget _buildErrorTest() {
return CachedBuilder<List<Map<String, dynamic>>>(
k: 'error-test',
future: _fetchErrorData,
connectivityWrapper: connectivityWrapper,
builder: (posts, isFromCache) {
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post['title']),
subtitle: Text(post['body']),
);
},
);
},
loading: const Center(child: CircularProgressIndicator()),
error: (error, retry, status) => Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Error: ${error.toString()}',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
if (status != null)
Text(
'Connection: ${status.description}',
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: retry,
child: const Text('Try Again'),
),
],
),
),
),
);
}
Widget _buildEmptyTest() {
return CachedBuilder<List<Map<String, dynamic>>>(
k: 'empty-test',
future: _fetchEmptyData,
connectivityWrapper: connectivityWrapper,
builder: (posts, isFromCache) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.inbox_outlined, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
isFromCache ? 'No cached data available' : 'No data found',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 8),
const Text('Try refreshing or check your connection'),
],
),
);
},
loading: const Center(child: CircularProgressIndicator()),
);
}
// Data fetching methods
Future<List<Map<String, dynamic>>> _fetchNormalData() async {
await Future.delayed(const Duration(milliseconds: 500)); // Simulate network
return [
{'id': 1, 'title': 'Normal Post 1', 'body': 'This is normal data'},
{'id': 2, 'title': 'Normal Post 2', 'body': 'This is also normal data'},
{'id': 3, 'title': 'Normal Post 3', 'body': 'More normal content here'},
{'id': 4, 'title': 'Normal Post 4', 'body': 'More normal content here'},
{'id': 5, 'title': 'Normal Post 5', 'body': 'More normal content here'},
{'id': 6, 'title': 'Normal Post 6', 'body': 'More normal content here'},
{'id': 7, 'title': 'Normal Post 7', 'body': 'More normal content here'},
{'id': 8, 'title': 'Normal Post 8', 'body': 'More normal content here'},
{'id': 9, 'title': 'Normal Post 9', 'body': 'More normal content here'},
{'id': 10, 'title': 'Normal Post 10', 'body': 'More normal content here'},
];
}
Future<List<Map<String, dynamic>>> _fetchSlowData() async {
await Future.delayed(const Duration(seconds: 3)); // Simulate slow network
return [
{'id': 1, 'title': 'Slow Post 1', 'body': 'This took 3 seconds to load'},
{'id': 2, 'title': 'Slow Post 2', 'body': 'Very slow connection simulation'},
];
}
Future<List<Map<String, dynamic>>> _fetchErrorData() async {
await Future.delayed(const Duration(seconds: 1));
throw Exception('Failed to load data: Server returned 500 error');
}
Future<List<Map<String, dynamic>>> _fetchEmptyData() async {
await Future.delayed(const Duration(seconds: 1));
return []; // Empty list
}
String _getScenarioName(int scenario) {
switch (scenario) {
case 0: return 'Normal Data';
case 1: return 'Slow Network';
case 2: return 'Error Simulation';
case 3: return 'Empty Data';
default: return 'Unknown';
}
}
}