[译] 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 中查看以上示例的源代码。


若是发现译文存在错误或其余须要改进的地方,敬请提出。

相关文章
相关标签/搜索