TabBar scroll/slide making inaccuracy of displaying - flutter

I uses Container 's color to make indicator-like rather than using TabBar's indicator as I've to implement some animation to the Container.
When TabController index is changing, setState is called in the listener. Tries scroll/slide on the TabBar, the TabBar isn't properly changing the index, as listener doesn't listen to animation for TabBar.
I've tried using tabcontroller.animation.addListener method, but there isn't any workaround for me to control the scroll movement.
Attached video below demonstrates tapping and scroll/slide applied on the TabBar.
TabBar-Scroll/Slide
Code:
class TabTest extends StatefulWidget {
#override
_TabTestState createState() => _TabTestState();
}
class _TabTestState extends State<TabTest> with TickerProviderStateMixin {
late TabController _tabController;
late List<AnimationController> _animationControllers;
#override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this)
..addListener(_listener);
_animationControllers = List.generate(
4,
(i) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
reverseDuration: Duration(milliseconds: 350),
));
}
#override
Widget build(BuildContext context) {
List<IconData> _tabIconData = [
Icons.card_giftcard,
Icons.confirmation_num_outlined,
Icons.emoji_events_outlined,
Icons.wine_bar_outlined,
];
List<String> _tabLabel = [
'Tab1',
'Tab2',
'Tab3',
'Tab4',
];
Widget _tab({
required IconData iconData,
required String label,
required bool isSelectedIndex,
// required double widthAnimation,
// required heightAnimation,
}) {
const _tabTextStyle = TextStyle(
fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
return AnimatedContainer(
duration: Duration(milliseconds: 300),
padding: EdgeInsets.only(bottom: 2.0),
height: 55,
width: double.infinity, //_animContainerWidth - widthAnimation,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelectedIndex ? Colors.black : Colors.transparent,
width: 2.0,
),
),
),
child: Tab(
iconMargin: EdgeInsets.only(bottom: 5.0),
icon: Icon(iconData, color: Colors.black),
child: Text(label, style: _tabTextStyle),
),
);
}
List<Widget> _animationGenerator() {
return List.generate(
4,
(index) => ClipRRect(
child: AnimatedBuilder(
animation: _animationControllers[index],
builder: (ctx, _) {
final value = _animationControllers[index].value;
final angle = math.sin(value * math.pi * 2) * math.pi * 0.04;
return Transform.rotate(
angle: angle,
child: _tab(
iconData: _tabIconData[index],
label: _tabLabel[index],
isSelectedIndex: _tabController.index == index,
));
}),
),
);
}
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(100),
child: AppBar(
iconTheme: Theme.of(context).iconTheme,
title: Text(
'Tab Bar',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size.fromHeight(20),
child: Container(
child: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.transparent,
tabs: _animationGenerator(),
),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.white,
spreadRadius: 5.0,
offset: Offset(0, 3))
],
),
),
),
),
),
body: TabBarView(
controller: _tabController,
children: List.generate(
4,
(index) => FittedBox(
child: Text('Tab $index'),
)),
),
);
}
void _listener() {
if (_tabController.indexIsChanging) {
setState(() {}); // To refresh color for Container bottom Border
_animationControllers[_tabController.previousIndex].reverse();
} else {
_animationControllers[_tabController.index].forward();
}
}
#override
void dispose() {
super.dispose();
_tabController.removeListener(_listener);
}
}

