Flutter Build Tiles onTap showing error " undefined name 'context'" - flutter

I am trying to build multi level list view, when we tap child items, it should pass parameter to other page named QuizOptionsDialog.
1) I am able to print the root.name using print(root.name); using onTap
2) but when we try to navigate using following code it is showing undefined name 'context'
Full Code and error
enter code hereCode page 1/2
enter code hereCode page 2/2
enter code hereError
import 'package:flutter/material.dart';
import 'package:iti/quiz/ui/widgets/quiz_options.dart';
class ExpansionTileDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('List of Question Papers'),
),
body: ListView.builder(
itemCount: data.length,
itemBuilder: (BuildContext context, int index) => CategoryItem(
data[index],
),
),
),
);
}
}
// Welcome to another flutter tutorial
// In this video we will see how to create a multi-level Expansion List
// First Let's create a class for each row in the Expansion List
class Category {
final String id;
final String name;
final List<Category>
children; // Since this is an expansion list ...children can be another list of entries
Category(this.id, this.name, [this.children = const <Category>[]]);
}
// This is the entire multi-level list displayed by this app
final List<Category> data = <Category>[
Category(
'1',
'Main Category',
<Category>[
Category(
'1.1',
'Sub Category',
<Category>[
Category('1.1.1', 'Sub-Sub Category', <Category>[
Category('1.1.1.1', 'Sub-Sub-Sub Category',),
Category('1.1.1.2', 'Sub-Sub-Sub Category',),
]),
Category('1.1.2','Sub-Sub Category',
<Category>[
Category('1.1.2.1', 'Sub-Sub-Sub Category',),
Category('1.1.2.2', 'Sub-Sub-Sub Category',),
]
),
Category('1.1.3', 'Sub-Sub Category',
<Category>[
Category('1.1.3.1', 'Sub-Sub-Sub Category',),
Category('1.1.3.2', 'Sub-Sub-Sub Category',),
]
),
],
),
],
),
];
// Create the Widget for the row
class CategoryItem extends StatelessWidget {
const CategoryItem(this.category);
final Category category;
// This function recursively creates the multi-level list rows.
Widget _buildTiles(Category root) {
if (root.children.isEmpty) {
return ListTile(
title: Text(root.name),
onTap: () {
print(root.name);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuizOptionsDialog(category: category,),
),
);
},
);
}
return ExpansionTile(
key: PageStorageKey<Category>(root),
title: Text(root.name),
children: root.children.map<Widget>(_buildTiles).toList(),
);
}
#override
Widget build(BuildContext context) {
return _buildTiles(category);
}
_categoryPressed(BuildContext context,Category category) {
showModalBottomSheet(
context: context,
builder: (sheetContext) => BottomSheet(
builder: (_) => QuizOptionsDialog(category: category,),
onClosing: (){},
),
);
}
}

