How do I show a snackbar from a StateNotifier in Riverpod? - flutter

I have the following class that is working fine.
class CartRiverpod extends StateNotifier<List<CartItemModel>> {
CartRiverpod([List<CartItemModel> products]) : super(products ?? []);
void add(ProductModel addProduct) {
bool productExists = false;
for (final product in state) {
if (product.id == addProduct.id) {
print("not added");
productExists = true;
}
else {
}
}
if (productExists==false)
{
state = [
...state, new CartItemModel(product: addProduct),
];
print("added");
}
}
void remove(String id) {
state = state.where((product) => product.id != id).toList();
}
}
The code above works perfectly. In my shopping cart, I want to limit the order of products to just 1 unit, that is why I am doing the code above. It works as I expected.
The only thing now is that, I'd like to show a snackbar alerting the user that he or she can only order 1 unit of each product.
How do I add a snackbar inside my StateNotifier?

DON'T show snackbar here.
You need to listen to the needed value in the widget tree as the follows:
#override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen(cartListPageStateNotifierProvider, (value) {
// show snackbar here...
});
...
}

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/all.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'cart_list_page.freezed.dart';
final cartListPageStateNotifierProvider =
StateNotifierProvider((ref) => CartListPageStateNotifier(ref.read));
final cartListProvider = StateProvider((ref) {
return <CartListItemModel>[];
}); // this holds the cart list, at initial, its empty
class CartListPage extends StatefulWidget {
#override
_CartListPageState createState() => _CartListPageState();
}
class _CartListPageState extends State<CartListPage> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
List<CartListItemModel> productsToBeAddedToCart = [
CartListItemModel(id: 1, name: "Apple"),
CartListItemModel(id: 2, name: "Tomatoes"),
CartListItemModel(id: 3, name: "Oranges"),
CartListItemModel(id: 4, name: "Ginger"),
CartListItemModel(id: 5, name: "Garlic"),
CartListItemModel(id: 6, name: "Pine"),
];
#override
Widget build(BuildContext context) {
return ProviderListener<CartListState>(
provider: cartListPageStateNotifierProvider.state,
onChange: (context, state) {
return state.maybeWhen(
loading: () {
final snackBar = SnackBar(
duration: Duration(seconds: 2),
backgroundColor: Colors.yellow,
content: Text('updating cart....'),
);
return _scaffoldKey.currentState.showSnackBar(snackBar);
},
success: (info) {
final snackBar = SnackBar(
duration: Duration(seconds: 2),
backgroundColor: Colors.green,
content: Text('$info'),
);
_scaffoldKey.currentState.hideCurrentSnackBar();
return _scaffoldKey.currentState.showSnackBar(snackBar);
},
error: (errMsg) {
final snackBar = SnackBar(
duration: Duration(seconds: 2),
backgroundColor: Colors.red,
content: Text('$errMsg'),
);
_scaffoldKey.currentState.hideCurrentSnackBar();
return _scaffoldKey.currentState.showSnackBar(snackBar);
},
orElse: () => SizedBox.shrink(),
);
},
child: Scaffold(
key: _scaffoldKey,
body: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
SizedBox(height: 40),
Expanded(
child: ListView.builder(
itemCount: productsToBeAddedToCart.length,
itemBuilder: (context, index) {
final item = productsToBeAddedToCart[index];
return ListTile(
title: Text("${item.name}"),
leading: CircleAvatar(child: Text("${item.id}")),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.add),
onPressed: () => context
.read(cartListPageStateNotifierProvider)
.addProduct(item),
),
SizedBox(width: 2),
IconButton(
icon: Icon(Icons.remove),
onPressed: () => context
.read(cartListPageStateNotifierProvider)
.removeProduct(item),
),
],
),
);
},
),
)
],
),
),
),
);
}
}
class CartListPageStateNotifier extends StateNotifier<CartListState> {
final Reader reader;
CartListPageStateNotifier(this.reader) : super(CartListState.initial());
addProduct(CartListItemModel product) async {
state = CartListState.loading();
await Future.delayed(Duration(seconds: 1));
var products = reader(cartListProvider).state;
if (!products.contains(product)) {
reader(cartListProvider).state.add(product);
return state =
CartListState.success("Added Successfully ${product.name}");
} else {
return state = CartListState.error(
"cannot add more than 1 product of the same kind");
}
}
removeProduct(CartListItemModel product) async {
state = CartListState.loading();
await Future.delayed(Duration(seconds: 1));
var products = reader(cartListProvider).state;
if (products.isNotEmpty) {
bool status = reader(cartListProvider).state.remove(product);
if (status) {
return state =
CartListState.success("removed Successfully ${product.name}");
} else {
return state;
}
}
return state = CartListState.error("cart is empty");
}
}
#freezed
abstract class CartListState with _$CartListState {
const factory CartListState.initial() = _CartListInitial;
const factory CartListState.loading() = _CartListLoading;
const factory CartListState.success([String message]) = _CartListSuccess;
const factory CartListState.error([String message]) = _CartListError;
}
#freezed
abstract class CartListItemModel with _$CartListItemModel {
factory CartListItemModel({final String name, final int id}) =
_CartListItemModel;
}

Related

Trying to use a nested set of PopupMenuButtons in a flutter app

