[译]高效的使用Flutter Inherited Widgets
原文地址: ofollow,noindex">ericwindmill.com/posts/inher…
[译者注:InheritedWidget在Flutter框架里面是一个非常重要的Widget,相当多的机制都是通这个Widget来实现;比如:Material库里面的主题管理(Theme), Scoped Model, Redux都是使用这个机制来实现]
如果你之前使用过Flutter,那么你可能已经使用过很多类里面的'of'方法
Theme.of(context).textTheme MediaQuery.of(context).size 复制代码
这些Widget(Theme, MediaQuery)都是Inherited Widgets。 在你的App的任何地方你都能访问到你的theme(主题对象),因为他们都是继承了的(继承至InheritedWidget)。 在Flutter中,sdk的每个部分都向开发人员公开,因此你可以自己利用inherited widget(译注: 使用了InheritedWidget的widget) 的优点。你可以使用定制的InheritedWidget来管理Flutter内置的state,类似于Redux的Store或者Vue的Vuex Store。 设置了类似于这样的Store之后,你就可以像这样子使用:
class RedText extends StatelessWidget { // ... Widget build(BuildContext context) { var state = StateContainer.of(context).state; return new Text( state.user.username, style: const TextStyle(color: Colors.red), ); // ... 复制代码
状态提升(Lifting State Up)
当你使用InheritedWidget作为你的状态管理工具时,你就要依赖这种‘状态提升’的架构模式。
我们来改造一下,创建Flutter新工程时候的启动模板工程(计数器App)。如果你要把这个App分拆分成两个页面,一个用来显示计数,一个用来改变计数器的数字。出乎意料,这个简单的App有点让人迷惑。每次你改变路由(routes)(译注:改变页面)时,你都得把这个状态(计数,Counter),来回传递。
InheritedWidget通过赋予整颗Widget树访问同一个状态(state)的权限,来解决这个问题。

了,然后你就不再关心这篇文章了:sob:。
相对于使用像Redux这样的框架,使用状态提升的优势在于:InheritedWidget非常容易理解和使用。
PS:我是Redux、Vuex等所有'ux'相关架构模式的粉丝,这只是你的Flutter工具箱里面的另一个工具;如果Redux超出了你的需要你就可以使用这个。
为什么要使用?
这个时候你可能会问,你为什么会需要使用InheritedWidget。为什么不能在你的app的根widget使用 stateful widget (译注:Flutter最底层的状态管理API, flutter.io/docs/develo… ) ?
确实,这就是我们这里要做的。inherited widget跟stateful widget结合,允许你将状态传递到他的所有的祖先节点(widget)。这是一个非常方便的widget, 所以你不需要在每个类里面去写代码把状态传递给他的孩子节点
第一部分:设置App页面骨架
在这个例子中,我们编写一个这样简单的App:

简单来说,这个App的状态是由根Widget提升的,当你点击提交表单,它在inherited widgets调用setState通知主页面有新的信息需要渲染。
1、Material App 的根节点
这只是你的Flutter App的标准配置
void main() { runApp(new UserApp()); } class UserApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( home: new HomeScreen(), ); } } 复制代码
2、(首页)HomeScreen Widget
到现在为止,这都是非常基础的。在好东西登场之前,先跟着这些往下走吧
class HomeScreen extends StatefulWidget { @override HomeScreenState createState() => new HomeScreenState(); } class HomeScreenState extends State<HomeScreen> { Widget get _logInPrompt { return new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ new Text( 'Please add user information', style: const TextStyle(fontSize: 18.0), ), ], ), ); } // All this method does is bring up the form page. void _updateUser(BuildContext context) { Navigator.push( context, new MaterialPageRoute( fullscreenDialog: true, builder: (context) { return new UpdateUserScreen(); }, ), ); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('Inherited Widget Test'), ), body: _logInPrompt, floatingActionButton: new FloatingActionButton( onPressed: () => _updateUser(context), child: new Icon(Icons.edit), ), ); } } 复制代码
3、(更新用户信息的页面)The UpdateUserScreen Widget
到目前为止,这个表单页面什么也没做。
class UpdateUserScreen extends StatelessWidget { static final GlobalKey<FormState> formKey = new GlobalKey<FormState>(); static final GlobalKey<FormFieldState<String>> firstNameKey = new GlobalKey<FormFieldState<String>>(); static final GlobalKey<FormFieldState<String>> lastNameKey = new GlobalKey<FormFieldState<String>>(); static final GlobalKey<FormFieldState<String>> emailKey = new GlobalKey<FormFieldState<String>>(); const UpdateUserScreen({Key key}) : super(key: key); @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('Edit User Info'), ), body: new Padding( padding: new EdgeInsets.all(16.0), child: new Form( key: formKey, autovalidate: false, child: new ListView( children: [ new TextFormField( key: firstNameKey, style: Theme.of(context).textTheme.headline, decoration: new InputDecoration( hintText: 'First Name', ), ), new TextFormField( key: lastNameKey, style: Theme.of(context).textTheme.headline, decoration: new InputDecoration( hintText: 'Last Name', ), ), new TextFormField( key: emailKey, style: Theme.of(context).textTheme.headline, decoration: new InputDecoration( hintText: 'Email Address', ), ) ], ), ), ), floatingActionButton: new FloatingActionButton( child: new Icon(Icons.add), onPressed: () { final form = formKey.currentState; if (form.validate()) { var firstName = firstNameKey.currentState.value; var lastName = lastNameKey.currentState.value; var email = emailKey.currentState.value; // Later, do some stuff here Navigator.pop(context); } }, ), ); } } 复制代码
第二部分:添加Inherited Widgets功能
1、添加两个Widget: StateContainer 和 InheritedStateContainer
新建一个文件命名为state_container.dart,所有事情都将在这里发生。 第一步,在那个文件中创建一个叫User的类。在真实的App里面这个可能是一个非常大的类叫AppState,在这里你保存了所有你的App需要访问的属性。
class User { String firstName; String lastName; String email; User(this.firstName, this.lastName, this.email); } 复制代码
InheritedWidget通过StatefulWidget进行连接成为Store。所以你的StateContainer其实是有3个类:
class StateContainer extends StatefulWidget class StateContainerState extends State<StateContainer> class _InheritedStateContainer extends InheritedWidget 复制代码
InheritedWidget和StateContainer都是最简单的设置,他们一旦被设置就不会被改变。逻辑主要存放在StateContainerState。先写前面两个类:
class _InheritedStateContainer extends InheritedWidget { // Data 是你整个的状态(state). 在我们的例子中就是 'User' final StateContainerState data; // 必须传入一个 孩子widget 和你的状态. _InheritedStateContainer({ Key key, @required this.data, @required Widget child, }) : super(key: key, child: child); // 这个一个内建方法可以在这里检查状态是否有变化. 如果没有变化就不需要重新创建所有Widget. @override bool updateShouldNotify(_InheritedStateContainer old) => true; } class StateContainer extends StatefulWidget { // You must pass through a child. final Widget child; final User user; StateContainer({ @required this.child, this.user, }); // 这个是所有一切的秘诀. 写一个你自己的'of'方法,像MediaQuery.of and Theme.of // 简单的说,就是:从指定的Widget类型获取data. static StateContainerState of(BuildContext context) { return (context.inheritFromWidgetOfExactType(_InheritedStateContainer) as _InheritedStateContainer).data; } @override StateContainerState createState() => new StateContainerState(); } 复制代码
那个'of'方法不应该做任何其他事情。事实上,这两个类可以永远单独存在。
2、StateContainerState Widget
你所有的逻辑和状态都存放在这里,对于这个App来说,你将在这里简单的封装和存储User对象
class StateContainerState extends State<StateContainer> { // Whichever properties you wanna pass around your app as state User user; // You can (and probably will) have methods on your StateContainer // These methods are then used through our your app to // change state. // Using setState() here tells Flutter to repaint all the // Widgets in the app that rely on the state you've changed. void updateUserInfo({firstName, lastName, email}) { if (user == null) { user = new User(firstName, lastName, email); setState(() { user = user; }); } else { setState(() { user.firstName = firstName ?? user.firstName; user.lastName = lastName ?? user.lastName; user.email = email ?? user.email; }); } } // Simple build method that just passes this state through // your InheritedWidget @override Widget build(BuildContext context) { return new _InheritedStateContainer( data: this, child: widget.child, ); } } 复制代码
如果你使用过Redux,你可以看到这里涉及到的概念非常少。更少的概念意味着,潜在的Bug也会少,但是对于一个简单的App来说,这样子非常的实际。这就是建立你的Store所需要做的所有工作。接下来,你只需要在你的类中加入需要的方法和属性。
3、重构首页和表单页面
首先用StateContainer封装你的App:
void main() { runApp(new StateContainer(child: new UserApp())); } 复制代码
就这样,现在你能在你整个App里面访问你的Store了。可以这样做:
// main.dart // ... class HomeScreenState extends State<HomeScreen> { // Make a class property for the data you want User user; // This Widget will display the users info: Widget get _userInfo { return new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ // This refers to the user in your store new Text("${user.firstName} ${user.lastName}", style: new TextStyle(fontSize: 24.0)), new Text(user.email, style: new TextStyle(fontSize: 24.0)), ], ), ); } Widget get _logInPrompt { // ... } void _updateUser(BuildContext context) { // ... } @override Widget build(BuildContext context) { // This is how you access your store. This container // is where your properties and methods live final container = StateContainer.of(context); // set the class's user user = container.user; var body = user != null ? _userInfo : _logInPrompt; return new Scaffold( appBar: new AppBar( title: new Text('Inherited Widget Test'), ), // The body will rerender to show user info // as its updated body: body, floatingActionButton: new FloatingActionButton( onPressed: () => _updateUser(context), child: new Icon(Icons.edit), ), ); } } 复制代码
非常简单的变化。表单页面也没什么不同:
// form_page.dart // ... class UpdateUserScreen extends StatelessWidget { // ... @override Widget build(BuildContext context) { // get reference to your store final container = StateContainer.of(context); return new Scaffold( // the form is the same until here: floatingActionButton: new FloatingActionButton( child: new Icon(Icons.add), onPressed: () { final form = formKey.currentState; if (form.validate()) { var firstName = firstNameKey.currentState.value; var lastName = lastNameKey.currentState.value; var email = emailKey.currentState.value; // This is a hack that isn't important // To this lesson. Basically, it prevents // The store from overriding user info // with an empty string if you only want // to change a single attribute if (firstName == '') { firstName = null; } if (lastName == '') { lastName = null; } if (email == '') { email = null; } // You can call the method from your store, // which will call set state and rerender // the widgets that rely on the user slice of state. // In this case, thats the home page container.updateUserInfo( firstName: firstName, lastName: lastName, email: email, ); Navigator.pop(context); } }, ), ); } } 复制代码
就是这样!InheritedWidget很简单,对于简单的App、原型等来说是一个非常好的选择。