1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-N7.md 21 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Небольшие хитрости Flutter: оптимизация MediaQuery и метода build, о которых вы могли не знать

Цель этой статьи — заполнить пробелы в ваших знаниях о механизме MediaQuery и связанной с ней логике перестроения (rebuild). Уверены, что эта информация будет полезна для оптимизации производительности и отладки ошибок.

В Flutter практически каждый использует MediaQuery, например, чтобы получить размер экрана через MediaQuery.of(context).size или высоту области состояния через MediaQuery.of(context).padding.top. Но есть ли проблемы при использовании MediaQuery.of(context)?

Для начала давайте объясним несколько параметров, содержащихся в объекте MediaQueryData, который мы получаем через MediaQuery.of:

  • viewInsets: размер частей, полностью скрытых системой пользователя, другими словами, это высота клавиатуры
  • padding: это пространство между областью состояния и нижней безопасной зоной, но bottom становится равным нулю при появлении клавиатуры
  • viewPadding: аналогично padding, но bottom остаётся неизменным

Приведём пример на iOS: как показано ниже, можно заметить изменения некоторых параметров в объекте MediaQueryData при появлении и отсутствии клавиатуры.

  • viewInsets равен нулю до появления клавиатуры, а после её появления bottom становится равным 336 пикселям
  • padding меняется перед и после появления клавиатуры, bottom переходит с 34 пикселей до нуля
  • viewPadding остаётся неизменным до и после появления клавиатурыimage-20220624115935998

Как видно, данные в объекте MediaQueryData могут меняться в зависимости от состояния клавиатуры, так как MediaQuery является InheritedWidget, то мы можем использовать MediaQuery.of(context) для получения общего доступа к объекту MediaQueryData.

И вот вопрос: логика обновления InheritedWidget связана с контекстом, то есть вызов MediaQuery.of(context) сам по себе является привязкой, а поскольку MediaQueryData также зависит от состояния клавиатуры, то: появление клавиатуры может привести к перестроению (rebuild) мест, где используется MediaQuery.of(context). Например:

Как показано ниже, в MyHomePage мы используем MediaQuery.of(context).size и выводим его значение, затем переходим на страницу EditPage и открываем клавиатуру. В этом случае произойдет следующее:dartmarkdown Класс МоейГлавнаяСтраница extends StatelessWidget { @override Widget build(BuildContext контекст) { print("######### МоейГлавнаяСтраница ${MediaQuery.of(контекст).размер}"); return Scaffold( body: Container( alignment: Alignment.center, child: InkWell( onTap: () { Navigator.of(контекст).push(CupertinoPageRoute(builder: (контекст) { return РедактироватьСтраница(); })); }, child: new Text( "Нажмите", style: TextStyle(fontSize: 50), ), ), ), ); } }

