SliverProblem

Run Settings
LanguageDart
Language Version
Run Command
return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return <Widget>[ // Используем метод для построения заголовка _buildHomeHeader(context, username, unViewedCounts, opportunitiesState), // Добавляем SliverOverlapAbsorber для корректной работы с вложенными списками SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( pinned: true, toolbarHeight: 36, backgroundColor: Colors.white, forceElevated: innerBoxIsScrolled, flexibleSpace: FlexibleSpaceBar( background: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), child: Container( color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Возможности', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), ], ), ), ), ), bottom: TabBar( controller: _tabController, labelColor: Colors.blue, unselectedLabelColor: Colors.grey, indicatorColor: Colors.blue, isScrollable: true, tabs: [ // Используем кастомные табы с индикаторами непросмотренных элементов BadgedTab( text: 'Стипендии', unviewedCount: unViewedCounts.isNotEmpty ? unViewedCounts[0] : 0, ), BadgedTab( text: 'Стажировки', unviewedCount: unViewedCounts.length > 1 ? unViewedCounts[1] : 0, ), BadgedTab( text: 'Олимпиады', unviewedCount: unViewedCounts.length > 2 ? unViewedCounts[2] : 0, ), // Tab(text: 'Избранные'), // Для избранных индикатор не нужен ], ), ), ), ]; }, body: TabBarView( controller: _tabController, children: tabs.keys.map((String name) { // Map tab name to opportunity type for API calls String? opportunityType; switch (name) { case 'Стипендии': opportunityType = 'grant'; break; case 'Стажировки': opportunityType = 'internship'; break; case 'Олимпиады': opportunityType = 'olympiad'; break; } return SafeArea( top: false, bottom: false, child: Builder( builder: (BuildContext context) { return EasyRefresh( header: const MaterialHeader( position: IndicatorPosition.locator, ), footer: const ClassicFooter( position: IndicatorPosition.locator, processedDuration: Duration(milliseconds: 400), ), onRefresh: () async { // Обновляем данные в зависимости от вкладки context.read<OpportunitiesBloc>().add(RefreshOpportunities()); }, onLoad: () async { // Calculate current offset based on list length final currentList = tabs[name]!; final offset = currentList.length; // Для других вкладок используем OpportunitiesBloc context.read<OpportunitiesBloc>().add(FetchMoreOpportunities( type: opportunityType, offset: offset, )); }, child: CustomScrollView( key: PageStorageKey<String>(name), slivers: <Widget>[ // Overlap injector for nested scrolling SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), // Header locator for pull-to-refresh const HeaderLocator.sliver(), tabs[name]!.isNotEmpty ? SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { final opportunity = tabs[name]![index]; return OpportunityListItem( data: opportunity, onFavoritePressed: (data) { BlocProvider.of<FavoritesBloc>(context).add( ToggleFavoriteStatus(opportunity: data) ); BlocProvider.of<OpportunitiesBloc>(context).add(ToggleFavorite(opportunity: data)); print('Favorite toggled for ${data.title}'); }, onReturnRefresh: () { // Обновляем оба блока при возвращении на экран BlocProvider.of<OpportunitiesBloc>(context).add(RefreshWOAnimation()); BlocProvider.of<FavoritesBloc>(context).add(LoadFavorites()); }, onMarkViewed: (data){ BlocProvider.of<OpportunitiesBloc>(context).add(MarkOpportunityAsViewed( eventId: data.eventId, category: data.typesEvent )); }, ); }, // Use correct length based on the current tab childCount: tabs[name]!.length, ), ) : const SliverFillRemaining( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inbox, color: Colors.grey, size: 60), SizedBox(height: 16), Text('Список пуст'), ], ), ), ), // Footer locator for infinite loading const FooterLocator.sliver(), ], ), ); }, ), ); }).toList(), ), ), ); } // Метод для построения заголовка главного экрана с приветствием, уведомлениями, поиском и событиями Widget _buildHomeHeader(BuildContext context, String username, List<int> unViewedCounts, OpportunitiesLoaded opportunitiesState) { return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Секция приветствия и кнопка уведомлений Padding( padding: const EdgeInsets.only(left: 16, right: 16, top: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Привет, $username!', style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, ), ), const Text( 'Найди своё будущее вместе с нами!', style: TextStyle( color: Colors.grey, fontSize: 14, ), ), ], ), Material( color: Colors.white, borderRadius: BorderRadius.circular(12), elevation: 2, child: InkWell( onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => UnviewedScreen(), ), ); }, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(10), child: Badge( // Показываем только маркер без цифры внутри smallSize: 8, backgroundColor: Colors.red, // Показываем бейдж только если есть непросмотренные элементы isLabelVisible: unViewedCounts.fold(0, (total, count) => total + count) > 0, child: const Icon( Icons.notifications, color: Colors.grey, size: 24, ), ), ), ), ), ], ), ), // Поисковая строка с кнопкой фильтрации Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Row( children: [ Expanded( child: GestureDetector( onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const SearchScreen(), ), ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), height: 48, // Фиксированная высота для поисковой строки decoration: BoxDecoration( color: Colors.white, // Фон контейнера borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), // Цвет тени blurRadius: 12, // Размытие spreadRadius: 2, // Радиус распространения offset: const Offset(0, 4), // Смещение вниз ), ], ), child: const TextField( enabled: false, // Отключаем редактирование, чтобы работал только onTap decoration: InputDecoration( icon: Icon(Icons.search, color: Colors.grey), hintText: 'Искать', border: InputBorder.none, ), ), ), ), ), const SizedBox(width: 8), Container( decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), // Цвет тени blurRadius: 12, // Размытие spreadRadius: 2, // Радиус распространения offset: const Offset(0, 4), // Смещение вниз ), ], ), child: IconButton( icon: const Icon(Icons.tune), onPressed: () { // Показываем диалог фильтрации или переходим на экран поиска Navigator.of(context).push( MaterialPageRoute( builder: (context) => const SearchScreen(), ), ); }, color: Colors.white, ), ), ], ), ), // Секция "События" с горизонтальным списком Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Мероприятия', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), TextButton( onPressed: () { // Переход в EventsScreen Navigator.of(context).push( MaterialPageRoute( builder: (context) => EventsScreen(), ), ); }, child: const Text( 'Смотреть все', style: TextStyle( color: Colors.blue, ), ), ), ], ), const SizedBox(height: 8), SizedBox( height: 180, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: opportunitiesState.events.length, itemBuilder: (context, index) { final event = opportunitiesState.events[index]; return OpportunityCard( event: event, onReturnRefresh: () { BlocProvider.of<EventBloc>(context).add(RefreshWOAnimationEvent()); }, isHorizontal: true, ); }, ), ), ], ), ), ], ), ), ); }
Editor Settings
Theme
Key bindings
Full width
Lines