Generating widgets in Pageview based on conditional logic - flutter

Currently facing a problem with conditional logic and PageView widget. Let's say that the PageView will dynamically generate 3 pages. In those 3 pages different widgets will be generated. One of the widgets is a button (called "Next"), which is a PageController, but that widget has to be replaced by a button widget that is supposed to submit (called "Submit") the whole form (the PageView is wrapped in a form).
It seems obvious, just write conditional logic that compares the current page of the PageView to the length of the PageView (PageView is populated with a List, so it is easy to get the length). Then switch the widgets when the right conditions meet: when current page equals to 3, change the widget. Unfortunately, the PageView renders the "Next" button on every page. So only when I get to the last page and then click "Next" again will it change to "Submit". It is supposed to be "Submit", when the user gets on the last page.
const int TRIVIA_STARTING_TIME = 10;
class TriviaOneForm extends StatefulWidget {
final UserRepository _userRepository;
TriviaOneForm({Key key, #required UserRepository userRepository})
: assert(userRepository != null),
_userRepository = userRepository,
super(key: key);
State<TriviaOneForm> createState() => _TriviaOneFormState();
}
class _TriviaOneFormState extends State<TriviaOneForm> {
final TextEditingController _answerController = TextEditingController();
UserRepository get _userRepository => widget._userRepository;
TriviaOneBloc _triviaOneBloc;
PageController _pageController;
Timer _timer;
bool _isLoadingScreen;
bool _isNextOrSubmitButton;
int _start;
int _indexOfCarouselItem;
List<int> _selectedValList;
List _triviaDataList;
#override
void initState() {
super.initState();
_isLoadingScreen = true;
_getTriviaData();
_pageController = PageController();
_indexOfCarouselItem = 0;
_isNextOrSubmitButton = true;
_selectedValList = [0, 0, 0, 0, 0];
_triviaDataList = [];
_start = TRIVIA_STARTING_TIME;
_triviaOneBloc = BlocProvider.of<TriviaOneBloc>(context);
_answerController.addListener(_onAnswerChanged);
}
#override
void dispose() {
if (_timer != null) {
_timer.cancel();
}
_pageController.dispose();
super.dispose();
}
void startTimer() {
const oneSec = const Duration(seconds: 1);
_timer = new Timer.periodic(
oneSec,
(Timer timer) => setState(
() {
if (_start < 1) {
timer.cancel();
} else {
_start = _start - 1;
}
},
),
);
}
#override
Widget build(BuildContext context) {
return BlocListener<TriviaOneBloc, TriviaOneState>(
listener: (context, state) {
if (state.isFailure) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('Submition Failure'),
Icon(Icons.error)
],
),
backgroundColor: Colors.red,
),
);
}
if (state.isSubmitting) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('Submitting Answers...'),
],
),
),
);
}
if (state.isSuccess) {
BlocProvider.of<TriviaOneBloc>(context).add(Submitted());
}
},
child: BlocBuilder<TriviaOneBloc, TriviaOneState>(
builder: (context, state) {
return _isLoadingScreen
? _displayLoadScreen()
: Padding(
padding: EdgeInsets.all(20.0),
child: Form(
child: PageView(
physics: NeverScrollableScrollPhysics(),
controller: _pageController,
reverse: false,
scrollDirection: Axis.horizontal,
children: _triviaDataList.map<Widget>((triviaData) {
return ListView(
shrinkWrap: true,
children: <Widget>[
Text(triviaData.getQuestion),
ListView(
shrinkWrap: true,
children: triviaData.getAnswers
.map<Widget>((triviaAnswer) {
int index =
triviaData.getAnswers.indexOf(triviaAnswer);
return ListTile(
title: Text(triviaAnswer.getAnswer),
leading: Radio(
value: index,
groupValue:
_selectedValList[_indexOfCarouselItem],
onChanged: (int value) {
setState(() {
print(value);
_selectedValList[_indexOfCarouselItem] =
value;
});
},
),
);
}).toList(),
),
_isNextOrSubmitButton ? _nextButton() : _submitButton(),
RaisedButton(
onPressed: () {
startTimer();
},
child: Text('Start'),
),
Text('$_start'),
],
);
}).toList(),
),
),
);
},
),
);
}
Widget _triviaControlButton(PageController pageController) {
if (0 < _triviaDataList.length) {
return RaisedButton(
child: Text('Next'),
onPressed: () {
pageController.nextPage(
duration: Duration(seconds: 1), curve: Curves.easeInOut);
print('Next');
},
);
} else if (pageController.page.toInt() == _triviaDataList.length) {
return RaisedButton(
child: Text('Submit'),
onPressed: () {
print('Submit');
},
);
} else {
return RaisedButton(
child: Text('Error'),
onPressed: () {
print('Error');
},
);
}
}
Widget _displayLoadScreen() {
return Container(
alignment: Alignment(0.0, 0.0),
child: CircularProgressIndicator(),
);
}
void _onAnswerChanged() {
_triviaOneBloc.add(AnswerChanged(answer: _answerController.text));
}
void _getTriviaData() async {
var data = _userRepository.retrieveTriviaData();
// Await trivia data to be retrieved from firebase
await data.getDocuments().then((collection) {
collection.documents.forEach((document) {
TriviaData triviaData = TriviaData();
List<TriviaAnswer> triviaAnswerList = List<TriviaAnswer>();
// Iterate through all of the answers for a question
// Create a list of TriviaAnswer objects to hold key and value
document.data['answers'].forEach((key, value) {
TriviaAnswer triviaAnswer = TriviaAnswer();
triviaAnswer.setAnswer = key;
triviaAnswer.setAnswerValue = value;
triviaAnswerList.add(triviaAnswer);
});
// Assign question String and answer List to TriviaData
// Add all data to data list
triviaData.setAnswers = triviaAnswerList;
triviaData.setQuestion = document.data['question'];
_triviaDataList.add(triviaData);
});
});
setState(() {
_isLoadingScreen = false;
});
}
Widget _nextButton() {
return RaisedButton(
child: Text('Next'),
onPressed: () {
if (_indexOfCarouselItem < _triviaDataList.length) {
_pageController.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut);
setState(() {
_start = TRIVIA_STARTING_TIME;
_indexOfCarouselItem += 1;
});
}
if (_indexOfCarouselItem == _triviaDataList.length) {
Future.delayed(const Duration(seconds: 0), () {
setState(() {
_isNextOrSubmitButton = false;
});
});
}
try {
if (_timer != null || !_timer.isActive) {
startTimer();
}
} catch (_) {
print('Error: Timer is already disabled');
}
},
);
}
Widget _submitButton() {
return RaisedButton(
child: Text('Submit'),
onPressed: () {
print(_selectedValList);
_userRepository.storeTriviaToFirebase();
setState(() {
if (_timer != null || _timer.isActive) {
_timer.cancel();
}
});
},
);
}
}
EDIT 1:
This is the updated code I use for the button to populate in the PageView. I am setting a String to initial value "Next" then updating it when _indexOfCarouselItem + 2 == _triviaDataList.length is true. The updated value will be "Submit", when the condition is met.
Widget _triviaControlButton() {
return RaisedButton(
child: Text(buttonText),
onPressed: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut);
if (_indexOfCarouselItem + 2 == _triviaDataList.length) {
setState(() {
buttonText = "Submit";
});
}
if (_indexOfCarouselItem < _triviaDataList.length) {
setState(() {
_start = TRIVIA_STARTING_TIME;
_indexOfCarouselItem += 1;
});
}
print(_indexOfCarouselItem);
print(_triviaDataList.length);
},
);
}

