ChangeNotifierProvider can't seem to get List<String> - flutter

I am using ChangeNotifierProvider to get a list of strings that i have read from Cloudfirestore. I know that the is okay because i print the legnth of my list to confirm. But somewhere in between the change notifier provider and my UI, I seem to get things lost. Someone help me understand this concept.
Here, i am setting the retrieved list:
class RestorationNetworkService {
static Future<List<String?>> getRepairProducts(RestorationNotifier restNot,) async {
final firebaseref = FirebaseFirestore.instance;
List<String?> repairproducts = [];
final data = await firebaseref.collection("restoration_products").get();
final datasetMap = data.docs.single.data();
repairproducts= (datasetMap['product'] as List).map((e) => e as String).toList();
print(repairproducts.length);
restNot.restoreProductList = repairproducts;
return repairproducts;
}
}
Here is my changeNotifier class:
import 'package:flutter/cupertino.dart';
class RestorationNotifier with ChangeNotifier {
List<String?> _repairproducts = [];
set restoreProductList(List<String?> repairproductlist) {
_repairproducts= repairproductlist;
notifyListeners();
}
List<String?> get repairProductList => _repairproducts;
}
Here i am trying to update my UI with the list:
#override
void initState() {
//lets load our future right here
RestorationNetworkService.getRepairProducts(rest);
super.initState();
}
#override
Widget build(BuildContext context) {
List<String?> restProdList = Provider.of<RestorationNotifier>(context, listen: false).repairProductList;
return Scaffold(
appBar: AppBar(
title: Text('Restore Furniture')
),
body: Center(
child: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Wrap(
children: repairChips(restProdList),
),
],
),
)),
),
);
}
List<Widget> repairChips(List<String?> restoreProdList) {
List<Widget> chips = [];
int selectedIndex = 0;
for (int i = 0; i < restoreProdList.length; i++) {
Widget item = PhysicalModel(
clipBehavior: Clip.hardEdge,
color: Colors.transparent,
shadowColor: Colors.grey.withOpacity(0.3),
elevation: 10,
child: Padding(
padding: EdgeInsets.only(left: 10, right: 5),
child: ChoiceChip(
selected: selectedIndex == i,
selectedColor: Color(0xFFF7B239),
label: Text(restoreProdList[i] ?? ''),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2)),
backgroundColor: Colors.white,
onSelected: (bool value) {
setState(() {
selectedIndex = i;
});
},
)));
chips.add(item);
}
return chips;
}
}
I can't really seem to trace where exactly i am going wrong?

The list you get from Cloudfirestore not set to the state yet.
for current state in your app, the List<String?> = [ ];
you can modify your init function like below:
#override
void initState() {
// Fetch the data
fetchData();
super.initState();
}
Future<void> fetchData() async {
final state = context.read<RestorationNotifier>(); // initialize state provider
final result = await RestorationNetworkService.getRepairProducts(rest);
state.restoreProductList(result); // you miss this step. that assign the data to state
}
// this is my style using provider.
#override
Widget build(BuildContext context) {
final appState = context.watch<RestorationNetworkService>(); // we watching any change
return Scaffold(
appBar: AppBar(
title: Text('Restore Furniture')
),
body: Center(
child: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
// then you can access the object here.
Text('${appState.repairProductList.length}'), //
],
),
)),
),
);
}

Related

local state variable not updating

