I coded a simple Lottie animation state manager. From a pool of lottie-json files, each containing also a probability to play, I select one and set the _index to play that file.
However, if randomly the same file will that played before will be played again, the loop just stops and I have to restart... I depict a minimal example of my code here:
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
final Random rnd = Random();
int _index = 0;
late BechtoAnimation _animation;
late final AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this)
..addStatusListener((status) {
if(status == AnimationStatus.completed){
setState(() {
int i = 0;
double p = rnd.nextDouble();
double cumulativeProbability = 0.0;
for(AnimationLoop animationLoop in _animation.loop){
cumulativeProbability += animationLoop.probability;
if (p <= cumulativeProbability) {
_index = i;
break;
}
i++;
}
});
}
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext buildContext) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BlocBuilder<MyCubit, MyState>(
builder: (context, state) {
if(state is AnimationLoadedFailure){
return Text(state.toString(), style: const TextStyle(color: Colors.blueGrey));
}else if(state is AnimationLoadedSuccess){
_animation = state.animation;
return SizedBox(
width: 200,
height: 200,
child: Lottie.asset(
state.animation.loop.elementAt(_index).fileName,
width: 200,
height: 200,
fit: BoxFit.fill,
controller: _controller,
onLoaded: (composition){
_controller.duration = composition.duration;
_controller.reset();
_controller.forward();
}
),
);
}else{
return Text(state.toString(), style: const TextStyle(color: Colors.blueGrey));
}
}
)
],
),
));
}
}
As you can see, I select the new animation when the old one is finished via the StatusListener. However, when its the same, everything just stops... Any idea?
Flutter version is: 3.3.0 and my Dart version is 2.18.0
Related
I want to connect to an ESP32 with my Flutter app. To implement the connection via ESPTouch I use the package "esptouch_flutter".
Now there is the following problem: If I enter my password incorrectly, it tries to connect for a minute and aborts, which is what it is supposed to do. However, if I then enter the correct password, it also searches for a minute and aborts, although it should actually connect. If I enter the correct password without having entered anything wrong before, it works fine.
The actual connection is established via a stream.
stream = widget.task.execute();
I have a suspicion that even after dispose of the page, the stream continues to run and then does not restart properly. Can this be? Or maybe anyone else have an idea?
Full Code:
class wifiConnection extends StatefulWidget {
const wifiConnection({Key? key, required this.task}) : super(key: key);
final ESPTouchTask task;
#override
State<StatefulWidget> createState() => wifiConnectionState();
}
class wifiConnectionState extends State<wifiConnection> {
late final Stream<ESPTouchResult> stream;
late final StreamSubscription<ESPTouchResult> streamSubscription;
late final Timer timer;
final List<ESPTouchResult> results = [];
var connectionSuccessfull = true;
#override
void initState() {
stream = widget.task.execute();
streamSubscription = stream.listen(results.add);
final receiving = widget.task.taskParameter.waitUdpReceiving;
final sending = widget.task.taskParameter.waitUdpSending;
// final cancelLatestAfter = receiving + sending; // Default = 1 min
const testTimer = Duration(seconds: 15);
timer = Timer(
// cancelLatestAfter,
testTimer,
() {
streamSubscription.cancel();
if (results.isEmpty && mounted) {
setState(() {
connectionSuccessfull = false;
});
}
},
);
super.initState();
}
#override
dispose() {
timer.cancel();
streamSubscription.cancel();
super.dispose();
}
_dissolve() {
setState(() async {
await Future.delayed(const Duration(milliseconds: 1000));
setOnboardingEnum(Onboarding.wifiFinished);
Navigator.of(context).pushReplacement(createRoute(const StaticBellPage()));
});
}
getConnectionSite(AsyncSnapshot<ESPTouchResult> snapshot) {
if (snapshot.hasError) {
return connectionError();
}
if (!snapshot.hasData) {
return connectionPending();
}
switch (snapshot.connectionState) {
case ConnectionState.active:
return connectionActive();
case ConnectionState.none:
return connectionError();
case ConnectionState.done:
return connectionActive();
case ConnectionState.waiting:
return connectionPending();
}
}
connectionPending() {
return SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const MydoobeAvatar(avatarWidget: CircularProgressIndicator()),
connectionPendingText,
MydoobeButton(
buttonText: "Zurück",
callback: () => Navigator.of(context)..pop(),
),
],
),
);
}
connectionActive() {
return SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const MydoobeAvatar(avatarWidget: CheckArrow()),
connectionSuccessfullText,
MydoobeButton(buttonText: "Weiter", callback: _dissolve),
],
),
);
}
connectionError() {
return SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const MydoobeAvatar(avatarWidget: ErrorCross()),
connectionErrorText,
MydoobeButton(
buttonText: "Zurück",
callback: () => Navigator.of(context)..pop(),
),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
StreamBuilder<ESPTouchResult>(
builder: (context, AsyncSnapshot<ESPTouchResult> snapshot) {
return AnimatedSwitcher(
duration: const Duration(seconds: 2),
child: connectionSuccessfull ? getConnectionSite(snapshot) : connectionError() as Widget);
},
stream: stream,
),
],
),
],
),
);
}
}
var connectionPendingText = RichText(...);
var connectionSuccessfullText = RichText(...);
var connectionErrorText = RichText(...);
I Like to create animation that will follow the TextField text, here is video from
Flutter YouTube.
Now how can I follow the Target.
Here is My rive file on rive.app or GitHub and Design.
We need a StateMachineController and SMIInput<double> that will be responsible to follow the text.
Result
OutPutVideo
Result with Slider
You can follow the GitHub Repository or
class TextFieldWithRive extends StatefulWidget {
TextFieldWithRive({Key? key}) : super(key: key);
#override
_TextFieldWithRiveState createState() => _TextFieldWithRiveState();
}
class _TextFieldWithRiveState extends State<TextFieldWithRive> {
StateMachineController? controller;
SMIInput<double>? valueController;
Artboard? _riveArtboard;
double sliderVal = 0.0;
/// change value based on size>Width
final double strengthOverTextFiled = 1.5;
final TextEditingController textEditingController = TextEditingController();
#override
void initState() {
super.initState();
rootBundle.load("rives/eyeMovement.riv").then((value) async {
final file = RiveFile.import(value);
final artboard = file.mainArtboard;
controller = StateMachineController.fromArtboard(artboard, "eyeMovement");
if (controller != null) {
print("got state");
setState(() {
artboard.addController(controller!);
valueController = controller!.findInput('moevement_controll');
controller!.inputs.forEach((element) {
print(element.name);
});
});
}
_riveArtboard = artboard;
});
///* eye controll with textFiled
textEditingController.addListener(() {
print(textEditingController.text);
if (valueController != null) {
valueController!.value =
textEditingController.text.length * strengthOverTextFiled;
}
});
}
#override
void dispose() {
textEditingController.removeListener(() {});
textEditingController.dispose();
controller!.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: LayoutBuilder(
builder: (context, constraints) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: constraints.maxWidth * .5,
width: constraints.maxWidth * .5,
child: _riveArtboard == null
? CircularProgressIndicator()
: Rive(
artboard: _riveArtboard!,
),
),
SizedBox(
width: constraints.maxWidth * .8,
child: TextField(
controller: textEditingController,
decoration: InputDecoration(hintText: "keep typing"),
),
),
],
),
),
),
);
}
}
I am trying to recreate RobinHood slide number animation, I am treating every digit as an Item in n animated list, When I am adding an digit to the start of the list the last Item in the list receives a jump and I cant quite figure out how to fix it.
this is the code for every digit
class NumberColView extends StatefulWidget {
final int animateTo;
final bool comma;
final TextStyle textStyle;
final Duration duration;
final Curve curve;
NumberColView(
{#required this.animateTo,
#required this.textStyle,
#required this.duration,
this.comma = false,
#required this.curve})
: assert(animateTo != null && animateTo >= 0 && animateTo < 10);
#override
_NumberColState createState() => _NumberColState();
}
class _NumberColState extends State<NumberColView>
with SingleTickerProviderStateMixin {
ScrollController _scrollController;
double _elementSize = 0.0;
#override
void initState() {
super.initState();
print(_elementSize);
_scrollController = new ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
_elementSize = _scrollController.position.maxScrollExtent / 10;
setState(() {});
});
}
#override
void didUpdateWidget(NumberColView oldWidget) {
if (oldWidget.animateTo != widget.animateTo) {
_scrollController.animateTo(_elementSize * widget.animateTo,
duration: widget.duration, curve: widget.curve);
}
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
// print(widget.animateTo);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IgnorePointer(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: _elementSize),
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: List.generate(10, (position) {
return Text(position.toString(), style: widget.textStyle);
}),
),
),
),
),
widget.comma
? Container(
child: Text(', ',
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.bold)))
: Container(),
],
);
}
}
This the code to build the whole number
class _NumberSlideAnimationState extends State<NumberSlideAnimation> {
#override
void didUpdateWidget(oldWidget) {
if (oldWidget.number.length != widget.number.length) {
int newLen = widget.number.length - oldWidget.number.length;
if(newLen > 0) {
widget.listKey.currentState.insertItem(0,
duration: const Duration(milliseconds: 200));
}
// setState(() {
// animateTo = widget.animateTo;
// });
}
super.didUpdateWidget(oldWidget);
}
Widget digitSlide(BuildContext context, int position, animation) {
int item = int.parse(widget.number[position]);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset(0, 0),
).animate(animation),
child:
// TestCol(animateTo: item, style: widget.textStyle)
NumberColView(
textStyle: widget.textStyle,
duration: widget.duration,
curve: widget.curve,
comma: (widget.number.length - 1) != position &&
(widget.number.length - position) % 3 == 1 ??
true,
animateTo: item,
),
);
}
#override
Widget build(BuildContext context) {
return Container(
height: 40,
child: AnimatedList(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
key: widget.listKey,
initialItemCount: widget.number.length,
itemBuilder: (context, position, animation) {
return digitSlide(context, position, animation);
},
),
);
}
}
I don't know the right solution for your problem, however I know another way and I am just sharing it.
Here the full code If you want to use this method:
update: now 0 will come form bottom as what it should be and the uncharged values will be in there palaces.
class Count extends StatefulWidget {
const Count({Key key}) : super(key: key);
#override
_CountState createState() => _CountState();
}
class _CountState extends State<Count> {
int count = 10;
int count2 = 10;
int count3 = 10;
Alignment alignment = Alignment.topCenter;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRRect(
child: Align(
alignment: Alignment.center,
heightFactor: 0.2,
child: SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...List.generate(count.toString().length, (index) {
final num2IndexInRange =
count2.toString().length - 1 >= index;
final num3IndexInRange =
count3.toString().length - 1 >= index;
final num1 = int.parse(count.toString()[index]),
num2 = num2IndexInRange
? int.parse(count2.toString()[index])
: 9,
num3 = num3IndexInRange
? int.parse(count3.toString()[index])
: 1;
return AnimatedAlign(
key: Key("${num2} ${index}"),
duration: Duration(milliseconds: 500),
alignment: (num1 != num2 || num1 != num3)
? alignment
: Alignment.center,
child: Text(
num3.toString(),
style: TextStyle(
fontSize: 20,
),
),
);
}),
],
),
),
),
),
SizedBox(height: 200),
RaisedButton(
onPressed: () async {
setState(() {
alignment = Alignment.topCenter;
count += 10;
});
await Future.delayed(Duration(milliseconds: 250));
setState(() {
alignment = Alignment.bottomCenter;
count2 = count;
});
await Future.delayed(Duration(milliseconds: 250));
setState(() {
count3 = count;
});
},
),
],
),
);
}
}
if you don't like how the spacing between the numbers changed you can warp the text widget with a sized box and give it a fixed width.
How can I play the same audio file as many times as I want, not loop but I want the possibility to precise how many times, I followed this tutorial to make the basics which mean the play/pause function. And I'm struggling with the rest.
I use the last version of the audioplayers package.
This is my code :
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:audioplayers/audio_cache.dart';
class Player extends StatefulWidget {
#override
_PlayerState createState() => _PlayerState();
}
AnimationController _animationIconController ;
AudioCache audioCache;
AudioPlayer audioPlayer;
bool issongplaying = false;
bool isplaying = false;
class _PlayerState extends State<Player> with TickerProviderStateMixin {
#override
void initState() {
// TODO: implement initState
super.initState();
initPlayer();
}
void initPlayer(){
_animationIconController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
reverseDuration: Duration(milliseconds: 750),
);
audioPlayer = new AudioPlayer();
audioCache = new AudioCache(fixedPlayer: audioPlayer);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
onTap: (){
setState((){
isplaying
? _animationIconController.reverse()
: _animationIconController.forward();
isplaying = !isplaying;
});
if (issongplaying == false) {
audioCache.play('audio.wav');
setState(() {
issongplaying = true;
});
} else {
audioPlayer.pause();
setState(() {
issongplaying = false;
});
}
},
child: ClipOval(
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 2,
color: Colors.greenAccent,
),
borderRadius: BorderRadius.all(Radius.circular(50.0)
),
),
width: 75,
height: 75,
child: Center(
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _animationIconController,
color: Colors.grey,
size: 60,
)
),
),
),
)
],
),
),
);
}
}
Thank you
This might be complex for you but try giving it a shot.
I can see the audioplayers package has the event onPlayerCompletion, which will be called each time you finish playing an audio.
You can tell your player in this event to track the amount of times it ends and set the audio to loop, it will stop when it repeats X times:
int timesPlayed = 0;
int timesToRepeat = 3; //The audio will repeat 3 times
//This method gets called each time your audio finishes playing.
player.onPlayerCompletion.listen((event) {
//Here goes the code that will be called when the audio finishes
onComplete();
setState(() {
position = duration;
timesPlayed++;
if(timesPlayed >= timesToRepeat) {
timesPlayed = 0;
await player.stop();
}
});
});
I have an app, it has a page that act as an entry point and showing a TabView containing 3 or more pages on it. It uses NestedScrollView and SliverAppBar to give some animation when user scroll the view.
I want to implement lazy load of a paginated list but since it does not allows me to use a controller inside the CustomScrollView as mentioned in the docs in this line:
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
I cannot make use of ScrollController in the child page to get the scroll value to trigger the loadMore function. Fortunately, there is a similar widget to listen the scroll event called ScrollNotification. But I don't know which property is holding the value of the maximum scroll limit.
Tried to compare the available properties by this:
bool _onScrollNotification(ScrollNotification notification) {
if (notification is! ScrollEndNotification) return false;
print('extentBefore: ${notification.metrics.extentBefore}');
print('extentAfter: ${notification.metrics.extentAfter}');
print('maxScrollExtent: ${notification.metrics.maxScrollExtent}');
return true;
}
But its seems like they doesn't hold any fixed value as I need. It always changed its value independently.
I also cannot use the ScrollController on the parent page (the tabview_holder) since each page in each tabs has independent bloc, events, data & fetching algorithm. With that in mind, how can I achieve this requirement?
Please have a look at my script:
tabview_holder.dart (not a real file name, just to illustrate it)
class EventPage extends StatefulWidget {
EventPage({Key key}) : super(key: key);
#override
_EventPageState createState() => _EventPageState();
}
class _EventPageState extends State<EventPage>
with SingleTickerProviderStateMixin {
final ScrollController _scrollController = ScrollController();
final List<Widget> _tabs = [
Tab(text: 'My Events'),
Tab(text: "Private Events"),
Tab(text: "Division Events"),
Tab(text: "Department Events"),
Tab(text: "Public Events"),
];
double _bottomNavigatorPosition = 0.0;
double _gradientStop = 0.2;
TabController _tabController;
#override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
_tabController = TabController(
initialIndex: 0,
length: _tabs.length,
vsync: this,
);
}
#override
void dispose() {
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
void _scrollListener() {
ScrollDirection direction = _scrollController.position.userScrollDirection;
switch (direction) {
case ScrollDirection.reverse:
setState(() {
_gradientStop = 0.0;
_bottomNavigatorPosition = -100.0;
});
return;
break;
case ScrollDirection.forward:
case ScrollDirection.idle:
setState(() {
_gradientStop = 0.2;
_bottomNavigatorPosition = 0.0;
});
break;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
NestedScrollView(
controller: _scrollController,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
sliver: SliverAppBar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
automaticallyImplyLeading: false,
floating: true,
expandedHeight: 100,
flexibleSpace: FlexibleSpaceBar(
background: Container(
child: Stack(
children: [
Positioned(
left: 30.0,
bottom: 10,
child: PageHeader(title: 'Events'),
),
],
),
),
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
TabBar(
controller: _tabController,
isScrollable: true,
indicator: BubbleTabIndicator(
indicatorHeight: 35.0,
indicatorColor: Theme.of(context).primaryColor,
tabBarIndicatorSize: TabBarIndicatorSize.tab,
),
tabs: _tabs,
),
),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
MyEventsPage(),
PrivateEventsPage(),
MyEventsPage(),
MyEventsPage(),
MyEventsPage(),
],
),
),
_buildBottomGradient(),
_buildBottomNavigator(),
],
),
),
);
}
Widget _buildBottomGradient() {
return IgnorePointer(
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [_gradientStop / 2, _gradientStop],
colors: [
Color(0xFF121212),
Colors.transparent,
],
),
),
),
);
}
Widget _buildBottomNavigator() {
return AnimatedPositioned(
duration: Duration(milliseconds: 200),
left: 0.0,
right: 0.0,
bottom: _bottomNavigatorPosition,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: PageNavigator(
primaryButtonText: 'Create new event',
onPressedPrimaryButton: () {
Navigator.of(context).pushNamed(Routes.EVENT_CREATE);
},
),
),
);
}
}
tabview_item.dart
class MyEventsPage extends StatefulWidget {
MyEventsPage({Key key}) : super(key: key);
#override
_MyEventsPageState createState() => _MyEventsPageState();
}
class _MyEventsPageState extends State<MyEventsPage>
with AutomaticKeepAliveClientMixin<MyEventsPage> {
Completer<void> _refreshCompleter;
PaginatedEvent _paginated;
MyEventsBloc _myEventsBloc;
bool _isFetchingMoreInBackground;
#override
void initState() {
super.initState();
_myEventsBloc = BlocProvider.of<MyEventsBloc>(context);
_myEventsBloc.add(MyEventsPageInitialized());
_refreshCompleter = Completer<void>();
_isFetchingMoreInBackground = false;
}
void _set(PaginatedEvent paginated) {
setState(() {
_paginated = paginated;
});
_refreshCompleter?.complete();
_refreshCompleter = Completer();
}
void _add(Event data) {
setState(() {
_paginated.data.add(data);
});
}
void _update(Event data) {
final int index = _paginated.data.indexWhere((leave) {
return leave.id == data.id;
});
setState(() {
_paginated.data[index] = data;
});
}
void _destroy(Event data) {
final int index = _paginated.data.indexWhere((leave) {
return leave.id == data.id;
});
setState(() {
_paginated.data.removeAt(index);
});
}
void _append(PaginatedEvent paginated) {
setState(() {
_paginated.currentPage = paginated.currentPage;
_paginated.data.addAll(paginated.data);
});
}
bool _onScrollNotification(ScrollNotification notification) {
if (notification is! ScrollEndNotification) return false;
print('extentBefore: ${notification.metrics.extentBefore}');
print('extentAfter: ${notification.metrics.extentAfter}');
print('maxScrollExtent: ${notification.metrics.maxScrollExtent}');
return true;
}
#override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () {
_myEventsBloc.add(MyEventsRefreshRequested());
return _refreshCompleter.future;
},
child: NotificationListener<ScrollNotification>(
onNotification: _onScrollNotification,
child: CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverToBoxAdapter(
child: BlocConsumer<MyEventsBloc, MyEventsState>(
listener: (context, state) {
if (state is MyEventsLoadSuccess) {
_set(state.data);
}
if (state is MyEventsCreateSuccess) {
_add(state.data);
}
if (state is MyEventsUpdateSuccess) {
_update(state.data);
}
if (state is MyEventsDestroySuccess) {
_destroy(state.data);
}
if (state is MyEventsLoadMoreSuccess) {
_append(state.data);
}
},
builder: (context, state) {
if (state is MyEventsLoadSuccess) {
return EventList(data: _paginated.data);
}
return ListLoading();
},
),
),
],
),
),
);
}
#override
bool get wantKeepAlive => true;
}
Finally found the answer by my own after doing some research. Not a perfect solution but it works.
bool _onScrollNotification(UserScrollNotification notification) {
/// Make sure it listening to the nearest depth of scrollable views
/// and ignore notifications if scroll axis is not vertical.
if (notification.depth == 0 && notification.metrics.axis == Axis.vertical) {
ScrollDirection direction = notification.direction;
if (direction == ScrollDirection.reverse && !_isFetchingMoreData) {
/// Check if the user is scrolling the list downward to prevent
/// function call on upward. Also check if there is any fetch
/// queues, if it still fetching, skip this step and do nothing.
/// It was necessary to prevent the notification to bubble up
/// the widget with `_loadMoreData()` call.
if (_paginated.currentPage < _paginated.lastPage)
/// If the conditions above are passed, we are safe to load more.
return _loadMoreData();
}
}
return true;
}