[译]高效的使用Flutter Inherited Widgets

·  阅读 3942

原文地址: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)的权限,来解决这个问题。

观看Brian Egan在DartConf 2018的演讲,可以了解到非常多优秀的Flutter架构概念,不要看太多,不然你就被说服去使用flutter_redux了,然后你就不再关心这篇文章了😭。

相对于使用像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、原型等来说是一个非常好的选择。

完整的源码在这里

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改