problem GestureDetector onTapDown with PageView flutter - flutter

I have a Container A wrapped inside GestureDetector, which tap down to change background color to yellow, tap up to restore color to red. When the Container A is tapped, the color will be changed like this (red -> yellow -> red). But if this container A is wrapped inside a PageView with another empty Container B, tapping the Container A will only show the tap up color(red), the tap down color(yellow) will only be shown if I onLongPress the Container A, which is not expected.
And I tried to remove Container B from the PageView, then the color changing of Container A will be shown correctly(red -> yellow -> red).
Please help me to figure out, thanks. Here is the sample code.
class PageViewDemo extends StatefulWidget {
#override
_PageViewDemoState createState() => _PageViewDemoState();
}
class _PageViewDemoState extends State<PageViewDemo>
with SingleTickerProviderStateMixin {
PageController _pageController;
TabController _tabController;
static var pageOneColor;
static var isTapped;
#override
void initState() {
super.initState();
pageOneColor = Colors.red;
isTapped = false;
_pageController = PageController();
_tabController = TabController(vsync: this, length: 2);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
top: 40.0,
),
child: TabPageSelector(
controller: _tabController,
color: Colors.transparent,
selectedColor: Colors.blue,
),
),
Container(
width: MediaQuery.of(context).size.width,
height: 400,
child: PageView(
controller: _pageController,
scrollDirection: Axis.horizontal,
children: <Widget>[
Container(
color: Colors.greenAccent,
child: Container(
padding: EdgeInsets.all(50),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Colors.black,
),
),
height: 100,
width: 100,
child: RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
CustomButtonGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
CustomButtonGestureRecognizer>(
() => CustomButtonGestureRecognizer(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
),
(CustomButtonGestureRecognizer instance) {},
),
},
child: Container(
color: pageOneColor,
child: Center(
child: Text('Container A'),
),
),
),
// child: GestureDetector(
// onTapDown: (detail) {
// /// TapDown Color isn't shown if 'Container B' is added to PageView
// print(detail);
// setState(() {
// isTapped = true;
// pageOneColor = Colors.yellow;
// });
// },
// onTapUp: (detail) {
// print(detail);
// setState(() {
// isTapped = false;
// pageOneColor = Colors.red;
// });
// },
// child: Container(
// color: pageOneColor,
// child: Center(
// child: Text('Container A'),
// ),
// ),
// ),
),
),
Container(
/// remove Container B will see the correct color changing of Container A
color: Colors.grey,
child: Center(
child: Text('Container B'),
),
),
],
onPageChanged: (int pageId) {
setState(() {
debugPrint('pageId:$pageId');
_tabController.index = pageId;
});
},
),
),
],
),
);
}
_onTapDown(TapDownDetails details) {
print(details);
setState(() {
isTapped = true;
pageOneColor = Colors.yellow;
});
}
_onTapUp(TapUpDetails details) {
print(details);
setState(() {
isTapped = false;
pageOneColor = Colors.red;
});
}
_onTapCancel() {
print('tap cancelled');
setState(() {
isTapped = false;
pageOneColor = Colors.red;
});
}
}
class CustomButtonGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onTapDown;
final Function onTapUp;
final Function onTapCancel;
CustomButtonGestureRecognizer(
{#required this.onTapDown,
#required this.onTapUp,
#required this.onTapCancel});
#override
void addPointer(PointerEvent event) {
if (event is PointerDownEvent) {
onTapDown(TapDownDetails());
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
} else {
stopTrackingPointer(event.pointer);
}
}
#override
void handleEvent(PointerEvent event) {
if (event is PointerDownEvent) {
print('tap down detected');
onTapDown(TapDownDetails());
}
if (event is PointerUpEvent) {
print('tap up detected');
onTapUp(TapUpDetails());
stopTrackingPointer(event.pointer);
}
if (event is PointerCancelEvent) {
print('tap cancel detected');
onTapCancel();
stopTrackingPointer(event.pointer);
}
}
#override
String get debugDescription => 'customButtonTap';
#override
void didStopTrackingLastPointer(int pointer) {}
}

The issue here seems that you're trying to use multiple GestureDetector and it interferes with each other. What you can do here is implement a custom GestureRecognizer in a RawGestureDetector. You can follow this guide fore more details.

Related

Once video paly the second will be pause automatically

file1 : pageViewWidg.dart
class PageViewWidg extends StatefulWidget {
final String videoUrl;
final String videoUrl2;
final String avatarImg;
PageViewWidg({
super.key,
required this.videoUrl,
required this.avatarImg,
required this.videoUrl2,
});
#override
State<PageViewWidg> createState() => _PageViewWidgState();
}
class _PageViewWidgState extends State<PageViewWidg> {
late VideoPlayerController controller;
#override
void initState() {
controller = VideoPlayerController.asset(widget.videoUrl)..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
});
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
// build Profile
buildProfuile(String profilePhoto){
return SizedBox(
width: 60,
height:60,
child: Stack(
children: [
Positioned(child: Container(
width: 50,
height: 50,
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
color:Colors.white,
borderRadius: BorderRadius.circular(25),
),
child:ClipRRect(
borderRadius: BorderRadius.circular(25),
child: Image(image: NetworkImage(profilePhoto),fit: BoxFit.cover),
),
),
),
],
),
);
}
// build Music Album
buildMusicAlbum(String profilePhoto){
return SizedBox(
width: 60,
height:60,
child:Column(
children: [
Container(
padding: const EdgeInsets.all(8),
width: 50,
height:50,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ Colors.grey,Colors.white]
),
borderRadius: BorderRadius.circular(25),
),
child:ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Image(image: NetworkImage(profilePhoto),fit: BoxFit.cover,),
)
)
],
)
);
}
#override
Widget build(BuildContext context) {
return Expanded(
child: InkWell(
onTap:(){
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => ExpandPageView(avatarImg: widget.avatarImg,imgUrl: widget.videoUrl,controller: controller,),
));
},
child: Stack(
children: [
// AspectRatio(
// aspectRatio: controller.value.aspectRatio,
// child: VideoPlayer(controller),
// ),
controller.value.isInitialized
?
VideoPlayer(controller)
:
Container(),
Center(
child: InkWell(
onTap:(){
controller.value.isPlaying
?
setState((){
controller.pause();
})
:
setState((){
controller.play();
});
},
child:
Image.asset(
controller.value.isPlaying
?
'assets/images/pause_icon.png'
:
'assets/images/play_icon.png'
,
height:50,
)
),
),
file2 : home_screen.dart
class HomeSceen extends StatefulWidget {
const HomeSceen({super.key});
#override
State<HomeSceen> createState() => _HomeSceenState();
}
class _HomeSceenState extends State<HomeSceen> {
List<PageViewWidg> videos=[
PageViewWidg(
videoUrl: 'assets/videos/video.mp4',
videoUrl2: 'assets/videos/video.mp4',
avatarImg: 'assets/images/pic2.jpg',
),
PageViewWidg(
videoUrl: 'assets/videos/video2.mp4',
videoUrl2: 'assets/videos/video2.mp4',
avatarImg: 'assets/images/pic1.jfif',
),
PageViewWidg(
videoUrl: 'assets/videos/video3.mp4',
videoUrl2: 'assets/videos/video3.mp4',
avatarImg: 'assets/images/pic2.jpg',
),
PageViewWidg(
videoUrl: 'assets/videos/video4.mp4',
videoUrl2: 'assets/videos/video2.mp4',
avatarImg: 'assets/images/pic1.jfif',
),
PageViewWidg(
videoUrl: 'assets/videos/video2.mp4',
videoUrl2: 'assets/videos/video2.mp4',
avatarImg: 'assets/images/pic2.jpg',
),
PageViewWidg(
videoUrl: 'assets/videos/video2.mp4',
videoUrl2: 'assets/videos/video2.mp4',
avatarImg: 'assets/images/pic2.jpg',
),
];
final PageController pagecontroller=PageController(
initialPage: 0,
);
//
#override
Widget build(BuildContext context) {
// final size=MediaQuery.of(context).size;
bool like=true;
return Scaffold(
body:SafeArea(
child: PageView.builder(
itemCount: videos.length,
controller:pagecontroller,
scrollDirection: Axis.vertical,
itemBuilder: (context,index){
return Stack(
alignment: AlignmentDirectional.centerEnd,
children:[
Column(
children: [
videos[index],
// Divider(height:2,color:Color.fromARGB(255, 76, 72, 72)),
videos[index+1],
],
),
I tried to add two controller and every controller of video,and it's not work for me.
VideoPlayerController _controller1;
VideoPlayerController _controller2;
#override
void initState() {
super.initState();
_controller1 = VideoPlayerController.assets(videoUrl1)
..initialize().then((_) {
setState(() {});
});
_controller2 = VideoPlayerController.assets(videoUrl2)
..initialize().then((_) {
setState(() {});
});
}
what I need now is how to control two videos when video is start the second will stop and
change their icon or imageAssets like if the icon is paused it changes to the play icon and the second video also if the first video has a pause icon another video will be change to start video.
here is the answer that I found finally.
late VideoPlayerController _controller1;
late VideoPlayerController _controller2;
bool _isPaused1 = true;
bool _isPaused2 = true;
#override
void initState() {
super.initState();
_controller1 =VideoPlayerController.network(videos[3]);
_controller1.addListener(() {
setState(() {});
});
// _controller1.setLooping(true);
_controller1.initialize().then((_) => setState(() {}));
_controller2 =VideoPlayerController.network(videos[5]);
_controller2.addListener(() {
setState(() {});
});
// _controller2.setLooping(true);
_controller2.initialize().then((_) => setState(() {}));
}
#override
void dispose() {
_controller1.dispose();
_controller2.dispose();
super.dispose();
}
_playPauseToggle1() {
setState(() {
if (_isPaused1) {
_controller1.play();
_controller2.pause(); // if _controller1 is playing, _controller2 should be paused
_isPaused2 = true;
} else {
_controller1.pause();
}
_isPaused1 = !_isPaused1;
});
}
_playPauseToggle2() {
setState(() {
if (_isPaused2) {
_controller2.play();
_controller1.pause(); // if _controller2 is playing, _controller1 should be paused
_isPaused1 = true;
} else {
_controller2.pause();
}
_isPaused2 = !_isPaused2;
});
}
VideoPlayer(_controller1),
Center(
child: InkWell(
onTap: _playPauseToggle1,
// ignore: dead_code
child:_isPaused1?
Image.asset('assets/images/play_icon.png',height:50,color: Colors.grey,)
:
Image.asset('assets/images/pause_icon.png',height:50,color: Colors.grey,)
),
),
VideoPlayer(_controller2),
Center(
child: InkWell(
onTap: _playPauseToggle2,
// ignore: dead_code
child:_isPaused2 ?
Image.asset('assets/images/play_icon.png',height:50,color: Colors.grey,)
:
Image.asset('assets/images/pause_icon.png',height:50,color: Colors.grey,)
),
),
and depends on the video if is starting I change the icon otherwise I change the icon to another state.
and I hope it will be helpful for some who looking for like this question.

Animation library is giving me this error about lifecycle state

Animation library is giving me this error about lifecycle state-- 'package:flutter/src/widgets/framework.dart': Failed assertion: line 4519 pos 12: '_lifecycleState != _ElementLifecycle.defunct': is not true. is was getting this error when i used animation controller.
After the error my flutter application crashed and i have to reload everytime.
// ignore_for_file: depend_on_referenced_packages, sized_box_for_whitespace, avoid_print
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:habbit_app/controllers/timer_tab_controller.dart';
import 'package:habbit_app/screens/timer/set_timer_component.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';
import 'package:habbit_app/widgets/text_widget/label_text.dart';
class TimerTab extends StatefulWidget {
const TimerTab({super.key});
#override
State<TimerTab> createState() => _TimerTabState();
}
class _TimerTabState extends State<TimerTab> with TickerProviderStateMixin {
AnimationController? animationController;
TimerTabController tabController =
Get.put(TimerTabController(), permanent: false);
late TabController _controller;
bool isPlaying = false;
// int get countText2 {
// int count = tabController.totalSeconds;
// return;
// }
String get countText {
Duration count =
animationController!.duration! * animationController!.value;
return animationController!.isDismissed
? '${animationController!.duration!.inHours}:${(animationController!.duration!.inMinutes % 60).toString().padLeft(2, '0')}:${(animationController!.duration!.inSeconds % 60).toString().padLeft(2, '0')}'
: '${count.inHours}:${(count.inMinutes % 60).toString().padLeft(2, '0')}:${(count.inSeconds % 60).toString().padLeft(2, '0')}';
}
#override
void dispose() {
animationController!.dispose();
super.dispose();
}
double progress = 1.0;
void notifiy() {
if (countText == "0:00:00") {
FlutterRingtonePlayer.playNotification();
}
}
#override
void initState() {
// ignore: todo
// TODO: implement initState
TimerTabController tabController =
Get.put(TimerTabController(), permanent: false);
super.initState();
_controller = TabController(length: 3, vsync: this);
_controller.addListener(() {
tabController.tabIndex.value = _controller.index;
// print(tabController.tabIndex.value);
});
super.initState();
animationController = AnimationController(
vsync: this, duration: Duration(seconds: tabController.totalSeconds));
animationController!.addListener(() {
notifiy();
if (animationController!.isAnimating) {
setState(() {
progress = animationController!.value;
});
} else {
setState(() {
progress = 1.0;
isPlaying = false;
});
}
});
}
#override
Widget build(BuildContext context) {
var color = Theme.of(context);
return Obx(
() => tabController.isFirst.value
? SetTimerComponent(
changeAnimation: () {
animationController!.duration =
Duration(seconds: tabController.totalSeconds);
tabController.changeFirst();
},
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// start timer screen
Container(
height: 350,
width: MediaQuery.of(context).size.width,
// color: color.cardColor,
child: Column(children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 300,
height: 300,
child: CircularProgressIndicator(
color: color.primaryColor,
backgroundColor:
color.disabledColor.withOpacity(0.6),
value: progress,
strokeWidth: 10,
)),
GestureDetector(
onTap: () {
if (animationController!.isDismissed) {
showModalBottomSheet(
context: context,
builder: (context) => Container(
height: 300,
child: CupertinoTimerPicker(
initialTimerDuration:
animationController!.duration!,
backgroundColor:
color.backgroundColor,
onTimerDurationChanged: (time) {
setState(() {
animationController!.duration =
time;
});
},
),
));
}
},
child: AnimatedBuilder(
animation: animationController!,
builder: ( context, child) => Text(
countText,
style: TextStyle(
fontSize: 60,
fontWeight: FontWeight.bold,
color: color.primaryColor),
),
),
),
],
),
]),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: () {
print(tabController.totalSeconds);
if (animationController!.isAnimating) {
animationController!.stop();
setState(() {
isPlaying = false;
});
} else {
animationController!.reverse(
from: animationController!.value == 0
? 1.0
: animationController!.value);
setState(() {
isPlaying = true;
});
}
},
child: Container(
height: 35,
width: 90,
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(30)),
color: color.primaryColor),
child: Center(
child: LabelText(
text: isPlaying == true ? "PAUSE" : "RESUME",
isBold: true,
isColor: true,
color: Colors.white,
)),
),
),
GestureDetector(
onTap: () {
animationController!.reset();
tabController.currentvalueHour.value = 0;
tabController.currentvalueMin.value = 0;
tabController.currentvalueSec.value = 0;
setState(() {
isPlaying = false;
});
tabController.changeFirst2();
},
child: Container(
height: 35,
width: 90,
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(30)),
color: color.disabledColor.withOpacity(0.5)),
child: const Center(
child: LabelText(
text: "DELETE",
isColor: true,
color: Colors.white,
isBold: true,
)),
),
)
],
)
],
),
);
}
}

