Flutter - setState is not Updating inner Custom Stateful widget - flutter

I have created a Custom Segments widget which creates Multiple TABS according to List. I am updating selectionsList from homepage.dart but still, my segments are not updating runtime according to changed selectionsList
Segments.dart (Custom SegmentWidget which creates Cupertino tabs)
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class SegmentsWidget extends StatefulWidget {
#override
_SegmentsWidgetState createState() => _SegmentsWidgetState();
final List selectionsList;
final ValueChanged<int> onSelectTab;
final VoidCallback onTap;
final int selectedValue;
SegmentsWidget(
{this.selectionsList, this.onSelectTab, this.onTap, this.selectedValue});
}
class _SegmentsWidgetState extends State<SegmentsWidget> {
Map<int, Widget> tabWidget = Map<int, Widget>();
int selectedTab = 0;
#override
void initState() {
super.initState();
print("INit State ${widget.selectionsList}");
setState(() {
widget.selectionsList.asMap().forEach((index, value) {
tabWidget.addAll({
index: Container(
height: 40,
child: Center(
child: Text(
widget.selectionsList[index],
style: TextStyle(fontFamily: 'Exo2', fontSize: 12.0),
),
))
});
});
});
}
#override
void didUpdateWidget(SegmentsWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("Did update");
}
#override
Widget build(BuildContext context) {
return Container(
child: Row(
children: <Widget>[
Expanded(
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: CupertinoSegmentedControl<int>(
padding: EdgeInsets.symmetric(vertical: 8),
children: tabWidget,
onValueChanged: (int index) {
setState(() {
selectedTab = index;
});
widget.onSelectTab(index);
},
groupValue: widget.selectedValue ?? selectedTab,
),
),
)
],
),
);
}
}
HomePage.dart
From Home Page, I am updating selection array but still my segments are not updating according to selectionList.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<String> selection;
int selectedTab = -1;
#override
void initState() {
super.initState();
selection = ["A", "B"];
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Dynamic Segments")),
body: Container(
child: Column(
children: <Widget>[
SegmentsWidget(
selectionsList: selection,
onSelectTab: (selectTab) {
setState(() {
selectedTab = selectTab;
});
},
),
RaisedButton(child: Text("AB"),onPressed: (){
setState(() {
selection = ["A", "B"];
});
}),
RaisedButton(child: Text("ABC"),onPressed: (){
setState(() {
selection = ["A", "B", "C"];
});
}),
RaisedButton(child: Text("ABCD"),onPressed: (){
setState(() {
selection = ["A", "B", "C","D"];
});
})
],
),
),
);
}
}
I assume that initState of Segment Widget called once only. I even tried in didUpdateWidget but still not getting updated tabs.
Issue: How to update tabWidgets which is mentioned in my custom widget from another stateful widget?

I change some parts of code.
Instead of calling your code (that need to be called again on setState) in initState() function, call your code inside the widget with your own method.
see getTabChilds() function below of code.
class _SegmentsWidgetState extends State<SegmentsWidget> {
Map<int, Widget> tabWidget = Map<int, Widget>();
int selectedTab = 0;
#override
void initState() {
super.initState();
print("INit State ${widget.selectionsList}");
}
#override
void didUpdateWidget(SegmentsWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("Did update");
}
#override
Widget build(BuildContext context) {
return Container(
child: Row(
children: <Widget>[
Expanded(
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: CupertinoSegmentedControl<int>(
padding: EdgeInsets.symmetric(vertical: 8),
children: getTabChilds(),
onValueChanged: (int index) {
setState(() {
selectedTab = index;
});
widget.onSelectTab(index);
},
groupValue: widget.selectedValue ?? selectedTab,
),
),
)
],
),
);
}
Map<int, Widget> getTabChilds() {
tabWidget = Map<int, Widget>();
widget.selectionsList.asMap().forEach((index, value) {
tabWidget.addAll({
index: Container(
height: 40,
child: Center(
child: Text(
widget.selectionsList[index],
style: TextStyle(fontFamily: 'Exo2', fontSize: 12.0),
),
))
});
});
return tabWidget;
}
}
It's tested and works fine.

Related

Flutter Bad State Stream has been listened to

