ttl_etag_cache 1.0.4 copy "ttl_etag_cache: ^1.0.4" to clipboard
ttl_etag_cache: ^1.0.4 copied to clipboard

A powerful, reactive caching solution for Flutter with TTL, ETag support, and optional AES-256 encryption for offline-first applications.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:ttl_etag_cache/ttl_etag_cache.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize cache with encryption
  await TtlEtagCache.init(enableEncryption: true);

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Cache Demo',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cache Examples')),
      body: ListView(
        children: [
          ListTile(
            title: const Text('User Profile'),
            subtitle: const Text('Simple cached user data'),
            trailing: const Icon(Icons.arrow_forward_ios),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const UserProfileScreen(userId: '1'),
                ),
              );
            },
          ),
          ListTile(
            title: const Text('Posts List'),
            subtitle: const Text('List with pagination'),
            trailing: const Icon(Icons.arrow_forward_ios),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const PostsListScreen(),
                ),
              );
            },
          ),
          ListTile(
            title: const Text('Combined Data'),
            subtitle: const Text('Multiple repositories'),
            trailing: const Icon(Icons.arrow_forward_ios),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const DashboardScreen(),
                ),
              );
            },
          ),
          const Divider(),
          ListTile(
            title: const Text('Cache Settings'),
            subtitle: const Text('Manage cache and encryption'),
            trailing: const Icon(Icons.settings),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const CacheSettingsScreen(),
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

// Example 1: Simple User Profile
class UserProfileScreen extends StatefulWidget {
  final String userId;

  const UserProfileScreen({super.key, required this.userId});

  @override
  State<UserProfileScreen> createState() => _UserProfileScreenState();
}

class _UserProfileScreenState extends State<UserProfileScreen> {
  late final CachedTtlEtagRepository<User> _repository;

  @override
  void initState() {
    super.initState();
    _repository = CachedTtlEtagRepository<User>(
      config: CacheTtlEtagConfig<User>(
        url: 'https://jsonplaceholder.typicode.com/users/${widget.userId}',
        headers: {"accept": "application/json"},
        fromJson: (json) => User.fromJson(json),
        defaultTtl: const Duration(minutes: 5),
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User Profile'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => _repository.refresh(),
          ),
        ],
      ),
      body: StreamBuilder<CacheTtlEtagState<User>>(
        stream: _repository.stream,
        builder: (context, snapshot) {
          final state = snapshot.data ?? const CacheTtlEtagState<User>();

          if (state.isEmpty && state.isLoading) {
            return const Center(child: CircularProgressIndicator());
          }

          if (state.hasError && !state.hasData) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.error_outline, size: 48, color: Colors.red),
                  const SizedBox(height: 16),
                  Text('Error: ${state.error}'),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () => _repository.fetch(),
                    child: const Text('Retry'),
                  ),
                ],
              ),
            );
          }

          if (state.hasData) {
            return Stack(
              children: [
                ListView(
                  children: [
                    if (state.isStale)
                      Container(
                        color: Colors.orange.shade100,
                        padding: const EdgeInsets.all(8),
                        child: const Row(
                          children: [
                            Icon(Icons.warning_amber, size: 16),
                            SizedBox(width: 8),
                            Text('Data is stale, refreshing...'),
                          ],
                        ),
                      ),

                    Padding(
                      padding: const EdgeInsets.all(16),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            state.data!.name,
                            style: Theme.of(context).textTheme.headlineMedium,
                          ),
                          const SizedBox(height: 8),
                          Text(state.data!.email),
                          Text(state.data!.phone),
                          const SizedBox(height: 16),
                          if (state.timestamp != null)
                            Text(
                              'Last updated: ${state.timestamp!.toLocal()}',
                              style: const TextStyle(
                                fontSize: 12,
                                color: Colors.grey,
                              ),
                            ),
                          if (state.timeUntilExpiry != null)
                            Text(
                              'Expires in: ${state.timeUntilExpiry!.inMinutes} minutes',
                              style: const TextStyle(
                                fontSize: 12,
                                color: Colors.grey,
                              ),
                            ),
                        ],
                      ),
                    ),
                  ],
                ),

                if (state.isLoading)
                  const Positioned(
                    top: 0,
                    left: 0,
                    right: 0,
                    child: LinearProgressIndicator(),
                  ),
              ],
            );
          }

          return const Center(child: Text('No data'));
        },
      ),
    );
  }
}

