Flutter 提供了一種現代的響應式框架,豐富的元件集和工具,但是還沒有如同 Android 中應用架構指南一樣的東西。
的確,沒有任何終極架構方案能滿足所有需求,但我們面對的事實是,我們正在開發的大多數移動應用至少具有以下的某些功能:
- 從網路請求資料/向網路上傳資料。
- 遍歷,轉換,準備資料並呈現給使用者。
- 向資料庫傳送資料/從資料庫獲取資料。
考慮到這一點,我建立了一個示例應用,使用三種不同的架構方法解決完全相同的問題。
在螢幕中央向使用者顯示“載入使用者資料”按鈕。當使用者單擊該按鈕時,將非同步載入資料,並使用載入指示器替換該按鈕。資料載入完成後,載入指示器將替換為資料。
讓我們開始吧。
資料
為了簡單起見,我建立了類 Repository
,其中包含模擬非同步網路呼叫的方法 getUser()
,並返回帶有硬編碼值的 Future<User>
物件。
如果您不熟悉 Dart 中的 Futures 和非同步程式設計,可以通過這個教程或閱讀文件來了解更多相關資訊。
class Repository {
Future<User> getUser() async {
await Future.delayed(Duration(seconds: 2));
return User(name: 'John', surname: 'Smith');
}
}
複製程式碼
class User {
User({
@required this.name,
@required this.surname,
});
final String name;
final String surname;
}
複製程式碼
Vanilla
讓我們按照大多數開發人員閱讀 Flutter 官方文件後的方式構建應用。
使用 Navigator
導航到 VanillaScreen
頁面。
由於元件的狀態可能會在其生命週期中多次更改,因此我們應該繼承 StatefulWidget
。實現有狀態元件還需要具有類 State
。類 _VanillaScreenState
中的欄位 bool _isLoading
和 User _user
表示元件的狀態。在呼叫 build(BuildContext context)
方法之前,這兩個欄位都已初始化。
建立元件狀態物件後,將呼叫 build(BuildContext context)
方法來構建 UI。關於如何構建表示元件當前狀態的所有決策都在 UI 宣告程式碼中做出。
body: SafeArea(
child: _isLoading ? _buildLoading() : _buildBody(),
)
複製程式碼
當使用者單擊“載入使用者詳細資訊”按鈕時,為了顯示進度指示器,我們執行以下操作。
setState(() {
_isLoading = true;
});
複製程式碼
呼叫
setState()
會通知框架該物件的內部狀態已經發生改變,並有可能影響此子樹中的使用者介面,這會導致框架為此 State 物件安排構建。
這意味著在呼叫 setState()
方法後,框架再次呼叫 build(BuildContext context)
方法,並重建整個元件樹。由於 _isLoading
現在設定為 true
,因此呼叫 _buildLoading()
而不是 _buildBody()
,並在螢幕上顯示載入指示器。與當我們處理來自 getUser()
的回撥並呼叫 setState()
來重新分配 _isLoading
和 _user
欄位的情況相同。
widget._repository.getUser().then((user) {
setState(() {
_user = user;
_isLoading = false;
});
});
複製程式碼
優點
- 學習簡單,易於理解。
- 不需要第三方庫。
缺點
- 元件的狀態的每次改變都會重建整個元件樹。
- 它打破了單一責任原則。元件不僅負責構建 UI,還負責資料載入,業務邏輯和狀態管理。
- 關於如何表示當前狀態的決策是在 UI 宣告程式碼中做出的。如果我們的狀態複雜一些,程式碼可讀性會降低。
Scoped Model
Scoped Model是 第三方包,未包含在 Flutter 框架中。 這是 Scoped Model 開發人員的描述:
一組實用程式,允許您輕鬆地將資料模型從父元件傳遞到其後代。此外,它還會在模型更新時重建使用該模型的所有子項。該庫最初是從 Fuchsia 程式碼庫中提取的。
讓我們使用 Scoped Model 構建相同的頁面。首先,我們需要通過 pubspec.yaml
在 dependencies
下新增 scoped_model
依賴項來安裝 Scoped Model 包。
scoped_model: ^1.0.1
複製程式碼
讓我們看一下 UserModelScreen
元件,並將其與之前未使用 Scoped Model 構建的示例進行比較。由於我們想讓我們的模型可用於所有元件的後代,我們應該使用通用的 ScopedModel
包裝它並提供元件和模型。
class UserModelScreen extends StatefulWidget {
UserModelScreen(this._repository);
final Repository _repository;
@override
State<StatefulWidget> createState() => _UserModelScreenState();
}
class _UserModelScreenState extends State<UserModelScreen> {
UserModel _userModel;
@override
void initState() {
_userModel = UserModel(widget._repository);
super.initState();
}
@override
Widget build(BuildContext context) {
return ScopedModel(
model: _userModel,
child: Scaffold(
appBar: AppBar(
title: const Text('Scoped model'),
),
body: SafeArea(
child: ScopedModelDescendant<UserModel>(
builder: (context, child, model) {
if (model.isLoading) {
return _buildLoading();
} else {
if (model.user != null) {
return _buildContent(model);
} else {
return _buildInit(model);
}
}
},
),
),
),
);
}
Widget _buildInit(UserModel userModel) {
return Center(
child: RaisedButton(
child: const Text('Load user data'),
onPressed: () {
userModel.loadUserData();
},
),
);
}
Widget _buildContent(UserModel userModel) {
return Center(
child: Text('Hello ${userModel.user.name} ${userModel.user.surname}'),
);
}
Widget _buildLoading() {
return const Center(
child: CircularProgressIndicator(),
);
}
}
複製程式碼
在前面的示例中,當元件的的狀態發生更改時,重建了整個元件樹。但我們真的需要重建整個頁面嗎?例如,AppBar 根本不應該改變,因此重建它沒有意義。理想情況下,我們應該只重建那些更新的元件。Scoped Model 可以幫助我們解決這個問題。
ScopedModelDescendant<UserModel>
元件用於在元件樹中查詢 UserModel
。只要 UserModel
通知發生了更改,它就會自動重建。
另一個改進是 UserModelScreen
不再負責狀態管理和業務邏輯。
我們來看看 UserModel
程式碼。
class UserModel extends Model {
UserModel(this._repository);
final Repository _repository;
bool _isLoading = false;
User _user;
User get user => _user;
bool get isLoading => _isLoading;
void loadUserData() {
_isLoading = true;
notifyListeners();
_repository.getUser().then((user) {
_user = user;
_isLoading = false;
notifyListeners();
});
}
static UserModel of(BuildContext context) =>
ScopedModel.of<UserModel>(context);
}
複製程式碼
現在 UserModel
儲存並管理狀態。為了通知監聽器(並重建後代)發生了更改,應呼叫 notifyListeners()
方法。
優點
- 業務邏輯,狀態管理和 UI 程式碼分離。
- 簡單易學。
缺點
- 需要第三方庫。
- 隨著模型越來越複雜,在呼叫
notifyListeners()
時很難跟蹤。
BLoC
BLoC(Business Logic Components)是 Google 開發人員推薦的模式。它利用流功能來管理和廣播狀態更改。
對於 Android 開發人員:您可以將 Bloc
物件視為 ViewModel
,將 StreamController
視為 LiveData
。這將使以下程式碼非常簡單,因為您已經熟悉了這些概念。
class UserBloc {
UserBloc(this._repository);
final Repository _repository;
final _userStreamController = StreamController<UserState>();
Stream<UserState> get user => _userStreamController.stream;
void loadUserData() {
_userStreamController.sink.add(UserState._userLoading());
_repository.getUser().then((user) {
_userStreamController.sink.add(UserState._userData(user));
});
}
void dispose() {
_userStreamController.close();
}
}
class UserState {
UserState();
factory UserState._userData(User user) = UserDataState;
factory UserState._userLoading() = UserLoadingState;
}
class UserInitState extends UserState {}
class UserLoadingState extends UserState {}
class UserDataState extends UserState {
UserDataState(this.user);
final User user;
}
複製程式碼
當狀態改變時,不需要額外的方法呼叫來通知訂閱者。
我建立了 3 個類來表示頁面的可能狀態:
- 當使用者開啟一箇中心帶有按鈕的頁面時,狀態為
UserInitState
。 - 當載入資料顯示載入指示器時,狀態為
UserLoadingState
。 - 當資料載入完成並顯示在頁面上時,狀態為
UserDataState
。
以這種方式廣播狀態更改允許我們擺脫 UI 宣告程式碼中的所有邏輯。在使用 Scoped Model 的示例中,我們仍在檢查 UI 宣告程式碼中的 _isLoading
是否為 true
,以決定我們應該呈現哪個元件。在 BLoC 的示例中,我們正在廣播頁面的狀態,UserBlocScreen
元件的唯一責任是呈現此狀態的 UI。
class UserBlocScreen extends StatefulWidget {
UserBlocScreen(this._repository);
final Repository _repository;
@override
State<StatefulWidget> createState() => _UserBlocScreenState();
}
class _UserBlocScreenState extends State<UserBlocScreen> {
UserBloc _userBloc;
@override
void initState() {
_userBloc = UserBloc(widget._repository);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Bloc'),
),
body: SafeArea(
child: StreamBuilder<UserState>(
stream: _userBloc.user,
initialData: UserInitState(),
builder: (context, snapshot) {
if (snapshot.data is UserInitState) {
return _buildInit();
}
if (snapshot.data is UserDataState) {
UserDataState state = snapshot.data;
return _buildContent(state.user);
}
if (snapshot.data is UserLoadingState) {
return _buildLoading();
}
},
),
),
);
}
Widget _buildInit() {
return Center(
child: RaisedButton(
child: const Text('Load user data'),
onPressed: () {
_userBloc.loadUserData();
},
),
);
}
Widget _buildContent(User user) {
return Center(
child: Text('Hello ${user.name} ${user.surname}'),
);
}
Widget _buildLoading() {
return const Center(
child: CircularProgressIndicator(),
);
}
@override
void dispose() {
_userBloc.dispose();
super.dispose();
}
}
複製程式碼
與前面的示例相比,UserBlocScreen
程式碼變得更加簡單。我們使用 StreamBuilder
監聽狀態更改。
StreamBuilder
是一個 StatefulWidget
,它基於與 Stream
互動的最新快照來構建自身。
優點
- 不需要第三方庫。
- 業務邏輯,狀態管理和 UI 邏輯分離。
- 這是響應式的。不需要額外的呼叫,就像 Scoped Model 的
notifyListeners()
一樣。
缺點
- 需要有使用 stream 或 rxdart 的經驗。
原始碼
你可以在這個 github repo 中檢視以上示例的原始碼。
如果發現譯文存在錯誤或其他需要改進的地方,敬請提出。