Flutter - Combine dynamically generated elements with hard-coded ones - dart

I'm basically creating a game where a grid is getting generated dynamically. It creates the tiles, adds them to a list and uses that list as an argument for the children parameter. What I find difficult however is combining it with fixed widgets.
Let's say on top of everything, I want a text element. The problem I now encounter is that if I assign my dynamically generated content like this:
...
children: mycontent,
...
then I have nowhere to put my hard coded widgets. I hope you know what I mean. Until now, I have solved it by creating a larget list and copying the dynamically generated elements over, and afterwards adding my hard-coded widgets:
Widget buildTile(int counter) {
return new GestureDetector(
onTap: (){
setState((){
toggleColor(counter);
});
},
child: new Container(
color: colors[counter],
foregroundDecoration: new BoxDecoration(
border: new Border(),
),
width: 75.0,
height: 75.0,
margin: new EdgeInsets.all(2.0),
)
);
}
List<Widget> buildGrid(){
Map dimensions = {"width" : 4, "height" : 6};
List<Widget> grid = new List<Widget>(dimensions["height"]);
List<Widget> tiles = [];
int counter = 0;
for (int i = 0; i < dimensions["height"]; i++){
tiles = [];
for (int j = 0; j < dimensions["width"]; j++){
tiles.add(buildTile(counter));
counter++;
}
grid[i] = new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: tiles,
);
}
return grid;
}
List<Widget> copyElements(List<Widget> from){
List<Widget> to = [];
for (int i = 0; i < from.length; i++){
to.add(from[i]);
}
return to;
}
List<Widget> buildPlayground(List<Widget> grid){
List<Widget> playground = [];
playground = copyElements(grid);
playground.add(new Padding(
padding: new EdgeInsets.all(20.0),
child: new RaisedButton(
color: Colors.purple,
child: new Container(
width: 100.0,
child: new Center(
child: new Text("Done", style: new TextStyle(fontSize: 20.0)),
),
),
onPressed: (){
}
),
));
return playground;
}
#override
build(BuildContext context){
return new Scaffold(
appBar: new AppBar(
title: new Text("Game"),
),
body: new Container(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: buildPlayground(buildGrid()),
)
),
);
}
It kinda works, but is very tedious as soon as I figure out that I want to add another hard coded widget. Any suggestions for how I can address this problem?
Thanks

I guess this is the way to go, however you could use the GridView widget, and you could create a TileWidget instead of you buildTile function. Using GridView should clean your code, but what do you mean by hard-coded widgets ?

You can combine the accepted answer from this question and use the spread operator, like what's below, to combine a list with another list or with singular items.
List<Widget> generatedWidgets = generateWidgetList();
List<Widget> hardCodedWidgets = hardCodeWidgetList();
Widget singleHardCodedWidget = Container(
child: Text('some text'),
);
combinedList = [...generatedWidgets, ...hardCodedWidgets, singleHardCodedWidget];

Related

Flutter : setState() is not working properly

