Related
I want to persist value after user leaves page, also I would like to persist selected values, so I found out shared prefernces and I save it locally, but when I left page and return it remains unselected.
So I decided to convert my multipleSelected list to String, because sharedprefernces can't save list of ints and sfter that save selected values in lists. So how can i solve that problem when user leaves page and selected items become unselected.
class DataBaseUser extends StatefulWidget {
const DataBaseUser({Key? key}) : super(key: key);
#override
State<DataBaseUser> createState() => _DataBaseUserState();
}
class _DataBaseUserState extends State<DataBaseUser> {
int index = 1;
/// add selected items from list
List multipleSelected = [];
/// another list to form the new list above previous one
List chosenListsAbove = [];
List basesNames = [];
SharedPreferences? sharedPreferences;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Typographys.primaryColor,
appBar: PreferredSize(
preferredSize: const Size(125, 125),
child: AppBarService(),
),
body: Column(
children: [
// chosenOne(),
Card(
color: Typographys.gradientCard2,
child: ExpansionTile(
iconColor: Colors.white,
maintainState: true,
title: Text(
'Bases',
style: TextStyle(
fontFamily: 'fonts/Montserrat',
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 35),
),
children: [
SizedBox(
height: 10,
),
getDataBaseList(),
SizedBox(
height: 22,
),
getUpdateBaseButtons(),
SizedBox(
height: 10,
),
],
),
),
],
),
);
}
Widget getDataBaseList() {
return FutureBuilder<List>(
future: BasesService().GetBases(),
builder: (context, snapshot) {
List? baseNames = snapshot.data;
print(baseNames);
return ListView.builder(
shrinkWrap: true,
itemCount: baseNames?.length ?? 0,
itemBuilder: (context, i) {
Future<void> _onCategorySelected(bool selected, id) async {
final pref = await SharedPreferences.getInstance();
if (selected == true) {
setState(() {
multipleSelected.add(id);
List<String> stringsList =
multipleSelected.map((i) => i.toString()).toList();
// store your string list in shared prefs
pref.setStringList("stringList", stringsList);
List<String> mList =
(pref.getStringList('stringList') ?? <String>[]);
print('HERE');
print(mList);
print('HERE 2');
});
} else {
setState(
() {
multipleSelected.remove(id);
},
);
}
}
return Column(
children: [
ListTile(
title: Padding(
padding: const EdgeInsets.only(left: 1.0),
child: Text(
baseNames?[i]['name'] ?? 'not loading',
style: TextStyle(
fontFamily: 'fonts/Montserrat',
fontSize: 24,
fontWeight: FontWeight.w900,
color: Colors.white),
),
),
leading: Checkbox(
activeColor: Colors.green,
checkColor: Colors.green,
side: BorderSide(width: 2, color: Colors.white),
value: multipleSelected.contains(
baseNames?[i]['id'],
),
onChanged: (bool? selected) {
_onCategorySelected(selected!, baseNames?[i]['id']);
},
)
//you can use checkboxlistTile too
),
],
);
},
);
},
);
}
Widget getUpdateBaseButtons() {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FutureBuilder<bool>(
future: BasesService().SelectBaseAsync(multipleSelected.cast()),
builder: (context, snapshot) {
return ElevatedButton(
onPressed: () {
if (snapshot.data == true) {
BasesService().SelectBaseAsync(multipleSelected.cast());
print(multipleSelected.cast());
print(multipleSelected);
successSnackBar();
} else {
notSuccessSnackBar();
}
},
child: Text(
'Send bases',
style: TextStyle(
fontFamily: 'fonts/Montserrat',
fontSize: 22,
fontWeight: FontWeight.w900,
color: Colors.white,
letterSpacing: 2),
),
style: ElevatedButton.styleFrom(
minimumSize: Size(200, 40),
primary: Colors.green,
onPrimary: Colors.white,
),
);
return Container();
})
],
),
);
}
If I understand you correclty, cant you just save items in WillPopScope like
return WillPopScope(
onWillPop: () async => SaveMyPreferences,
child: const Scaffold(
body: Container(
color: Colors.red,
size: 50.0,
),
),
);
I found a solution. If your data that you want to save comes from the API and is constantly updated (as it was in my case), then you do not need to use the shared preference package. This package will not help you. In my case, in order to save the checkboxes selected by the user and after reloading the page to show him which items in the list were selected (I use checkboxes), I write to a file on the device and then read the saved data from this file. So you are going to need path_provider package and dart:io and these two functions
to write from function where you choose items
_onCategorySelected(bool selected, id) async {
final Directory directory =
await getApplicationDocumentsDirectory();
if (selected == true) {
multipleSelected.add(id);
} else {
multipleSelected.remove(id);
}
final File file = File('${directory.path}/my_file.json');
file.writeAsStringSync('{"selected": $multipleSelected}');
setState(() {});
}
to read from file:
Future<String> read() async {
String text = '';
try {
final Directory directory =
await getApplicationDocumentsDirectory();
final File file = File('${directory.path}/my_file.json');
text = await file.readAsString();
print('HELLO');
multipleSelected = json.decode(text)["selected"];
} catch (e) {
print("Couldn't read file");
}
return text;
}
and before the listview.builder comes, you need to use read() function ro read the saved values from file.
It is not the greatest solution (maybe, the worst one), but if you haven't got enough time and you don't have any state management and you just need to solve issue right now, it can be really helpfull.
I was building a todo list app which uses a ListView.builder to render multiple task widgets which have three properties, title, checked, and starred. It works just fine until I star/check an existing task and add a new task, in which case the state of the previous task seems to jump to the newly added task. What could be causing the problem? Could this have something to do with keys?
class Main extends StatefulWidget {
#override
State<Main> createState() => _MyAppState();
}
class _MyAppState extends State<Main> {
var todoList = [];
var stars = 0;
void addStar() {
setState(() {
stars++;
});
}
void removeStar() {
if (stars > 0) {
setState(() {
stars--;
});
}
}
void addTodo(title) {
setState(() {
todoList.add(title);
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
floatingActionButton: Builder(builder: (context) {
return FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return NewTodo(addTodo: addTodo);
});
},
);
}),
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'My Day',
style:
TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
Container(
child: Row(
children: [
const Text(
'IMPORTANT: ',
style: TextStyle(
fontSize: 16.0,
letterSpacing: 1.0,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 58, 58, 58),
),
),
Text(
'$stars',
style: const TextStyle(
fontSize: 24.0,
letterSpacing: 1.0,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 253, 147, 8)),
),
const SizedBox(width: 20.0),
IconButton(
icon: Icon(Icons.more_vert, size: 24),
onPressed: () => {print('more ...')},
),
],
),
)
],
),
),
Expanded(
// height: 300,
child: todoList.length == 0
? Text('No Tasks Yet 💪',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.w600,
color: Colors.grey))
: ListView.builder(
itemCount: todoList.length,
itemBuilder: (context, index) {
return Todo(
title: todoList[(todoList.length - 1) - index],
addStar: addStar,
removeStar: removeStar);
}))
],
),
),
),
);
}
}
the app currently only has three files, below is a link to the gist.
https://gist.github.com/FENETMULER/eb4a898b82f9aa4c6a871a1fa9833c84
Change your logic like
: ListView.builder(
itemCount: todoList.length,
itemBuilder: (context, index) {
return Todo(
title: todoList[index], //this
addStar: addStar,
removeStar: removeStar);
},
),
In your code, you have this line:
title: todoList[(todoList.length - 1) - index],
Let's imagine you have 3 items in the list list = [1,2,3]; The ListView.builder should iterate through them by saying
itemBuilder: (context, index) {
return Text(list[index].toString());
},
This way, the ListView.builder Widget will return a text with '1', '2' and '3' in this order since you call list[index] for the length of list (3 times) and each time index increments (starting from 0) so you call list[0], list[1] and list [2] (which return the three texts '1', '2' and '3'.) You, however, call list[(list.length-1) - index], so when index iterates from 0 to 2 you get:
list [ 3 - 1 - 0 ] = list [2] = text '3'
list [ 3 - 1 - 1 ] = list [1] = text '2'
list [ 3 - 1 - 2 ] = list [0] = text '1'
with this you can clearly see that you get a reverse list, hence, if you use list.reversed, you will get the correct list.
The solution is to use Keys with a ListView and not a ListView.builder as it doesn't seem to work with ListView.builder, the problem actually comes from reversing the list, since when the list is reversed every time a new task is added the objects in the Widget Tree won't be in line with their corresponding Object in the Element Tree.
ListView(children: todoList.reversed.map((title) => Todo(title: title, key: ValueKey(title))).toList())
So I have managed to fetch the names of the folder and store them in a list. But the problem I am facing is is the list gets called twice so I tend to have duplicate items in my list view. I've asked similar questions about this but the solutions given don't work on my side.
I know the problem is me calling the getFolders() in the future(which is a bad practice) but that is the only way my code actually work. When I change my list to a type Future I can't use the .add() functionality.
Below is my code:
Here is where I have declared my list:
class Semester extends StatefulWidget {
final String value;
const Semester({Key? key, required this.value}) : super(key: key);
#override
State<Semester> createState() => _SemesterState();
}
class _SemesterState extends State<Semester> {
late List<String> courses = []; // possible culprit
Future<List<FirebaseFolder>>? listResult;
Future<List<String>> getFolders() async {
final storageRef = FirebaseStorage.instance.ref().child(widget.value);
final listResult = await storageRef.listAll();
for (var prefix in listResult.prefixes) {
courses.add(prefix.name);
}
return courses;
}
#override
void initState() {
// TODO: implement initState
super.initState();
getFolders();
}
So when I change the courses to Future<List> I can't use the courses.add(prefix.name) since it says it is not of type future:
And as you can see below I had to use the getFolder() function on my future for it to display contents on my listview, (NOTE: even if I use it on instantiating the result is still same:)
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
leading: kIconButton(),
elevation: 0,
centerTitle: true,
title: const Text("Semester", style: kTitleStyle),
backgroundColor: Colors.white,
),
body: FutureBuilder(
future: getFolders(), // possible culprit
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return const Center(
child: spinkitLines,
);
default:
return Column(
children: [
const SizedBox(
height: 20.0,
),
const Center(
child: Text(
'Tap to choose course',
style: kPlaceholderStyle,
),
),
const SizedBox(
height: 30.0,
),
Expanded(
child: ListView.builder(
itemCount: courses.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
String passedValue = widget.value;
String course = courses[index];
String value = "$passedValue""/""$course";
Get.to(() => CourseContent(value: value, courseChosen: course,));
},
child: Container(
height: 80,
decoration: const BoxDecoration(
color: Colors.black,
borderRadius:
BorderRadius.all(Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Color.fromARGB(75, 0, 0, 0),
blurRadius: 4,
spreadRadius: 0,
offset: Offset(0, 4))
],
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Text(
courses[index],
style: kCardTitleStyle,
),
),
),
),
),
);
}),
),
],
);
}
},
),
);
}
}
So I am looking for a way to fetch the list(folders prefixes) and display them in a list view. What I have tried above works but sometimes it displays duplicates which I can tell the getfolder() is being called multiple times.. Help me solve this.
So I came up with a clever and simple solution.
I wrapped my for loop with if to check if the array is empty. if not it won't run that code!
late List<String> courses = [];
Future<List<FirebaseFolder>>? listResult;
Future<List<String>> getFolders() async {
final storageRef = FirebaseStorage.instance.ref().child(widget.value);
final listResult = await storageRef.listAll();
if(courses.isEmpty){
for (var prefix in listResult.prefixes) {
courses.add(prefix.name);
}
}
return courses;
}
It is possible to make sure you get the data from snapshot like this way as shown on blow.
Note: elements in that child is an example.
if(!snapshot.hasData) {
return Center(
child: const CircularProgressIndicator(
backgroundColor: Colors.lightBlue,
),
);
}
class Header extends StatefulWidget {
#override
_HeaderState createState() => _HeaderState();
Future<List> getSearch() async {
List<String> searchList;
final List<DocumentSnapshot> documents =
(await FirebaseFirestore.instance.collection('movies').get()).docs;
searchList = documents
.map((documentSnapshot) => documentSnapshot['movieName'] as String)
.toList();
print(searchList); //This does print the data from the database.
return searchList;
}
}
The above code fetches data from FirebaseFirestore and also the print statement prints the list fetched.
class _HeaderState extends State<Header> {
final MenuController _controller = Get.put(MenuController());
#override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
color: kDarkBlackColor,
child: SafeArea(
child: Column(
children: [
Container(
padding: EdgeInsets.all(kDefaultPadding),
constraints: BoxConstraints(maxWidth: kMaxWidth),
child: Column(
children: [
Row(
children: [
Container(
decoration: BoxDecoration(
borderRadius:
new BorderRadius.all(Radius.circular(10.0)),
shape: BoxShape.rectangle,
border: Border.all(color: Colors.white)),
child: IconButton(
icon: Icon(
Icons.menu,
color: Colors.white,
),
onPressed: () {
_controller.openOrCloseDrawer();
},
),
),
Container(
width: MediaQuery.of(context).size.width / 7,
child: Image.asset('assets/images/logo.png')),
Spacer(),
Container(
color: Colors.white,
width: MediaQuery.of(context).size.width / 5,
child: TypeAheadField(
hideOnEmpty: true,
textFieldConfiguration: TextFieldConfiguration(
autofocus: true,
style: DefaultTextStyle.of(context)
.style
.copyWith(fontStyle: FontStyle.italic),
decoration: InputDecoration(
border: OutlineInputBorder())),
suggestionsCallback: (pattern) async {
return CitiesService.getSuggestions(pattern);
},
transitionBuilder:
(context, suggestionsBox, controller) {
return suggestionsBox;
},
itemBuilder: (context, suggestion) {
return ListTile(
title: Text(suggestion),
);
},
onSuggestionSelected: (suggestion) {},
)),
Spacer(),
Socal(),
//Spacer(),
],
),
],
),
)
],
),
),
);
}
}
class CitiesService {
static List<String> search = Header().getSearch() as List<String>; //This is not adding data to list
static List<String> getSuggestions(String query) {
print(search); //This does not print any thing.
List<String> matches = List();
matches.addAll(search);
matches.retainWhere((s) => s.toLowerCase().contains(query.toLowerCase()));
return matches;
}
}
I am trying to store the data fetched in getSearch() into search List so that I can use it to provide suggestions but the list is empty. I don't know if this is the correct way to convert future into list of strings. Help would be really appreciated. Also, if there is another way to implement search from FirebaseFirestore, please do let me know.
Thanks in advance.
The problem here is that getSearch is an async function, which mean that it return intially a Future while is awaiting the async part of the function to be fulfilled and continue to execute, so in order to capture that you should be using a future.then() notation, doing something like this while calling getSearch will fix the issue:
Header().getSearch().then((value) {
static List<String> search = value as List<String>;
...
});
recently I have followed a Youtube tutorial where it shows how to add product item to cart. But in the video, it didn't show how to delete an item from cart. Video link: https://www.youtube.com/watch?v=K8d3qqbP3qk
ProductModel.dart
class ProductModel{
String name;
int price;
String image;
ProductModel(String name, int price, String image){
this.name = name;
this.price = price;
this.image = image;
}
}
ProductScreen.dart
import 'package:flutter/material.dart';
import '../ProductModel.dart';
import 'package:ecommerce_int2/models/product.dart';
class ProductScreen2 extends StatelessWidget {
final ValueSetter<ProductModel> _valueSetter;
ProductScreen2(this._valueSetter);
List<ProductModel> products = [
ProductModel("Grey Jacket", 100, 'assets/jacket_1.png'),
ProductModel("Brown Pants", 60, 'assets/jeans_9.png'),
ProductModel("Grey Pants", 50, 'assets/jeans_6.png'),
ProductModel("Orange Pants", 70, 'assets/jeans_8.png'),
ProductModel("Long Jeans", 80, 'assets/jeans_2.png'),
ProductModel("Black and Blue Cap", 40, 'assets/cap_2.png'),
ProductModel("Black Cap", 30, 'assets/cap_6.png'),
ProductModel("Red Cap", 35, 'assets/cap_4.png'),
];
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.separated(
itemBuilder: (context, index){
return ListTile(
leading: Image.asset(
products[index].image,
width: 100,
height: 100,
fit: BoxFit.fitWidth,
),
title: Text(products[index].name),
trailing: Text("\RM${products[index].price}", style: TextStyle(color: Colors.redAccent, fontSize: 20, fontWeight: FontWeight.w500),),
onTap: (){
_valueSetter(products[index]);
},
);
},
separatorBuilder: (context, index){
return Divider();
},
itemCount: products.length
),
);
}
}
CheckoutScreen.dart
import 'package:flutter/material.dart';
import 'package:ecommerce_int2/screens/address/add_address_page.dart';
import 'package:device_apps/device_apps.dart';
class CheckoutScreen extends StatelessWidget {
final cart;
final sum;
CheckoutScreen(this.cart, this.sum);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
ListView.separated(
itemBuilder: (context, index){
return ListTile(
title: Text(cart[index].name),
trailing: Text("\RM${cart[index].price}", style: TextStyle(color: Colors.redAccent, fontSize: 20, fontWeight: FontWeight.w500),),
onTap: (){
},
);
},
separatorBuilder: (context, index){
return Divider();
},
itemCount: cart.length,
shrinkWrap: true,
),
Divider(),
Text("Total : \RM$sum", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), textAlign: TextAlign.right,),
SizedBox(
height: 20,
),
Text("Remarks", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),),
TextFormField(
decoration: InputDecoration(
hintText: ('Example: Red Cap: Free Size, Grey Jacket: UK, M Size'),
),
maxLines: 5,
),
SizedBox(
height: 50,
),
RaisedButton(
color: Theme.of(context).accentColor,
child: Text('Buy Now',style: TextStyle(color: Colors.white, fontSize: 20)),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => AddAddressPage()));
},
),
RaisedButton(
color: Theme.of(context).accentColor,
child: Text('Try Out',style: TextStyle(color: Colors.white, fontSize: 20)),
onPressed: () => DeviceApps.openApp('com.DefaultCompany.clothestryingfunction2'),
),
],
),
);
}
}
add_to_cart.dart
import 'package:ecommerce_int2/ProductModel.dart';
import 'package:ecommerce_int2/screens/CheckoutScreen.dart';
import 'package:ecommerce_int2/screens/ProductScreen.dart';
import 'package:flutter/material.dart';
class CartApp extends StatefulWidget {
#override
_CartAppState createState() => _CartAppState();
}
class _CartAppState extends State<CartApp> {
List<ProductModel> cart = [];
int sum = 0;
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text("Add To Cart"),
bottom: TabBar(
tabs: <Widget>[
Tab(text: "Products",),
Tab(text: "Cart",),
],
),
),
body: TabBarView(
children: <Widget>[
ProductScreen2((selectedProduct){
setState(() {
cart.add(selectedProduct);//update
sum = 0;
cart.forEach((item){
sum = sum + item.price;
});
});
}),
CheckoutScreen(cart, sum),
],
),
),
);
}
}
The goal is to remove the selected item from cart and minus the selected item's price from the sum. Can anyone tell me how to do that?
First, you need to add a callback in the CheckoutScreen:
class CheckoutScreen extends StatelessWidget {
final cart;
final sum;
final ValueSetter<ProductModel> _valueDeleter;
CheckoutScreen(this.cart, this.sum, this._valueDeleter);
...
After that, add the callback function when using it in the TabBarView in CartApp:
CheckoutScreen(cart, sum, (deleteProduct) {
setState(() {
// Use this loop instead of cart.removeWhere() to delete 1 item at a time
for (var i = 0; i < cart.length; i++) {
if (cart[i].name == deleteProduct.name) {
cart.removeAt(i);
break;
}
}
sum = 0;
cart.forEach((item) {
sum = sum + item.price;
});
});
}),
Finally, add a button in the ListTile within CheckoutScreen to initiate the delete action (I'm using a Row in title here for simplicity):
ListTile(
...
title: Row(
children: [
IconButton(
icon: Icon(Icons.delete),
color: Colors.red,
onPressed: () => _valueDeleter(cart[index]),
),
Text(cart[index].name),
],
),
trailing: Text(
...