📋 Overview
Dokumentasi ini menjelaskan implementasi lengkap fitur offline-first untuk transaksi POS di MStore Mobile menggunakan Isar database dan batch sync ke backend.
🎯 Fitur Utama
Offline Transaction Simpan transaksi lokal saat offline menggunakan Isar
Auto Batch Sync Sinkronisasi otomatis saat koneksi kembali online
Conflict Detection Deteksi duplikasi menggunakan offline_reference
Sync Status Tracking Tracking status: pending, syncing, synced, failed
🏗️ Arsitektur
┌─────────────────────────────────────┐
│ Flutter App │
│ ┌───────────────────────────────┐ │
│ │ TransactionLocalService │ │
│ │ (Create Offline) │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌───────────▼───────────────────┐ │
│ │ Isar DB (Local Storage) │ │
│ │ TransactionLocalEntity │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌───────────▼───────────────────┐ │
│ │ BatchSyncService │ │
│ │ (Auto Sync) │ │
│ └───────────┬───────────────────┘ │
└──────────────┼───────────────────────┘
│
│ HTTP POST /batch-sync
│
┌──────────────▼───────────────────────┐
│ Backend API │
│ ┌───────────────────────────────┐ │
│ │ Check Duplicate │ │
│ │ (offline_reference) │ │
│ └───────────┬───────────────────┘ │
│ │ │
│ ┌───────────▼───────────────────┐ │
│ │ Save to MySQL │ │
│ └───────────────────────────────┘ │
└──────────────────────────────────────┘
📦 Komponen Utama
1. TransactionLocalEntity
Entity Isar untuk menyimpan transaksi offline:
@collection
@Name ( "transaction_local" )
class TransactionLocalEntity {
Id id = Isar .autoIncrement;
@Index (unique : true )
late String offlineId; // UUID v4
@Index (unique : true )
late String offlineReference; // DEVICE_ID-timestamp
late String deviceId;
late String branchCode;
late int idUserApps;
late double grandTotal;
late String status;
@Enumerated ( EnumType .name)
late SyncStatus syncStatus; // pending, syncing, synced, failed
int ? serverId; // ID dari server setelah sync
String ? transactionCode; // Kode transaksi dari server
late DateTime createdAtDevice;
late String itemsJson; // Items dalam format JSON
}
enum SyncStatus {
pending,
syncing,
synced,
failed,
}
File: lib/database/isar/entities/transaction_local_entity.dart
2. TransactionLocalService
Service untuk CRUD transaksi offline:
@LazySingleton ()
class TransactionLocalService {
final IsarDb _isarDb;
// Create offline transaction
Future < TransactionLocalEntity > createTransaction ({
required String deviceId,
required String branchCode,
required double grandTotal,
required String itemsJson,
// ... other params
}) async {
final offlineId = Uuid (). v4 ();
final offlineReference = ' $ deviceId - ${ DateTime . now (). millisecondsSinceEpoch } ' ;
final transaction = TransactionLocalEntity ()
..offlineId = offlineId
..offlineReference = offlineReference
..syncStatus = SyncStatus .pending
// ... set other fields
await _isarDb.isar. writeTxn (() async {
await _isarDb.isar.transactionLocalEntitys. put (transaction);
});
return transaction;
}
// Get pending transactions
Future < List < TransactionLocalEntity >> getPendingTransactions () async {
return await _isarDb.isar.transactionLocalEntitys
. filter ()
. syncStatusEqualTo ( SyncStatus .pending)
. findAll ();
}
// Update sync status
Future < void > updateSyncStatus ({
required String offlineId,
required SyncStatus status,
int ? serverId,
String ? transactionCode,
}) async {
// ... implementation
}
}
File: lib/core/transaction/transaction_local_service.dart
3. BatchSyncService
Service untuk batch sync ke backend:
@LazySingleton ()
class BatchSyncService {
final TransactionApi _transactionApi;
final TransactionLocalService _transactionLocalService;
Future < int > syncPendingTransactions ({
required String deviceId,
}) async {
// 1. Get pending transactions
final pending = await _transactionLocalService. getPendingTransactions ();
// 2. Update status to syncing
for ( final tx in pending) {
await _transactionLocalService. updateSyncStatus (
offlineId : tx.offlineId,
status : SyncStatus .syncing,
);
}
// 3. Prepare payload
final payload = {
'transactions' : pending. map ((tx) => {
'offline_id' : tx.offlineId,
'offline_reference' : tx.offlineReference,
'device_id' : tx.deviceId,
'branch_code' : tx.branchCode,
'grand_total' : tx.grandTotal,
'items' : jsonDecode (tx.itemsJson),
// ... other fields
}). toList (),
'device_id' : deviceId,
'synced_at' : DateTime . now (). toIso8601String (),
};
// 4. Send to backend
final response = await _transactionApi. batchSyncOfflineTransactions (payload);
// 5. Update status based on results
final results = response.data[ 'data' ][ 'results' ] as List ;
await _transactionLocalService. batchUpdateSyncStatus (results);
return results. where ((r) => r[ 'success' ] == true ).length;
}
}
File: lib/core/sync/batch_sync_service.dart
4. OfflineSyncService Integration
Integrasi auto-sync saat online:
class OfflineSyncService with WidgetsBindingObserver {
final BatchSyncService _batchSyncService = getIt < BatchSyncService >();
void initialize ({
Duration ? transactionSyncTTL,
}) {
this .transactionSyncTTL = transactionSyncTTL ?? const Duration (minutes : 2 );
// Listen connectivity changes
Connectivity ().onConnectivityChanged. listen ((results) async {
final isOnline = results. any ((r) => r != ConnectivityResult .none);
if (isOnline) {
await _triggerTransactionSyncIfStale (ttl : this .transactionSyncTTL);
}
});
}
Future < void > _triggerTransactionSyncIfStale ({ required Duration ttl}) async {
// Check if stale based on TTL
final deviceId = await DeviceInfoService (). getOrCreateDeviceId ();
final syncedCount = await _batchSyncService. syncPendingTransactions (
deviceId : deviceId,
);
AppLog . i ( 'sync' , 'Transactions synced' , meta : { 'count' : syncedCount});
}
}
File: lib/core/sync/offline_sync_service.dart
5. TransactionApi Endpoint
Endpoint untuk batch sync:
@RestApi ()
abstract class TransactionApi {
@POST ( '/api/v1/pos/offline/batch-sync' )
Future < HttpResponse < dynamic >> batchSyncOfflineTransactions (
@Body () Map < String , dynamic > body
);
}
File: lib/core/transaction/transaction_api.dart
🔧 Dependency Injection
Registrasi di NetworkModule:
@module
abstract class NetworkModule {
@LazySingleton ()
TransactionLocalService transactionLocalService ( IsarDb isarDb) {
return TransactionLocalService (isarDb);
}
@LazySingleton ()
BatchSyncService batchSyncService (
TransactionApi transactionApi,
TransactionLocalService transactionLocalService,
) {
return BatchSyncService (transactionApi, transactionLocalService);
}
}
📝 Cara Penggunaan
1. Create Offline Transaction
final transactionLocalService = getIt < TransactionLocalService >();
final transaction = await transactionLocalService. createTransaction (
deviceId : 'DEVICE001' ,
branchCode : 'BRN-MST-OSK00001' ,
idUserApps : 1 ,
grandTotal : 33000.0 ,
subtotal : 33000.0 ,
discountTotal : 0.0 ,
taxTotal : 0.0 ,
status : 'paid' ,
paymentStatus : 'paid' ,
paymentMethod : 'CASH' ,
itemsJson : jsonEncode ([
{
'product_id' : 1 ,
'product_name' : 'Kopi' ,
'quantity' : 2 ,
'price' : 15000 ,
'subtotal' : 30000 ,
}
]),
);
print ( 'Created offline: ${ transaction . offlineReference } ' );
2. Manual Sync
final batchSyncService = getIt < BatchSyncService >();
final deviceId = await DeviceInfoService (). getOrCreateDeviceId ();
final syncedCount = await batchSyncService. syncPendingTransactions (
deviceId : deviceId,
);
print ( 'Synced $ syncedCount transactions' );
3. Check Sync Statistics
final stats = await transactionLocalService. getStatistics ();
print ( 'Pending: ${ stats [ 'pending' ]} ' );
print ( 'Synced: ${ stats [ 'synced' ]} ' );
print ( 'Failed: ${ stats [ 'failed' ]} ' );
4. Retry Failed Transactions
await transactionLocalService. retryFailedTransactions ();
await batchSyncService. syncPendingTransactions (deviceId : deviceId);
🚀 Setup & Installation
1. Update IsarDb Schema
Tambahkan TransactionLocalEntity ke schema Isar:
// lib/database/isar/isar_db.dart
final db = await Isar . open ([
BranchEntitySchema ,
ProductEntitySchema ,
TransactionLocalEntitySchema , // ← Add this
// ... other schemas
]);
2. Run Build Runner
Generate file .g.dart:
cd /Users/faisalaffan/Documents/01_FAISAL/01_HUSTLE/19_MUSHOLA_STORE/mstore_mobile
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs
3. Initialize OfflineSyncService
Di main.dart:
void main () async {
WidgetsFlutterBinding . ensureInitialized ();
// Initialize DI
await configureDependencies ();
// Initialize Isar
await getIt < IsarDb >(). open ();
// Initialize OfflineSyncService
getIt < OfflineSyncService >(). initialize (
transactionSyncTTL : const Duration (minutes : 2 ),
);
runApp ( MyApp ());
}
🔍 Monitoring & Debugging
AppLog Integration
Semua operasi offline-first sudah terintegrasi dengan AppLog:
AppLog . i ( 'transaction_local' , 'Transaction created offline' , meta : {
'offlineId' : offlineId,
'grandTotal' : grandTotal,
});
AppLog . i ( 'batch_sync' , 'Batch sync completed' , meta : {
'total' : results.length,
'success' : successCount,
'failed' : failedCount,
});
Isar Inspector
Gunakan Isar Inspector untuk debug database lokal:
flutter pub run isar_inspector
⚠️ Best Practices
Penting: Selalu gunakan offline_reference yang unik untuk mencegah duplikasi transaksi saat sync.
Jalankan cleanup transaksi yang sudah sync secara berkala untuk menghemat storage: await batchSyncService. cleanupOldSyncedTransactions (daysOld : 30 );
TTL default untuk transaction sync adalah 2 menit. Sesuaikan berdasarkan kebutuhan bisnis.
🐛 Troubleshooting
Transaksi Tidak Tersinkronisasi
Periksa koneksi internet
Cek status sync: await transactionLocalService.getStatistics()
Lihat log error di AppLog
Retry manual: await transactionLocalService.retryFailedTransactions()
Duplikasi Transaksi
Backend sudah menangani duplikasi menggunakan offline_reference. Jika tetap terjadi duplikasi:
Pastikan offline_reference unik (format: DEVICE_ID-timestamp)
Periksa index unique di database backend
Cek response dari batch-sync endpoint
Build Runner Error
# Clean dan rebuild
flutter clean
flutter pub get
flutter pub run build_runner clean
flutter pub run build_runner build --delete-conflicting-outputs
📚 Referensi
🎯 Next Steps