dartquery 1.0.0 copy "dartquery: ^1.0.0" to clipboard
dartquery: ^1.0.0 copied to clipboard

A reactive in-memory data management library for Dart and Flutter with intelligent caching, similar to React Query.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:dartquery/dartquery.dart';
import 'dart:async';
import 'dart:math';

// Simulated API service
class ApiService {
  static final Random _random = Random();

  static Future<Map<String, dynamic>> fetchUser(int id) async {
    await Future.delayed(Duration(seconds: 1));
    if (_random.nextInt(10) < 2) {
      throw 'Network error: Failed to fetch user';
    }
    return {
      'id': id,
      'name': 'User $id',
      'email': 'user$id@example.com',
      'avatar': 'https://i.pravatar.cc/150?u=$id',
      'bio': 'This is the bio for user $id. ' * 10, // Large data
    };
  }

  static Future<List<Map<String, dynamic>>> fetchPosts(int page) async {
    await Future.delayed(Duration(milliseconds: 800));
    return List.generate(
        20,
        (i) => {
              'id': page * 20 + i,
              'title': 'Post ${page * 20 + i}',
              'content':
                  'Content for post ${page * 20 + i}. ' * 50, // Large content
              'author': 'User ${_random.nextInt(100)}',
              'likes': _random.nextInt(1000),
              'comments': _random.nextInt(100),
            });
  }

  static Future<Map<String, dynamic>> fetchStats() async {
    await Future.delayed(Duration(milliseconds: 500));
    return {
      'totalUsers': _random.nextInt(10000),
      'totalPosts': _random.nextInt(50000),
      'activeUsers': _random.nextInt(1000),
      'timestamp': DateTime.now().toIso8601String(),
    };
  }
}

void main() {
  // Configure cache for the example app
  final cacheConfig = CacheConfig(
    maxQueries: 50, // Limit to 50 queries
    maxMemoryBytes: 10 * 1024 * 1024, // 10MB memory limit
    evictionPolicy: EvictionPolicy.lru, // Use LRU eviction
    enableMemoryPressureHandling: true,
    cleanupInterval: Duration(minutes: 1),
  );

  runApp(MyApp(cacheConfig: cacheConfig));
}

class MyApp extends StatelessWidget {
  final CacheConfig cacheConfig;

  const MyApp({Key? key, required this.cacheConfig}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'DartQuery Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: QueryProvider(
        client: QueryClient.withConfig(cacheConfig),
        child: HomePage(),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _selectedIndex = 0;

  final List<Widget> _pages = [
    CacheManagementDemo(),
    UserProfilesDemo(),
    InfinitePostsDemo(),
    LiveStatsDemo(),
  ];

  final List<String> _titles = [
    'Cache Management',
    'User Profiles',
    'Infinite Posts',
    'Live Stats',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('DartQuery: ${_titles[_selectedIndex]}'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: _pages[_selectedIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
        destinations: [
          NavigationDestination(
            icon: Icon(Icons.storage),
            label: 'Cache',
          ),
          NavigationDestination(
            icon: Icon(Icons.person),
            label: 'Users',
          ),
          NavigationDestination(
            icon: Icon(Icons.list),
            label: 'Posts',
          ),
          NavigationDestination(
            icon: Icon(Icons.analytics),
            label: 'Stats',
          ),
        ],
      ),
    );
  }
}

// Cache Management Demo
class CacheManagementDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final client = QueryProvider.of(context);