this is a solution with a CustomPaint widget driven by TabController.animation:
class TabTest extends StatefulWidget {
#override
_TabTestState createState() => _TabTestState();
}
class _TabTestState extends State<TabTest> with TickerProviderStateMixin {
late TabController _tabController;
late List<AnimationController> _animationControllers;
#override
void initState() {
super.initState();
// timeDilation = 10;
_tabController = TabController(length: 4, vsync: this)
..addListener(_listener);
_animationControllers = List.generate(4, (i) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
));
}
#override
Widget build(BuildContext context) {
List<IconData> _tabIconData = [
Icons.card_giftcard,
Icons.confirmation_num_outlined,
Icons.emoji_events_outlined,
Icons.wine_bar_outlined,
];
List<String> _tabLabel = [
'Tab1',
'Tab2',
'Tab3',
'Tab4',
];
List<Color> _tabColor = [
Color(0xffaa0000),
Color(0xff00aa00),
Color(0xff0000aa),
Colors.black,
];
Widget _tab({
required IconData iconData,
required String label,
required Color color,
required int index,
required Animation<double>? animation,
}) {
const _tabTextStyle = TextStyle(fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
return CustomPaint(
painter: TabPainter(
animation: animation!,
index: index,
color: color,
),
child: SizedBox(
width: double.infinity,
child: Tab(
iconMargin: EdgeInsets.only(bottom: 5.0),
icon: Icon(iconData, color: Colors.black),
child: Text(label, style: _tabTextStyle),
),
),
);
}
List<Widget> _animationGenerator() {
return List.generate(
4,
(index) => AnimatedBuilder(
animation: _animationControllers[index],
builder: (ctx, _) {
final value = _animationControllers[index].value;
final angle = sin(value * pi * 3) * pi * 0.04;
return Transform.rotate(
angle: angle,
child: _tab(
iconData: _tabIconData[index],
label: _tabLabel[index],
color: _tabColor[index],
index: index,
animation: _tabController.animation,
));
}),
);
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text('Tab Bar',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
bottom: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.transparent,
tabs: _animationGenerator(),
),
),
body: TabBarView(
controller: _tabController,
children: List.generate(4, (index) => FittedBox(
child: Text('Tab $index'),
)),
),
);
}
void _listener() {
if (_tabController.indexIsChanging) {
_animationControllers[_tabController.previousIndex].value = 0;
} else {
_animationControllers[_tabController.index].forward();
}
}
#override
void dispose() {
super.dispose();
_tabController
..removeListener(_listener)
..dispose();
_animationControllers.forEach((ac) => ac.dispose());
}
}
class TabPainter extends CustomPainter {
final Animation<double> animation;
final int index;
final Color color;
final tabPaint = Paint();
TabPainter({
required this.animation,
required this.index,
required this.color,
});
#override
void paint(ui.Canvas canvas, ui.Size size) {
// timeDilation = 10;
if ((animation.value - index).abs() < 1) {
final rect = Offset.zero & size;
canvas.clipRect(rect);
canvas.translate(size.width * (animation.value - index), 0);
final tabRect = Alignment.bottomCenter.inscribe(Size(size.width, 3), rect);
canvas.drawRect(tabRect, tabPaint..color = color);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Related

Consumer doesn't update the UI when using notifyListeners()

I'm using animated containers and I want them to change color when I use NotifyListeners();
Thing is, the color only updates when you click on the container, otherwise it remains the same.
I tried switching to normal containers, I tried changing the home screen to stateful, nothing seems to work, any help is appreciated.
Github link: https://github.com/amrogad/beat_maker
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider<SoundProvider>(create: (_) => SoundProvider(),
),
ChangeNotifierProvider<IconProvider>(create: (_) => IconProvider(),
),
], child: MyApp()));
}
class IconProvider extends ChangeNotifier {
Color primary1 = Colors.deepPurpleAccent;
Color secondary1 = Colors.deepPurple;
Color primary2 = Colors.yellowAccent;
Color secondary2 = Colors.orangeAccent;
Color primary3 = Colors.cyanAccent;
Color secondary3 = Colors.blue;
Color primary4 = Colors.lime;
Color secondary4 = Colors.lightGreen;
void changeIcons(
Color primaryPath1,
Color secondaryPath1,
Color primaryPath2,
Color secondaryPath2,
Color primaryPath3,
Color secondaryPath3,
Color primaryPath4,
Color secondaryPath4,
) {
primary1 = primaryPath1;
secondary1 = secondaryPath1;
primary2 = primaryPath2;
secondary2 = secondaryPath2;
primary3 = primaryPath3;
secondary3 = secondaryPath3;
primary4 = primaryPath4;
secondary4 = secondaryPath4;
notifyListeners();
}
}
class HomeScreen extends StatelessWidget {
static const String routeName = 'Home Screen';
#override
Widget build(BuildContext context) {
var soundProvider = Provider.of<SoundProvider>(context);
return SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.black,
title: Text(
'Beat Maker',
style: GoogleFonts.anton(color: Colors.lime, fontSize: 24),
),
centerTitle: true,
actions: [
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(SettingsScreen.routeName);
},
child: Icon(
Icons.settings,
color: Colors.lime,
size: 24,
)),
SizedBox(
width: 10,
),
],
),
body: Consumer<IconProvider>(
builder: (context, icon, child) {
return GridView(
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4
),
children: [
SoundButton(
icon.primary1, icon.secondary1, soundProvider.note1),
SoundButton(
icon.primary2, icon.secondary2, soundProvider.note2),
SoundButton(
icon.primary3, icon.secondary3, soundProvider.note3),
SoundButton(
icon.primary4, icon.secondary4, soundProvider.note4)
],
);
}
),
),
);
}
class SoundButton extends StatefulWidget {
final Color mainColor;
final Color sideColor;
final note;
const SoundButton(this.mainColor, this.sideColor, this.note, {Key? key})
: super(key: key);
#override
State<SoundButton> createState() => _SoundButtonState();
}
class _SoundButtonState extends State<SoundButton> {
late Color _mainColor;
late Color _sideColor;
final player = AudioPlayer();
#override
void initState() {
_mainColor = widget.mainColor;
_sideColor = widget.sideColor;
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_mainColor = Colors.redAccent;
_sideColor = Colors.red;
player.play(AssetSource(widget.note));
});
},
child: AnimatedContainer(
decoration: BoxDecoration(
border: Border.all(width: 1),
gradient: RadialGradient(colors: [_mainColor, _sideColor]),
boxShadow: const [
BoxShadow(
blurRadius: 1.0,
spreadRadius: 1.0,
offset: Offset(
1.0,
1.0,
),
),
]),
curve: Curves.easeOutCubic,
onEnd: () => Reset(),
duration: const Duration(milliseconds: 100),
),
);
}
void Reset() {
setState(() {
_mainColor = widget.mainColor;
_sideColor = widget.sideColor;
});
}
}
class OpenIconBottomSheet extends StatelessWidget {
const OpenIconBottomSheet({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var iconProvider = Provider.of<IconProvider>(context);
return Container(
height: 106,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12), topRight: Radius.circular(12)),
color: Colors.black,
border: Border.all(width: 3, color: Colors.lime)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
InkWell(
onTap: () {
iconProvider.changeIcons(
Colors.deepPurpleAccent,
Colors.deepPurple,
Colors.yellowAccent,
Colors.orangeAccent,
Colors.cyanAccent,
Colors.blue,
Colors.lime,
Colors.lightGreen,
);
Navigator.pop(context);
},
child: isSelected(
'Main Theme',
iconProvider.primary1 == Colors.deepPurpleAccent
? true
: false)),
Container(
width: double.infinity,
height: 3,
color: Colors.lime,
),
SizedBox(
height: 7,
),
InkWell(
onTap: () {
iconProvider.changeIcons(
Colors.white70,
Colors.white,
Colors.pinkAccent,
Colors.pink,
Colors.tealAccent,
Colors.teal,
Colors.amberAccent,
Colors.redAccent,
);
Navigator.pop(context);
},
child: isSelected('Secondary Theme',
iconProvider.primary1 == Colors.white70 ? true : false)),
],
),
);
}
Widget isSelected(String text, bool selected) {
if (selected) {
return Padding(
padding: EdgeInsets.all(6),
child: Text(text,
style: GoogleFonts.anton(color: Colors.lime, fontSize: 24)),
);
} else {
return Padding(
padding: EdgeInsets.all(6),
child: Text(text,
style: GoogleFonts.anton(color: Colors.white, fontSize: 24)),
);
}
}
}

