
Пример как можно использовать 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 и вложенные в них виджеты.
Комментарии к статье: Scope во Flutter с InheritedWidget