๐ฏ 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),
);
}());
}
// lib/core/transaction/transaction_service.dart
Future < Either < Failure , TransactionCursorResponse >> getTransactionsCursor ({
int ? lastId,
int ? limit
}) async {
// 1) Read from Isar
final local = await getTransactionsCursorLocalOnly (
lastId : lastId,
limit : limit
);
// 2) Background refresh
_triggerBackgroundRefresh (lastId : lastId, limit : limit);
// 3) Return local
return local;
}
void _triggerBackgroundRefresh ({ int ? lastId, int ? limit}) {
unawaited (() async {
if ( ! await _isOnline ()) return ;
final syncResult = await syncTransactionsCursor (
lastId : lastId,
limit : limit
);
syncResult. fold (
(failure) => AppLog . w ( 'transaction' , 'Sync failed' ),
(success) => AppLog . i ( 'transaction' , 'Sync success' ),
);
}());
}
// lib/core/product/product_service.dart
Future < Either < Failure , MasterProductByBranchResult >> getProductsLocalThenRefresh ({
required String branchCode,
int limit = 100 ,
int lastId = 0 ,
String search = '' ,
}) async {
// 1) Read from Isar
final local = await getProductByBranchLocalOnly (
branchCode : branchCode,
limit : limit,
lastId : lastId,
search : search,
);
// 2) Background refresh
try {
if ( await _isOnline ()) {
unawaited ( refreshMasterInventory (
branchCode : branchCode,
limit : limit,
lastId : lastId,
search : search
));
}
} catch (_) {}
// 3) Return local
return local;
}
๐จ 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.