Flutter 提供了一種現代的響應式框架,豐富的組件集和工具,可是尚未如同 Android 中應用架構指南同樣的東西。html
的確,沒有任何終極架構方案能知足全部需求,但咱們面對的事實是,咱們正在開發的大多數移動應用至少具備如下的某些功能:android
考慮到這一點,我建立了一個示例應用,使用三種不一樣的架構方法解決徹底相同的問題。git
在屏幕中央向用戶顯示「加載用戶數據」按鈕。當用戶單擊該按鈕時,將異步加載數據,並使用加載指示器替換該按鈕。數據加載完成後,加載指示器將替換爲數據。github
讓咱們開始吧。數據庫
爲了簡單起見,我建立了類 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;
}
複製代碼
讓咱們按照大多數開發人員閱讀 Flutter 官方文檔後的方式構建應用。api
使用 Navigator
導航到 VanillaScreen
頁面。網絡
因爲組件的狀態可能會在其生命週期中屢次更改,所以咱們應該繼承 StatefulWidget
。實現有狀態組件還須要具備類 State
。類 _VanillaScreenState
中的字段 bool _isLoading
和 User _user
表示組件的狀態。在調用 build(BuildContext context)
方法以前,這兩個字段都已初始化。 建立組件狀態對象後,將調用 build(BuildContext context)
方法來構建 UI。關於如何構建表示組件當前狀態的全部決策都在 UI 聲明代碼中作出。架構
body: SafeArea(
child: _isLoading ? _buildLoading() : _buildBody(),
)
複製代碼
當用戶單擊「加載用戶詳細信息」按鈕時,爲了顯示進度指示器,咱們執行如下操做。app
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;
});
});
複製代碼
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()
方法。
notifyListeners()
時很難跟蹤。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
交互的最新快照來構建自身。
notifyListeners()
同樣。你能夠在這個 github repo 中查看以上示例的源代碼。
若是發現譯文存在錯誤或其餘須要改進的地方,敬請提出。