Flutter - How to flip a single card from swipe stack?

I am using swipe_stack
I need to flip the front card when I click on it. The problem currently I am facing is that the entire stacks get flipped.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:quiero_funku/widgets/appbar.dart';
import '../../utils/swipe_stack.dart';
class SwipeDeck extends StatefulWidget {
const SwipeDeck({Key? key}) : super(key: key);
#override
State<SwipeDeck> createState() => _SwipeDeckState();
}
class _SwipeDeckState extends State<SwipeDeck>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _animation;
AnimationStatus _status = AnimationStatus.dismissed;
List<AnimationController> dataCtrl = <AnimationController>[];
// initialize _controller, _animation
#override
void initState() {
super.initState();
// add some AnimationController object before using any index
dataCtrl.add(AnimationController(vsync: this, duration: const Duration(seconds: 1)));
for (int i = 1; i < 10; i++) {
var "_controller$i" = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
"_animation$i" = Tween(end: 1.0, begin: 0.0).animate(_controller)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
_status = status;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppbar(
onBackPressed: () {},
title: '',
),
body: Container(
padding: const EdgeInsets.all(20),
height: 300,
width: double.infinity,
child: SwipeStack(
children: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((int index) {
return SwiperItem(
builder: (SwiperPosition position, double progress) {
return Transform(
alignment: FractionalOffset.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.0015)
..rotateX(pi * _animation.value),
child: GestureDetector(
onTap: () {
print("tapped");
if (_status == AnimationStatus.dismissed) {
_controller.forward();
} else {
_controller.reverse();
}
},
child: Material(
elevation: 4,
borderRadius: const BorderRadius.all(Radius.circular(6)),
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"Item $index",
style: const TextStyle(
color: Colors.black,
fontSize: 20,
),
),
Text(
"Progress $progress",
style: const TextStyle(
color: Colors.blue,
fontSize: 12,
),
),
],
),
),
),
),
);
});
}).toList(),
visibleCount: 3,
stackFrom: StackFrom.Right,
translationInterval: 6,
scaleInterval: 0.03,
onEnd: () => debugPrint("onEnd"),
onSwipe: (int index, SwiperPosition position) =>
debugPrint("onSwipe $index $position"),
onRewind: (int index, SwiperPosition position) =>
debugPrint("onRewind $index $position"),
),
),
);
}

