Related
I have a list and I need to set the container's background when clicking on it. However, what I have now does not work. When clicked, the color of the entire list changes, not the selected one. It seems to me that I need to add an index somewhere. I can't put it in a separate widget, because I'm attached to the list. Tell me how to do it?
setState -
Color? _textColor;
Color? _bgColor;
void initState() {
_bgColor = configColors.orange;
_textColor = Colors.white;
super.initState();
}
List
ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: HomeStore.storage.length,
itemBuilder: (BuildContext ctx, index) {
return Row (
// mainAxisAlignment: MainAxisAlignment.start,
children: <Widget> [
InkWell(
onTap: () {
setState(() {
if (_bgColor ==
configColors
.orange) {
_bgColor =
Colors.white;
_textColor =
configColors
.textStorage;
} else {
_bgColor =
configColors.orange;
_textColor =
Colors.white;
}
}
);
},
child: Container(
width: 71.4,
height: 30.3,
decoration: BoxDecoration(
color: _bgColor,
borderRadius: BorderRadius.circular(10)
),
child: Align(
alignment: Alignment.center,
child: Text(HomeStore.storage[index], style: TextStyle(color: _textColor,),),
)
),
),
SizedBox(
width: 18,
),
],
);
}),
For single item selection, you can use a int variable, this snippet will help you to understand the concept.
int? selectedIndex;
onTap: () {
setState(() {
selectedIndex = index;
});
},
And to select color
color:selectedIndex == index ? Colors.red : Colors.blue
Test snippet
class Sg extends StatefulWidget {
Sg({Key? key}) : super(key: key);
#override
State<Sg> createState() => _SgState();
}
class _SgState extends State<Sg> {
int? selectedIndex;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: 4,
itemBuilder: (BuildContext ctx, index) {
return Row(
// mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
InkWell(
onTap: () {
setState(() {
selectedIndex = index;
});
},
child: Container(
width: 71.4,
height: 30.3,
decoration: BoxDecoration(
color:
selectedIndex == index ? Colors.red : Colors.blue,
borderRadius: BorderRadius.circular(10)),
child: Align(
alignment: Alignment.center,
child: Text(
"HomeStore.storage[index]",
),
)),
),
],
);
}),
);
}
}
sharing one of my code demo
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: MyWidget(),
);
}
}
class MyWidget extends StatefulWidget {
#override
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
late int tappedIndex;
#override
void initState() {
super.initState();
tappedIndex = 0;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ListView.builder(
shrinkWrap: true,
itemCount: 4,
itemBuilder: (context, index) {
return Container(
color: tappedIndex == index ? Colors.blue : Colors.grey,
child: ListTile(
title: Center(
child: Text('${index + 1}'),
),onTap:(){
setState((){
tappedIndex=index;
});
}));
})
]));
}
}
taped index will solve problem
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);
}
First of all, my English is not good, sorry!
Here is my description of the problem:
consists of the following components: (contained in CustomScrollView)
1: SilverToBoxAdapter
2: SilverPersistentHeader
3: SliverFillRemaining
The SilverPersistentHeader only appears when the SilverToBoxAdapter disappears from the screen, and the SilverToBoxAdapter is hidden by default. After the SilverPersistentHeader appears, it will have the effect of pined=true as the page slides.
When SilverPersistentHeader appeared, I found that SilverPersistentHeader would block part of SliverFillRemaining.
here is my code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late ScrollController _mainController;
late PageController _pageController;
late bool isScrollable = true;
bool isVisiable = false;
#override
void initState() {
super.initState();
_mainController = ScrollController();
_pageController = PageController();
_pageController.addListener(() {
debugPrint('offset:${_pageController.page}');
if (_pageController.page == 0.0) {
setState(() {
isScrollable = true;
});
} else {
setState(() {
isScrollable = false;
});
}
});
_mainController.addListener(() {
// debugPrint('offset:${_mainController.offset}');
if (_mainController.offset >= 200) {
setState(() {
isVisiable = true;
});
} else {
setState(() {
isVisiable = false;
});
}
});
}
#override
void dispose() {
super.dispose();
_mainController.dispose();
_pageController.dispose();
}
#override
Widget build(BuildContext context) {
return CustomScrollView(
physics: isScrollable
? const AlwaysScrollableScrollPhysics()
: const NeverScrollableScrollPhysics(),
controller: _mainController,
slivers: <Widget>[
const SliverAppBar(
title: Text('SliverDemo'),
pinned: true,
),
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.amber,
),
),
SliverPersistentHeader(
delegate: MyPersistentHeader(
isVisiable: isVisiable,
child: Container(
color: Colors.green,
)),
pinned: true,
),
SliverFillRemaining(
hasScrollBody: true,
child: PageView(
controller: _pageController,
children: [
Container(
color: Colors.primaries[0],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[1],
child: Column(
children: [
Container(
color: Colors.black,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[2],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[3],
child: Column(
children: [
Container(
color: Colors.black,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[4],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
)
],
),
),
],
);
}
}
class MyPersistentHeader extends SliverPersistentHeaderDelegate {
final Widget child;
final bool isVisiable;
MyPersistentHeader({required this.child, required this.isVisiable});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
#override
double get maxExtent => isVisiable ? 60 : 0;
#override
double get minExtent => isVisiable ? 60 : 0;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
I've searched online for a long time and can't find a solution. I hope you can give me a good suggestion, thanks.
I am trying to reproduce the following example from the earlier Material design specifications (open for animated demo):
Until now I was able to produce the scrolling effect, but the overlap of the content is still missing. I couldn't find out how to do this properly.
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text('Title'),
expandedHeight: 200.0,
primary: true,
pinned: true,
),
SliverFixedExtentList(
itemExtent: 30.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) => Text('Item $i')
),
),
],
),
);
}
}
I managed to get this functionality, using the ScrollController and a couple of tricks:
Here's the code:
ScrollController _scrollController;
static const kHeaderHeight = 235.0;
double get _headerOffset {
if (_scrollController.hasClients) if (_scrollController.offset > kHeaderHeight)
return -1 * (kHeaderHeight + 50.0);
else
return -1 * (_scrollController.offset * 1.5);
return 0.0;
}
#override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(() => setState(() {}));
}
#override
Widget build(BuildContext context) {
super.build(context);
return StackWithAllChildrenReceiveEvents(
alignment: AlignmentDirectional.topCenter,
children: [
Positioned(
top: _headerOffset,
child: Container(
height: kHeaderHeight,
width: MediaQuery.of(context).size.width,
color: Colors.blue,
),
),
Padding(
padding: EdgeInsets.only(left: 20.0, right: 20.0),
child: Feed(controller: _scrollController, headerHeight: kHeaderHeight),
),
],
);
}
To make the Feed() not overlap the blue container, I simply made the first child of it a SizedBox with the required height property.
Note that I am using a modified Stack class. That is in order to let the first Widget in the stack (the blue container) to detect presses, so it will fit my uses; unfortunately at this point the default Stack widget has an issue with that, you can read more about it over https://github.com/flutter/flutter/issues/18450.
The StackWithAllChildrenReceiveEvents code can be found over https://github.com/flutter/flutter/issues/18450#issuecomment-575447316.
I had the same problem and could not solve it with slivers. This example from another stackoverflow question solved my problem.
flutter - App bar scrolling with overlapping content in Flexible space
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Scroll demo',
home: new Scaffold(
appBar: new AppBar(elevation: 0.0),
body: new CustomScroll(),
),
);
}
}
class CustomScroll extends StatefulWidget {
#override
State createState() => new CustomScrollState();
}
class CustomScrollState extends State<CustomScroll> {
ScrollController scrollController;
double offset = 0.0;
static const double kEffectHeight = 100.0;
#override
Widget build(BuildContext context) {
return new Stack(
alignment: AlignmentDirectional.topCenter,
children: <Widget> [
new Container(
color: Colors.blue,
height: (kEffectHeight - offset * 0.5).clamp(0.0, kEffectHeight),
),
new Positioned(
child: new Container(
width: 200.0,
child: new ListView.builder(
itemCount: 100,
itemBuilder: buildListItem,
controller: scrollController,
),
),
),
],
);
}
Widget buildListItem(BuildContext context, int index) {
return new Container(
color: Colors.white,
child: new Text('Item $index')
);
}
void updateOffset() {
setState(() {
offset = scrollController.offset;
});
}
#override
void initState() {
super.initState();
scrollController = new ScrollController();
scrollController.addListener(updateOffset);
}
#override
void dispose() {
super.dispose();
scrollController.removeListener(updateOffset);
}
}
Change the list to a grid and its what you want