Распознавание виджетов на экране приложения Flutter

Hola, Amigos! На связи Саша Чаплыгин, Flutter-dev агентства продуктовой разработки Amiga и соавтор телеграм-канала Flutter. Много. Сегодня мы вновь займемся практикой! Расскажу об интересной теме — определение положения объекта на экране. Это может быть полезно, когда мы хотим понять, виден тот или иной виджет на экране в данный момент или нет.

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

Как же это сделать?

Для начала создадим кастомный виджет уведомления. Для определения объекта на экране используем пакет visibility_detector. При применении данного пакета обязательно используйте UniqueKey, так как данный пакет включает в себя RenderObject.

import 'package:flutter/material.dart'; import 'package:hmelbakery/core/style/colors.dart'; import 'package:visibility_detector/visibility_detector.dart'; class NotificationChip extends StatefulWidget { NotificationChip({ required this.index, required this.visibleIndex, super.key, this.title = '', this.text = '', this.subtitle = '', this.status = true, }); final String title; final int index; final ValueChanged<int> visibleIndex; final String text; final String subtitle; final bool status; @override State<NotificationChip> createState() => _NotificationChipState(); } class _NotificationChipState extends State<NotificationChip> { final UniqueKey key = UniqueKey(); late bool _visible; @override void initState() { _visible = widget.status; super.initState(); } @override Widget build(BuildContext context) { return VisibilityDetector( onVisibilityChanged: !_visible ? (visibilityInfo) { // Здесь определяется на сколько виден объект в процентах var visiblePercentage = visibilityInfo.visibleFraction * 100; if (visiblePercentage == 100.0) { setState(() { _visible = true; }); widget.visibleIndex.call(widget.index); // прочитали } } : null, key: key, child: Container( decoration: BoxDecoration(color: AppColors.grey, borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( widget.title, style: Theme.of(context).textTheme.titleLarge, ), if (!_visible) Container( width: 16, height: 16, decoration: const BoxDecoration( color: AppColors.yellow, shape: BoxShape.circle, ), ), ], ), const SizedBox(height: 8), Text(widget.text, style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 16), Text( widget.subtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: AppColors.black400), ), ], ), ), ), ); } }

Отображаем список уведомлений на экране.

ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemBuilder: (BuildContext context, int index) { if (index == state.pageState.data.length - 1 && !state.pageState.loadNewPage) { context.read<ProfileNotificationsBloc>().add(ProfileNotificationsFetchDataEvent()); } return Column( children: [ NotificationChip( title: state.pageState.data[index].title, text: state.pageState.data[index].text, subtitle: DateConverter.formattingDateWTime(state.pageState.data[index].datetime), status: !(state.pageState.data[index].statusCode == 'unread'), index: index, visibleIndex: (int value) { context .read<ProfileNotificationsBloc>() .add(ProfileNotificationsMarkReadEvent(index: value)); // передаем в блок индекс прочитаного }, ), const SizedBox(height: 8), if (index == state.pageState.data.length - 1 && state.pageState.loadNewPage) ...[ const Center(child: CircularProgressIndicator(color: AppColors.black)), const SizedBox(height: 20), ], ], ); }, itemCount: state.pageState.data.length, ),

В блоке создаем список прочитанных индексов и тут же меняем состояние тех, что уже просмотрели, для снятия значка «прочитанности».

markRead(ProfileNotificationsMarkReadEvent event, emit) async { if (!state.pageState.readIndexes.contains(event.index)) { emit(ProfileNotificationsUp(state.pageState.copyWith( readIndexes: [...state.pageState.readIndexes, event.index], data: state.pageState.data..[event.index] = state.pageState.data[event.index].copyWith(statusCode: 'read'), ))); } }

Далее нужно решить задачу, как правильно отправить на сервер информацию? Не будем же мы каждый прочитанный индекс отправлять, верно?

Timer? _timer; List<int> _tempList = []; _timerFunc() { // старт функции в блоке _timer = Timer.periodic( const Duration(seconds: 1), (timer) { if (_tempList.length < state.pageState.readIndexes.length) { _tempList = state.pageState.readIndexes; notificationsRepository.markRead( request: MarkReadNotificationRequest( notifications: state.pageState.data .sublist(_tempList.first, _tempList.length > 1 ? _tempList.last : _tempList.first + 1) .map((e) => e.id) .toList(), ), ); } }, ); }

Создаем таймер, который каждую секунду проверяет изменился ли список прочитанных уведомлений, и тогда отправляем на сервер. Конечно же мы не будем заставлять пользователя ждать ответа и блокировать экран загрузкой, поэтому не используем async/await.

Всё готово! Надеюсь, вам будет полезно. Делитесь в нашем чате мобильных разработчиков о своем опыте применения пакета visibility_detector.

А также всегда ждем вас в нашем телеграм-канале Flutter. Много, который мы ведем командой мобильных разработчиков. Рассказываем про свой личный опыт и делимся советами от софт-скиллов до технических знаний. Присоединяйтесь!

0
Комментарии
-3 комментариев
Раскрывать всегда