Related
I a beginner in Flutter development and I was trying to replicate an animated login screen from this LinkedIn post. I am using Rive animations to make all the states required for the screen possible. A base rive file was available for the screen and after some tweaks, I was able to get the required states.
After implementing the animations in the screen along with two text fields, I noticed an issue with the animations. Whenever I click on the TextField, the soft keyboard pops up. The required animation runs for a short duration then resets to the idle animation. After digging through stack overflow and google and printing debug messages, I noticed that the whole widget rebuilds itself whenever soft keyboard pops up.
Here's the code for my screen -
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
#override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
StateMachineController? _controller;
SMIInput<bool>? isChecking;
SMIInput<bool>? trigSuccess;
SMIInput<bool>? trigFail;
SMIInput<bool>? isHandsUp;
SMIInput<double>? numLook;
FocusNode emailFocusNode = FocusNode();
TextEditingController emailController = TextEditingController();
FocusNode passwordFocusNode = FocusNode();
TextEditingController passController = TextEditingController();
#override
void initState() {
super.initState();
emailFocusNode.addListener(emailFocus);
passwordFocusNode.addListener(passFocus);
}
#override
void dispose() {
super.dispose();
emailFocusNode.removeListener(emailFocus);
passwordFocusNode.removeListener(passFocus);
}
void emailFocus() {
isChecking?.change(emailFocusNode.hasFocus);
}
void passFocus() {
isHandsUp?.change(passwordFocusNode.hasFocus);
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Color(0xffeef2f3),
// Colors.lightBlueAccent.withBlue(255).withGreen(242).withRed(238),
body: SafeArea(
child: SingleChildScrollView(
child: Container(
width: size.width,
height: size.height,
child: Center(
child: Container(
height: 600,
width: 320,
decoration: BoxDecoration(
color: Color(0xFFffffff),
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 5,
),
],
),
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 25,
horizontal: 25,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CircleAvatar(
backgroundColor: Colors.black87,
radius: 90,
child: CircleAvatar(
backgroundColor: Colors.transparent,
radius: 88,
child: ClipOval(
child: RiveAnimation.asset(
'assets/login_screen_character.riv',
stateMachines: ["Login Machine"],
onInit: (Artboard artboard) {
_controller =
StateMachineController.fromArtboard(
artboard, "Login Machine");
if (_controller == null) return;
artboard.addController(_controller!);
isChecking =
_controller?.findInput("isChecking");
trigSuccess =
_controller?.findInput("trigSuccess");
trigFail = _controller?.findInput("trigFail");
isHandsUp = _controller?.findInput("isHandsUp");
numLook = _controller?.findInput("numLook");
},
),
),
),
),
SizedBox(
height: 32,
),
Text(
"Email",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xff32779d),
),
),
SizedBox(
height: 4,
),
Container(
decoration: BoxDecoration(
color: Color(0x321fb0d2),
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: Color(0xff1fb0d2),
),
),
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"email#example.com",
style: TextStyle(
fontSize: 12,
color: Color(0xff32779d),
),
),
TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Enter email",
),
focusNode: emailFocusNode,
controller: emailController,
onChanged: (value) {
if (isChecking?.value == false)
isChecking?.change(true);
numLook?.change(value.length.toDouble() * 3);
},
),
],
),
),
),
SizedBox(
height: 32,
),
Text(
"Password",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xff32779d),
),
),
SizedBox(
height: 2,
),
Container(
decoration: BoxDecoration(
color: Color(0x321fb0d2),
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: Color(0xff1fb0d2),
),
),
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 4,
bottom: 4,
),
child: TextField(
onChanged: (value) {
if (isHandsUp?.value == false)
isHandsUp?.change(true);
},
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Enter password",
),
focusNode: passwordFocusNode,
controller: passController,
),
),
),
SizedBox(
height: 32,
),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Color(0xff1fb0d2),
),
),
onPressed: () {
if (isHandsUp?.value == true)
isHandsUp?.change(false);
Future.delayed(Duration(milliseconds: 1200), () {
if (passController.text == "thisisatest")
trigSuccess?.change(true);
else
trigFail?.change(true);
});
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"Log In",
style: TextStyle(
color: Colors.white,
),
),
),
)
],
),
),
),
),
),
),
),
);
}
}
The code is a bit too long, please excuse me for that I haven't really refactored the code.
I've tried using the resizeToAvoidBottomInset: false but animation still resets to the idle state. I've even tried adding the android:windowSoftInputMode="adjustNothing" inside the <activity...> tag but still no result.
I would really like to know if there's any workaround in my code to make the animation states run properly.
Hello guys i have two stateful widget InactiveCustomersListView and MessageCustomerHeader
InactiveCustomersListView has a List of tiles, and these tiles have checkboxes
class InactiveCustomersListView extends StatefulWidget {
final bool checkAll;
const InactiveCustomersListView({
Key key,
this.checkAll,
}) : super(key: key);
#override
State<InactiveCustomersListView> createState() =>
_InactiveCustomersListViewState();
}
class _InactiveCustomersListViewState extends State<InactiveCustomersListView> {
bool checkAll;
List<CheckBoxModel> listOfCustomers = [];
#override
void initState() {
listOfCustomers.addAll({
CheckBoxModel(selected: false),
});
super.initState();
}
#override
Widget build(BuildContext context) {
final controller = Get.put(CustomersController());
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: listOfCustomers.length,
itemBuilder: (context, index) {
return Obx(() {
return Container(
color: Color(0xffFFFFFF),
child: ListTile(
contentPadding: controller.isCheboxVisible.isTrue
? EdgeInsets.only(left: 0, right: 22, top: 10, bottom: 10)
: EdgeInsets.only(left: 22, right: 22, top: 10, bottom: 10),
horizontalTitleGap: 5,
leading: controller.isCheboxVisible.isTrue
? Visibility(
visible: controller.isCheboxVisible.value,
child: Transform.scale(
scale: 1.3,
child: Checkbox(
side: MaterialStateBorderSide.resolveWith(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const BorderSide(
width: 2, color: Color(0xff34495E));
}
return const BorderSide(
width: 1, color: Color(0xffB0BEC1));
},
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
activeColor: Color(0xff34495E),
//materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
//visualDensity: VisualDensity(horizontal: -4, vertical: -4),
value: listOfCustomers[index].selected,
onChanged: (value) {
setState(() {
listOfCustomers[index].selected = value;
final check = listOfCustomers
.every((element) => element.selected);
checkAll = check;
});
},
),
),
)
: null,
title: Row(
children: [
SizedBox(
width: 60,
height: 60,
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(80)),
child: CachedNetworkImage(
height: 100,
width: double.infinity,
fit: BoxFit.cover,
imageUrl:
Get.find<AuthService>().user.value.avatar.thumb,
placeholder: (context, url) => Image.asset(
'assets/img/loading.gif',
fit: BoxFit.cover,
width: double.infinity,
height: 80,
),
errorWidget: (context, url, error) =>
Icon(Icons.error_outline),
),
),
),
SizedBox(
width: 10,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'John Cletus',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xff151515)),
),
Container(
decoration: BoxDecoration(
color: Color(0xffECF0F1),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.only(
left: 10.0,
right: 10.0,
top: 5.0,
bottom: 5.0),
child: Text(
'Inactive',
style: GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w400,
color: Color(0xff7F8D90)),
),
),
),
],
),
SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
'2',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xff151515)),
),
SizedBox(
width: 5,
),
Text(
'jobs',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Color(0xff151515)),
),
],
),
Text(
'Last job 5 days ago',
style: GoogleFonts.poppins(
fontSize: 9,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w400,
color: Color(0xff151515)),
),
],
),
],
),
),
],
),
),
);
});
});
}
}
class CheckBoxModel {
bool selected;
CheckBoxModel({this.selected});
}
MessageCustomerHeader widget class has a checkbox that controls the Check boxes in the InactiveCustomersListView widget class:
class MessageCustomerHeader extends StatefulWidget {
final List<CheckBoxModel> listOfCustomers;
const MessageCustomerHeader({
Key key,
this.listOfCustomers,
}) : super(key: key);
#override
State<MessageCustomerHeader> createState() => _MessageCustomerHeaderState();
}
class _MessageCustomerHeaderState extends State<MessageCustomerHeader> {
bool checkAll = false;
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select customers you woult like to message',
style: GoogleFonts.poppins(
color: Color(0xff596780).withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500),
),
SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Transform.scale(
scale: 1.3,
child: Checkbox(
side: MaterialStateBorderSide.resolveWith(
(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const BorderSide(
width: 2, color: Color(0xff34495E));
}
return const BorderSide(
width: 1, color: Color(0xffB0BEC1));
},
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
activeColor: Color(0xff34495E),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
value: checkAll,
onChanged: (value) {
setState(() {
checkAll = value;
widget.listOfCustomers.forEach((element) {
element.selected = value;
});
});
},
),
),
SizedBox(
width: 10,
),
Text(
'Select all',
style: GoogleFonts.poppins(
color: Colors.black87,
fontSize: 12,
fontWeight: FontWeight.w400),
),
],
),
],
),
),
);
}
}
I tried using constructor to pass "listOfCustomers" from InactiveCustomersListView widget to MessageCustomerHeader widget
And also passing "checkAll" from MessageCustomerHeader to InactiveCustomersListView widget by also using constructors.
The problem is having is that when i click on the checkbox in MessageCustomerHeader widget to check all the check boxes in InactiveCustomersListView widget, the state don't seem to change and it doesn't even after setting all the logic. What am i doing wrong guys and how do i resolve this issue :(
This is the error i get when i tap the checkbox on the MessageCustomerHeader checkbox.
"The method 'forEach' was called on null.
Receiver: null
Tried calling: forEach(Closure: (CheckBoxModel) => Null)"
Thank you.
If you have the proper lints enabled you would have seen this warning: DON'T put any logic in createState().
From the associated description:
Implementations of createState() should return a new instance of a State object and do nothing more. Since state access is preferred via the widget field, passing data to State objects using custom constructor parameters should also be avoided and so further, the State constructor is required to be passed no arguments.
(emphasis added)
You are passing arguments to your State constructors. Instead, use widget.checkAll and widget.listOfCustomers to access widget properties from the state objects.
Also:
It would be better to specify a type for checkAll. In your code you just have final checkAll;.
It is difficult to read the screen shot of the error message. It would be better to include the error message as text in the question, along with an indication in your code of the line to which the error message is referring.
I have two widgets
JobsHeaderWidget
JobsView
JobsHeaderWidget is a stateful widget where i code all the logic and initialise int current = 0; in the state. In this same file, i have another class named CategoriesBuilder where i use switch cases to make sure at each switch case a different container is returned. ( a switch case for each tab )
This switch cases is now responsible for switching containers depending on the tab bar selected as seen in this image:
I will also drop the code snippet of the JobsHeaderWidget for better clarifications.
The problem is - when i use this CategoriesBuilder in same widget as the 'JobsHeaderWidget' it works.
But i don't want to use it in same widget cos of the logic of my design. I want to be able to use this builder in JobsView widget which is another dart file and it doesn't work maybe because of wrong approach.
I tried converting the JobsView to a stateful widget and initialising 'int current = 0;' but it doesn't work.
I also tried making int current = 0; global var, it worked but the state doesn't change when i select individual tab bars. ( I mean my switch cases don't seem to work ).
I have gone round stackoverflow for answers before asking this but can't find a solution.
Snippets of each widgets below.
JobsHeaderWidget
class JobsHeaderWidget extends StatefulWidget {
const JobsHeaderWidget({
Key key,
}) : super(key: key);
#override
State<JobsHeaderWidget> createState() => _JobsHeaderWidgetState();
}
class _JobsHeaderWidgetState extends State<JobsHeaderWidget> {
List<String> items = [
"All",
"Critical",
"Open",
"Closed",
"Overdue",
];
ValueChanged<int> onChange;
int current = 0;
List<DropdownMenuItem<String>> get dropdownItems {
List<DropdownMenuItem<String>> menuItems = [
DropdownMenuItem(
child: Text(
"Today",
),
value: "Today"),
];
return menuItems;
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 15.0, right: 15.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jobs',
style: GoogleFonts.poppins(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.w600),
),
Row(
children: [
Text(
'View Insights ',
style: GoogleFonts.poppins(
color: Color(0xff3498DB),
fontSize: 12,
fontWeight: FontWeight.w500),
),
Icon(
Icons.arrow_forward_ios,
color: Color(0xff3498DB),
size: 12,
),
],
),
SizedBox(
height: 10,
),
filterJobs(),
],
),
),
);
}
Widget filterJobs() {
String selectedValue = "Today";
return Column(
children: [
Container(
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 100),
width: double.infinity,
child: IntrinsicWidth(
child: FittedBox(
fit: BoxFit.fitWidth,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (int i = 0; i < items.length; i++) ...[
GestureDetector(
onTap: () {
setState(() {
current = i;
});
},
child: AnimatedContainer(
height: 40,
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.all(5),
padding: const EdgeInsets.only(
left: 14.0, right: 14.0, top: 4, bottom: 4),
decoration: BoxDecoration(
color: current == i
? const Color(0xff34495E)
: const Color(0xffF5F5F5),
borderRadius: BorderRadius.circular(50),
),
child: Center(
child: Text(
items[i],
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w500,
color:
current == i ? Colors.white : Colors.grey),
),
),
),
),
]
],
),
),
),
),
Divider(
color: Color(0xff34495E).withOpacity(0.2),
),
Row(
children: [
Text(
'All Jobs',
style:
GoogleFonts.poppins(fontSize: 9, fontWeight: FontWeight.w400),
),
SizedBox(
width: 5,
),
Text(
' * This Week',
style:
GoogleFonts.poppins(fontSize: 9, fontWeight: FontWeight.w400),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'25',
style: GoogleFonts.poppins(
fontSize: 20, fontWeight: FontWeight.w600),
),
Container(
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: Color(0xffF4F4F4)),
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
),
child: Row(
children: [
Container(
decoration: BoxDecoration(
color: Color(0xff34495E),
borderRadius: BorderRadius.circular(2)),
child: Icon(
Icons.tune,
size: 15,
color: Colors.white,
),
),
SizedBox(
width: 5,
),
DropdownMenuItem(
child: DropdownButtonHideUnderline(
child: Container(
child: DropdownButton(
isDense: true,
style: GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color(0xff34495E),
),
onChanged: (value) {},
items: dropdownItems,
value: selectedValue,
),
),
),
),
],
),
),
),
],
),
//If i uncomment this line and use the category builder here, it works fine! CategoriesBuilder(current: current)
],
);
}
}
class CategoriesBuilder extends StatelessWidget {
const CategoriesBuilder({
Key key,
#required this.current,
}) : super(key: key);
final int current;
#override
Widget build(BuildContext context) {
return Builder(
builder: (context) {
switch (current) {
case 0:
return AllJobsListView();
case 1:
return CriticalJobsListView();
case 2:
return OpenJobsListView();
case 3:
return ClosedJobsListView();
case 4:
return OverdueJobsListView();
default:
return SizedBox.shrink();
}
},
);
}
}
JobsView
class JobsView extends StatefulWidget {
const JobsView({
Key key,
}) : super(key: key);
#override
State<JobsView> createState() => _JobsViewState();
}
class _JobsViewState extends State<JobsView> {
int current = 0;
#override
Widget build(BuildContext context) {
final controller = Get.put(EServicesController());
return Scaffold(
// floatingActionButton: new FloatingActionButton(
// child: new Icon(Icons.add, size: 32, color: Get.theme.primaryColor),
// onPressed: () => {Get.offAndToNamed(Routes.E_SERVICE_FORM)},
// backgroundColor: Get.theme.colorScheme.secondary,
// ),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
body: RefreshIndicator(
onRefresh: () async {
Get.find<LaravelApiClient>().forceRefresh();
controller.refreshEServices(showMessage: true);
Get.find<LaravelApiClient>().unForceRefresh();
},
child: CustomScrollView(
controller: controller.scrollController,
physics: AlwaysScrollableScrollPhysics(),
shrinkWrap: false,
slivers: <Widget>[
SliverAppBar(
backgroundColor: Color(0xffFFFFFF),
expandedHeight: MediaQuery.of(context).size.height * 0.4,
elevation: 0.5,
primary: true,
pinned: false,
floating: false,
//iconTheme: IconThemeData(color: Get.theme.primaryColor),
// title: Text(
// "Jobs".tr,
// style: Get.textTheme.headline6
// .merge(TextStyle(color: Get.theme.primaryColor)),
// ),
centerTitle: false,
automaticallyImplyLeading: false,
// leading: new IconButton(
// icon: new Icon(Icons.arrow_back_ios,
// color: Get.theme.primaryColor),
// onPressed: () => {Get.back()},
// ),
actions: [
SearchButtonWidget(),
],
//bottom: HomeSearchBarWidget(),
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
title: JobsHeaderWidget(),
)),
SliverToBoxAdapter(
child: Wrap(
children: [
//ServicesListWidget(),
// The state doesnt change here for some reasosns CategoriesBuilder(current: current)
],
),
),
],
),
),
);
}
}
Try this: keep your 'int current' within _JobsViewState as you have in your code.
When you call your JobsHeaderWidget , pass the function it will use to update the the value of the current variable; and rebuild the state from here.
Something like this:
class JobsHeaderWidget extends StatefulWidget {
final Function changeCurrentValue(int newValue);
const JobsHeaderWidget({
this.changeCurrentValue,
Key key,
}) : super(key: key);
#override
State<JobsHeaderWidget> createState() => _JobsHeaderWidgetState();
}
class _JobsHeaderWidgetState extends State<JobsHeaderWidget> {
#override
Widget build(BuildContext context) {
// Somewhere inside build, instead calling setState()
// call the function you passed to the widget
GestureDetector(
onTap: () {
changeCurrentValue(i);
},
)
}
}
class _JobsViewState extends State<JobsView> {
int current = 0;
void changeCurrentValue(int newValue) {
setState(() {
current = newValue;
});
}
#override
Widget build(BuildContext context) {
//somewhere inside build
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
title: JobsHeaderWidget(changeCurrentValue: changeCurrentValue),
)),
}
}
After hours and even sleeping overnight on this question i came up with a work-around that works. (minor refactoring)
This was my approach :
Convert my JobsView widget to a stateful widget and put all my controllers in place.
Copied all my variables from JobHeaderWidget and put it in the state of my JobsView widget.
Instead of returning a widget in the title of my sliver app as thus :
flexibleSpace: FlexibleSpaceBar( collapseMode: CollapseMode.parallax, title: JobsHeaderWidget(), )),
I copied all of my code from the widget tree from JobsHeaderWidget and put converted to a method and replaced it in my title.
My builder CategoryBuilder was put in a separate then imported as i used it in my SliverAppAdapter .
Of cos i got rid of the unnecessary dart file JobsHeaderWidget.
FULL CODE BELOW
class JobsView extends StatefulWidget {
#override
State<JobsView> createState() => _JobsViewState();
}
class _JobsViewState extends State<JobsView> {
List<String> items = [
"All",
"Critical",
"Open",
"Closed",
"Overdue",
];
int current = 0;
List<DropdownMenuItem<String>> get dropdownItems {
List<DropdownMenuItem<String>> menuItems = [
DropdownMenuItem(
child: Text(
"Today",
),
value: "Today"),
];
return menuItems;
}
#override
Widget build(BuildContext context) {
final controller = Get.put(EServicesController());
return Scaffold(
// floatingActionButton: new FloatingActionButton(
// child: new Icon(Icons.add, size: 32, color: Get.theme.primaryColor),
// onPressed: () => {Get.offAndToNamed(Routes.E_SERVICE_FORM)},
// backgroundColor: Get.theme.colorScheme.secondary,
// ),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
body: RefreshIndicator(
onRefresh: () async {
Get.find<LaravelApiClient>().forceRefresh();
controller.refreshEServices(showMessage: true);
Get.find<LaravelApiClient>().unForceRefresh();
},
child: CustomScrollView(
controller: controller.scrollController,
physics: AlwaysScrollableScrollPhysics(),
shrinkWrap: false,
slivers: <Widget>[
SliverAppBar(
backgroundColor: Color(0xffFFFFFF),
expandedHeight: MediaQuery.of(context).size.height * 0.4,
elevation: 0.5,
primary: true,
pinned: false,
floating: false,
//iconTheme: IconThemeData(color: Get.theme.primaryColor),
// title: Text(
// "Jobs".tr,
// style: Get.textTheme.headline6
// .merge(TextStyle(color: Get.theme.primaryColor)),
// ),
centerTitle: false,
automaticallyImplyLeading: false,
// leading: new IconButton(
// icon: new Icon(Icons.arrow_back_ios,
// color: Get.theme.primaryColor),
// onPressed: () => {Get.back()},
// ),
actions: [
SearchButtonWidget(),
],
//bottom: HomeSearchBarWidget(),
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
title: mainHeader(),
)),
SliverToBoxAdapter(
child: Wrap(
children: [
//ServicesListWidget(),
CategoriesBuilder(current: current)
],
),
),
],
),
),
);
}
Padding mainHeader() {
return Padding(
padding: const EdgeInsets.only(left: 15.0, right: 15.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jobs',
style: GoogleFonts.poppins(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.w600),
),
Row(
children: [
Text(
'View Insights ',
style: GoogleFonts.poppins(
color: Color(0xff3498DB),
fontSize: 12,
fontWeight: FontWeight.w500),
),
Icon(
Icons.arrow_forward_ios,
color: Color(0xff3498DB),
size: 12,
),
],
),
SizedBox(
height: 10,
),
() {
String selectedValue = "Today";
return Column(
children: [
Container(
constraints:
const BoxConstraints(maxWidth: 600, maxHeight: 100),
width: double.infinity,
child: IntrinsicWidth(
child: FittedBox(
fit: BoxFit.fitWidth,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (int i = 0; i < items.length; i++) ...[
GestureDetector(
onTap: () {
setState(() {
current = i;
});
},
child: AnimatedContainer(
height: 40,
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.all(5),
padding: const EdgeInsets.only(
left: 14.0,
right: 14.0,
top: 4,
bottom: 4),
decoration: BoxDecoration(
color: current == i
? const Color(0xff34495E)
: const Color(0xffF5F5F5),
borderRadius: BorderRadius.circular(50),
),
child: Center(
child: Text(
items[i],
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w500,
color: current == i
? Colors.white
: Colors.grey),
),
),
),
),
]
],
),
),
),
),
Divider(
color: Color(0xff34495E).withOpacity(0.2),
),
Row(
children: [
Text(
'All Jobs',
style: GoogleFonts.poppins(
fontSize: 9, fontWeight: FontWeight.w400),
),
SizedBox(
width: 5,
),
Text(
' * This Week',
style: GoogleFonts.poppins(
fontSize: 9, fontWeight: FontWeight.w400),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'25',
style: GoogleFonts.poppins(
fontSize: 20, fontWeight: FontWeight.w600),
),
Container(
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: Color(0xffF4F4F4)),
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
),
child: Row(
children: [
Container(
decoration: BoxDecoration(
color: Color(0xff34495E),
borderRadius: BorderRadius.circular(2)),
child: Icon(
Icons.tune,
size: 15,
color: Colors.white,
),
),
SizedBox(
width: 5,
),
DropdownMenuItem(
child: DropdownButtonHideUnderline(
child: Container(
child: DropdownButton(
isDense: true,
style: GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Color(0xff34495E),
),
onChanged: (value) {},
items: dropdownItems,
value: selectedValue,
),
),
),
),
],
),
),
),
],
),
//CategoriesBuilder(current: current)
],
);
}(),
],
),
),
);
}
}
There is two files that have similar code, it is add and update area feature, that I decide to make it only one, with using condition.
You can see that two features have similar widgets, only different in texts.
Add Area Feature
Update Area Feature
In my case, when user want to adding area, the UI will display add area feature. When user want to updating area, the UI will display update area feature, but it only from one codebase.
Is it possible to make that kind of condition?
Here I copy paste the whole codes of add area feature. I don't have to copy paste update area because it has the similar codes.
class AddAreaItem extends StatefulWidget {
const AddAreaItem({super.key, this.isUpdate = false});
final bool isUpdate;
#override
State<AddAreaItem> createState() => _AddAreaItemState();
}
class _AddAreaItemState extends State<AddAreaItem> {
//--------- selectedCategory_1 variable
String selectedCategoryArea = '';
#override
Widget build(BuildContext context) {
return Container(
height: 366,
width: 514,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
height: 25,
),
SizedBox(
width: 434,
height: 42,
child: Wrap(
alignment: WrapAlignment.spaceBetween,
children: [
Text(
widget.isUpdate ? 'Add Area' : 'Update Area',
style: heading2(
color: ColorName.blackPrimary,
),
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: InkWell(
child: SvgPicture.asset(
Assets.icons.closeIcon.path,
width: 16,
height: 16,
),
onTap: () => {
Navigator.pop(context),
},
),
)
],
),
),
//----------- TextField Code Area dan Category
SizedBox(
width: 435,
child: Wrap(
alignment: WrapAlignment.spaceAround,
children: [
Wrap(
direction: Axis.vertical,
children: [
Text(
'Area Code',
style: body1(
color: ColorName.blackPrimary,
),
),
SizedBox(
height: 10,
),
SizedBox(
width: 126,
height: 60,
child: TextField(
style: body1(color: ColorName.blackPrimary),
cursorColor: ColorName.blackPrimary,
decoration: InputDecoration(
hintText: 'Area Code',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
borderSide: BorderSide(
color: ColorName.grey,
),
),
),
),
),
],
),
Wrap(
direction: Axis.vertical,
children: [
Text(
'Category Area',
style: body1(
color: ColorName.blackPrimary,
),
),
const SizedBox(
height: 10,
),
Container(
width: 288,
height: 56,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: BorderSide(
style: BorderStyle.solid, color: ColorName.grey),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
),
child: DropdownButton<String>(
icon: Padding(
padding: const EdgeInsets.only(right: 10, top: 8),
child: SvgPicture.asset(
Assets.icons.dropdownIcon.path,
fit: BoxFit.scaleDown,
),
),
style: body1(color: ColorName.blackPrimary),
items: <String>[
'Block',
'Fining Line',
].map((String value) {
return DropdownMenuItem(
value: value,
child: Text(value),
);
}).toList(),
hint: Padding(
padding: const EdgeInsets.only(top: 8, left: 10),
child: Text(
style: body1(color: ColorName.grey),
selectedCategoryArea.isEmpty
? 'Category Area'
: selectedCategoryArea),
),
borderRadius: BorderRadius.circular(10),
underline: const SizedBox(),
isExpanded: true,
onChanged: (value) {
if (value != null) {
setState(() {
selectedCategoryArea = value;
});
}
},
),
),
],
),
],
),
),
//----------- Tombol Add Area dan Cancel
SizedBox(
width: 425,
child: Wrap(
alignment: WrapAlignment.spaceBetween,
children: [
SizedBox(
width: 183,
height: 60,
child: OutlinedButton(
style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
side: MaterialStateProperty.all(
const BorderSide(
color: ColorName.darkBlue,
),
),
),
child: Text(
'Cancel',
style: subtitle1(
color: ColorName.darkBlue,
),
),
onPressed: () {
Navigator.pop(context);
},
),
),
//------------- Tombol add area
SizedBox(
width: 183,
height: 60,
child: OutlinedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
ColorName.darkBlue,
),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
side: MaterialStateProperty.all(
const BorderSide(
color: ColorName.darkBlue,
),
),
),
child: Text(
widget.isUpdate ? 'Add Area' : 'Update Area',
style: subtitle1(
color: ColorName.white,
),
),
onPressed: () {
if (widget.isUpdate) {
AddAreaSuccess().showCustomDialog(context);
} else {
UpdateAreaSuccess().showCustomDialog(context);
}
},
),
),
],
),
),
const SizedBox(
height: 25,
),
],
),
);
}
}
Firstly you need to define from where you know that the screen is updating screen or adding screen. I mean like
class AddAreaItem extends StatefulWidget {
final bool isUpdate;
const AddAreaItem({super.key, this.isUpdate=false});//IT MEANS THAT isUpdate is FALSE by default, but you can define it while creating AddAreaItem
...
}
Then in your widgets you can do like this:
Text(
widget.isUpdate?'Update Area':'Add Area',
//if it is inside Stateless widget then you use isUpdate? instead of widget.isUpdate
style: subtitle1(
color: ColorName.white,
),
),
Inside functions like onPressed you can just use like this:
onPressed: (){
if(widget.isUpdating){
AddAreaSuccess().showCustomDialog(context);
}else{...}
}
You can pass those two values in the properties when rendering this widget. Then check if the properties are null then you are trying to add new data. But if those contains the value then its updating the data.
Something like this
class AreaWidget extends StatefulWidget {
const AreaWidget({super.key, this.areaCode, this.areaName});
final int? areaCode;
final String? areaName;
#override
State<AreaWidget> createState() => _AreaWidgetState();
}
class _AreaWidgetState extends State<AreaWidget> {
#override
Widget build(BuildContext context) {
return Container(
child: Text(widget.areaCode == null ? "Add Area" : "Upate Area"),
);
}
}
You can then check value and make the changes according to the value. Even inside the function as well to call the specific add or update related code.
I am trying to do this test TODOs: but i have been have issuses pls help: i am trying to Uncomment the _confirmOrderModalBottomSheet() method to show summary of order, Uncomment the setState() function to clear the price and cups, and Change the 'price' to 0 when this button is clicked Increment the _cupsCounter when 'Add to Bag' button is clicked, and to Call setState((){}) method to update both price and cups counter when 'Add to Bag' button is clicked
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Coffee Test',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.white,
),
home: MyHomePage(title: 'Coffee Test'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _selectedPosition = -1;
String _coffeePrice ="0";
int _cupsCounter =0;
int price = 0;
String _currency ="₦";
static const String coffeeCup ="images/coffee_cup_size.png";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
title: FlatButton(
onPressed: (){
//TODO: Uncomment the _confirmOrderModalBottomSheet() method to show summary of order
//_confirmOrderModalBottomSheet(totalPrice: "$_currency$price", numOfCups: "x $_cupsCounter");
},
child: Text("Buy Now",style: TextStyle(color: Colors.black87),),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0), side: BorderSide(color: Colors.blue))
),
actions: [
InkWell(
onTap: () {
//TODO: Uncomment the setState() function to clear the price and cups
//TODO: Change the 'price' to 0 when this button is clicked
setState(() {
this.price = -1;
this._cupsCounter = 0;
});
Icon(Icons.clear);
}),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Container(
height: double.maxFinite,
alignment: Alignment.center,
child: Text("$_cupsCounter Cups = $_currency$price.00", style: TextStyle(fontSize: 18),),
),
)
],
),
body: Padding(padding: EdgeInsets.all(20), child: _mainBody(),) // This trailing comma makes auto-formatting nicer for build methods.
);
}
Widget _mainBody(){
return SingleChildScrollView(
child: Container(
height: double.maxFinite,
width: double.maxFinite,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 0,
child: Stack(
children: [
Container(
width: double.maxFinite,
height: 250,
margin: EdgeInsets.only(left: 50, right: 50, bottom: 50, top: 60),
decoration: BoxDecoration(borderRadius:
BorderRadius.all(Radius.circular(180)),
color: Color.fromRGBO(239, 235, 233, 100)),
),
Container(
alignment: Alignment.center,
width: double.maxFinite,
height: 350,
child: Image.asset("images/cup_of_coffee.png", height: 300,),
)
],
)),
Padding(padding: EdgeInsets.all(10),),
Expanded(flex: 0,child: Text("Caffè Americano",
style: TextStyle(fontWeight: FontWeight.bold,
fontSize: 30),)),
Padding(padding: EdgeInsets.all(6),),
Expanded(flex: 0, child: Text("Select the cup size you want and we will deliver it to you in less than 48hours",
style: TextStyle(fontWeight: FontWeight.bold,
fontSize: 14, color: Colors.black45,),
textAlign: TextAlign.start,),
),
Container(
margin: EdgeInsets.only(top: 30, left: 20),
height: 55,
width: double.maxFinite,
alignment: Alignment.center,
child:Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
RichText(text: TextSpan(
text: _currency,
style: TextStyle(fontWeight: FontWeight.bold,
fontSize: 25, color: Colors.black87),
children: [
TextSpan(text: _coffeePrice, style: TextStyle(fontSize: 50, fontWeight: FontWeight.bold))
]
),),
Padding(
padding: EdgeInsets.only(right: 15),
),
ListView.builder(itemBuilder: (context, index){
return InkWell(
child: _coffeeSizeButton(_selectedPosition == index,
index ==0? "S" : index ==1? "M": "L"),
onTap: (){
setState(() {
this._coffeePrice= index ==0? "300" : index ==1? "600": "900";
_selectedPosition = index;
});
},
);
}, scrollDirection: Axis.horizontal,
itemCount: 3, shrinkWrap: true,),
],),
),
Container(
margin: EdgeInsets.only(top: 30),
padding: EdgeInsets.all(10),
width: double.maxFinite,
height: 70,
child: FlatButton(onPressed: (){
//TODO: Currently _cupsCounter only show 1 when this button is clicked.
// TODO: Increment the _cupsCounter when 'Add to Bag' button is clicked'
//TODO: Call setState((){}) method to update both price and cups counter when 'Add to Bag' button is clicked
this._cupsCounter = 1;
this.price += int.parse(_coffeePrice);
}, child: Center(child: Text("Add to Bag",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),)
,),
color: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50)
),
),
)
],
),
),
);
}
Widget _coffeeSizeButton(bool isSelected, String coffeeSize){
return Stack(
children: [
Container(alignment: Alignment.center, width: 55,
child: Text(coffeeSize, style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold,
color: isSelected? Colors.blue: Colors.black45),),),
new Container(
margin: EdgeInsets.only(right: 10),
child: Image.asset(coffeeCup, width:50, color: isSelected ? Colors.blue: Colors.black45,),
decoration: BoxDecoration(border: Border(top: BorderSide(color: isSelected? Colors.blue: Colors.black45,
width: isSelected? 2: 1), left: BorderSide(color: isSelected? Colors.blue: Colors.black45,
width: isSelected? 2: 1), bottom: BorderSide(color: isSelected? Colors.blue: Colors.black45,
width: isSelected? 2: 1), right: BorderSide(color: isSelected ?Colors.blue: Colors.black45 ,
width: isSelected? 2: 1)), borderRadius: BorderRadius.all(Radius.circular(5))),
)
],
);
}
void _confirmOrderModalBottomSheet({String totalPrice, String numOfCups}){
showModalBottomSheet(
context: context,
builder: (builder){
return new Container(
height: 150.0,
color: Colors.transparent, //could change this to Color(0xFF737373),
//so you don't have to change MaterialApp canvasColor
child: new Container(
padding: EdgeInsets.all(10),
decoration: new BoxDecoration(
color: Colors.white,
borderRadius: new BorderRadius.only(
topLeft: const Radius.circular(10.0),
topRight: const Radius.circular(10.0))),
child: Column(
children: [
Container(
child: Text("Confirm Order",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),),
alignment: Alignment.center, height: 30, decoration: BoxDecoration(
), ),
_getEstimate(totalPrice, numOfCups)
],
)),
);
}
);
}
Widget _getEstimate(String totalPrice, String numOfCups){
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Image.asset("images/cup_of_coffee.png", height: 70, width: 50,),
Padding(padding: EdgeInsets.all(10)),
Text(numOfCups, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),),
Padding(padding: EdgeInsets.all(10)),
Text(totalPrice, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),)
],
);
}
}
You're setting 1 to your _cupsCounter instead of adding one to it.
When updating your values, putting the operation i.e _cupsCounter += 1; in setState will update the state of your widget which makes values change in your widgets.
setState((){
_cupsCounter += 1; // make it +=1 instead of =1.
price += int.parse(_coffeePrice);
});
Also you can use setState by putting it after your operation which will update the state of your widget after the operation is done.
_cupsCounter += 1; // make it +=1 instead of =1.
price += int.parse(_coffeePrice);
setState((){});
Full code should look like this.
Container(
margin: EdgeInsets.only(top: 30),
padding: EdgeInsets.all(10),
width: double.maxFinite,
height: 70,
child: FlatButton(onPressed: (){
// Currently _cupsCounter only show 1 when this button is clicked.
// Increment the _cupsCounter when 'Add to Bag' button is clicked'
// Call setState((){}) method to update both price and cups counter when 'Add to Bag' button is clicked
setState((){
_cupsCounter += 1; // make it +=1 instead of =1.
price += int.parse(_coffeePrice);
}); // call setState like this
}, child: Center(child: Text("Add to Bag",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),)
,),
color: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50)
),
),
)