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

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

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

ad38a956f034459f4946ba7cb1faf54e.gif

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

Для начала создадим кастомный виджет уведомления. Для определения объекта на экране используем пакет 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. Много, который мы ведем командой мобильных разработчиков. Рассказываем про свой личный опыт и делимся советами от софт-скиллов до технических знаний. Присоединяйтесь!

© Habrahabr.ru