I'm making a new stateful widget that would show a listview according to the option selected, which are ONE and TWO here. The value of index changes once the GestureDetector is tapped, fontsize and color of the text changes. but, the Container with pages[index] does not rebuild
I don't know what is wrong since, one of the container in the column rebuilds and the other doesn't.
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return MatchStatsState();
}
}
class MatchStatsState extends State<MatchStats>{
List<Widget> pages = [
ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
BattingStatsView(CskBatting),
BowlingStatsView(cskBowling),
],
),
ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
BattingStatsView(kxipBatting),
BowlingStatsView(kxipBowling)
],
),
];
Color activeColor = Colors.yellow;
Color inactiveColor = Colors.white;
num activeFontSize = 20.0;
num inactiveFontSize = 15.0;
int index = 0;
#override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
height: MediaQuery.of(context).size.height*0.4,
width: MediaQuery.of(context).size.width*0.95,
child: Column(
children: <Widget>[
Container(
height: MediaQuery.of(context).size.height*0.05,
width: MediaQuery.of(context).size.width*0.95,
child: Row(
children: <Widget>[
GestureDetector(
onTap: (){
setState(() {
index = 0;
});
},
child: Container(
width: MediaQuery.of(context).size.width*0.45,
child: Text("ONE",style: TextStyle(color: index == 0?activeColor:inactiveColor,fontSize: index == 0? activeFontSize: inactiveFontSize)),
),
),
GestureDetector(
onTap: (){
setState(() {
index = 1;
});
},
child: Container(
width: MediaQuery.of(context).size.width*0.45,
child: Text("TWO",style: TextStyle(color: index == 1?activeColor:inactiveColor, fontSize: index == 1? activeFontSize: inactiveFontSize)),
),
),
],
),
),
Container(
height: MediaQuery.of(context).size.height*0.35,
width: MediaQuery.of(context).size.width*0.95,
child: pages[index]
),
]
)
);
}
}
I want the second container in the column to rebuild when the value of index changes, how could I achieve that?
Try with this:
create a method that return a List Widget like this:
List<Widget> buildPages() {
return [
ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
BattingStatsView(CskBatting),
BowlingStatsView(cskBowling),
],
),
ListView(
scrollDirection: Axis.horizontal,
children: <Widget>[
BattingStatsView(kxipBatting),
BowlingStatsView(kxipBowling)
],
),
];
}
Widget getProperWidget(int index) {
return buildPages()[index];
}
Than your column container:
Container(
height: MediaQuery.of(context).size.height*0.35,
width: MediaQuery.of(context).size.width*0.95,
child: getproperWidget(index)
),
Remember to override the initState.
I think the cause of this issue is the element tree doesn't recognize the change that has been done in the widget tree , so you can add Key to the container which holds pages[index] or
you can do something like this :
Widget getWidget(int index){
return Container(
child:pages[index],
);
}
instead of using Container in the widget tree, use a function that will be called every time the ui re renders .
I hope that can help

Flutter Bottomsheet Modal not rerendering

On my grid items when I click, it opens a ModalBottomSheet and listed with filter chips of strings. When you click a filter chip value, the value is updated but the widget does not re-render. The app is a StatefulWidget.
I have called the function setState.
What I expect is filterchips becomes checked and unchecked on selection.
void _showBottom(index){
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context){
return new Container(
padding: new EdgeInsets.all(27.0),
child: new Column(
children: <Widget>[
new Text('Some headline', style: new TextStyle( fontWeight: FontWeight.bold, fontSize: 22),),
getFilterChipsWidgets(index),
],
),
);
}
);
}
Widget getFilterChipsWidgets(index)
{
List<Widget> tags_list = new List<Widget>();
for(var i=0; i<list[index]["tags"].length; i++) {
var _isSelected = true;
FilterChip item = new FilterChip(
label: Text("Filtertext",),
selected: _isSelected,
onSelected: (bool newValue) {
setState(() {
_isSelected = !_isSelected;
debugPrint(_isSelected.toString());
});
},
);
tags_list.add(item);
}
return new Row(children: tags_list);
}
You need to add height for the root node of the bottom sheet. So change your container to have a fixed height.
Container(
height: 300.0, // Add height
padding: new EdgeInsets.all(27.0),
child: new Column(
children: <Widget>[
new Text('Some headline',
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 22),),
getFilterChipsWidgets(index),
],
),
I usually calculate this dynamically based on the widgets I'm passing in. But this should get you started.
Edit
The comment I gave below was the actual answer.
You should wrap all the widgets in the bottom sheet into it's own stateful widget and set your values in there.
Modal Bottom sheet seems to call the constructor whenever any change in the UI occurs in the child widget for example changing focus inside a form.
So you need to create instances of the objects you are working with inside a parent StatefulWidget and then to reflect changes in the UI of the Model Bottom Sheet you need to call setState whenever you make changes to the data.
You need to wrap the under StatefulBuilder like this
import 'package:flutter/material.dart';
void _showBottom(index){
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context){
return StatefulBuilder( builder: (BuildContext context, StateSetter setState){
return new Container(
padding: new EdgeInsets.all(27.0),
child: new Column(
children: <Widget>[
new Text('Some headline', style: new TextStyle( fontWeight: FontWeight.bold, fontSize: 22),),
getFilterChipsWidgets(index, setState),
],
),
);
});
}
);}
Widget getFilterChipsWidgets(index, StateSetter setState)
{
List<Widget> tags_list = new List<Widget>();
for(var i=0; i<list[index]["tags"].length; i++) {
var _isSelected = true;
FilterChip item = new FilterChip(
label: Text("Filtertext",),
selected: _isSelected,
onSelected: (bool newValue) {
setState(() {
_isSelected = !_isSelected;
debugPrint(_isSelected.toString());
});
},
);
tags_list.add(item);
}
return new Row(children: tags_list);
}

