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. In this post, we'll explore how an offline-first approach with Isar can transform your Flutter app's performance and user experience, and why this matters for building production-ready applications.

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: 50-200ms on good connections, seconds on poor ones
  • Backend processing: Database queries, authentication, business logic
  • Parsing and rendering: JSON deserialization, UI updates

Stack these together, and users face 2-5 second load times.

Real-world impact: Users on spotty connections (rural areas, congested networks) experience timeouts, failed requests, and loading spinners. According to Google research, 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:

  1. Fetch once, store locally
  2. Read from cache instantly (microseconds, not milliseconds)
  3. Update in background when network is available

The difference is dramatic. Cached data loads in 10-50ms—up to 100x faster than network calls. Your UI renders immediately. Users scroll, navigate, and interact without waiting.

The catch: You're showing slightly stale data. That feed post from 5 minutes ago? Might be cached from 30 minutes ago. The like count? Could be outdated.

The Live Data vs Speed Tradeoff

This is where architecture decisions get interesting. You can't cache everything, and you shouldn't cache nothing. At Hoomanely, we give utter importance to understanding the trade off between speed and live data that directly affects the app performance and user experience. The key is understanding data freshness requirements:

Always Live (No Caching)

  • Financial transactions: Balance updates, payment confirmations
  • Real-time messaging: Chat messages, typing indicators
  • Critical actions: Order placement, booking confirmations
  • Security-sensitive: Auth tokens, permissions, sensitive user data

These need immediate accuracy. A stale bank balance is unacceptable. A 5-minute-old chat is broken. However,

  • User profiles: Names, avatars, bios (change infrequently)
  • Feed content: Posts, articles, videos (acceptable if 5-30 min old)
  • Search results: Recent queries, suggested content

These can tolerate slight staleness. Users won't notice if a profile picture is from 10 minutes ago.

Hybrid Approach (Smart Caching)

  • Show cached data immediately
  • Fetch fresh data in parallel
  • Update UI when new data arrives

This gives you both speed AND accuracy. Users see content instantly while the app quietly updates behind the scenes.

Decision framework:

  1. What's the cost of stale data? (User confusion? Wrong decisions? No impact?)
  2. How often does this data change? (Every second? Daily? Weekly?)
  3. What's the user's intent? (Browsing? Making a critical decision?)
  4. What's the network reality? (Urban 5G? Rural 3G?)

Isar: The Offline-First Database

Isar is a blazingly fast, NoSQL database built specifically for Flutter and Dart. Unlike SQLite wrappers or shared preferences, Isar is designed from the ground up for offline-first mobile apps.

Why Isar Solves the Problem

1. Speed Where It Counts

  • Synchronous queries return in microseconds
  • Asynchronous operations for large datasets
  • Lazy loading and streaming for memory efficiency
  • ACID transactions for data consistency

2. Flutter-Native Design

  • Works with Dart objects directly (no ORM mapping)
  • Full isolate support for background operations
  • Hot reload compatible

3. Powerful Querying

  • Compound indexes for complex queries
  • Full-text search built-in
  • Links and backlinks for relationships
  • Filters, sorting, and aggregations without SQL

Isar & The Counterparts

Isar vs SQLite (sqflite, drift)

SQLite strengths: Mature, widely understood, SQL familiarity
Isar advantages:

  • 10-30x faster for typical mobile queries
  • No SQL string building or parsing
  • Native Dart objects (no serialization overhead)
  • Better full-text search out of the box
  • Simpler migration and schema management

Isar vs Hive

Hive strengths: Simpler API, smaller footprint, pure Dart
Isar advantages:

  • Multi-threaded query execution
  • Proper indexing (Hive scans everything)
  • ACID transactions (Hive can lose data on crashes)
  • Queries 100x faster on large datasets
  • Better memory management

Isar vs ObjectBox

ObjectBox strengths: Similar performance, good documentation Isar advantages:

  • Open source (ObjectBox is proprietary)
  • Smaller binary size
  • Free tier has no limits
  • Better community adoption in Flutter

Feature Comparison Table

FeatureIsarSQLiteHiveObjectBox
Query Speed⚡⚡⚡⚡⚡⚡⚡⚡
IndexingMulti-column, compositeStandard SQLNoneMulti-column
Full-text SearchNativeLimitedNoneAvailable
Type SafetyGeneratedManualManualGenerated
TransactionsACIDACIDLimitedACID
Binary Size~1.5 MB~2 MB~200 KB~2 MB
LicenseApache 2.0Public DomainApache 2.0Commercial

Implementation Strategies

1. The Repository Pattern

Separate your data layer from your UI logic:

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);
  }
}

2. Schema Design Best Practices

@collection
class Post {
  Id id = Isar.autoIncrement;
  
  @Index()
  late String userId; // Index frequently queried fields
  
  late String content;
  
  @Index(type: IndexType.value)
  late DateTime createdAt; // Index for sorting
  
  late int likeCount;
  
  // Use links for relationships
  final author = IsarLink<User>();
  
  // Track cache freshness
  late DateTime cachedAt;
}

Key principles:

  • Index fields you query or sort by
  • Keep frequently accessed data in one collection
  • Use links for relationships (avoid nested objects)
  • Store timestamps for cache invalidation

3. Sync Strategy

class SyncManager {
  static Future<void> syncInBackground() async {
    final connectivity = await Connectivity().checkConnectivity();
    if (connectivity != ConnectivityResult.wifi) return;
    
    final lastSync = await getLastSyncTime();
    final now = DateTime.now();
    
    // Sync every 30 minutes
    if (now.difference(lastSync).inMinutes < 30) return;
    
    await Future.wait([
      _syncPosts(),
      _syncUsers(),
      _syncSettings(),
    ]);
    
    await setLastSyncTime(now);
  }
}

Key Takeaways

  1. Network calls are expensive: Latency, failures, and battery drain hurt UX more than you think
  2. Cache strategically: Not all data deserves the same treatment—understand freshness requirements
  3. Isar wins on speed: 10-100x faster than alternatives for typical mobile workloads
  4. Offline-first is user-first: Instant responses build trust and engagement
  5. Monitor and maintain: Cache health metrics prevent staleness and bloat
  6. Repository pattern: Separate data concerns from business logic for maintainable code

Start simple: Pick one feature, cache it with Isar, measure the impact. You'll see the difference in milliseconds, and your users will feel it in every interaction.


Read more