Scroll end detect to many times in flutter

I uses Listview.builder. it detect scroll end to many time that is why API call to many times and add duplicate Data in Listview.
Code:-
ListView.builder(
controller: _scrollController
..addListener(() async {
if (_scrollController
.position.pixels -
10 ==
_scrollController.position
.maxScrollExtent -
10 &&
!state.isPaginationLoading) {
print("Scroll End TEst Screen");
await ctx
.read<ProfileCubit>()
.getProfiles(
context, true, null);
}
Dont put logic code inside build. In your case _scrollController will addListener every times widget build called, cause multiple handle will trigger.
Advice for you is create and put handle logic to a function, put addListener/removeListener in initState/dispose because they was called only once.
With your problem, you can create a variale to check api was called yet and prevent other call.
class AppState extends State<App> {
var scroll = ScrollController();
var preventCall = false;
#override
initState() {
scroll.addListener(onScroll);
super.initState();
}
#override
void dispose() {
scroll.removeListener(onScroll);
super.dispose();
}
Future yourFuture() async {}
void onScroll() {
var position = scroll.position.pixels;
if (position >= scroll.position.maxScrollExtent - 10) {
if (!preventCall) {
yourFuture().then((_) => preventCall = false);
preventCall = true;
}
}
}
#override
Widget build(BuildContext context) {
return ...
}
}
You can add a condition to check if API call is happening or not and based on it you can you can call the API. You would also need to handle pagination logic if all info is loaded.
you can always check if you reached at the limit of max Limit of your scroll controller then you can call API
condition is like
child: ListView.builder(
controller: _controller
..addListener(() async {
if (_controller.position.pixels >
_controller.position.maxScrollExtent) {
_loadMore();
}
}),
you may be like full example
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
class Paggination2HomeScreen extends StatefulWidget {
const Paggination2HomeScreen({Key? key}) : super(key: key);
#override
State<Paggination2HomeScreen> createState() => _Paggination2HomeScreenState();
}
class _Paggination2HomeScreenState extends State<Paggination2HomeScreen> {
final String _baseUrl = "https://jsonplaceholder.typicode.com/photos";
int _page = 1;
final int _limit = 10;
bool _hasNextPage = true;
bool _isFirstLoadRunning = false;
bool _isLoadMoreRunning = false;
List _posts = [];
void _firstLoad() async {
setState(() {
_isFirstLoadRunning = true;
});
try {
final res = await http.get(
Uri.parse("$_baseUrl?_page=$_page&_limit=$_limit"),
);
if (res.statusCode == 200) {
setState(() {
_posts = jsonDecode(res.body);
});
log('receivedData : ${jsonDecode(res.body)}');
}
} catch (e) {
if (kDebugMode) {
log('Something went wrong');
}
}
setState(() {
_isFirstLoadRunning = false;
});
}
void _loadMore() async {
if (_hasNextPage == true &&
_isFirstLoadRunning == false &&
_isLoadMoreRunning == false) {
setState(() {
_isLoadMoreRunning = true;
});
_page++;
try {
final res = await http.get(
Uri.parse('$_baseUrl?_page=$_page&_limit=$_limit'),
);
log('url : $_baseUrl?_page=$_page&_limit=$_limit');
if (res.statusCode == 200) {
final List fetchedPost = jsonDecode(res.body);
if (fetchedPost.isNotEmpty) {
setState(() {
_posts.addAll(fetchedPost);
});
} else {
setState(() {
_hasNextPage = false;
});
}
}
} catch (e) {
if (kDebugMode) {
log('something went wrong');
}
}
setState(() {
_isLoadMoreRunning = false;
});
}
}
// the controller for listyView
late ScrollController _controller;
#override
void initState() {
super.initState();
_firstLoad();
_controller = ScrollController();
}
#override
void dispose() {
super.dispose();
_controller.removeListener(_loadMore);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.orangeAccent,
elevation: 0.0,
centerTitle: true,
title: Text(
'Paggination',
style: TextStyle(
color: Colors.black.withOpacity(0.52),
fontSize: 20,
),
),
systemOverlayStyle: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
),
),
body: _isFirstLoadRunning
? Center(
child: CircularProgressIndicator(
color: Colors.black.withOpacity(0.25),
),
)
: Column(
children: [
Expanded(
child: ListView.builder(
controller: _controller
..addListener(() async {
if (_controller.position.pixels >
_controller.position.maxScrollExtent) {
_loadMore();
}
}),
physics: const BouncingScrollPhysics(),
itemCount: _posts.length,
itemBuilder: (context, index) => Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.065),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.black.withOpacity(0.2),
width: 0.5,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'ID : ${_posts.elementAt(index)['id']}',
),
Text(
'AlbumID : ${_posts.elementAt(index)['albumId']}',
),
Text(
'Title : ${_posts.elementAt(index)['title']}',
textAlign: TextAlign.justify,
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
height: MediaQuery.of(context).size.width * 0.4,
width: MediaQuery.of(context).size.width * 0.4,
child: Image.network(
_posts.elementAt(index)['url'],
),
),
SizedBox(
height: MediaQuery.of(context).size.width * 0.4,
width: MediaQuery.of(context).size.width * 0.4,
child: Image.network(
_posts.elementAt(index)['thumbnailUrl'],
),
),
],
),
],
),
),
),
),
// when the _loadMore running
if (_isLoadMoreRunning == true)
Container(
color: Colors.transparent,
padding: const EdgeInsets.only(top: 10, bottom: 20),
child: const Center(
child: CircularProgressIndicator(
color: Colors.amberAccent,
strokeWidth: 3,
backgroundColor: Colors.transparent,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
),
),
if (_hasNextPage == false)
SafeArea(
child: Container(
width: double.maxFinite,
padding: const EdgeInsets.only(top: 20, bottom: 20),
color: Colors.orangeAccent,
child: const Text('you get all'),
),
)
],
),
);
}
}