For some weird reason, my local state variable "_jobApplicationState" is not updating.
I see that it is updated in the database, but its not updating on my page.
If I leave the record and come back, everything works as expected.
I am driving this functionality by pressing the button 'Send inquiry'.
I took out a bunch of code to make it easy to read.
I got this to work for a minute at somepoint. but I forgot to save:(
class JobApplicationView extends StatefulWidget {
const JobApplicationView({Key? key}) : super(key: key);
#override
_JobApplicationViewState createState() => _JobApplicationViewState();
}
// https://youtu.be/VPvVD8t02U8?t=90350
class _JobApplicationViewState extends State<JobApplicationView> {
CloudJobApplication? _jobApplication;
final _formKey = GlobalKey<FormState>();
final currentUser = AuthService.firebase().currentUser!;
late final FirebaseCloudStorage _firebaseService;
//
late String _jobApplicationState;
//
late DateTime _jobApplicationStartDate;
late DateTime _jobApplicationEndDate;
//
bool? isJobCreatorSameAsJobApplicator;
String? _jobCreatorId;
String? _jobApplicatorId;
String? _jobDescription;
List? _jobUserData;
String? _jobAddress;
String? _jobType;
//
#override
void initState() {
super.initState();
_jobApplicationStartDate = DateTime.now();
_jobApplicationEndDate = DateTime.now();
_firebaseService = FirebaseCloudStorage();
// _jobDescriptionController = TextEditingController();
// _jobAreaCodeController = TextEditingController();
// _jobApplicationStateController = TextEditingController();
}
//Future<CloudJobApplication>
createOrGetExistingJob(BuildContext context) async {
final widgetJobApplication = context.getArgument<CloudJobApplication>();
if (widgetJobApplication != null) {
_jobApplication = widgetJobApplication;
_jobApplicationState = widgetJobApplication.jobApplicationState;
_jobApplicatorId = widgetJobApplication.jobApplicatorId;
_jobCreatorId = widgetJobApplication.jobCreatorId;
_jobDescription = widgetJobApplication.jobApplicationDescription;
return widgetJobApplication;
}
print('ELSE TRIGGERED!');
return widgetJobApplication;
}
void _updateJobField(localStateField, jobColumn, jobColumnValue) async {
//* localStateField: local field to update so that the build context is refreshed
//* jobColumn: the name of the column in the db
//* jobColumnValue: the value for the jobColumn
setState(() {
if (localStateField == '_jobApplicationState') {
_jobApplicationState = jobColumnValue;
}
});
await _firebaseService.updateJobApplicationColumn(
documentId: _jobApplication?.documentId as String,
fieldNameColumn: jobColumn,
fieldNameColumnValue: jobColumnValue,
);
}
sendInqury() {
print('setting job applications state!');
print('_jobApplicationState b4:: $_jobApplicationState');
_updateJobField(_jobApplicationState, jobApplicationStateColumn,
jobApplicationStateOpen);
print('_jobApplicationState after:: $_jobApplicationState');
}
#override
void dispose() {
//_deleteJobIfTextIsEmpty();
// _jobDescriptionController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('update job application'),
actions: [],
),
body: FutureBuilder(
future: createOrGetExistingJob(context),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(32.0),
children: [
//getStateChevrons(_jobApplicationState),
const Divider(
height: 20,
thickness: 5,
indent: 0,
endIndent: 0,
color: Colors.blue,
),
Text(_jobApplicationState),
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.blue,
padding: const EdgeInsets.all(16.0),
textStyle: const TextStyle(fontSize: 20),
),
onPressed: sendInqury,
child: const Text('Send inquiry'),
)
],
),
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
I figured out the answer, here is the answer code:
import 'dart:developer';
import 'package:flutter/material.dart';
import '../../services/cloud/cloud_job_application.dart';
import '/services/auth/auth_service.dart';
import '/utilities/generics/get_arguments.dart';
import '/services/cloud/firebase_cloud_storage.dart';
class JobApplicationView extends StatefulWidget {
const JobApplicationView({Key? key}) : super(key: key);
#override
_JobApplicationViewState createState() => _JobApplicationViewState();
}
// https://youtu.be/VPvVD8t02U8?t=90350
class _JobApplicationViewState extends State<JobApplicationView> {
CloudJobApplication? _jobApplication;
late final FirebaseCloudStorage cloudFunctions;
final _formKey = GlobalKey<FormState>();
final currentUser = AuthService.firebase().currentUser!;
// state varibles
String _jobApplicationState = 'default';
String _jobApplicationSubState = 'default';
late final TextEditingController _jobDescriptionController;
#override
void initState() {
super.initState();
cloudFunctions = FirebaseCloudStorage();
_jobDescriptionController = TextEditingController();
}
//Future<CloudJobApplication>
getExistingJobApplication(BuildContext context) async {
log('getExistingJobApplication()');
if (_jobApplicationState == 'default') {
var widgetJobApplication = context.getArgument<CloudJobApplication>();
log('first time openning job application, returning server data');
_jobApplication = widgetJobApplication;
_jobApplicationState =
widgetJobApplication?.jobApplicationState as String;
_jobDescriptionController.text =
widgetJobApplication?.jobApplicationDescription as String;
return widgetJobApplication;
} else {
log('job application has been updated, returnnig local data');
return cloudFunctions.getJobApplication(_jobApplication!.documentId);
}
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('update job application'),
actions: [],
),
body: FutureBuilder(
future: getExistingJobApplication(context),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return Form(
key: _formKey,
child: ListView(padding: const EdgeInsets.all(32.0), children: [
Text(_jobApplicationState),
Text(_jobDescriptionController.text),
const Divider(
height: 20,
thickness: 5,
indent: 0,
endIndent: 0,
color: Colors.blue,
),
TextFormField(
controller: _jobDescriptionController,
maxLines: 5,
decoration: InputDecoration(
// enabled: _jobState == jobStateNew ? true : false,
hintText: "The toilet wont flush",
filled: true,
// fillColor: _jobState == jobStateNew ? Colors.white : Colors.grey,
label: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Padding(padding: EdgeInsets.only(left: 8.0)),
Icon(Icons.info_outline),
Padding(
padding: EdgeInsets.only(left: 8.0, right: 8.0),
child: Text("Job description"),
),
],
),
),
),
validator: (str) =>
str == '' ? "Job description can't be empty" : null,
),
TextButton(
onPressed: () async {
setState(() {
_jobApplicationState = 'Open';
});
await cloudFunctions.updateJobApplication(
documentId: _jobApplication?.documentId as String,
jobDescription: _jobDescriptionController.text,
jobApplicationState: 'Open',
);
},
child: const Text('update state')),
//
]),
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
You should separate the UI and logic -> create a jobApplication Model.
Pack all your logic into a ChangeNotifier and notifyListeners on change.
This is also better for performance because it only rebuilds needed parts of the UI.
I can recommend using a ChangeNotifierProvider.
class JobApplicationProvider extends ChangeNotifier {
JobApplication jobapplication = BasicParam.standard;
void setJobApplication(json) async {
jobapplication = JobApplication.fromJson(json);
notifyListeners();
}
}
And in the build Method use it like this:
Widget build(BuildContext context) {
JobApplicationProvider jobApplication= Provider.of(context);
return Text(jobApplication.state);
}

