-->

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

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


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

(Notifications read)

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

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

Хотите связаться с владельцами компании напрямую?
Константин Франгуриди
Константин Франгуриди
Account director

НАПИСАТЬ

Дмитрий Тарасов
Дмитрий Тарасов
СЕО

НАПИСАТЬ