Offline-First with ISAR: Building Faster, More Reliable Flutter Apps

Offline-First with ISAR: Building Faster, More Reliable Flutter Apps

Building mobile apps that feel fast and responsive isn't just about clean code, it's about smart architecture. Every time your app makes a network call, you're introducing latency, uncertainty, and potential frustration. This post explores how an offline-first approach with Isar can transform your Flutter app's performance and reliability.

The network call problem

Modern mobile apps are data-hungry. A typical social feed might make dozens of API calls just to load a single screen, user profiles, posts, comments, likes, media URLs. Each call takes time, network latency of 50-200ms on good connections and seconds on poor ones, backend processing for database queries and business logic, and parsing and rendering overhead. Stack these together and users face 2-5 second load times.

Users on spotty connections, rural areas, congested networks, experience timeouts, failed requests, and loading spinners. Google research finds that 53% of mobile users abandon apps that take longer than 3 seconds to load.

Caching: the speed solution

Device-side caching flips this model. Instead of fetching data on every screen load, you fetch once and store locally, read from cache instantly (microseconds, not milliseconds), and update in background when network is available. Cached data loads in 10-50ms, up to 100x faster than network calls, and your UI renders immediately. The catch is you're showing slightly stale data, that feed post from 5 minutes ago might be cached from 30 minutes ago.

The live data versus speed trade-off

You can't cache everything, and you shouldn't cache nothing. The key is understanding data freshness requirements. Some data needs to always be live with no caching, financial transactions, real-time messaging, critical actions like order placement, and security-sensitive data like auth tokens and permissions, since a stale bank balance is unacceptable and a 5-minute-old chat is broken.

Other data can tolerate slight staleness, user profiles, feed content, search results, users won't notice if a profile picture is from 10 minutes ago. A hybrid approach, smart caching, shows cached data immediately, fetches fresh data in parallel, and updates the UI when new data arrives, giving both speed and accuracy. The decision framework asks what the cost of stale data is, how often the data changes, what the user's intent is, and what the network reality looks like.

Isar: the offline-first database

Isar is a blazingly fast NoSQL database built specifically for Flutter and Dart. Unlike SQLite wrappers or shared preferences, it's designed from the ground up for offline-first mobile apps. It offers speed where it counts, synchronous queries returning in microseconds, async operations for large datasets, and ACID transactions for consistency. It's Flutter-native, working with Dart objects directly with no ORM mapping, and supports full isolates for background operations. And it offers powerful querying, compound indexes, built-in full-text search, and links for relationships without needing SQL.

Compared to SQLite, Isar is 10-30x faster for typical mobile queries with no SQL string building and native Dart objects. Compared to Hive, Isar offers multi-threaded query execution, proper indexing (Hive scans everything), and ACID transactions (Hive can lose data on crashes). Compared to ObjectBox, Isar is open source with a smaller binary size and no tier limits.

Implementation strategies

Separate your data layer from UI logic using the repository pattern:

abstract class PostRepository {
  Future<List<Post>> getPosts({bool forceRefresh = false});
  Stream<List<Post>> watchPosts();
}
 
class OfflineFirstPostRepository implements PostRepository {
  final Isar isar;
  final ApiClient api;
  
  @override
  Future<List<Post>> getPosts({bool forceRefresh = false}) async {
    // 1. Return cached data immediately
    final cached = await isar.posts.where().findAll();
    if (cached.isNotEmpty && !forceRefresh) {
      return cached;
    }
  
    // 2. Fetch fresh data in background
    try {
      final fresh = await api.fetchPosts();
      // 3. Update cache
      await isar.writeTxn(() async {
        await isar.posts.putAll(fresh);
      });
      return fresh;
    } catch (e) {
      // 4. Fall back to cache on error
      return cached;
    }
  }
  
  @override
  Stream<List<Post>> watchPosts() {
    return isar.posts.where().watch(fireImmediately: true);
  }
}

For schema design, index fields you query or sort by, keep frequently accessed data in one collection, use links for relationships rather than nested objects, and store timestamps for cache invalidation. For sync strategy, sync in the background under favorable network conditions, like Wi-Fi, and respect a minimum interval between syncs to avoid excessive battery and data usage.

Key takeaways

  • Network calls are expensive, latency, failures, and battery drain hurt UX more than you think.
  • Cache strategically, not all data deserves the same treatment, understand freshness requirements.
  • Isar wins on speed, 10-100x faster than alternatives for typical mobile workloads.
  • Offline-first is user-first, instant responses build trust and engagement.
  • Monitor and maintain cache health metrics to prevent staleness and bloat.
  • And use the repository pattern to separate data concerns from business logic for maintainable code.
  • Start simple, pick one feature, cache it with Isar, and measure the impact.