How to save User rating in flutter rating bar?

Im trying to saving user rating to displaying it when user comes back to page. But im a lit struggling cannot figure out how to do this. Rating works but as I said the saving not .
So what happens is that its always empty. What I actually want is that if user comes back to the page he see his rating and if he rate again and the rating is different the last rating I let him rating and if not then not and if he press clear the rating will be deleting what also works fine.
Maybe anyone can help.
lass Ratingpage extends StatefulWidget {
final int maximumRating;
final Function(int) onRatingSelected;
Ratingpage(this.onRatingSelected, [this.maximumRating = 5]);
#override
_RatingpageState createState() => _RatingpageState();
}
class _RatingpageState extends State<Ratingpage> {
int haveusercurrentchoice;
int _currentRating = 0;
Widget _buildRatingStar(int index) {
if (index < _currentRating) {
return Icon(
Icons.star,
color: Colors.yellow,
);
} else {
return Icon(
Icons.star,
color: Colors.white,
);
}
}
Widget _buildBody() {
final stars = List<Widget>.generate(this.widget.maximumRating, (index) {
return Expanded(
child: GestureDetector(
child: _buildRatingStar(index),
onTap: () {
setState(() {
_currentRating = index;
});
this.widget.onRatingSelected(_currentRating);
},
),
);
});
return Row(
children: [
Expanded(
child: Row(
children: stars,
),
),
Expanded(
child: TextButton(
onPressed: () {
setState(() {
_currentRating = 0;
});
this.widget.onRatingSelected(_currentRating);
},
child: Text(
"Clear",
style: TextStyle(color: Colors.white),
),
),
),
],
);
}
#override
Widget build(BuildContext context) {
return _buildBody();
}
if you need more information please leave a comment.
This is how im calling the page
Container(
width: 210,
height: 94,
//color: Colors.blue.withOpacity(0.5),
child: Column(
children: [
InkWell(
onTap: () {
setState(() {
israting = true;
});
// if( _rating !=null && _rating >0){
// likevideo(videos.data()['id']);}
},
child: israting
? Container(
height: 50,
margin: EdgeInsets.fromLTRB(
0, 0, 5, 0),
child: Column(
children: [
Ratingpage((rating) {
setState(() {
_rating = rating;
});
if (_rating != null &&
_rating > 0) {
likevideo(
videos.data()['id'],
_rating);
print(delteuserchoicing);
} else if (_rating ==
null ||
_rating == 0) {
dislike(
videos.data()['id'],
_rating);
}
}),
],
),
)
: Icon(
Icons.star,
size: 37,
color: videos
.data()['likes']
.contains(uid)
? Colors.yellow
: Colors.white,
),
),
it is inside a column actually
So you have an issue of storing state between pages, then you have an issue of storing the rating upon app restart. 2 separate things. You may only be concerned with the former but here's how you would do both with GetX State management and GetStorage for local database storage. Same thing can be accomplished with literally any other state management solution ie. Provider, Riverpod, Bloc etc...
GetStorage is interchangeable with SharedPreferences but I think anyone who has used both would agree GetStorage is a bit easier to use.
To clean up my example I got rid of anything that wasn't necessary for accomplishing what you're asking. Depending on whats going on in the rest of your app, you probably won't need to bring back most or all of the variables I got rid of.
For starters, let's move the logic and variables to a GetX class so they're accessible from anywhere in the app. It also helps clean up your UI code.
class RatingController extends GetxController {
int currentRating = 0;
final box = GetStorage();
#override
void onInit() { // called whenever we initialize the controller
super.onInit();
currentRating = box.read('rating') ?? 0; // initializing current rating from storage or 0 if storage is null
}
void updateAndStoreRating(int rating) {
currentRating = rating;
box.write('rating', rating); // stores to local database
update(); // triggers a rebuild of the GetBuilder Widget
}
Widget buildRatingStar(int index) {
if (index < currentRating) {
return Icon(
Icons.star,
color: Colors.yellow,
);
} else {
return Icon(
Icons.star,
color: Colors.white,
);
}
}
}
I added a button on this page just for demo purposes. Since this demo includes routing, I'm using Getx for a way easier to do routing also, but it's not at all related or necessary to answer your question. This page can now also be stateless.
class Ratingpage extends StatelessWidget {
static const id = 'rating_page'; // see GetMaterialApp for this usage
final controller = Get.find<RatingController>(); // finding the same instance of initialized controller
Widget _buildBody() {
final stars = List<Widget>.generate(5, (index) {
return GetBuilder<RatingController>( // rebuilds when update() is called from GetX class
builder: (controller) => Expanded(
child: GestureDetector(
child: controller.buildRatingStar(index),
onTap: () {
controller.updateAndStoreRating(index + 1); // +1 because index starts at 0 otherwise the star rating is offset by one
},
),
),
);
});
return Row(
children: [
Expanded(
child: Row(
children: stars,
),
),
Expanded(
child: TextButton(
onPressed: () {
controller.updateAndStoreRating(0);
},
child: Text(
"Clear",
style: TextStyle(color: Colors.white),
),
),
),
],
);
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildBody(),
ElevatedButton(
onPressed: () {
Get.to(() => OtherPage()); // equivalent of Navigator.push....
},
child: Text('Other Page'),
)
],
);
}
}
Your main method now looks like this because we need to initialize the controller and storage.
void main() async {
await GetStorage.init();
Get.put(RatingController());
runApp(MyApp());
}
And again, only necessary for easier routing, we use GetMaterialApp and define pages there.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Material App',
home: Ratingpage(),
getPages: [ // only necessary for routing, not for storage or state management
GetPage(name: OtherPage.id, page: () => OtherPage()),
GetPage(name: Ratingpage.id, page: () => Ratingpage()),
],
);
}
}
EDIT: Added with SharedPreferences due to an unmaintained package conflicting with GetStorage path provider dependency.
Add SharedPreferences prefs; to your GetX class.
This is your update function now.
void updateAndStoreRating(int rating) {
currentRating = rating;
prefs.setInt('rating', rating); //SharedPreferences way
update(); // triggers a rebuild of the GetBuilder Widget
}
Add an init function in GetX Controller class.
Future<void> initSp() async {
prefs = await SharedPreferences.getInstance();
currentRating = prefs.getInt('rating') ?? 0;
}
Now your main is a bit different.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final controller = Get.put(RatingController());
await controller.initSp();
runApp(MyApp());
}