I’m on phone now so I can’t guarantee the code I’ll post is ok, but you’ll get the idea.
First: I don’t think you need 2 buttons if they are equals on size etc. so you can implement something like this:
child: Text( _indexOfCarouselItem += 1 != _triviaDataList.length
? 'Next' : 'Submit')
And then use the same logic in the onPressed:
onPressed() {
_indexOfCarouselItem += 1 != _triviaDataList.length ? doSomethibg : doSomethingDifferent;
}
Edit:
Ok if I understand correctly the problem right now is that because of the transition the button says "Submit" but there are no question yet? If this is the case you can like you said, add delay, but I think a better approach will be wire the text of the button with the question. I mean you can keep the actual logic (because it works) and add something like this:
child: Text( _indexOfCarouselItem += 1 != _triviaDataList.length && questionText != ""
? 'Next' : 'Submit')
This logic can be applied in if ... else ... block too.
Edit 2: try this one:
Widget _triviaControlButton() {
return RaisedButton(
child: Text(buttonText),
onPressed: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut);
if (_indexOfCarouselItem < _triviaDataList.length) {
setState(() {
_start = TRIVIA_STARTING_TIME;
_indexOfCarouselItem += 1;
});
if (_indexOfCarouselItem == _triviaDataList.length) {
setState(() {
buttonText = "Submit";
});
}
},
);
}