Flutter - Showing suggestion list on top of other widgets

I am developing a screen where I have to show suggestions list below the textfield.
I want to achieve this
I have developed this so far
Following code shows textfield with suggestions in a list.
#override
Widget build(BuildContext context) {
final header = new Container(
height: 39.0,
padding: const EdgeInsets.only(left: 16.0, right: 2.0),
decoration: _textFieldBorderDecoration,
child: new Row(
children: <Widget>[
new Expanded(
child: new TextField(
maxLines: 1,
controller: _controller,
style: _textFieldTextStyle,
decoration:
const InputDecoration.collapsed(hintText: 'Enter location'),
onChanged: (v) {
_onTextChanged.add(v);
if (widget.onStartTyping != null) {
widget.onStartTyping();
}
},
),
),
new Container(
height: 32.0,
width: 32.0,
child: new InkWell(
child: new Icon(
Icons.clear,
size: 20.0,
color: const Color(0xFF7C7C7C),
),
borderRadius: new BorderRadius.circular(35.0),
onTap: (){
setState(() {
_controller.clear();
_places = [];
if (widget.onClearPressed != null) {
widget.onClearPressed();
}
});
},
),
),
],
),
);
if (_places.length > 0) {
final body = new Material(
elevation: 8.0,
child: new SingleChildScrollView(
child: new ListBody(
children: _places.map((p) {
return new InkWell(
child: new Container(
height: 38.0,
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
alignment: Alignment.centerLeft,
decoration: _suggestionBorderDecoration,
child: new Text(
p.formattedAddress,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: _suggestionTextStyle,
),
),
onTap: () {
_getPlaceDetail(p);
},
);
}).toList(growable: false),
),
),
);
return new Container(
child: new Column(
children: <Widget>[header, body],
),
);
} else {
return new Container(
child: new Column(
children: <Widget>[header],
),
);
}
}
Header(Textfield) and body(Suggestions List - SingleChildScrollView with ListBody) is wrapped inside the Column widget, and column expands based on the total height of the children.
Now the problem is as Column expands, layout system pushes other widgets on screen to the bottom. But I want other widgets to stay on their positions but suggestion list starts to appear on top of other widgets.
How can I show suggestions list on top of other widgets? And the suggestions list is dynamic, as user types I call the Google Places API and update the suggestions list.
I have seen there is showMenu<T>() method with RelativeRect positions but it doesn't fulfills my purpose, my suggestion list is dynamic(changing based on user input) and the styling for each item I have is different from what PopupMenuItem provides.
There is one possibility I can think of using Stack widget as root widget of this screen and arrange everything by absolute position and I put suggestion list as a last child of the stack children list. But it is not the right solution I believe.
What other possibilities I need to look into? What other Widgets can be used here in this use-case?
And again use-case is simple, overlaying suggestion list on other widgets on the screen and when user tap any of the item from the list then hiding this overlaid suggestion list.
The reason why your autocomplete list pushes down the widgets below it is because the List is being expanded on the Container. You can use Flutter's Autocomplete widget and it should inflate the autocomplete list over other widgets.
var fruits = ['Apple', 'Banana', 'Mango', 'Orange'];
_autoCompleteTextField() {
return Autocomplete(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable<String>.empty();
}
return fruits.where((String option) {
return option
.toLowerCase()
.contains(textEditingValue.text.toLowerCase());
});
},
onSelected: (String selection) {
debugPrint('You just selected $selection');
},
);
}

How to use a "for" loop inside children of a widget?

