Break out dialogs in it's own file or class - flutter

I have a beginner question. It's really simple to break out a Widget to it's own class. Like having a Column with buttons in a stateless widget that accepts some functions and some strings in the constructor. Then I can include and use this from any screen and widget in my app.
But how is this achieved with dialogs? If I design a dialog I would love to have that in its own file so I can just import it, and then pass functions and texts into it.
Right now I'm trying to break out a Picker dialog form the flutter_picker package. In one of my screens I have this:
void _showTimeDialog() {
Picker(
adapter: NumberPickerAdapter(data: <NumberPickerColumn>[
NumberPickerColumn(begin: 0, end: 60, initValue: _minutes),
NumberPickerColumn(begin: 0, end: 60, initValue: _seconds),
]),
delimiter: <PickerDelimiter>[
PickerDelimiter(
child: Container(
width: 30.0,
alignment: Alignment.center,
child: Text(':'),
),
)
],
builderHeader: (context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('MINUTES'),
Container(
width: 30,
),
Text('SECONDS'),
],
),
);
},
hideHeader: false,
confirmText: 'OK',
diameterRatio: 1.3,
magnification: 1.3,
height: 100,
squeeze: 1,
title: Center(child: const Text('DURATION')),
selectedTextStyle: TextStyle(color: Theme.of(context).primaryColor),
onConfirm: (Picker picker, List<int> value) {
onConfirmDurationPicker(picker, value);
},
).showDialog(context);
}
void onConfirmDurationPicker(Picker picker, List<int> value) {
setState(() {
_minutes = picker.getSelectedValues()[0];
_seconds = picker.getSelectedValues()[1];
});
}
What I would like is to have this in it's own file. And then I want to pass the onConfirmDurationPicker function (that will change in different screens) and some other values to set this picker up. But I don't want to have to duplicate all this code in every single screen that need this kind of picker dialog.
What's the kosher way of breaking stuff like this out in its own classes/files?
Let me know if anything is unclear in my question.

You are on the right path! Indeed it is best practice to split your app into meaningful parts to avoid boilerplate code. In your case just create a new File and build a Stateless Widget there. This Stateless Widget should return your Picker and can take the arguments via it's constructor. You can then call your class with the .showDialog(context) wherever you want!

Related

Make a variable in build build context run once

I have a stateful widget which uses a provider to get questions. The question type looks like this:
{
"question": "What...",
"answer: 1829,
"buffer": [1928, 1874, 1825]
}
I have a shuffle method which shuffles the items passed to it. So in my widget, I have this code:
#override
Widget build(BuildContext context) {
var state = context.watch<Services>();
Tion tion;
List<int> shuffled;
int selectedNumber;
if (state.questions != null) {
tion = state.questions[0];
shuffled = shuffle([tion.answer, ...tion.buffer]); // here's my issue
}
return ...
}
Deeper in the widget tree, I render these numbers:
GridView.count(
crossAxisCount: 2,
children: List.generate(4, (index) =>
Center(
child: GestureDetector(
onTap: () => setState(() {
selectedNumber = shuffled[index]; // setstate
}),
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: selectedNumber == shuffled[index] ? Color(0xff6C63FF) : Colors.grey[200],
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
child: Center(
child: Text(
'${shuffled[index]}',
style: GoogleFonts.lato(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey[800]
)
)
),
),
),
)
),
)
The problem is when I call setState(), the widget rebuilds, and the order of the numbers along with it. Is there any way to prevent this? I tries with initState but it's called outside the scope of context.
If you need a BuildContext for your function you can use didChangeDependencies(): It is called when a dependency of this State object changes and also immediately after initState, it is safe to use BuildContext here. Subclasses rarely override this method because the framework always calls build after a dependency changes. Some subclasses do override this method because they need to do some expensive work (e.g., network fetches) when their dependencies change, and that work would be too expensive to do for every build.
#override
void didChangeDependencies() {
// Your function.
super.didChangeDependencies();
}
Getx package also has variety of ways to insert a Middleware function. You can check them on package page.

Flutter did future widget didnt update screen ? i need to update data when its updated

