[譯]高效的使用Flutter Inherited Widgets

原文地址:ericwindmill.com/posts/inher…git

[譯者注:InheritedWidget在Flutter框架裏面是一個很是重要的Widget,至關多的機制都是經過這個Widget來實現;好比:Material庫裏面的主題管理(Theme), Scoped Model, Redux都是使用這個機制來實現]github

若是你以前使用過Flutter,那麼你可能已經使用過不少類裏面的'of'方法redux

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以後,你就能夠像這樣子使用:bash

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),來回傳遞。app

InheritedWidget經過賦予整顆Widget樹訪問同一個狀態(state)的權限,來解決這個問題。框架

觀看 Brian Egan在DartConf 2018的演講,能夠了解到很是多優秀的Flutter架構概念,不要看太多,否則你就被說服去使用 flutter_redux了,而後你就再也不關心這篇文章了😭。

相對於使用像Redux這樣的框架,使用狀態提高的優點在於:InheritedWidget很是容易理解和使用。less

PS: 我是Redux、Vuex等全部'ux'相關架構模式的粉絲,這只是你的Flutter工具箱裏面的另外一個工具;若是Redux超出了你的須要你就可使用這個。ide

爲何要使用?

這個時候你可能會問,你爲何會須要使用InheritedWidget。爲何不能在你的app的根widget使用 stateful widget (譯註:Flutter最底層的狀態管理API, flutter.io/docs/develo…) ?工具

確實,這就是咱們這裏要作的。inherited widget跟stateful widget結合,容許你將狀態傳遞到他的全部的祖先節點(widget)。這是一個很是方便的widget, 因此你不須要在每一個類裏面去寫代碼把狀態傳遞給他的孩子節點

第一部分:設置App頁面骨架

在這個例子中,咱們編寫一個這樣簡單的App:

簡單來講,這個App的狀態是由根Widget提高的,當你點擊提交表單,它在inherited widgets調用setState通知主頁面有新的信息須要渲染。

一、Material App 的根節點

這只是你的Flutter App的標準配置

void main() {
  runApp(new UserApp());
}

class UserApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new HomeScreen(),
    );
  }
}
複製代碼

二、(首頁)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),
      ),
    );
  }
}
複製代碼

三、(更新用戶信息的頁面)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功能

一、添加兩個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'方法不該該作任何其餘事情。事實上,這兩個類能夠永遠單獨存在。

二、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所須要作的全部工做。接下來,你只須要在你的類中加入須要的方法和屬性。

三、重構首頁和表單頁面

首先用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、原型等來講是一個很是好的選擇。

完整的源碼在這裏

相關文章
相關標籤/搜索