Skip to main content

Offline-First Quick Start

Get started dengan offline-first implementation dalam 5 menit!

🎯 What You’ll Build

Setelah mengikuti guide ini, Anda akan memiliki:
  • βœ… Flutter app yang bisa create transactions offline
  • βœ… Auto-sync ke backend saat online
  • βœ… Conflict detection & resolution
  • βœ… Real-time sync status UI

πŸ“‹ Prerequisites

Backend: MStore Backend v1.0+ sudah running
Flutter: Flutter 3.16+ installed
Database: MySQL 8.0+ dengan schema terbaru

πŸš€ 5-Minute Setup

Step 1: Add Dependencies (1 min)

# pubspec.yaml
dependencies:
  isar: ^3.1.0+1
  isar_flutter_libs: ^3.1.0+1
  uuid: ^4.2.1
  connectivity_plus: ^5.0.2

dev_dependencies:
  isar_generator: ^3.1.0+1
  build_runner: ^2.4.7
flutter pub get

Step 2: Define Isar Model (1 min)

// lib/models/transaction_local.dart
import 'package:isar/isar.dart';

part 'transaction_local.g.dart';

@collection
class TransactionLocal {
  Id id = Isar.autoIncrement;
  
  @Index(unique: true)
  late String offlineId;
  
  @Index(unique: true)
  late String offlineReference;
  
  late String deviceId;
  late double grandTotal;
  late String status;
  
  @enumerated
  late SyncStatus syncStatus;
  
  int? serverId;
  String? transactionCode;
}

enum SyncStatus { pending, syncing, synced, failed }
flutter pub run build_runner build

Step 3: Initialize Isar (1 min)

// lib/main.dart
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  final dir = await getApplicationDocumentsDirectory();
  final isar = await Isar.open(
    [TransactionLocalSchema],
    directory: dir.path,
  );
  
  runApp(MyApp(isar: isar));
}

Step 4: Create Transaction Offline (1 min)

// lib/services/transaction_service.dart
import 'package:uuid/uuid.dart';

Future<TransactionLocal> createTransaction(Isar isar) async {
  final tx = TransactionLocal()
    ..offlineId = const Uuid().v4()
    ..offlineReference = 'DEVICE001-${DateTime.now().millisecondsSinceEpoch}'
    ..deviceId = 'DEVICE001'
    ..grandTotal = 33000
    ..status = 'paid'
    ..syncStatus = SyncStatus.pending;
  
  await isar.writeTxn(() async {
    await isar.transactionLocals.put(tx);
  });
  
  return tx;
}

Step 5: Sync to Backend (1 min)

// lib/services/sync_service.dart
import 'package:dio/dio.dart';

Future<void> syncToBackend(Isar isar) async {
  final pending = await isar.transactionLocals
      .filter()
      .syncStatusEqualTo(SyncStatus.pending)
      .findAll();
  
  if (pending.isEmpty) return;
  
  final dio = Dio(BaseOptions(baseUrl: 'http://localhost:3002/api/v1'));
  
  final response = await dio.post('/pos/offline/batch-sync', data: {
    'transactions': pending.map((tx) => {
      'offline_id': tx.offlineId,
      'offline_reference': tx.offlineReference,
      'device_id': tx.deviceId,
      'grand_total': tx.grandTotal,
      'status': tx.status,
      'branch_code': 'BRN-MST-OSK00001',
      'payment_method': 'CASH',
      'id_user_apps': 1,
      'created_at_device': DateTime.now().toIso8601String(),
      'subtotal': tx.grandTotal,
      'discount_total': 0,
      'tax_total': 0,
      'payment_status': 'paid',
    }).toList(),
    'payments': [],
    'voids': [],
    'device_id': 'DEVICE001',
    'synced_at': DateTime.now().toIso8601String(),
  });
  
  // Update sync status
  final results = response.data['data']['results'] as List;
  await isar.writeTxn(() async {
    for (final result in results) {
      final tx = pending.firstWhere((t) => t.offlineId == result['offline_id']);
      if (result['success']) {
        tx.syncStatus = SyncStatus.synced;
        tx.serverId = result['server_id'];
        tx.transactionCode = result['transaction_code'];
      } else {
        tx.syncStatus = SyncStatus.failed;
      }
      await isar.transactionLocals.put(tx);
    }
  });
}

πŸŽ‰ Done!

Anda sekarang sudah memiliki offline-first app yang working!

Test It

  1. Create transaction offline:
    final tx = await createTransaction(isar);
    print('Created: ${tx.offlineReference}');
    
  2. Sync to backend:
    await syncToBackend(isar);
    print('Synced!');
    
  3. Check status:
    final synced = await isar.transactionLocals
        .filter()
        .syncStatusEqualTo(SyncStatus.synced)
        .findAll();
    print('Synced: ${synced.length} transactions');
    

πŸ“– Next Steps


πŸ†˜ Troubleshooting

Transaction Not Syncing?

final connectivity = await Connectivity().checkConnectivity();
print('Connected: ${connectivity != ConnectivityResult.none}');
final pending = await isar.transactionLocals
    .filter()
    .syncStatusEqualTo(SyncStatus.pending)
    .findAll();
print('Pending: ${pending.length}');
# Check backend logs
tail -f logs/app.log | grep "offline"
-- Check transactions table
SELECT * FROM transactions 
WHERE is_offline = 1 
ORDER BY created_at DESC 
LIMIT 10;

-- Check conflicts
SELECT * FROM offline_conflicts 
WHERE status = 'pending';

πŸ’‘ Pro Tips

Batch Size: Sync max 50 transactions per request untuk optimal performance
Auto Sync: Setup Timer untuk auto-sync setiap 30 detik
Timer.periodic(Duration(seconds: 30), (_) => syncToBackend(isar));
Conflict Handling: Always show conflicts to user untuk manual resolution
Don’t Block UI: Sync harus non-blocking, jangan tunggu response

πŸ“Š Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Flutter App                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Create Transaction          β”‚  β”‚
β”‚  β”‚   (Offline-First)             β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚              β”‚                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Save to Isar DB             β”‚  β”‚
β”‚  β”‚   (Local Storage)             β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚              β”‚                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Add to Sync Queue           β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
               β”‚ Background Sync
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Backend API                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   POST /batch-sync            β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚              β”‚                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Check Duplicate             β”‚  β”‚
β”‚  β”‚   (offline_reference)         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚              β”‚                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Save to MySQL               β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Congratulations! πŸŽ‰ Anda sudah berhasil implement offline-first architecture. Check complete documentation untuk production-ready implementation.