I have class :
class FavoritesState extends State<Favorites> {
var configOk = isConfigLoaded;
#override
Widget build(BuildContext context) {
if (configOk) {
return Padding(
padding: EdgeInsets.only(top: 10),
child: ListView.builder(
itemCount: stationList.length,
itemBuilder: (context, position) {
return StationCard(stationList[position]);
},
),
);
} else {
return LoadingScreen();
}
}
void callback() {
setState(() {
this.configOk = true;
});
}
}
And I have method:
Future<http.Response> getConfig() async {
//Load Config
isConfigLoaded = true;
}
I need at the end this method call setstate in FavoritesState and upload information there. How I can do this? Thank you.
You can also use a FutureBuilder if you're only updating the state of the widget once. The FutureBuilder will force your widget to rebuild as soon as your getConfig() call has completed.
This would simplify things:
class Favorites extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getConfig(),
builder: (context, snapshot) {
if (snapshot.data == null) {
// display the loading screen as long as your getConfig() call hasn't finished
return LoadingScreen();
}
return Padding(
padding: EdgeInsets.only(top: 10),
child: ListView.builder(
itemCount: stationList.length,
itemBuilder: (context, position) {
return StationCard(stationList[position]);
},
),
);
},
);
}
}
And then tweak your call to something like this:
Future<http.Response> getConfig() async {
//Load Config
// return your result
return result;
}
Related
Below is my code but it is showing the SnackBar frequently when I reach the bottom of ListView. It also shows the SnackBar on the pages also but I wants to show it only one time how to do that.
final snackBar = SnackBar(content: const Text('Yay! A SnackBar!'));
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: docs.length,
itemBuilder: (context, index) {
final doc = docs[index];
print(doc);
//_checkController();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
if (_scrollController.position.pixels !=
_scrollController.position.maxScrollExtent) {
return null;
}
}
});
return builddoc(doc);
},
Because you are assigning new listeners every time item builder calls.
put this code in ititState so it just called once.
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
if (_scrollController.position.pixels !=
_scrollController.position.maxScrollExtent) {
return null;
}
}
});
Remove the listener from the itembuilder, instead use it in initState(),as everytime item is builded on listview, it will call this listener, so it is going on everytime item get builded.
You can use Listener on ScrollController, your issue is that you assign Listener to controller in build method which is wrong, you should do it once in initState. This is a full example of what you want:
class ScrollPageTest extends StatefulWidget {
const ScrollPageTest({Key? key}) : super(key: key);
#override
State<ScrollPageTest> createState() => _ScrollPageTest();
}
class _ScrollPageTest extends State<ScrollPageTest> {
ScrollController controller = ScrollController();
#override
void initState() {
// TODO: implement initState
super.initState();
controller.addListener(() {
if (controller.position.atEdge) {
if (controller.position.pixels != 0) {
final snackBar = SnackBar(content: const Text('Yay! A SnackBar!'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text('data = $index'),
);
},
itemCount: 100,
),
);
}
}
Try this:
bool isSnackBarShown = false;
...
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: docs.length,
itemBuilder: (context, index) {
final doc = docs[index];
print(doc);
//_checkController();
_scrollController.addListener(() {
if ((_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent)
&& !isSnackBarShown) {
isSnackBarShown = true;
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
if (_scrollController.position.pixels !=
_scrollController.position.maxScrollExtent) {
return null;
}
}
});
return builddoc(doc);
},
I have a stateful widget whose state builds a ListView. The ListView gets its data from an http API. I am using a Future<void> method called getData to retrieve this data and populate a List<> with it before calling setState.
My question is where should I call getData when this screen first launches? If I call it in initState(), I get the following error in the debug console:
[VERBOSE-2:ui_dart_state.cc(198)] Unhandled Exception: dependOnInheritedWidgetOfExactType<_InheritedTheme>() or dependOnInheritedElement() was called before _EventListState.initState() completed.
If I wrap the call to getData in a delayed Future, I do not see the error. Here's my code:
class _EventListState extends State<EventList> {
Future<void> getData() async {
events = [];
events = await Network.getUsers(context);
setState(() {});
}
List<Event> events = [];
#override
initState() {
super.initState();
getData(); // this cause the error
// Future.delayed(Duration(seconds: 1), getData); // this works
}
#override
build(context) {
return PlatformScaffold(
iosContentPadding: true,
body: ListView.builder(
padding: const EdgeInsets.all(10),
physics: const AlwaysScrollableScrollPhysics(),
itemCount: events.length,
itemBuilder: (context, index) => Text(events[index].summary),
),
);
}
}
Forcing a delay to retrieve the data does not feel right, so is there a better way?
Use FutureBuilder.
List<Event> events = [];
#override
Widget build(BuildContext context) {
return FutureBuilder(
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return PlatformScaffold(
iosContentPadding: true,
body: ListView.builder(
padding: const EdgeInsets.all(10),
physics: const AlwaysScrollableScrollPhysics(),
itemCount: events.length,
itemBuilder: (context, index) => Text(events[index].summary),
),
);
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else {
return Center(child: Text('Please wait its loading...'));
}
},
future: getData(),
);
}
Future<void> getData() async {
events = [];
events = await Network.getUsers(context);
}
I have problem with lazy loading. I tried may ways and packages like LazyLoadingScollview (example here), Pagewise etc.
What the problem is (probably easy to solve).
I have list of 50 events and I want to display only 10 of it at once, than add more (ex another 10) while reach the bottom of the list. (I cannot change limit from 50 to 10 and change it later because it's refreshing whole screen - need to fetch all at once).
To be more clear - need update count value dynamicly.
class DiscountTab extends DiscountsBaseTab {
#override
_DiscountTabState createState() => _DiscountTabState();
}
class _DiscountTabState extends DiscountsBaseTabState
with SnackBarMixin, TitleDescriptionTextMixin {
DiscountsBloc bloc;
PermissionStatus permissionStatus;
bool isError = false;
#override
void initState() {
super.initState();
bloc = DiscountsBloc(
DiscountsState.notProcessing(activeTab: DiscountsTabs.discount));
_onRefresh();
bloc.errors.listen((error) {
showSnackBarTextWithContext(context: context, text: error.message);
if (error.message ==
"Connection error, try again later")
isError = true;
});
}
void _onRefresh() => bloc.emitEvent(DiscountsListEventFetch(limit: 50)); //Here I'm fetching events
#override
Widget buildBody(BuildContext context) {
return StreamBuilder<List<DiscountsModel>>(
stream: bloc.dataField.stream,
builder: (BuildContext context,
AsyncSnapshot<List<DiscountsModel>> snapshot) {
if (!snapshot.hasData) {
return Container();
}
return RefreshIndicator(
onRefresh: () {
_onRefresh();
isError = false;
return Future.sync(() {
return;
});
},
color: LegionColors.primaryRedHigh,
child: buildView(context, snapshot.data));
});
}
buildView(BuildContext context, List<DiscountsModel> list) {
int count = 10;
return LazyLoadScrollView(
onEndOfPage: () => print('End of page'),
child: ListView.builder(
shrinkWrap: true,
itemCount: count + 1,
itemBuilder: (BuildContext context, int index) {
if (index == list.length) {
return Padding(
padding: const EdgeInsets.all(10.0),
child: Center(
child: SizedBox(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator())),
);
}
return DiscountsWidget(model: list[index]);
}),
);
}
}
When I'm using regular ScrollController everything works fine since this moment. I mean my print statement works when i reach bottom, hovewer i cannot use loop inside loadMore.
class DiscountTab extends DiscountsBaseTab {
#override
_DiscountTabState createState() => _DiscountTabState();
}
class _DiscountTabState extends DiscountsBaseTabState
with SnackBarMixin, TitleDescriptionTextMixin {
DiscountsBloc bloc;
PermissionStatus permissionStatus;
bool isError = false;
int count = 10;
ScrollController _scrollController = ScrollController();
#override
void initState() {
super.initState();
bloc = DiscountsBloc(
DiscountsState.notProcessing(activeTab: DiscountsTabs.discount));
_onRefresh();
bloc.errors.listen((error) {
showSnackBarTextWithContext(context: context, text: error.message);
if (error.message ==
"Connection error")
isError = true;
});
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_loadMore();
}
});
}
_loadMore() {
print('End of page');
for (int i = count; i < count + 10; i++) {
//here i should add items but:
// 1. i have it fetched already (all 50)
// 2. cannot use list.add here because it's undefined
}
}
void _onRefresh() => bloc.emitEvent(DiscountsListEventFetch(limit: 50));
#override
Widget buildBody(BuildContext context) {
return StreamBuilder<List<DiscountsModel>>(
stream: bloc.dataField.stream,
builder: (BuildContext context,
AsyncSnapshot<List<DiscountsModel>> snapshot) {
if (!snapshot.hasData) {
return Container();
}
return RefreshIndicator(
onRefresh: () {
_onRefresh();
isError = false;
return Future.sync(() {
return;
});
},
color: LegionColors.primaryRedHigh,
child: buildView(context, snapshot.data));
});
}
buildView(BuildContext context, List<DiscountsModel> list) {
return ListView.builder(
shrinkWrap: true,
controller: _scrollController,
itemCount: count,
itemBuilder: (BuildContext context, int index) {
// if (index == list.length) {
// return Padding(
// padding: const EdgeInsets.all(10.0),
// child: Center(
// child: SizedBox(
// width: 20.0,
// height: 20.0,
// child: CircularProgressIndicator())),
// );
// }
return DiscountsWidget(model: list[index]);
});
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
Background: Currently I work on a remote server image viewer,I use a FutureBuilder to show image and swith between image,it work fine,but will show blank screen in some ms between switch images.
Question: I want to remain the old image widget when FutureBuilder loading or show loading circle over old image widget instead of showing new blank loading page when loading.Or any other solution without Futurebuilder (like Scrollable Widget)?
the skeleton code:
class Viewer extends StatefulWidget {
Viewer(this.filename, {Key key}) : super(key: key);
final String filename;
#override
_ViewerState createState() {
return _ViewerState();
}
}
class _ViewerState extends State<Viewer> {
int _index = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
child: Container(
child: showImage(context, _index),
),
onPanDown: (DragDownDetails e) {
//it will change _index to move to next image when tap down
_index+=1;
}
);
}
Widget showImage(BuildContext context, int index) {
return FutureBuilder<SmbHalfResult>(
future: () async {
//load image from remote server or from memory cache,the SmbHalfResult contain a Uint8List image.
return SmbHalfResult();
}(),
builder: (BuildContext context, AsyncSnapshot<SmbHalfResult> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
} else {
//show image.
return Image.memory(snapshot.data.result.content);
}
} else {
// show loading circle,it will new a blank page,but I even want to display old image other than a blank page when loading.
return Center(child: CircularProgressIndicator());
}
},
);
}
}
finally I end up with PreloadPageView with paramter physics: new NeverScrollableScrollPhysics() and preloadPageController.jumpToPage(index), it perfectly meet my need without any flicker.
GestureDetector(
child: Container(
child: PreloadPageView.builder(
preloadPagesCount: 5,
itemBuilder: (BuildContext context, int index) =>
FutureImage(index, widget.filename),
controller: preloadPageController,
physics: new NeverScrollableScrollPhysics(),
)),
onPanDown: (DragDownDetails e) {
Offset globalPosition = e.globalPosition;
RenderBox findRenderObject = context.findRenderObject();
Size size = findRenderObject.size;
Area area = getArea(globalPosition, size);
if (area == Area.lef) {
index--;
preloadPageController.jumpToPage(index);
} else if (area == Area.right) {
index++;
preloadPageController.jumpToPage(index);
}
},
)
Provider will be good solution for this.
In your widget tree, the widget above the Viewer widget must look like this
ChangeNotifierProvider<SmbHalfResult>(
create: (context) {
var smbHalfResult = SmbHalfResult();
smbHalfResult.fetchImage(0);
return smbHalfResult;
},
child: Viewer();
)
The SmbHalfResult class should look something similar to this
class SmbHalfResult extends ChangeNotifier{
Uint8List image;
void fetchImage(int index) async {
this.image = await downloadImage(index);
notifyListeners();
}
}
And finally your actual UI must be like this
class _ViewerState extends State<Viewer> {
int _index = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
child: Container(
child: Consumer<SmbHalfResult>(
builder: (context, model) {
if(model.image != null) return Image.memory(model.image);
return CircularProgressIndicator();
}
),
),
onPanDown: (DragDownDetails e) {
Provider.of<SmbHalfResult>(context, listen: false).fetchImage();
}
);
}
}
I have a problem with my BLoC implementation, I have this code in synchronize.dart:
...
class _SynchronizeState extends State<Synchronize> {
UserBloc userBloc;
//final dbRef = FirebaseDatabase.instance.reference();
#override
Widget build(BuildContext context) {
userBloc = BlocProvider.of(context);
return Scaffold(
resizeToAvoidBottomPadding: false,
body: Container(
...
),
child: StreamBuilder(
stream: dbRef.child('info_tekax').limitToLast(10).onValue,
builder: (context, snapshot) {
if(snapshot.hasData && !snapshot.hasError){
Map data = snapshot.data.snapshot.value;
List keys = [];
data.forEach( (index, data) => keys.add(index) );
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => SynchronizeItem(title: keys[index], bottom: 10, onPressed: (){ print(keys[index]); })
);
}else{
return Container(
child: Center(
child: Text('Loading...'),
),
);
}
}
),
),
);
}
}
The previos code, works correctly, but i want implemente bloc Pattern, i have userBloc then i want to put this
userBloc.getDevicesForSinchronized()
instead of
dbRef.child('info_tekax').limitToLast(10).onValue,
my problem is this:
void getDevicesForSynchronized() {
return dbRef.child(DEVICES).limitToLast(10).onValue;
}
i get this error **A vaue of type 'Stream' can't be returned from method 'getDevicesForSynchronized' because it has a return type of 'void'
The error is very clear, but i don't know what is type that i need return, try:
Furure<void> getDevicesForSynchronized() async {
return await dbRef.child(DEVICES).limitToLast(10).onValue;
}
or
Furure<void> getDevicesForSynchronized() async {
dynamic result = await dbRef.child(DEVICES).limitToLast(10).onValue;
}
and another solutions, but I don't know how return correctly value for use in the StreamBuilder
From the error message you can see that the return type is Stream. Change your method like:
Future<Stream> getDevicesForSynchronized() async {
return dbRef.child(DEVICES).limitToLast(10).onValue;
}