[Open source, Flutter] Тап в статус бар. Делаем простое сложно c помощью Flutter
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет!Я работаю над приложением для изучения английского языка Memo. Недавно мы выложили первую версию в сторы и тут же получили отзыв от одного из активных пользователей:
Привет, Максим!Тут я вспомнил про аналогичную проблему у приложения Meduza (иностранный агент):Извините, данный ресурс не поддреживается. :( Я пользуюсь Android телефоном и даже не думал, что это может быть настолько востребовано. Открыв iOS версию приложения, я увидел, что действительно тап в статус бар не приводит ни к какой реакции.О чем речь? В iOS есть такая фича - scrolls to top. Вот, как нам ее описывает документация:
The scroll-to-top gesture is a tap on the status bar. When a user makes this gesture, the system asks the scroll view closest to the status bar to scroll to the top
Перейдем кодуПосмотрим, может ли Flutter предоставить нам такую функциональность из коробки? Сделаем простой семпл с единственным экраном со списком:
import 'package:example/home_page.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Scroll to top', home: HomePage());
}
}
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: _list(),
appBar: AppBar(title: const Text('Scroll to top')),
);
}
Widget _list() {
return ListView.builder(itemBuilder: _itemBuilder, itemCount: 100);
}
Widget _itemBuilder(BuildContext context, int index) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
),
padding: const EdgeInsets.all(20),
child: Text('$index', style: const TextStyle(fontSize: 20)),
);
}
}
Попробуем проскролить список и кликнуть в статус бар (заставить эмулятор показывать место тапа, неочевидная задача)
Круто, фича работает из коробки. Можем расходиться?В реальных проектах нам иногда нужно иметь свой ScrollController для подскролов к элементам списка. Давайте попробуем добавить простой контроллер:
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _list(),
appBar: AppBar(title: const Text('Scroll to top')),
);
}
Widget _list() {
return ListView.builder(
itemBuilder: _itemBuilder,
itemCount: 100,
controller: _scrollController,
);
}
Widget _itemBuilder(BuildContext context, int index) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
),
padding: const EdgeInsets.all(20),
child: Text('$index', style: const TextStyle(fontSize: 20)),
);
}
}
В этом случае тап в статус бар перестает скролить список. Давайте разбираться. За счет чего вообще скроллится список? Я поискал по исходникам Flutter и нашел вот такой код из класса Scaffold:
// iOS FEATURES - status bar tap, back gesture
// On iOS, tapping the status bar scrolls the app's primary scrollable to the
// top. We implement this by looking up the primary scroll controller and
// scrolling it to the top when tapped.
void _handleStatusBarTap() {
final ScrollController? _primaryScrollController = PrimaryScrollController.of(context);
if (_primaryScrollController != null && _primaryScrollController.hasClients) {
_primaryScrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
);
}
}
Есть определенный обработчик жестов, который в конце своей работы вызывает метод_handleStatusBarTap (обожаю TODO, которые висят в релизном коде по 4 года <3).
Итак, Flutter пытается найти ближайший к текущему контексту PrimaryScrollController и начать его скролить. Что такое PrimaryScrollController ?
/// Associates a [ScrollController] with a subtree.
///
/// When a [ScrollView] has [ScrollView.primary] set to true and is not given
/// an explicit [ScrollController], the [ScrollView] uses [of] to find the
/// [ScrollController] associated with its subtree.
///
/// This mechanism can be used to provide default behavior for scroll views in a
/// subtree. For example, the [Scaffold] uses this mechanism to implement the
/// scroll-to-top gesture on iOS.
///
/// Another default behavior handled by the PrimaryScrollController is default
/// [ScrollAction]s. If a ScrollAction is not handled by an otherwise focused
/// part of the application, the ScrollAction will be evaluated using the scroll
/// view associated with a PrimaryScrollController, for example, when executing
/// [Shortcuts] key events like page up and down.
///
/// See also:
/// * [ScrollAction], an [Action] that scrolls the [Scrollable] that encloses
/// the current [primaryFocus] or is attached to the PrimaryScrollController.
/// * [Shortcuts], a widget that establishes a [ShortcutManager] to be used
/// by its descendants when invoking an [Action] via a keyboard key
/// combination that maps to an [Intent].
class PrimaryScrollController extends InheritedWidget {
/// Creates a widget that associates a [ScrollController] with a subtree.
const PrimaryScrollController({
Key? key,
required ScrollController this.controller,
required Widget child,
}) : assert(controller != null),
super(key: key, child: child);
PrimaryScrollController – это не ScrollController, как могло показаться из названия. Это виджет, который держит ScrollController, отвечающий за primary скролл в приложении.Возникают три вопроса:
- Почему по умолчанию все работает?
- Откуда берется PrimaryScrollController?
- Будет ли это тап в статус бар работать, если у нас будет несколько вложенных Scaffold?
Почему по умолчанию все работает?Заглянем в класс ScrollView:
final ScrollController? scrollController =
primary ? PrimaryScrollController.of(context) : controller
В случае, если установлен флаг primary, то будет использован PrimaryScrollController – тем самым виджет начнет реагировать на события тапа в статус бар.Что за флаг primary? Смотрим документацию:
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
Флаг primary позволяет нам пометить ScrollView в качестве дефолтного обработчика события тапа в статус бар. Окей. Но мы не выставляли это значение. Какое значение установлено по умолчанию?
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool primary;
Если мы явно не указали значение primary, то флаг будет выставлен в true, если ориентация списка вертикальная и controller == null. В нашем случае получаем значение false.
primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical),
Откуда берется PrimaryScrollControllerЗапускаем дебагер и смотрим в метод static ScrollController? of(BuildContext context) класса PrimaryScrollController. Он находит PrimaryScrollController из routes(смотреть в значение _location):
Как именно происходит поиск виджета при помощи dependOnInheritedWidgetOfExactType , объясняет вот это видео. Если коротко: метод dependOnInheritedWidgetOfExactType позволяет пройтись вверх по дереву виджетов и найти ближайший виджет с определенным типом.Если выше по контексту главного Scaffold нет другого PrimaryScrollController, то мы всегда придем к PrimaryScrollController из routes.dart:
//...
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: widget.route.restorationScopeId,
builder: (BuildContext context, Widget? child) {
assert(child != null);
return RestorationScope(
restorationId: widget.route.restorationScopeId.value,
child: child!,
);
},
child: _ModalScopeStatus(
route: widget.route,
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
canPop: widget.route.canPop, // _routeSetState is called if this updates
child: Offstage(
offstage: widget.route.offstage, // _routeSetState is called if this updates
child: PageStorage(
bucket: widget.route._storageBucket, // immutable
child: Builder(
builder: (BuildContext context) {
return Actions(
actions: <Type, Action<Intent>>{
DismissIntent: _DismissModalAction(context),
},
child: PrimaryScrollController( //<- Вот это место
//...
Как же нам тогда сохранить возможность скролла при тапе и при этом использовать свой контроллер? Нам нужно создать свой PrimaryScrollController над Scaffold, который будет реагировать на события скролла при тапе в статус бар:
@override
Widget build(BuildContext context) {
return PrimaryScrollController(
controller: _scrollController,
child: Scaffold(
body: _list(),
appBar: AppBar(title: const Text('Scroll to top')),
),
);
}
Снова запустим дебагер. Действительно, в этом случае мы находим наш PrimaryScrollController:
Будет ли этот тап в статус бар работать, если у нас будет несколько вложенных Scaffold?Возьмем типичный пример с экраном на котором несколько табов:
import 'package:example/content_page.dart';
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Scroll to top')),
body: IndexedStack(
index: _currentIndex,
children: const [
ContentPage(backgroundColor: Colors.white),
ContentPage(backgroundColor: Colors.deepOrange),
ContentPage(backgroundColor: Colors.green),
],
),
bottomNavigationBar: BottomNavigationBar(
onTap: _onTabTapped,
currentIndex: _currentIndex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.mail), label: 'Messages'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile')
],
),
);
}
void _onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
}
В виджетах экранов будем использовать PrimaryScrollController :
...
@override
Widget build(BuildContext context) {
return PrimaryScrollController(
controller: _scrollController,
child: Scaffold(
primary: true,
body: _list(),
backgroundColor: widget.backgroundColor,
),
);
}
...
Тап в статус бар перестал работать. Все потому, что под статус баром у нас находится самый первый Scaffold, и он будет реагировать на события. Виджеты экранов располагаются ниже и уже не будут реагировать на тап. Что делать?Я вижу два основных решения:
- заворачивать главный Scaffold в PrimaryScrollController и передавать события в нужный таб;
- в виджетах табов регистрировать свой лисенер скроллов для PrimaryScrollController из routes.dart
Второй вариант более гибкий, давайте его и сделаем. Для начала создадим ScrollContext (это абстрактный класс для работы со ScrollPosition), который сможет слушать события animateTo:
class _FakeScrollPositionWithSingleContext
extends ScrollPositionWithSingleContext {
_FakeScrollPositionWithSingleContext({
required BuildContext context,
required ScrollsToTopCallback callback,
}) : _callback = callback,
super(
physics: const NeverScrollableScrollPhysics(),
context: _FakeScrollContext(context),
);
final ScrollsToTopCallback _callback;
@override
Future<void> animateTo(
double to, {
required Duration duration,
required Curve curve,
}) {
return _callback(
ScrollsToTopEvent(to, duration: duration, curve: curve),
);
}
}
Далее нам нужно найти PrimaryScrollController и добавить в него свой лисенер. У ScrollController есть метод attach:
void _attach(BuildContext context) {
final primaryScrollController = PrimaryScrollController.of(context);
if (primaryScrollController == null) return;
final scrollPositionWithSingleContext =
_FakeScrollPositionWithSingleContext(
context: context,
callback: widget.onScrollsToTop,
);
primaryScrollController.attach(scrollPositionWithSingleContext);
_primaryScrollController = primaryScrollController;
_scrollPositionWithSingleContext = scrollPositionWithSingleContext;
}
И завернем написанный код в виджет:
/// Widget for catch scrolls-to-top event
class ScrollsToTop extends StatefulWidget {
/// Creates new ScrollsToTop widget
const ScrollsToTop({
Key? key,
required this.scaffold,
required this.onScrollsToTop,
}) : super(key: key);
/// Primary scaffold of your app
final Scaffold scaffold;
/// Callback for handle scrolls-to-top event
final ScrollsToTopCallback onScrollsToTop;
@override
State<ScrollsToTop> createState() => _ScrollsToTopState();
}
Теперь мы можем оборачивать любой виджет в ScrollsToTop и реагировать на тапы в любом месте, где нам это нужно. Пример использования можно посмотреть тут.Важное замечание: PrimaryScrollController может быть использован и другими участками кода, несвязанными с тапом в статус бар. Например, при клике page up / page down.Весь написанный код я выделил в пакет и опубликовал в pub. Буду очень рад пул реквестам.Как такой подход работает в реальном приложении, вы можете увидеть в приложении: Memo доступно под iOS и AndroidЧао!
===========
Источник:
habr.com
===========
Похожие новости:
- [Open source, Сетевые технологии, Mesh-сети] Yggdrasil Network 0.4 — Скачок в развитии защищенной самоорганизующейся сети
- [Open source, .NET, C#, Математика, F#] AngouriMath 1.3 update
- [Настройка Linux, *nix, Разработка под Linux, Учебный процесс в IT, DevOps] Зачем уметь работать в командной строке?
- [Open source, Python] Dramatiq как современная альтернатива Celery: больше нет проблем с версиями и поддержкой Windows
- [Open source, API, Dart, Flutter] Как перестать писать код для взаимодействия с бэкендом
- [Open source, Программирование, Учебный процесс в IT] Как я учил студентов Северной Кореи разрабатывать ПО с открытым исходным кодом (перевод)
- [Изучение языков] 5 способов перевода английских идиом на русский, чтобы не было мучительно больно
- [Open source, C++, Qt, Софт] Haiku, Inc. проспонсировала приобретение RISC-V материнских плат для портирования системы Haiku
- [Open source, Системы сборки, DevOps, Kubernetes] werf vs Docker. Чем лучше собирать образы
- [Open source, Разработка игр, HTML, Дизайн игр, DIY или Сделай сам] Что случилось с игрой «Колобок» в июне
Теги для поиска: #_open_source, #_flutter, #_flutter, #_scrolls_to_top, #_memo, #_anglijskij (английский), #_open_source, #_flutter
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 09:10
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет!Я работаю над приложением для изучения английского языка Memo. Недавно мы выложили первую версию в сторы и тут же получили отзыв от одного из активных пользователей: Привет, Максим!Тут я вспомнил про аналогичную проблему у приложения Meduza (иностранный агент):Извините, данный ресурс не поддреживается. :( Я пользуюсь Android телефоном и даже не думал, что это может быть настолько востребовано. Открыв iOS версию приложения, я увидел, что действительно тап в статус бар не приводит ни к какой реакции.О чем речь? В iOS есть такая фича - scrolls to top. Вот, как нам ее описывает документация: The scroll-to-top gesture is a tap on the status bar. When a user makes this gesture, the system asks the scroll view closest to the status bar to scroll to the top
import 'package:example/home_page.dart';
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const MaterialApp(title: 'Scroll to top', home: HomePage()); } } import 'package:flutter/material.dart';
class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Scaffold( body: _list(), appBar: AppBar(title: const Text('Scroll to top')), ); } Widget _list() { return ListView.builder(itemBuilder: _itemBuilder, itemCount: 100); } Widget _itemBuilder(BuildContext context, int index) { return Container( decoration: BoxDecoration( border: Border.all(color: Colors.blue), ), padding: const EdgeInsets.all(20), child: Text('$index', style: const TextStyle(fontSize: 20)), ); } } Круто, фича работает из коробки. Можем расходиться?В реальных проектах нам иногда нужно иметь свой ScrollController для подскролов к элементам списка. Давайте попробуем добавить простой контроллер: import 'package:flutter/material.dart';
class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { late ScrollController _scrollController; @override void initState() { super.initState(); _scrollController = ScrollController(); } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: _list(), appBar: AppBar(title: const Text('Scroll to top')), ); } Widget _list() { return ListView.builder( itemBuilder: _itemBuilder, itemCount: 100, controller: _scrollController, ); } Widget _itemBuilder(BuildContext context, int index) { return Container( decoration: BoxDecoration( border: Border.all(color: Colors.blue), ), padding: const EdgeInsets.all(20), child: Text('$index', style: const TextStyle(fontSize: 20)), ); } } // iOS FEATURES - status bar tap, back gesture
// On iOS, tapping the status bar scrolls the app's primary scrollable to the // top. We implement this by looking up the primary scroll controller and // scrolling it to the top when tapped. void _handleStatusBarTap() { final ScrollController? _primaryScrollController = PrimaryScrollController.of(context); if (_primaryScrollController != null && _primaryScrollController.hasClients) { _primaryScrollController.animateTo( 0.0, duration: const Duration(milliseconds: 300), curve: Curves.linear, // TODO(ianh): Use a more appropriate curve. ); } } Итак, Flutter пытается найти ближайший к текущему контексту PrimaryScrollController и начать его скролить. Что такое PrimaryScrollController ? /// Associates a [ScrollController] with a subtree.
/// /// When a [ScrollView] has [ScrollView.primary] set to true and is not given /// an explicit [ScrollController], the [ScrollView] uses [of] to find the /// [ScrollController] associated with its subtree. /// /// This mechanism can be used to provide default behavior for scroll views in a /// subtree. For example, the [Scaffold] uses this mechanism to implement the /// scroll-to-top gesture on iOS. /// /// Another default behavior handled by the PrimaryScrollController is default /// [ScrollAction]s. If a ScrollAction is not handled by an otherwise focused /// part of the application, the ScrollAction will be evaluated using the scroll /// view associated with a PrimaryScrollController, for example, when executing /// [Shortcuts] key events like page up and down. /// /// See also: /// * [ScrollAction], an [Action] that scrolls the [Scrollable] that encloses /// the current [primaryFocus] or is attached to the PrimaryScrollController. /// * [Shortcuts], a widget that establishes a [ShortcutManager] to be used /// by its descendants when invoking an [Action] via a keyboard key /// combination that maps to an [Intent]. class PrimaryScrollController extends InheritedWidget { /// Creates a widget that associates a [ScrollController] with a subtree. const PrimaryScrollController({ Key? key, required ScrollController this.controller, required Widget child, }) : assert(controller != null), super(key: key, child: child);
final ScrollController? scrollController =
primary ? PrimaryScrollController.of(context) : controller /// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by an otherwise focused part of the application, /// the ScrollAction will be evaluated using this scroll view, for example, /// when executing [Shortcuts] key events like page up and down. /// /// On iOS, this also identifies the scroll view that will scroll to top in /// response to a tap in the status bar. /// {@endtemplate} /// /// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null. final bool primary; primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical),
Как именно происходит поиск виджета при помощи dependOnInheritedWidgetOfExactType , объясняет вот это видео. Если коротко: метод dependOnInheritedWidgetOfExactType позволяет пройтись вверх по дереву виджетов и найти ближайший виджет с определенным типом.Если выше по контексту главного Scaffold нет другого PrimaryScrollController, то мы всегда придем к PrimaryScrollController из routes.dart: //...
@override Widget build(BuildContext context) { return AnimatedBuilder( animation: widget.route.restorationScopeId, builder: (BuildContext context, Widget? child) { assert(child != null); return RestorationScope( restorationId: widget.route.restorationScopeId.value, child: child!, ); }, child: _ModalScopeStatus( route: widget.route, isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates canPop: widget.route.canPop, // _routeSetState is called if this updates child: Offstage( offstage: widget.route.offstage, // _routeSetState is called if this updates child: PageStorage( bucket: widget.route._storageBucket, // immutable child: Builder( builder: (BuildContext context) { return Actions( actions: <Type, Action<Intent>>{ DismissIntent: _DismissModalAction(context), }, child: PrimaryScrollController( //<- Вот это место //... @override
Widget build(BuildContext context) { return PrimaryScrollController( controller: _scrollController, child: Scaffold( body: _list(), appBar: AppBar(title: const Text('Scroll to top')), ), ); } Будет ли этот тап в статус бар работать, если у нас будет несколько вложенных Scaffold?Возьмем типичный пример с экраном на котором несколько табов: import 'package:example/content_page.dart';
import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int _currentIndex = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Scroll to top')), body: IndexedStack( index: _currentIndex, children: const [ ContentPage(backgroundColor: Colors.white), ContentPage(backgroundColor: Colors.deepOrange), ContentPage(backgroundColor: Colors.green), ], ), bottomNavigationBar: BottomNavigationBar( onTap: _onTabTapped, currentIndex: _currentIndex, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.mail), label: 'Messages'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile') ], ), ); } void _onTabTapped(int index) { setState(() { _currentIndex = index; }); } } ...
@override Widget build(BuildContext context) { return PrimaryScrollController( controller: _scrollController, child: Scaffold( primary: true, body: _list(), backgroundColor: widget.backgroundColor, ), ); } ...
class _FakeScrollPositionWithSingleContext
extends ScrollPositionWithSingleContext { _FakeScrollPositionWithSingleContext({ required BuildContext context, required ScrollsToTopCallback callback, }) : _callback = callback, super( physics: const NeverScrollableScrollPhysics(), context: _FakeScrollContext(context), ); final ScrollsToTopCallback _callback; @override Future<void> animateTo( double to, { required Duration duration, required Curve curve, }) { return _callback( ScrollsToTopEvent(to, duration: duration, curve: curve), ); } } void _attach(BuildContext context) {
final primaryScrollController = PrimaryScrollController.of(context); if (primaryScrollController == null) return; final scrollPositionWithSingleContext = _FakeScrollPositionWithSingleContext( context: context, callback: widget.onScrollsToTop, ); primaryScrollController.attach(scrollPositionWithSingleContext); _primaryScrollController = primaryScrollController; _scrollPositionWithSingleContext = scrollPositionWithSingleContext; } /// Widget for catch scrolls-to-top event
class ScrollsToTop extends StatefulWidget { /// Creates new ScrollsToTop widget const ScrollsToTop({ Key? key, required this.scaffold, required this.onScrollsToTop, }) : super(key: key); /// Primary scaffold of your app final Scaffold scaffold; /// Callback for handle scrolls-to-top event final ScrollsToTopCallback onScrollsToTop; @override State<ScrollsToTop> createState() => _ScrollsToTopState(); } =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 09:10
Часовой пояс: UTC + 5