I have an array which i set as a class like this
class FilterArray {
static var FilterArrayData = [];
}
I am simply adding the values in an array. Issue is i am calling this array in a page when array is null. Then on next Page i am adding values in array. Now issue is when i come back in previous page the array is still null. I need to refresh page for this. Which i dont want thats why i use FutureWidget i though from Future widget when array update it will also update in my screen but thats not working. Need to know what can i do for this here i need to update data when array is update so it can show in a Future Widget.
This is my total code
class _SearchPgState extends State<SearchPg> {
Future getData() async {
var result = FilterArray.FilterArrayData;
if (result.length != 0) {
return result;
} else {
return null;
}
}
#override
Widget build(BuildContext context) {
print(FilterArray.FilterArrayData);
return Scaffold(
appBar: AppBar(
title: Container(
height: 50.0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0),
child: Center(
child: TextField(
onTap: () => Get.to(SearchPgExtra()),
readOnly: true,
decoration: InputDecoration(
hintText: tr('search.search'),
alignLabelWithHint: true,
hintStyle: Theme.of(context).textTheme.subtitle2,
prefixIcon: Icon(Icons.search),
),
),
),
),
),
actions: [
IconButton(
icon: Icon(
FlutterIcons.sort_descending_mco,
color: Theme.of(context).accentColor,
),
onPressed: navigateToSortPage,
),
IconButton(
icon: Icon(
FlutterIcons.filter_fea,
color: Theme.of(context).primaryColor,
),
onPressed: navigateToFilterPage,
),
],
),
body: FutureBuilder(
future: getData(), // async work
builder: (context, projectSnap) {
print(projectSnap.data);
if (projectSnap.hasData) {
return StaggeredGridView.countBuilder(
itemCount: projectSnap.data.length,
crossAxisCount: 4,
staggeredTileBuilder: (int index) => StaggeredTile.fit(2),
mainAxisSpacing: 15.0,
crossAxisSpacing: 15.0,
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 18.0),
itemBuilder: (context, index) {
var product = projectSnap.data[0][index];
return FadeInAnimation(
index,
child: ProductCard2(
product: product,
isHorizontalList: false,
),
);
},
);
} else {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/search.png',
width: MediaQuery.of(context).size.width / 2,
),
SizedBox(height: 15.0),
Text(
'search.title',
style: Theme.of(context).textTheme.headline1,
).tr(),
SizedBox(height: 15.0),
Text(
'search.subtitle',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
).tr(),
SizedBox(
height: MediaQuery.of(context).size.height / 5,
),
],
),
);
}
},
),
);
}
}
In start array is null then ill add values in array then comeback nothing change then i reload the screen then its working fine.
This is the how i am adding array
RangeSlider(
values: _currentRangeValues,
min: 0,
max: 10000,
divisions: 10,
labels: RangeLabels(
_currentRangeValues.start.round().toString(),
_currentRangeValues.end.round().toString(),
),
onChanged: (RangeValues values) {
setState(() {
_currentRangeValues = values;
//print(_currentRangeValues);
});
var data = searchArray.searchArrayData;
for (int i = 0; i < data.length; i++) {
var current = data[i];
if(current['Price'] >= _currentRangeValues.start && current['Price'] <= _currentRangeValues.end){
print(data);
FilterArray.FilterArrayData.add(data);
}
}
},
),
when data add to FilterArrayData ill go back on Page array on that page not updating so then i change the page and comeback again in SearchPg then i can see data
Don't do your validation with the length of your array. It is like trying to do a validation with something that doesn't existe yet. Instead of that, try using
if(snapshot.hasData)
{ return ... ; }
then, after that, now you can do another validation, for instance, sometimes what you receive is data, but an empty array. There is where I would place the other two options. Remember, inside of the first if.
if(array.isNotEmpty)
{ return ... ; }
and
else
{ return ... ; }
After the first if, then you can now also validate, what will happen if you didn't receive data at all. Simply with an else.
else
{ return ... ; }
In summary: use one first validation with hasData and then, inside of that, decide what to do with the received information. Outside all that, decide what to do if you didn't receive any information at all.
Such cases are faced by new developers often. The best way to deal with it is state management packages like Provider, Bloc, etc. Visit the link and you will find all the relevant packages. Personally, I have used Provider a lot. Bloc is also a good option. A lot of people use it. But I haven't had the chance to use it. Riverpod is an up and coming package. But it still requires a lot of fixing.

Searchable SliverGrid Rendering Wrong Items