How to reset my quiz app questions choices

I am new to flutter, I have built a quizz app that takes 5 questions randomly from a pool of questions and presents them to the user one after the other, then displays the total score at the end (on a different) screen with the option of retaking the quiz (with another set of randomly picked questions).
My issue I am facing is that when I choose to retake the quiz, if in the pool of questions presented there is a question from the past quiz, it still has its options highlighted (marked either wrong or correct as per the previous selection).
Can someone help me on how to totally dismiss previous choices after taking a quiz ?
This is an example of question answered in the previous quiz, and it came back with the option already highlighted (my previous answer).
[enter image description here][1]
[1]: https://i.stack.imgur.com/U1YFf.png[enter image description here][1]
Here is my code:
import 'package:flutter/material.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'package:schoolest_app/widgets/quizz/quizz_image_container.dart';
import '../../models/quizz.dart';
import '../../widgets/quizz/options_widget.dart';
import '../../widgets/quizz/quizz_border_container.dart';
import '../../widgets/quizz/result_page.dart';
class QuizzDisplayScreen extends StatefulWidget {
const QuizzDisplayScreen({
Key? key,
}) : super(key: key);
static const routeName = '/quizz-display';
#override
State<QuizzDisplayScreen> createState() => _QuizzDisplayScreenState();
}
class _QuizzDisplayScreenState extends State<QuizzDisplayScreen> {
enter code here
late String quizzCategoryTitle;
late List<Question> categoryQuestions;
late List<Question> quizCategoryQuestions;
var _loadedInitData = false;
#override
void didChangeDependencies() {
if (!_loadedInitData) {
final routeArgs =
ModalRoute.of(context)!.settings.arguments as Map<String, String>;
quizzCategoryTitle = (routeArgs['title']).toString();
// final categoryId = routeArgs['id'];
categoryQuestions = questions.where((question) {
return question.categories.contains(quizzCategoryTitle);
}).toList();
quizCategoryQuestions =
(categoryQuestions.toSet().toList()..shuffle()).take(5).toList();
_loadedInitData = true;
}
super.didChangeDependencies();
}
late PageController _controller;
int _questionNumber = 1;
int _score = 0;
int _totalQuestions = 0;
bool _isLocked = false;
void _resetQuiz() {
for (var element in quizCategoryQuestions) {
setState(()=> element.isLocked == false);
}
}
#override
void initState() {
super.initState();
_controller = PageController(initialPage: 0);
}
#override
void dispose() {
_controller.dispose();
_resetQuiz();
super.dispose();
}
#override
Widget build(BuildContext context) {
final myPrimaryColor = Theme.of(context).colorScheme.primary;
final mySecondaryColor = Theme.of(context).colorScheme.secondary;
double answeredPercentage =
(_questionNumber / quizCategoryQuestions.length);
return quizCategoryQuestions.isEmpty
? Scaffold(
appBar: AppBar(
title: Text(
'Quizz - $quizzCategoryTitle',
style: TextStyle(color: myPrimaryColor),
),
iconTheme: IconThemeData(
color: myPrimaryColor,
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
flexibleSpace: Container(
decoration: BoxDecoration(`enter code here`
borderRadius: const BorderRadius.only(`enter code here`
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15),
),
color: mySecondaryColor,
border: Border.all(color: myPrimaryColor, width: 1.0),
),
),
),
body: const Center(
child: Text('Cette catégorie est vide pour l\'instant'),
))
: Scaffold(
appBar: AppBar(
title: Text(
'Quizz - $quizzCategoryTitle',
style: TextStyle(color: myPrimaryColor),
),
iconTheme: IconThemeData(
color: myPrimaryColor,
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
flexibleSpace: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15),
),
color: mySecondaryColor,
border: Border.all(color: myPrimaryColor, width: 1.0),
),
),
),
body: Container(
// height: 600,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
'Question $_questionNumber/${quizCategoryQuestions.length}',
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
CircularPercentIndicator(
radius: 40,
// animation: true,
// animationDuration: 2000,
percent: answeredPercentage,
progressColor: myPrimaryColor,
backgroundColor: Colors.cyan.shade100,
circularStrokeCap: CircularStrokeCap.round,
center: Text(
// ignore: unnecessary_brace_in_string_interps
'${(answeredPercentage * 100).round()} %',
style: const TextStyle(
fontSize: 10, fontWeight: FontWeight.bold),
),
// lineWidth: 10,
)
],
),
const SizedBox(height: 10),
Divider(
thickness: 1,
color: myPrimaryColor,
),
Expanded(
child: PageView.builder(
itemCount: quizCategoryQuestions.length,
controller: _controller,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final _question = quizCategoryQuestions[index];
return buildQuestion(_question);
},
),
),
_isLocked
? buildElevatedButton(context)
: const SizedBox.shrink(),
const SizedBox(height: 10),
],
),
),
);
}
Column buildQuestion(Question question) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 10),
question.text!.isNotEmpty
? QuizzBorderContainer(
childWidget: Text(
question.text!,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
)
: const SizedBox.shrink(),
question.imagePath!.isNotEmpty
? QuizzImageContainer(imagePath: question.imagePath!)
: const SizedBox.shrink(),
Expanded(
child: OptionsWidget(
question: question,
onClickedOption: (option) {
if (question.isLocked) {
return;
} else {
setState(() {
question.isLocked = true;
question.selectedOption = option;
});
_isLocked = question.isLocked;
if (question.selectedOption!.isCorrect) {
_score++;
}
}
},
),
),
],
);
}
ElevatedButton buildElevatedButton(BuildContext context) {
final mySecondaryColor = Theme.of(context).colorScheme.secondary;
return ElevatedButton(
onPressed: () {
if (_questionNumber < quizCategoryQuestions.length) {
_controller.nextPage(
duration: const Duration(milliseconds: 1000),
curve: Curves.easeInExpo,
);
setState(() {
_questionNumber++;
_isLocked = false;
});
} else {
setState(() {
// _isLocked = false;
_totalQuestions = quizCategoryQuestions.length;
});
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
ResultPage(score: _score, totalQuestions: _totalQuestions),
),
);
}
},
child: Text(
_questionNumber < quizCategoryQuestions.length
? 'Suivant'
: 'Voir le résultat',
style: TextStyle(
color: mySecondaryColor,
fontWeight: FontWeight.bold,
),
),
);
}
}
I don't seem to the solution to this.
And this is the code on the result page:
import 'package:flutter/material.dart';
import '../../screens/quizz/quizz_screen.dart';
class ResultPage extends StatefulWidget {
final int score;
final int totalQuestions;
const ResultPage({
Key? key,
required this.score,
required this.totalQuestions,
}) : super(key: key);
#override
State<ResultPage> createState() => _ResultPageState();
}
class _ResultPageState extends State<ResultPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
height: 150,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
'You got ${widget.score}/${widget.totalQuestions}',
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const QuizzScreen(),
),
);
},
child: const Text('OK'),
),
],
),
),
),
);
}
}
I don't know what is missing to get the reset right.
When you want to take retest try to dispose all the answers which are saved in the memory. Or you can use these navigators to which might help you in solving the issue. Try using pushReplacement or pushAndRemoveUntil when navigating to retest, this will clear the memory of last pages and you will achive the goal which you want.

