List view not getting rebuild properly - flutter

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

Related

Flutter rest api getting double result in pagination

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

flutter - futurebuilder doesn't load until textformfield is clicked

My Bad
It was problem with my future function HashtagService().getSuggestion('topic'); returning empty List before data is properly loaded. EditTopicPage didn't have any problem.
Original Question
I have text form field inside futurebuilder. When I first open page, future is not loaded. When I click on text field to enter something, future is loaded.
I want future to be loaded when the page is first opened.
class EditTopicPage extends StatefulWidget {
const EditTopicPage({required UserProfileModel userProfile, Key? key}) : _userProfile = userProfile, super(key: key);
final UserProfileModel _userProfile;
#override
_EditTopicPageState createState() => _EditTopicPageState();
}
class _EditTopicPageState extends State<EditTopicPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _controller = TextEditingController();
List<String> _hashList = [];
List<String> _suggestionList = [];
late final Future<List<String>> _future;
bool _disabled = true;
final RegExp hashRegex = RegExp(r'^[a-z|A-Z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣|ㆍ|ᆢ]*$');
#override
void initState() {
_future = HashtagService().getSuggestion('topic');
_hashList = widget._userProfile.topic.cast<String>();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
UserProfileService _userProfileService = UserProfileService();
final Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
appBar: TopBar(pageTitle: "관심 대화 주제 수정", context: context),
body: ListView(
padding: EdgeInsets.all(16.sp),
children: [
FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
_suggestionList = List<String>.from(snapshot.data.reversed);
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SingleLineTextFormField(
controller: _controller,
hintText: '어떤 주제로 대화하고 싶으신가요?',
onChanged: (value) {
if (value.length > 1) {
var lastChar = value.substring(
value.length - 1);
if (!hashRegex.hasMatch(lastChar)) {
var newHash = value.substring(
0, value.length - 1);
if (!_hashList.contains(newHash)) {
setState(() {
_hashList.add(newHash);
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
}
_controller.clear();
}
} // else if (_expHash.length == 3 && value.isNotEmpty) {
_formKey.currentState!.validate();
},
validator: (value) {
if (!hashRegex.hasMatch(value!)) {
return '\u26a0 한글, 영문만 가능해요';
}
return null;
}
),
DefaultSpacing(),
Row(
children: [
Text('추천 : ',
style: TextStyle(
fontSize: 10.sp,
color: Colors.grey[800],
),
),
Wrap(
spacing: 6.0,
runSpacing: 6.0,
children: _suggestionList.map((suggestion) =>
HashtagSuggestionChip(
suggestion: suggestion,
type: 'topic',
onPressed: () {
if (!_hashList.contains(suggestion)) {
setState(() {
_hashList.add(suggestion);
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
}
},
)).toList(),
),
],
),
DefaultSpacing(),
Wrap(
spacing: 6.0,
runSpacing: 6.0,
children: _hashList.map((hashtag) =>
HashtagChip(
hashtag: hashtag,
type: 'topic',
onDelete: () {
setState(() {
_hashList.remove(hashtag);
_formKey.currentState!.validate();
_disabled = _hashList.isEmpty || (_hashList.length > 3);
});
},
)).toList()
),
]),
);
}
return CircularProgressIndicator();
}
),
],
),
floatingActionButton: FloatingSaveButton(context: context,
text: '저장',
width: size.width * 0.9,
disabled: _disabled,
onPressed: () async {
_userProfileService.updateTopicHashtag(hashList: _hashList);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(SaveSnackBar());
}),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}
Result: when the page is first opened
Neither hashtag nor progress indicator is shown.
After text form field is selected
I searched similar questions in stackoverflow, but none of the answers solved my problem.
Try like this
#override
void initState() {
setState(() {
_future = HashtagService().getSuggestion('topic');
_hashList = widget._userProfile.topic.cast<String>();
});
super.initState();
}
so your _future is async so the widget will be loaded before loading the service