I have a SliverGrid. I have a search field. In my search field onChange event I have a function that searches my local sqlite db based on the keyword entered by the user returns the results and reassigns to a variable and calls notifyListeners(). Now my problem is for some weird reason whenever I search for an item the wrong item is rendered.
I checked the results from my functions by iterating over the list and logging the title and the overall count as well and the results were correct however my view always rendered the wrong items. Not sure how this is possible.
I also noticed something strange, whenever it rendered the wrong item and I went back to my code and hit save, triggering live reload, when I switched back to my emulator it now displayed the right item.
I have tried the release build on an actual phone and it's the same behaviour. Another weird thing is sometimes certain items will duplicate and show twice in my list while the user is typing.
This is my function that searches my sqlite db:
Future<List<Book>> searchBookshelf(String keyword) async {
try {
Database db = await _storageService.database;
final List<Map<String, dynamic>> rows = await db
.rawQuery("SELECT * FROM bookshelf WHERE title LIKE '%$keyword%'; ");
return rows.map((i) => Book.fromJson(i)).toList();
} catch (e) {
print(e);
return null;
}
}
This is my function that calls the above function from my viewmodel:
Future<void> getBooksByKeyword(String keyword) async {
books = await _bookService.searchBookshelf(keyword);
notifyListeners();
}
This is my actual view where i have the SliverGrid:
class BooksView extends ViewModelBuilderWidget<BooksViewModel> {
#override
bool get reactive => true;
#override
bool get createNewModelOnInsert => true;
#override
bool get disposeViewModel => true;
#override
void onViewModelReady(BooksViewModel vm) {
vm.initialise();
super.onViewModelReady(vm);
}
#override
Widget builder(BuildContext context, vm, Widget child) {
var size = MediaQuery.of(context).size;
final double itemHeight = (size.height) / 4.3;
final double itemWidth = size.width / 3;
var heading = Container(
margin: EdgeInsets.only(top: 35),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Align(
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Books',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w900),
),
Text(
'Lorem ipsum dolor sit amet.',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 14),
),
],
),
),
);
var searchField = Container(
margin: EdgeInsets.only(top: 5, left: 15, bottom: 15, right: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 1.0,
spreadRadius: 0.0,
offset: Offset(2.0, 1.0), // shadow direction: bottom right
),
],
),
child: TextFormField(
decoration: InputDecoration(
border: InputBorder.none,
prefixIcon: Icon(
FlutterIcons.search_faw,
size: 18,
),
suffixIcon: Icon(
FlutterIcons.filter_fou,
size: 18,
),
hintText: 'Search...',
),
onChanged: (keyword) async {
await vm.getBooksByKeyword(keyword);
},
onFieldSubmitted: (keyword) async {},
),
);
return Scaffold(
body: SafeArea(
child: Container(
padding: EdgeInsets.only(left: 1, right: 1),
child: LiquidPullToRefresh(
color: Colors.amber,
key: vm.refreshIndicatorKey, // key if you want to add
onRefresh: vm.refresh,
showChildOpacityTransition: true,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
heading,
searchField,
],
),
),
SliverToBoxAdapter(
child: SpaceY(15),
),
SliverToBoxAdapter(
child: vm.books.length == 0
? Column(
children: [
Image.asset(
Images.manReading,
width: 250,
height: 250,
fit: BoxFit.contain,
),
Text('No books in your bookshelf,'),
Text('Grab a book from our bookstore.')
],
)
: SizedBox(),
),
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid.count(
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
crossAxisCount: 3,
children: vm.books
.map((book) => BookTile(book: book))
.toList(),
),
)
],
),
))));
}
#override
BooksViewModel viewModelBuilder(BuildContext context) =>
BooksViewModel();
}
Now the reason I am even using SliverGrid in the first place is because I have a search field and a title above the grid and I want all items to scroll along with the page, I didn't want just the list to be scrollable.
I believe this odd behavior can be attributed to you calling vm.getBooksByKeyword() in onChanged. As this is an async method, there is no guarantee that the last result returned will be the result for the final text in the TextFormField. The reason you see the correct results after a live reload is because the method is being called again with the full text currently in the TextFormField.
The quickest way to verify this is to move the function call to onFieldSubmitted or onEditingComplete and see if it behaves correctly.
If you require calling the function with every change to the text, you will need to add a listener to the controller and be sure to only make the call after input has stopped for a specified amount of time, using a Timer, like so:
final _controller = TextEditingController();
Timer _timer;
...
_controller.addListener(() {
_timer?.cancel();
if(_controller.text.isNotEmpty) {
// only call the search method if keyword text does not change for 300 ms
_timer = Timer(Duration(milliseconds: 300),
() => vm.getBooksByKeyword(_controller.text));
}
});
...
#override
void dispose() {
// DON'T FORGET TO DISPOSE OF THE TextEditingController
_controller.dispose();
super.dispose();
}
...
TextFormField(
controller: controller,
...
);
So I found the problem and the solution:
The widget tree is remembering the list items place and providing the
same viewmodel as it had originally. Not only that it also takes every
item that goes into index 0 and provides it with the same data that
was enclosed on the Construction of the object.
Taken from here.
So basically the solution was to add and set a key property for each list item generated:
SliverPadding(
padding: EdgeInsets.only(bottom: 35),
sliver: SliverGrid(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: (itemWidth / itemHeight),
mainAxisSpacing: 20.0,
),
delegate: SliverChildListDelegate(vm.books
.map((book) => BookTile(
key: Key(book.id.toString()), book: book))
.toList()),
),
)
And also here:
const BookTile({Key key, this.book}) : super(key: key, reactive: false);
My search works perfectly now. :)

