Home Coding Inherited Widgets trong Flutter

Inherited Widgets trong Flutter

by Khanh Tran

Nếu bạn đã từng làm việc với Flutter trước đây, ắt hẳn bạn sẽ thường xuyên bắt gặp phương thức of trong một số class. Ví dụ như:

Theme.of(context).textTheme
MediaQuery.of(context).size

Các widget đó (MediaQuery, Theme) được gọi là Inherited Widgets mà Flutter đã tích hợp sẵn. Chúng có một phương thức đặc biệt chính là of method, mà bạn có thể sử dụng phương thức đó để truy cập các thuộc tính ở bất cứ đâu trong Widget tree.

Bạn có thể tận dụng điều này bằng nhiều cách, ví dụ như custom một Widgets của riêng bạn cách mở rộng  InheritedWidget. Khi bạn đã có một InheritedWidget  hoạt động tại thư mục gốc của ứng dụng, bạn có thể sử dụng một of method để truy cập các thuộc tính của nó ở bất cứ đâu trong ứng dụng của bạn. Bạn có thể sử dụng một custom InheritedWidget làm bộ lưu trữ trạng thái trung tâm tích hợp sẵn (built-in central state storage), tương tự như Redux Store hoặc Vue’s Vuex Store.

Sau khi bạn thiết lập một store, bạn sẽ có thể thực hiện một số việc như sau:

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),
    );
  // ...

Set up Material App

Set up Material App Root

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

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

HomeScreen Widget

Thêm đoạn code sau xuống phía dưới đoạn code phía trên:

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),
      ),
    );
  }
}

Thêm UpdateUserScreen Widget

Tiếp theo, thêm 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);
          }
        },
      ),
    );
  }
}
Hình 1: Màn hình chính
Hình 2: Màn hình updateUser

Set up Inherited Widget

StateContainer và InheritedStateContainer Widgets

Tạo một tệp mới có tên state_container.dart. Đây là nơi tất cả mọi thứ xảy ra.

Đầu tiên, trong tệp đó, tạo một class đơn giản được gọi là User. Đây là nơi bạn lưu tất cả các thuộc tính mà bạn muốn truy cập trên ứng dụng của mình.

class User {
  String firstName;
  String lastName;
  String email;

  User(this.firstName, this.lastName, this.email);
}

InheritedWidget hoạt động như một store bằng cách kết nối với StatefulWidget. Vì vậy, StateContainer của bạn gồm có ba lớp:

class StateContainer extends StatefulWidget
class StateContainerState extends State<StateContainer>
class _InheritedStateContainer extends InheritedWidget

Các InheritedWidgetvà một StateContainerlà cách thiết lập đơn giản nhất, và chúng sẽ không thay đổi. Các Logic chủ yếu trong StateContainerState. Trước tiên là 2 class StateContainer và _InheritedStateContainer:

class _InheritedStateContainer extends InheritedWidget {
   // Data is your entire state. In our case just 'User' 
  final StateContainerState data;
   
  // You must pass through a child and your state.
  _InheritedStateContainer({
    Key key,
    @required this.data,
    @required Widget child,
  }) : super(key: key, child: child);

  // This is a built in method which you can use to check if
  // any state has changed. If not, no reason to rebuild all the widgets
  // that rely on your state.
  @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,
  });

  // This is the secret sauce. Write your own 'of' method that will behave
  // Exactly like MediaQuery.of and Theme.of
  // It basically says 'get the data from the widget of this type.
  static StateContainerState of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(_InheritedStateContainer)
            as _InheritedStateContainer).data;
  }
  
  @override
  StateContainerState createState() => new StateContainerState();
}

StateContainerState Widget

Widget này là nơi tất cả state và logic của bạn có thể  hoặt động. Đối với ứng dụng này, bạn chỉ cần lưu trữ và thao tác với người dùng của mình.

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,
    );
  }
}

Refactor Home and Form screens

Đầu tiên, thêm StateContainer vào hàm main:

void main() {
  runApp(new StateContainer(child: new UserApp()));
}

Vậy là xong: bây giờ bạn có thể truy cập store của mình từ mọi nơi trong ứng dụng.

// 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),
      ),
    );
  }
}

Chỉnh sửa class UpdateUserScreen

@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);
          }
        },
      ),
    );
  }
Hình 3: UpdateUserScreen
Hình 4: Main Screen

Cấu trúc thư mục hiện tại:

Hình 5: Cấu trúc thư mục

Đã xong! InheritedWidget rất dễ dàng và là một lựa chọn cực kỳ khả thi cho các ứng dụng đơn giản, nguyên mẫu, v.v…

You may also like

Leave a Comment