Передача параметров с помощью InheritedWidget в Flutter

Передача параметров с помощью InheritedWidget в Flutter

Пример использования библиотеки InheritedWidget в Flutter приложении для передачи параметров между виджетами.

Я уже писал, что Flutter приложение обычно состоит из множества виджетов. Приложение разбивают на виджеты для удобства повторного использования и чтобы не вызывать лишние перерисовки ненужных частей, для экономии ресурсов и повышения производительности приложения.

Часто нужно передавать какие либо данные между виджетами.

Можно передавать через конструктор. Предположим, у нас есть счетчик. У меня есть экран HomePage, а в нем я буду скрывать кнопку после 3х нажатий. Но количество нажатий выводит виджет CounterText вложенный в CounterBar.

import 'package:flutter/material.dart';

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

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 createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) => Scaffold(
      body: Center(
        child: _counter > 2 ? Text('Готово!') : CounterBar(counter: _counter),
      ),
      floatingActionButton: _counter > 2 ? Container() : FloatingActionButton(
        onPressed: () => setState(() { _counter ++; }),
        child: const Icon(Icons.add),
      )
  );
}

class CounterBar extends StatelessWidget {
  final int counter;
  CounterBar({ @required this.counter }): assert(counter != null);

  @override
  Widget build(BuildContext context) => Container(
      color: Colors.blueGrey,
      width: 200,
      height: 200,
      padding: EdgeInsets.all(12.0),
      child: Center(child: CounterText(counter: counter))
  );
}

class CounterText extends StatelessWidget {
  final int counter;
  CounterText({ @required this.counter }): assert(counter != null);

  @override
  Widget build(BuildContext context) => Container(
      color: Colors.black,
      padding: EdgeInsets.all(12.0),
      child: Text(
          "Счетчик: $counter",
          style: TextStyle(color: Colors.white)
      )
  );
}

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

Но как это будет выглядеть при передачи через 5, 10 и более виджетов? А что, если нужно добавить или перенести виджет в другое дерево? Придется переписать много виджетов.

Достать виджет из контекста можно с помощью метода findAncestorStateOfType, в результате пробежав вверх по дереву, можно найти нужный нам виджет.

Реализация с использованием findAncestorStateOfType:

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

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

class _HomePageState extends State<HomePage> {
  int counter = 0;

  @override
  Widget build(BuildContext context) => Scaffold(
      body: Center(
        child: counter > 2 ? Text('Готово!') : CounterBar(),
      ),
      floatingActionButton: counter > 2 ? Container() : FloatingActionButton(
        onPressed: () => setState(() { counter ++; }),
        child: const Icon(Icons.add),
      )
  );
}

class CounterBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Container(
      color: Colors.blueGrey,
      width: 200,
      height: 200,
      padding: EdgeInsets.all(12.0),
      child: Center(child: CounterText())
  );
}

class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = HomePage.of(context)?.counter ?? 0;
    return Container(
        color: Colors.black,
        padding: EdgeInsets.all(12.0),
        child: Text(
            "Счетчик: $counter",
            style: TextStyle(color: Colors.white)
        )
    );
  }
}

В HomePage для удобства добавим статичный метод of, который принимает контекст того виджета, в котором мы собираемся использовать полученные данные. Можно не определять этот метод, но так удобней.

Как видим из описания функции, понадобится O(N) итераций чтобы найти интересующий нас виджет. Это вызвано тем, что будут перебираться по очереди все родительские виджеты пока не будет найден интересующий нас. То есть работа функции примерно сводится к

Получить родителя
Проверить тип
Вернуть если совпал тип или повторить если не совпал

Если поиск будет с большой глубины, да еще и разные виджеты перебирают всех своих родителей, каждый в поисках нужных им данных, плюс частые перерисовки. Все это приводит к большему потреблению ресурсов, и мы тратим много процессорного времени на перебор.

На помощь приходит InheritedWidget. Мы можем обернуть виджет в InheritedWidget и получить доступ к этому InheritedWidget ниже по дереву из BuildContext. Данный виджет специально предназначен для передачи данных через BuildContext любому и быстрому доступу к этим данным.

InheritedWidget можно получить с помощью функции dependOnInheritedWidgetOfExactType, которая достает нужный вам виджет с O(1). То есть нет перебора в цикле и мы всегда получим данные быстро, без лишних проверок.

Еще одна особенность виджета, это наличие метода updateShouldNotify.

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

bool updateShouldNotify(oldWidget)

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

Еще один полезный метод, это getElementForInheritedWidgetOfExactType. Этот метод вернет виджет так же быстро, но не подпишется на обновления. Это полезно, если нужно получить доступ например из initState.

Пример InheritedWidget:

class CounterState extends InheritedWidget {
  final int counter;
  const CounterState({
    @required this.counter,
    @required Widget child,
  }) : assert(counter != null), assert(child != null), super(child: child);

  @override
  bool updateShouldNotify(covariant CounterState oldWidget) =>
      oldWidget.counter != counter;

  static int of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CounterState>()?.counter;
}

Мы описали InheritedWidget, который будет вызывать метод build у всех подписчиков ,если переменная counter изменилась.

Теперь немного изменим старый код:

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

class _HomePageState extends State<HomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) => Scaffold(
      body: Center(
        child: _counter > 2
            ? Text('Готово!')
            : CounterState(
              counter: _counter,
              child: CounterBar(),
            )
      ),
      floatingActionButton: _counter > 2 ? Container() : FloatingActionButton(
        onPressed: () => setState(() { _counter ++; }),
        child: const Icon(Icons.add),
      )
  );
}

class CounterBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Container(
      color: Colors.blueGrey,
      width: 200,
      height: 200,
      padding: EdgeInsets.all(12.0),
      child: Center(child: CounterText())
  );
}

class CounterText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = CounterState.of(context) ?? 0;
    return Container(
        color: Colors.black,
        padding: EdgeInsets.all(12.0),
        child: Text(
            "Счетчик: $counter",
            style: TextStyle(color: Colors.white)
        )
    );
  }
}

Код очень похож на прошлый, мы обернули CounterBar в CounterState для передачи стейта ниже через BuildContext. Но теперь для частых вызовов c глубокой вложенностью виджетов этот код будет работать быстрее.

Рекомендую так же ознакомиться с пакетом Provider, основанном на InheritedWidget.

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

Комментарии к статье: Передача параметров с помощью InheritedWidget в Flutter

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