how to fix too many variables in flutter

I'm trying to create stacks of cards in my Flutter project. Each card contains different data/information and when I try visualize with a dummy data, I have to use a lot of variables which is pretty much repeating variable name for each card. Is there aways to make a reusable card component in flutter so that I can make it clear and simple because when I use real data in the future, I might have more than 2 cards in a group and they will also have different data. Any suggestion will be really appreciated.
class MyConstructor {
MyConstructor({this.jonathan1,this.jonathan2,this.jonathan3});
}
class StackedCardsState extends State<HomePage> {
List<MyConstructor> cards = [
MyConstructor(h1: "Hello", h2: "hello3")
];
/////
Padding(
padding: EdgeInsets.all(15.0),
child: Column(children: [
Text(MyConstructor.hey, style: TextStyle(fontWeight: FontWeight.bold),),
Text(MyConstructor.hey),
Text(MyConstructor.hey, style: TextStyle(color: Colors.red[500]),),
VerticalDivider(color: Colors.blue),
])),
Your problem is first of all rather simple, you are violating the DRY concept (Don't repeat yourself, https://en.wikipedia.org/wiki/Don%27t_repeat_yourself ).
As soon as you start copy pasting code take a moment and think about your code and how you can abstract it into a reusable component.
Another big issue that I think you are lacking is variable naming. It is a very very important part of writing code. Might seem trivial but it will be very hard to understand what a variable named cardOne1 and cardTwo2 actually mean. What is the purpose of that variable? What does it do?
Now with that said I understand your app has something to do with car sales but other than that I'm not really sure what I'm looking at. There for I will have a harder time finding a good variable for this code but here is an example.
So lets break down the contents in the card to a single reusable widget, we can also make a data class (or model) for storing the data that we then give to the widget.
//car_details.dart
class CarDetails {
String title;
String diffNumber;
String diffPercent;
Color colorIndicator;
CarDetails({
this.title,
this.diffNumber,
this.diffPercent,
this.colorIndicator,
});
}
//car_card_details.dart
class CarCardDetails extends StatelessWidget {
final double padding;
final CarDetails carDetails;
CarCardDetails({
this.carDetails,
this.padding = 15,
});
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
carDetails.colorIndicator != null
? Container(
color: carDetails.colorIndicator,
height: 60,
width: 2,
)
: Container(),
Padding(
padding: EdgeInsets.all(padding),
child: Column(children: [
Text(carDetails.title),
Text(carDetails.diffNumber),
Text(carDetails.diffPercent),
VerticalDivider(color: Colors.blue),
])),
],
);
}
}
To use this component we make a CarCard Widget that takes a title and a list of CarDetails like so:
// car_card.dart
class CarCard extends StatelessWidget {
final String title;
final List<CarDetails> carDetails;
CarCard({this.title, this.carDetails});
#override
Widget build(BuildContext context) {
List<Widget> detailRow = List();
if (carDetails != null) {
carDetails.forEach((element) {
detailRow.add(CarCardDetails(
top: element.title,
middle: element.diffNumber,
bottom: element.diffPercent,
lineColor: element.colorIndicator,
));
});
}
return Container(
//height: 150, //I would not hardcode the height, let the childrent expand the widget instead
child: SingleChildScrollView(
child: Card(
elevation: 8.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
child: InkWell(
child: Column(children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: [
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Spacer(),
Icon(Icons.favorite)
]),
),
Divider(color: Colors.black),
Row(children: detailRow),
]),
),
),
),
);
}
}
And instead of saving all the variables you had in app we can now make them into a list of CarDetails where each element contains the strings.
// some other widget
...
List<CarDetails> carDetails = [
CarDetails(
title: "2 hrs ago",
diffNumber: "+/ TRACK",
diffPercent: "% to DBJ",
),
CarDetails(
title: "CHEVEROLET",
diffNumber: "-2706",
diffPercent: "42.2%",
colorIndicator: Colors.red,
),
CarDetails(
title: "BUICK",
diffNumber: "+300",
diffPercent: "50%",
colorIndicator: Colors.green,
),
CarDetails(
title: "GMC",
diffNumber: "-712",
diffPercent: "52.1%",
colorIndicator: Colors.black26,
),
];
#override
Widget build(BuildContext context) {
return CarCard(
title: "US Daily Retail Delieveries by Brand",
carDetails: carDetails,
);
}
...
This can of course be abstracted even further with the groups of cards etc, etc. But I hope you get the idea.
This is an example of how you could do it, with that said I do not know what data you are intending to use and how you want to structure it. So consider this a starting point and take it from there. :)

