Related
I want to change color of line in stepper, Look at the below picture
my try here :
Theme(
data: ThemeData(canvasColor: whiteColor),
child: Stepper(
type: StepperType.horizontal,
steps: getSteps(),
currentStep: currentStep,
elevation: 2,
),
),
Please help to continue my code.
Let me be short and clear.
TO CHANGE STEPPER LINE COLOR
Unfortunately, the Flutter code for the Stepper does not support the change of the Stepper line color (_buildLine) and more.
So I created a custom stepper called CusStepper that uses CusStep
NOTE: This CusStepper has the same properties and behaves like the normal Stepper with the only difference of lineColor property.
Steps on how to use the CusStepper
Create a new .dart file to store the CusStepper code
Paste the code below for the CusStepper in the file.
Import it to the file you want to use it in.
Finally change the stepper line color with the lineColor params. on the CusStepper
CusStepper code to paste in the .dart file created in step 1
// THIS IS A CUSTOM STEPPER THAT ADDS LINE COLOR
import 'package:flutter/material.dart';
enum CusStepState {
indexed,
editing,
complete,
disabled,
error,
}
enum CusStepperType {
vertical,
horizontal,
}
#immutable
class ControlsDetails {
const ControlsDetails({
required this.currentStep,
required this.stepIndex,
this.onStepCancel,
this.onStepContinue,
});
final int currentStep;
final int stepIndex;
final VoidCallback? onStepContinue;
final VoidCallback? onStepCancel;
bool get isActive => currentStep == stepIndex;
}
typedef ControlsWidgetBuilder = Widget Function(
BuildContext context, ControlsDetails details);
const TextStyle _kStepStyle = TextStyle(
fontSize: 12.0,
color: Colors.white,
);
const Color _kErrorLight = Colors.red;
final Color _kErrorDark = Colors.red.shade400;
const Color _kCircleActiveLight = Colors.white;
const Color _kCircleActiveDark = Colors.black87;
const Color _kDisabledLight = Colors.black38;
const Color _kDisabledDark = Colors.white38;
const double _kStepSize = 24.0;
const double _kTriangleHeight = _kStepSize * 0.866025;
#immutable
class CusStep {
const CusStep({
required this.title,
this.subtitle,
required this.content,
this.state = CusStepState.indexed,
this.isActive = false,
this.label,
});
final Widget title;
final Widget? subtitle;
final Widget content;
final CusStepState state;
final bool isActive;
final Widget? label;
}
class CusStepper extends StatefulWidget {
const CusStepper({
super.key,
required this.steps,
this.physics,
this.type = CusStepperType.vertical,
this.currentStep = 0,
this.onStepTapped,
this.onStepContinue,
this.onStepCancel,
this.controlsBuilder,
this.elevation,
this.margin,
this.lineColor = Colors.grey,
}) : assert(0 <= currentStep && currentStep < steps.length);
final List<CusStep> steps;
final ScrollPhysics? physics;
final CusStepperType type;
final int currentStep;
final ValueChanged<int>? onStepTapped;
final VoidCallback? onStepContinue;
final VoidCallback? onStepCancel;
final ControlsWidgetBuilder? controlsBuilder;
final double? elevation;
final EdgeInsetsGeometry? margin;
final Color lineColor;
#override
State<CusStepper> createState() => _CusStepperState();
}
class _CusStepperState extends State<CusStepper> with TickerProviderStateMixin {
late List<GlobalKey> _keys;
final Map<int, CusStepState> _oldStates = <int, CusStepState>{};
#override
void initState() {
super.initState();
_keys = List<GlobalKey>.generate(
widget.steps.length,
(int i) => GlobalKey(),
);
for (int i = 0; i < widget.steps.length; i += 1) {
_oldStates[i] = widget.steps[i].state;
}
}
#override
void didUpdateWidget(CusStepper oldWidget) {
super.didUpdateWidget(oldWidget);
assert(widget.steps.length == oldWidget.steps.length);
for (int i = 0; i < oldWidget.steps.length; i += 1) {
_oldStates[i] = oldWidget.steps[i].state;
}
}
bool _isFirst(int index) {
return index == 0;
}
bool _isLast(int index) {
return widget.steps.length - 1 == index;
}
bool _isCurrent(int index) {
return widget.currentStep == index;
}
bool _isDark() {
return Theme.of(context).brightness == Brightness.dark;
}
bool _isLabel() {
for (final CusStep step in widget.steps) {
if (step.label != null) {
return true;
}
}
return false;
}
Widget _buildLine(bool visible) {
return Container(
width: visible ? 1.0 : 0.0,
height: 16.0,
color: widget.lineColor,
);
}
Widget _buildCircleChild(int index, bool oldState) {
final CusStepState state =
oldState ? _oldStates[index]! : widget.steps[index].state;
final bool isDarkActive = _isDark() && widget.steps[index].isActive;
switch (state) {
case CusStepState.indexed:
case CusStepState.disabled:
return Text(
'${index + 1}',
style: isDarkActive
? _kStepStyle.copyWith(color: Colors.black87)
: _kStepStyle,
);
case CusStepState.editing:
return Icon(
Icons.edit,
color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight,
size: 18.0,
);
case CusStepState.complete:
return Icon(
Icons.check,
color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight,
size: 18.0,
);
case CusStepState.error:
return const Text('!', style: _kStepStyle);
}
}
Color _circleColor(int index) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
if (!_isDark()) {
return widget.steps[index].isActive
? colorScheme.primary
: colorScheme.onSurface.withOpacity(0.38);
} else {
return widget.steps[index].isActive
? colorScheme.secondary
: colorScheme.background;
}
}
Widget _buildCircle(int index, bool oldState) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
width: _kStepSize,
height: _kStepSize,
child: AnimatedContainer(
curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration,
decoration: BoxDecoration(
color: _circleColor(index),
shape: BoxShape.circle,
),
child: Center(
child: _buildCircleChild(
index, oldState && widget.steps[index].state == CusStepState.error),
),
),
);
}
Widget _buildTriangle(int index, bool oldState) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
width: _kStepSize,
height: _kStepSize,
child: Center(
child: SizedBox(
width: _kStepSize,
height:
_kTriangleHeight, // Height of 24dp-long-sided equilateral triangle.
child: CustomPaint(
painter: _TrianglePainter(
color: _isDark() ? _kErrorDark : _kErrorLight,
),
child: Align(
alignment: const Alignment(
0.0, 0.8), // 0.8 looks better than the geometrical 0.33.
child: _buildCircleChild(index,
oldState && widget.steps[index].state != CusStepState.error),
),
),
),
),
);
}
Widget _buildIcon(int index) {
if (widget.steps[index].state != _oldStates[index]) {
return AnimatedCrossFade(
firstChild: _buildCircle(index, true),
secondChild: _buildTriangle(index, true),
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
crossFadeState: widget.steps[index].state == CusStepState.error
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: kThemeAnimationDuration,
);
} else {
if (widget.steps[index].state != CusStepState.error) {
return _buildCircle(index, false);
} else {
return _buildTriangle(index, false);
}
}
}
Widget _buildVerticalControls(int stepIndex) {
if (widget.controlsBuilder != null) {
return widget.controlsBuilder!(
context,
ControlsDetails(
currentStep: widget.currentStep,
onStepContinue: widget.onStepContinue,
onStepCancel: widget.onStepCancel,
stepIndex: stepIndex,
),
);
}
final Color cancelColor;
switch (Theme.of(context).brightness) {
case Brightness.light:
cancelColor = Colors.black54;
break;
case Brightness.dark:
cancelColor = Colors.white70;
break;
}
final ThemeData themeData = Theme.of(context);
final ColorScheme colorScheme = themeData.colorScheme;
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
const OutlinedBorder buttonShape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2)));
const EdgeInsets buttonPadding = EdgeInsets.symmetric(horizontal: 16.0);
return Container(
margin: const EdgeInsets.only(top: 16.0),
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(height: 48.0),
child: Row(
children: <Widget>[
TextButton(
onPressed: widget.onStepContinue,
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return states.contains(MaterialState.disabled)
? null
: (_isDark()
? colorScheme.onSurface
: colorScheme.onPrimary);
}),
backgroundColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> states) {
return _isDark() || states.contains(MaterialState.disabled)
? null
: colorScheme.primary;
}),
padding: const MaterialStatePropertyAll<EdgeInsetsGeometry>(
buttonPadding),
shape:
const MaterialStatePropertyAll<OutlinedBorder>(buttonShape),
),
child: Text(themeData.useMaterial3
? localizations.continueButtonLabel
: localizations.continueButtonLabel.toUpperCase()),
),
Container(
margin: const EdgeInsetsDirectional.only(start: 8.0),
child: TextButton(
onPressed: widget.onStepCancel,
style: TextButton.styleFrom(
foregroundColor: cancelColor,
padding: buttonPadding,
shape: buttonShape,
),
child: Text(themeData.useMaterial3
? localizations.cancelButtonLabel
: localizations.cancelButtonLabel.toUpperCase()),
),
),
],
),
),
);
}
TextStyle _titleStyle(int index) {
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
switch (widget.steps[index].state) {
case CusStepState.indexed:
case CusStepState.editing:
case CusStepState.complete:
return textTheme.bodyLarge!;
case CusStepState.disabled:
return textTheme.bodyLarge!.copyWith(
color: _isDark() ? _kDisabledDark : _kDisabledLight,
);
case CusStepState.error:
return textTheme.bodyLarge!.copyWith(
color: _isDark() ? _kErrorDark : _kErrorLight,
);
}
}
TextStyle _subtitleStyle(int index) {
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
switch (widget.steps[index].state) {
case CusStepState.indexed:
case CusStepState.editing:
case CusStepState.complete:
return textTheme.bodySmall!;
case CusStepState.disabled:
return textTheme.bodySmall!.copyWith(
color: _isDark() ? _kDisabledDark : _kDisabledLight,
);
case CusStepState.error:
return textTheme.bodySmall!.copyWith(
color: _isDark() ? _kErrorDark : _kErrorLight,
);
}
}
TextStyle _labelStyle(int index) {
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
switch (widget.steps[index].state) {
case CusStepState.indexed:
case CusStepState.editing:
case CusStepState.complete:
return textTheme.bodyLarge!;
case CusStepState.disabled:
return textTheme.bodyLarge!.copyWith(
color: _isDark() ? _kDisabledDark : _kDisabledLight,
);
case CusStepState.error:
return textTheme.bodyLarge!.copyWith(
color: _isDark() ? _kErrorDark : _kErrorLight,
);
}
}
Widget _buildHeaderText(int index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
AnimatedDefaultTextStyle(
style: _titleStyle(index),
duration: kThemeAnimationDuration,
curve: Curves.fastOutSlowIn,
child: widget.steps[index].title,
),
if (widget.steps[index].subtitle != null)
Container(
margin: const EdgeInsets.only(top: 2.0),
child: AnimatedDefaultTextStyle(
style: _subtitleStyle(index),
duration: kThemeAnimationDuration,
curve: Curves.fastOutSlowIn,
child: widget.steps[index].subtitle!,
),
),
],
);
}
Widget _buildLabelText(int index) {
if (widget.steps[index].label != null) {
return AnimatedDefaultTextStyle(
style: _labelStyle(index),
duration: kThemeAnimationDuration,
child: widget.steps[index].label!,
);
}
return const SizedBox.shrink();
}
Widget _buildVerticalHeader(int index) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
children: <Widget>[
Column(
children: <Widget>[
_buildLine(!_isFirst(index)),
_buildIcon(index),
_buildLine(!_isLast(index)),
],
),
Expanded(
child: Container(
margin: const EdgeInsetsDirectional.only(start: 12.0),
child: _buildHeaderText(index),
),
),
],
),
);
}
Widget _buildVerticalBody(int index) {
return Stack(
children: <Widget>[
PositionedDirectional(
start: 24.0,
top: 0.0,
bottom: 0.0,
child: SizedBox(
width: 24.0,
child: Center(
child: SizedBox(
width: _isLast(index) ? 0.0 : 1.0,
child: Container(
color: widget.lineColor,
),
),
),
),
),
AnimatedCrossFade(
firstChild: Container(height: 0.0),
secondChild: Container(
margin: widget.margin ??
const EdgeInsetsDirectional.only(
start: 60.0,
end: 24.0,
bottom: 24.0,
),
child: Column(
children: <Widget>[
widget.steps[index].content,
_buildVerticalControls(index),
],
),
),
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
crossFadeState: _isCurrent(index)
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: kThemeAnimationDuration,
),
],
);
}
Widget _buildVertical() {
return ListView(
shrinkWrap: true,
physics: widget.physics,
children: <Widget>[
for (int i = 0; i < widget.steps.length; i += 1)
Column(
key: _keys[i],
children: <Widget>[
InkWell(
onTap: widget.steps[i].state != CusStepState.disabled
? () {
// In the vertical case we need to scroll to the newly tapped
// step.
Scrollable.ensureVisible(
_keys[i].currentContext!,
curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration,
);
widget.onStepTapped?.call(i);
}
: null,
canRequestFocus: widget.steps[i].state != CusStepState.disabled,
child: _buildVerticalHeader(i),
),
_buildVerticalBody(i),
],
),
],
);
}
Widget _buildHorizontal() {
final List<Widget> children = <Widget>[
for (int i = 0; i < widget.steps.length; i += 1) ...<Widget>[
InkResponse(
onTap: widget.steps[i].state != CusStepState.disabled
? () {
widget.onStepTapped?.call(i);
}
: null,
canRequestFocus: widget.steps[i].state != CusStepState.disabled,
child: Row(
children: <Widget>[
SizedBox(
height: _isLabel() ? 104.0 : 72.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (widget.steps[i].label != null)
const SizedBox(
height: 24.0,
),
Center(child: _buildIcon(i)),
if (widget.steps[i].label != null)
SizedBox(
height: 24.0,
child: _buildLabelText(i),
),
],
),
),
Container(
margin: const EdgeInsetsDirectional.only(start: 12.0),
child: _buildHeaderText(i),
),
],
),
),
if (!_isLast(i))
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
height: 1.0,
color: widget.lineColor,
),
),
],
];
final List<Widget> stepPanels = <Widget>[];
for (int i = 0; i < widget.steps.length; i += 1) {
stepPanels.add(
Visibility(
maintainState: true,
visible: i == widget.currentStep,
child: widget.steps[i].content,
),
);
}
return Column(
children: <Widget>[
Material(
elevation: widget.elevation ?? 2,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
children: children,
),
),
),
Expanded(
child: ListView(
physics: widget.physics,
padding: const EdgeInsets.all(24.0),
children: <Widget>[
AnimatedSize(
curve: Curves.fastOutSlowIn,
duration: kThemeAnimationDuration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: stepPanels),
),
_buildVerticalControls(widget.currentStep),
],
),
),
],
);
}
#override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context));
assert(() {
if (context.findAncestorWidgetOfExactType<Stepper>() != null) {
throw FlutterError(
'Steppers must not be nested.\n'
'The material specification advises that one should avoid embedding '
'steppers within steppers. '
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage',
);
}
return true;
}());
switch (widget.type) {
case CusStepperType.vertical:
return _buildVertical();
case CusStepperType.horizontal:
return _buildHorizontal();
}
}
}
class _TrianglePainter extends CustomPainter {
_TrianglePainter({
required this.color,
});
final Color color;
#override
bool hitTest(Offset point) => true;
#override
bool shouldRepaint(_TrianglePainter oldPainter) {
return oldPainter.color != color;
}
#override
void paint(Canvas canvas, Size size) {
final double base = size.width;
final double halfBase = size.width / 2.0;
final double height = size.height;
final List<Offset> points = <Offset>[
Offset(0.0, height),
Offset(base, height),
Offset(halfBase, 0.0),
];
canvas.drawPath(
Path()..addPolygon(points, true),
Paint()..color = color,
);
}
}
EXAMPLE CODEBASE ON HOW TO USE THE CusStepper
import 'package:flutter/material.dart';
import 'package:tester/stepper/stepper.dart'; // this is the file path of where you store your `cusStepper`
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Tester',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _index = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CusStepper(
lineColor: Colors.red, // new line add for the color change
currentStep: _index,
onStepCancel: () {
if (_index > 0) {
setState(() {
_index -= 1;
});
}
},
onStepContinue: () {
if (_index <= 0) {
setState(() {
_index += 1;
});
}
},
onStepTapped: (int index) {
setState(() {
_index = index;
});
},
steps: <CusStep>[
CusStep(
title: const Text('Step 1 title'),
content: Container(
alignment: Alignment.centerLeft,
child: const Text('Content for Step 1')),
),
const CusStep(
title: Text('Step 2 title'),
content: Text('Content for Step 2'),
),
],
),
),
);
}
}
OUTPUT
Leave a comment below if you have any questions or help on this.
Bye!
The Flutter Default stepper Line has Static color use so can't change this.
chack below Image.
Here https://fluttergems.dev/stepper mention many stepper package used as you want.
I'm trying to setup a Flutter parent/child widget configuration to allow for multiple textControllers on the child widget and the parent widget to control the saving of the form answer and some validation.
I need to be able to have multiple answer areas. Most questions will have 1 answer area but some will have multiple.
So far, I have a function being passed into the Child Widget called nextPageStatus. This is used in the textController Listener to send a true/false back to the Parent Widget and set the active status of the "Next" question button.
What I can't figure out is how to get the values from the textController in the Parent Widget. I need those values on the Parent Widget because I call the database to save the answers on the Parent Widget.
Parent Wiget:
class ParentWidget extends StatefulWidget {
ParentWidget({super.key});
#override
State<StatefulWidget> createState() => ParentWidgetState();
}
class ParentWidgetState extends State<ParentWidget> {
late int _currentIndex = 0;
bool nextPageIsActive = false;
bool prevPageIsActive = false;
late SwiperController _swiperController;
List<QuestionModel> questionList = [];
List<AnswerModel> answerList = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children:[
Row(
children: [
Column(
children: [
IconButton(
onPressed: prevPageIsActive ? _previousCard : () {},
icon: Icon(
Icons.arrow_back,
color: prevPageIsActive ? null : Colors.grey,
)
),
const Text("Back")
]),
Column(
children: [
IconButton(
onPressed: nextPageIsActive ? _nextPageAction : () { },
icon: Icon(
Icons.arrow_forward,
color: nextPageIsActive ? null : Colors.grey
)
),
Text("Next")
]
),
],
),
]
)
);
}
void setNextPageStatus(bool status) {
setState(() {
nextPageIsActive = status;
});
}
void _previousCard() {
_swiperController.previous(animation: true);
setState(() {
prevPageIsActive = false;
})
}
void _nextCard() {
_swiperController.next(animation: true);
setState(() {
prevPageIsActive = true;
});
}
void _saveAnswerToDatabase() {
final firestoreDatabase =
Provider.of<FirestoreDatabase>(context, listen: false);
List<String> answers = [];
// TODO: Get all answers from Child Widget here
// for (var ans in answerList) {
// answers.add(ans);
// }
firestoreDatabase.setUserAnswers(answers, userId);
}
void _nextPageAction() {
// this function should only be available
// once requirements are met on child
// (must have at least 7 letters typed for question)
_saveAnswerToDatabase();
if (questionList.length == (_currentIndex + 1)) {
// last prompt -- finish
Navigator.pushNamed(
context,
AppRoutes.home,
);
} else {
_nextCard();
}
}
Widget buildSwiper() {
return Expanded(
child: Swiper(
controller: _swiperController,
itemCount: questionList.length,
onIndexChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemBuilder: Card(
child: SingleChildScrollView(
child: Column(
children: [
// !! this is where I iterate over the all the possible questions
for (int i = 0; i < currentPrompt.questionPrompts.length; i++)
AnswerArea(
key: widget.promptScreenKey,
nextPageStatus: setNextPageStatus,
questionPrompt: questionList[index].questionPrompts[i],)
],
),
),
),
),
);
}
}
Child Widget (Answer Area):
class AnswerArea extends StatefulWidget {
AnswerArea({Key? key, required this.textPrompt, required this.nextPageStatus}) : super(key: key);
late String textPrompt;
late Function(bool) nextPageStatus;
#override
State<AnswerArea> createState() => _AnswerAreaState();
}
class _AnswerAreaState extends State<AnswerArea> {
late List<TextEditingController> answerAreaTextControllers;
final answerAreaTextController = TextEditingController();
_textAnswerListener() {
if (answerAreaTextController.text.isNotEmpty && answerAreaTextController.text.length >= 7) {
widget.nextPageStatus(true);
} else {
widget.nextPageStatus(false);
}
}
#override
Widget build(BuildContext context) {
answerAreaTextController.addListener(_textAnswerListener);
const maxLines = 5;
const numberOfLines = 5;
const cursorHeight = 22.0;
return Container(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
child: Column(
children: [
Stack(
children: [
Container(
child: SizedBox(
height: numberOfLines * (cursorHeight + 8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: TextField(
controller: answerAreaTextController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.textPrompt,
hintStyle: TextStyle(color: Colors.white),
),
cursorHeight: cursorHeight,
keyboardType: TextInputType.multiline,
maxLines: maxLines,
),
),
),
),
for (int i = 0; i < numberOfLines; i++)
answerAreaTextController.text.isEmpty ? Container(
width: double.infinity,
margin: EdgeInsets.only(
top: 4 + (i + 1) * cursorHeight,
left: 15,
right: 15,
),
height: 1,
color: Colors.white,
) : Container(),
],
),
],
),
);
}
}
I have an issue with rebuilding pages in the stack with Flutter
This is all my users and have added the search to the top appBar.
But it works with conditions to see if it is widgets there then get height of the widgets that is being fixed underneath the appBar...But that happens asynchronously.
So when firstLoad it works but when I call setState it then rebuilds all the pages in the stack and with that it looks like this
This is how it looks after a set state. The problem i saw is that the previous pages have an influence. I couldn't find a good viable solution to this. Will explain my architecture.
I have a page Layout that is a container wrapper for all my pages that has it's appBar styles and just sends through the children. But that is the page Layout wrapper that is being rebuild every time the a set States happen
HOW I GET MY WIDGET SIZE
HOW I IMPLEMENTED IT
It goes in the else with the other pages in the stack.. I tried putting it in the initState but it never goes inside because it is used in the other pages in the stack
I only need an implementation to rebuild the TOP page in the stack.
PAGE LAYOUT
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutterweb/constants.dart';
import 'package:flutterweb/controllers/channel_controller.dart';
import 'package:flutterweb/controllers/user_controller.dart';
import 'package:flutterweb/main.dart';
import 'package:flutterweb/models/channels_model.dart';
import 'package:flutterweb/utils/functions.dart';
import 'package:flutterweb/views/channels/func.dart';
import 'package:flutterweb/views/home/home.dart';
import 'package:flutterweb/views/menu/permissions/choose_assign_group.dart';
import 'package:flutterweb/widgets/builders/KNetworkFadeImage.dart';
import 'package:flutterweb/widgets/builders/kPopups.dart';
import 'package:flutterweb/widgets/drawerDara.dart';
import 'package:get/get.dart';
class CustomAppBar extends StatefulWidget {
final String title;
final List<Map<String, dynamic>>? topTabs;
final TabController? topTabController;
final List<Widget>? children;
final List<Widget>? childrenFixed;
final Function? leftActionFunction;
final Icon? leftActionIcon;
final Drawer? drawer;
final Function? logOutPressed;
final bool showOptionsMenu;
final Widget? optionMenu;
final Widget? bottomNavigationBar;
final String? backGroundImage;
final ScrollController? scrollController;
CustomAppBar({
required this.title,
this.topTabs,
this.topTabController,
this.leftActionFunction,
this.leftActionIcon,
this.children,
this.childrenFixed,
this.drawer,
this.logOutPressed,
this.showOptionsMenu = false,
this.optionMenu,
this.bottomNavigationBar,
this.backGroundImage,
this.scrollController,
});
#override
_CustomAppBarState createState() => _CustomAppBarState();
}
double app_content_height = 0;
double fixedWidgetSize = 0;
String prevTitle = "";
class _CustomAppBarState extends State<CustomAppBar>
with SingleTickerProviderStateMixin {
final GlobalKey<ScaffoldState> scaffoldkey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
_toggleAnimation() {
scaffoldkey.currentState!.openDrawer();
}
double _getAppBarSize() {
double fixedHeightInclude = fixedWidgetSize;
if (widget.topTabs != null) {
fixedHeightInclude += 100;
} else if (widget.title == "") {
fixedHeightInclude += 0;
} else {
fixedHeightInclude += 60;
}
return fixedHeightInclude;
}
#override
Widget build(BuildContext context) {
// if (widget.title != global_title) return SizedBox();
List<Widget> arr = [];
Widget arrView = SizedBox();
double statusBar = 0;
double _width = MediaQuery.of(context).size.width;
Widget? fixedChild = SizedBox();
if ((widget.childrenFixed?.length ?? 0) > 1) {
fixedChild = WidgetSize(
child: Column(children: widget.childrenFixed!),
onChange: (Size size) {
fixedWidgetSize = 0;
setState(() {
fixedWidgetSize = size.height;
});
kPrint("fixedWidgetSize ${size.height}");
},
);
} else {
fixedWidgetSize = 0;
}
// Widget? fixedChild = (widget.childrenFixed?.length ?? 0) > 1
// ? WidgetSize(
// child: Column(children: widget.childrenFixed!),
// onChange: (Size size) {
// fixedWidgetSize = 0;
// setState(() {
// fixedWidgetSize = size.height;
// });
// kPrint("fixedWidgetSize ${size.height}");
// },
// )
// : SizedBox();
app_content_height =
MediaQuery.of(context).size.height - _getAppBarSize() - statusBar;
if (widget.title != "") {
arr.add(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 2,
child: widget.showOptionsMenu == true
? IconButton(
icon: const Icon(Icons.menu, color: Colors.white),
onPressed: () => _toggleAnimation(),
)
: IconButton(
icon: widget.leftActionIcon ??
const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
if (widget.leftActionFunction != null) {
widget.leftActionFunction!();
} else {
if (Navigator.canPop(context)) {
Get.back();
}
}
},
),
),
Expanded(flex: 2, child: SizedBox()),
Expanded(
flex: 10,
child: Center(
child: Text(
widget.title,
style: const TextStyle(color: Colors.white, fontSize: 24.0),
),
),
),
Expanded(
flex: 4,
child: widget.logOutPressed != null
? IconButton(
icon: const Icon(Icons.power_settings_new_outlined,
color: Colors.white),
onPressed: () {
widget.logOutPressed!();
},
)
: widget.optionMenu ?? Container(),
),
],
),
);
}
if (widget.topTabs != null) {
List<Widget> tempTopBar = [];
List<Widget> tempTopView = [];
for (var i = 0; i < widget.topTabs!.length; i++) {
String key = widget.topTabs![i].keys
.toString()
.replaceAll("(", "")
.replaceAll(")", "");
Widget value = widget.topTabs![i][key];
tempTopBar.add(Tab(text: key));
tempTopView.add(SingleChildScrollView(child: value));
}
arr.add(
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Center(
child: Container(
height: 30,
child: DefaultTabController(
length: widget.topTabs!.length,
child: TabBar(
labelPadding: widget.topTabs!.length == 2
? const EdgeInsets.symmetric(horizontal: 40.0)
: const EdgeInsets.symmetric(horizontal: 16.0),
controller: widget.topTabController,
indicatorSize: TabBarIndicatorSize.tab,
indicator: CircleTabIndicator(color: Colors.white, radius: 4),
isScrollable: true,
labelColor: Colors.white,
tabs: tempTopBar,
),
),
),
),
),
);
// arr.add(child);
arrView = Container(
width: _width,
height: app_content_height,
child: TabBarView(
controller: widget.topTabController,
children: tempTopView,
),
);
}
if (widget.children != null) {
arrView = Container(
width: _width,
height: app_content_height,
child: ListView(
// controller: widget.scrollController ?? ScrollController(),
children: widget.children!,
),
);
}
_getStatus() {
if (statusBar > 0) {
Color color = AppColors.kBlue;
return Container(
height: Get.height * 0.03,
width: Get.width,
color: color,
child: const Center(
child: Text(
"",
style: const TextStyle(color: Colors.black),
),
),
);
} else {
return const SizedBox();
}
}
return SafeArea(
child: Material(
child: Stack(
children: [
Scaffold(
resizeToAvoidBottomInset:
true, //That the keyboard shows correctly
extendBodyBehindAppBar: true,
key: scaffoldkey,
appBar: PreferredSize(
preferredSize: Size.fromHeight(
_getAppBarSize()), // here the desired height
child: Container(
decoration: kAppBarBoxDecorations,
child: Column(
children: [
Column(
children: arr,
),
fixedChild,
],
),
),
),
drawer: Drawer(
child: ListView(
children: <Widget>[
UserAccountsDrawerHeader(
accountName: InkWell(
onTap: () {
Channels element =
ChannelController.to.gSelectedChannel.value;
getChannelRoles(element);
},
child: Text(
"${ChannelController.to.gSelectedChannel.value.rolDesc} >",
),
),
accountEmail: Text(
UserController.to.gUserModel.value.email.toString()),
currentAccountPicture: GestureDetector(
child: const CircleAvatar(
backgroundImage: NetworkImage(
"https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500"),
),
onTap: () => print("Current User")),
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image: NetworkImage(
"${URLS.keyBaseUrl}/assets/images/background/background7.jpg")),
),
),
ListTile(
title: const Text("Home"),
trailing: const Icon(Icons.home),
onTap: () =>
Get.toNamed(Home.router, preventDuplicates: false),
),
ListTile(
title: const Text("Menu Permissions"),
trailing: const Icon(Icons.home),
onTap: () => Get.toNamed(ChooseAssignGroup.router,
preventDuplicates: false),
),
const Divider(
thickness: 1.0,
),
drawerData(),
const Divider(
thickness: 1.0,
),
ListTile(
title: const Text("Close"),
trailing: const Icon(Icons.cancel),
onTap: () => Navigator.of(context).pop(),
),
ListTile(
title: const Text("Log Out"),
trailing: const Icon(Icons.logout),
onTap: () => UserController.to.logOutUser(),
),
],
),
),
body: Container(
decoration: widget.backGroundImage != null
? BoxDecoration(
color: Colors.black.withOpacity(0.9),
image: DecorationImage(
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(0.2), BlendMode.dstATop),
image: NetworkImage(widget.backGroundImage!),
),
)
: BoxDecoration(color: Colors.grey.shade400),
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 800),
padding: EdgeInsets.only(
left: 15.0, right: 15.0, top: _getAppBarSize()),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: arrView,
),
),
),
),
bottomNavigationBar: widget.bottomNavigationBar,
),
],
),
),
);
}
}
class CircleTabIndicator extends Decoration {
final BoxPainter _painter;
CircleTabIndicator({required Color color, required double radius})
: _painter = _CirclePainter(color, radius);
#override
BoxPainter createBoxPainter([onChanged()?]) => _painter;
}
class _CirclePainter extends BoxPainter {
final Paint _paint;
final double radius;
_CirclePainter(Color color, this.radius)
: _paint = Paint()
..color = color
..isAntiAlias = true;
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
final Offset circleOffset =
offset + Offset(cfg.size!.width / 2, cfg.size!.height - radius);
canvas.drawCircle(circleOffset, radius, _paint);
}
}
class WidgetSize extends StatefulWidget {
final Widget child;
final Function onChange;
const WidgetSize({
required this.onChange,
required this.child,
});
#override
_WidgetSizeState createState() => _WidgetSizeState();
}
class _WidgetSizeState extends State<WidgetSize> {
#override
Widget build(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
return Container(
key: widgetKey,
child: widget.child,
);
}
var widgetKey = GlobalKey();
var oldSize;
void postFrameCallback(_) {
var context = widgetKey.currentContext;
if (context == null) return;
var newSize = context.size;
if (oldSize == newSize) return;
oldSize = newSize;
widget.onChange(newSize);
}
}
ALL VERIFIED USERS
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutterweb/controllers/channel_controller.dart';
import 'package:flutterweb/controllers/user_controller.dart';
import 'package:flutterweb/main.dart';
import 'package:flutterweb/models/user_model.dart';
import 'package:flutterweb/thirdParty/googleSignin.dart';
import 'package:flutterweb/utils/functions.dart';
import 'package:flutterweb/views/menu/permissions/menu_assign.dart';
import 'package:flutterweb/widgets/builders/kPopups.dart';
import 'package:flutterweb/widgets/buttons/KIconOnlyButton.dart';
import 'package:flutterweb/widgets/builders/KNetworkFadeImage.dart';
import 'package:flutterweb/widgets/builders/customAppBar.dart';
import 'package:flutterweb/widgets/buttons/KButton.dart';
import 'package:flutterweb/constants.dart';
import 'package:flutterweb/widgets/cards/KStudentInfoCard.dart';
import 'package:flutterweb/widgets/cards/kStudentCard.dart';
import 'package:flutterweb/widgets/input/KInputBar.dart';
import 'package:flutterweb/widgets/input/KTextField.dart';
import 'package:flutterweb/widgets/static/kLabel.dart';
import 'package:flutterweb/widgets/text/kInfo.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class AllVerifiedUsers extends StatefulWidget {
static const String router = "/allVerifiedUsers";
AllVerifiedUsers({Key? key}) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
// The controller for the ListView
late ScrollController _controller;
class _MyPageState extends State<AllVerifiedUsers> {
// The controller for the ListView
late ScrollController _controllerTest;
int _page = 1;
final int _limit = 20;
bool _hasNextPage = true;
bool _isFirstLoadRunning = false;
bool _isLoadMoreRunning = false;
List<UserModel> _posts = [];
String searchVal = "";
void _firstLoad() async {
setState(() {
_isFirstLoadRunning = true;
});
try {
List<UserModel> lUserMode = await ChannelController.to
.fetchUsersChannels(_limit, _page, searchVal);
setState(() {
_posts = lUserMode;
});
} catch (err) {
kPrint('Something went wrong');
}
setState(() {
_isFirstLoadRunning = false;
});
}
void _loadMore() async {
if (_isFirstLoadRunning == false &&
_isLoadMoreRunning == false &&
_controller.position.extentAfter < 300) {
setState(() {
_isLoadMoreRunning = true; // Display a progress indicator at the bottom
});
_page += 1; // Increase _page by 1
try {
List<UserModel> lUserMode = await ChannelController.to
.fetchUsersChannels(_limit, _page, searchVal);
if (lUserMode.isNotEmpty) {
setState(() {
_hasNextPage = true;
_posts.addAll(lUserMode);
});
} else {
// This means there is no more data
// and therefore, we will not send another GET request
setState(() {
_hasNextPage = false;
});
}
} catch (err) {
print('Something went wrong!');
}
setState(() {
_isLoadMoreRunning = false;
});
}
}
#override
void initState() {
super.initState();
_firstLoad();
_controller = ScrollController()..addListener(_loadMore);
_controllerTest = ScrollController()
..addListener(() => {kPrint("CustomView Scroll")});
}
#override
void dispose() {
super.dispose();
_controller.removeListener(_loadMore);
}
#override
Widget build(BuildContext context) {
global_title = "All Verified Users";
return CustomAppBar(
title: "All Verified Users",
scrollController: _controllerTest,
childrenFixed: [
kAddSpace(2),
CustomInputBar(
inverse: true,
title: "Search",
onChanged: (String value) {
if (value == "") {
_firstLoad();
return;
}
setState(() {
searchVal = value;
_posts = [];
});
_loadMore();
},
),
],
children: [
kAddSpace(2),
KLabel(
label: "Choose Verified User",
),
kAddSpace(2),
_isFirstLoadRunning
? const Center(
child: CircularProgressIndicator(),
)
: Column(
children: [
SizedBox(
height: app_content_height,
// width: Get.width,
child: ListView.builder(
shrinkWrap: true,
controller: _controller,
itemCount: _posts.length,
itemBuilder: (_, index) {
UserModel item = _posts[index];
return KStudentCard(
imgUrl: "",
onPressed: () {
ChannelController.to.gSelectedMenuUserModel.value =
item;
Get.toNamed(MenuAssign.router);
},
name: "${item.name} ${item.surname}",
);
},
),
),
// when the _loadMore function is running
if (_isLoadMoreRunning == true)
const Padding(
padding: EdgeInsets.only(top: 10, bottom: 40),
child: Center(
child: CircularProgressIndicator(),
),
),
// When nothing else to load
if (_hasNextPage == false)
Container(
padding: const EdgeInsets.only(top: 30, bottom: 40),
color: Colors.amber,
child: const Center(
child: Text('You have fetched all of the content'),
),
),
],
),
kAddSpace(2),
],
);
}
}
Thank you
As I understand it, the idea is a fixed TextArea and a scrollable List beneath. A solution that works without computing any height would be:
final items = List<String>.generate(1000, (i) => 'Item $i');
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
//
// TEXTBOX
//
Container(
color: Colors.red,
child: Padding(
padding: const EdgeInsets.all(16),
child: TextFormField(
// controller: controller,
))),
const SizedBox(height: 16),
//
// LIST
//
Expanded(
child: ListView.builder(
shrinkWrap: true, // IMPORTANT
itemCount: items.length,
itemBuilder: (context, index) {
return Text(items[index]);
})),
]),
);
}
I'm trying to make a reorderable list of custom widgets, so I use a UniqueKey() in their constructors (seeing as there's not really anything else to differentiate them by). However when I go to create another element, I get the Multiple widgets use the same GlobalKey assertion. I took a look at the Widget Inspector and both of the widgets have the UniqueKey but somehow also a key of null? I suspect this is the origin of the issue but I can't figure out how to solve it.
I have a few other properties in the constructor but all of them are late and can't be used in the constructor of a ValueKey.
My code:
section.dart:
class Section extends StatefulWidget {
final String title;
late int index;
bool selected = false;
Section ({required this.title, required this.index});
Key key = UniqueKey();
#override
SectionState createState() => SectionState();
void deselect() {
this.selected = false;
}
}
main.dart:
class WritingPageState extends State<WritingPage> {
List<Section> sections = [Section(index: 0, title: "First Section")];
ValueNotifier<int> selectedIndex = ValueNotifier(-1);
int newKeyConcatter = 1;
Widget build(BuildContext context) {
Widget addSectionButton = Padding(
key: Key("__ADD_SECTION_BUTTON_KEY__"),
padding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
child: InkWell(
onTap: () => setState(() {
sections.add(Section(index: sections.length, title: "New Section $newKeyConcatter",));
newKeyConcatter++;
}),
child: Container(
height: 90,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).primaryColor, width: 2),
//color: Theme.of(context).primaryColorLight
),
child: Center(
child: Icon(Icons.add, color: Theme.of(context).primaryColor, size: 40,),
),
),
),
);
List<Widget> sectionsToBeDisplayed = [];
for (Widget actualSection in sections) {
sectionsToBeDisplayed.add(actualSection);
sectionsToBeDisplayed.add(addSectionButton);
}
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(32.0),
child: Row(
children: [
Flexible(
flex: 1,
child: NotificationListener<SectionSelectedNotification> (
onNotification: (notif) {
setState(() {
for (Section s in sections) {
if (s.index != notif.index) {s.deselect();}
}
});
return true;
},
child: ReorderableListView(
buildDefaultDragHandles: false,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final Section item = sections.removeAt(oldIndex);
sections.insert(newIndex, item);
for (int i = 0; i < sections.length; i++) {
sections[i].index = i;
}
});
},
children: sectionsToBeDisplayed,
),
),
),
VerticalDivider(
indent: 20,
endIndent: 20,
color: Colors.grey,
width: 20,
),
Flexible(
flex: 2,
child: Card(
color: Theme.of(context).primaryColorLight,
child: Center(
child: ValueListenableBuilder(
valueListenable: selectedIndex,
builder: (BuildContext context, int newSelected, Widget? child) {
if ((newSelected < 0 && newSelected != -1) || newSelected > sections.length) {
return Text("""An internal error has occured. Namely, newSelected is $newSelected,
which is something that normally shouldn't happen.""");
} else if (newSelected == -1) {
return Text("Select a section to begin writing.");
}
return TextField(
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
hintText: "Once upon a time...",
),
);
},
)
)
)
)
],
)
),
);
}
}
You have several issues with your approach.
First - as I mentioned in my comment - you are not assigning the key properly. Instead of passing the constructor argument 'key', you actually did override the key property with your own.
Additionally, in this code you are adding your addSectionButton multiple times - and each time it has the same Key - which will cause a problem:
List<Widget> sectionsToBeDisplayed = [];
for (Widget actualSection in sections) {
sectionsToBeDisplayed.add(actualSection);
sectionsToBeDisplayed.add(addSectionButton);
}
But the main problem is - your approach is wrong in several ways.
You are trying to maintain list of Widgets to be reordered. You should be reordering the data. Let the widgets rebuild on their own.
Your StatefullWidget is tracking if it is selected or not, and you try to sync all the widgets once the selection changes. Instead, you should - again - be focusing on your data. Let the widgets rebuild on their own.
Here's the solution that works - you can try it in DartPad.
You will see key changes:
-I introduced a List to track your ListTile titles and the text you edit
-New variable to track the selected list item
-onReorder is almost exactly the same as Flutter doc - I only added few lines to track the selected item
-everything is in a single widget. You could still extract your Section widget (and pass a callback function for it to post changes back) - but your Section widget should be stateless.
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final _items=<String> ["First Section"];
final _itemsText=<String> ["First Section Text"];
int _selectedIndex=-1;
void _setSelectedIndex(int? value) {
setState(() {
_selectedIndex=value ?? -1;
});
}
#override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
final Color evenItemColor = colorScheme.primary.withOpacity(0.15);
Widget addSectionButton = Padding(
key: const Key("__ADD_SECTION_BUTTON_KEY__"),
padding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
child: InkWell(
onTap: () => setState(() {
_items.add("New Section ${_items.length+1}");
_itemsText.add("");
}),
child: Container(
height: 90,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).primaryColor, width: 2),
),
child: Center(
child: Icon(Icons.add, color: Theme.of(context).primaryColor, size: 40,),
),
),
),
);
Widget editor;
if ((_selectedIndex < 0 && _selectedIndex != -1) || _selectedIndex > _items.length) {
editor=Text("""An internal error has occured. Namely, newSelected is $_selectedIndex,
which is something that normally shouldn't happen.""");
} else if (_selectedIndex == -1) {
editor=const Text("Select a section to begin writing.");
} else {
TextEditingController _controller=TextEditingController(text: _itemsText[_selectedIndex]);
editor=TextField(
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
hintText: "Once upon a time...",
),
controller: _controller,
onSubmitted: (String value) {
_itemsText[_selectedIndex]=value;
}
);
}
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(32.0),
child: Row(
children: [
Flexible(
flex: 1,
child:
ReorderableListView(
buildDefaultDragHandles: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final String item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
final String itemText = _itemsText.removeAt(oldIndex);
_itemsText.insert(newIndex, itemText);
if (_selectedIndex==oldIndex) {
_selectedIndex=newIndex;
}
});
},
children: <Widget>[
for (int index = 0; index < _items.length; index++)
RadioListTile(
key: Key('$index'),
value: index,
groupValue: _selectedIndex,
tileColor: index.isOdd ? oddItemColor : evenItemColor,
title: Text(_items[index]),
onChanged: _setSelectedIndex
),
addSectionButton
],
),
//),
),
const VerticalDivider(
indent: 20,
endIndent: 20,
color: Colors.grey,
width: 20,
),
Flexible(
flex: 2,
child: Card(
color: Theme.of(context).primaryColorLight,
child: Center(
child: editor
)
)
)
],
)
),
);
}
}
I have been using Stepper view by flutter.And I am having an issue on getting value from child to parent on button click as the button is in parent widget.
Here is my Parent class and my child class.
Parent Class
This is my parent class which has a Stepper view with next and back button.I want to get value from my child class to parent class when next button is clicked.
class DeliveryTimeline extends StatefulWidget {
DeliveryTimeline({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<DeliveryTimeline> {
int _currentStep = 0;
String shippingtype;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
centerTitle: true,
iconTheme: new IconThemeData(color: Colors.black),
elevation: 0,
title: Text(
"Checkout",
style: TextStyle(color: Colors.black),
),
),
body: Stepper(
type: StepperType.horizontal,
steps: _mySteps(),
currentStep: this._currentStep,
onStepTapped: (step) {
setState(() {
this._currentStep = step;
});
},
onStepContinue: () {
setState(() {
if (this._currentStep == 0) {
this._currentStep = this._currentStep + 1;
**//need to get value here on first next click**
} else if (this._currentStep == 1) {
this._currentStep = this._currentStep + 1;
} else {
print('Completed, check fields.');
}
});
},
onStepCancel: () {
setState(() {
if (this._currentStep > 0) {
this._currentStep = this._currentStep - 1;
} else {
this._currentStep = 0;
}
});
},
controlsBuilder: (BuildContext context,
{VoidCallback onStepContinue,
VoidCallback onStepCancel,
Function onShippingNextClick}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
OutlineButton(
child: new Text("Back"),
onPressed: onStepCancel,
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(4.0))),
MaterialButton(
child: Text("Next"),
color: AppColors.primarycolor,
textColor: Colors.white,
onPressed: onStepContinue,
),
],
);
}));
}
List<Step> _mySteps() {
List<Step> _steps = [
Step(
title: Text('Delivery'),
content: Center(
child: Container(
height: MediaQuery.of(context).size.height / 1.5,
child: Delivery(onShipingTypeClicked: (shippingtype){
shippingtype = shippingtype;
print("myvalue${shippingtype}");
},),
),
),
isActive: _currentStep >= 0,
),
Step(
title: Text('Address'),
content: Address(),
isActive: _currentStep >= 1,
),
Step(
title: Text('Payment'),
content: Payment(),
isActive: _currentStep >= 2,
)
];
return _steps;
}
}
Child Class
This is my child class i have a Listview which act like a radio button.I want the selected item and its value to the parent class when button is clicked..
class Delivery extends StatefulWidget {
final ValueChanged<String> onShipingTypeClicked;
Delivery({this.onShipingTypeClicked});
#override
_DeliveryState createState() => _DeliveryState();
}
class _DeliveryState extends State<Delivery> {
List<RadioModel> sampleData = new List<RadioModel>();
#override
void initState() {
// TODO: implement initState
super.initState();
sampleData.add(new RadioModel(false, 'A', 0xffe6194B, "Standard Delivery",
"Order will be delivered between 3 - 5 business days", 1));
sampleData.add(new RadioModel(
true,
'A',
0xffe6194B,
"Next Day Delivery",
"Place your order before 6pm and your items will be delivered the next day",
2));
sampleData.add(new RadioModel(
false,
'A',
0xffe6194B,
"Nominated Delivery",
"Pick a particular date from the calendar and order will be delivered on selected date",
3));
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new ListView.builder(
itemCount: sampleData.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return new InkWell(
onTap: () {
setState(() {
sampleData.forEach((element) => element.isSelected = false);
sampleData[index].isSelected = true;
widget.onShipingTypeClicked(sampleData[index].buttonText);
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextSmallTitleSize(
title: sampleData[index].title,
),
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Flexible(
child: TextSmallDimText(sampleData[index].label),
),
RadioItem(sampleData[index]),
],
),
)
],
));
},
),
);
}
}
class RadioItem extends StatelessWidget {
final RadioModel _item;
RadioItem(this._item);
#override
Widget build(BuildContext context) {
return new Container(
margin: new EdgeInsets.all(15.0),
child: new Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
new Container(
height: 25.0,
width: 25.0,
alignment: Alignment.center,
child: Container(
height: 15.0,
width: 15.0,
decoration: new BoxDecoration(
color: AppColors.primarycolor,
borderRadius:
const BorderRadius.all(const Radius.circular(15)),
)),
decoration: new BoxDecoration(
color: Colors.transparent,
border: new Border.all(
width: 3.0,
color: _item.isSelected
? AppColors.primarycolor
: Colors.transparent),
borderRadius: const BorderRadius.all(const Radius.circular(25)),
),
),
new Container(margin: new EdgeInsets.only(left: 10.0))
],
),
);
}
}
class RadioModel {
bool isSelected;
final String buttonText;
final int colorCode;
final String title, label;
final int buttonid;
RadioModel(this.isSelected, this.buttonText, this.colorCode, this.title,
this.label, this.buttonid);
}
You can copy paste run full code below
You can use GlobalKey to get deliveryState of Delivery, and use deliveryState to get attribute of Delivery, attribute here is RadioModel selected
Step 1: Use GlobalKey _key = GlobalKey();
class _MyHomePageState extends State<DeliveryTimeline> {
...
GlobalKey _key = GlobalKey();
Step 2: Use deliveryState to get seleted item
onStepContinue: () {
setState(() {
if (this._currentStep == 0) {
this._currentStep = this._currentStep + 1;
final _DeliveryState deliveryState =
_key.currentState;
print("hi ${deliveryState.selected.title} ${deliveryState.selected.label} ");
Step 3: Delivery need key
child: Delivery(
key: _key,
onShipingTypeClicked: (shippingtype) {
...
Delivery({Key key, this.onShipingTypeClicked}) : super(key:key);
Step 4: Set variable selected
RadioModel selected = null;
...
return InkWell(
onTap: () {
setState(() {
...
selected = sampleData[index];
working demo
output of working demo
I/flutter ( 6246): hi Standard Delivery Order will be delivered between 3 - 5 business days
full code
import 'package:flutter/material.dart';
class DeliveryTimeline extends StatefulWidget {
DeliveryTimeline({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<DeliveryTimeline> {
int _currentStep = 0;
String shippingtype;
GlobalKey _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
centerTitle: true,
iconTheme: IconThemeData(color: Colors.black),
elevation: 0,
title: Text(
"Checkout",
style: TextStyle(color: Colors.black),
),
),
body: Stepper(
type: StepperType.horizontal,
steps: _mySteps(),
currentStep: this._currentStep,
onStepTapped: (step) {
setState(() {
this._currentStep = step;
});
},
onStepContinue: () {
setState(() {
if (this._currentStep == 0) {
this._currentStep = this._currentStep + 1;
final _DeliveryState deliveryState =
_key.currentState;
print("hi ${deliveryState.selected.title} ${deliveryState.selected.label} ");
} else if (this._currentStep == 1) {
this._currentStep = this._currentStep + 1;
} else {
print('Completed, check fields.');
}
});
},
onStepCancel: () {
setState(() {
if (this._currentStep > 0) {
this._currentStep = this._currentStep - 1;
} else {
this._currentStep = 0;
}
});
},
controlsBuilder: (BuildContext context,
{VoidCallback onStepContinue,
VoidCallback onStepCancel,
Function onShippingNextClick}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
OutlineButton(
child: Text("Back"),
onPressed: onStepCancel,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.0))),
MaterialButton(
child: Text("Next"),
color: Colors.blue,
textColor: Colors.white,
onPressed: onStepContinue,
),
],
);
}));
}
List<Step> _mySteps() {
List<Step> _steps = [
Step(
title: Text('Delivery'),
content: Center(
child: Container(
height: MediaQuery.of(context).size.height / 1.5,
child: Delivery(
key: _key,
onShipingTypeClicked: (shippingtype) {
shippingtype = shippingtype;
print("myvalue${shippingtype}");
},
),
),
),
isActive: _currentStep >= 0,
),
Step(
title: Text('Address'),
content: Text("Address()"),
isActive: _currentStep >= 1,
),
Step(
title: Text('Payment'),
content: Text("Payment()"),
isActive: _currentStep >= 2,
)
];
return _steps;
}
}
class Delivery extends StatefulWidget {
final ValueChanged<String> onShipingTypeClicked;
Delivery({Key key, this.onShipingTypeClicked}) : super(key:key);
#override
_DeliveryState createState() => _DeliveryState();
}
class _DeliveryState extends State<Delivery> {
List<RadioModel> sampleData = List<RadioModel>();
#override
void initState() {
// TODO: implement initState
super.initState();
sampleData.add(RadioModel(false, 'A', 0xffe6194B, "Standard Delivery",
"Order will be delivered between 3 - 5 business days", 1));
sampleData.add(RadioModel(
true,
'A',
0xffe6194B,
"Next Day Delivery",
"Place your order before 6pm and your items will be delivered the next day",
2));
sampleData.add(RadioModel(
false,
'A',
0xffe6194B,
"Nominated Delivery",
"Pick a particular date from the calendar and order will be delivered on selected date",
3));
}
RadioModel selected = null;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: sampleData.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
setState(() {
sampleData.forEach((element) => element.isSelected = false);
sampleData[index].isSelected = true;
selected = sampleData[index];
widget.onShipingTypeClicked(sampleData[index].buttonText);
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ListTile(
title: Text(sampleData[index].title),
),
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Flexible(
child: Text(sampleData[index].label),
),
RadioItem(sampleData[index]),
],
),
)
],
));
},
),
);
}
}
class RadioItem extends StatelessWidget {
final RadioModel _item;
RadioItem(this._item);
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(15.0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
height: 25.0,
width: 25.0,
alignment: Alignment.center,
child: Container(
height: 15.0,
width: 15.0,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius:
const BorderRadius.all(const Radius.circular(15)),
)),
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.all(
width: 3.0,
color: _item.isSelected ? Colors.blue : Colors.transparent),
borderRadius: const BorderRadius.all(const Radius.circular(25)),
),
),
Container(margin: EdgeInsets.only(left: 10.0))
],
),
);
}
}
class RadioModel {
bool isSelected;
final String buttonText;
final int colorCode;
final String title, label;
final int buttonid;
RadioModel(this.isSelected, this.buttonText, this.colorCode, this.title,
this.label, this.buttonid);
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: DeliveryTimeline(),
);
}
}