Trying to use a set of nested PopupMenuButtons in a flutter app. The first menu opens as expected. The second menu opens only after tapping many times, closing the first menu, re-opening it, i.e. random behavior. Same is true for the third menu. Sometimes the first or second menu close prematurely without having collected all three pieces of information from the user. What is wrong in my code below???
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:localstore/localstore.dart';
import 'package:http/http.dart' as http;
late Map<String, dynamic> dirList;
List eventList = [];
List eventYearList = [];
List eventDayList = [];
String eventName = '';
String eventYear = '';
String eventDay = '';
String eventDomain = '';
late Map<String, String> eventInfo;
String eventTitle = "Selecteer een evenement";
// create a list of maptypes, just with the names of the maptypes in Dutch
const List ourMapTypes = ['Wegenkaart', 'Satelliet met labels',
'Satelliet zonder labels', 'Terrein', 'Open Sea Map'];
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late GoogleMapController mapController;
MapType currentMapType = MapType.normal;
final LatLng initialMapPosition = const LatLng(52.2, 4.535);
#override
void initState() {
super.initState();
}
Future<void> _onMapCreated(GoogleMapController controller) async {
mapController = controller;
// Get the list of events ready for selection
dirList = await fetchDirList();
dirList.forEach((k, v) => eventList.add(k));
eventYearList = [];
eventDayList = [];
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.green[900],
title: PopupMenuButton(
offset: const Offset(0,40),
child: Text(eventTitle),
itemBuilder: (BuildContext context) {
return eventList.map((events) {
return PopupMenuItem(
height: 30.0,
value: events,
child: PopupMenuButton(
offset: const Offset(30,0),
child: Text(events),
itemBuilder: (BuildContext context) {
return eventYearList.map((years) {
return PopupMenuItem(
height: 30.0,
value: years,
child: PopupMenuButton(
offset: const Offset(30,0),
child: Text(years),
itemBuilder: (BuildContext context) {
return eventDayList.map((days) {
return PopupMenuItem(
height: 30.0,
value: days,
child: Text(days)
);
}).toList();
},
onSelected: (eventDayList == []) ? null : newEventSelected,
),
);
}).toList();
},
onSelected: (eventYearList == []) ? null : selectEventDay,
),
);
}).toList();
},
onSelected: selectEventYear,
),
actions: <Widget>[
PopupMenuButton(
child: Image.asset('assets/images/mapicon.png'),
offset: Offset(0,55),
tooltip: 'Selecteer een kaarttype',
onSelected: selectMapType,
itemBuilder: (BuildContext context) {
return ourMapTypes.map((types) {
return PopupMenuItem(
height: 30.0,
value: types,
child: Text(types)
);
}).toList();
},
),
],
),
body: GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: initialMapPosition,
zoom: 12.0,
),
mapType: currentMapType,
),
bottomNavigationBar: Text('bottombar'),
),
);
}
// Routine to change the Type of the map based on the user selection
void selectMapType(selectedMapType) {
setState(() { // Causes the app to rebuild with the selected choice.
switch (selectedMapType) {
case "Wegenkaart":
currentMapType = MapType.normal;
break;
case "Satelliet met labels":
currentMapType = MapType.hybrid;
break;
case "Satelliet zonder labels":
currentMapType = MapType.satellite;
break;
case "Terrein":
currentMapType = MapType.terrain;
break;
case "Open Sea Map":
currentMapType = MapType.normal;
break;
default:
currentMapType = MapType.normal;
break;
}
});
}
void selectEventYear(event) {
setState(() {
eventName = event;
eventYearList = [];
dirList[event].forEach((k, v) => eventYearList.add(k));
eventYearList = eventYearList.reversed.toList();
eventDayList = [];
});
}
void selectEventDay(year) {
setState(() {
eventYear = year;
eventDayList = [];
eventTitle = eventName + '/' + year;
if (dirList[eventName][eventYear].length != 0) {
dirList[eventName][eventYear].forEach((k, v) => eventDayList.add(k));
} else {
newEventSelected('');
}
});
}
void newEventSelected(day) {
setState(() {
eventDay = day;
eventDomain = eventName + '/' + eventYear;
if (eventDay != '') eventDomain = eventDomain + '/' + eventDay;
eventTitle = eventDomain; // for the time being
eventYearList = [];
eventDayList = [];
});
}
Future<Map<String, dynamic>> fetchDirList() async {
final response = await http
.get(Uri.parse('https://tt.zeilvaartwarmond.nl/get-dirlist.php?tst=true&msg=simple'));
if (response.statusCode == 200) {
return (jsonDecode(response.body));
} else {
throw Exception('Failed to load dirList');
}
}
}
The default behavior of PopupMenuButton is to close it after selecting. While using nested PopupMenuButton you need to be careful about context, which one when and how it is closing.
Next issue comes from the padding of PopupMenuItem, each item does not take full size.
You can use PopupMenuItem's onTap or onSelected from PopupMenuButton to find selected value. If you want to update UI on dialog, check StatefulBuilder.
This is a test snippet:
PopupMenuButton(
child: const Text("POP U"),
onSelected: (value) {
print(value);
},
itemBuilder: (BuildContext context_p0) {
return [
const PopupMenuItem(value: "item: p1", child: Text("Item:p1 ")),
PopupMenuItem(
value: "item: p1",
onTap: () {},
padding: EdgeInsets.zero,
child: PopupMenuButton(
padding: EdgeInsets.zero,
child: Container(
alignment: Alignment.center,
height: 48.0, //default height
width: double.infinity,
child: Text("inner PopUp Menu"),
),
itemBuilder: (context_p1) {
return [
PopupMenuItem(
value: "inner p2",
child: Text("inner p2: close with parent "),
onTap: () {
Navigator.of(context_p1).pop();
},
),
const PopupMenuItem(
value: 'inner p1',
child: Text("inner p1, just close this one"),
),
];
},
),
)
];
},
),

