🎯 Overview
Pure Local Architecture adalah evolusi dari offline-first strategy yang memastikan 100% data read dari Isar dengan background sync menggunakan SWR (Stale-While-Revalidate) pattern.
Key Principles
Always Local First : Semua read operation dari Isar DB (< 10ms)
Background Sync : API calls non-blocking, update Isar silently
No Loading Spinner : UI selalu show data, never wait for API
Offline = Online : Consistent UX regardless of network state
🏗️ Architecture
Flow Diagram
📊 Implementation Status
Data Type Status File Pattern Products ✅ Done product_service.dartSWR Inventory ✅ Done product_service.dartSWR Branches ✅ Done branches_service.dartPure Local + SWR Transaction History ✅ Done transaction_service.dartPure Local + SWR New Transactions ✅ Done checkout_bloc.dartOffline-First Settings ✅ Template settings_service.dartPure Local + SWR Reports ✅ Template reports_service.dartPure Local + SWR
💻 Code Implementation
Service Pattern Template
import 'dart:async' ;
import 'package:connectivity_plus/connectivity_plus.dart' ;
import 'package:dartz/dartz.dart' ;
import 'package:injectable/injectable.dart' ;
import 'package:mstore_mobile/database/isar/isar_db.dart' ;
import 'package:mstore_mobile/pkg/utils/app_log.dart' ;
import 'package:mstore_mobile/pkg/utils/failures.dart' ;
@LazySingleton ()
class YourService {
final YourRepository _repository;
final IsarDb _isarDb;
/// Pure local read dengan SWR pattern
/// Selalu return data dari Isar, trigger background refresh jika online
Future < Either < Failure , YourResponse >> getData () async {
// 1) SELALU baca dari Isar dulu (instant, no loading)
final local = await _getDataLocalOnly ();
// 2) Trigger background refresh (non-blocking)
_triggerBackgroundRefresh ();
// 3) Return local data immediately
return local;
}
/// Background refresh: update Isar dari API secara silent
void _triggerBackgroundRefresh () {
unawaited (() async {
try {
// Check connectivity
final results = await Connectivity (). checkConnectivity ();
final isOnline = results. any ((r) => r != ConnectivityResult .none);
if ( ! isOnline) {
AppLog . d ( 'service' , 'Skip bg refresh: offline' );
return ;
}
AppLog . d ( 'service' , 'Triggering background refresh...' );
final apiResult = await _repository. fetchFromApi ();
await apiResult. fold (
(failure) async {
AppLog . w ( 'service' , 'Background refresh failed: ${ failure . message } ' );
},
(data) async {
await _upsertToIsar (data);
AppLog . i ( 'service' , 'Background refresh success' );
},
);
} catch (e) {
AppLog . e ( 'service' , 'Background refresh error' , e as Object ? );
}
}());
}
/// Read from Isar only (no network)
Future < Either < Failure , YourResponse >> _getDataLocalOnly () async {
try {
final data = await _isarDb.isar.yourEntitys. where (). findAll ();
return Right ( YourResponse (data : data));
} catch (e) {
return Left ( CacheFailure (message : 'Gagal membaca data lokal: $ e ' ));
}
}
/// Upsert API data to Isar
Future < void > _upsertToIsar ( YourApiData apiData) async {
await _isarDb.isar. writeTxn (() async {
final entities = apiData.items. map ((item) => YourEntity ()
..apiId = item.id
..name = item.name
..updatedAt = DateTime . now ()
). toList ();
await _isarDb.isar.yourEntitys. putAll (entities);
});
}
}
Real Implementation Examples
Branches Service
Transaction History
Products & Inventory
// lib/core/branches/branches_service.dart
Future < Either < Failure , BranchResponse >> getMerchantBranches ({
bool forceNetwork = false
}) async {
// 1) Read from Isar instantly
final local = await _getMerchantBranchesLocalOnly ();
// 2) Trigger background refresh
_triggerBackgroundRefresh ();
// 3) Return local
return local;
}
void _triggerBackgroundRefresh () {
unawaited (() async {
if ( ! await _isOnline ()) return ;
final netEither = await repository. getMerchantBranches ();
await netEither. fold (
(failure) async => AppLog . w ( 'branches' , 'Refresh failed' ),
(resp) async => await _upsertBranchesFromApi (resp),
);
}());
}
🎨 UI Pattern
No Loading Spinner
JANGAN show loading spinner untuk data refresh!
// ❌ WRONG: Show loading spinner
BlocBuilder < DataBloc , DataState >(
builder : (context, state) {
if (state is DataLoading ) {
return CircularProgressIndicator (); // ❌ Bad UX
}
return ListView (children : state.data. map (...));
},
)
// ✅ CORRECT: Always show data
BlocBuilder < DataBloc , DataState >(
builder : (context, state) {
// Langsung show data dari Isar
// Background refresh akan auto-update via stream
return ListView (children : state.data. map (...));
},
)
Isar Stream for Auto-Update
StreamBuilder < List < YourEntity >>(
stream : isarDb.isar.yourEntitys. watchLazy (),
builder : (context, snapshot) {
// Trigger fetch (akan return dari Isar + bg refresh)
context. read < YourBloc >(). add ( LoadData ());
return BlocBuilder < YourBloc , YourState >(
builder : (context, state) {
if (state is YourLoaded ) {
return ListView (children : state.data. map (...));
}
return EmptyState (); // Jika Isar kosong
},
);
},
)
Pull-to-Refresh
RefreshIndicator (
onRefresh : () async {
// Force manual sync
context. read < YourBloc >(). add ( RefreshData (force : true ));
await Future . delayed ( Duration (milliseconds : 500 ));
},
child : ListView (...),
)
🚀 Benefits
User Experience
⚡ Instant Load Data muncul < 10ms dari Isar, tidak ada loading spinner
📱 Offline = Online UX konsisten, app selalu berfungsi tanpa network
🔄 Auto-Update Background sync silent, user tidak perlu manual refresh
🎯 No Blocking User bisa langsung interaksi, tidak tunggu API
Developer Experience
🧪 Easy Testing Mock Isar lebih mudah daripada mock API
🐛 Better Debugging Data selalu ada di local, mudah inspect
📝 Simple Code Tidak ada complex fallback logic
🔧 Maintainable Single source of truth (Isar)
Fast : Isar query < 10ms vs API 100-500ms
Reduced API Calls : Background only, tidak setiap screen
Battery Friendly : Less network activity
Scalable : Isar handle millions of records
📋 Migration Checklist
From API-First to Pure Local
📚 Code References
Backend (Go)
internal/domains/*/service.go - Service layer
internal/domains/*/repository.go - Repository layer
internal/domains/*/handler.go - HTTP handlers
Mobile (Flutter)
lib/core/*/service.dart - Service layer dengan SWR
lib/core/*/repository.dart - Repository layer
lib/database/isar/entities/*.dart - Isar entities
lib/features/*/bloc/*.dart - Bloc state management
🎯 Best Practices
1. Always Read from Isar First
// ✅ CORRECT
Future < Data > getData () async {
final local = await _getLocalOnly ();
_triggerBackgroundRefresh ();
return local;
}
// ❌ WRONG
Future < Data > getData () async {
try {
return await api. fetch (); // Block UI
} catch (e) {
return await _getLocalOnly (); // Fallback
}
}
2. Non-Blocking Background Sync
// ✅ CORRECT
void _triggerBackgroundRefresh () {
unawaited (() async {
// Non-blocking async
}());
}
// ❌ WRONG
Future < void > _triggerBackgroundRefresh () async {
await api. fetch (); // Blocks caller
}
3. Connectivity Check Before API
// ✅ CORRECT
void _triggerBackgroundRefresh () {
unawaited (() async {
if ( ! await _isOnline ()) return ; // Skip if offline
await api. fetch ();
}());
}
// ❌ WRONG
void _triggerBackgroundRefresh () {
unawaited (() async {
await api. fetch (); // Fail if offline
}());
}
// ✅ CORRECT
void _triggerBackgroundRefresh () {
unawaited (() async {
try {
await api. fetch ();
} catch (e) {
AppLog . w ( 'service' , 'Refresh failed: $ e ' );
// Silent fail, tidak block user
}
}());
}
// ❌ WRONG
void _triggerBackgroundRefresh () {
unawaited (() async {
await api. fetch (); // Throw error ke user
}());
}
🎓 Summary
Pure Local Architecture + SWR adalah pattern yang memastikan:
✅ 100% data dari Isar (instant load)
✅ Background sync (silent update)
✅ No loading spinner (always show data)
✅ Offline = Online (consistent UX)
Result : App yang super fast , always works , dan auto-update tanpa user intervention.
Dokumentasi ini adalah living document . Update sesuai dengan evolusi implementation.