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

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"),
),
];
},
),
)
];
},
),

Related

The state of my object is not stored properly

I want to mark my object as favorite.
I have a list of object <RobotAnimation> which is displayed in a ListView. The class have two fields: title and isFavorite. Marking an object as a favorite works, but there is a problem when it comes to storing that state. When I perform a search of all items, after selecting an item as favorite, my favorite items are not being remembered. It seems like the state is being discarded.
What can I do to fix this problem?
Here's what's going on:
Here's my code:
class RobotAnimation {
String title;
bool isFavorite;
RobotAnimation({required this.title, this.isFavorite = false});
#override
String toString() {
return '{Title: $title, isFavortite: $isFavorite}';
}
}
class Animations extends StatefulWidget {
const Animations({super.key});
#override
State<Animations> createState() => _AnimationsState();
}
class _AnimationsState extends State<Animations> with TickerProviderStateMixin {
late TabController _tabController;
List<RobotAnimation> animations = [];
List<RobotAnimation> favoriteAnimations = [];
List<String> results = store.state.animations;
List<String> defaultFavorites = [];
List<RobotAnimation> getAnimationList(
List<String> animations, List<String> favorites) {
List<RobotAnimation> robotAnimations = [];
for (var animation in animations) {
bool isFav = false;
for (var favorite in favorites) {
if (favorite == animation) {
isFav = true;
}
}
robotAnimations.add(RobotAnimation(title: animation, isFavorite: isFav));
}
return robotAnimations;
}
List<RobotAnimation> filterFavorites() {
List<RobotAnimation> filtered = favoriteAnimations;
animations.where((element) => element.isFavorite == true).toList();
return filtered;
}
void filterSearchResults(String query) {
List<RobotAnimation> searchList =
getAnimationList(results, defaultFavorites);
log('query: $query');
List<RobotAnimation> filteredList = searchList
.where((element) =>
element.title.toLowerCase().contains(query.toLowerCase()))
.toList();
log(searchList.toString());
log(filteredList.toString());
setState(() => animations = filteredList);
}
#override
void initState() {
animations = getAnimationList(results, defaultFavorites);
super.initState();
}
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, _Props>(
converter: (store) => _mapStateToProps(store),
builder: (_, props) {
return Scaffold(
body: TabBarView(
...
children: [
Container(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
TextField(
onChanged: filterSearchResults,
decoration: const InputDecoration(
labelText: 'Search',
hintText: 'Search animation',
prefixIcon: Icon(Icons.search),
),
),
Expanded(
child: ListView.separated(
itemCount: animations.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
return ListTile(
onTap: () {
props.socket?.animation(IAnimation(
animation: animations[index].title));
},
title: ExtendedText(
animations[index].title,
maxLines: 1,
overflowWidget: const TextOverflowWidget(
position: TextOverflowPosition.middle,
align: TextOverflowAlign.center,
child: Text(
'...',
overflow: TextOverflow.ellipsis,
),
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
setState(() {
animations[index].isFavorite
? animations[index].isFavorite = false
: animations[index].isFavorite = true;
});
},
icon: animations[index].isFavorite
? Icon(
Icons.favorite,
color: Colors.red.shade500,
)
: Icon(
Icons.favorite_border,
color: Colors.grey.shade500,
),
),
],
),
);
},
),
)
],
),
),
],
),
);
},
);
}
}
class _Props {
final Connection? socket;
final List<String> animations;
_Props({
required this.socket,
required this.animations,
});
}
_Props _mapStateToProps(Store<AppState> store) {
return _Props(
socket: store.state.socket,
animations: store.state.animations,
);
}
Try this inside your IconButton onPressed-Method:
setState(() {
if (animations[index].isFavorite) {
animations[index].isFavorite = false
defaultFavorites.remove(animations[index].title)
} else {
animations[index].isFavorite = true;
defaultFavorites.add(animations[index].title)
}
});
It seems like you always generate a new list of animations based on the two lists List<String>.

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

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;
}

Flutter pagination loading the same data as in page one when scrolling

