原文地址: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、原型等来说是一个非常好的选择。