有些 Flutter 專案一開始本地儲存(local storage)很單純:存個 user preferences、做點 cache、放 application configurations。結果需求一多,就開始同時碰 SharedPreferences、Secure Storage、甚至 Realm / Hive 這種資料庫型的東西。這時候如果沒有一個集中管理的 StorageManager,presentation layer 很快會出現 code duplication、tight coupling,整體 complexity 也會跟著上去。
核心結論:為什麼 Flutter 需要 StorageManager(集中式 storage manager)
Flutter 專案導入 StorageManager 的目的,是把 SharedPreferences、Secure Storage、Realm 等不同 local storage 的初始化(configure)與 CRUD 存取收斂到同一個入口,避免 presentation layer 重複寫存取邏輯、降低 tight coupling,並用 Clients 提供受限且 type-safe 的資料介面,讓模組不會直接碰整個資料庫。
老實說,這類問題不是「會不會發生」,而是「你什麼時候開始覺得煩」。一開始你可能只存暗黑模式(isDarkTheme / isDarkMode 之類),但下一個 sprint 你就會想塞登入狀態、token、一些快取資料;再下一個 sprint 你開始有物件資料要存,SharedPreferences 立刻就不太像話了。然後你就開始在不同頁面、不同 service 重複寫初始化、重複寫讀寫,最後回頭看:啊,原來 presentation layer 已經被存儲細節佔滿。
沒有 storage manager 時,常見的 3 個後果(原文因果保留)
在沒有 Storage Manager 的情境下,專案通常會逐步走向三種狀況:code duplication、tight coupling、increased complexity。這些不是抽象詞,它們會直接反映在你改一個 storage 實作時,需要動到多少地方、以及你能不能好好測試與除錯。
-
Code duplication:初始化與存取邏輯會在多處重複出現。你今天在 A 模組做一次 configure,明天在 B 模組又做一次;讀寫 key-value 的方式也可能散落各處。
-
Tight coupling:presentation layer 直接依賴特定儲存方案的細節。當底層 storage 換了(例如原文舉例把 Realm 換成 Hive),你會發現改動不只在 storage 層,而是一路往上炸。
-
Increased complexity:存儲相關邏輯混進 presentation layer,維護成本與測試成本都上升。尤其當你要追 bug 時,會很痛苦:到底是 UI 流程錯,還是 storage 初始化時序錯?
對,這三點聽起來像教科書。但你真的遇到時,就不太像教科書了,會比較像「怎麼每個地方都在存東西,而且存法還不一樣」。很現實。
導入 Storage Manager 能得到什麼:Plug and Play、Centralised Source、Reduce Boilerplate
原文把 Storage Manager 的好處整理成 3 點:Plug and Play(可替換服務)、Centralised Source(集中式入口)、Reduce Boilerplate(減少重複初始化與樣板碼)。重點是:它們都指向同一件事——把 storage 的變動面集中,讓其他層(尤其 presentation layer)不用跟著抖。
-
Plug and Play:能在不影響其他 components / layers 的前提下替換 storage 服務。原文例子是「adding or replacing Realm to Hive」這種替換情境。
-
Centralised Source:整個 storage layer 有單一入口點(single source of point)。測試、除錯、追初始化順序,都比較可控。
-
Reduce Boilerplate:初始化不需要散在各處。你不必每個模組都寫一次 configure / open / init 類似的流程。
講到「集中式」,我腦中會直接浮現另一個詞:責任邊界。你把 storage 的責任收進 StorageManager,其他地方就比較不會「順手」做一些不該做的事。這其實是乾淨架構(clean architecture)那套精神在這裡的落地方式之一。
先分清楚 2 類儲存:Preference Handlers vs Database Handlers
原文在進入程式碼前,先把 storage handlers 分成 2 類,這個分類很關鍵,因為後面 Clients 也會跟著分成兩類(2 (handlers/clients分成兩類))。
-
Preference Handlers:用 key-value pairs 存簡單資料。原文舉例包含 SharedPreferences、Secure Storage、Get Storage 等。
-
Database Handlers:存更複雜的資料(例如 custom objects),並支援比較完整的 CRUD 操作。原文舉例包含 Hive、Realm 等。
這裡不要硬把它想成「大小之分」,比較像「存取模型不同」:key-value 走 key,database 走 primaryKey / 型別 / 物件集合。你混在一起寫,後面一定亂。
Handlers 是什麼:用共同介面封裝 storage 實作,並統一 configure
Handlers 的定位是「封裝特定 storage 服務的實作」,讓 StorageManager 能用一致方式操作不同 storage 機制。因為每個 handler 的初始化方式不同,所以原文先抽出 StorageConfigurable,統一 configure 這件事。
/// This class configures the storage mechanism.
abstract class StorageConfigurable {
/// This method should be called before using the storage mechanism.
Future<void> configure();
}
這段其實很直白:任何 storage 機制在使用前都要 configure,但 configure 的內容怎麼做,由各 handler 自己決定。你只要求它「必須提供 configure」。就這樣。很乾脆。
接著是 database 類型的 handler 介面:DatabaseHandleable
///
/// Abstract class representing a local database storage mechanism.
///
/// Provides a common interface for interacting with local storage.
/// [T] is type of objects stored in the database.
///
///
abstract class DatabaseHandleable<T> implements StorageConfigurable {
/// This method reads a object of specific type.
/// Requires a [primaryKey] that will be used to find an exact data that
/// user needs.
Future<S?> read<S extends T>(Object primaryKey);
/// This method returns all the data of specific type [S] from the database.
/// If nothing is found, it will return an empty [Iterable]
Future<List<S>> readAll<S extends T>();
/// This method add/update a data of specific type.
/// [value] is the data that we need to add in database
/// It is required to add [S] type in order to add data to the
/// particular field.
Future<void> write<S extends T>(
S value, {
bool shouldUpdate = true,
});
/// This method removes an entry of data from the database.
/// Needs [primaryKey] in order to find a particular data
Future<void> delete<S extends T>(Object primaryKey);
/// This method removes all data of a given type [S] from the database.
/// If not found, it will throw an error.
Future<void> deleteAll<S extends T>();
}
另一邊是 PreferenceHandleable:針對 key-value pairs 的介面。它同樣 implements StorageConfigurable,但 CRUD 形狀不同:read / write 用 key(String),delete / deleteAll 也圍繞 key-value 的模型。
/// Abstract class representing a key-value storage mechanism.
///
/// Provides a common interface for interacting with various
/// preference database.
///
/// This is for key-value pair storage database.
///
abstract class PreferenceHandleable implements StorageConfigurable {
/// This method reads a object of specific type.
/// Requires a [key] that will be used to find an exact data that
/// user needs.
Future<S?> read<S extends Object?>(String key);
/// This method removes an entry of data from the preference database.
/// Requires [key] in order to find a particular data
Future<void> delete(String key);
/// This method removes all the key-value pairs stored in
/// preference storage.
Future<void> deleteAll();
/// This method add/update data.
/// [value] is the data that we need to add in preference database.
/// particular key.
Future<void> write(
String key,
Object value,
);
}
講到這裡,你其實已經把「規則」寫出來了:任何 SharedPreferences / Secure Storage / Get Storage 這種偏 preference 的實作,都得符合 PreferenceHandleable;任何 Realm / Hive 這種偏 database 的實作,都得符合 DatabaseHandleable
Clients 的角色:提供受限介面,避免模組直接碰整個資料庫
Handlers 解決的是「怎麼跟某個 storage 互動」;Clients 解決的是「每個模組可以摸到多少」。原文的立場很清楚:如果沒有 clients,各模組可能存取與修改整個資料庫,帶來 security 和 integrity 的 potential risks,例如資料毀損或非預期修改。它不是說一定會發生,但風險是存在的。
原文也給了具體情境:login module 應該只碰 user authentication data,不該能隨便讀寫整個 database。這種「只拿你該拿的」的想法,就是 clients 在做的事。
同樣地,clients 也分成兩類,對應兩類 handlers:
KeyValuePreferenceClient:對 PreferenceHandleable 提供「綁定單一 key」的受限存取。
LocalDatabaseClient
:對 DatabaseHandleable 提供以 primaryKey / 型別為核心的 CRUD。
KeyValuePreferenceClient:把 key 綁死,讓模組少一點自由發揮
KeyValuePreferenceClient 的結構很固定:建構子拿一個 PreferenceHandleable handler,再拿一個 String key。之後 read / write / delete 都圍繞這個 key 進行。也就是說,模組拿到 client 後,不需要(也不應該)再到處傳 key 字串。
/// Provides a restricted interface for accessing and modifying specific data
/// within a preference storage mechanism.
///
/// This class encapsulates the logic for reading, writing, and deleting
/// data associated with a given key.
class KeyValuePreferenceClient {
/// Takes [PreferenceHandleable] handler instance and a [String] type key.
KeyValuePreferenceClient(
this._handler,
this._key,
);
late final PreferenceHandleable _handler;
final String _key;
/// This method reads a object of specific type.
/// user needs.
Future<S?> read<S extends Object>() async {
return _handler.read<S?>(_key);
}
/// This method removes an entry of data from the preference database.
Future<void> delete() async {
return _handler.delete(_key);
}
/// This method removes all the key-value pairs stored in
/// preference storage.
/// This method add/update data.
/// [value] is the data that we need to add in preference database.
/// particular key.
Future<void> write(Object value) async {
return _handler.write(_key, value);
}
/// This method removes All entries from the preference database.
Future<void> deleteAll() async {
return _handler.deleteAll();
}
}
你會注意到 deleteAll 也被放進來了,代表 client 仍然能觸發清空整個 preference storage。這個是否符合你的邊界設計,取決於你怎麼分配 client 的使用位置;原文在這裡只是示範 API 形狀,沒有進一步限制「哪些模組拿得到 deleteAll」。這點要自己小心。真的。
LocalDatabaseClient:針對物件資料的 CRUD 包裝
LocalDatabaseClient
/// Provides a restricted interface for accessing and modifying specific data
/// within a local storage mechanism.
///
/// This class encapsulates the logic for reading, writing, and deleting
/// data associated with a given primary key.
class LocalDatabaseClient<T extends Object> {
/// This class takes instance of [DatabaseHandleable],
/// and a primaryKey of type [Object]
LocalDatabaseClient(
this._handler,
);
late final DatabaseHandleable<Object> _handler;
/// This method returns a value of type [T] if it is found
/// Primary key is used to find the exact result.
/// If not found, it will return a [Null] value.
Future<T?> read(Object primaryKey) async {
return _handler.read<T>(primaryKey);
}
/// This method returns all the data of specific type [T] from the database.
/// If nothing is found, it will return an empty [Iterable]
Future<List<T>> readAll() async {
return _handler.readAll<T>();
}
/// This method add/update a data of specific type.
/// [value] is the data that we need to add in database
/// It is required to add [T] type in order to add data to the
/// particular field.
Future<void> write(
T value, {
bool shouldUpdate = true,
}) async {
return _handler.write<T>(value);
}
/// This method removes an entry of data from the database.
Future<void> delete(Object primaryKey) async {
return _handler.delete<T>(primaryKey);
}
/// This method removes all the entry of a specific data type in database.
Future<void> deleteAll() async {
return _handler.deleteAll<T>();
}
}
原文也點到一個可能讓讀者卡住的地方:LocalDatabaseClient
StorageManager:集中式入口(factory),負責 configure handlers 並發 client
StorageManager 在原文的定位非常明確:它是一個 factory,也是一個 single source,讓 presentation layer 用同一個入口取得不同 storage clients。它會整合多個 handlers,並在使用前呼叫 configure 來完成初始化。
/// This class act as a factory to provide different storage clients
/// instances.
/// Act as a single source that will be used in Presentation Layer to interact
/// with different storage handlers
class StorageManager {
/// Once the object is created, it will configure the instances of differnet
/// clients.
/// [PreferenceHandleable] and [DatabaseHandleable] is configured.
/// These are not required, It not passed, handlers will be
/// configured internally.
StorageManager({
required DatabaseHandleable<Object> realmHandler,
required PreferenceHandleable sharedPreferenceHandler,
required PreferenceHandleable secureStoreHandler,
}) : _secureStoreHandler = secureStoreHandler,
_sharedPreferenceHandler = sharedPreferenceHandler,
_realmHandler = realmHandler;
/// Instance of [RealmDatabaseHandler]
final DatabaseHandleable<Object> _realmHandler;
/// Instance of [SharedPreferenceHandler]
final PreferenceHandleable _sharedPreferenceHandler;
/// Instance of [SecureStorageHandler]
final PreferenceHandleable _secureStoreHandler;
/// This method configures all the dependency related to storage handlers.
Future<void> configure() async {
try {
await Future.wait(
[
_configureHandler(_sharedPreferenceHandler),
_configureHandler(_realmHandler),
_configureHandler(_secureStoreHandler),
],
eagerError: true,
);
} catch (e) {
// rethrow;
// Logs the first error that occurs in future.
// log error here as well if want to .
}
}
Future<void> _configureHandler(StorageConfigurable handler) async {
try {
await handler.configure();
} catch (e) {
rethrow;
}
}
/// A single soruce where all the `Secure Storage Handlers` can access
/// the database.
KeyValuePreferenceClient getSecureStorageClient(
SecureStorageKey key,
) =>
KeyValuePreferenceClient(_secureStoreHandler, key.name);
/// A single soruce where all the `Shared Prefernce Storage Handlers`
/// can access the database.
KeyValuePreferenceClient getPreferenceStorageClient(
PreferenceStorageKey key,
) =>
KeyValuePreferenceClient(_sharedPreferenceHandler, key.name);
/// A single soruce where all the `Local Storage Handlers`
/// can access the database.
LocalDatabaseClient<T> getLocalStorageClient<T extends Object>() =>
LocalDatabaseClient<T>(_realmHandler);
}
這段有幾個點,原文其實都有講到(只是散在不同段落):
-
configure 是必要步驟:StorageManager.configure() 會對 handlers 做 configure,使用 Future.wait 並設定 eagerError: true。也就是多個初始化一起跑,第一個錯就提早報錯。
-
錯誤處理意圖留在註解:catch 裡面留了 rethrow / log 的註解,表示這裡預期你可以接上日誌或調整錯誤策略,但原文沒有宣稱已完成日誌實作。
-
提供三種 client getter:getSecureStorageClient(SecureStorageKey)、getPreferenceStorageClient(PreferenceStorageKey)、getLocalStorageClient
()。presentation layer 不直接碰 handler,只拿 client。
有個小細節:原文註解提到「These are not required」以及「If not passed, handlers will be configured internally」,但同一段建構子參數是 required。就文章呈現而言,這是一種描述上的不一致;重點仍然是「StorageManager 需要拿到 handlers,並在使用前 configure」。如果你真的要做內部配置,那是另一套設計取捨,但原文沒有展開。
用 GetIt 在 runApp 前初始化:registerSingletonAsync 與使用方式
原文建議在 runApp 前初始化並註冊 StorageManager,讓各模組可以立刻做 CRUD,並避免每個模組重複 configure。這裡的範例使用 GetIt,並透過 registerSingletonAsync 以非同步方式建立 StorageManager、呼叫 configure、再回傳實例。
另外,原文程式碼區塊的語言標示寫成 php,但內容實際上是 Dart / GetIt 的用法;重寫時只更正描述,不改動程式碼本身。
GetIt.instance.registerSingletonAsync<StorageManager>(() async {
final storageManager = StorageManager(
sharedPreferenceHandler: SharedPreferenceHandler(),
realmHandler: RealmDatabaseHandler(),
secureStoreHandler: SecureStorageHandler(),
);
await storageManager.configure();
return storageManager;
})
註冊完成後,就可以透過 GetIt 取回 StorageManager,再拿到你要的 client:
final realmData = GetIt.instance<StorageManager>().getLocalStorageClient<RealmDataObject>();
final preferenceString = GetIt.instance<StorageManager>().getPreferenceStorageClient(PreferenceStorageKey.isDarkMode);
final secureString = GetIt.instance<StorageManager>().getSecureStorageClient(SecureStorageKey.secretKey1);
看到這裡其實就夠了:模組拿 client 做 read / write / delete / readAll / deleteAll,底下到底是 SharedPreferences 還是 Secure Storage,或者 Realm / Hive(原文把 Hive 放在可替換例子裡),模組本身不需要知道。它只需要知道 client 的介面。
可選增強(optional enhancements):StorageFailure 與 Type-Safe Keys
原文最後提了兩個 optional enhancements:Storage Failure(針對 storage 失敗建立失敗模型)以及 Type-Safe Keys(用 enum 或自訂 key 型別,避免硬編字串 key)。這些不是「一定要做」,但它們通常會讓系統更穩。
StorageFailure:當 storage client 出問題時,你需要能描述「發生什麼事」,並讓上層可以處理。原文用 sealed class StorageFailure implements Failure 來定義失敗藍圖,包含 title、message、errorCode(可選)。
/// Blueprint for Storage Failures
sealed class StorageFailure implements Failure {
const StorageFailure({
required this.title,
required this.message,
this.errorCode,
});
/// [title] for [StorageFailure]
@override
final String title;
/// [message] for [StorageFailure]
@override
final String message;
/// [errorCode] for [StorageFailure]
@override
final int? errorCode;
}
Type-Safe Keys:原文的想法是,不要到處傳 String keys,改用 enum(或 custom key type)集中管理。這能降低 key 拼錯、命名不一致的風險,也讓整個 key 的來源更清楚。
/// This provide a centrailised point where all the keys for key-valuse based
/// clients can be accessed.
enum PreferenceStorageKey {
isDarkTheme,
keyNameExample,
}
/// This enum provides a centralized location for storing secure storage keys.
///
/// By using an enum, we ensure consistency and avoid hardcoding keys
/// throughout the application.
enum SecureStorageKey {
secretStringData1,
userData,
}
你會發現 StorageManager.getPreferenceStorageClient(PreferenceStorageKey key) 與 getSecureStorageClient(SecureStorageKey key) 正好把這件事接起來:你在上層只傳 enum,底下用 key.name 轉成 String。這樣 key 的集合就被「鎖」在 enum 裡,不會散落各處。
結語:把 storage 變動面收斂到一個入口,其他層才會乾淨
StorageManager 的核心價值,是把不同 local storage(SharedPreferences、Secure Storage、Realm 等)的 configure 與存取方式抽象化,並用 Handlers + Clients 的分層讓模組只拿到需要的資料介面,降低 code duplication、tight coupling 與 complexity,讓 Flutter 專案在離線(offline-capable)、快取(cache)、偏好設定(user preferences)等需求成長時仍能維持可維護性。
至於你要不要把它做得更完整——例如把錯誤處理從註解變成一致策略、把 keys 管得更嚴——那就是下一步了。原文把方向點出來了,剩下就是你專案的取捨。就這樣。