I'm building a list of news from an api that has next page results as in the image attached.
The api has only two pages with 10 list items each page.
Data is being passed to the widget. My problem is that when I scroll down the view, it loads the same 10 list items from page one.
This is the api I'm using enter link description here
Rest API
//newsModal.dart
class NewsNote {
String banner_image;
String title;
String text;
String sport;
NewsNote(this.banner_image, this.title, this.text, this.sport);
NewsNote.fromJson(Map<String, dynamic> json) {
banner_image = json['banner_image'];
title = json['title'];
text = json['text'];
sport = json['sport'];
}
}
//page news
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:jabboltapp/models/newsModal.dart';
class JabNews extends StatefulWidget {
#override
_JabNewsState createState() => _JabNewsState();
}
class _JabNewsState extends State<JabNews> {
ScrollController _scrollController = ScrollController();
bool isLoading = false;
String url = "https://jabbolt.com/api/news";
List<NewsNote> _newsNotes = List<NewsNote>();
Future<List<NewsNote>> fetchNewsNotes() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
var response = await http.get(url);
var newsNotes = List<NewsNote>();
if (response.statusCode == 200) {
url = jsonDecode(response.body)['next'];
var newsNotesJson = json.decode(response.body)["results"];
for (var newsNoteJson in newsNotesJson) {
newsNotes.add(NewsNote.fromJson(newsNoteJson));
}
setState(() {
isLoading = false;
_newsNotes.addAll(newsNotes);
});
} else {
setState(() {
isLoading = false;
});
}
return newsNotes;
}
}
#override
void initState() {
fetchNewsNotes().then((value) {
setState(() {
_newsNotes.addAll(value);
});
});
this.fetchNewsNotes();
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
fetchNewsNotes();
}
});
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget _buildProgressIndicator() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Opacity(
opacity: isLoading ? 1.0 : 00,
child: CircularProgressIndicator(),
),
),
);
}
Widget _buildList() {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
if (index == _newsNotes.length) {
return _buildProgressIndicator();
} else {
return Padding(
padding: EdgeInsets.all(8.0),
child: Card(
child: ListTile(
title: Text((_newsNotes[index].title)),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(_newsNotes[index])));
},
),
),
);
}
},
controller: _scrollController,
itemCount: _newsNotes.length,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: dGrey,
appBar: AppBar(
title: Text(
"News",
style: TextStyle(
color: textGrey,
fontFamily: 'bison',
fontSize: 32.0,
letterSpacing: 1.2,
),
),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Container(
child: _buildList(),
),
);
}
}
You need to add the page number concatenation in the URL
https://jabbolt.com/api/news?page=2

How to change items of a gridview in flutter

I am new to flutter and i current have an app that has a grid view that gets its list from an api. Some of the grid view items have child nodes in them, so what i want to achieve is to set a click function that checks if there is a child node and if that is true; i would want to re-populate the same grid view but with only members of the child node. is this possible in flutter?
import 'package:bringam/network/Models/ProductGroupModel.dart';
import 'package:bringam/network/sharedpreferences/SharedPreferences.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'dart:convert';
class Product_Category extends StatefulWidget {
#override
_Product_CategoryState createState() => _Product_CategoryState();
}
class _Product_CategoryState extends State<Product_Category> {
Future<List<ProductGroupModel>> _getChildrenCategories(String tag) async {
List<ProductGroupModel> categories = [];
SharedPref sharedPref = SharedPref();
var cacheCategories =
json.decode(await sharedPref.read('PRODUCT_CATEGORY'));
// FILTERING THE LIST STARTS
var filteredJson =
cacheCategories.where((i) => i["ParentGroupId"] == tag).toList();
// FILTERING THE LIST ENDS
for (var u in filteredJson) {
ProductGroupModel productCat = ProductGroupModel(
u["Description"],
u["IconURL"],
u["ProductGroup"],
u["ParentGroupId"],
u["HasChildNode"],
u["Order"]);
categories.add(productCat);
}
print(categories);
return categories;
}
Future<List<ProductGroupModel>> _getCategories() async {
List<ProductGroupModel> categories = [];
SharedPref sharedPref = SharedPref();
var cacheCategories =
json.decode(await sharedPref.read('PRODUCT_CATEGORY'));
if (cacheCategories.isEmpty) {
var data = await http.get(
'PRIVATE API ENDPOINT PLEASE');
var jsonData = json.decode(data.body);
// FILTERING THE LIST STARTS
var filteredJson =
jsonData.where((i) => i["ParentGroupId"] == '0').toList();
// FILTERING THE LIST ENDS
for (var u in filteredJson) {
ProductGroupModel productCat = ProductGroupModel(
u["Description"],
u["IconURL"],
u["ProductGroup"],
u["ParentGroupId"],
u["HasChildNode"],
u["Order"]);
categories.add(productCat);
}
} else {
// FILTERING THE LIST STARTS
var filteredJson =
cacheCategories.where((i) => i["ParentGroupId"] == '0').toList();
// FILTERING THE LIST ENDS
for (var u in filteredJson) {
ProductGroupModel productCat = ProductGroupModel(
u["Description"],
u["IconURL"],
u["ProductGroup"],
u["ParentGroupId"],
u["HasChildNode"],
u["Order"]);
categories.add(productCat);
}
return categories;
}
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _getCategories(),
builder: (BuildContext context,
AsyncSnapshot<List<ProductGroupModel>> snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return GridView.builder(
itemCount: snapshot.data.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:
2),
itemBuilder: (BuildContext context, int index) {
return Card(
elevation: 0,
color: Colors.transparent,
child: Hero(
tag: snapshot.data[index].ProductGroup,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
bool hasChild = snapshot.data[index].HasChildNode;
if (hasChild == true) {
setState(() {
_getChildrenCategories(
snapshot.data[index].ProductGroup);
});
} else {
Scaffold.of(context).showSnackBar(SnackBar(
content: new Text("Nothing found!"),
duration: const Duration(milliseconds: 500)));
}
},
child: GridTile(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircleAvatar(
backgroundImage:
NetworkImage(snapshot.data[index].IconURL),
radius: 75.0,
),
Text(
snapshot.data[index].Description,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0,
color: Colors.white,
),
),
],
),
),
),
),
),
);
});
}
},
);
}
}
//THE MODEL CLASS
class ProductGroupModel {
final String Description;
final String IconURL;
final String ProductGroup;
final String ParentGroupId;
final bool HasChildNode;
final int Order;
ProductGroupModel(
this.Description,
this.IconURL,
this.ProductGroup,
this.ParentGroupId,
this.HasChildNode,
this.Order,
);
}