Updating value in nested page(not in widget tree) without refresh when I use provider pattern

I am making an app in the form of a news feed like facebook. I would like to make it user go through contenst in the feed and make them read more detail by clicking on the feed(feedPage to postPage).
I use Firestore as a backend and the provider pattern.
feedPage refers to StreamProvider and myUserData Provider(from main.dart)
Whenever the content is modified or changed or I clicked like, added a comment, it is updated immediately without the need to refresh.
// feed_page.dart
class FeedPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamProvider<List<Post>>.value(
value: firestoreProvider.fetchAllPosts(),
child: Scaffold(
backgroundColor: Colors.grey[200],
appBar: AppBar(
automaticallyImplyLeading: false,
title: GestureDetector(
child: Text('test'),
-----
Container _postActions(
BuildContext context, MyUserData myUserData, Post post) {
return Container(
child: Padding(
padding: const EdgeInsets.fromLTRB(
common_gap, common_gap, common_gap, common_gap),
child: Row(
children: <Widget>[
GestureDetector(
onTap: () {
final route = MaterialPageRoute(
builder: (context) => PostPage(myUserData.data, post));
Navigator.of(context).push(route);
},
the issue is in post_page.dart. When I access to postPage through Navigator, I could see all the details in the postPage. But when I modify something(such as clicking like/heart button, added a comment), I can't see any change in postPage.
// post_page.dart
class PostPage extends StatefulWidget {
final User user;
final Post post;
const PostPage(this.user, this.post, {Key key}) : super(key: key);
#override
_PostPageState createState() => _PostPageState();
}
class _PostPageState extends State<PostPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[200],
appBar: AppBar(
centerTitle: false,
elevation: 0,
actions: <Widget>[
IconButton(
icon: Icon(
Icons.label_outline,
color: Colors.grey[900],
size: 30,
),
onPressed: null,
),
],
),
body: Form(
key: _formkey,
child: SafeArea(
child: Column(
children: <Widget>[
Expanded(
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: _postItem(
context, widget.post.title, widget.post.content),
),
SliverToBoxAdapter(
child: _postComments(),
),
],
),
),
_commentField()
],
),
),
),
);
}
Container _postActions(BuildContext context) {
return Container(
child: Padding(
padding: const EdgeInsets.fromLTRB(
common_gap, common_gap, common_gap, common_xl_gap),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
GestureDetector(
onTap: () {},
child: Row(
children: <Widget>[
Image.asset('assets/comment.png',
width: 17, fit: BoxFit.cover),
SizedBox(
width: common_xxs_gap,
),
Text("${widget.post.numOfComments}", // <-------------------------- my issue is here!
style: TextStyle(fontSize: 13.0, color: Colors.black87))
],
),
),
)
);
}
For example, if I added a comment in this page, widget.post.numOfComments in PostPage should be updated immediately, but to get it updated I got to refresh it by going back to feedPage and return to postPage again.
I think it is because I do not set up provider or setState in postPage.
But I actually have no idea of how to use setState with FirebaseProvider.
And if possible, I want to use Provider either since it is nested in the feedPage(I know it is not in the widget tree of feedPage).
// firestore_provider.dart
class FirestoreProvider with Transformer {
final Firestore _firestore = Firestore.instance;
Future<void> attemptCreateUser({String userKey, String phone}) async {
final DocumentReference userRef =
_firestore.collection(COLLECTION_USERS).document(userKey);
final DocumentSnapshot snapshot = await userRef.get();
return _firestore.runTransaction((Transaction tx) async {
if (!snapshot.exists) {
await tx.set(userRef, User.getMapForCreateUser(userKey, phone));
}
});
}
Stream<User> connectMyUserData(String userKey) {
return _firestore
.collection(COLLECTION_USERS)
.document(userKey)
.snapshots()
.transform(toUser);
}
Stream<List<User>> fetchAllUsers() {
return _firestore
.collection(COLLECTION_USERS)
.snapshots()
.transform(toUsers);
}
Future<Map<String, dynamic>> createNewPost(
String postKey, Map<String, dynamic> postData) async {
final DocumentReference postRef = _firestore
.collection(COLLECTION_POSTS)
.document(postKey); // 없으면 자동으로 Reference 생성됨.
final DocumentSnapshot postSnapshot = await postRef.get();
final DocumentReference userRef =
_firestore.collection(COLLECTION_USERS).document(postData[KEY_USERKEY]);
return _firestore.runTransaction((Transaction tx) async {
if (!postSnapshot.exists) {
await tx.update(userRef, {
KEY_MYPOSTS: FieldValue.arrayUnion([postKey])
});
await tx.set(postRef, postData);
}
});
}
Stream<List<Post>> fetchAllPosts() {
return _firestore
.collection(COLLECTION_POSTS)
.orderBy(KEY_POSTTIME, descending: true)
.snapshots()
.transform(toPosts);
}
Future<Map<String, dynamic>> createNewComments(
String postKey, Map<String, dynamic> commentData) async {
final DocumentReference postRef = _firestore
.collection(COLLECTION_POSTS)
.document(postKey); // 없으면 자동으로 Reference 생성됨.
final DocumentSnapshot postSnapshot = await postRef.get();
print(postSnapshot);
final DocumentReference commentRef =
postRef.collection(COLLECTION_COMMENTS).document();
return _firestore.runTransaction((Transaction tx) async {
if (postSnapshot.exists) {
await tx.set(commentRef, commentData);
int numOfComments = postSnapshot.data[KEY_NUMOFCOMMENTS];
await tx.update(postRef, {KEY_NUMOFCOMMENTS: numOfComments + 1});
}
});
}
Stream<List<CommentModel>> fetchAllComments(String postKey) {
return _firestore
.collection(COLLECTION_POSTS)
.document(postKey)
.collection(COLLECTION_COMMENTS)
.orderBy(KEY_COMMENTTIME)
.snapshots()
.transform(toComments);
}
}
FirestoreProvider firestoreProvider = FirestoreProvider();
plz help.. thanks!

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!

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