// Example 2: Posts List
class PostsListScreen extends StatefulWidget {
  const PostsListScreen({super.key});

  @override
  State<PostsListScreen> createState() => _PostsListScreenState();
}

class _PostsListScreenState extends State<PostsListScreen> {
  late final CachedTtlEtagRepository<List<Post>> _repository;

  @override
  void initState() {
    super.initState();
    _repository = CachedTtlEtagRepository<List<Post>>(
      config: CacheTtlEtagConfig<List<Post>>(
        url: 'https://jsonplaceholder.typicode.com/posts',
        method: "GET",
        headers: {"accept": "application/json"},
        fromJson: (json) =>
            (json as List).map((e) => Post.fromJson(e)).toList(),
        defaultTtl: const Duration(minutes: 10),
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Posts'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => _repository.refresh(),
          ),
        ],
      ),
      body: StreamBuilder<CacheTtlEtagState<List<Post>>>(
        stream: _repository.stream,
        builder: (context, snapshot) {
          final state = snapshot.data ?? const CacheTtlEtagState<List<Post>>();

          if (state.isEmpty && state.isLoading) {
            return const Center(child: CircularProgressIndicator());
          }

          if (state.hasData) {
            return Stack(
              children: [
                RefreshIndicator(
                  onRefresh: () => _repository.refresh(),
                  child: ListView.builder(
                    itemCount: state.data!.length,
                    itemBuilder: (context, index) {
                      final post = state.data![index];
                      return ListTile(
                        title: Text(post.title),
                        subtitle: Text(
                          post.body,
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                      );
                    },
                  ),
                ),

                if (state.isLoading)
                  const Positioned(
                    top: 0,
                    left: 0,
                    right: 0,
                    child: LinearProgressIndicator(),
                  ),
              ],
            );
          }

          return const Center(child: Text('No posts'));
        },
      ),
    );
  }
}

// Example 3: Dashboard with Multiple Data Sources
class DashboardScreen extends StatefulWidget {
  const DashboardScreen({super.key});

  @override
  State<DashboardScreen> createState() => _DashboardScreenState();
}

class _DashboardScreenState extends State<DashboardScreen> {
  late final CachedTtlEtagRepository<User> _userRepo;
  late final CachedTtlEtagRepository<List<Post>> _postsRepo;

  @override
  void initState() {
    super.initState();
    _userRepo = CachedTtlEtagRepository<User>(
      config: CacheTtlEtagConfig(
        url: 'https://jsonplaceholder.typicode.com/users/1',
        fromJson: (json) => User.fromJson(json),
        defaultTtl: const Duration(minutes: 5),
      ),
    );
    _postsRepo = CachedTtlEtagRepository<List<Post>>(
      config: CacheTtlEtagConfig(
        url: 'https://jsonplaceholder.typicode.com/posts',
        fromJson: (json) =>
            (json as List).take(5).map((e) => Post.fromJson(e)).toList(),
        defaultTtl: const Duration(minutes: 5),
      ),
    );
  }