Рассмотрим следующий лог, который показывает процесс открытия клавиатуры. Из-за изменения положения нижней части экрана (`bottom`) меняется объект `MediaQueryData`, что приводит к постоянному перестроению верхнего уровня страницы `МоейГлавнаяСтраница`, даже если она невидима.![image-20220624121917686](http://img.cdn.guoshuyu.cn/2bk/image2.png)

Представьте, что вы используете `MediaQuery.of(context)` в начале каждого экрана. Открыв пять экранов, при открытии клавиатуры на пятом экране будут перестраиваться все четыре предыдущих экрана, что может вызвать замедление работы приложения.

**Если бы вы не использовали `MediaQuery.of(context)` непосредственно в методе `build` класса `MyHomePage`, то открытие клавиатуры на экране `EditPage` не привело бы к перестроению `MyHomePage`.**

Ответ — да, без использования `MediaQuery.of(context).size` объект `MyHomePage` не будет перестраиваться из-за открытия клавиатуры на экране `EditPage`.

Поэтому первый маленький трюк заключается в том, чтобы осторожно использовать `MediaQuery.of(context)` вне компонента `Scaffold`. Возможно, вам сейчас кажется странным понятие "вне Scaffold", но мы объясним это далее.

Некоторые могут спросить: ведь мы получаем данные через `MediaQuery.of(context)`, а они связаны с `MediaQuery` внутри `MaterialApp`. Так почему же изменения данных не приводят к перестроению всех дочерних элементов?

На самом деле, это связано с маршрутом страниц, другими словами, реализация `PageRoute`.

Как показано на следующем рисунке, из-за вложенного структурного дизайна, открытие клавиатуры действительно приводит к перестроению всех дочерних элементов внутри `MaterialApp`. Поскольку `MediaQuery` расположен над `Navigator`, открытие клавиатуры автоматически приводит к перестроению `Navigator`.
```![image-20220624141749056](http://img.cdn.guoshuyu.cn/20220628_N7/image3.png)
```**И так, почему при нормальной работе `Navigator` вызывает перестроение, но страницы не перестраиваются?**

Это связано с базовым классом маршрутов `ModalRoute`. Внутри него используется параметр `_modalScopeCache`, который кэширует `Widget`. Как указано в комментариях:

> **Кэш области не меняется между кадрами, чтобы минимизировать перестроение**.

![](http://img.cdn.guoshuyu.cn/20220628_N7/image4.png)

Рассмотрим пример кода:

- Сначала определяется `TextGlobal`, который выводит `"######## TextGlobal"` в методе `build`.
- Затем в `MyHomePage` создается глобальный объект `TextGlobal globalText = TextGlobal();`.
- Далее в `MyHomePage` добавляются три экземпляра `globalText`.
- Наконец, нажатие кнопки `FloatingActionButton` вызывает `setState(() {});`.

```dart
class TextGlobal extends StatelessWidget {
  const TextGlobal({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print("######## TextGlobal");
    return Container(
      child: new Text(
        "тест",
        style: new TextStyle(fontSize: 40, color: Colors.redAccent),
        textAlign: TextAlign.center,
      ),
    );
  }
}
class MyHomePage extends StatefulWidget {
  final String? title;
  MyHomePage({Key? key, this.title}) : super(key: key);
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TextGlobal globalText = TextGlobal();
  @override
  Widget build(BuildContext context) {
    print("######## MyHomePage");
    return Scaffold(
      appBar: AppBar(),
      body: new Container(
        alignment: Alignment.center,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            globalText,
            globalText,
            globalText,
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {});
        },
      ),
    );
  }
}
```Интересно то, что показывают логи ниже. `"######## TextGlobal"` выводится только один раз при первоначальном построении, а затем больше не вызывается даже после выполнения `setState(() {});`. Это поведение аналогично тому, как работает `ModalRoute`: **открытие клавиатуры приводит к перестроению `MediaQuery`, которое вызывает перестроение `Navigator`, но перестроение прекращается на уровне `ModalRoute`**. На самом деле это поведение также проявляется в `Scaffold`, если вы посмотрите исходный код `Scaffold`, то заметите, что там широко используется `MediaQuery.of(context)`.

Например, в приведённом выше коде, если вы зададите `MyHomePage`'s `Scaffold` значение ключа `ValueKey` равное 3333, то при открытии клавиатуры на странице `EditPage` будет вызвано перестроение (`rebuild`) `Scaffold`'а `MyHomePage`. Однако, поскольку он использует `widget.body`, это не приведёт к перестроению объектов внутри `body`.

```
Если `MyHomePage` перестроится, то все новые объекты, созданные в методе `build`, будут перестроены; но если перестроение произойдёт только внутри `Scaffold` `MyHomePage`, то это не приведёт к перестроению объекта `child`, связанного с параметром `body`.
```

Не слишком ли абстрактно? Вот простой пример:- Мы определяем контроллер `LikeScaffold`, который передает объект через `widget.body`.
- Внутри `LikeScaffold` мы используем `MediaQuery.of(context).viewInsets.bottom`, имитируя использование `MediaQuery` в `Scaffold`.
- На странице `MyHomePage` мы используем `LikeScaffold` и задаем `Builder` как значение параметра `body`, выводя текст `"############ HomePage Builder Text"` для наблюдения за изменениями.
- Переходим на страницу `EditPage` и открываем клавиатуру.```dart
class LikeScaffold extends StatefulWidget {
  final Widget body;

  const LikeScaffold({Key? key, required this.body}) : super(key: key);

  @override
  State<LikeScaffold> createState() => _LikeScaffoldState();
}
``````markdown
## Класс `_LikeScaffoldState`

Класс `_LikeScaffoldState` расширяет состояние `LikeScaffold`.

```dart
class _LikeScaffoldState extends State<LikeScaffold> {
  @override
  Widget build(BuildContext context) {
    print("####### LikeScaffold build ${MediaQuery.of(context).viewInsets.bottom}");
    return Material(
      child: new Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [widget.body],
      ),
    );
  }
}
```
### Класс `_MyHomePageState`

Класс `_MyHomePageState` также расширяет состояние `MyHomePage`.

```dart
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    var routeLists = routers.keys.toList();
    return new LikeScaffold(
      body: Builder(
        builder: (_) {
          print("############ HomePage Builder Text");
          return InkWell(
            onTap: () {
              Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
                return EditPage();
              }));
            },
            child: Text(
              "FFFFFFF",
              style: TextStyle(fontSize: 50),
            ),
          );
        },
      ),
    );
  }
}
```
```Увидеть можно, что вначале `"####### LikeScaffold build 0.0"` и `"############ HomePage Builder Text"` выполняются нормально, затем после открытия клавиатуры, `"####### LikeScaffold build` постоянно выводит размер `bottom`, но `"############ HomePage Builder Text"` не выводится, так как это экземпляр `widget.body`.

