[譯] Flutter 應用架構 101:Vanilla, Scoped Model, BLoC

Flutter 提供了一種現代的響應式框架,豐富的組件集和工具,可是尚未如同 Android 中應用架構指南同樣的東西。html

的確,沒有任何終極架構方案能知足全部需求,但咱們面對的事實是,咱們正在開發的大多數移動應用至少具備如下的某些功能:android

  1. 從網絡請求數據/向網絡上傳數據。
  2. 遍歷,轉換,準備數據並呈現給用戶。
  3. 向數據庫發送數據/從數據庫獲取數據。

考慮到這一點,我建立了一個示例應用,使用三種不一樣的架構方法解決徹底相同的問題。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;
}
複製代碼

Vanilla

讓咱們按照大多數開發人員閱讀 Flutter 官方文檔後的方式構建應用。api

使用 Navigator 導航到 VanillaScreen 頁面。網絡

因爲組件的狀態可能會在其生命週期中屢次更改,所以咱們應該繼承 StatefulWidget。實現有狀態組件還須要具備類 State。類 _VanillaScreenState 中的字段 bool _isLoadingUser _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;
  });
});
複製代碼

優勢

  1. 學習簡單,易於理解。
  2. 不須要第三方庫。

缺點

  1. 組件的狀態的每次改變都會重建整個組件樹。
  2. 它打破了單一責任原則。組件不只負責構建 UI,還負責數據加載,業務邏輯和狀態管理。
  3. 關於如何表示當前狀態的決策是在 UI 聲明代碼中作出的。若是咱們的狀態複雜一些,代碼可讀性會下降。

Scoped Model

Scoped Model是 第三方包,未包含在 Flutter 框架中。 這是 Scoped Model 開發人員的描述:

一組實用程序,容許您輕鬆地將數據模型從父組件傳遞到其後代。此外,它還會在模型更新時重建使用該模型的全部子項。該庫最初是從 Fuchsia 代碼庫中提取的。

讓咱們使用 Scoped Model 構建相同的頁面。首先,咱們須要經過 pubspec.yamldependencies 下添加 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() 方法。

優勢

  1. 業務邏輯,狀態管理和 UI 代碼分離。
  2. 簡單易學。

缺點

  1. 須要第三方庫。
  2. 隨着模型愈來愈複雜,在調用 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 個類來表示頁面的可能狀態:

  1. 當用戶打開一箇中心帶有按鈕的頁面時,狀態爲 UserInitState
  2. 當加載數據顯示加載指示器時,狀態爲 UserLoadingState
  3. 當數據加載完成並顯示在頁面上時,狀態爲 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 交互的最新快照來構建自身。

優勢

  1. 不須要第三方庫。
  2. 業務邏輯,狀態管理和 UI 邏輯分離。
  3. 這是響應式的。不須要額外的調用,就像 Scoped Model 的 notifyListeners() 同樣。

缺點

  1. 須要有使用 stream 或 rxdart 的經驗。

源碼

你能夠在這個 github repo 中查看以上示例的源代碼。


若是發現譯文存在錯誤或其餘須要改進的地方,敬請提出。

相關文章
相關標籤/搜索