    return Padding(
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Cache Statistics',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          SizedBox(height: 16),
          StreamBuilder(
            stream: Stream.periodic(Duration(seconds: 1)),
            builder: (context, snapshot) {
              final stats = client.getCacheStats();
              final isNearLimit = client.isCacheNearLimit();

              return Card(
                color: isNearLimit ? Colors.orange.shade50 : null,
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: Column(
                    children: [
                      _StatRow('Queries in Cache', '${stats.queryCount}'),
                      _StatRow('Memory Usage',
                          '${(stats.memoryBytes / 1024 / 1024).toStringAsFixed(2)} MB'),
                      _StatRow('Cache Hit Ratio',
                          '${(stats.hitRatio * 100).toStringAsFixed(1)}%'),
                      _StatRow('Total Hits', '${stats.hits}'),
                      _StatRow('Total Misses', '${stats.misses}'),
                      _StatRow('Evictions', '${stats.evictions}'),
                      _StatRow('Expirations', '${stats.expirations}'),
                      if (isNearLimit)
                        Padding(
                          padding: EdgeInsets.only(top: 8),
                          child: Text(
                            '⚠️ Cache is approaching size limits',
                            style: TextStyle(color: Colors.orange.shade700),
                          ),
                        ),
                    ],
                  ),
                ),
              );
            },
          ),
          SizedBox(height: 16),
          Text(
            'Cache Actions',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  client.cleanup();
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Cache cleanup completed')),
                  );
                },
                icon: Icon(Icons.cleaning_services),
                label: Text('Force Cleanup'),
              ),
              ElevatedButton.icon(
                onPressed: () {
                  // Simulate memory pressure
                  MemoryPressureHandler.instance.triggerMemoryPressure();
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Memory pressure triggered')),
                  );
                },
                icon: Icon(Icons.memory),
                label: Text('Simulate Memory Pressure'),
              ),
              ElevatedButton.icon(
                onPressed: () {
                  client.clear();
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Cache cleared')),
                  );
                },
                icon: Icon(Icons.clear),
                label: Text('Clear All'),
                style: ElevatedButton.styleFrom(
                  foregroundColor: Colors.red,
                ),
              ),
            ],
          ),
          SizedBox(height: 16),
          Text(
            'Memory Pressure Info',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          SizedBox(height: 8),
          StreamBuilder(
            stream: Stream.periodic(Duration(seconds: 2)),
            builder: (context, snapshot) {
              final info =
                  MemoryPressureHandler.instance.getMemoryPressureInfo();

              return Card(
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: Column(
                    children: [
                      _StatRow('Total Memory',
                          '${info.memoryMB.toStringAsFixed(2)} MB'),
                      _StatRow('Cache Managers', '${info.totalCacheManagers}'),
                      _StatRow('Total Queries', '${info.totalQueries}'),
                      _StatRow('Under Pressure',
                          info.isUnderPressure ? 'Yes' : 'No'),
                    ],
                  ),
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

// User Profiles Demo
class UserProfilesDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      padding: EdgeInsets.all(8),
      itemCount: 20,
      itemBuilder: (context, index) {
        final userId = index + 1;

        return Card(
          margin: EdgeInsets.symmetric(vertical: 4),
          child: QueryBuilder<Map<String, dynamic>>(
            queryKey: 'user-$userId',
            fetcher: () => ApiService.fetchUser(userId),
            staleTime: Duration(minutes: 5),
            builder: (context, query) {
              if (query.isLoading) {
                return ListTile(
                  leading: CircleAvatar(
                    child: CircularProgressIndicator(strokeWidth: 2),
                  ),
                  title: Text('Loading...'),
                );
              }

              if (query.isError) {
                return ListTile(
                  leading: CircleAvatar(
                    backgroundColor: Colors.red,
                    child: Icon(Icons.error, color: Colors.white),
                  ),
                  title: Text('Error loading user'),
                  subtitle: Text(query.error.toString()),
                  trailing: IconButton(
                    icon: Icon(Icons.refresh),
                    onPressed: () {
                      DartQuery.instance.invalidate('user-$userId');
                    },
                  ),
                );
              }

              final user = query.data!;
              return ListTile(
                leading: CircleAvatar(
                  backgroundImage: NetworkImage(user['avatar']),
                ),
                title: Text(user['name']),
                subtitle: Text(user['email']),
                trailing: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(
                      query.isStale ? Icons.access_time : Icons.check_circle,
                      color: query.isStale ? Colors.orange : Colors.green,
                      size: 16,
                    ),
                    Text(
                      query.isStale ? 'Stale' : 'Fresh',
                      style: TextStyle(fontSize: 10),
                    ),
                  ],
                ),
                onTap: () {
                  showDialog(
                    context: context,
                    builder: (_) => AlertDialog(
                      title: Text(user['name']),
                      content: SingleChildScrollView(
                        child: Text(user['bio']),
                      ),
                      actions: [
                        TextButton(
                          onPressed: () => Navigator.pop(context),
                          child: Text('Close'),
                        ),
                      ],
                    ),
                  );
                },
              );
            },
          ),
        );
      },
    );
  }
}