Not able to display initial data from server using provider

I am trying to display some initial data that gets pulled from a server in my app, I get the data but I am not able to display it. Here is my code please help
Class where data has to be displayed
import 'package:deep_pocket/models/data_feed.dart';
import 'package:deep_pocket/models/mock_data.dart';
import 'package:deep_pocket/widgets/menu_buttons.dart';
import 'package:deep_pocket/widgets/post_widget.dart';
import 'package:deep_pocket/screens/user_input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
class feedScreen extends StatefulWidget {
static const route = '/feed-screen';
#override
State<feedScreen> createState() => _feedScreenState();
}
class _feedScreenState extends State<feedScreen> {
int filter = 0;
var _intstate = true;
void updateFilter(tx, context) {
setState(() {
filter = tx;
});
Navigator.of(context).pop();
}
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
void didChangeDependencies() {
if (_intstate) {
Provider.of<mockData>(context).fetchandAddPost();
}
_intstate = false;
// TODO: implement didChangeDependencies
super.didChangeDependencies();
}
void filterSheet(ctx) {
showModalBottomSheet(
context: ctx,
builder: (ctx) => Container(
height: 300,
child: SingleChildScrollView(
child: Container(
height: 280,
child: ListView.builder(
itemCount: Tag.length,
itemBuilder: (ctx, i) => TextButton(
onPressed: () {
return updateFilter(i, context);
},
child: Text(Tag[i]),
)),
)),
));
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<mockData>(
create: (context) => mockData(),
builder: (context, child) {
var posts = context.select((mockData m) => m.items);
print(posts.length);
if (filter != 0) {
posts = posts.where((i) => i.tag == filter).toList();
}
return Scaffold(
// drawer: Drawer(
// // Populate the Drawer in the next step.
// ),
appBar: AppBar(
title: const Text("Home"),
actions: [
TextButton(
onPressed: () => {filterSheet(context)},
child: const Text(
"Filters",
style: TextStyle(color: Colors.white),
))
],
),
body: SingleChildScrollView(
child: Column(
children: [
menuButtons(),
Container(
padding: const EdgeInsets.all(8),
child: ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: posts.length,
itemBuilder: (ctx, i) => postWidget(post: posts[i])),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
//Waiting for result
var newData =
await Navigator.pushNamed(context, userInput.route);
if (newData != null) {
context.read<mockData>().addPost(newData as dataFeed);
}
},
child: const Icon(Icons.add)),
);
});
}
}
Class where I have my Provider and fetch setup
import 'package:deep_pocket/models/data_feed.dart';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class mockData with ChangeNotifier {
List<dataFeed> _data = [
// dataFeed(
// id: DateTime.now().toString(),
// imgsrc: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
// name: "Priyam Srivastava",
// title: "How to change room ?",
// tag: 1,
// text:
// "I would like to know the process of changing my room cause I have not been able to study, and my roomate always plays music and drinks too much then shouts all night, please tell me how",
// ),
// dataFeed(
// id: DateTime.now().toString(),
// imgsrc: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
// title: "Anyone intresed in playing BGMI?",
// name: "Part Agarwal",
// tag: 2,
// text:
// "So I have been looing for a squad for a long time and now i have finally decided that I am gonna buckle up and ask you all to join me",
// ),
// dataFeed(
// id: DateTime.now().toString(),
// imgsrc: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
// title: "How to solve this question in O(n) complexity",
// name: "Preet Singh",
// tag: 3,
// text:
// "So I have been looing for a squad for a long time and now i have finally decided that I am gonna buckle up and ask you all to join me",
// ),
];
List<dataFeed> get items {
return [..._data];
}
Future<void> fetchandAddPost() async {
var url = link;
try {
print("getting your data");
final response = await http.get(url);
final extractedData = json.decode(response.body) as Map<String, dynamic>;
final List<dataFeed> loadedPosts = [];
extractedData.forEach((key, value) {
loadedPosts.add(dataFeed(
id: key,
imgsrc: value['imgsrc'],
name: value['name'],
title: value['title'],
text: value['text'],
date: value['date']));
});
print(loadedPosts.length);
_data = loadedPosts;
print(_data.length);
print("got your data");
notifyListeners();
} catch (e) {
print(e);
// TODO
}
}
Future<void> addPost(dataFeed newpost) async {
var url = link;
try {
final response = await http.post(
url as Uri,
body: json.encode({
'imgsrc': newpost.imgsrc,
'name': newpost.name,
'title': newpost.title,
'text': newpost.text,
'tag': newpost.tag,
'date': newpost.date,
}),
);
final newPost = dataFeed(
id: json.decode(response.body)['name'],
imgsrc: newpost.imgsrc,
name: newpost.name,
title: newpost.title,
text: newpost.text,
tag: newpost.tag,
date: newpost.date);
_data.insert(0, newPost);
notifyListeners();
} catch (e) {
print(e);
// TODO
}
}
}
I am getting data from server but it isn't being displayed, if I add new data it gets displayed.
This is the basic use age for using Provider, fetch data from network, updating ListView and pagination.
class ExampleWidget extends StatelessWidget {
const ExampleWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<ExampleChangeNotifier>(
create: (_) => ExampleChangeNotifier.instance(),
builder: (_, child) {
return Selector<ExampleChangeNotifier, NetworkStatus>(
selector: (_, model) => model.networkStatus,
builder: (_, nStatus, __) => nStatus == NetworkStatus.Loading
? const Center(
child: CircularProgressIndicator(),
)
: nStatus == NetworkStatus.Error
? const Center(
child: Text('Your error widget'),
)
: Selector<ExampleChangeNotifier, int>(
selector: (_, model) => model.listLength,
builder: (_, length, __) => length > 0
? ListView.builder(
itemCount: length + 1,
itemBuilder: (context, index) {
if (index < length) {
var listItem = _.read<ExampleChangeNotifier>().list[index];
return SizedBox(
height: 60,
child: Text('List item: ${listItem.whatever}'),
);
} else {
return Center(
child: ElevatedButton(
onPressed: () {
_.read<ExampleChangeNotifier>().loadMore();
},
child: Selector<ExampleChangeNotifier, bool>(
selector: (_, model) => model.loadMoreRequest,
builder: (_, value, __) => value ? const Text('loading...') : const Text('load more'),
),
),
);
}
},
)
: const Center(
child: Text('No data found'),
),
),
);
},
);
}
}
enum NetworkStatus { Loading, Done, Error }
class ExampleChangeNotifier extends ChangeNotifier {
NetworkStatus _networkStatus = NetworkStatus.Loading;
NetworkStatus get networkStatus => _networkStatus;
final List<dynamic> _list = <dynamic>[];
List<dynamic> get list => _list;
int _listLength = 0;
int get listLength => _listLength;
int _skip = 0; //Send this in your request parameters and use for pagination, e.g (for mysql query) => ... DESC LIMIT _skip, 10
bool _loadMoreRequest = false;
bool get loadMoreRequest => _loadMoreRequest;
ExampleChangeNotifier.instance() {
_getDataFromNetwork();
}
Future<void> _getDataFromNetwork() async {
try {
//Make your http request
// For example : await http.get('https:example.com?skip=$_skip');
_loadMoreRequest = false;
// ... Parse your data
List<dynamic> networkData = <dynamic>[];
_networkStatus = NetworkStatus.Done;
if (networkData.isNotEmpty) {
for (var item in networkData) {
_list.add(item);
_listLength++;
}
}
notifyListeners();
} catch (e) {
_loadMoreRequest = false;
_networkStatus = NetworkStatus.Error;
notifyListeners();
}
}
Future<void> loadMore() async {
_skip = _listLength;
_loadMoreRequest = true;
notifyListeners();
await _getDataFromNetwork();
}
}