UI not updating on setState. But does after i shuffle the list i want to display (confused). Not solved

I'm writing a word game with Flutter + Dart.
I have 2 rows that should display X amount of widgets in the form of letter pieces. The first row is empty, the second has letters in it, when i press a row 2 letter i want the first row pieces to include this letter.
Imagine a word game, with letters ready to be tapped to fill out the empty boxes to create the correct word.
I have 3 classes, main, LetterPiece, EmptyLetterPiece.
Including some variables such as letter. EmptyLetterPiece has the default letter = ''; And will on button press be set to a letter.
Main has a statefulwidget, the others are Statelesswidgets.
I create new lists for both LetterPiece and EmptyLetterPiece objects.
Length based on a word i choose.
MY PROBLEM:
"A" is the test value i assign when letter is pressed, they get the values when i print them out, but the UI is not updating. UNLESS i do -> emptyPieces.shuffle(); every time in the generate method.
Then the objects in emptyPieces display "A".
I don't want to shuffle every time i setState, but that makes the UI update, versus if i dont shuffle the UI won't update!
Read on.
My main class has a Statefulwidget to update the ui.
I have also tried to add a Statefulwidget to the other classes but with no success.
I took a look on this post -> Flutter setState changing, but not rerendering <-
but with no success.
//MY BUILD METHOD
#override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
stops: [0.3, 1],
colors: [
Color(firstColor),
Color(secondColor)
])
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(word1, textAlign: TextAlign.center, style: TextStyle(fontSize: textScale, color: Colors.white),),
generateEmptyPiecesRow(), // <---
generateLetterPiecesRow() // <---
],
),
);
}
//THE TWO GENERATE METHODS.
Widget generateEmptyPiecesRow(){
if(emptyPieces.length < word2.length) {
for (int i = 0; i < word2.length; i++) {
emptyPieces.add(new EmptyLetterPiece(pieceTextScale, 5, 50));
print("Added. $i");
}
emptyPieces.shuffle();
}
//emptyPieces.shuffle(); <--- WORKS IF I PUT THIS HERE, UI UPDATES ONLY IF I SHUFFLE THE PIECES BEFORE RETURNING THEM WITH THE ROW. <---
return Row(mainAxisAlignment: MainAxisAlignment.center, children: emptyPieces);
}
Widget generateLetterPiecesRow(){
if(letterPieces.length < word2.length) {
for (int i = 0; i < word2.length; i++) {
letterPieces.add(new LetterPiece(
word2.substring(i, i + 1).toUpperCase(), pieceTextScale, 15,
addLetter, removeLetter));
}
letterPieces.shuffle();
}
return Row(mainAxisAlignment: MainAxisAlignment.center, children: letterPieces);
}
//setState methods
addLetter(){
setState(() {
emptyPieces[getPiecePos()].setLetter("A");
increasePiecePos();
});
}
removeLetter(){
setState(() {
emptyPieces[getPiecePos()].removeLetter();
decreasePiecePos();
});
}
// EDIT: THIS IS THE EmptyLetterPiece class build method, LetterPiece has the same structure too.
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Container(
margin: EdgeInsets.all(_padding),
width: _pieceScale,
height: _pieceScale,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 3),
borderRadius: new BorderRadius.all(Radius.circular(20.0)),
),
child: Center(
child: Text(_letter, textAlign: TextAlign.center, style: TextStyle(color: Colors.white), textScaleFactor: _textScale),
)
);
}
I expected the view to update, but it only did so if i first shuffled the list i returned to the view, i don't want the list to update with a shuffle.
I'm clueless as to why it works when i shuffle, and not otherwise. Am I missing something obvious? I'm fairly new to Flutter.
Thanks for your time, if I was unclear please tell me and I try update the question.
The view will be updated when you call SetState within the widget you want refreshed. Child widgets will also be refreshed. If you don't want to refresh the widget itself or its child widgets, there are other ways to achieve that.