Good day
Please can anyone help me with this error, the code works properly but whenever I do hot restart it shows me 'Bad state: Stream has already been listened to.'.
This is the on_boarding_view.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mvvm_design_pattern/domain/model.dart';
import 'package:mvvm_design_pattern/presentation/on_boarding/on_boarding_view_model.dart';
import 'package:mvvm_design_pattern/presentation/resources/assets_manager.dart';
import 'package:mvvm_design_pattern/presentation/resources/color_manager.dart';
import 'package:mvvm_design_pattern/presentation/resources/routes_manager.dart';
import 'package:mvvm_design_pattern/presentation/resources/string_manager.dart';
import 'package:mvvm_design_pattern/presentation/resources/values_manager.dart';
class OnBoardingView extends StatefulWidget {
const OnBoardingView({Key? key}) : super(key: key);
#override
_OnBoardingViewState createState() => _OnBoardingViewState();
}
class _OnBoardingViewState extends State<OnBoardingView> {
final PageController _pageController = PageController(initialPage: 0);
final OnBoardingViewModel _viewModel = OnBoardingViewModel();
_bind() {
_viewModel.start();
}
#override
void initState() {
_bind();
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<SliderViewObject>(
stream: _viewModel.outputSliderViewObject,
builder: (context, snapShot) {
return _getContentWidget(snapShot.data);
});
}
Widget _getContentWidget(SliderViewObject? sliderViewObject) {
if (sliderViewObject == null) {
return Container();
} else {
return Scaffold(
backgroundColor: ColorManager.white,
appBar: AppBar(
backgroundColor: ColorManager.white,
elevation: AppSize.s0,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.white,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
),
body: PageView.builder(
controller: _pageController,
itemCount: sliderViewObject.numOfSlides,
onPageChanged: (index) {
_viewModel.onPageChanged(index);
},
itemBuilder: (context, index) {
return OnBoardingPage(sliderViewObject.sliderObject);
}),
bottomSheet: Container(
color: ColorManager.white,
height: AppSize.s100,
child: Column(
children: [
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
Navigator.pushReplacementNamed(
context, Routes.loginRoute);
},
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.subtitle2,
textAlign: TextAlign.end,
),
)),
// add layout for indicator and arrows
_getBottomSheetWidget(sliderViewObject)
],
),
),
);
}
}
Widget _getBottomSheetWidget(SliderViewObject sliderViewObject) {
return Container(
color: ColorManager.primary,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// left arrow
Padding(
padding: const EdgeInsets.all(AppPadding.p14),
child: GestureDetector(
child: SizedBox(
height: AppSize.s20,
width: AppSize.s20,
child: SvgPicture.asset(ImageAssets.leftArrowIc),
),
onTap: () {
// go to previous slide
_pageController.animateToPage(_viewModel.goPrevious(),
duration: const Duration(milliseconds: DurationConstants.d300),
curve: Curves.bounceInOut);
},
),
),
// circles indicator
Row(
children: [
for (int i = 0; i < sliderViewObject.numOfSlides; i++)
Padding(
padding: const EdgeInsets.all(AppPadding.p8),
child: _getProperCircle(i, sliderViewObject.currentIndex),
)
],
),
// right arrow
Padding(
padding: const EdgeInsets.all(AppPadding.p14),
child: GestureDetector(
child: SizedBox(
height: AppSize.s20,
width: AppSize.s20,
child: SvgPicture.asset(ImageAssets.rightArrowIc),
),
onTap: () {
// go to next slide
_pageController.animateToPage(_viewModel.goNext(),
duration: const Duration(milliseconds: DurationConstants.d300),
curve: Curves.bounceInOut);
},
),
)
],
),
);
}
Widget _getProperCircle(int index, int currentIndex) {
if (index == currentIndex) {
return SvgPicture.asset(ImageAssets.hollowCircleIc); // selected slider
} else {
return SvgPicture.asset(ImageAssets.solidCircleIc); // unselected slider
}
}
#override
void dispose() {
_viewModel.dispose();
super.dispose();
}
}
class OnBoardingPage extends StatelessWidget {
final SliderObject _sliderObject;
const OnBoardingPage(this._sliderObject, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: AppSize.s40),
Padding(
padding: const EdgeInsets.all(AppPadding.p8),
child: Text(
_sliderObject.title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline1,
),
),
Padding(
padding: const EdgeInsets.all(AppPadding.p8),
child: Text(
_sliderObject.subTitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
),
),
const SizedBox(
height: AppSize.s60,
),
SvgPicture.asset(_sliderObject.image)
// image widget
],
);
}
}
This is the on_boarding_view_model.dart
import 'dart:async';
import 'package:mvvm_design_pattern/domain/model.dart';
import 'package:mvvm_design_pattern/presentation/base/base_view_model.dart';
import 'package:mvvm_design_pattern/presentation/resources/assets_manager.dart';
import 'package:mvvm_design_pattern/presentation/resources/string_manager.dart';
class OnBoardingViewModel extends BaseViewModel
with OnBoardingViewModelInputs, OnBoardingViewModelOutputs {
final StreamController _streamController =
StreamController<SliderViewObject>();
late final List<SliderObject> _list;
int _currentIndex = 0;
// inputs
#override
void dispose() {
_streamController.close();
}
#override
void start() {
_list = _getSliderData();
// send data to view
_postDataToView();
}
#override
int goNext() {
int nextIndex = _currentIndex++;
if (nextIndex >= _list.length) {
_currentIndex = 0;
}
//_postDataToView();
return _currentIndex;
}
#override
int goPrevious() {
int previousIndex = _currentIndex--;
if (previousIndex == -1) {
_currentIndex = _list.length - 1;
}
//_postDataToView();
return _currentIndex;
}
#override
void onPageChanged(int index) {
_currentIndex = index;
_postDataToView();
}
#override
Sink get inputSliderViewObject => _streamController.sink;
// outputs
#override
Stream<SliderViewObject> get outputSliderViewObject =>
_streamController.stream.map((sliderViewObject) => sliderViewObject);
// private functions
List<SliderObject> _getSliderData() => [
SliderObject(AppStrings.onBoardingTitle1,
AppStrings.onBoardingSubTitle1, ImageAssets.onBoardingLogo1),
SliderObject(AppStrings.onBoardingTitle2,
AppStrings.onBoardingSubTitle2, ImageAssets.onBoardingLogo2),
SliderObject(AppStrings.onBoardingTitle3,
AppStrings.onBoardingSubTitle3, ImageAssets.onBoardingLogo3),
SliderObject(AppStrings.onBoardingTitle4,
AppStrings.onBoardingSubTitle4, ImageAssets.onBoardingLogo4),
];
_postDataToView(){
inputSliderViewObject.add(SliderViewObject(_list[_currentIndex], _list.length, _currentIndex));
}
}
// inputs mean the orders that our view model will receive from our view
abstract class OnBoardingViewModelInputs {
void goNext(); // when user clicks on right arrow or swipe to left
void goPrevious(); // when user clicks on left arrow or swipe right
void onPageChanged(int index);
Sink
get inputSliderViewObject; // this is the way to add data to the stream .. stream inputs
}
// outputs mean data or results that will be sent from our view model to our view
abstract class OnBoardingViewModelOutputs {
Stream<SliderViewObject> get outputSliderViewObject;
}
class SliderViewObject {
// things that the view needs to know
SliderObject sliderObject;
int numOfSlides;
int currentIndex;
SliderViewObject(this.sliderObject, this.numOfSlides, this.currentIndex);
}
Can someone please explain to me why the code is doing like that, and show me if it is possible to fix it.
final StreamController _streamController =
StreamController<SliderViewObject>.broadcast();
You need to make your stream a broadcast stream so that you can continue listening to it.
You current stream by default will be listened to only once.
To learn more check this video here