I want to refresh a ListView when search item is selected from dropdown

I have a screen as soon as i navigate to this screen from previous screen i fetchs data from server according to selected location. For this i call getData() method in initState() method.
On this Screen i have a searchbar on top which shows dropdown list of locations, and below i have a listview which shows data according to choosen location.
Now I want to refresh listView when a user selects any location from dropdown list from search bar. I'm stuck how can i call my getData() method again when selected loation changed.
.
class SearchResultPage extends StatefulWidget {
final TypeAheadModel selectedLocation;
SearchResultPage(this.selectedLocation);
#override
_SearchResultPageState createState() => _SearchResultPageState();
}
class _SearchResultPageState extends State<SearchResultPage>
with AutomaticKeepAliveClientMixin<SearchResultPage> {
late ScrollController _scrollController;
late FoodSearchViewModel viewModel;
#override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_scrollListener);
viewModel = FoodSearchViewModel();
getData(widget.selectedLocation);
}
#override
void didUpdateWidget(SearchResultPage oldWidget) {
super.didUpdateWidget(oldWidget);
}
Future<List<DishModel>> getData(TypeAheadModel typeAheadModel) async {
if (!viewModel.isLoading) {
setState(() {
viewModel.isLoading = true;
});
var result = await viewModel.getData(widget.selectedLocation);
setState(() {
if (result.isEmpty || result.length == viewModel.totalCount) {
viewModel.hasLoadMore = false;
}
viewModel.isLoading = false;
viewModel.showShimmer = false;
});
}
return viewModel.foodList;
}
void _scrollListener() {
loadMore();
}
Future<List<DishModel>> loadMore() async {
if (!viewModel.isLoading) {
if (_scrollController.position.extentAfter < 150) {
setState(() {
viewModel.isLoading = true;
});
var moreItems = await viewModel.loadMore(widget.selectedLocation);
setState(() {
if (moreItems.isEmpty ||
viewModel.foodList.length == viewModel.totalCount) {
viewModel.hasLoadMore = false;
}
viewModel.isLoading = false;
});
}
return viewModel.foodList;
}
return viewModel.foodList;
}
final brandId = 0;
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (Navigator.of(context).userGestureInProgress)
return false;
else
return true;
},
child: Scaffold(
backgroundColor: kBackgroundColor,
body: Container(
child: ListView.builder(
itemCount: viewModel.showShimmer
? 5
: (viewModel.hasLoadMore
? viewModel.foodList.length + 1
: viewModel.foodList.length),
controller: _scrollController,
cacheExtent: 9999,
itemBuilder: (context, index) {
print(index);
if (!viewModel.showShimmer &&
viewModel.hasLoadMore &&
index >= viewModel.foodList.length) {
return Padding(
padding: EdgeInsets.all(5),
child: const Center(child: CupertinoActivityIndicator()),
);
} else {
if (viewModel.showShimmer) {
return buildShimmer();
} else {
return DishTile(
dishNameTextStyle: titleTextStyle,
textStyle: subtitleTextStyle,
dish: viewModel.foodList[index]);
}
}
},
),
),
),
);
}
Widget buildShimmer() => Container(
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
width: double.infinity,
height: 140,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
),
child: Row(
children: [
ShimmerWidget.rectangular(
height: double.infinity,
width: MediaQuery.of(context).size.width * .45,
),
Flexible(
child: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShimmerWidget.rectangular(height: 10),
SizedBox(
height: 10,
),
ShimmerWidget.rectangular(
height: 8,
width: MediaQuery.of(context).size.width * .30,
),
SizedBox(
height: 6,
),
ShimmerWidget.rectangular(
height: 8,
width: MediaQuery.of(context).size.width * .20,
),
],
),
),
),
)
],
),
);
#override
bool get wantKeepAlive => true;
}
It's quite hard to give any concrete example since your code is not executable but the basic idea is: use didUpdateWidget:
#override
void didUpdateWidget(SearchResultPage oldWidget) {
super.didUpdateWidget(oldWidget);
getData(widget.selectedLocation);
}
I see you already had didUpdateWidget so maybe you had an error trying to do this? If so please share the error.