// Infinite Posts Demo
class InfinitePostsDemo extends StatefulWidget {
  @override
  _InfinitePostsDemoState createState() => _InfinitePostsDemoState();
}

class _InfinitePostsDemoState extends State<InfinitePostsDemo> {
  final ScrollController _scrollController = ScrollController();
  int _currentPage = 1;
  List<Map<String, dynamic>> _allPosts = [];
  bool _isLoadingMore = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  void _onScroll() {
    if (_scrollController.position.pixels >=
        _scrollController.position.maxScrollExtent - 200) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    if (_isLoadingMore) return;

    setState(() {
      _isLoadingMore = true;
    });

    try {
      final newPosts = await DartQuery.instance.fetch(
        'posts-page-$_currentPage',
        () => ApiService.fetchPosts(_currentPage),
        staleTime: Duration(minutes: 10),
      );

      setState(() {
        _allPosts.addAll(newPosts);
        _currentPage++;
        _isLoadingMore = false;
      });
    } catch (e) {
      setState(() {
        _isLoadingMore = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return QueryBuilder<List<Map<String, dynamic>>>(
      queryKey: 'posts-page-1',
      fetcher: () => ApiService.fetchPosts(1),
      builder: (context, query) {
        if (query.isLoading && _allPosts.isEmpty) {
          return Center(child: CircularProgressIndicator());
        }

        if (query.isError && _allPosts.isEmpty) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.error_outline, size: 64, color: Colors.red),
                SizedBox(height: 16),
                Text('Failed to load posts'),
                SizedBox(height: 8),
                ElevatedButton(
                  onPressed: () {
                    DartQuery.instance.invalidate('posts-page-1');
                  },
                  child: Text('Retry'),
                ),
              ],
            ),
          );
        }

        if (query.data != null && _allPosts.isEmpty) {
          _allPosts = List.from(query.data!);
        }

        return RefreshIndicator(
          onRefresh: () async {
            setState(() {
              _currentPage = 1;
              _allPosts.clear();
            });
            DartQuery.instance.invalidate('posts-page-1');
          },
          child: ListView.builder(
            controller: _scrollController,
            padding: EdgeInsets.all(8),
            itemCount: _allPosts.length + (_isLoadingMore ? 1 : 0),
            itemBuilder: (context, index) {
              if (index == _allPosts.length) {
                return Center(
                  child: Padding(
                    padding: EdgeInsets.all(16),
                    child: CircularProgressIndicator(),
                  ),
                );
              }

              final post = _allPosts[index];
              return Card(
                margin: EdgeInsets.symmetric(vertical: 4),
                child: ListTile(
                  title: Text(
                    post['title'],
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  subtitle: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      SizedBox(height: 4),
                      Text(
                        post['content'],
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                      SizedBox(height: 8),
                      Row(
                        children: [
                          Icon(Icons.person, size: 16),
                          SizedBox(width: 4),
                          Text(post['author']),
                          SizedBox(width: 16),
                          Icon(Icons.favorite, size: 16, color: Colors.red),
                          SizedBox(width: 4),
                          Text('${post['likes']}'),
                          SizedBox(width: 16),
                          Icon(Icons.comment, size: 16, color: Colors.blue),
                          SizedBox(width: 4),
                          Text('${post['comments']}'),
                        ],
                      ),
                    ],
                  ),
                ),
              );
            },
          ),
        );
      },
    );
  }
}