Flutter BLoC - state doesn't triggers widget rebuild

the app is simple categories/products display , everything works fine except select a product from a category products and swip back to the products widget , the state changes and it's neither one of the states i created and just shows a loading indicator ( ProductsWrapper default return from state).
so here is the code :
ProductBloc :
class ProductBloc extends Bloc<ProductEvent, ProductState> {
final ProductRepository productRepository;
ProductBloc({required this.productRepository}) : super(ProductsEmpty());
#override
Stream<Transition<ProductEvent, ProductState>> transformEvents(
Stream<ProductEvent> events,
TransitionFunction<ProductEvent, ProductState> transitionFn) {
return super.transformEvents(
events.debounceTime(const Duration(microseconds: 500)), transitionFn);
}
#override
Stream<ProductState> mapEventToState(ProductEvent event) async* {
if (event is FetchProducts) {
yield* _mapFetchProductsToState(event);
} else if (event is RefreshProducts) {
yield* _mapRefreshProductsToState(event);
} else if (event is FetchProduct) {
yield* _mapFetchProductToState(event);
} else if (event is RefreshProduct) {
yield* _mapRefreshProductToState(event);
}
}
Stream<ProductState> _mapFetchProductsToState(FetchProducts event) async* {
try {
final products =
(await productRepository.getCategoryProducts(event.categoryId));
yield ProductsLoaded(products: products.products!);
} catch (_) {
yield state;
}
}
Stream<ProductState> _mapRefreshProductsToState(
RefreshProducts event) async* {
try {
final products =
await productRepository.getCategoryProducts(event.categoryId);
yield ProductsLoaded(products: products.products!);
return;
} catch (_) {
yield state;
}
}
Stream<ProductState> _mapFetchProductToState(FetchProduct event) async* {
try {
final product =
(await productRepository.getProductDetails(event.productId));
yield ProductLoaded(product: product);
} catch (e) {
yield state;
}
}
Stream<ProductState> _mapRefreshProductToState(RefreshProduct event) async* {
try {
final product =
await productRepository.getProductDetails(event.productId);
yield ProductLoaded(product: product);
return;
} catch (_) {
yield state;
}
}
}
states :
abstract class ProductState extends Equatable {
const ProductState();
#override
List<Object?> get props => [];
}
class ProductsEmpty extends ProductState {}
class ProductEmpty extends ProductState {}
class ProductLoading extends ProductState {}
class ProductsLoading extends ProductState {}
class ProductLoaded extends ProductState {
final Product product;
const ProductLoaded({required this.product});
ProductLoaded copyWith({required Product product}) {
return ProductLoaded(product: product);
}
#override
List<Object?> get props => [product];
#override
String toString() => 'ProductLoaded { product: ${product.name}}';
}
class ProductsLoaded extends ProductState {
final List<Product> products;
const ProductsLoaded({required this.products});
ProductsLoaded copyWith({required List<Product> products}) {
return ProductsLoaded(products: products);
}
#override
List<Object?> get props => [products];
#override
String toString() => 'ProductLoaded { products: ${products.length}}';
}
class ProductError extends ProductState {}
ProductRepository ( ProductApiService is just the api and it's working fine ) :
class ProductRepository {
final ProductApiService productApiService;
ProductRepository({ProductApiService? productApiService})
: productApiService = productApiService ?? ProductApiService();
Future<Products> getCategoryProducts(int? categoryId) async {
return productApiService.fetchCategoryProducts(categoryId);
}
Future<Product> getProductDetails(int? productId) async {
return productApiService.fetchProductDetails(productId);
}
}
ProductsWrapper :
final int? categoryId;
const ProductsWrapper({Key? key, required this.categoryId}) : super(key: key);
#override
_ProductsWrapperState createState() => _ProductsWrapperState();
}
class _ProductsWrapperState extends State<ProductsWrapper> {
final _scrollController = ScrollController();
final _scrollThreshold = 200;
Completer _productsRefreshCompleter = new Completer();
List<Product> products = [];
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _onScroll() {
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
if (maxScroll - currentScroll <= _scrollThreshold) {
context
.read<ProductBloc>()
.add(FetchProducts(categoryId: widget.categoryId!));
}
}
#override
void initState() {
super.initState();
context
.read<ProductBloc>()
.add(FetchProducts(categoryId: widget.categoryId!));
_scrollController.addListener(_onScroll);
_productsRefreshCompleter = Completer();
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
final double itemHeight = 260;
final double itemWidth = size.width / 2;
return Scaffold(
key: _scaffoldKey,
body: BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductsLoaded) {
products = state.products;
_productsRefreshCompleter.complete();
}
},
child: Container(
margin: EdgeInsets.all(8.0),
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductsLoading) {
print('a7a');
return Center(
child: LoadingIndicator(),
);
}
if (state is ProductsLoaded) {
products = state.products;
if (state.products.isEmpty) {
return Center(
child: Text("No Products Found in this category"),
);
}
return Scaffold(
body: SafeArea(
child: Container(
child: GridView.builder(
itemCount: products.length,
scrollDirection: Axis.vertical,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio:
(itemWidth / itemHeight)),
itemBuilder: (context, index) => Card(
elevation: 0,
child: InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ProductDetailScreen(
productId:
products[index]
.id)));
},
child: Container(
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
ClipRRect(
child: Image.network(
products[index]
.image!
.image
.toString(),
height: 150,
fit: BoxFit.fitWidth,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
products[index].name.toString(),
style: TextStyle(
color: Colors.black,
fontWeight:
FontWeight.bold),
),
),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Padding(
padding: EdgeInsets.all(12.0),
child: Text(
'\$${products[index].price.toString()}'),
),
Padding(
padding: EdgeInsets.only(
right: 8.0),
child: CircleAvatar(
backgroundColor:
Theme.of(context)
.primaryColor,
radius: 10,
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.add,
size: 20,
),
color: Colors.white,
onPressed: () {},
),
),
)
],
)
],
),
),
),
)),
),
),
);
}
return Center(
child: LoadingIndicator(strokeWidth: 5.0,),
);
}))));
}
}
ProductDetailScreen :
class ProductDetailScreen extends StatefulWidget {
final int? productId;
const ProductDetailScreen({Key? key, required this.productId})
: super(key: key);
#override
_ProductDetailScreenState createState() => _ProductDetailScreenState();
}
class _ProductDetailScreenState extends State<ProductDetailScreen> {
Completer _productRefreshCompleter = new Completer();
Product product = new Product();
GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
context.read<ProductBloc>().add(FetchProduct(productId: widget.productId));
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: BlocListener<ProductBloc, ProductState>(
listener: (context, state) {
if (state is ProductLoaded) {
product = state.product;
_productRefreshCompleter.complete();
_productRefreshCompleter = Completer();
}
},
child: Container(
child: BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductLoading) {
return Center(
child: LoadingIndicator(),
);
}
if (state is ProductLoaded) {
return Scaffold(
body: SafeArea(
child: Container(
child: Text(product.name.toString()),
),
),
);
}
return Center(
child: LoadingIndicator(
strokeWidth: 5.0,
),
);
},
),
),
),
);
}
}
any help is appreciated .
thanks for taking time reading this , have a nice day and stay safe.
The problem is that you are using one bloc to do 2 things. The products list is an entity, the single detail is another entity. And you need to use the properties of the states as a result inside blocBuilders.
Plus, you don't need any listener and completer. The bloc pattern refreshes all when state changes.
I have created a repo with a working solution.
https://github.com/eugenioamato/categoryproducts