How to access all of child's state from Parent Widget in flutter?

I have a parent widget called createRoutineScreen and it has 7 similar children widget called RoutineFormCard. RoutineFormCard is a form and which has a state _isPostSuccesful of boolean type to tell whether the form is saved to database or not. Now, I have to move to the other screen from createRoutine only when all of it's 7 children has _isPostSuccesful true. How can I access all of children's state from createRoutineScreen widget?
My Code is:
class CreateRoutineScreen extends StatefulWidget {
final String userID;
CreateRoutineScreen({this.userID});
//TITLE TEXT
final Text titleSection = Text(
'Create a Routine',
style: TextStyle(
color: Colors.white,
fontSize: 25,
)
);
final List<Map> weekDays = [
{"name":"Sunday", "value":1},
{"name":"Monday", "value":2},
{"name":"Tuesday", "value":3},
{"name":"Wednesday", "value":4},
{"name":"Thursday", "value":5},
{"name":"Friday", "value":6},
{"name":"Saturday", "value":7},
];
#override
_CreateRoutineScreenState createState() => _CreateRoutineScreenState();
}
class _CreateRoutineScreenState extends State<CreateRoutineScreen> {
Routine routine;
Future<List<dynamic>> _exercises;
dynamic selectedDay;
int _noOfRoutineSaved;
List _keys = [];
Future<List<dynamic>>_loadExercisesData()async{
String url = BASE_URL+ "exercises";
var res = await http.get(url);
var exercisesList = Exercises.listFromJSON(res.body);
//var value = await Future.delayed(Duration(seconds: 5));
return exercisesList;
}
#override
void initState(){
super.initState();
_exercises = _loadExercisesData();
_noOfRoutineSaved = 0;
for (int i = 0; i< 7; i++){
_keys.add(UniqueKey());
}
}
void _changeNoOfRoutineSaved(int a){
setState(() {
_noOfRoutineSaved= _noOfRoutineSaved + a;
});
}
#override
Widget build(BuildContext context) {
print(_noOfRoutineSaved);
return Scaffold(
appBar: AppBar(
title:Text("Create a Routine"),
centerTitle: true,
actions: <Widget>[
FlatButton(
child: Text("Done"),
onPressed: (){
},
),
],
),
body: Container(
color: Theme.of(context).primaryColor,
padding: EdgeInsets.only(top:5.0,left: 10,right: 10,bottom: 10),
child: FutureBuilder(
future: _exercises,
builder: (context, snapshot){
if(snapshot.hasData){
return ListView.builder(
itemCount: widget.weekDays.length,
itemBuilder: (context,index){
return RoutineFormCard(
weekDay: widget.weekDays[index]["name"],
exerciseList: snapshot.data,
userID : widget.userID,
changeNoOfRoutineSaved:_changeNoOfRoutineSaved,
key:_keys[index]
);
},
);
}
else if(snapshot.hasError){
return SnackBar(
content: Text(snapshot.error),
);
}
else{
return Center(
child: CircularProgressIndicator(
backgroundColor: Colors.grey,
)
);
}
},
)
),
);
}
}
And my child widget is:
class RoutineFormCard extends StatefulWidget {
final Function createRoutineState;
final String weekDay;
final List<dynamic> exerciseList;
final String userID;
final Function changeNoOfRoutineSaved;
RoutineFormCard({this.createRoutineState,
this.weekDay, this.exerciseList, this.changeNoOfRoutineSaved,
this.userID, Key key}):super(key:key);
#override
_RoutineFormCardState createState() => _RoutineFormCardState();
}
class _RoutineFormCardState extends State<RoutineFormCard> {
bool _checkBoxValue= false;
List<int> _selectedExercises;
bool _inAsyncCall;
bool _successfulPost;
#override
void initState(){
super.initState();
_selectedExercises = [];
_inAsyncCall = false;
_successfulPost= false;
}
void onSaveClick()async{
setState(() {
_inAsyncCall = true;
});
String url = BASE_URL + "users/routine";
List selectedExercises = _selectedExercises.map((item){
return widget.exerciseList[item].value;
}).toList();
String dataToSubmit = jsonEncode({
"weekDay":widget.weekDay,
"userID": widget.userID==null?"5e9eb190b355c742c887b88d":widget.userID,
"exercises": selectedExercises
});
try{
var res =await http.post(url, body: dataToSubmit,
headers: {"Content-Type":"application/json"});
if(res.statusCode==200){
print("Succesful ${res.body}");
widget.changeNoOfRoutineSaved(1);
setState(() {
_inAsyncCall = false;
_successfulPost = true;
});
}
else{
print("Not succesful ${res.body}");
setState(() {
_inAsyncCall = false;
});
}
}catch(err){
setState(() {
_inAsyncCall = false;
});
print(err);
}
}
Widget saveAndEditButton(){
if(_inAsyncCall){
return CircularProgressIndicator();
}
else if(_successfulPost)
{
return IconButton(
icon: Icon(Icons.edit, color: Colors.black,),
onPressed: (){
widget.changeNoOfRoutineSaved(-1);
setState(() {
_successfulPost = false;
});
},
);
}
else{
return FlatButton(child: Text("Save"),
onPressed: !_checkBoxValue&&_selectedExercises.length==0?null:onSaveClick,);
}
}
//Card Header
Widget cardHeader(){
return AppBar(
title: Text(widget.weekDay, style: TextStyle(
fontFamily: "Raleway",
fontSize: 20,
color: Colors.black,),
),
actions: <Widget>[
saveAndEditButton()
],
backgroundColor: Colors.lime[400],
);
}
Widget cardBody(){
return Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Text("Rest Day"),
Checkbox(
value: _checkBoxValue,
onChanged: (value){
setState(() {
_checkBoxValue = value;
});
},
)
],
),
),
_checkBoxValue?Container():
SearchableDropdown.multiple(
hint: "Select Exercise",
style: TextStyle(color: Colors.black),
items: widget.exerciseList.map<DropdownMenuItem>((item){
return DropdownMenuItem(
child: Text(item.name), value: item
);
}).toList(),
selectedItems: _selectedExercises,
onChanged: (values){
setState(() {
_selectedExercises = values;
});
},
isExpanded: true,
dialogBox: true,
),
],
);
}
#override
Widget build(BuildContext context) {
print("<><><><><><><><><><><>${widget.weekDay} called");
return Card(
elevation: 8.0,
child: Form(
key: GlobalKey(),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
cardHeader(),
_successfulPost?Container():cardBody()
],
),
),
);
}
}
As you can see, I've tried callBack from parent widget which increases or decrease no of form saved from each of the child widget. It does the work but, when one form is saved, parent state is modified and all other children got rebuild which is unnecessary in my opionion. What's the best way to do it?
Try to use GlobalKey instead of UniqueKey for each RoutineFormCard. It will help you to access the state of each RoutineFormCard. You can do it like this :
// 1. In the top of your CreateRoutineScreen file, add this line (make your RoutineFormCardState class public before)
final List<GlobalKey<RoutineFormCardState>> routineFormCardKeys = <GlobalKey<RoutineFormCardState>>[
GlobalKey<RoutineFormCardState>(),
GlobalKey<RoutineFormCardState>(),
GlobalKey<RoutineFormCardState>(),
GlobalKey<RoutineFormCardState>(),
GlobalKey<RoutineFormCardState>(),
GlobalKey<RoutineFormCardState>(),
GlobalKey<RoutineFormCardState>(),
];
// 2. Then construct your RoutineFormCard using the right key
RoutineFormCard(
weekDay: widget.weekDays[index]["name"],
exerciseList: snapshot.data,
userID : widget.userID,
changeNoOfRoutineSaved:_changeNoOfRoutineSaved,
key: routineFormCardKeys[index]
);
// 3. Now you can create a method in CreateRoutineScreen which will check the state of all RoutineFormCard
bool _allRoutineFormCardsCompleted() {
bool result = true;
for (int i = 0; i < 7; i++)
result = result && routineFormCardKeys[i].currentState.isPostSuccessful;
return result;
}
// 4. Finally use the result of the previous method where you want to move on another page
I'm sharing a quick idea to solve your problem, I've not tested it, but I'm ready to improve the answer if needed
Hope this will help!