Single Selection for ListView Flutter - flutter

I am trying to implement a listView single selection in my app such that once an item in the list is tapped such that pressed item color state is different from the others. I have done all I know but it does not work well. The problem is that even though my implementation updates each item state when pressed, it doesn't reset the others to their initial state.
class BoxSelection{
bool isSelected;
String title;
String options;
BoxSelection({this.title, this.isSelected, this.options});
}
class _AddProjectState extends State<AddProject> {
List<BoxSelection> projectType = new List();
#override
void didChangeDependencies() {
super.didChangeDependencies();
projectType
.add(BoxSelection(title: "Building", isSelected: false, options: "A"));
projectType
.add(BoxSelection(title: "Gym House", isSelected: false, options: "B"));
projectType
.add(BoxSelection(title: "School", isSelected: false, options: "C"));
}
child: ListView.builder(
itemCount: projectType.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
//here am trying to implement single selection for the options in the list but it don't work well
for(int i = 0; i < projectType.length; i++) {
if (i == index) {
setState(() {
projectType[index].isSelected = true;
});
} else {
setState(() {
projectType[index].isSelected = false;
});
}
}
});
},
child: BoxSelectionButton(
isSelected: projectType[index].isSelected,
option: projectType[index].options,
title: projectType[index].title,
),
);
},
),

Your problem is that you're using index to access projectType elements but you should be using i
if (i == index) {
setState(() {
projectType[i].isSelected = true;
});
} else {
setState(() {
projectType[i].isSelected = false;
});
}
In any case I think your code can be improved since it's not as efficient as it could be. You're iterating over the entire list and calling setState twice in every iteration, recreating the widget tree a lot of times unnecessarily when it can be done in one shoot.
Save your current selection in a class level variable
BoxSelection _selectedBox
Simplify your code to act directly over the current selection insted of iterating over the entire list
onTap: () =>
setState(() {
if (_selectedBox != null) {
_selectedBox.isSelected = false;
}
projectType[index].isSelected = !projectType[index].isSelected;
_selectedBox = projectType[index];
});

Related

Last item of list view remains even after deleting it in Flutter

I'm building a todo app in which CRUD operations can be carried out. With the help of Sqlflite todo items will be stored/retrieved from the database. When i try to delete todos, it works just as fine except the last item in the list view remains even after deleting it from the database. But, once the app restarts the last item gets removed. Tried setState() manually to refresh the page but nothings works. Help me sort this out and thanks in adavance.
//Holds the todos and will be passed to ListView.builder
List<Todo> listOfValues;
//Holds the indexes of selected items to highlight the items in the ListView
List<int> indexes = [];
//Holds the ids of selected items to perform CRUD
List<int> selectedItems = [];
//This is where the todos are retrieved from the database.
//Following DatabaseModel is a database helper class
DatabaseModel data = DatabaseModel();
data.queryingData().then((value) {
setState(() {
if (value.isNotEmpty) {
listOfValues = value;
}
});
});
Following block builds/returns the body of the main page of the app which is a ListView contains todos.
Widget content() {
return SafeArea(
child: Container(
child: ListView.builder(
itemBuilder: (context, index) {
return Card(
color: (indexes.contains(index))
? Colors.blue.withOpacity(0.5)
: Colors.transparent,
child: ListTile(
title: Text(listOfValues[index].todo),
subtitle: Text('Planned on ${listOfValues[index].date}'),
trailing: Text(listOfValues[index].time),
onLongPress: () {
setState(() {
lonPressEnabled = true;
if (!indexes.contains(index)) {
indexes.add(index);
selectedItems.add(listOfValues[index].id);
}
});
},
onTap: () {
setState(() {
if (indexes.isNotEmpty && lonPressEnabled) {
if (indexes.contains(index)) {
indexes.remove(index);
selectedItems.remove(listOfValues[index].id);
} else {
indexes.add(index);
selectedItems.add(listOfValues[index].id);
}
}
});
},
),
);
},
itemCount: listOfValues.length,
),
),
);}
Following block is used to delete items from database
ElevatedButton(
onPressed: () {
lonPressEnabled = false;
DatabaseModel model = DatabaseModel();
for (int i = 0; i < selectedItems.length; i++) {
model.deletingRecord(selectedItems[i]);
//selectedItems holds the ids of each todos
}
selectedItems = [];
indexes = [];
setState(() {
print();
});
},
child: const Icon(Icons.delete));
Following block deletes todo from the database
deletingRecord(int ids) async {
Database db = await openingDB;
db.rawDelete('delete from ToDos where id=$ids');}

Flutter Expansion Pannel not Expanding without immutable bool