setState is not updating the UI

I am trying to fetch data from the API and I am able to get logs but setState is not working.
Overall what I want to achieve is if there is response show the data on the screen, if there is any error in the API or on server or anything else I want to show it in the snackbar. My moto is to show errors as well.
Below is my model class
import 'http.dart';
class User {
int userId;
int id;
String title;
String body;
User({this.userId, this.id, this.title, this.body});
User.fromJson(Map<String, dynamic> json) {
userId = json['userId'];
id = json['id'];
title = json['title'];
body = json['body'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['userId'] = this.userId;
data['id'] = this.id;
data['title'] = this.title;
data['body'] = this.body;
return data;
}
}
class UserExt {
static getUserInfo(Function(User user) success, Function(String errorMesssage) error) async{
final response = await HTTP.get(api: "https://jsonplaceholder.typicode.com/posts/1");
if(response.isSuccess == true) {
success(User.fromJson(response.response));
} else {
error(response.response);
}
}
}
Below is my http.dart file
import 'dart:html';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'dart:convert' as convert;
import 'package:http/http.dart';
const _timeoutDuration = Duration(seconds: 5);
class HTTP {
static Future<HttpResponse> get({#required String api}) async {
try {
Response response = await http.get(api).timeout(_timeoutDuration);
return _modeledResponse(response);
} catch (error) {
return HttpResponse(isSuccess: false, response: error.toString());
}
}
static Future<HttpResponse> _modeledResponse(Response response) async {
try {
if(response.statusCode == HttpStatus.ok) {
var jsonResponse = convert.jsonDecode(response.body);
return HttpResponse(isSuccess: true, response: jsonResponse);
} else {
return HttpResponse(isSuccess: false, response: response.statusCode.toString());
}
} catch (error) {
return HttpResponse(isSuccess: false, response: error.toString());
}
}
}
class HttpResponse {
final bool isSuccess;
final dynamic response;
HttpResponse({#required this.isSuccess, #required this.response});
}
Below is my screen from where I am calling the API.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http_request/User.dart';
import 'http.dart';
class ApiCalling extends StatefulWidget {
#override
_ApiCallingState createState() => _ApiCallingState();
}
class _ApiCallingState extends State<ApiCalling> {
bool showLoader = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Stack(
children: <Widget>[
Center(
child: RaisedButton(
child: Text("Call API"),
onPressed: () {
setState(() {
showLoader = true;
});
UserExt.getUserInfo((user){
print("UUUser id = ${user.userId}");
Scaffold.of(context).showSnackBar(SnackBar(content: Text("${user.userId}"),));
setState(() {
showLoader = false;
});
}, (error){
Scaffold.of(context).showSnackBar(SnackBar(content: Text("${error}"),));
setState(() {
showLoader = false;
});
});
},
),
),
Visibility(child: CircularProgressIndicator(backgroundColor: Colors.pink,), visible: showLoader,),
],
),
),
);
}
}
In the current code indicator is not getting show/hide or snackbar is also not getting displayed.
Just made some changes and addons just check the below code :
import 'package:flutter/material.dart';
import 'package:sample_project_for_api/model.dart';
void main() => runApp(ApiCalling());
class ApiCalling extends StatefulWidget {
#override
_ApiCallingState createState() => _ApiCallingState();
}
class _ApiCallingState extends State<ApiCalling> {
bool showLoader = false;
final _scaffoldKey = GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
key: _scaffoldKey,
body: Center(
child: Stack(
children: <Widget>[
Builder(
builder: (context) {
return Column(
children: <Widget>[
RaisedButton(
child: Text(
"this is first api call under the builder widget"),
onPressed: () {
UserExt.getUserInfo((user) {
print("UUUser id = ${user.userId}");
Scaffold.of(context).showSnackBar(SnackBar(
backgroundColor: Colors.redAccent,
content:
Text("This is you user id ${user.userId}"),
));
}, (error) {
Scaffold.of(context).showSnackBar(SnackBar(
duration: Duration(seconds: 2),
backgroundColor: Colors.redAccent,
content: Text("${error.toString()}"),
));
});
},
),
RaisedButton(
child: Text(
"this is second api call under the builder widget"),
onPressed: () {
UserExt.getUserInfo((user) {
print("UUUser id = ${user.userId}");
Scaffold.of(context).showSnackBar(SnackBar(
backgroundColor: Colors.redAccent,
content:
Text("This is you user id ${user.userId}"),
));
}, (error) {
Scaffold.of(context).showSnackBar(SnackBar(
duration: Duration(seconds: 2),
backgroundColor: Colors.redAccent,
content: Text("${error.toString()}"),
));
});
},
)
],
);
},
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text("call snacker using the global key"),
onPressed: () {
setState(() {
showLoader = true;
});
UserExt.getUserInfo((user) {
print("UUUser id = ${user.userId}");
_scaffoldKey.currentState.showSnackBar(new SnackBar(
duration: Duration(seconds: 2),
content: new Text(
"This is you user id :${user.userId}")));
setState(() {
showLoader = false;
});
}, (error) {
_scaffoldKey.currentState.showSnackBar(new SnackBar(
duration: Duration(seconds: 2),
content: new Text("${error.toString()}")));
setState(() {
showLoader = false;
});
});
},
),
Secondbutton(),
],
),
),
Visibility(
child: CircularProgressIndicator(
backgroundColor: Colors.pink,
),
visible: showLoader,
),
],
),
),
),
);
}
}
class Secondbutton extends StatefulWidget {
#override
_SecondbuttonState createState() => _SecondbuttonState();
}
class _SecondbuttonState extends State<Secondbutton> {
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text("calling snacker without using the global key"),
onPressed: () {
UserExt.getUserInfo((user) {
print("UUUser id = ${user.userId}");
Scaffold.of(context).showSnackBar(SnackBar(
backgroundColor: Colors.redAccent,
content: Text("This is you user id ${user.userId}"),
));
}, (error) {
Scaffold.of(context).showSnackBar(SnackBar(
duration: Duration(seconds: 2),
backgroundColor: Colors.redAccent,
content: Text("${error.toString()}"),
));
});
},
);
}
}
Your problem was because it was not getting the proper context.
From the official Documentation of flutter https://api.flutter.dev/flutter/material/Scaffold/of.html
When the Scaffold is actually created in the same build function, the context argument to the build function can't be used to find the Scaffold (since it's "above" the widget being returned in the widget tree). In such cases, the following technique with a Builder can be used to provide a new scope with a BuildContext that is "under" the Scaffold:
So basically the problem is with your context of Scaffold,so instead of using context of Direct parent that instantiate the Scaffold, use the context of the child.
Below code will work.
class _ApiCallingState extends State<ApiCalling> {
bool showLoader = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
builder: (context)=>
Center(
child: Stack(
children: <Widget>[
Center(
child: RaisedButton(
child: Text("Call API"),
onPressed: () {
setState(() {
showLoader = true;
});
UserExt.getUserInfo((user) {
print("UUUser id = ${user.userId}");
print("context==$context");
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(" User Id${user.userId}"),
));
setState(() {
showLoader = false;
});
}, (error) {
setState(() {
showLoader = false;
});
Scaffold.of(context).showSnackBar(SnackBar(
content: Text("${error}"),
));
});
},
),
),
Visibility(
child:
Center(
child:CircularProgressIndicator(
backgroundColor: Colors.pink,
),
),
visible: showLoader,
)
],
),
),
)
);
}
}
declare a global key
final _scaffoldKey = GlobalKey();
and in UI
_scaffoldKey.currentState.showSnackBar(snackbar);

