Related
I have to create multiple files for different stalls but it seems so wrong and I know there's a better way but I just don't know how. Is there a way to create something like a page builder that will let me create multiple pages with different information from a single file. The difficult part is to make the onTap function of the images send the user to the stall_page of the selected stall. I tried doing this by making a view attribute in which I create a page and manually import the page route. But that involves creating a stall_info and stall_page for every single stall.
Instead of creating stall1_page, stall2_page and so on, can I create a generic stall function that will use the same page but just change the data? I know that's LITERALLY the point of object oriented programming languages but I'm really new to them as you'll tell my previous stupid questions.
This is the homescreen dashboard
class GridDashboard extends StatelessWidget {
Item item1 = Item(
title: 'Tray blazers',
subtitle: 'Open',
event: 'by Chef Tracy',
img: 'assets/images/tray_blazers-cr.png',
view: stallPage,
);
Item item2 = Item(
title: 'Papa Rimz',
subtitle: 'Open',
event: '',
img: 'assets/images/papa_rimz.png',
view: papaRimzPage,
);
Item item3 = Item(
title: 'W SAUCE',
subtitle: 'Open',
event: '',
img: 'assets/images/w_sauce-removebg.png',
view: wSaucePage,
);
Item item4 = Item(
title: 'African Kitchen',
subtitle: 'Open',
event: '',
img: 'assets/images/cherry-kitchen.png',
view: africanKitchenPage,
);
Item item5 = Item(
title: 'Suya Craze',
subtitle: 'Open',
event: '',
img: 'assets/images/suya_craze.png',
view: suyaCrazePage,
);
Item item6 = Item(
title: 'Zulkys cafe',
subtitle: 'Open',
event: '',
img: 'assets/images/zulkys-removeb.png',
view: zulkysCafePage,
);
Item item7 = Item(
title: 'Street food',
subtitle: 'Open',
event: '',
img: 'assets/images/street_food--removebg-.png',
view: streetFoodPage,
);
#override
Widget build(BuildContext context) {
List<Item> myList = [
item1,
item2,
item3,
item4,
item5,
item6,
item7,
];
return Flexible(
child: GridView.count(
childAspectRatio: 1.0,
padding: const EdgeInsets.only(left: 16, right: 16),
crossAxisCount: 2,
crossAxisSpacing: 18,
mainAxisSpacing: 18,
children: myList.map(
(data) {
return Container(
decoration: BoxDecoration(
color: const Color(0xff453658),
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(data.view);
},
child: Image.asset(
data.img,
width: 90, //double.infinity
),
),
const SizedBox(height: 14),
Text(
data.title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
color: Colors.white,
),
),
const SizedBox(height: 8),
Text(
data.subtitle,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 10,
color: Colors.white38,
),
),
const SizedBox(height: 8),
// Text(
// data.event,
// style: const TextStyle(
// fontWeight: FontWeight.w600,
// fontSize: 11,
// color: Colors.white70,
// ),
// ),
],
),
);
},
).toList(),
),
);
}
}
class Item {
String title;
String subtitle;
String event;
String img;
String view;
Item({
required this.title,
required this.subtitle,
required this.event,
required this.img,
required this.view,
});
}
This is my stall_page:
class StallPage extends StatefulWidget {
const StallPage({super.key});
#override
State<StallPage> createState() => _StallPageState();
}
class _StallPageState extends State<StallPage> {
var selected = 0;
final pageController = PageController();
final stall = Stall.generateRestaurant1();
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xff392850), //kBackground,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomAppBar(
Icons.arrow_back_ios_outlined,
Icons.search_outlined,
leftCallback: () => Navigator.of(context).pop(),
),
StallInfo(), //
FoodList(
selected,
(int index) {
setState(() {
selected = index;
});
pageController.jumpToPage(index);
},
stall,
),
Expanded(
child: FoodListView(
selected,
(int index) {
setState(() {
selected = index;
});
},
pageController,
stall,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 25),
height: 60,
child: SmoothPageIndicator(
controller: pageController,
count: stall.menu.length,
effect: CustomizableEffect(
dotDecoration: DotDecoration(
width: 8,
height: 8,
color: Colors.grey.withOpacity(0.5),
borderRadius: BorderRadius.circular(8),
),
activeDotDecoration: DotDecoration(
width: 10,
height: 10,
color: kBackground,
borderRadius: BorderRadius.circular(10),
dotBorder: const DotBorder(
color: kPrimaryColor,
padding: 2,
width: 2,
),
),
),
onDotClicked: (index) => pageController.jumpToPage(index),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
backgroundColor: kPrimaryColor,
elevation: 2,
child: const Icon(
Icons.shopping_cart_outlined,
color: Colors.black,
size: 30,
),
),
);
}
}
This is my stall_info
class StallInfo extends StatelessWidget {
final stall = Stall.generateRestaurant1();
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 40),
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stall.name,
style: const TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Row(
children: [
Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: Colors.blueGrey.withOpacity(0.4),
borderRadius: BorderRadius.circular(5),
),
child: Text(
stall.label,
style: const TextStyle(
color: Colors.white,
),
)),
const SizedBox(
width: 10,
),
],
)
],
),
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset(
stall.logoUrl,
width: 80,
),
),
],
),
const SizedBox(
height: 5,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
stall.desc,
style: const TextStyle(fontSize: 16),
),
Row(
children: [
const Icon(
Icons.star_outline,
color: Colors.amber,
),
Text(
'${stall.score}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 15),
],
)
],
)
],
),
);
}
}
And this is stall
class Stall {
String name;
String label;
String logoUrl;
String desc;
num score;
Map<String, List<Food>> menu;
Stall(
this.name,
this.label,
this.logoUrl,
this.desc,
this.score,
this.menu,
);
static Stall generateRestaurant1() {
return Stall(
'Tray blazers',
'Restaurant',
'assets/images/tray_blazers.jpg',
'Tray Blazers by Chef Tracy',
4.5,
{
'Recommended': Food.generateRecommendedFoods1(),
'Popular': Food.generatePopularFoods1(),
'Smoothie': [],
'Rice': [],
},
);
}
}
If I understand the question correctly, you want to open the StallPage but show different values on the page depending on which image (pertaining to a given 'Stall') was selected on the previous page? I.e. clicking on item2 should open the StallPage with the restaurant title "Papa Rimz" etc.?
In that case, you can pass the argument to your new route builder via the onTap() function as a constructor parameter instead of calling Stall.generateRestaurant1() with hardcoded values in a given dart file.
StallInfo
Instead of getting your stall data inside the build method, you simply accept it as a required parameter for your widget. Now you have access to the data (title, ...) anywhere inside here.
class StallInfo extends StatelessWidget {
// Contains the stall object with its name, label, menu etc.
final Stall stall;
StallInfo({super.key, required this.stall});
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 40),
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Column(
...
),
);
}
}
HomeScreen
I'm a bit confused as to what the item list in your your home screen is for. Are these food items in a restaurant? Because if so, I think it would be much easier to save them inside the stall as a list of items and then use that list here:
List<Stall> _stalls = [...];
I'd like to note here that you hardcoded all the items by name and then, in your build method, added them to a list. Since you don't need their names anywhere, it would be just a little bit better to move the List<Stall> myList outside the build method and simply assign the objects directly (that is, before you add a real database):
class GridDashboard extends StatelessWidget {
List<Stall> _stalls = [
Stall('Tray blazers', ...),
Stall('Papa Rimz', ...),
];
#override
Widget build(BuildContext context) {
// do something with your stalls, onTap, pass the element directly
....
children: _stalls.map(
(data) {
return GestureDetector(
onTap: (){
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => StallPage(stall: data)
));
}
);
}),
}
}
If you use a builder function for your GridView (which you should if there can be a lot of stalls), in the onTap() you can instead call:
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => StallPage(stall: _stalls.elementAt(index))
));
StallPage
This page will look something like this
class StallPage extends StatefulWidget {
final Stall stall; // Take in the stall you passed from your home screen
const StallPage({super.key, required this.stall});
#override
State<StallPage> createState() => _StallPageState();
}
class _StallPageState extends State<StallPage> {
var selected = 0;
final pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
...
StallInfo(stall: widget.stall), // This is how you can access the values passed inside a StatefulWidget
...
);
}
}
First of all, let me fill you in the details:
I have this Widget:
class MenuAction extends StatelessWidget {
const MenuAction({
Key? key,
this.height,
required this.icon,
required this.label,
required this.willNavigate,
this.route,
this.onPressed,
required this.identifier,
}) : super(key: key);
final double? height;
final IconData icon;
final String label;
final bool willNavigate;
final Widget? route;
final VoidCallback? onPressed;
final String identifier;
#override
Widget build(BuildContext context) {
return SizedBox(
width: 70,
height: height ?? 80,
child: Column(
children: [
ClipOval(
child: Material(
color: Palette.aliceBlue,
child: IconButton(
onPressed: () async {
increaseUsage(identifier: identifier);
willNavigate ? navigate(context, route) : onPressed;
},
icon: Icon(
icon,
color: Palette.celticBlue,
size: 25,
),
),
),
),
const Spacer(),
Text(
label,
style: TextStyle(
color: Palette.oxfordBlue,
fontSize: 12,
),
textAlign: TextAlign.center,
),
const Spacer(
flex: 3,
),
],
),
);
}
}
This widget must have an icon, a label, an identifier and it should be specified if this widget willNavigate to a route or not.
If it should navigate, a route should be specified. If it shouldn't navigate, a onPressed function should be specific.
I also have a list of predefined actions built:
List<MenuAction> actions = [
MenuAction(
icon: Icons.cloud_download_outlined,
label: "Sincronizar",
willNavigate: false,
onPressed: () async {},
identifier: "syncDat",
),
MenuAction(
icon: Icons.business_center_outlined,
label: "Proposta Comercial",
willNavigate: true,
route: AppRoutes.comercialProposalList,
identifier: "propCom",
),
MenuAction(
icon: Icons.shopping_cart_outlined,
label: "Pedidos",
willNavigate: true,
route: AppRoutes.orderList,
identifier: "order",
),
MenuAction(
icon: Icons.cloud_upload_outlined,
label: "Enviar Pedidos",
willNavigate: false,
onPressed: () {},
identifier: "sendOrd",
),
MenuAction(
icon: Icons.receipt_long_rounded,
label: "Nota Fiscal",
willNavigate: true,
route: AppRoutes.invoiceList,
identifier: "invoice",
),
MenuAction(
icon: Icons.person_outline,
label: "Clientes",
willNavigate: true,
route: AppRoutes.clientsList,
identifier: "clients",
),
MenuAction(
icon: Icons.map_outlined,
label: "Endereços",
willNavigate: true,
route: AppRoutes.addressesList,
identifier: "address",
),
MenuAction(
icon: Icons.inventory_2_outlined,
label: "Estoque",
willNavigate: true,
route: AppRoutes.inventoryList,
identifier: "invent",
),
];
I access these actions through this function:
MenuAction getActionByIdentifier({String identifier = ""}) {
return actions.elementAt(
actions.indexWhere((element) => element.identifier == identifier),
);
}
Why go through all this trouble to create some simple widgets? they'll be used at my app's home screen, where all actions will be available in a horizontal slider. Also, the most used actions will be displayed below to the user. A count of how many times said action was used is stored in my local database along with the action's identifier, allowing me to retrieve and create it later using the function described earlier. Why did I did it this way? because I wanted to code my action only once and access it from different sources.
Now, if I create my actions statically, their onPressed event works flawlessly.
This is the Widget that creates all my actions statically:
class HomeActionsListView extends StatefulWidget {
const HomeActionsListView({
Key? key,
}) : super(key: key);
#override
State<HomeActionsListView> createState() => _HomeActionsListViewState();
}
class _HomeActionsListViewState extends State<HomeActionsListView> {
#override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
children: [
getActionByIdentifier(identifier: "syncDat"),
getActionByIdentifier(identifier: "propCom"),
getActionByIdentifier(identifier: "order"),
getActionByIdentifier(identifier: "sendOrd"),
getActionByIdentifier(identifier: "invoice"),
getActionByIdentifier(identifier: "clients"),
getActionByIdentifier(identifier: "address"),
getActionByIdentifier(identifier: "invent"),
],
);
}
}
Keep in mind that the onPressed event is ALWAYS present within my widget, since ALL actions increase their usage count in the database. As you can see in the code, the increaseUsage is called and then the widget will decide if it will only navigate to the provided route OR execute the specified custom onPressed function.
But for some reason, when I create the same widgets using the getActionByIdentifier function and try to pass it's label and onPressed event to a custom Card I built, the event doesn't trigger.
This is the FutureBuilder that creates my cards dynamically within a ListView based on the first most used actions:
FutureBuilder(
future: getMostUsedActions(),
builder: (BuildContext context,
AsyncSnapshot<List<String>> snapshot) {
if (snapshot.hasData) {
if (snapshot.data!.isNotEmpty) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
MenuAction action = getActionByIdentifier(
identifier: snapshot.data![index],
);
return RecentActionCard(
text: action.label,
onPressed: action.onPressed,
);
},
);
} else {
return const Center(
heightFactor: 15,
child: Text(
"Nenhuma ação utilizada recentemente!",
),
);
}
} else {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
},
);
This is the code to my custom Card:
class RecentActionCard extends StatelessWidget {
const RecentActionCard({
Key? key,
required this.text,
this.onPressed,
}) : super(key: key);
final String text;
final VoidCallback? onPressed;
#override
Widget build(BuildContext context) {
return Card(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(
color: Palette.celticBlue,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
fit: FlexFit.tight,
child: Padding(
padding: const EdgeInsets.only(left: 30),
child: Text(
text,
style: TextStyle(
fontFamily: "Barlow",
fontSize: 16,
fontWeight: FontWeight.bold,
color: Palette.oxfordBlue,
),
),
),
),
SizedBox(
height: 55,
width: 55,
child: IconButton(
onPressed: onPressed,
icon: Icon(
Icons.arrow_forward_ios,
color: Palette.oxfordBlue,
),
),
)
],
)
],
),
);
}
}
Finally, my question is: Why do my statically created widget's onPressed event work and when I try to create them dinamically they are returned as null?
Also, any way to make it work?
And, last but not least, if you have any suggestions on how to improve my code, they'll be much appreciated.
It shows this error although I have added late and required in the Question class constructor. It's repeatedly shows
Exception caught by widgets library
The following LateError was thrown building _BodyBuilder:
LateInitializationError: Field 'ques' has not been initialized
Main class:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'QuestionsAnswers.dart';
void main() {
runApp(const Quizzler());
}
class Quizzler extends StatelessWidget {
const Quizzler({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.grey[900],
leading: Icon(Icons.games),
title: Text(
'Quizzler',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
),
body: QuizPlay(),
),
),
);
}
}
class QuizPlay extends StatefulWidget {
const QuizPlay({Key? key}) : super(key: key);
#override
State<QuizPlay> createState() => _QuizplayState();
}
class _QuizplayState extends State<QuizPlay> {
List<Icon> score=[];// array of score icon
List<Questions>questionsAndAnswers=[
Questions(a:'Pakistan is an under developed country',b:true),
Questions(a:'Imran Khan is the Prime Minister of Pakistan',b:true),
Questions(a:'Y comes after U',b:false)
];
int questiontracker=0;// variable to increment of questions
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 4,
child: Padding(
padding: EdgeInsets.all(10.0),
child: Center(
child: Text(
questionsAndAnswers[questiontracker].ques,
style: TextStyle(
fontSize: 25.0,
color: Colors.white70,
),
),
),
),
),
Expanded(
child: Padding(
padding: EdgeInsets.all(10.0),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.green),
),
onPressed: () {
//Yes button
bool answer=questionsAndAnswers[questiontracker].ans;
if (answer==true)
{
print('correct answer');
}
else
{
print('wrong answer ');
}
setState(() {
questiontracker++;
score.add(Icon(Icons.check,color: Colors.green,)) ;
});
},
child: Text(
'Yes',
style: TextStyle(
fontSize: 20.0,
),
),
),
),
),
Expanded(
child: Padding(
padding: EdgeInsets.all(10.0),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: () {
// No button
bool answer=questionsAndAnswers[questiontracker].ans;
if (answer==false)
{
print('correct answer');
}
else
{
print('wrong answer ');
}
setState(() {
questiontracker++;
score.add(Icon(Icons.close,color: Colors.red,)) ;
});
},
child: Text(
'No',
style: TextStyle(
fontSize: 20.0,
),
),
),
),
),
Row(
children: score,
),
],
);
}
}
###Question CLASS###
class Questions{
late String ques;
late bool ans;
Questions({required String a,required bool b})
{
a=ques;
b=ans;
}
}
make it
ques = a;
ans = b;
This stores the value on the right in the value on the left.
Your class constructor Questions is wrong, change it to:
class Questions{
late String ques;
late bool ans;
Questions({required String a,required bool b}) {
ques = a;
and = b;
}
}
What is the purpose of having your questions as a plain class? I'd suggest turning it into a module class which in turn should be
class Question
{
String? ques;
bool? ans;
Question({
this.ques, this.ans});
}
and when you want to initialize a question I'd suggest creating a list
List<Question> questions = [];
question.add(Question("question",true));
// add more as you wish
This will allow you to turn it into JSON which will enable you to maybe provide questions from an online database to the app without needing to update the app every time you want to add a question.
I want to create toggle buttons and evenly space each element in the list of toggle buttons and make each selected button rounded like this,
I've tried using boxconstraints property, width property, margin property and the rest,
But this is what I'm getting, I've tried every other thing but I can't get it, this is what I'm getting
This is the code I'm using
import 'package:flutter/material.dart';
class TestingScreen extends StatefulWidget {
#override
State<TestingScreen> createState() => _TestingScreenState();
}
class _TestingScreenState extends State<TestingScreen> {
List<bool> _isSelected = [true, false, false, false];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 100),
child: Row(
children: [
Text(
'Time',
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w400,
),
),
ToggleButtons(
color: Color(0xff001666),
fillColor: Color(0xff001666),
selectedColor: Colors.white,
children: [
ToggleButton(name: '1D'),
ToggleButton(name: '1W'),
ToggleButton(name: '1M'),
ToggleButton(name: '1Y'),
],
isSelected: _isSelected,
onPressed: (int newIndex) {
setState(() {
for (int i = 0; i < _isSelected.length; i++) {
if (i == newIndex) {
_isSelected[i] = true;
} else {
_isSelected[i] = false;
}
print(_isSelected);
}
});
},
)
],
),
),
);
}
}
class ToggleButton extends StatelessWidget {
final String name;
const ToggleButton({Key? key, required this.name}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width * 0.1,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)),
padding: EdgeInsets.symmetric(vertical: 4),
alignment: Alignment.center,
child: Text(
name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
),
),
);
}
}
You can add renderBorder: false, property to remove the ash colored border and borderRadius: BorderRadius.circular(15), to make the round circled border in the outside and make the shape you can use constraints: const BoxConstraints.expand(height: 25,width: 34), to get the the exact size of the height and the width.enter image description here
but to get the exact result you have to use Inkwell() or ElvatedButton() or IconButton() bcz there isnt any property to use the borderRadius: BorderRadius.circular(15), for the each of the icons in the buttons as showed in the picture.
Hope it will work for you.
I'm new to flutter and I'm trying to make a widget which changes its text when you press it.
I can't make the couter variable final because it can be changed in the setState methode. But because "a class that [my] class inherits from" is marked as #immutable (the StatefulWidget I suppose), I always get an "Incorrect use of ParentDataWidget" exception.
Is there a solution to this problem or is there a better way to implement such a widget.
Here is my code:
class TopInfoBanner extends StatefulWidget {
int counter = 0;
TopInfoBanner({Key? key}) : super(key: key);
#override
State<TopInfoBanner> createState() => _TopInfoBannerState();
}
class _TopInfoBannerState extends State<TopInfoBanner> {
final Color textColor = cSecondaryColor;
#override
Widget build(BuildContext context) {
return Container(
height: 42.0,
color: cBoxColor,
child: Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: InkWell(
onTap: () {
setState(
() {
widget.counter++;
},
);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (widget.counter % 3 == 0)
infoBuilder('text 1', Icons.update_sharp),
if (widget.counter % 3 == 1)
infoBuilder(
'text 2', Icons.ac_unit_sharp),
if (widget.counter % 3 == 2)
infoBuilder('text 3',
Icons.gpp_good_outlined),
],
),
),
),
),
);
}
Padding infoBuilder(String text, IconData icon) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(
icon,
color: textColor,
),
Text(
text,
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(color: textColor),
),
],
),
);
}
}
Expanded widget is for the Row and Column.