Распознавание виджетов на экране приложения 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 visibleIndex;
final String text;
final String subtitle;
final bool status;
@override
State createState() => _NotificationChipState();
}
class _NotificationChipState extends State {
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().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()
.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 _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. Много, который мы ведем командой мобильных разработчиков. Рассказываем про свой личный опыт и делимся советами от софт-скиллов до технических знаний. Присоединяйтесь!