Flutter PageView makes buggy preview of the content while swiping using SreamBuilder in MVVM

I'm doing a simple onboarding screen with PageView and faced a problem with repetitive and inconsistent preview during swiping and also laggy animation during swiping in PageView..
I have no clue why as i just trying Flutter and dart newly..
here's 3 screenshots, 2 for normal pages and one screenshot between them while scrolling.
as shown the scroll between them is duplicated with the first content while scrolling ..
here's the code for the onBoarding.dart and the onboardingViewModel.dart
class OnBoardingView extends StatefulWidget {
const OnBoardingView({Key? key}) : super(key: key);
#override
State<OnBoardingView> createState() => _OnBoardingViewState();
}
class _OnBoardingViewState extends State<OnBoardingView> {
final PageController _pageController = PageController(initialPage: 0,);
final OnBoardingViewModel _viewModel = OnBoardingViewModel();
_bind() {
_viewModel.start();
}
#override
void initState() {
super.initState();
_bind();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<SliderViewObject>(
stream: _viewModel.outputSliderViewObject,
builder: (context, snapShot) {
return _getContentWidget(snapShot.data);
},
);
}
_goNext() {
Navigator.pushReplacementNamed(context, Routes.mainRoute);
}
Widget _getContentWidget(SliderViewObject? sliderViewObject) {
if (sliderViewObject == null) {
return const Center(
child: CircularProgressIndicator(),
);
} else {
return Scaffold(
backgroundColor: ColorManager.white,
appBar: AppBar(
backgroundColor: ColorManager.white,
elevation: AppSize.s0,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.white,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark),
),
body: PageView.builder(
controller: _pageController,
itemCount: sliderViewObject.numOfSlides,
onPageChanged: (index) {
_viewModel.onPageChanged(index);
},
itemBuilder: (context, index) {
return OnBoardingPage(sliderViewObject.sliderObject);
},
),
bottomSheet: Container(
color: ColorManager.white,
height: AppSize.s100,
child: Column(
children: [
Spacer(),
Align(
alignment: Alignment.centerRight,
child: TextButton(
style: TextButton.styleFrom(primary: ColorManager.primary),
onPressed: () {
_goNext();
},
child: Text(
AppStrings.skip,
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.subtitle2,
)),
),
Align(
alignment: Alignment.center,
child: _getBottomSheetWidget(sliderViewObject),
),
],
),
),
);
}
}
Widget _getBottomSheetWidget(SliderViewObject sliderViewObject) {
return Container(
color: ColorManager.primary,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//left Arrow
Padding(
padding: EdgeInsets.all(AppPadding.p8),
child: GestureDetector(
child: SizedBox(
height: AppSize.s28,
width: AppSize.s28,
child: SvgPicture.asset(ImageAssets.leftArrow),
),
onTap: () {
// go to prev slide.
_pageController.animateToPage(_viewModel.goPrevious(),
duration:
const Duration(milliseconds: DurationConstant.d300),
curve: Curves.bounceInOut);
},
),
),
// 4 circles
Row(
children: [
for (int i = 0; i < sliderViewObject.numOfSlides; i++)
Padding(
padding: const EdgeInsets.all(AppPadding.p8),
child:
_getProperCircle(i, sliderViewObject.currentSlideIndex),
),
],
),
//right Arrow
Padding(
padding: EdgeInsets.all(AppPadding.p8),
child: GestureDetector(
child: SizedBox(
height: AppSize.s28,
width: AppSize.s28,
child: SvgPicture.asset(ImageAssets.rightArrow),
),
onTap: () {
// go to next slide.
_pageController.animateToPage(_viewModel.goNext(),
duration:
const Duration(milliseconds: DurationConstant.d300),
curve: Curves.bounceInOut);
},
),
),
],
),
);
}
Widget _getProperCircle(int index, int currentIndex) {
if (index == currentIndex) {
return SvgPicture.asset(ImageAssets.hollowCircle); //selected
} else {
return SvgPicture.asset(ImageAssets.solidCircle);
}
}
#override
void dispose() {
_viewModel.dispose();
_pageController.dispose();
super.dispose();
}
}
class OnBoardingPage extends StatelessWidget {
SliderObject _sliderObject;
OnBoardingPage(this._sliderObject, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(
height: AppSize.s0,
),
Padding(
padding: const EdgeInsets.all(AppPadding.p8),
child: Text(
_sliderObject.title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headline1,
),
),
Padding(
padding: const EdgeInsets.all(AppPadding.p8),
child: Text(
_sliderObject.subTitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
),
),
const SizedBox(
height: AppSize.s18,
),
SvgPicture.asset(_sliderObject.image),
],
);
}
}
and here's the OnBoardingViewModel.dart to pass data using Stream and Sink
class OnBoardingViewModel extends BaseViewModel
with OnBoardingViewModelInputs, OnBoardingViewModelOutputs {
// stream controllers
final StreamController _streamController = StreamController<SliderViewObject>();
late final List<SliderObject> _list;
int _currentIndex = 0;
//inputs
#override
void dispose() {
_streamController.close();
}
#override
void start() {
_list = _getSliderData(); //populate list
//send data list to view
_postDataToView();
}
#override
int goNext() {
int nextIndex = _currentIndex++;
print('TST user clicked prev and count = $_currentIndex ');
if (nextIndex >= _list.length - 1) {
_currentIndex = 0;
}
return _currentIndex;
}
#override
int goPrevious() {
int previousIndex = _currentIndex--;
print('TST user clicked prev and count = $_currentIndex ');
if (previousIndex <= 0) {
_currentIndex = _list.length - 1;
}
return _currentIndex;
}
#override
void onPageChanged(int index) {
_currentIndex = index;
_postDataToView();
}
#override
void skip() {
// TODO: implement skip
}
#override
Sink get inputSliderViewObject => _streamController.sink;
//outputs
#override
Stream<SliderViewObject> get outputSliderViewObject =>
_streamController.stream.map((slideViewObject) => slideViewObject);
List<SliderObject> _getSliderData() => [
SliderObject(AppStrings.onBoardingTitle1,
AppStrings.onBoardingSubTitle1, ImageAssets.onBoardingLogo1),
SliderObject(AppStrings.onBoardingTitle2,
AppStrings.onBoardingSubTitle2, ImageAssets.onBoardingLogo2),
SliderObject(AppStrings.onBoardingTitle3,
AppStrings.onBoardingSubTitle3, ImageAssets.onBoardingLogo3),
SliderObject(AppStrings.onBoardingTitle4,
AppStrings.onBoardingSubTitle4, ImageAssets.onBoardingLogo4)
];
void _postDataToView() {
inputSliderViewObject.add(
SliderViewObject(_list[_currentIndex], _list.length, _currentIndex));
}
}
class SliderViewObject {
SliderObject sliderObject;
int numOfSlides;
int currentSlideIndex;
SliderViewObject(this.sliderObject, this.numOfSlides, this.currentSlideIndex);
}
//orders received by the view
abstract class OnBoardingViewModelInputs {
void goNext();
void goPrevious();
void onPageChanged(int index);
void skip();
Sink get inputSliderViewObject; // stream input from the view
}
//orders sent to the view
abstract class OnBoardingViewModelOutputs {
Stream<SliderViewObject>
get outputSliderViewObject; //send the stream to the view.
}
I think the problem is in passing the current index. We are passing the current index for the current slider object and also for the next one. So until the index changes, the images remain identical for the one and upcoming, and later when the current index changes both images change at the same time and are still the same.
Considering you are referring to the Mina Farid course, here is how I solved it by passing a list of all objects:
// In ViewModel
void _postDataToView() {
inputSliderViewObject.add(SliderViewObject(_sliderObjects[_currentIndex],
_sliderObjects, _sliderObjects.length, _currentIndex));
}
class SliderViewObject {
SliderObject sliderObject;
List<SliderObject> list;
int numberOfSlides;
int currentIndex;
SliderViewObject(
this.sliderObject, this.list, this.numberOfSlides, this.currentIndex);
}
/// Now you do as below in the view
PageView.builder(
controller: _pageController,
itemCount: sliderViewObject.numberOfSlides,
onPageChanged: (index) {
_viewModel.onPageChanged(index);
},
itemBuilder: (context, index) {
return OnboardingItem(sliderViewObject.list[index]);
},
),
Here you are passing current index for current image, and the next index for the next image. So this solves the issue..
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:trading/domain/slider_view_object.dart';
import 'package:trading/presentation/res/app_color.dart';
import 'package:trading/presentation/res/app_dimen.dart';
import 'package:trading/presentation/res/app_media.dart';
import 'package:trading/presentation/res/app_routes.dart';
import 'package:trading/presentation/res/app_strings.dart';
import 'onboarding_viewmodel.dart';
class OnBoardingView extends StatefulWidget {
const OnBoardingView({Key? key}) : super(key: key);
#override
_OnBoardingViewState createState() => _OnBoardingViewState();
}
class _OnBoardingViewState extends State<OnBoardingView> {
final PageController _pageController = PageController(initialPage: 0);
final OnBoardingViewModel _viewModel = OnBoardingViewModel();
_bind() {
_viewModel.start();
}
#override
void initState() {
_bind();
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<SliderViewObject>(
stream: _viewModel.outputSliderViewObject,
builder: (context, snapShot) {
return _getContentWidget(snapShot.data);
});
}
Widget _getContentWidget(SliderViewObject? sliderViewObject) {
if (sliderViewObject == null) {
return Container();
} else {
return Scaffold(
backgroundColor: AppColor.white,
appBar: AppBar(
backgroundColor: AppColor.white,
elevation: AppSize.s0,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: AppColor.white,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
),
body: PageView.builder(
controller: _pageController,
itemCount: sliderViewObject.numOfSlides,
onPageChanged: (index) {
_viewModel.onPageChanged(index);
},
itemBuilder: (context, index) {
return OnBoardingPage(sliderViewObject.sliderObject);
}),
bottomSheet: Container(
color: AppColor.white,
height: AppSize.s100,
child: Column(
children: [
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
Navigator.pushReplacementNamed(
context, Routes.loginRoute);
},
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.subtitle2,
textAlign: TextAlign.end,
),
)),
// add layout for indicator and arrows
_getBottomSheetWidget(sliderViewObject)
],
),
),
);
}
}
Widget _getBottomSheetWidget(SliderViewObject sliderViewObject) {
return Container(
color: AppColor.primary,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// left arrow
Padding(
padding: const EdgeInsets.all(AppPadding.p14),
child: GestureDetector(
child: SizedBox(
height: AppSize.s20,
width: AppSize.s20,
child: SvgPicture.asset(AppMedia.leftArrowIc),
),
onTap: () {
// go to previous slide
_pageController.animateToPage(_viewModel.goPrevious(),
duration:const Duration(milliseconds: DurationConstant.d300),
curve: Curves.bounceInOut);
},
),
),
// circles indicator
Row(
children: [
for (int i = 0; i < sliderViewObject.numOfSlides; i++)
Padding(
padding:const EdgeInsets.all(AppPadding.p8),
child: _getProperCircle(i, sliderViewObject.currentIndex),
)
],
),
// right arrow
Padding(
padding: EdgeInsets.all(AppPadding.p14),
child: GestureDetector(
child: SizedBox(
height: AppSize.s20,
width: AppSize.s20,
child: SvgPicture.asset(AppMedia.rightarrowIc),
),
onTap: () {
// go to next slide
_pageController.animateToPage(_viewModel.goNext(),
duration:const Duration(milliseconds: DurationConstant.d300),
curve: Curves.bounceInOut);
},
),
)
],
),
);
}
Widget _getProperCircle(int index, int _currentIndex) {
if (index == _currentIndex) {
return SvgPicture.asset(AppMedia.hollowCircleIc); // selected slider
} else {
return SvgPicture.asset(AppMedia.solidCircleIc); // unselected slider
}
}
#override
void dispose() {
_viewModel.dispose();
super.dispose();
}
}
class OnBoardingPage extends StatelessWidget {
final SliderObject _sliderObject;
const OnBoardingPage(this._sliderObject, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(height: AppSize.s40),
Padding(
padding: const EdgeInsets.all(AppPadding.p8),
child: Text(
_sliderObject.title,
textAlign: TextAlign.center,
style: Theme
.of(context)
.textTheme
.headline1,
),
),
Padding(
padding: const EdgeInsets.all(AppPadding.p8),
child: Text(
_sliderObject.subTitle,
textAlign: TextAlign.center,
style: Theme
.of(context)
.textTheme
.subtitle1,
),
),
const SizedBox(
height: AppSize.s60,
),
SvgPicture.asset(_sliderObject.image)
// image widget
],
);
}
}
ViewModel
import 'dart:async';
import 'package:trading/domain/model/model.dart';
import 'package:trading/presentation/base/baseviewmodel.dart';
import 'package:trading/presentation/resources/assets_manager.dart';
import 'package:trading/presentation/resources/strings_manager.dart';
import 'package:easy_localization/easy_localization.dart';
class OnBoardingViewModel extends BaseViewModel
with OnBoardingViewModelInputs, OnBoardingViewModelOutputs {
// stream controllers
final StreamController _streamController =
StreamController<SliderViewObject>();
late final List<SliderObject> _list;
int _currentIndex = 0;
// inputs
#override
void dispose() {
_streamController.close();
}
#override
void start() {
_list = _getSliderData();
// send this slider data to our view
_postDataToView();
}
#override
int goNext() {
int nextIndex = _currentIndex++; // +1
if (nextIndex >= _list.length) {
_currentIndex = 0; // infinite loop to go to first item inside the slider
}
return _currentIndex;
}
#override
int goPrevious() {
int previousIndex = _currentIndex--; // -1
if (previousIndex == -1) {
_currentIndex =
_list.length - 1; // infinite loop to go to the length of slider list
}
return _currentIndex;
}
#override
void onPageChanged(int index) {
_currentIndex = index;
_postDataToView();
}
#override
Sink get inputSliderViewObject => _streamController.sink;
// outputs
#override
Stream<SliderViewObject> get outputSliderViewObject =>
_streamController.stream.map((slideViewObject) => slideViewObject);
// private functions
List<SliderObject> _getSliderData() => [
SliderObject(
AppStrings.onBoardingTitle1.tr(),
AppStrings.onBoardingSubTitle1.tr(),
ImageAssets.onboardingLogo1),
SliderObject(
AppStrings.onBoardingTitle2.tr(),
AppStrings.onBoardingSubTitle2.tr(),
ImageAssets.onboardingLogo2),
SliderObject(
AppStrings.onBoardingTitle3.tr(),
AppStrings.onBoardingSubTitle3.tr(),
ImageAssets.onboardingLogo3),
SliderObject(
AppStrings.onBoardingTitle4.tr(),
AppStrings.onBoardingSubTitle4.tr(),
ImageAssets.onboardingLogo4)
];
_postDataToView() {
inputSliderViewObject.add(
SliderViewObject(_list[_currentIndex], _list.length, _currentIndex));
}
}
// inputs mean the orders that our view model will recieve from our view
abstract class OnBoardingViewModelInputs {
void goNext(); // when user clicks on right arrow or swipe left.
void goPrevious(); // when user clicks on left arrow or swipe right.
void onPageChanged(int index);
Sink
get inputSliderViewObject; // this is the way to add data to the stream .. stream input
}
// outputs mean data or results that will be sent from our view model to our view
abstract class OnBoardingViewModelOutputs {
Stream<SliderViewObject> get outputSliderViewObject;
}
class SliderViewObject {
SliderObject sliderObject;
int numOfSlides;
int currentIndex;
SliderViewObject(this.sliderObject, this.numOfSlides, this.currentIndex);
}

