Scope во Flutter с InheritedWidget

Scope во Flutter с InheritedWidget

Пример как можно использовать InheritedWidget и StatefulWidget для создания UserScope.

Как мы знаем, Flutter приложение обычно состоит из большого количества виджетов. Чтобы ускорить получение доступа к данным по дереву можно использовать InheritedWidget. Именно на этом виджете основана работа Provider.

Когда нужно передавать какие либо данные между виджетами, например информацию о корзине, пользователе, настройках, локалях, темах и тд., нужно обернуть дерево виджетов максимально высоко над всеми контекстами которые нуждаются в доступе к данным.

Рассмотрим пример. Допустим у нас есть где то стейт-менеджер который хранит текущего пользователя. В данном случае для упрощения примера я буду хранить юзера прям в HomePage, так как это виджет верхнего уровня, а юзер мне может понадобится в любом месте приложения. У меня есть экран HomePage, а в нем UserBar, в котором UserView, а он уже выводит приветствие юзера, и UserLogin для отрисовки кнопки.

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class User {
  final int id;
  final String name;
  User({
    @required this.id,
    @required this.name,
  }): assert(id != null), assert(name != null);
}

class UserState extends InheritedWidget {
  const UserState({
    @required this.user,
    @required Widget child,
  }) : assert(child != null),
        super(child: child);

  final User user;

  @override
  bool updateShouldNotify(UserState old) => user != old.user;

  static User of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<UserState>().user;
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => HomePageState();

  static HomePageState of(BuildContext context) =>
      context.findAncestorStateOfType<HomePageState>();
}

class HomePageState extends State<HomePage> {
  User _user;

  User get user => _user;

  set user(User user) => setState(() {
    _user = user;
  });

  @override
  Widget build(BuildContext context) => Scaffold(
      body: Container(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            UserLogin(),
            UserState(user: _user, child: UserBar()),
          ],
        ),
      )
  );
}

class UserLogin extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final homeState = HomePage.of(context);
    return homeState.user == null
        ? RaisedButton(
        onPressed: () => homeState.user = User(id: 1, name: 'Александр'),
        child: Text('Войти')
    ) : RaisedButton(
        onPressed: () => homeState.user = null,
        child: Text('Выйти')
    );
  }
}

class UserBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        width: double.infinity,
        padding: EdgeInsets.all(20),
        color: Color(Random().nextInt(0xffffffff)).withAlpha(0xff),
        child: Center(child: UserView())
    );
  }
}

class UserView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final user = UserState.of(context);
    return user == null ? Container() : Container(
        color: Colors.blueGrey,
        padding: EdgeInsets.all(12.0),
        child: Text(
            "Привет, ${user.name}!",
            style: TextStyle(color: Colors.white)
        )
    );
  }
}

Чтобы установить нового юзера, приходится вызвать setState для пересоздания UserState с новыми данными.

Можно запустить код на сайте DartPad и посмотреть результат.


Вы могли заметить что цвет фона изменился, но цвет задается при отрисовке виджета UserBar. То есть изменение в виджете HomePage вызвали перерисовку всех вложенных виджетов, хотя у нас нет нужды перерисовывать HomePage.

Мы можем обернуть виджет в StatefulWidget и получить доступ к его стейту через InheritedWidget ниже по дереву из BuildContext.

Чтобы обновить данные в InheritedWidget, мы знаем, что должны пересоздать данный виджет передав новые данные. После этого будет вызван метод:

bool updateShouldNotify(oldWidget)

В котором нужно вернуть true если необходимо уведомить об изменении данных и перерисовать виджеты, которые подписаны на обновления.

Пример кода:

class UserScope extends StatefulWidget {
  final Widget child;
  UserScope({ @required this.child }): assert(child != null);

  @override
  State<StatefulWidget> createState() => UserScopeState();

  static UserScopeState of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedUserScope>().state;
  }
}

class UserScopeState extends State<UserScope> {
  User _user;

  set user(User user) {
    setState(() {
      _user = user;
    });
  }

  get user => _user;

  @override
  Widget build(BuildContext context) {
    return InheritedUserScope(state: this, user: _user, child: widget.child);
  }
}

class InheritedUserScope extends InheritedWidget {
  const InheritedUserScope({
    @required this.state,
    @required this.user,
    @required Widget child,
  }) : assert(child != null),
        super(child: child);

  final UserScopeState state;
  final User user;

  @override
  bool updateShouldNotify(InheritedUserScope old) => user != old.user;
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => HomePageState();
}

class HomePageState extends State<HomePage> {

  @override
  Widget build(BuildContext context) => Scaffold(
      body: Container(
        child: UserScope(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              UserLogin(),
              UserBar()
            ],
          ),
        ),
      )
  );
}

class UserLogin extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final userScope = UserScope.of(context);
    return userScope.user == null
        ? RaisedButton(
        onPressed: () => userScope.user = User(id: 1, name: 'Александр'),
        child: Text('Войти')
    ) : RaisedButton(
        onPressed: () => userScope.user = null,
        child: Text('Выйти')
    );
  }
}

class UserBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        width: double.infinity,
        padding: EdgeInsets.all(20),
        color: Color(Random().nextInt(0xffffffff)).withAlpha(0xff),
        child: Center(child: UserView())
    );
  }
}

class UserView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final user = UserScope.of(context).user;
    return user == null ? Container() : Container(
        color: Colors.blueGrey,
        padding: EdgeInsets.all(12.0),
        child: Text(
            "Привет, ${user.name}!",
            style: TextStyle(color: Colors.white)
        )
    );
  }
}

Мы создали новый StatefulWidget, который будет пересоздавать наш InheritedWidget при изменении User.

В InheritedUserScope передаем state, для быстрого доступа к UserScopeState через dependOnInheritedWidgetOfExactType (чтобы иметь возможность обновить переменную user), и саму переменную user, для проверки были ли изменения и нужно ли перерисовывать другие виджеты.

В стейте уже есть user, но мы не сможем проверить были ли изменения, так как объекты передаются по ссылке и state всегда ссылается на самого себя, а значит oldWidget.state == state. Соответсвенно oldWidget.state.user всегда будет равен state.user.

Больше нет перерисовки всего дерева виджетов при изменении User, так как обновляются только виджеты, которые подписаны на изменения данных в InheritedWidget и вложенные в них виджеты.

При копировании материалов ссылка на https://terraideas.ru/ обязательна

Комментарии к статье: Scope во Flutter с InheritedWidget

Нет ни одного комментария. Будьте первым!