How nagivate between pages with BottomNavyBar

I'm trying to navigate between my pages but I can't work it out. I'm not figuring it out. I tried to create a controller and put into "BarItem" but appers a mesage "only static members can be accessed in initializers"
I'm trying to replace the part:
body: AnimatedContainer(
color: widget.barItems[selectedBarIndex].color, duration: const Duration(milliseconds: 300),),
For a method to change my pages as I click on menu.
Help me guys!
import 'package:flutter/material.dart';
class AnimatedBottomBar extends StatefulWidget {
final List<BarItem> barItems;
final Duration animationDuration;
final Function onBarTap;
AnimatedBottomBar({this.barItems, this.animationDuration = const Duration(milliseconds: 500), this.onBarTap});
#override
_AnimatedBottomBarState createState() => _AnimatedBottomBarState();
}
class _AnimatedBottomBarState extends State<AnimatedBottomBar> with TickerProviderStateMixin{
int selectedBarIndex = 0;
#override
Widget build(BuildContext context) {
return Material(
elevation: 10.0,
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 16.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBarItems(),
),
),
);
}
List<Widget> _buildBarItems() {
List<Widget> _barItems = List();
for (int i = 0; i < widget.barItems.length; i++){
BarItem item = widget.barItems[i];
bool isSelected = selectedBarIndex == i;
_barItems.add(InkWell(
splashColor: Colors.transparent,
onTap: (){
setState(() {
selectedBarIndex = i;
widget.onBarTap(selectedBarIndex);
});
},
child: AnimatedContainer(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
duration: widget.animationDuration,
decoration: BoxDecoration(
color: isSelected ? item.color.withOpacity(0.2) : Colors.transparent,
borderRadius: BorderRadius.all(Radius.circular(30))
),
child: Row(
children: <Widget>[
Icon(item.iconData,
color: isSelected ? item.color : Colors.black,),
SizedBox(width: 10.0,),
AnimatedSize(
duration: widget.animationDuration,
curve: Curves.easeInOut,
vsync: this,
child: Text(
isSelected ? item.text : "",
style: TextStyle(color: item.color, fontWeight: FontWeight.w600, fontSize: 18.0),),
)
],
),
),
));
}
return _barItems;
}
}
class BarItem {
String text;
IconData iconData;
Color color;
PageController controller;//deletar
int page;//deletar
BarItem({this.text, this.iconData, this.color, this.controller, this.page});
}
import 'animated_bottom_bar.dart';
class BottomBarNavigationPattern extends StatefulWidget {
final List<BarItem> barItems = [
BarItem(
text: "Home",
iconData: Icons.home,
color: Colors.indigo,),
BarItem(
text: "Likes",
iconData: Icons.favorite_border,
color: Colors.pinkAccent),
BarItem(
text: "Search",
iconData: Icons.search,
color: Colors.yellow.shade900),
BarItem(
text: "Profile",
iconData: Icons.person_outline,
color: Colors.teal),
];
#override
_BottomBarNavigationPatternState createState() => _BottomBarNavigationPatternState();
}
class _BottomBarNavigationPatternState extends State<BottomBarNavigationPattern> {
int selectedBarIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedContainer(
color: widget.barItems[selectedBarIndex].color, duration: const Duration(milliseconds: 300),),
bottomNavigationBar: AnimatedBottomBar(
barItems: widget.barItems,
animationDuration: const Duration(milliseconds: 200),
onBarTap: (index){
setState(() {
selectedBarIndex = index;
});
}
),
);
}
}
You just need to add List of Widget you want to navigate to example :
final List<Widget> _buildScreens = [
TiersPage(),
SearchPage(),
SavedPage(),
];
And then change the body of scaffold to _buildScreens[selectedBarIndex]