Pagination scroll top in flutter

I'm currently create chat in flutter and get the last messages , I want to handle when scrolling to top to load more messages how can I create that ?
If you want to implement swipe to refresh kind of behaviour, you can use RefreshIndicator. See the example and usage in this YouTube video.
All you have to do is wrap your scrollable widget (it can be ListView or SingleChildScrollView) in a RefreshIndicator and provide onRefresh method:
class PullToRefresh extends StatelessWidget {
const PullToRefresh({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _refreshData,
child: ListView.builder( // or SingleChildScrollView
...
),
);
}
Future<void> _refreshData() async {
// load more items
}
}
ListView reverse: true displays the List from the bottom to the top.
and this is how to implement pagination
class HomeState extends State<Home> {
ScrollController? controller;
final _all = <WordPair>[];
final _saved = Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
bool isLoading = false;
#override
void initState() {
super.initState();
_all.addAll(generateWordPairs().take(20));
controller = ScrollController()..addListener(_scrollListener);
}
#override
void dispose() {
super.dispose();
controller?.dispose();
}
void _scrollListener() {
if (controller!.position.pixels == controller!.position.maxScrollExtent) {
startLoader();
}
}
void startLoader() {
setState(() {
isLoading = !isLoading;
fetchData();
});
}
fetchData() async {
var _duration = const Duration(seconds: 2);
return Timer(_duration, onResponse);
}
void onResponse() {
setState(() {
isLoading = !isLoading;
_all.addAll(generateWordPairs().take(20));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
title: const Text(
"List load more example",
style: TextStyle(color: Colors.white),
),
),
body: Stack(
children: <Widget>[
_buildSuggestions(),
_loader(),
],
),
);
}
Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return Column(
children: <Widget>[
ListTile(
title: Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: Icon(
alreadySaved ? Icons.check : Icons.check,
color: alreadySaved ? Colors.deepOrange : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
),
const Divider(),
],
);
}
Widget _buildSuggestions() {
return ListView.builder(
reverse: true,
padding: const EdgeInsets.all(16.0),
controller: controller,
itemCount: _all.length,
itemBuilder: (context, i) {
return _buildRow(_all[i]);
});
}
Widget _loader() {
return isLoading
? const Align(
child: SizedBox(
width: 70.0,
height: 70.0,
child: Padding(
padding: EdgeInsets.all(5.0),
child: Center(child: CircularProgressIndicator())),
),
alignment: FractionalOffset.topCenter,
)
: const SizedBox(
width: 0.0,
height: 0.0,
);
}
}
You can get full code from Github HERE