Flutter remove OverlayEntry if touch outside

I have a CustomDropDown, done with a OverlayEntry. The problem is that I have a StatefulWidget for that, which I place in my Screen simply like that:
CustomDropDownButton(
buttonLabel: 'Aus Vorauswahl wählen',
options: [
'1',
'2',
'3',
'4',
],
),
Now inside that CustomDropDownButton I can simply call floatingDropdown.remove(); where ever I want but how can I call that from a Parent-Widget?? I hope you understand my problem. Right now the only way to remove the overlay is by pressing the DropDownButton again, but it should be removed everytime the user taps outside the actual overlay.
I am quite lost here so happy for every help! Let me know if you need any more details!
This is the code for my CustomDropDownButton if that helps:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../constants/styles/colors.dart';
import '../../../constants/styles/text_styles.dart';
import '../../../services/size_service.dart';
import 'drop_down.dart';
class CustomDropDownButton extends StatefulWidget {
String buttonLabel;
final List<String> options;
CustomDropDownButton({
required this.buttonLabel,
required this.options,
});
#override
_CustomDropdownState createState() => _CustomDropdownState();
}
class _CustomDropdownState extends State<CustomDropDownButton> {
late GlobalKey actionKey;
late double height, width, xPosition, yPosition;
bool _isDropdownOpened = false;
int _selectedIndex = -1;
late OverlayEntry floatingDropdown;
#override
void initState() {
actionKey = LabeledGlobalKey(widget.buttonLabel);
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
key: actionKey,
onTap: () {
setState(() {
if (_isDropdownOpened) {
floatingDropdown.remove();
} else {
findDropdownData();
floatingDropdown = _createFloatingDropdown();
Overlay.of(context)!.insert(floatingDropdown);
}
_isDropdownOpened = !_isDropdownOpened;
});
},
child: Container(
height: scaleWidth(50),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(width: 1.0, color: AppColors.black),
),
color: AppColors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: scaleWidth(10),
),
Text(
widget.buttonLabel,
style: AppTextStyles.h5Light,
),
Spacer(),
_isDropdownOpened
? SvgPicture.asset(
'images/icons/arrow_down_primary.svg',
width: scaleWidth(21),
)
: SvgPicture.asset(
'images/icons/arrow_up_primary.svg',
width: scaleWidth(21),
),
SizedBox(
width: scaleWidth(10),
),
],
),
),
);
}
void findDropdownData() {
RenderBox renderBox =
actionKey.currentContext!.findRenderObject()! as RenderBox;
height = renderBox.size.height;
width = renderBox.size.width;
Offset? offset = renderBox.localToGlobal(Offset.zero);
xPosition = offset.dx;
yPosition = offset.dy;
}
OverlayEntry _createFloatingDropdown() {
return OverlayEntry(builder: (context) {
return Positioned(
left: xPosition,
width: width,
top: yPosition + height,
height: widget.options.length * height + scaleWidth(5),
child: DropDown(
itemHeight: height,
options: widget.options,
onOptionTap: (selectedIndex) {
setState(() {
widget.buttonLabel = widget.options[selectedIndex];
_selectedIndex = selectedIndex;
floatingDropdown.remove();
_isDropdownOpened = !_isDropdownOpened;
});
},
selectedIndex: _selectedIndex,
),
);
});
}
}
1. Return a ListView instead GestureDetector
2. Under Listview use that GestureDetector containing DropDown as one of the children.
3. Add another children(widgets) as GestureDetector and set onTap of each one as:
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
In short you have to add GestureDetector to the part wherever you want the tapping should close overlay entry
** Full Code **
//This is to close overlay when you navigate to another screen
#override
void dispose() {
// TODO: implement dispose
floatingDropDown!.remove();
super.dispose();
}
Widget build(BuildContext context) {
return ListView(
children: [
Padding(
padding: EdgeInsets.all(20),
child: GestureDetector(
key: _actionKey,
onTap: () {
setState(() {
if (isDropdownOpened) {
floatingDropDown!.remove();
} else {
findDropDownData();
floatingDropDown = _createFloatingDropDown();
Overlay.of(context)!.insert(floatingDropDown!);
}
isDropdownOpened = !isDropdownOpened;
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), color: Colors.orangeAccent),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: <Widget>[
Text(
widget.text,
style: TextStyle(color: Colors.white, fontSize: 20),
),
Spacer(),
Icon(
Icons.arrow_drop_down,
color: Colors.white,
),
],
),
),
),
),
GestureDetector(
onTap: () {
if(isDropdownOpened){
floatingDropDown!.remove();
isDropdownOpened = false;
}
},
child: Container(
height: 200,
color: Colors.black,
),
)
],
);
}
Let me know whether it helped or not
Listen to full screen onTapDown gesture and navigation event.
The screen' s gesture event:
#override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
PenetrableTapRecognizer: GestureRecognizerFactoryWithHandlers<PenetrableTapRecognizer>(
() => PenetrableTapRecognizer(),
(instance) {
instance.onTapDown = (_) => _handleGlobalGesture();
},
),
},
behavior: HitTestBehavior.opaque,
child: Scaffold(
),
);
}
void _handleGlobalGesture {
// insert or remove the popup menu
// a bool flag maybe helpful
}
class PenetrableTapRecognizer extends TapGestureRecognizer {
#override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}