Flutter Stateful Widget State not Initializing

I'm making a command and control application using Flutter, and have come across an odd problem. The main status page of the app shows a list of stateful widgets, which each own a WebSocket connection that streams state data from a connected robotic platform. This worked well when the robots themselves were hardcoded in. However now that I'm adding them dynamically (via barcode scans), only the first widget is showing status.
Further investigation using the debugger shows that this is due to the fact that a state is only getting created for the first widget in the list. Subsequently added widgets are successfully getting constructed, but are not getting a state. Meaning that createState is not getting called for anything other than the very first widget added. I checked that the widgets themselves are indeed being added to the list and that they each have unique hash codes. Also, the IOWebSocketChannel's have unique hash codes, and all widget data is correct and unique for the different elements in the list.
Any ideas as to what could be causing this problem?
Code for the HomePageState:
class HomePageState extends State<HomePage> {
String submittedString = "";
StateContainerState container;
List<RobotSummary> robotList = [];
List<String> robotIps = [];
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
void addRobotToList(String ipAddress) {
var channel = new IOWebSocketChannel.connect('ws://' + container.slsData.slsIpAddress + ':' + container.slsData.wsPort);
channel.sink.add("http://" + ipAddress);
var newConnection = new RobotSummary(key: new UniqueKey(), channel: channel, ipAddress: ipAddress, state: -1, fullAddress: 'http://' + container.slsData.slsIpAddress + ':' + container.slsData.wsPort,);
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text("Adding robot..."), duration: Duration(seconds: 2),));
setState(() {
robotList.add(newConnection);
robotIps.add(ipAddress);
submittedString = ipAddress;
});
}
void _onSubmit(String val) {
// Determine the scan data that was entered
if(Validator.isIP(val)) {
if(ModalRoute.of(context).settings.name == '/') {
if (!robotIps.contains(val)) {
addRobotToList(val);
}
else {
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text("Robot already added..."), duration: Duration(seconds: 5),));
}
}
else {
setState(() {
_showSnackbar("Robot scanned. Go to page?", '/');
});
}
}
else if(Validator.isSlotId(val)) {
setState(() {
_showSnackbar("Slot scanned. Go to page?", '/slots');
});
}
else if(Validator.isUPC(val)) {
setState(() {
_showSnackbar("Product scanned. Go to page?", '/products');
});
}
else if (Validator.isToteId(val)) {
}
}
#override
Widget build(BuildContext context) {
container = StateContainer.of(context);
return new Scaffold (
key: scaffoldKey,
drawer: Drawer(
child: CategoryRoute(),
),
appBar: AppBar(
title: Text(widget.topText),
),
bottomNavigationBar: BottomAppBar(
child: new Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(icon: Icon(Icons.camera_alt), onPressed: scan,),
IconButton(icon: Icon(Icons.search), onPressed: _showModalSheet,),
],
),
),
body: robotList.length > 0 ? ListView(children: robotList) : Center(child: Text("Please scan a robot.", style: TextStyle(fontSize: 24.0, color: Colors.blue),),),
);
}
void _showModalSheet() {
showModalBottomSheet(
context: context,
builder: (builder) {
return _searchBar(context);
});
}
void _showSnackbar(String message, String route) {
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text(message),
action: SnackBarAction(
label: 'Go?',
onPressed: () {
if (route == '/') {
Navigator.popUntil(context,ModalRoute.withName('/'));
}
else {
Navigator.of(context).pushNamed(route);
}
},),
duration: Duration(seconds: 5),));
}
Widget _searchBar(BuildContext context) {
return new Scaffold(
body: Container(
height: 75.0,
color: iam_blue,
child: Center(
child: TextField(
style: TextStyle (color: Colors.white, fontSize: 18.0),
autofocus: true,
keyboardType: TextInputType.number,
onSubmitted: (String submittedStr) {
Navigator.pop(context);
_onSubmit(submittedStr);
},
decoration: new InputDecoration(
border: InputBorder.none,
hintText: 'Scan a tote, robot, UPC, or slot',
hintStyle: TextStyle(color: Colors.white70),
icon: const Icon(Icons.search, color: Colors.white70,)),
),
)));
}
Future scan() async {
try {
String barcode = await BarcodeScanner.scan();
setState(() => this._onSubmit(barcode));
} on PlatformException catch (e) {
if (e.code == BarcodeScanner.CameraAccessDenied) {
setState(() {
print('The user did not grant the camera permission!');
});
} else {
setState(() => print('Unknown error: $e'));
}
} on FormatException{
setState(() => print('null (User returned using the "back"-button before scanning anything. Result)'));
} catch (e) {
setState(() => print('Unknown error: $e'));
}
}
}
Code snippet for the RobotSummary class:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:test_app/genericStateSummary_static.dart';
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:test_app/StateDecodeJsonFull.dart';
import 'dart:async';
import 'package:test_app/dataValidation.dart';
class RobotSummary extends StatefulWidget {
final String ipAddress;
final String _port = '5000';
final int state;
final String fullAddress;
final WebSocketChannel channel;
RobotSummary({
Key key,
#required this.ipAddress,
#required this.channel,
this.state = -1,
this.fullAddress = "http://10.1.10.200:5000",
}) : assert(Validator.isIP(ipAddress)),
super(key: key);
#override
_RobotSummaryState createState() => new _RobotSummaryState();
}
class _RobotSummaryState extends State<RobotSummary> {
StreamController<StateDecodeJsonFull> streamController;
#override
void initState() {
super.initState();
streamController = StreamController.broadcast();
}
#override
Widget build(BuildContext context) {
return new Padding(
padding: const EdgeInsets.all(20.0),
child: new StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot) {
//streamController.sink.add('{"autonomyControllerState" : 3, "pickCurrentListName" : "69152", "plannerExecutionProgress" : 82, "pickUpcCode" : "00814638", "robotName" : "Adam"}');
return getStateWidget(snapshot);
},
),
);
}
#override
void dispose() {
streamController.sink.close();
super.dispose();
}
}
Based on what Jacob said in his initial comments, I came up with a solution that works and is a combination of his suggestions. The code solution he proposed above can't be implemented (see my comment), but perhaps a modification can be attempted that takes elements of it. For the solution I'm working with now, the builder call for HomePageState becomes as follows:
Widget build(BuildContext context) {
List<RobotSummary> tempList = [];
if (robotList.length > 0) {
tempList.addAll(robotList);
}
container = StateContainer.of(context);
return new Scaffold (
key: scaffoldKey,
drawer: Drawer(
child: CategoryRoute(),
),
appBar: AppBar(
title: Text(widget.topText),
),
bottomNavigationBar: BottomAppBar(
child: new Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(icon: Icon(Icons.camera_alt), onPressed: scan,),
IconButton(icon: Icon(Icons.search), onPressed: _showModalSheet,),
],
),
),
body: robotList.length > 0 ? ListView(children: tempList) : Center(child: Text("Please scan a robot.", style: TextStyle(fontSize: 24.0, color: iam_blue),),),
);
}
The problem is you are holding on to the StatefulWidgets between build calls, so their state is always the same. Try separating RobotSummary business logic from the view logic. Something like
class RobotSummary {
final String ipAddress;
final String _port = '5000';
final int state;
final String fullAddress;
final WebSocketChannel channel;
StreamController<StateDecodeJsonFull> streamController;
RobotSummary({
#required this.ipAddress,
#required this.channel,
this.state = -1,
this.fullAddress = "http://10.1.10.200:5000",
}) : assert(Validator.isIP(ipAddress));
void init() => streamController = StreamController.broadcast();
void dispose() => streamController.sink.close();
}
And then in your Scaffold body:
...
body: ListView.builder(itemCount: robotList.length, itemBuilder: _buildItem)
...
Widget _buildItem(BuildContext context, int index) {
return new Padding(
padding: const EdgeInsets.all(20.0),
child: new StreamBuilder(
stream: robotList[index].channel.stream,
builder: (context, snapshot) {
//streamController.sink.add('{"autonomyControllerState" : 3, "pickCurrentListName" : "69152", "plannerExecutionProgress" : 82, "pickUpcCode" : "00814638", "robotName" : "Adam"}');
return getStateWidget(snapshot); // not sure how to change this.
},
),
);
}