How to make a text become clickable after 30 secs in flutter?

I am making a login app where by i have created an OTP validation page. In this page i want to make a resend option which is clickable only after 30 seconds of page loading and once clicked becomes unclickable for ever.
I am new to flutter so I am sorry if this seems trivial.
You can follow this code.
class TestButton extends StatefulWidget {
#override
_TestButtonState createState() => _TestButtonState();
}
class _TestButtonState extends State<TestButton> {
bool firstStateEnabled = false;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
Timer(Duration(milliseconds: 30000), () {
setState(() {
firstStateEnabled = true;
});
});
return Scaffold(
body: Container(
child: firstStateEnabled
? Center(
child: Container(
width: 200,
height: 55,
child: RaisedButton(
onPressed: () {},
child: Text("Resend OTP"),
),
),
)
: Center(child: Container()),
),
);
}
}
Or if you need only one time the button than you can follow below codes.
Install timer_count_down.
Then, below code.
class TestButton extends StatefulWidget {
#override
_TestButtonState createState() => _TestButtonState();
}
class _TestButtonState extends State<TestButton> {
bool firstStateEnabled = false;
final CountdownController controller = CountdownController();
final int seconds = 30;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(
child: firstStateEnabled
? Center(
child: Container(
width: (200),
height: 55,
child: RaisedButton(
onPressed: () {
setState(() {
firstStateEnabled = false;
});
},
child: Text("Resend OTP"),
),
),
)
: Center(child: Container()),
),
Countdown(
controller: controller,
seconds: seconds,
build: (context, double time) {
return Container();
},
interval: Duration(milliseconds: 100),
onFinished: () {
setState(() {
firstStateEnabled = true;
;
});
},
)
],
),
);
}
}

