I have quiz app (flutter+Firebase), but I want to sort by question(1) not by question id(2). or hot to rename questions id in firebase?
what should I change in my code? Is there any way to do it?
and how to fix this error?
.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:quiz2/database/firebase.dart';
import 'package:quiz2/model/question.dart';
import 'package:quiz2/screens/words/results.dart';
import 'package:quiz2/widgets/play.dart';
class QuizPlay extends StatefulWidget {
final String quizId;
QuizPlay(this.quizId);
#override
_QuizPlayState createState() => _QuizPlayState();
}
int _correct = 0;
int _incorrect = 0;
int _notAttempted = 0;
int total = 0;
/// Stream
Stream infoStream;
class _QuizPlayState extends State<QuizPlay> {
QuerySnapshot questionSnaphot;
DatabaseService databaseService = new DatabaseService();
bool isLoading = true;
#override
void initState() {
databaseService.getQuizData(widget.quizId).then((value) {
questionSnaphot = value;
_notAttempted = questionSnaphot.docs.length;
_correct = 0;
_incorrect = 0;
isLoading = false;
total = questionSnaphot.docs.length;
setState(() {});
print("init don $total ${widget.quizId} ");
});
if (infoStream == null) {
infoStream = Stream<List<int>>.periodic(Duration(milliseconds: 100), (x) {
print("this is x $x");
return [_correct, _incorrect];
});
}
super.initState();
}
QuestionModel getQuestionModelFromDatasnapshot(
DocumentSnapshot questionSnapshot) {
QuestionModel questionModel = new QuestionModel();
questionModel.question = questionSnapshot["question"];
/// shuffling the options
List<String> options = [
questionSnapshot["option1"],
questionSnapshot["option2"],
questionSnapshot["option3"],
questionSnapshot["option4"]
];
options.shuffle();
questionModel.option1 = options[0];
questionModel.option2 = options[1];
questionModel.option3 = options[2];
questionModel.option4 = options[3];
questionModel.correctOption = questionSnapshot["option1"];
questionModel.answered = false;
print(questionModel.correctOption.toLowerCase());
return questionModel;
}
#override
void dispose() {
infoStream = null;
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Align(
child: Text("#ЄВІ_ПІДГОТОВКА"),
alignment: Alignment.center,
),
),
body: isLoading
? Container(
child: Center(child: CircularProgressIndicator()),
)
: SingleChildScrollView(
child: Container(
child: Column(
children: [
InfoHeader(
length: questionSnaphot.docs.length,
),
SizedBox(
height: 10,
),
questionSnaphot.docs == null
? Container(
child: Center(
child: Text("No Data"),
),
)
: ListView.builder(
itemCount: questionSnaphot.docs.length,
shrinkWrap: true,
physics: ClampingScrollPhysics(),
itemBuilder: (context, index) {
return QuizPlayTile(
questionModel: getQuestionModelFromDatasnapshot(
questionSnaphot.docs[index]),
index: index,
);
})
],
),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.check),
onPressed: (){
Navigator.pushReplacement(context, MaterialPageRoute(
builder: (contex) => Results(
correct: _correct,
incorrect: _incorrect,
total: total,
) ));
},)
);
}
}
class InfoHeader extends StatefulWidget {
final int length;
InfoHeader({#required this.length});
#override
_InfoHeaderState createState() => _InfoHeaderState();
}
class _InfoHeaderState extends State<InfoHeader> {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: infoStream,
builder: (context, snapshot) {
return snapshot.hasData
? Container(
height: 40,
margin: EdgeInsets.only(left: 14),
child: ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
children: <Widget>[
NoOfQuestionTile(
text: "Total",
number: widget.length,
),
NoOfQuestionTile(
text: "Correct",
number: _correct,
),
NoOfQuestionTile(
text: "Incorrect",
number: _incorrect,
),
NoOfQuestionTile(
text: "NotAttempted",
number: _notAttempted,
),
],
),
)
: Container();
});
}
}
class QuizPlayTile extends StatefulWidget {
final QuestionModel questionModel;
final int index;
QuizPlayTile({#required this.questionModel, #required this.index});
#override
_QuizPlayTileState createState() => _QuizPlayTileState();
}
class _QuizPlayTileState extends State<QuizPlayTile> {
String optionSelected = "";
#override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.symmetric(horizontal: 20),
child: Text(
"Q${widget.index + 1}. ${widget.questionModel.question}",
style:
TextStyle(fontSize: 18, color: Colors.black.withOpacity(0.8)),
),
),
SizedBox(
height: 12,
),
GestureDetector(
onTap: () {
if (!widget.questionModel.answered) {
///correct
if (widget.questionModel.option1 ==
widget.questionModel.correctOption) {
setState(() {
optionSelected = widget.questionModel.option1;
widget.questionModel.answered = true;
_correct = _correct + 1;
_notAttempted = _notAttempted - 1;
});
} else {
setState(() {
optionSelected = widget.questionModel.option1;
widget.questionModel.answered = true;
_incorrect = _incorrect + 1;
_notAttempted = _notAttempted - 1;
});
}
}
},
child: OptionTile(
option: "A",
description: "${widget.questionModel.option1}",
correctAnswer: widget.questionModel.correctOption,
optionSelected: optionSelected,
),
),
SizedBox(
height: 4,
),
GestureDetector(
onTap: () {
if (!widget.questionModel.answered) {
///correct
if (widget.questionModel.option2 ==
widget.questionModel.correctOption) {
setState(() {
optionSelected = widget.questionModel.option2;
widget.questionModel.answered = true;
_correct = _correct + 1;
_notAttempted = _notAttempted - 1;
});
} else {
setState(() {
optionSelected = widget.questionModel.option2;
widget.questionModel.answered = true;
_incorrect = _incorrect + 1;
_notAttempted = _notAttempted - 1;
});
}
}
},
child: OptionTile(
option: "B",
description: "${widget.questionModel.option2}",
correctAnswer: widget.questionModel.correctOption,
optionSelected: optionSelected,
),
),
SizedBox(
height: 4,
),
GestureDetector(
onTap: () {
if (!widget.questionModel.answered) {
///correct
if (widget.questionModel.option3 ==
widget.questionModel.correctOption) {
setState(() {
optionSelected = widget.questionModel.option3;
widget.questionModel.answered = true;
_correct = _correct + 1;
_notAttempted = _notAttempted - 1;
});
} else {
setState(() {
optionSelected = widget.questionModel.option3;
widget.questionModel.answered = true;
_incorrect = _incorrect + 1;
_notAttempted = _notAttempted - 1;
});
}
}
},
child: OptionTile(
option: "C",
description: "${widget.questionModel.option3}",
correctAnswer: widget.questionModel.correctOption,
optionSelected: optionSelected,
),
),
SizedBox(
height: 4,
),
GestureDetector(
onTap: () {
if (!widget.questionModel.answered) {
///correct
if (widget.questionModel.option4 ==
widget.questionModel.correctOption) {
setState(() {
optionSelected = widget.questionModel.option4;
widget.questionModel.answered = true;
_correct = _correct + 1;
_notAttempted = _notAttempted - 1;
});
} else {
setState(() {
optionSelected = widget.questionModel.option4;
widget.questionModel.answered = true;
_incorrect = _incorrect + 1;
_notAttempted = _notAttempted - 1;
});
}
}
},
child: OptionTile(
option: "D",
description: "${widget.questionModel.option4}",
correctAnswer: widget.questionModel.correctOption,
optionSelected: optionSelected,
),
),
SizedBox(
height: 20,
),
],
),
);
}
}
Is there any way to do it? In case you want to see the code please let me know I will update more.
Is there any way to do it? In case you want to see the code please let me know I will update more.
It all comes back to how you save your data.
Can questions be shared between quizzes
here are sample db diagram
If your Questions can be shared between quizzes you might want to have a collection with all the quizzes and another with questions and tag each question with a quizID, and its no/ occurrence.
How To query for this, get a quiz ID and fetch all questions for where quizID match and order by no/ occurrence
so your db would look something like this
collectionQUiz-|_quiz0
|_quiz1
|_quiz2-|-quizId,
|-timestamp //always good to use for querying
collectionQuestions-|q1d-|quizId
|-other prams
|q2d-|quizId
|-other prams
querying questions you can use timestamps, We cant see how you create the questions, my guess, you are either creating one question at a time, uploading using a for loop,sending a list, if you add a timestamp key to your question object, you can use it to query for the first added question for that quiz.
Else the one creating a quiz can number the questions ,or you number them before uploading , and save the value in the db and use it to sort
I think this would be a better approach, with firestore indexing , your database should handle this easily
Related
I am using my rest API to get data in the app I used HTTP I write some code for pagination. the pagination is working good but when my all data get returned it's not show a message to all data is returned else its starts duplicate data from the top again. it's creating the duplicate data again pagination not stopping its returning the same data again and agin
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_browser/flutter_web_browser.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'dart:convert';
import "package:http/http.dart" as http;
import 'package:voterlist/Widgets/adhelper.dart';
import 'package:voterlist/Widgets/constant.dart';
import 'package:voterlist/Widgets/skeleton.dart';
import 'package:voterlist/Widgets/widget.dart';
class Jobs extends StatefulWidget {
const Jobs({Key? key}) : super(key: key);
#override
State<Jobs> createState() => _JobsState();
}
class _JobsState extends State<Jobs> {
final adManager = AdManager();
// We will fetch data from this Rest api
final _baseUrl = 'api';
// At the beginning, we fetch the first 20 posts
int _page = 0;
// you can change this value to fetch more or less posts per page (10, 15, 5, etc)
final int _limit = 15;
// There is next page or not
bool _hasNextPage = true;
// Used to display loading indicators when _firstLoad function is running
bool _isFirstLoadRunning = false;
// Used to display loading indicators when _loadMore function is running
bool _isLoadMoreRunning = false;
// This holds the posts fetched from the server
List _posts = [];
// This function will be called when the app launches (see the initState function)
void _firstLoad() async {
setState(() {
_isFirstLoadRunning = true;
});
try {
final res = await http.get(Uri.parse("$_baseUrl?$_page&$_limit"));
setState(() {
_posts = json.decode(res.body);
});
} catch (err) {
if (kDebugMode) {
print('Something went wrong');
}
}
setState(() {
_isFirstLoadRunning = false;
});
}
// This function will be triggered whenver the user scroll
// to near the bottom of the list view
void _loadMore() async {
if (_hasNextPage == true &&
_isFirstLoadRunning == false &&
_isLoadMoreRunning == false &&
_controller.position.extentAfter < 300) {
setState(() {
_isLoadMoreRunning = true; // Display a progress indicator at the bottom
});
_page += 1; // Increase _page by 1
try {
final res = await http.get(Uri.parse("$_baseUrl?$_page&$_limit"));
final List fetchedPosts = json.decode(res.body);
if (fetchedPosts.isNotEmpty) {
if (!fetchedPosts.contains(fetchedPosts)) {
setState(() {
_posts.addAll(fetchedPosts);
});
}
} else {
// This means there is no more data
// and therefore, we will not send another GET request
setState(() {
_hasNextPage = false;
});
}
} catch (err) {
if (kDebugMode) {
print('Something went wrong!');
}
}
setState(() {
_isLoadMoreRunning = false;
});
}
}
// The controller for the ListView
late ScrollController _controller;
#override
void initState() {
super.initState();
_firstLoad();
_controller = ScrollController()..addListener(_loadMore);
adManager.addAds(true, true, true);
adManager.showInterstitial();
}
#override
void dispose() {
_controller.removeListener(_loadMore);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: headerNav(title: "Sarkari Naukri"),
body: _isFirstLoadRunning
? ListView.separated(
itemCount: 5,
itemBuilder: (context, index) => const NewsCardSkelton(),
separatorBuilder: (context, index) =>
const SizedBox(height: defaultPadding),
)
: Column(
children: [
Expanded(
child: ListView.separated(
controller: _controller,
itemCount: _posts.length,
itemBuilder: (_, index) => JobCard(
onPress: () {
FlutterWebBrowser.openWebPage(
url: _posts[index]['moredetils'],
safariVCOptions: const SafariViewControllerOptions(
barCollapsingEnabled: true,
preferredBarTintColor: Colors.green,
preferredControlTintColor: Colors.amber,
dismissButtonStyle:
SafariViewControllerDismissButtonStyle.close,
modalPresentationCapturesStatusBarAppearance: true,
),
);
},
image: _posts[index]['imagelink'],
state: _posts[index]['state'],
title: _posts[index]['title'],
subtitle: _posts[index]['totalpost'].toString(),
),
separatorBuilder: (BuildContext context, int index) {
if ((index + 1) % 4 == 0) {
return Container(
width: adManager.getBannerAd()?.size.width.toDouble(),
height:
adManager.getBannerAd()?.size.height.toDouble(),
child: AdWidget(ad: adManager.getBannerAd()!),
);
}
return const SizedBox();
},
),
),
// when the _loadMore function is running
if (_isLoadMoreRunning == true)
const Padding(
padding: EdgeInsets.only(top: 10, bottom: 40),
child: Center(
child: CircularProgressIndicator(),
),
),
// When nothing else to load
if (_hasNextPage == false)
Container(
padding: const EdgeInsets.only(top: 30, bottom: 40),
color: Colors.amber,
child: const Center(
child: Text('No more jobs'),
),
),
],
),
);
}
}
class NewsCardSkelton extends StatelessWidget {
const NewsCardSkelton({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Row(
children: [
const Skeleton(height: 120, width: 120),
const SizedBox(width: defaultPadding),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Skeleton(width: 80),
const SizedBox(height: defaultPadding / 2),
const Skeleton(),
const SizedBox(height: defaultPadding / 2),
const Skeleton(),
const SizedBox(height: defaultPadding / 2),
Row(
children: const [
Expanded(
child: Skeleton(),
),
SizedBox(width: defaultPadding),
Expanded(
child: Skeleton(),
),
],
)
],
),
)
],
);
}
}
Thank you.
This code looks good to me!!. Maybe you should clear _posts in loadMore(). For example:
// This function will be triggered whenver the user scroll
// to near the bottom of the list view
void _loadMore() async {
if (_hasNextPage == true &&
_isFirstLoadRunning == false &&
_isLoadMoreRunning == false &&
_controller.position.extentAfter < 300) {
setState(() {
_isLoadMoreRunning = true; // Display a progress indicator at the bottom
});
_page += 1; // Increase _page by 1
try {
final res = await http.get(Uri.parse("$_baseUrl?$_page&$_limit"));
final List fetchedPosts = json.decode(res.body);
if (fetchedPosts.isNotEmpty) {
if (!fetchedPosts.contains(fetchedPosts)) {
setState(() {
// added this line
_posts.clear();
_posts.addAll(fetchedPosts);
});
}
} else {
// This means there is no more data
// and therefore, we will not send another GET request
setState(() {
_hasNextPage = false;
});
}
} catch (err) {
if (kDebugMode) {
print('Something went wrong!');
}
}
setState(() {
_isLoadMoreRunning = false;
});
}
}
I can see your URL pattern look like trying to use GET URL parameters like this $_baseUrl?$_page&$_limit, I think it supposed to be something like $_baseUrl?page=$_page&limit=$_limit
so im trying to build a lazyload, from my backend i already made per page 5 items. but i dont understand why when i try in flutter the first page is always loaded twice?
for example if i scroll up to page 5, the order of the page will be 1,1,2,3,4 then when i try to refresh, it will show page 5, refresh again then it will show page 1, any idea why?
class ProgressScreen extends StatefulWidget {
#override
_ProgressScreenState createState() => _ProgressScreenState();
}
class _ProgressScreenState extends State<ProgressScreen> {
late MainProgressStore _mainProgressStore;
late UserStore _userStore;
int currentPage = 1;
late int totalPages;
#override
void initState() {
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
// initializing stores
_mainProgressStore = Provider.of<MainProgressStore>(context);
_userStore = Provider.of<UserStore>(context);
if (!_userStore.loading) {
_userStore.getUser();
}
if (!_mainProgressStore.loading) {
_mainProgressStore.postMainProgress(
'loading', 'all', 'desc', 3, 0, currentPage);
}
}
List<Datum> mainProgress = [];
final RefreshController refreshController =
RefreshController(initialRefresh: true);
Future<bool> getmainProgress({bool isRefresh = false}) async {
if (isRefresh) {
currentPage = 1;
} else {
if (currentPage >= totalPages) {
refreshController.loadNoData();
return false;
}
}
final response = await _mainProgressStore.postMainProgress(
'loading', 'all', 'desc', 3, 0, currentPage);
if (_mainProgressStore.success == true) {
final result = _mainProgressStore.mainProgress!.mainProgress;
if (isRefresh) {
mainProgress = result.response;
} else {
mainProgress.addAll(result.response);
}
currentPage++;
totalPages = 10;
print(result.response);
setState(() {});
return true;
} else {
return false;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppNavBar(),
drawer: DrawerNavBar(userStore: _userStore),
body: _buildMainContent(),
);
}
Widget _buildMainContent() {
return Observer(
builder: (context) {
return _mainProgressStore.success
? _buildRefresh(_mainProgressStore)
: CustomProgressIndicatorWidget();
},
);
}
Widget _buildRefresh(_mainProgressStore) {
return Platform.isIOS
? _buildIOSList(_mainProgressStore)
: _refreshView(_mainProgressStore);
}
Widget _refreshView(_mainProgressStore) {
return SmartRefresher(
controller: refreshController,
enablePullUp: true,
onRefresh: () async {
final result = await getmainProgress(isRefresh: true);
if (result) {
refreshController.refreshCompleted();
} else {
refreshController.refreshFailed();
}
},
onLoading: () async {
final result = await getmainProgress();
if (result) {
refreshController.loadComplete();
} else {
refreshController.loadFailed();
}
},
child: _buildBody(_mainProgressStore));
}
Widget _buildIOSList(_mainProgressStore) {
return Container(
child: CustomScrollView(
slivers: [
CupertinoSliverRefreshControl(
onRefresh: () async {
_mainProgressStore.getHomepage(currentPage);
},
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return _buildBody(_mainProgressStore);
}, childCount: _mainProgressStore.response.length))
],
),
);
}
Widget _buildBody(_mainProgressStore) {
return SingleChildScrollView(
child: Column(children: <Widget>[
Align(
alignment: Alignment.centerLeft,
child: Container(
padding: EdgeInsets.only(
top: DeviceUtils.getScaledHeight(context, 0.03),
left: DeviceUtils.getScaledWidth(context, 0.06),
bottom: DeviceUtils.getScaledHeight(context, 0.03)),
child: Text(
'MY ORDERS',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: DeviceUtils.getScaledWidth(context, 0.035),
color: Colors.black),
),
),
),
SearchProgress(
currentPage: currentPage, mainProgressStore: _mainProgressStore),
OrderInfo(
progressData: mainProgress, mainProgressStore: _mainProgressStore),
]));
}
}
I have this comment section in my application.
User can like a comment.
Whenever a comment is added, I call the database to get the latest comment and rebuild the class.
But as shown below When a new comment is added, it retains the state of the previous comment
as seen above when a not liked comment is added it is having state of previous comment
Below is the given code
#override
void initState() {
getComments();
super.initState();
}
getComments() async {
try {
List<Comment> commentList =
await Provider.of<Database>(context, listen: false)
.postComments(widget.postid);
setState(() {
data = commentList;
});
} catch (e) {
setState(() {
data = 'error';
});
// throw ('e');
}
}
dynamic data;
addComment(BuildContext context) async {
String commentData = _textEditingController.text.trim();
if (commentData.isNotEmpty) {
setState(() {
showShimmer = true;
_textEditingController.clear();
});
bool result = await Provider.of<Database>(context, listen: false)
.addMessageToPost(widget.postid, true, commentData);
if (result) {
getComments();
}
}
}
final TextEditingController _textEditingController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: settingsAppBar(context, 'Comments'),
body: Column(
children: [
showShimmer
? Shimmer.fromColors(
baseColor: Colors.grey[200]!,
highlightColor: Colors.grey[100]!,
child: ShimmerWidget())
: Container(),
Expanded(
child: data == null
? Center(child: CircularProgressIndicator())
: data == 'error'
? Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextButton(
onPressed: () {
getComments();
},
child: Text('Retry')),
],
),
)
: data.length >= 1
? RefreshIndicator(
onRefresh: () async {
setState(() {
data = null;
});
getComments();
},
child: ListView(
controller: _scrollController,
children: buidlCommentWidgets()),
)
: Text('No data'),
),
SafeArea(...textfield) ],
),
),
],
),
),
],
),
);
}
List<Widget> buidlCommentWidgets() {
List<Widget> commentWidgetList = [];
for (var i = 0; i < data.length; i++) {
commentWidgetList.add(Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0),
child: CommentListTile(
comment: data[i],
postUid: widget.postedUid,
),
));
}
return commentWidgetList;
}
}
//////////////////////////////////////////////////////////////
class CommentListTile extends StatefulWidget {
const CommentListTile({
Key? key,
required this.comment,
required this.postUid,
this.isReply = false,
}) : super(key: key);
final Comment comment;
final String postUid;
final bool isReply;
#override
_CommentListTileState createState() => _CommentListTileState();
}
class _CommentListTileState extends State<CommentListTile> {
late bool isLiked;
late int likeCount;
handleLike(BuildContext context) async {
setState(() {
isLiked = !isLiked;
likeCount = isLiked ? likeCount + 1 : likeCount - 1;
});
bool result = await Provider.of<Database>(context, listen: false)
.postActivity(
PostActivityEnum.likeDislikeComment, widget.comment.commentId);
if (!result) {
setState(() {
isLiked = !isLiked;
likeCount = isLiked ? likeCount + 1 : likeCount - 1;
});
}
}
#override
void initState() {
isLiked = widget.comment.isLikedByViewer;
likeCount = widget.comment.likesCount;
super.initState();
}
#override
Widget build(BuildContext context) {
....ListTile
This is a very common problem in the flutter.
This happens due to the widget tree and the render tree difference, Identification of the widget by its own Key can be easier for the flutter to keep an update on both render and widget tree.
For an easy solution, you can pass a unique key to the CommentListTile widget while building the list and also while adding a new widget to the list.
List<Widget> buidlCommentWidgets() {
List<Widget> commentWidgetList = [];
for (var i = 0; i < data.length; i++) {
commentWidgetList.add(Padding(
padding: const EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0),
child: CommentListTile(
key: SOME_UNIQUE_KEY_HERE // Add Unique key here
comment: data[i],
postUid: widget.postedUid,
),
));
}
return commentWidgetList;
}
It can be easy if you have some kind of commentId.
EDIT: 27 Aug 2021
You definitely should use your buildCommentWidgets() method like this:
List<Widget> buidlCommentWidgets() {
return data.map((comment) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: CommentListTile(
key: SOME_UNIQUE_KEY_HERE // Add Unique key here
comment: comment,
postUid: widget.postedUid,
),
);
}).toList();
}
I'm passing data from a stateful widget, and I access it like in this example (widget.variable)
https://stackoverflow.com/a/50818870/806009
However, occasionally it throws an error (about 1 in 20) for a line in this comparison
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The method '>' was called on null.
Receiver: null
Tried calling: >(10)
if (widget.x > 10){...}
It seems that widget.x is not known at this time. Should I use initState to ensure that value is "ready"?
Such as
#override
void initState() {
super.initState();
this.x = widget.x
}
Full Code
class PlayerSelection extends StatefulWidget {
final int teamId;
final int leagueId;
final League.League league;
final String className;
List<PlayerObj> playersListOfClass;
final int index;
final int remaining;
PlayerSelection({Key key, #required this.teamId, this.leagueId, this.league, this.className, this.playersListOfClass, this.index, this.remaining}) : super(key: key);
#override
_PlayerSelection createState() => _PlayerSelection();
}
class _PlayerSelection extends State<PlayerSelection> {
var playerWidgets = <Widget>[];
List<PlayerObj> selectedPlayers = [];
List<PlayerObj> playerList = [];
PlayerObj draftedPlayer;
List<PlayerClass> playerClassList = [];
bool _isFavorited = false;
Modal modal = new Modal();
int lineupWeek = 0;
int lineupSize = 0;
String intervalLabel = '';
bool _is_full = false;
bool _is_locked = false;
int remaining = 0;
var currentColor = Colors.black;
var rejectedPlayerId;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
resizeToAvoidBottomPadding: false,
appBar: globals.MyAppBar(
leading: IconButton(icon: Icon(Icons.close),
onPressed: (){
Navigator.pop(context, {'player': null,'index': this.widget.index});
}), // go to league route,),
title: Text(widget.className)
),
body: Container(
child: Column(children: [Expanded(child: ListView(children: _getLineup(this.widget.playersListOfClass))),]),
),
);
}
List<Widget>_getLineup(playerList) {
List<Widget> widgets = [];
var index = 0;
playerList.forEach((player) =>
widgets.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal:16.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(width: 1.0, color: Colors.grey.withOpacity(0.3)),
),
),
child: new ListTile(
leading: GestureDetector(
onTap: (){
if(!_is_full) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) =>
Players.PlayerDetail(playerId: player.playerId,
leagueId: widget.leagueId,
playerName: player.playerName,
playerBio: player.playerBio)),
);
}
},
child: ClipOval(
child: _playerPicWidget(player)
),
),
title: GestureDetector(
onTap: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) =>
Players.PlayerDetail(playerId: player.playerId,
leagueId: widget.leagueId,
playerName: player.playerName,
playerBio: player.playerBio)),
);
},
child: _playerNameWidget(player)
),
trailing: _trailingWidget(player),
onTap: () => player.playerPrice <= widget.remaining ? Navigator.pop(context, {'player': player,'index': this.widget.index}) : null,
// return player back
),
),
)
)
);
return widgets;
}
Widget _playerNameWidget(player){
if(this._is_full && !this.selectedPlayers.contains(player)){ //show disabled selection
return Opacity(opacity:.25, child:Text("${player.playerName}"));
}
else {
if(this.widget.league.hasBudget){ // can afford player, not full
if(player.playerPrice <= widget.remaining || this.selectedPlayers.contains(player)){
return Text("${player.playerName}");
}
else { // can't afford player, not full
return Opacity(opacity:.25, child:Text("${player.playerName}"));
}
}
else { // slot still open
return Text("${player.playerName}");
}
}
}
Widget _playerPicWidget(player){
if(player == this.draftedPlayer){
return Opacity(opacity: .25, child: Image.network('${player.playerImageUrl}',
fit: BoxFit.scaleDown,
height: 45,
width: 45,
));
}
else {
if(player.playerPrice <= widget.remaining || this.selectedPlayers.contains(player)){
return Image.network('${player.playerImageUrl}',
fit: BoxFit.scaleDown,
height: 45,
width: 45,
);
}
}
}
Widget _trailingWidget(player){
List<Widget> tWidgets;
double playerOpacity = 1;
if(player.playerPrice > widget.remaining){
playerOpacity = .25;
}
tWidgets = [
Padding(padding: const EdgeInsets.symmetric(horizontal:10.0),
child: Opacity(opacity:playerOpacity, child:Text("\$${globals.commaFormat.format(player.playerPrice)}")),
), Opacity(opacity: playerOpacity, child: Icon(Icons.add))];
return Row(mainAxisSize: MainAxisSize.min, children: tWidgets);
}
}
The issue was a data issue unrelated to the passing of data.
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!