Related
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
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);
}
Is there a way in Flutter to show a dot indicator on Gridview ? or is there a widget like gridview that I can combine it with CarouselSlider ?
Something like this:
Thank you in advance for your answers!
based on The Design picture you provided, Instead of a grid view, you actually need a Carousel, or a page, to implement the pagination.
carousel_builder package or PageView widget is the solution for implementing the pagination.
to achieve the dots indicator, you can use dots_indicator mentioned in the comments.
then you should use your pageview or carousel controller, and on changing the index, update your indicator.
I'll provide the code of an image_slider I implemented, you can edit and achieve what you want:
class ImageSlider extends StatefulWidget {
final List<String> urls;
final bool openImagePopUp;
final PageController sliderController;
const ImageSlider(
{Key key, this.urls, this.openImagePopUp = false, this.sliderController})
: super(key: key);
#override
_ImageSliderState createState() => _ImageSliderState();
}
class _ImageSliderState extends State<ImageSlider> {
int currentSlideIndex;
PageController sliderController;
void setCurrentImageIndex(int newIndex) {
setState(() {
currentSlideIndex = newIndex;
});
}
#override
void initState() {
currentSlideIndex = 0;
sliderController =
widget.sliderController ?? PageController(initialPage: 0);
super.initState();
}
#override
void dispose() {
sliderController.dispose();
super.dispose();
}
void nextPage() {
sliderController.nextPage(
duration: Duration(milliseconds: 250), curve: Curves.easeOut);
}
void previousPage() {
sliderController.previousPage(
duration: Duration(milliseconds: 250), curve: Curves.easeOut);
}
#override
Widget build(BuildContext context) {
return Stack(children: [
Directionality(
textDirection: TextDirection.ltr,
child: PageView.builder(
controller: sliderController,
onPageChanged: (newIndex) {
setCurrentImageIndex(newIndex);
},
itemCount: widget.urls.length,
itemBuilder: (context, index) {
return InkWell(
onTap: widget.openImagePopUp
? () {
context.read<NavigationService>().showPopUp(
ProductDetailsImagePopUp(urls: widget.urls), context);
}
: null,
child: Image.network(
createImageUrl(widget.urls[index]),
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return LoadImageErrorWidget();
},
),
);
},
),
),
if (widget.urls.length > 1) ...[
Positioned(
left: 0,
bottom: 10,
child: CircleIndicators(
index: currentSlideIndex,
length: widget.urls.length,
),
),
Center(
child: Directionality(
textDirection: TextDirection.rtl,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (currentSlideIndex < widget.urls.length - 1)
IconButton(
icon: Icon(Icons.chevron_left), onPressed: nextPage),
Expanded(child: Container()),
if (currentSlideIndex > 0)
IconButton(
icon: Icon(Icons.chevron_right), onPressed: previousPage),
],
),
),
)
]
]);
}
}
i have a Widget that has an AppBar with a progress bar, and a PageView with 4 pages, when moving between pages i am increasing / decreasing the progress bar.
I'm trying to do all the logic in my ViewModel.
This is my ViewModel (omitted non relevant stuff):
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _sexStreamController = StreamController<int>.broadcast();
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Sink get progress => _progressBarController.sink;
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
}
And here is my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose() {
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
}
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
List<Widget> pagesList = [
SexPage(
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
},
),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(_pageController),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
);
}
}
class AppBarWidget extends StatelessWidget {
final PageController pageController;
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
AppBarWidget(
this.pageController, {
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: StreamBuilder<double>(
stream: _viewModel.outputProgress,
builder: (context, snapshot) {
return Progresso(
progress: snapshot.data ?? 0,
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
);
}),
),
),
),
StreamBuilder<int>(
stream: _viewModel.outputCurrentIndex,
builder: (context, snapshot) {
return Expanded(
flex: 1,
child: (snapshot.data ?? 0) > 0
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
);
}),
],
);
}
}
When i'm calling _viewModel.previousPage() & _viewModel.previousPage()` from the AppBarWidget, the progress bar view is updated, and there is a scroll animation to the next page.
But for some reason if the onConfirm callback:
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
}
is called from within SexPage, the scroll animation is working, but the progress bar view and the isBackEnabled is not updating.
I have checked and a new value is being added to the _progressBarController sink, but for some reason the StreamBuilder does not receive it? same for the isBackEnabled stream..
What am i doing wrong?
And another question i have is where should I listen to the outputCurrentIndex stream, and call _pageController.animateToPage()?
Apparently i had an issue with my Dependency Injection.
I'm using get_it and i used registerFactory, instead of registerLazySingleton.
Which probably made me have 2 separate ViewModels in each widget.
i'm trying to implement a View with a PageView widget, that is being controlled by the ViewModel.
In my view i have buttons that trigger the following method in the ViewModel:
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
currentIndex.add(_index);
}
}
currentIndex is just a sink of my _currentIndexController, and outputCurrentIndex is a stream of the controller:
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
it looks like the value is added to the stream successfully, but i can't get it to trigger changing pages, i've set up this listener in the initState() method of the view:
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
but it is not triggered for some reason.. what am i doing wrong?
here is a full code of my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose(){
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
}
#override
Widget build(BuildContext context) {
List<Widget> pagesList = [
const SexPage(),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return MultiProvider(
providers: [
StreamProvider.value(value: _viewModel.outputProgress, initialData: 0.25),
StreamProvider.value(value: _viewModel.outputCurrentIndex, initialData: 0),
],
child: Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
),
);
}
}
class AppBarWidget extends StatefulWidget {
const AppBarWidget({
Key? key,
}) : super(key: key);
#override
State<AppBarWidget> createState() => _AppBarWidgetState();
}
class _AppBarWidgetState extends State<AppBarWidget> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: Progresso(
progress: Provider.of<double>(context),
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
),
),
),
),
Expanded(
flex: 1,
child: Provider.of<int>(context) > 1
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
),
],
);
}
}
and my ViewModel:
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
void dispose() {
_progressBarController.close();
_currentIndexController.close();
_isBackEnabled.close();
}
#override
void start() {
// TODO: implement start
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Sink get progress => _progressBarController.sink;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
}
abstract class RegisterViewModelInputs {
register();
increaseProgress();
decreaseProgress();
nextPage();
previousPage();
setIsBackEnabled(int index);
Sink get currentIndex;
Sink get isBackEnabled;
Sink get progress;
}
abstract class RegisterViewModelOutputs {
Stream<int> get outputCurrentIndex;
Stream<double> get outputProgress;
Stream<bool> get outputIsBackEnabled;
}
EDIT: I have moved the stream listener to the _AppBarWidgetState build method, and it seems to work now, but i don't fully understand why it hasn't worked before..
is it because the PageController wasn't assigned to a view yet? where is the correct place for the listener? it doesn't makes sense to me for it to be in a child widget.