Rebuild widget when contents of list change

I have objects in a list that users can remove. If the list is empty, I want to display some text but I tried to do this with a ListView.builder() and it displayed the text many times and I want it to be displayed just once. Is there some sort of property that I can listen to in the list?
import 'package:flutter/material.dart';
main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List items =new List();
bool _isLoading =false;
#override
void initState() {
// TODO: implement initState
super.initState();
items = getDummyList();
setState(() {
_isLoading=false;
});
}
#override
Widget build(BuildContext context) {
return
MaterialApp(
home: Scaffold(
body: _isLoading
?Center(
child: CircularProgressIndicator())
:
items.length==0?Center(child: Text('No items in the list'),):
Container(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return Dismissible(
key: Key(items[index]),
onDismissed: (direction) {
setState(() {
items.removeAt(index);
});
},
child: Container(
height: 50.0,
decoration: BoxDecoration(border: Border.all(width: 1.0)),
padding: EdgeInsets.all(5.0),
child: Row(
children: <Widget>[
Text(
items[index],
style: TextStyle(
color: Colors.black,
fontSize: 20.0,
),
)
],
),
),
);
},
)),
),
);
}
List getDummyList(){
setState(() {
_isLoading=true;
});
List list = List.generate(5, (i) {
return "Item ${i +1 }";
});
return list;
}
}
Check out this example , i have made a swipe dismiss example for listview,where if you remove the items it will display the text.
let me know if it works.
Thanks.