Beyond Clean Architecture: The Iceberg Pattern for Real-Time Flutter Apps with BlocSignal
Beyond Clean Architecture: The Iceberg Pattern for Real-Time Flutter Apps with BlocSignal ## Introduction to the Problem Why does traditional clean architecture not fit modern real-world applications? Most Flutter...


Beyond Clean Architecture: The Iceberg Pattern for Real-Time Flutter Apps with BlocSignal
Introduction to the Problem
Why does traditional clean architecture not fit modern real-world applications? Most Flutter tutorials follow Uncle Bob's advice and use the classic BLoC structure, but they demonstrate architecture exclusively with static REST requests or trivial counter apps. As soon as you start building a modern application based on data storage and powerful cloud services, such an architecture quickly turns into one of two anti-patterns:
- "Anemic Lasagna": Layers of intermediary classes that simply redirect method calls to the lower layer without transforming data, concluding invariants, or providing architectural protection.
- Super-complex Streams and Macros: Synchronizing multiple live cloud streams using intricate Rx cha
els, nested subscriptions, and state flags, which can lead to errors.
Reality of Real-Time and Iceberg Pattern
Reality of Cloud Applications
When working with cloud services like Firebase Cloud Firestore or Supabase, you need to manage real-time data streams. This means your architecture must be capable of handling simultaneous data changes and ensuring reliable synchronization between the client and server.
Problems with Classical Clean Architecture
Classical clean architecture with REST APIs can be too complex and inefficient for real-world applications. It requires large amounts of code and u
ecessary rituals, especially when it comes to simple data reading operations.
Introduction to Iceberg Pattern
Iceberg Pattern offers a solution to this problem by combining reactive signals and one-way BLoC facades to create an efficient architecture for real-world applications.
Detailed Description of Iceberg Pattern
Persisted Data
Iceberg Pattern provides persisted data, which are retained even after navigating between screens. This improves performance and reduces the number of server requests.
// Example of using persisted data
final cache = ValueNotifier<Map<String, dynamic>>({});
void updateCache(String key, dynamic value) {
cache.value[key] = value;
}
void fetchFromCache(String key) {
final cachedValue = cache.value[key];
if (cachedValue != null) {
// Use cached value
} else {
// Fetch value from data source
}
}
Optimistic Mutations
Iceberg Pattern also provides support for optimistic mutations, where changes are displayed on the screen before they are confirmed by the server. This ensures a smoother user experience.
// Example of optimistic mutation
void optimisticUpdate(String key, dynamic newValue) async {
// Update data on screen
setState(() {
cache.value[key] = newValue;
});
try {
// Send request to server
await server.update(key, newValue);
// Update cache after successful request
updateCache(key, newValue);
} catch (e) {
// Rollback changes on error
setState(() {
cache.value[key] = oldValue;
});
}
}
Asynchronous Reconciliation
Iceberg Pattern automatically synchronizes changes between the client and the server, ensuring correct recovery after errors and conflicts.
// Example of asynchronous reconciliation
Future<void> reconcileData() async {
final localChanges = getLocalChanges();
final serverChanges = await fetchServerChanges();
// Handle conflicts and synchronize data
await applyChanges(localChanges, serverChanges);
}
Practical Tips
Using ValueNotifiers for Data Caching
ValueNotifiers allow easy updating and synchronization of data across architecture layers.
final cache = ValueNotifier<Map<String, dynamic>>({});
Error Handling and Rollbacks
It is important always to have a rollback mechanism for changes in case of errors.
try {
// Perform operation
} catch (e) {
// Rollback changes
}
Automatic UI Updates
Use StreamBuilder or StreamProvider for automatic UI updates in response to data changes.
StreamBuilder(
stream: dataStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.toString());
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return CircularProgressIndicator();
}
},
)
Modularity and Scalability
Iceberg Pattern supports modularity and scalability, allowing easy addition of new features and components.
Conclusion
Iceberg Pattern provides an effective solution for creating real-world Flutter applications with real cloud services. It combines the benefits of clean architecture and reactive signals, ensuring optimized user experience and reliable data synchronization.