I have an expansion panel in _buildCategoryListings() that does not expand when the header or the dropdown button is clicked. isExpanded is set to the boolean categoryView.isExpanded. Through printing via the console I can see that the setState is actually updating the bool value but it looks like the actual widget isn't being redrawn perhaps? If I manually set isExpanded to true I see the results I want from the GUI. I also had set isExtended to theExpanded (which is in MovieListingView) which raises the issue of a mutable variable being in a class that extends StatefulWidget, this did give me the desired results though.
The question: How do I get the expansion panel to update the categoryView.isExpanded (via theListings[panelIndex].isExpanded) bool and show it via the GUI?
Thank you in advance.
Side note I thought about using a provider to keep track of this bool but that seems like overkill.
class MovieListingView extends StatefulWidget {
#override
_MovieListingView createState() => _MovieListingView();
MovieListingView(this.movieList);
final MovieCatalog movieList;
//bool theExpanded = false;
List<MovieCategoryView> generateCategoryList() {
List<MovieCategoryView> tempList = [];
List<String> movieCategories = movieList.Categories;
movieCategories.forEach((category) {
MovieCategoryView categoryView = new MovieCategoryView(
movieCategoryName: category.toString(),
movieList: movieList.getMovieCardListByCategory(category));
tempList.add(categoryView);
});
return tempList;
}
}
class _MovieListingView extends State<MovieListingView> {
Widget build(BuildContext context) {
// TODO: implement build
return SingleChildScrollView(
physics: ScrollPhysics(),
padding: EdgeInsets.all(5.0),
child: _buildCategoryListings(),
);
}
List<MovieCategoryView> generateCategoryList() {
List<MovieCategoryView> tempList = [];
List<String> movieCategories = widget.movieList.Categories;
int counter = 0;
movieCategories.forEach((category) {
MovieCategoryView categoryView = new MovieCategoryView(
movieCategoryName: category.toString(),
movieList:
widget.movieList.getMenuItemCardListByCategory(category),
isExpanded: false);
tempList.add(categoryView);
});
return tempList;
}
Widget _buildCategoryListings() {
final List<MovieCategoryView> theListings = generateCategoryList();
return ExpansionPanelList(
expansionCallback: (panelIndex, isExpanded) {
setState(() {
theListings[panelIndex].isExpanded = !isExpanded;
//widget.theExpanded = !isExpanded;
});
},
children: theListings.map((MovieCategoryView movieCategoryView) {
return ExpansionPanel(
canTapOnHeader: true,
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(movieCategoryView.movieCategoryName),
);
},
body: Column(
children: movieCategoryView.movieList,
),
isExpanded: movieCategoryView.isExpanded);
}).toList(),
);
}
}
class MovieCategoryView {
MovieCategoryView(
{#required this.movieCategoryName,
#required this.movieList,
this.isExpanded});
String movieCategoryName;
List<MovieCard> movieList;
bool isExpanded = false;
}
This is happening because whenever the setstate() is called whole widget tree is rebuild and thus when you try changing the isexpandable value ,is gets changed but the
function generateCategoryList(); again gets called and generates the previous list again and again.
Widget _buildCategoryListings() {
final List<MovieCategoryView> theListings = generateCategoryList();
To fix this call the generateCategoryList(); once in initState() and remove the line above line.

apply multiple filters on a List (Flutter)

I have a method in my Provider to filter a list of Object :
List<Meal> applyFilter(bool snack, bool breakfast, bool dinner, bool lunch) {
if (breakfast == true) {
return _meals.where((element) => element.isBreakfast).toList();
}
if (snack == true) {
return _meals.where((element) => element.isSnack).toList();
}
if (dinner == true) {
return _meals.where((element) => element.isDinner).toList();
}
if (lunch == true) {
return _meals.where((element) => element.isLunch).toList();
}
return _meals;
}
and in my MainScreen I have a ListView builder like this here I have whole items in my list :
class _DietAvailableMealsState extends State<DietAvailableMeals> {
bool _breakFast = false;
bool _isSelected = false;
bool _snack = false;
bool _dinner = false;
bool _lunch = false;
#override
Widget build(BuildContext context) {
final mealsList = Provider.of<Meals>(context, listen: false);
List<Meal> customList = mealsList.applyFilter(_snack, _breakFast, _dinner, _lunch);
return Expanded(child:
istView.builder(
itemBuilder: (ctx, index) =>MealCard(customList[index]),
itemCount: customList.length,
),
);
now in my MainScreen I have some FilterChip I want to apply my method on this list and return a new list based on chosen filter :
FilterChip(
label: Text(widget.chipName),
selected:_isSelected,
backgroundColor: Colors.purple[50],
selectedColor: customGreen,
onSelected: (selected) {
setState(() {
_isSelected = selected;
_snack = !snack
});
},
);
everything works fine but I can't apply multiple filters for example when I choose Snack & Breakfast chip together I've got only breakfast list. It seems like a Choice chip not a FilterChip.
that's because you return from the function when you apply any filter,
try removing the return from every condition, create a list to apply the filters to, then return that list.
List<Meal> applyFilter(bool snack, bool breakfast, bool dinner, bool lunch) {
// cloned _meals into a new list to avoid changing the data in _meals
List<Meal> filteredList = List.from(_meals);
if (breakfast == true) {
// remove every element that does not satisfy the condition
filteredList.removeWhere((element) => !element.isBreakfast);
}
if (snack == true) {
filteredList.removeWhere((element) => !element.isSnack);
}
if (dinner == true) {
filteredList.removeWhere((element) => !element.isDinner);
}
if (lunch == true) {
filteredList.removeWhere((element) => !element.isLunch);
}
return filteredList;
}

How change value item in CheckBoxListTile?

I am trying to update the check of the CheckBoxListTile but I am not getting the desired result.
This is my code:
Widget build(BuildContext context) {
return Query(
options: QueryOptions(document: getVehiclesTypeQueryDoc),
builder: (result, {fetchMore, refetch}) {
if (result.isLoading && result.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
newVehiclesType = [];
final _vehiclesTypeJson = result.data['tipoVehiculos'] as List<dynamic>;
for (final i in _vehiclesTypeJson) {
var productMap = {
'id': i['id'],
'nombre': i['nombre'],
'imagenActivo': i['imagenActivo'],
'isSelected': true
};
newVehiclesType.add(productMap);
}
final List<dynamic> test = newVehiclesType;
print('test: $test');
return ListView.builder(
itemCount: test.length,
shrinkWrap: true,
itemBuilder: (context, index) {
print(index);
print('VALO: ${test[index]['isSelected']}');
return CheckboxListTile(
title: Text(test[index]['nombre'].toString()),
value: test[index]['isSelected'],
onChanged: (bool newValue) {
setState(() {
test[index]['isSelected'] = newValue;
});
},
);
},
);
},
);
}
I created a newVehiclesType variable, the query did not give me a variable to use the check for.
I am new to flutter.
this is the code that i use in my application hopefully hopefully it will help you
value: isCheckedEdit.contains(_ingredientList[index].nameIngredient),
onChanged: (value) {
if (value) {
setState(() {
isCheckedEdit
.add(_ingredientList[index].nameIngredient);
print(isCheckedEdit);
});
} else {
setState(() {
isCheckedEdit.remove(
_ingredientList[index].nameIngredient);
print(isCheckedEdit);
});
}
},
);
Angular has an amazing documentation that teaches every concept in detail (60%). I highly recommend you to visit the the documentation site. There are examples and explanations for each topic.
https://angular.io/guide/reactive-forms
Thank you so much for bringing such a beautiful question up. Angular is love.
class VehiclesList extends StatefulWidget {
#override
_VehiclesListState createState() => _VehiclesListState();
}
class _VehiclesListState extends State<VehiclesList> {
List<dynamic> test;
#override
Widget build(BuildContext context) {
// your code
// when result has data, instead of
// final List<dynamic> test = newVehiclesType;
// do this
test = newVehiclesType;
}
}
What this basically does is, it uses test to hold the widget state. In your code, test is local to your build function and would be reinitialized as many times build function runs.
This way it would not reinitialize on widget rebuilds when setState() is called.

How can I get the context outside of the build in flutter?

I use StreamProvider to receive firestore data in my app. And I use lazy_load_scrollview package for the pagination in the image gridview. In the StreamProvider I have to pass context to listen to data streams. Just like Provider.of<List>(context) . So I have to define it inside the build. But in the _loadMore() method I have defined in the code I need images(this is where I listen to the Stream) list to update the data list for pagination. Pagination works fine but when I first launch the app it only shows the loading indicator and does not load anything. When I swipe down the screen it starts loading and pagination works fine. To load the grid items when I first start, I need to call _loadMore() method in the initState(). I can't call it because it is inside the build. But I can't define that method outside of the build because it needs to define Stream listener(which is images). I can't get the context outside from the build to do that. Is there any way to get the context outside of the build ? or is there any better solution for pagination ? I would be grateful if you can suggest me a solution. here is my code,
class ImageGridView extends StatefulWidget {
#override
_ImageGridViewState createState() => _ImageGridViewState();
}
class _ImageGridViewState extends State<ImageGridView> {
List<GridImage> data = [];
int currentLength = 0;
final int increment = 10;
bool isLoading = false;
// I need to call _loadMore() method inside the initState
/*#override
void initState() {
_loadMore();
super.initState();
}*/
#override
Widget build(BuildContext context) {
// listening to firebase streams
final images = Provider.of<List<GridImage>>(context) ?? [];
Future _loadMore() async {
print('_loadMore called');
setState(() {
isLoading = true;
});
// Add in an artificial delay
await new Future.delayed(const Duration(seconds: 1));
for (var i = currentLength; i < currentLength + increment; i++) {
if (i >= images.length) {
setState(() {
isLoading = false;
});
print( i.toString());
} else {
data.add(images[i]);
}
}
setState(() {
print('future delayed called');
isLoading = false;
currentLength = data.length;
});
}
images.forEach((data) {
print('data' + data.location);
print(data.url);
//print('images length ' + images.length.toString());
});
try {
return LazyLoadScrollView(
isLoading: isLoading,
onEndOfPage: () {
return _loadMore();
},
child: GridView.builder(
itemCount: data.length + 1,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemBuilder: (context, index) {
if (index == data.length) {
return CupertinoActivityIndicator();
}
//passing images stream with the item index to ImageGridItem
return ImageGridItem(gridImage: data[index],);
},
),
);
} catch (e) {
return Container(
child: Center(
child: Text('Please Upload Images'),
)
);
}
}
}