Change your CategoryItem class to also accept a BuildContext context variable in the constructor and assign it to a BuildContext variable like you did with category, and then pass that context in from the main widget when creating new CategoryItem. This will give you access to a context and should allow you to do your navigation.
To do so:
Storing the context in your CategoryItem class so each instance has access to it.
class CategoryItem extends StatelessWidget {
const CategoryItem(this.category, this.context);
final Category category;
final BuildContext context;
Updating the ListView.builder() to instantiate the updated CategoryItem class appropriately and pass the context.
body: ListView.builder(
itemCount: data.length,
itemBuilder: (BuildContext context, int index) => CategoryItem(
data[index], context
),
),
And now you should have access to the BuildContext for your page where you needed it.

Related

Nullable class instance with Provider

I have a class named User which basically has two variables. name and age. Also, I have another class named UserList.The aim of this class is to add the user objects to a list and return that list.
User model
class User with ChangeNotifier{
late String? name;
late int? age;
//set user name;
setName(String name){
this.name=name;
}
//set user age
setAge(int age){
this.age=age;
}
}
UserList class
class UserList with ChangeNotifier{
final List<User> _list=[];
//add a new user to the list.
void addUserToList(User user){
_list.add(user);
notifyListeners();
}
//return the private list of users.
List<User>getUsers() =>_list;
}
Here is the scenario. I want to insert a value from one page and view that inserted value on the second page. Look at the pictures below. On the first page,
I insert the age and name values
Assign those values in a User object instance (user.setName, user.setAge)
Add that user object instance in a UserList list
Use provider to provide that list.(Provider.of<UserList>(context).addUserToList(user);
First Page
First Page Code
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserList()),
ChangeNotifierProvider(create: (context) => User()),
],
child: const MyApp())
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
TextEditingController nameController = TextEditingController();
TextEditingController ageController = TextEditingController();
#override
Widget build(BuildContext context) {
User user =User();
return Scaffold(
backgroundColor: Colors.grey[300],
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
hintText: "Name",
),
),
TextField(
controller: ageController,
decoration: const InputDecoration(
hintText: "Age",
),
),
ElevatedButton(onPressed: (){
//providing userList
user.setName(nameController.text); //setting user name
user.setAge(int.parse(ageController.text)); // setting user age
Provider.of<UserList>(context,listen: false).addUserToList(user); //providing the list of users
nameController.text=""; //after insertion, clearing the text field
ageController.text="";//after insertion, clearing the text field
user=User(); // instantiating a new user object for the following insertion.
Navigator.push(context, MaterialPageRoute(builder: (context)=>const ViewUser()));
}, child: const Text("Submit"))
],
),
),
);
}
}
On the second page, display those inserted values in a ListView. Let's head to the main problem. Each item of ListViewis wrapped with a GestureDetector to be able to gain functionality.I hope everything is clear up to now.
The problem is that; When I click on each item I want to return to the first page. But, this time the TextField shouldn't be empty. It must be replaced with the value of that item. For exmple. If I click on John Nash and 43. The First page must pop up with field value of that list item. To be able to do that I use another Provider to provide User object. But, I couldn't do it because of null safety it gives an error about the user object is null. Is there any idea that could be helpful.
Second Page
Second Page code
class ViewUser extends StatelessWidget {
const ViewUser({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
List<User> userList = Provider.of<UserList>(context).getUsers();
return Scaffold(
appBar: AppBar(
title: const Text("Users List"),
),
body: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: userList.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: [
const SizedBox(height: 15,),
GestureDetector(
onTap: (){
Provider.of<User>(context,listen: false)
.setName(userList[index].name!);
Provider.of<User>(context,listen: false)
.setAge(userList[index].age!);
Navigator.push(context, MaterialPageRoute(builder: (context)=>MyHomePage()));
},
child: Container(
height: 50,
color: Colors.amber,
child: Center(
child: Row(
children: [
Text("Name: " + userList[index].name!),
const SizedBox(
width: 40,
),
Text("Age:" + userList[index].age.toString()),
],
),
),
),
),
],
);
}),
);
}
}
While you are using Navigator.push you can pass user as arguments.
GestureDetector(
onTap: () {
final user = userList[index];
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MyHomePage(),
settings: RouteSettings(
arguments: user,
)),
);
},
MyHomePage build method will be
#override
Widget build(BuildContext context) {
User user = User();
User? args = ModalRoute.of(context)?.settings.arguments as User?;
if (args != null) {
nameController.text = args.name ?? "";
ageController.text = args.age.toString();
}
return Scaffold(....
You can also use Navigator.pop in this case.
GestureDetector(
onTap: () {
final user = userList[index];
Navigator.pop(context, user);
},
And to receive data on MyHomePage
onPressed: () async {
//....
final data = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ViewUser(),
),
);
if (data != null) {
nameController.text = data.name ?? "";
ageController.text = data.age.toString();
}
More about navigate-with-arguments.

Flutter: What is the best way to filter Firestore collection items based on user input?

[CASE]
I have a list of activities stored in a FireStore collection. Each activity has a type. In a dropdown, the user can filter the activities based on their type. There is also an option to select all items.
I currently manage this by passing the activity type to a new widget each time the user changes the value from the dropdown. However, I was wondering if this is the most efficient way of doing this (mainly when scaling up the number of activities in the collection), or if there is a different best practice I should use?
[CODE]
Screen 1: Dropdown and list of activities
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String eventClass;
final items = ["All", "Music", "Drinks"];
void _changeClass(selectedClass) {
eventClass = selectedClass;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(height: 10),
DropdownSearch<String>(
mode: Mode.BOTTOM_SHEET,
showSelectedItem: true,
items: items,
label: "Change event type",
onChanged: (String selectedClass) => _changeClass(selectedClass),
selectedItem: items[0]),
Expanded(
child: EventList(eventClass),
)
],
);
}
}
Screen2: EventList
class EventList extends StatelessWidget {
final String eventClass;
EventList(this.eventClass);
#override
Widget build(BuildContext context) {
final Stream<QuerySnapshot> _usersStream = FirebaseFirestore.instance
.collection('events')
.where('eventClass', isEqualTo: eventClass.toLowerCase())
.snapshots();
return StreamBuilder<QuerySnapshot>(
stream: _usersStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView(
children: snapshot.data.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data = document.data();
return ListTile(
leading: CircleAvatar(
child: Icon(Icons.access_alarm),
foregroundColor: Colors.white,
backgroundColor: Theme.of(context).primaryColor,
),
title: Text(data['eventName']),
subtitle: Text(data['eventDescription']),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EventItem(data),
),
);
},
);
}).toList(),
);
},
);
}
}
ListView constructor is generally used for creating a limited number of items, but in order to create a large list you can use ListView.builder constructor. This constructor is appropriate in this scenario as the builder is called only for those items that are actually visible.
The ListView.builder() constructor creates items as they’re scrolled onto the screen as opposed to the default ListView constructor, which requires creating all items at once. Please refer to this document for more details on this with an interactive example.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index]),
);
},
)