how to add onscroll fetch data (pagination) in flutter & firebase realtime?

Hope Well,
I am using firebase realtime database on my flutter app (similar social media app). i have feed page and feed state page. i wanna show 10 posts first and after scroll bottom, load again 10 posts. i tried some methods but not working.
my codes
feed page code
Widget build(BuildContext context) {
var authstate = Provider.of<AuthState>(context, listen: false);
return Consumer<FeedState>(
builder: (context, state, child) {
final List<FeedModel> list = state.getPostList(authstate.userModel);
return CustomScrollView(
slivers: <Widget>[
child,
state.isBusy && list == null
? SliverToBoxAdapter(
child: Container(
height: fullHeight(context) - 135,
child: CustomScreenLoader(
height: double.infinity,
width: fullWidth(context),
backgroundColor: Colors.white,
),
),
)
: !state.isBusy && list == null
? SliverToBoxAdapter(
child: EmptyList(
'Follow someone',
subTitle:
'goto search page to find & follow Someone.\n When they added new post,\n they\'ll show up here.',
),
)
: SliverList(
delegate: SliverChildListDelegate(
list.map(
(model) {
return Container(
color: Colors.white,
child: Post(
model: model,
trailing: PostBottomSheet().PostOptionIcon(
context,
model: model,
type: PostType.Post,
scaffoldKey: scaffoldKey),
),
);
},
).toList(),
),
)
],
);
},
feed state code
List<FeedModel> get feedlist {
if (_feedlist == null) {
return null;
} else {
return List.from(_feedlist.reversed);
}
}
List<FeedModel> getPosttList(UserModel userModel) {
if (userModel == null) {
return null;
}
return feedlist;
}
I modified your code and use a ScrollController to load more data when the user reaches the end of the loaded data. (The data provider is hard-coded but you should be able to relate it to your scenario.) Note that I changed your code to use SliverChildBuilderDelegate which is more efficient.
import 'package:flutter/material.dart';
class ScrollTest extends StatefulWidget {
#override
_ScrollTestState createState() => _ScrollTestState();
}
class _ScrollTestState extends State<ScrollTest> {
bool isLoading = false;
bool isEnd = false;
final List<FeedModel> list = [];
ScrollController _controller;
_scrollListener() async {
var position = _controller.offset /
(_controller.position.maxScrollExtent -
_controller.position.minScrollExtent);
if (position > 0.5 && !_controller.position.outOfRange) {
await _getMoreData(list.length);
}
}
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_scrollListener);
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_getMoreData(list.length);
}
Future<void> _getMoreData(int index) async {
if (!isLoading) {
setState(() {
isLoading = true;
});
var tlist = await Feed.getPostList(index);
setState(() {
if (tlist.length == 0) {
isEnd = true;
} else {
list.addAll(tlist);
index = list.length;
}
isLoading = false;
});
}
}
#override
Widget build(BuildContext context) {
return CustomScrollView(
controller: _controller,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.white,
height: 300,
child: Text(list[index].text),
);
},
childCount: list.length,
),
),
SliverFillRemaining(
child: Center(
child: isEnd ? Text('End') : CircularProgressIndicator(),
)),
],
);
}
}
// Dummy FeedModel
class FeedModel {
final String text;
FeedModel(this.text);
}
// Dummy Feed provider
class Feed {
static final data = [
FeedModel('1'),FeedModel('2'),FeedModel('3'),FeedModel('4'),
FeedModel('5'),FeedModel('6'),FeedModel('7'),FeedModel('8'),
FeedModel('9'),FeedModel('10'),FeedModel('11'),FeedModel('12'),
FeedModel('13'),
];
static Future<List<FeedModel>> getPostList(int index) async {
List<FeedModel> l = [];
for (var i = index; i < index + 5 && i < data.length; i++) {
l.add(data[i]);
}
await Future.delayed(Duration(seconds: 1));
return l;
}
}

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 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!