I am really confused with this, where should the FOR LOOP be placed, so that I don't get an error in flutter? As you can see on the picture, it has red underlines and it says.
Two alternatives :
final children = <Widget>[];
for (var i = 0; i < 10; i++) {
children.add(new ListTile());
}
return new ListView(
children: children,
);
or
return new ListView(
children: new List.generate(10, (index) => new ListTile()),
);
There are multiple ways of using a for loop in children for widgets like ListView, Column, etc.
Using a for loop
ListView(
children: [
for (var i = 0; i < 10; i++) Text('Item $i'),
],
)
Using a for-each loop
ListView(
children: [
for (var item in items) Text(item),
],
)
Combining ...[] with for loop
ListView(
children: [
...[
Text('Item 0'),
Text('Item 1'),
],
for (var item in items) Text(item), // Rest of the items
],
)
We can use the spread operator also to add multiple widgets using for the loop
Column(
children: [
Container() /// we can add some other widgets
for (var i = 0; i < 3; i++) ...[
CardListItem(),
Divider(),
],
]
Simple for loop in flutter using json response
Widget build(BuildContext context) {
var list = [{'id':"123123","date":"20/08/2016"},{'id':"123124","date":"26/08/2016"},{'id':"123125","date":"26/08/2016"}];
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text('Recent Claims'),
Table(
border: TableBorder.all(color: Colors.black),
columnWidths: {
0: FixedColumnWidth(100.0),
1: FixedColumnWidth(100.0)
},
children:[
for(var item in list ) TableRow(children: [
Text(item['id']),
Text(item['date']),
])]
),
}
If you don't have a List:
ListView(
scrollDirection: Axis.horizontal,
children: List.generate(10, (index) => …),
),
Else:
ListView(
scrollDirection: Axis.horizontal,
children: list.map((e) => …).toList(),
),

On tap, display value at a different location

I'm trying to create a word game. I have a local json file where I'm retrieving data from. I'm able to retrieve the data and display it on the first row. What I'm trying to do is on tap of one block (on the first row), get the value and display it in order on the second row.
I'm able to retrieve the value but I can't display it on the second row. I tested this by printing the value in the console.
Updated code:
body: new Container(
child: new Center(
// Use future builder and DefaultAssetBundle to load the local JSON file
child: new FutureBuilder(
future: DefaultAssetBundle
.of(context)
.loadString('data_repo/starwars_data.json'),
builder: (context, snapshot) {
var newData = JSON.decode(snapshot.data.toString());
List<Widget> listMyWidgets() {
List<Widget> list = new List();
for (var i = 0; i < newData.length; i++) {
var word = newData[i]['word']["letters"];
for (var n = 0; n < word.length; n++) {
list.add(new Text(word[n]['letter']));
}
}
return list;
}
List letters = [];
for (int i = 0; i < listMyWidgets().length; i++) {
var listMyWidgetsToString =
listMyWidgets()[i].toString();
var listWidgetToList =
listMyWidgetsToString.replaceAll('Text("', "");
var completeWordList =
listWidgetToList.replaceAll('")', "");
letters.add(completeWordList);
}
return new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
new Column(children: [
new Image.asset(newData[0]['image'])
]),
new GridView.count(
shrinkWrap: true,
crossAxisCount: listMyWidgets().length,
children: new List.generate(
listMyWidgets().length,
(i) => new GestureDetector(
onTap: () {
final int wordLength =
5; //this is a ref to the lenght of the word so you do not keep adding tiles
setState(() {
(letters.length + 1) <=
wordLength * 2
? letters.add(letters[i])
: null;
});
},
child: new Card(
elevation: 5.0,
color: Colors.brown[500],
child: new Padding(
padding:
const EdgeInsets.all(8.0),
child: new Center(
child:
new Text(letters[i])),
),
),
)),
),
],
),
);
},
itemCount: newData == null ? 0 : newData.length,
);
}),
),
)
It depends on how you want to structure your data. In this example, I just add the pressed letters into the same array for the word and it will do the job.
Note that I keep a reference (which you may add to your JSON) which is the initial length of the word so it stops adding tiles when all letters are used.
Also you need to have a StatefulWidget in order for this to work
Probably there is a better a way to handle this but that is what I managed to do atm.
class GridViewWords extends StatefulWidget {
#override
GridViewWordsState createState() {
return new GridViewWordsState();
}
}
class GridViewWordsState extends State<GridViewWords> {
List letters = [
"A",
"M",
"C",
"I",
"C"
];
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new GridView.count(
shrinkWrap: true,
crossAxisCount: 5,
children: new List.generate(letters.length, (i)=>
new GestureDetector(
onTap: (){
final int wordLength =5; //this is a ref to the lenght of the word so you do not keep adding tiles
setState((){
(letters.length+1)<=wordLength*2? letters.add(letters[i]):null;
});
},
child: new Card(
elevation: 5.0,
color: Colors.brown[500],
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(child: new Text(letters[i])),
),
),
)),
),
);
}
}

Resources