Provider - Selector not updating UI for list items

I have a ListView consists of several ListTiles which have a trailing icon. The color of icon should change from transparent to green based on user tap. However the UI is not updating on user interaction.
The ServiceModel is like this.
class ProviderService extends ChangeNotifier {
final List<String> totalNames = ['Somesh', 'Tarulata', 'Indranil', 'Satyajyoti', 'Biswas', 'Sajal', 'Kumar', 'Slipa', 'Sonam', 'Neelam'];
List<String> _selectedNames = [];
List<String> get selectedNames => _selectedNames;
void updateselectedNames(String name) {
bool isExists = _selectedNames.contains(name);
if (isExists)
_selectedNames.remove(name);
else
_selectedNames.add(name);
notifyListeners();
}
}
The ListView goes like this.
class Members extends StatelessWidget {
#override
Widget build(BuildContext context) {
ProviderService plService = Provider.of<ProviderService>(context, listen: false);
return Scaffold(
body: SafeArea(
child: Selector<ProviderService, List<String>>(
selector: (_, service) => service.selectedNames,
builder: (context, selNames, child) {
if (plService.totalNames.isEmpty) return child;
return ListView.separated(
shrinkWrap: true,
itemBuilder: (context, index) {
String _name = plService.totalNames[index];
return ListTile(
title: Text('$_name'),
trailing: Icon(
Icons.check_circle,
color: selNames.contains(_name) ? Colors.lightGreen : Colors.transparent,
),
onTap: () {
plService.updateselectedNames(_name),
print(selNames);
},
);
},
separatorBuilder: (_, __) => Divider(),
itemCount: plService.totalNames.length,
);
},
child: Center(
child: Text('No names have been found', textAlign: TextAlign.center),
),
),
),
);
}
}
and of course the main.dart is like this.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(
create: (context) => ProviderService(),
child: Members(),
),
);
}
}
Even though the list selectedNames updated, the UI remains same. What's going on wrong here ?
You may add the shouldRebuild parameter of Selector and return true。like this:
Selector<ProviderService, List<String>>(
selector: (_, service) => service.selectedNames,
builder: (context, selNames, child) {...},
shouldRebuild: (previous, next) => true,
)
When you use a Selector, you have to make sure that the selected object is immutable.
Selector<ProviderService, List<String>>(
selector: (_, service) => service.selectedNames,
builder: (context, selNames, child) { ...},
),
builder will only get called once because your selectedNames object always stays the same. You are removing and adding items in the same array Object.
So, you should instead provide a new array in your updateselectedNames:
void updateselectedNames(String name) {
_selectedNames = _selectedNames.contains(name)
? _selectedNames.where((item) => item != name).toList()
: [..._selectedNames, name];
notifyListeners();
}
My way would be like this for your scenario.
class ProviderService extends ChangeNotifier {
final List<Name> totalNames = [
Name(name: 'Somesh', isTransparent: false),
Name(name: 'Tarulata', isTransparent: false),
];
List<Name> _selectedNames = [];
List<Name> get selectedNames => _selectedNames;
void updateselectedNames(int index) {
var exist = _isExist(totalNames[index]);
if(exist){
_selectedNames.remove(totalNames[index]);
} else {
_selectedNames.add(totalNames[index]);
}
totalNames[index].isTransparent = !totalNames[index].isTransparent;
notifyListeners();
}
bool _isExist(Name name) {
var filter = _selectedNames.singleWhere(
(element) => element.name == name.name,
orElse: () => null,
);
return filter != null;
}
}
class Name {
String name;
bool isTransparent;
Name({this.name, this.isTransparent});
}
And you can use Selector in ListView for every ListTile
Selector<ProviderService, Name>(
selector: (_, service) => service.totalNames[index],
builder: (context, name, child) {
return ListTile(
title: Text('${name.name}'),
trailing: Icon(
Icons.check_circle,
color: !name.isTransparent ? Colors.lightGreen : Colors.transparent,
),
onTap: () {
plService.updateselectedNames(index),
},
);

Get index of an Item from an other List

I started to use providers but I have a problem. I want to get the index of items that are in an other list in an other screen. How can i get them ? I have two screens: a home screen and a favorite screen and I have a listView in each. I want to get the index of the item in the home screen when it is remove from the favorite screen. This is the link of my code on GitHub : https://github.com/Rianou20/my_app_from_scratch/tree/master/my_app_from_scratch. And some relevant parts of my code :
favModel.dart
class FavModel extends ChangeNotifier {
List<Item> favList = [];
List<bool> isInFav = [];
addInFavorite(title, description, index){
Item item = Item(title: title, description: description, );
favList.add(item);
isInFav[index] = true;
notifyListeners();
}
removeOfFavorite(int index, int index2){
favList.removeAt(index);
isInFav[index2] = false;
notifyListeners();
}
implement(){
isInFav.add(false);
}
}
favorite_screen.dart
class Favorite extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Favorite'),
),
body: Consumer<FavModel>(
builder: (context, favModel, child) {
return ListView.builder(
itemCount: favModel.favList.length,
itemBuilder: (context, index) {
return TextObject(favModel.favList[index].title,
favModel.favList[index].description),
Padding(
padding: const EdgeInsets.all(7.0),
child: GestureDetector(
child: Icon(
Icons.favorite,
color: Colors.red,
size: 32,
),
onTap: () {
favModel.removeOfFavorite(index, index);
}),
),
});
},
),
);
}
}
home_screen.dart
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
actions: [
IconButton(
icon: Icon(Icons.favorite_border),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return Favorite();
},
),
),
),
],
),
body: Consumer<FavModel>(builder: (context, favModel, child) {
return ListView.builder(
shrinkWrap: false,
itemCount: itemData.length,
itemBuilder: (context, index) {
favModel.implement();
return TextObject(
itemData[index].title, itemData[index].description),
Padding(
padding: const EdgeInsets.all(7.0),
child: GestureDetector(
child: Icon(
favModel.isInFav.elementAt(index)
? Icons.favorite
: Icons.favorite_border,
color:
favModel.isInFav[index] ? Colors.red : null,
size: 32,
),
onTap: () {
favModel.isInFav[index]
? null
: Provider.of<FavModel>(context,
listen: false)
.addInFavorite(
itemData[index].title,
itemData[index].description,
index,
);
}),
);
});
}),
);
}
}
Where I want to get the index is in the favorite_screen.dart at this line favModel.removeOfFavorite(index, index);
Without knowing the exact use case, you can potentially store the removed values in a list and use them on your home screen.
class FavModel extends ChangeNotifier {
List<Item> favList = [];
List<bool> isInFav = [];
List<int> _removedItemIndexList = []
get removedItemIndexList => _removedItemIndexList;
addInFavorite(title, description, countdown, imageURL, index){
Item item = Item(title: title, description: description, countdown:countdown, imageURL: imageURL);
favList.add(item);
isInFav[index] = true;
notifyListeners();
}
removeOfFavorite(int index, int index2){
favList.removeAt(index);
isInFav[index2] = false;
_addToRemovedIndexList(index);
notifyListeners();
}
void _addToRemovedIndexList(int index) {
_removedItemIndexList.add(index);
}
implement(){
isInFav.add(false);
}
}
And then use on home_sreen.dart as
...
body: Consumer<FavModel>(builder: (context, favModel, child) {
List<int> removedIndexes = favModel.removedItemIndexList;
return ListView.builder( ... ) };
Note that the FavModel provider class must be lifted above then home_screen.dart on the widget tree in order to be able to access its values. i.e. you would want to do something like this in your main.dart
...
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: FavModel(),
),
],
child: MaterialApp(...

How to use dynamic global list in flutter

I am new to Flutter and attempting sample mutual fund app to cover all basic widgets.
Requirement -> After selecting MF scheme, when user confirms on "buyNow" screen, corresponding scheme should get added to global dynamic list in "Cart" screen. This is basically a cart which is accessible to user on any screen, similar to shopping cart. I want to update cart list on "buyNow" screen and display same on "Cart" screen.
I have followed link to learn about 'provider' method of flutter to solve this, but not able to do.
PFB code
Main.dart
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: MaterialApp(
home: Schemelist(),
routes: {
'/landing': (context) => Landing(),
'/schemelist': (context) => Schemelist(),
'/schemeBuy': (context) => SchemeBuy(),
'/buyNow': (context) => BuyNow(),
'/cart': (context) => Cart(),
},
),
),
);
}
Cartmodel.dart
import 'package:flutter/foundation.dart';
class CartModel with ChangeNotifier{
String schemeName;
String type;
String fromDate;
String toDate;
double amount;
List<CartModel> _cartList=[];
CartModel({this.amount,this.fromDate,this.schemeName,this.toDate,this.type});
void addToCart(CartModel cartObj){
_cartList.add(cartObj);
notifyListeners();
}
double get totalAmount =>
_cartList.fold(0, (total, current) => total + current.amount);
}
BuyNow.dart
RaisedButton(
onPressed: () {
_cart=new CartModel(amount:1000,fromDate:_dateTime.toString(),schemeName:widget.investmentObj.schemeName,toDate:_dateTime1.toString(),type:'SIP');
var cart = Provider.of<CartModel>(context);
cart.addToCart(_cart);
Navigator.pushNamed(context, '/cart');
},
child: Text('Yes'),
),
Cart.dart //where I will display dynamic list
Widget build(BuildContext context) {
var cart = Provider.of<CartModel>(context);
return Scaffold(
appBar: AppBar(
title: Text('Cart'),
centerTitle: true,
),
body: ListView.builder(
itemCount: --not able to access list--
itemBuilder: (context, index) => ListTile(
title: Text(
-------
),
),
),
);
}
First we should modify CartModel class. The fields (such as schemeName) should belong to the CartItem class, and the CartModel should only do its own thing (addToCart and others).
class CartModel with ChangeNotifier {
List<CartItem> _itemList = [];
// An unmodifiable view of the items in the cart.
UnmodifiableListView<CartItem> get itemList => UnmodifiableListView(_itemList);
void addToCart(CartItem item) {
_itemList.add(item);
notifyListeners();
}
double get totalAmount => _itemList.fold(0, (total, current) => total + current.amount);
}
class CartItem{
String schemeName;
String type;
String fromDate;
String toDate;
double amount;
CartItem({this.amount, this.fromDate, this.schemeName, this.toDate, this.type});
}
Then, in Cart.dart
Widget build(BuildContext context) {
var itemList = Provider.of<CartModel>(context).itemList;
return Scaffold(
appBar: AppBar(
title: Text('Cart'),
centerTitle: true,
),
body: ListView.builder(
itemCount: itemList.length,
itemBuilder: (_, index) {
var item = itemList[index];
return Text(item.schemeName);
},
),
);
}
You will get a error while click RaisedButton:
Tried to listen to a value exposed with provider, from outside of the widget tree.
This is likely caused by an event handler (like a button's onPressed) that called
Provider.of without passing `listen: false`.
To fix it, edit BuyNow.dart:
RaisedButton(
onPressed: () {
var _item = CartItem(amount: 1000, fromDate: _dateTime.toString(), schemeName: widget.investmentObj.schemeName, toDate: _dateTime1.toString(), type: 'SIP');
//just set listen to false
var cart = Provider.of<CartModel>(context, listen: false);
cart.addToCart(_item);
Navigator.pushNamed(context, '/cart');
},
child: Text('Yes'),
),