// Live Stats Demo
class LiveStatsDemo extends StatefulWidget {
  @override
  _LiveStatsDemoState createState() => _LiveStatsDemoState();
}

class _LiveStatsDemoState extends State<LiveStatsDemo> {
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    // Auto-refresh stats every 5 seconds
    _timer = Timer.periodic(Duration(seconds: 5), (_) {
      DartQuery.instance.invalidate('live-stats');
    });
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: QueryBuilder<Map<String, dynamic>>(
          queryKey: 'live-stats',
          fetcher: () => ApiService.fetchStats(),
          staleTime: Duration(seconds: 5),
          builder: (context, query) {
            if (query.isLoading && query.data == null) {
              return CircularProgressIndicator();
            }

            final stats = query.data ?? {};
            final isRefreshing = query.isLoading && query.data != null;

            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Live Statistics',
                  style: Theme.of(context).textTheme.headlineLarge,
                ),
                SizedBox(height: 8),
                if (isRefreshing) LinearProgressIndicator(),
                SizedBox(height: 24),
                _StatCard(
                  icon: Icons.people,
                  label: 'Total Users',
                  value: '${stats['totalUsers'] ?? 0}',
                  color: Colors.blue,
                ),
                SizedBox(height: 16),
                _StatCard(
                  icon: Icons.article,
                  label: 'Total Posts',
                  value: '${stats['totalPosts'] ?? 0}',
                  color: Colors.green,
                ),
                SizedBox(height: 16),
                _StatCard(
                  icon: Icons.online_prediction,
                  label: 'Active Users',
                  value: '${stats['activeUsers'] ?? 0}',
                  color: Colors.orange,
                ),
                SizedBox(height: 24),
                Text(
                  'Last updated: ${_formatTimestamp(stats['timestamp'])}',
                  style: Theme.of(context).textTheme.bodySmall,
                ),
                SizedBox(height: 16),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(
                      query.isStale ? Icons.access_time : Icons.check_circle,
                      color: query.isStale ? Colors.orange : Colors.green,
                    ),
                    SizedBox(width: 8),
                    Text(
                      query.isStale ? 'Data is stale' : 'Data is fresh',
                      style: TextStyle(
                        color: query.isStale ? Colors.orange : Colors.green,
                      ),
                    ),
                  ],
                ),
              ],
            );
          },
        ),
      ),
    );
  }

  String _formatTimestamp(String? timestamp) {
    if (timestamp == null) return 'Never';
    try {
      final dt = DateTime.parse(timestamp);
      final now = DateTime.now();
      final diff = now.difference(dt);
      if (diff.inSeconds < 60) {
        return '${diff.inSeconds}s ago';
      } else if (diff.inMinutes < 60) {
        return '${diff.inMinutes}m ago';
      } else {
        return '${diff.inHours}h ago';
      }
    } catch (e) {
      return 'Invalid';
    }
  }
}

// Utility Widgets
class _StatRow extends StatelessWidget {
  final String label;
  final String value;

  const _StatRow(this.label, this.value);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Text(
            value,
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
        ],
      ),
    );
  }
}

class _StatCard extends StatelessWidget {
  final IconData icon;
  final String label;
  final String value;
  final Color color;

  const _StatCard({
    required this.icon,
    required this.label,
    required this.value,
    required this.color,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: EdgeInsets.all(24),
        child: Row(
          children: [
            Container(
              padding: EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: color.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(12),
              ),
              child: Icon(icon, color: color, size: 32),
            ),
            SizedBox(width: 24),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    label,
                    style: Theme.of(context).textTheme.bodySmall,
                  ),
                  Text(
                    value,
                    style: Theme.of(context).textTheme.headlineMedium,
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
150
points
26
downloads

Publisher

unverified uploader

Weekly Downloads

A reactive in-memory data management library for Dart and Flutter with intelligent caching, similar to React Query.

Topics

#cache #state-management #reactive #flutter #data-fetching

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on dartquery