![](http://img.cdn.guoshuyu.cn/20220628_N7/image7.png)

**Поэтому с помощью этого минимального примера видно, что хотя в `Scaffold` широко используется `MediaQuery.of(context)`, влияние ограничено внутренней областью `Scaffold`.**
```Дальше мы продолжаем рассмотрение модификации этого примера, если в `_LikeScaffold` добавить еще один `Scaffold`, то какой будет результат?

```dart

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    var routeLists = routers.keys.toList();
    /// Добавили ещё один Scaffold
    return Scaffold(
      body: new LikeScaffold(
        body: Builder(
          ...
        ),
      ),
    );
  }
}
```

Ответ заключается в том, что `"####### LikeScaffold build"` внутри `LikeScaffold` также не будет выводиться при открытии клавиатуры, то есть: **хотя `LikeScaffold` использует `MediaQuery.of(context)`, он больше не перестраивается из-за открытия клавиатуры**.

Потому что теперь `LikeScaffold` является потомком `Scaffold`, поэтому через `MediaQuery.of(context)` указывается уже обработанное значение `MediaQueryData` внутри `Scaffold`.

![image-20220624150712453](http://img.cdn.guoshuyu.cn/20220628_N7/image8.png)

> Внутри `Scaffold` много аналогичной обработки, например, в `body` учитываются наличие `AppBar` и `BottomNavigationBar` для удаления `paddingTop` и `paddingBottom` в этой области.

Поэтому, увидев это, неужели ничего не приходит вам в голову? **Почему иногда полученные через `MediaQuery.of(context)` значения padding имеют нулевые верхние границы, а иногда нет? Причина заключается в том, откуда берется контекст**.Например, как показано ниже, `ScaffoldChildPage` является потомком `Scaffold`, и мы печатаем `MediaQuery.of(context).padding` как в `MyHomePage`, так и в `ScaffoldChildPage`:

```dart
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("MyHomePage         MediaQuery padding: ${MediaQuery.of(context).padding}");
    return Scaffold(
      appBar: AppBar(
        title: new Text(""),
      ),
      extendBody: true,
      body: Column(
        children: [
          new Spacer(),
          ScaffoldChildPage(),
          new Spacer(),
        ],
      ),
    );
  }
}

class ScaffoldChildPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print("ScaffoldChildPage  MediaQuery padding: ${MediaQuery.of(context).padding}");
    return Container();
  }
}
```

Как показано на следующем рисунке, можно заметить, что поскольку в данный момент `MyHomePage` имеет `AppBar`, то значение `paddingTop`, полученное в `ScaffoldChildPage`, равно 0, так как полученные данные `MediaQueryData` в `ScaffoldChildPage` были переопределены данными `Scaffold` внутри `MyHomePage`.![image-20220624151522429](http://img.cdn.guoshuyu.cn/20220628_N7/image9.png)

Если вы добавите `BottomNavigationBar` к `MyHomePage`, вы заметите, что значение нижней границы (`bottom`) в `ScaffoldChildPage` изменится с исходных 34 до 90.

![image-20220624152008795](http://img.cdn.guoshuyu.cn/20220628_N7/image10.png)

На этом этапе становится очевидной важность объекта контекста (`context`) в методе `MediaQuery.of`:

- **Если страница использует `context` вне компонента `Scaffold` в вызове `MediaQuery.of`, она получает данные `MediaQueryData` верхнего уровня. В результате при появлении клавиатуры происходит перестроение страницы (`rebuild`).**
- **Если `MediaQuery.of` использует `context` внутри компонента `Scaffold`, он получает данные `MediaQueryData` для области внутри `Scaffold`. Например, для тела (`body`), которое было рассмотрено ранее. При этом значения `MediaQueryData` могут меняться в зависимости от конфигурации `Scaffold`.**

Поэтому, как показано на следующем анимированном рисунке, некоторые люди используют вложенный `MediaQuery` для выполнения некоторых операций по перехвату, таких как установка текста незаконченным, но это приводит к тому, что при появлении и скрытии клавиатуры происходит постоянное перестроение всех страниц. Например, во время открытия клавиатуры на странице 2, страница 1 также постоянно перестраивается.

![1111333](http://img.cdn.guoshuyu.cn/20220628_N7/image11.gif)

Поэтому, если вам требуется выполнить глобальное перехватывание, рекомендуется использовать такой подход, как `useInheritedMediaQuery`.```dart
return MediaQuery(
  data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
  child: MaterialApp(
    useInheritedMediaQuery: true,
  ),
);
```

Итогом является то, что в данной статье были проанализированы:

- Различия между `viewInsets`, `padding` и `viewPadding` в данных `MediaQueryData`.
- Отношение `MediaQuery` к состоянию клавиатуры.
- Влияние использования различных контекстов (`context`) на производительность метода `MediaQuery.of`.
- Влияние компонента `Scaffold` на получаемые данные `MediaQueryData`.

Так что, если после прочтения этой статьи у вас остались вопросы, пожалуйста, оставьте свои комментарии для обсуждения.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master