Цель этой статьи — заполнить пробелы в ваших знаниях о механизме 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
остаётся неизменным до и после появления клавиатурыКак видно, данные в объекте
MediaQueryData
могут меняться в зависимости от состояния клавиатуры, так какMediaQuery
являетсяInheritedWidget
, то мы можем использоватьMediaQuery.of(context)
для получения общего доступа к объектуMediaQueryData
.
И вот вопрос: логика обновления InheritedWidget
связана с контекстом, то есть вызов MediaQuery.of(context)
сам по себе является привязкой, а поскольку MediaQueryData
также зависит от состояния клавиатуры, то: появление клавиатуры может привести к перестроению (rebuild
) мест, где используется MediaQuery.of(context)
. Например:
Как показано ниже, в MyHomePage
мы используем MediaQuery.of(context).size
и выводим его значение, затем переходим на страницу EditPage
и открываем клавиатуру. В этом случае произойдет следующее:dart
markdown
Класс МоейГлавнаяСтраница 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`, что приводит к постоянному перестроению верхнего уровня страницы `МоейГлавнаяСтраница`, даже если она невидима.
Представьте, что вы используете `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`.
```
```**И так, почему при нормальной работе `Navigator` вызывает перестроение, но страницы не перестраиваются?**
Это связано с базовым классом маршрутов `ModalRoute`. Внутри него используется параметр `_modalScopeCache`, который кэширует `Widget`. Как указано в комментариях:
> **Кэш области не меняется между кадрами, чтобы минимизировать перестроение**.

Рассмотрим пример кода:
- Сначала определяется `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`.

**Поэтому с помощью этого минимального примера видно, что хотя в `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`.

> Внутри `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`.
Если вы добавите `BottomNavigationBar` к `MyHomePage`, вы заметите, что значение нижней границы (`bottom`) в `ScaffoldChildPage` изменится с исходных 34 до 90.

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

Поэтому, если вам требуется выполнить глобальное перехватывание, рекомендуется использовать такой подход, как `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 )