Дело было таким, что недавно компания Flutter выпустила стабильную версию 1.17
, и в соответствии с "традициями" началась работа по обновлению производственного проекта до версии 1.12.13+hotfix.9
. После завершения процесса адаптации внезапная ошибка заставила меня задуматься.
Как показано на приведённой выше图为所展示的,可以看到在键盘B页面打开后,返回至上一页面A时键盘已收回,但原先键盘所在区域在A页面变为了空白,并且A页面的内容也因键盘弹出而进行了重新调整大小。
По поводу этой проблемы, первым делом приходит в голову свойство resizeToAvoidBottomInset компонента Scaffold.
В Flutter по умолчанию значение свойства resizeToAvoidBottomInset компонента Scaffold равно true. Когда значение этого свойства равно true, внутри Scaffold медиа запрос mediaQuery.viewInsets.bottom включается в расчет размера BoxConstraints. То есть при появлении клавиатуры внутренний нижний край экрана корректируется для учета высоты клавиатуры.
Однако проблема возникает на странице A, где клавиатура уже скрыта, и значение mediaQuery.viewInsets.bottom должно быть обновлено до нуля. Почему же интерфейс не обновляется соответственно?
Тогда можно предположить, что проблема может быть связана с компонентом MediaQuery.
Согласно исходному коду мы знаем, что MediaQuery — это InheritedWidget, который передает соответствующий объект MediaQueryData, содержащий различные данные устройства, такие как размер, devicePixelRatio, textScaleFactor, viewPadding и viewInsets.
А что такое viewInsets? Официальное объяснение следующее:
"Это область, которая может быть отображена системой, обычно связанная с клавиатурой устройства. При появлении клавиатуры значение viewInsets.bottom соответствует верхней части клавиатуры."
Таким образом, указанный выше сбой кажется результатом некорректного обновления значения viewInsets.bottom компонента Scaffold после того, как клавиатура была скрыта.
Чтобы понять, как viewInsets компонента MediaQuery устанавливается, следует сначала узнать, как это происходит.После анализа исходного кода можно сделать вывод, что MediaQueryData из MediaQuery получена из WidgetsBinding.instance.window, и по умолчанию она задается в _MediaQueryFromWindow в MaterialApp:
@override
void didChangeMetrics() {
setState(() {
// Свойства окна были изменены. Мы используем их в нашей функции сборки,
// поэтому нам требуется setState(), но мы ничего не кэшируем локально.
});
}
@override
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: widget.child,
);
}
Исходный код демонстрирует, что MediaQueryData
берётся из объекта Window
, а также регистрируется обратный вызов didChangeMetrics
для WidgetsBindingObserver
. Это означает, что при изменении window
будет вызван метод setState
, чтобы обновить данные в MediaQuery
.
```Исходный код демонстрирует, что MediaQueryData
берётся из объекта `Window`, а также регистрируется обратный вызов `didChangeMetrics` для `WidgetsBindingObserver`. Это значит, что при изменении `window` будет вызван метод `setState`, чтобы обновить данные в `MediaQuery`.
В методе MediaQueryData.fromWindow
значение viewInsets
вычисляется путём деления значений window.viewInsets
и window.devicePixelRatio
.
viewInsets = EdgeInsets.fromWindowPadding(window.viewInsets, window.devicePixelRatio);
Но откуда берутся значения в объекте Window
?
На самом деле значения в объекте Window
берутся из движка Flutter. Когда клавиатура появляется, движок Flutter использует метод _updateWindowMetrics
, чтобы обновить данные в объекте Window
и выполнить методы window.onMetricsChanged
и window._onMetricsChangedZone
.Обратный вызов onMetricsChanged
в конечном итоге вызывает метод handleMetricsChanged
, который выполняет scheduleForcedFrame()
, чтобы обновить интерфейс, и вызывает observer.didChangeMetrics();
, чтобы сообщить о необходимости обновления данных MediaQueryData
внутри MaterialApp
.
@pragma('vm:entry-point')
// ignore: unused_element
void _updateWindowMetrics(
double devicePixelRatio,
double width,
double height,
double depth,
double viewPaddingTop,
double viewPaddingRight,
double viewPaddingBottom,
double viewPaddingLeft,
double viewInsetTop,
double viewInsetRight,
double viewInsetBottom,
double viewInsetLeft,
double systemGestureInsetTop,
double systemGestureInsetRight,
double systemGestureInsetBottom,
double systemGestureInsetLeft,
) {
window
.._devicePixelRatio = devicePixelRatio
.._physicalSize = Size(width, height)
.._physicalDepth = depth
.._viewPadding = WindowPadding._(
top: viewPaddingTop,
right: viewPaddingRight,
bottom: viewPaddingBottom,
left: viewPaddingLeft)
.._viewInsets = WindowPadding._(
top: viewInsetTop,
right: viewInsetRight,
bottom: viewInsetBottom,
left: viewInsetLeft)
.._padding = WindowPadding._(
top: math.max(0.0, viewPaddingTop - viewInsetTop),
right: math.max(0.0, viewPaddingRight - viewInsetRight),
bottom: math.max(0.0, viewPaddingBottom - viewInsetBottom),
left: math.max(0.0, viewPaddingLeft - viewInsetLeft))
.._systemGestureInsets = WindowPadding._(
top: math.max(0.0, systemGestureInsetTop),
right: math.max(0.0, systemGestureInsetRight),
bottom: math.max(0.0, systemGestureInsetBottom),
left: math.max(0.0, systemGestureInsetLeft));
_invoke(window.onMetricsChanged, window._onMetricsChangedZone);
}
Как можно заметить, когда клавиатура появляется и исчезает, Engine
обновляет данные окна Window
, а Window
запускает обновление интерфейса и одновременно обновляет MediaQueryData
в MaterialApp
.
Учитывая эту ситуацию, проблема с пустыми областями, вызванной клавиатурой, должна была бы не возникнуть. Однако проблема может заключаться в том, что MediaQueryData
в Scaffold
не обновляется.
В этот момент мне пришло в голову, что ранее, чтобы заблокировать изменение размера шрифта страницы вместе со сжатием системы, я использовал MediaQueryData.fromWindow
для создания копии MediaQuery
на уровне маршрутов. Возможно, проблема связана именно с этим:
Navigator.of(context).push(new CupertinoPageRoute(builder: (context) {
return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window)
.copyWith(textScaleFactor: 1),
child: Page2(),
);
}));
Однако это также выглядит неправильным, так как проблема возникает при переходе с экрана B, где есть клавиатура, обратно на экран A, где её нет. В этом случае экран A уже открыт, поэтому WidgetsBinding.instance.window
должен быть правильным. А не должен ли метод builder
маршрута CupertinoPageRoute
для экрана A снова выполняться при открытии экрана B с клавиатурой?
После отладки я был удивлен тем фактом, что программа после открытия экрана B с клавиатурой действительно триггерит повторное выполнение метода builder
маршрута CupertinoPageRoute
для экрана A.Первое, что приходит на ум при возможности обновления между экранами — это глобальная система управления состоянием, поскольку приложение требует глобального изменения темы, многоязычности и совместного использования информации пользователя. Обычно такие данные передаются и управляются через систему управления состоянием на уровне всего приложения.Учитывая сложность исходного проекта, я создал простой тестовый демон и внедрил в него простую систему управления состоянием ScopedModel
. После открытия экрана B с клавиатурой и выполнения notifyListeners()
с некоторым временем задержки, я обнаружил, что действительно возникает аналогичная проблема.
return ScopedModel(
model: t,
child: ScopedModelDescendant<TestModel>(
builder: (context, child, model) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
},
),
);
Здесь возникают вопросы: почему обновление MaterialApp
приводит к тому, что PageRoute
вызывает метод builder
заново?
Это связано с логикой работы Navigator
, который является StatefulWidget
. Когда MaterialApp
обновляется, в методе didUpdateWidget
класса NavigatorState
вызывается метод changedExternalState()
для всех маршрутов в истории навигатора.
@Override
void didUpdateWidget(Navigator oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.observers != widget.observers) {
for (NavigatorObserver observer in oldWidget.observers)
observer._navigator = null;
for (NavigatorObserver observer in widget.observers) {
assert(observer.navigator == null);
observer._navigator = this;
}
}
for (Route<dynamic> route in _history)
route.changedExternalState();
}
Вызов метода changedExternalState
приводит к выполнению _forceRebuildPage
, который очищает поле _page
маршрута. Это значит, что при следующем вызове метода build
для маршрута будет снова вызван метод builder
.```dart
@Override
void changedExternalState() {
super.changedExternalState();
if (_scopeKey.currentState != null)
_scopeKey.currentState._forceRebuildPage();
}
...
void _forceRebuildPage() { setState(() { _page = null; }); }
Итак, вернемся к первоначальному вопросу: **эта проблема произошла из-за некорректного использования `MediaQueryData.fromWindow(WidgetsBinding.instance.window)`. После открытия страницы с клавиатурой было вызвано обновление `MaterialApp`, что привело к повторному вызову метода `builder` для `PageRoute`. В результате `Scaffold` без клавиатуры использовал значение `viewInsets.bottom` для открытого с клавиатурой экрана.**
**Для решения этой проблемы достаточно заменить `MediaQueryData.fromWindow` на `MediaQuery.of(context)`. При отсутствии контекста или необходимости использовать `MediaQueryData.fromWindow` напрямую, следует использовать `WidgetsBindingObserver.didChangeMetrics` для обновления данных.**
```dart
Navigator.of(context).push(new CupertinoPageRoute(builder: (context) {
return MediaQuery(
data: MediaQuery.of(context)
.copyWith(textScaleFactor: 1),
child: Page2(),
);
}));
Наконец, стоит отметить, что хотя эта проблема и не сложна, она помогает выявить множество часто игнорируемых аспектов программирования. Поэтому подробное описание поможет вам решать больше задач в повседневной разработке.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )