← Back to Documentation

Offline Mode

Access your lists and data anywhere with MiyoList's 3-tier caching system

Overview

MiyoList implements a sophisticated 3-tier caching system that ensures you can access your anime and manga lists even without an internet connection. All data is stored locally and synced with AniList when online.

✓ Key Benefit: View and edit your lists anywhere, anytime. Changes are queued and synced automatically when you're back online.

3-Tier Caching System

Tier 1: Memory Cache

Speed: Instant access (microseconds)
Duration: App session
Storage: RAM

The fastest cache layer keeps frequently accessed data in memory for instant retrieval. Perfect for lists, media entries, and user profiles that are accessed multiple times during an app session.

// lib/core/cache/memory_cache.dart

class MemoryCache {
  final Map<String, CacheEntry> _cache = {};
  final Duration defaultTtl;

  MemoryCache({this.defaultTtl = const Duration(hours: 1)});

  void set(String key, dynamic value, {Duration? ttl}) {
    _cache[key] = CacheEntry(
      value: value,
      expiry: DateTime.now().add(ttl ?? defaultTtl),
    );
  }

  T? get<T>(String key) {
    final entry = _cache[key];
    if (entry == null) return null;
    
    if (entry.isExpired) {
      _cache.remove(key);
      return null;
    }
    
    return entry.value as T?;
  }

  void clear() => _cache.clear();
}

class CacheEntry {
  final dynamic value;
  final DateTime expiry;

  CacheEntry({required this.value, required this.expiry});

  bool get isExpired => DateTime.now().isAfter(expiry);
}
💾

Tier 2: Local Database

Speed: Fast (milliseconds)
Duration: Permanent
Storage: SQLite/Hive

Persistent storage using SQLite or Hive keeps your data available across app restarts. This is your primary offline data source that survives app updates and device restarts.

// lib/core/cache/local_database.dart

import 'package:hive_flutter/hive_flutter.dart';

class LocalDatabase {
  late Box<MediaEntry> _mediaBox;
  late Box<UserProfile> _userBox;

  Future<void> init() async {
    await Hive.initFlutter();
    
    // Register adapters
    Hive.registerAdapter(MediaEntryAdapter());
    Hive.registerAdapter(UserProfileAdapter());
    
    // Open boxes
    _mediaBox = await Hive.openBox<MediaEntry>('media');
    _userBox = await Hive.openBox<UserProfile>('user');
  }

  // Save media entry
  Future<void> saveMedia(MediaEntry entry) async {
    await _mediaBox.put(entry.id.toString(), entry);
  }

  // Get media entry
  MediaEntry? getMedia(int id) {
    return _mediaBox.get(id.toString());
  }

  // Save user list
  Future<void> saveUserList(List<MediaEntry> entries) async {
    final map = Map.fromEntries(
      entries.map((e) => MapEntry(e.id.toString(), e)),
    );
    await _mediaBox.putAll(map);
  }

  // Get all media
  List<MediaEntry> getAllMedia() {
    return _mediaBox.values.toList();
  }
}
🖼️

Tier 3: Image Cache

Speed: Fast (milliseconds)
Duration: Configurable (default 7 days)
Storage: File system

Images are cached on disk using cached_network_image for offline viewing. Covers and banners remain accessible even without internet.

// lib/core/widgets/cached_image.dart

import 'package:cached_network_image/cached_network_image.dart';

class CachedImage extends StatelessWidget {
  final String imageUrl;
  final double? width;
  final double? height;
  final BoxFit fit;

  const CachedImage({
    required this.imageUrl,
    this.width,
    this.height,
    this.fit = BoxFit.cover,
  });

  @override
  Widget build(BuildContext context) {
    return CachedNetworkImage(
      imageUrl: imageUrl,
      width: width,
      height: height,
      fit: fit,
      placeholder: (context, url) => Container(
        color: Colors.grey[800],
        child: Center(child: CircularProgressIndicator()),
      ),
      errorWidget: (context, url, error) => Container(
        color: Colors.grey[800],
        child: Icon(Icons.broken_image, color: Colors.grey),
      ),
      cacheManager: CacheManager(
        Config(
          'customCacheKey',
          stalePeriod: Duration(days: 7),
          maxNrOfCacheObjects: 500,
        ),
      ),
    );
  }
}