How I can handle Nested TabBar with dynamic length in Flutter

I'm trying to display tabs for each main tabs (Nested Tab Bar).
I have a course page that show the information for this course in SliverAppBar() . Each course has many sections and each section has many exams.
This is my Build method:
#override
Widget build(BuildContext context) {
double height = MediaQuery.of(context).size.height;
double statusBarHeight =
MediaQuery.of(context).padding.top + 56; // 56 is height of Appbar.
return Scaffold(
body: Container(
child: DefaultTabController(
length: _sections.length,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
elevation: 0,
title: Text(
widget._course.shortName +
' ' +
widget._course.code.toString(),
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
actionsIconTheme: IconThemeData(color: widget._course.color),
expandedHeight: height / 2,
floating: true,
pinned: true,
centerTitle: true,
titleSpacing: 5,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
tooltip: 'Back',
splashColor: Colors.transparent,
onPressed: () => Navigator.pop(context),
),
backgroundColor: widget._course.color,
flexibleSpace: Container(
padding: EdgeInsets.only(top: statusBarHeight),
child: Text('Course information will be here'),
),
),
SliverPersistentHeader(
floating: false,
delegate: _SliverAppBarDelegate(
TabBar(
indicatorSize: TabBarIndicatorSize.label,
labelPadding: EdgeInsets.symmetric(horizontal: 10),
indicator: CircleTabIndicator(
color: Colors.white,
radius: 2.5,
),
indicatorColor: Colors.white,
isScrollable: true,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
unselectedLabelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w300),
labelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
tabs: List<Widget>.generate(
_sections.length,
(int index) {
return customTab(_sections[index].id);
},
),
),
widget._course.color,
),
pinned: false,
),
];
},
body: Center(
child: getTabBarConten(),
),
),
),
),
);
}
I get _SliverAppBarDelegate() class from the internet to handle the TabBar() in the NestedScrollView(), this is the code:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar, this._color);
TabBar _tabBar;
final Color _color;
#override
double get minExtent => _tabBar.preferredSize.height;
#override
double get maxExtent => _tabBar.preferredSize.height;
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: _color,
alignment: Alignment.center,
child: _tabBar,
);
}
#override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return false;
}
}
Now each section tab content that I created it in Build method it's have many exams tabs. getTabBarConten() method:
Widget getTabBarConten() {
return TabBarView(
children: List<Widget>.generate(
_sections.length,
(int index) {
return CustomTabView(
color: widget._course.color,
initPosition: initPosition,
itemCount: _sections[index].exams.length,
tabBuilder: (context, index) => customTab(
_sections[index].exams[index].type.toString(),
isSection: true,
),
pageBuilder: (context, index) => Center(
child: Text(_sections[index].exams[index].supervisor +
' ' +
_sections[index].instructor)),
onPositionChange: (index) {
setState(() {
initPosition = index;
});
},
);
},
),
);
}
This getTabBarConten() method it's for sections tabs content and it's return CustomTabView that return tabs for each exam.
The problem is:
RangeError (index): Invalid value: Not in range 0..1, inclusive: 2
In this example the course has 2 sections and each section has 3 exams. So the itemCount in CustomTabView is 3 and the length of sections is 2 that is the error come from.
If I set the itemCount to same length of sections it's work fine (even if the itemCount is less than the length of sections):
See the image here
But if the itemCount is greater than the length of sections its not working!
See the error here
Why this error happened , I mean there is no relation between them, in getTabBarConten() method it's return TabBarView() for the section tabs and for each tab it's return CustomTabView() that return tab for each exam.
So, Why this this error and can anyone help me? please :(
Thanks for chunhunghan his answer in this question is helped me. It's another way but it's work..
[Update: Sep]
I will try to write my code for the same example. Maybe it will help someone :)
Here the code:
import 'package:flutter/material.dart';
class CustomTabView extends StatefulWidget {
final int itemCount;
final IndexedWidgetBuilder tabBuilder;
final IndexedWidgetBuilder pageBuilder;
final Widget stub;
final ValueChanged<int> onPositionChange;
final ValueChanged<double> onScroll;
final int initPosition;
final Color color;
final bool isExamTabs;
final TabController controller;
CustomTabView({
#required this.itemCount,
#required this.tabBuilder,
#required this.pageBuilder,
this.stub,
this.onPositionChange,
this.onScroll,
this.initPosition,
this.color,
this.isExamTabs = false,
this.controller,
});
#override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView>
with TickerProviderStateMixin {
TabController controller;
int _currentCount;
int _currentPosition;
#override
void initState() {
if (widget.controller == null) {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
_currentCount = widget.itemCount;
} else {
controller = widget.controller;
}
super.initState();
}
#override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition;
}
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1;
_currentPosition = _currentPosition < 0 ? 0 : _currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
widget.onPositionChange(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition);
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.itemCount < 1) return widget.stub ?? Container();
double height = MediaQuery.of(context).size.height;
return Container(
height: height - 100,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
color: widget.color,
alignment: Alignment.center,
child: widget.isExamTabs
? TabBar(
controller: controller,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 3.5,
indicatorColor: Colors.white,
isScrollable: true,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
unselectedLabelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w300),
labelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
)
: TabBar(
controller: controller,
indicatorSize: TabBarIndicatorSize.label,
labelPadding: EdgeInsets.symmetric(horizontal: 10),
indicatorColor: Colors.white,
isScrollable: true,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
unselectedLabelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w300),
labelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
),
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll(controller.animation.value);
}
}
}
I called it in my course page's body: (Please see the comments in the code)
CustomTabView(
initPosition: 0,
itemCount: _course.sections.length,
tabBuilder: (context, index) =>
secionTab(_course.sections[index].id), // Sections tabs
pageBuilder: (context, index) => getSectionTabBarConten(index), // Content for each section. To show exams for "_course.sections[index]" call inside it "CustomTabView()" again for exams. It's mean all Exams per secion.
onPositionChange: (index) {},
// onScroll: (position) => print("POS : " + '$position'),
color: _course.getColor(),
)