  @override
  void dispose() {
    _userRepo.dispose();
    _postsRepo.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dashboard'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              _userRepo.refresh();
              _postsRepo.refresh();
            },
          ),
        ],
      ),
      body: StreamBuilder<CacheTtlEtagState<User>>(
        stream: _userRepo.stream,
        builder: (context, userSnapshot) {
          return StreamBuilder<CacheTtlEtagState<List<Post>>>(
            stream: _postsRepo.stream,
            builder: (context, postsSnapshot) {
              final userState =
                  userSnapshot.data ?? const CacheTtlEtagState<User>();
              final postsState =
                  postsSnapshot.data ?? const CacheTtlEtagState<List<Post>>();

              final isLoading = userState.isLoading || postsState.isLoading;

              return Stack(
                children: [
                  ListView(
                    children: [
                      if (userState.hasData)
                        Card(
                          margin: const EdgeInsets.all(16),
                          child: Padding(
                            padding: const EdgeInsets.all(16),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  'Welcome, ${userState.data!.name}',
                                  style: Theme.of(context).textTheme.titleLarge,
                                ),
                                Text(userState.data!.email),
                              ],
                            ),
                          ),
                        ),

                      if (postsState.hasData)
                        Padding(
                          padding: const EdgeInsets.all(16),
                          child: Text(
                            'Recent Posts',
                            style: Theme.of(context).textTheme.titleMedium,
                          ),
                        ),

                      if (postsState.hasData)
                        ...postsState.data!.map(
                          (post) => ListTile(
                            title: Text(post.title),
                            subtitle: Text(
                              post.body,
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                        ),
                    ],
                  ),

                  if (isLoading)
                    const Positioned(
                      top: 0,
                      left: 0,
                      right: 0,
                      child: LinearProgressIndicator(),
                    ),
                ],
              );
            },
          );
        },
      ),
    );
  }
}

// Cache Settings Screen
class CacheSettingsScreen extends StatefulWidget {
  const CacheSettingsScreen({super.key});

  @override
  State<CacheSettingsScreen> createState() => _CacheSettingsScreenState();
}

class _CacheSettingsScreenState extends State<CacheSettingsScreen> {
  bool _encryptionEnabled = false;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _encryptionEnabled = TtlEtagCache.isEncryptionEnabled;
  }

  Future<void> _toggleEncryption(bool value) async {
    setState(() => _isLoading = true);

    try {
      await TtlEtagCache.migrateEncryption(enableEncryption: value);
      setState(() {
        _encryptionEnabled = value;
        _isLoading = false;
      });

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              value
                  ? 'Encryption enabled successfully'
                  : 'Encryption disabled successfully',
            ),
          ),
        );
      }
    } catch (e) {
      setState(() => _isLoading = false);
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Error: $e')));
      }
    }
  }

  Future<void> _clearCache() async {
    await TtlEtagCache.clearAll();
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Cache cleared successfully')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cache Settings')),
      body: ListView(
        children: [
          SwitchListTile(
            title: const Text('Enable Encryption'),
            subtitle: Text(
              _encryptionEnabled
                  ? 'Cache is encrypted with AES-256'
                  : 'Cache is stored in plain text',
            ),
            value: _encryptionEnabled,
            onChanged: _isLoading ? null : _toggleEncryption,
          ),

          if (_isLoading) const LinearProgressIndicator(),

          const Divider(),

          ListTile(
            title: const Text('Clear Cache'),
            subtitle: const Text('Remove all cached data'),
            trailing: const Icon(Icons.delete_outline),
            onTap: _clearCache,
          ),
        ],
      ),
    );
  }
}

// Models
class User {
  final int id;
  final String name;
  final String email;
  final String phone;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.phone,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
      phone: json['phone'] as String,
    );
  }
}

class Post {
  final int id;
  final int userId;
  final String title;
  final String body;

  Post({
    required this.id,
    required this.userId,
    required this.title,
    required this.body,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as int,
      userId: json['userId'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
    );
  }
}
1
likes
160
points
124
downloads

Publisher

unverified uploader

Weekly Downloads

A powerful, reactive caching solution for Flutter with TTL, ETag support, and optional AES-256 encryption for offline-first applications.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

crypto, dio, encrypt, flutter, flutter_secure_storage, isar_community, isar_community_flutter_libs, path_provider, rxdart

More

Packages that depend on ttl_etag_cache

Packages that implement ttl_etag_cache