Offline Features

✓ View Lists

Browse your complete anime and manga lists with all details, including covers, descriptions, and your personal notes.

✓ Edit Entries

Update progress, scores, status, and notes. Changes are queued and automatically synced when you're back online.

✓ Search Locally

Search through your cached lists instantly. Full-text search works completely offline with your synced data.

✓ View Statistics

All statistics and analytics are calculated from local data, so they're always available offline.

✓ Queue Changes

Offline changes are stored in a queue and synced automatically when connection is restored.

✓ View Images

Previously loaded covers and banners remain cached and viewable even without internet.

Sync Strategy

Automatic Synchronization

MiyoList automatically syncs your data in the background to keep everything up to date:

  • On App Start: Quick sync to check for updates
  • Manual Refresh: Pull-to-refresh on any list
  • After Edits: Changes are immediately queued for sync
  • Periodic Sync: Background sync every 15-30 minutes (when app is open)
  • On Connectivity: Automatic sync when internet is restored

Conflict Resolution

When conflicts occur between local and remote data:

// lib/core/sync/conflict_resolver.dart

class ConflictResolver {
  MediaEntry resolve(MediaEntry local, MediaEntry remote) {
    // Use most recent timestamp
    if (local.updatedAt.isAfter(remote.updatedAt)) {
      return local;
    } else if (remote.updatedAt.isAfter(local.updatedAt)) {
      return remote;
    }
    
    // If timestamps are equal, merge non-null fields
    return MediaEntry(
      id: local.id,
      progress: local.progress ?? remote.progress,
      score: local.score ?? remote.score,
      status: local.status,
      notes: local.notes?.isNotEmpty == true 
          ? local.notes 
          : remote.notes,
      updatedAt: local.updatedAt,
    );
  }
}

Complete Implementation

Unified Cache Manager

// lib/core/cache/cache_manager.dart

class CacheManager {
  final MemoryCache _memoryCache;
  final LocalDatabase _localStorage;
  final SyncQueue _syncQueue;

  CacheManager({
    required MemoryCache memoryCache,
    required LocalDatabase localStorage,
    required SyncQueue syncQueue,
  }) : _memoryCache = memoryCache,
       _localStorage = localStorage,
       _syncQueue = syncQueue;

  // Get media with fallback strategy
  Future<MediaEntry?> getMedia(int id) async {
    // Try memory cache first
    var entry = _memoryCache.get<MediaEntry>('media_$id');
    if (entry != null) return entry;
    
    // Try local database
    entry = _localStorage.getMedia(id);
    if (entry != null) {
      // Update memory cache
      _memoryCache.set('media_$id', entry);
      return entry;
    }
    
    // Not found in cache
    return null;
  }

  // Save media with multi-tier update
  Future<void> saveMedia(MediaEntry entry) async {
    // Update all cache tiers
    _memoryCache.set('media_${entry.id}', entry);
    await _localStorage.saveMedia(entry);
    
    // Queue for sync with server
    _syncQueue.add(SyncTask.updateMedia(entry));
  }

  // Get user list with smart loading
  Future<List<MediaEntry>> getUserList(
    String userId, 
    MediaType type,
  ) async {
    final cacheKey = 'list_${userId}_${type.name}';
    
    // Try memory cache
    var list = _memoryCache.get<List<MediaEntry>>(cacheKey);
    if (list != null) return list;
    
    // Load from database
    list = _localStorage.getAllMedia()
        .where((e) => e.type == type)
        .toList();
    
    // Update memory cache
    _memoryCache.set(cacheKey, list);
    
    return list;
  }
}

Best Practices

  • Cache strategically: Not everything needs to be cached, prioritize frequently accessed data
  • Set expiry times: Use appropriate TTL for different data types
  • Implement fallbacks: Always have a fallback strategy when cache misses
  • Monitor cache size: Implement cache eviction policies to prevent excessive storage use
  • Handle errors gracefully: Show appropriate messages when offline features are limited
  • Test offline scenarios: Regularly test app behavior without internet connection

Related Resources