Related

Flutter : scrollController.isAttached is always false

How can I scroll to a special widget in a ListView? For example, I want to automatically scroll to some container in ListView if I press a certain button on a previous screen. I will pass to the next screen an Id (from id I will know the index) and when I navigate to the next screen I want to automatically scroll to this widget.
the code in main screen : Navigator.push(context, MaterialPageRoute(builder: (_) => CreatedEstatesScreen(estateId: id)));
the code in the next screen :
class RecentEstateOrdersScreen extends StatefulWidget {
static const String id = "RecentEstateOrdersScreen";
String? estateId;
RecentEstateOrdersScreen({Key? key, this.estateId}) : super(key: key);
#override
_RecentEstateOrdersScreenState createState() =>
_RecentEstateOrdersScreenState();
}
class _RecentEstateOrdersScreenState extends State<RecentEstateOrdersScreen> {
late RecentEstatesOrdersBloc _recentEstatesOrdersBloc;
late ItemScrollController scrollController;
late ItemPositionsListener itemPositionsListener;
String? userToken;
List<EstateOrder> orders = [];
#override
void initState() {
super.initState();
_recentEstatesOrdersBloc = RecentEstatesOrdersBloc(EstateOrderRepository());
_onRefresh();
User? user = BlocProvider.of<UserLoginBloc>(context).user;
if (user != null && user.token != null) {
userToken = user.token;
}
scrollController = ItemScrollController();
itemPositionsListener = ItemPositionsListener.create();
}
_onRefresh() {
if (BlocProvider.of<UserLoginBloc>(context).user!.token != null) {
_recentEstatesOrdersBloc.add(
RecentEstatesOrdersFetchStarted(
token: BlocProvider.of<UserLoginBloc>(context).user!.token!),
);
}
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text(
AppLocalizations.of(context)!.recent_created_orders,
),
),
body: BlocConsumer<RecentEstatesOrdersBloc, RecentEstatesOrdersState>(
bloc: _recentEstatesOrdersBloc,
listener: (context, recentOrdersState) async {
if (recentOrdersState is RecentEstatesOrdersFetchError) {
var error = recentOrdersState.isConnectionError
? AppLocalizations.of(context)!.no_internet_connection
: recentOrdersState.error;
await showWonderfulAlertDialog(
context, AppLocalizations.of(context)!.error, error);
}
},
builder: (BuildContext context, recentOrdersState) {
if (recentOrdersState is RecentEstatesOrdersFetchProgress) {
return const ClientsOrdersShimmer();
}
if (recentOrdersState is! RecentEstatesOrdersFetchComplete) {
return Container();
}
orders = recentOrdersState.estateOrders;
if (orders.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
documentOutlineIconPath,
width: 0.5.sw,
height: 0.5.sw,
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.42),
),
48.verticalSpace,
Text(
AppLocalizations.of(context)!.have_not_recent_orders,
style: Theme.of(context).textTheme.headline4,
),
],
),
);
}
if (widget.estateId != null) {
SchedulerBinding.instance!.addPostFrameCallback((_) {
jumpToOrder(orders);
});
}
return RefreshIndicator(
color: Theme.of(context).colorScheme.primary,
onRefresh: () async {
_onRefresh();
},
child: ListView.builder(
itemCount: orders.length,
itemBuilder: (_, index) {
return EstateOrderCard(
estateOrder: orders.elementAt(index),
);
}),
);
},
),
),
);
}
jumpToOrder(List<EstateOrder> orders) {
int index = getIndexFromId(orders);
if (index != -1) {
if (scrollController.isAttached) {
scrollController.scrollTo(
index: index,
duration: const Duration(seconds: 2),
curve: Curves.easeInOutCubic);
}
}
}
getIndexFromId(List<EstateOrder> orders) {
for (int i = 0; i < orders.length; i++) {
if (orders.elementAt(i).id == int.parse(widget.estateId!)) {
return i;
}
}
return -1;
}
}```
If you are using the library then you have to use ScrollablePositionedList.builder, not the normal ListView.builder.

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 lazyload loading page twice

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

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

List view not getting rebuild properly

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