Flutter FutureBuilder rebuilds unnecessarily when navigate between screens - flutter

My FutureBuilder rebuilds unnecessarily when navigate between screens. Each time I have to get the download URL from the Firebase Storage and this results in extremely flickering. How can I prevent that the FutureBuilder rebuilds everytime I navigate between screens?
I already tried the following solutions:
https://medium.com/saugo360/flutter-my-futurebuilder-keeps-firing-6e774830bc2
Flutter Switching to Tab Reloads Widgets and runs FutureBuilder
But without success. With both the FutureBuilder get always rebuild.
This is my code (Attempt 1):
class _GuestbookCardImageState extends State<GuestbookCardImage> {
final AsyncMemoizer _memoizer = AsyncMemoizer();
_getFeaturedImages() {
return this._memoizer.runOnce(() async {
return await MyStorage().getDownloadUrl(widget.guestbook.imagePath);
});
}
#override
Widget build(BuildContext context) {
return Card(
semanticContainer: true,
clipBehavior: Clip.antiAliasWithSaveLayer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
elevation: 2,
margin: EdgeInsets.all(5),
child: FutureBuilder(
future: _getFeaturedImages(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return GuestbookPlaceholder();
} else {
if (snapshot.data != null) {
return Hero(
tag: "to_single_guestbook_" + widget.guestbook.entryCode,
child: Container(
width: Get.width / 1.7,
height: Get.height / 3.5,
child: Image.network(
snapshot.data,
fit: BoxFit.cover,
),
),
);
} else {
return Hero(
tag: "to_single_guestbook_" + widget.guestbook.entryCode,
child: GuestbookPlaceholder());
}
}
}),
);
}
}
Attempt 2
class _GuestbookCardImageState extends State<GuestbookCardImage> {
Future<String> _future;
#override
void initState() {
_future = MyStorage().getDownloadUrl(widget.guestbook.imagePath);
super.initState();
}
#override
Widget build(BuildContext context) {
return Card(
semanticContainer: true,
clipBehavior: Clip.antiAliasWithSaveLayer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
elevation: 2,
margin: EdgeInsets.all(5),
child: FutureBuilder(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return GuestbookPlaceholder();
} else {
if (snapshot.data != null) {
return Hero(
tag: "to_single_guestbook_" + widget.guestbook.entryCode,
child: Container(
width: Get.width / 1.7,
height: Get.height / 3.5,
child: CachedNetworkImage(
imageUrl: snapshot.data, fit: BoxFit.cover),
),
);
} else {
return Hero(
tag: "to_single_guestbook_" + widget.guestbook.entryCode,
child: GuestbookPlaceholder());
}
}
}),
);
}
}
What I expect
I expect that after changing the screen and return to the initial screen my images still appearing without the need of reload. I am using Hero animation for the images. This loading behavior destroys my animation because while loading the image urls it shows a placeholder.
A small video how this looks: (And this happens also if I push to the detail screen and pop back to the initial screen)
What could I do to solve this issue?

I think you need to go higher in the widget tree and preserve the state of your page. you can try this solution. The solution is to use PageView widget to display your screen through the navigation bar, and the PageView allowing you to save the page state easily.

What helped me was setting the memCacheWidth property to be screen width x devicePixelRatio, so the image is cached in the highest resolution that will ever be necessary.
Note: if you want to do zooming, consider doing width x 10 (or some other number which works for your case).
CachedNetworkImage(
imageUrl: url,
memCacheWidth: mediaQuery.size.width.ceil() * mediaQuery.devicePixelRatio.ceil(),
// ... other properties ...
),

Related

Flutter Looking up a deactivated widget's ancestor on dialog pop

I'm having an issue trying to pop a dialog that contains a circle loader. I actually pop fine once my data is loaded, but in debug mode it's showing an exception that I can't figure out how to fix.
I have a stateful screen that on init I use the following code:
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showLoading();
});
The method showLoading is as follows:
void showLoading() {
//let's show the loading bar
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
dialogContext = context;
return AppLoader();
},
);
}
Where AppLoader simply returns:
class AppLoader extends StatelessWidget {
#override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
backgroundColor: Colors.transparent,
body: Center(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
SizedBox(
child: new CircularProgressIndicator(),
height: 80.0,
width: 80.0,
),
],
),
),
);
}
}
dialogContent is defined in the initial of the class as:
late BuildContext dialogcontext;
The main bulk of my code looks like this:
#override
Widget build(BuildContext context) {
return Container(
color: ColorConstant.gray100,
child: Scaffold(
backgroundColor: ColorConstant.gray100,
body: Stack(
children: <Widget>[
getMainListViewUI(),
SizedBox(
height: MediaQuery.of(context).padding.bottom,
)
],
),
),
);
}
Widget getMainListViewUI() {
return FutureBuilder<bool>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return ListView.builder(
itemCount: listViews.length,
scrollDirection: Axis.vertical,
itemBuilder: (BuildContext context, int index) {
return listViews[index];
},
);
},
);
}
Basically, the issue that I have is that when I finish getting the data from (getData()), I use:
Navigator.pop(dialogContext);
This works great: it removes the circle loader and I can see the screen behind it, no issues, no errors. However, if I run in debug mode, when I do a hotsync, it always shows me the error:
Looking up a deactivated widget's ancestor on dialog pop
I understand that this is because of the Navigator.pop that I am doing, but I don't get it. I've defined the dialogContext, which is what I am passing to the showDialog, and that's what I am popping. I've also tried setting a scheduled navigator, but again, same issue.
Any advice please?
You can check if widget is mounted or not. This problem is likely to occur when you pass a context and have async function. Can you add this before navigation and see if problem is solved
if(!mounted) return;
Problem solved. The issue is the my getData method was being called multiple times, the first time it pops the loading widget fine, after that it would throw the error which is correct since the loading widget no longer exists.

How do i open sliding up panel that will cover all of the parent widgets higher in the tree?

I'm not sure how to properly ask this question in a semi coherent manner, so I will give context to the situation.
This is kind of a social network app, so it has posts in it. You can report these posts by pressing a button on these posts, which opens a sliding up panel at the top of the tree. And I have a few pages that have post list in them, but they are in this said main page.
Instead of constantly passing a callback function which opens this sliding up panel on mainPage, i wanted to place this sliding up panel in the post item widget itself. I tried to implement this, and it does work, but not how I want it to. It only covers the listView that has these postItemWidgets, instead of the entire screen. It should have covered the entire screen, AppBar and bottomNavBar included, but it doesn't do that.
I'm pretty sure there is at least some way of doing this, but I genuinely have no idea how to properly formulate a question to google.
Thank you for any help.
Here is mainPage code:
#override
Widget build(BuildContext context) {
return VisibilityDetector(
key: Key("MainPage"),
onVisibilityChanged: (VisibilityInfo info) {
_isVisible = info.visibleFraction == 1.0;
},
child: GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: SafeArea(
top: false,
bottom: false,
child: SlidingUpPanel(
maxHeight: 600.h,
backdropEnabled: true,
color: Colors.transparent,
collapsed:
Container(decoration: BoxDecoration(color: Colors.grey)),
minHeight: 0,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(
UiConstants.reportDialogBorderRadiusTopLeftRight),
topRight: Radius.circular(
UiConstants.reportDialogBorderRadiusTopLeftRight),
),
panel: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: StreamBuilder(
stream: _bloc.openReportPageStream,
builder: (context, snapshot) {
if (snapshot.data is ReportPageModelState) {
ReportPageModelState state =
snapshot.data as ReportPageModelState;
return Center(
child: ReportMainPageWidgets(
_bloc, edit, removeSome, state.model),
);
} else {
return Center(
child: ReportMainPageWidgets(
_bloc, edit, removeSome, _bloc.postModelForEditing),
);
}
}),
),
controller: _panelController,
onPanelClosed: () {
_bloc.reportWidgetState = ReportWidgetState.loading;
_bloc.setPanelState();
},
onPanelOpened: () {
_bloc.setPanelState();
},
body: Scaffold(
extendBody: true,
body: Scaffold(
body: _openPageStreamWidget(),
bottomNavigationBar: _bottomAppBarWidget(),
),
bottomNavigationBar: _navBarStreamWidget(),
),
),
),
),
),
);
}
And this is the callback function that opens the sliding up panel:
void clickOnPostReport(int id) {
if (id != _bloc.id) {
_bloc.editPostModel(id).then((value) {
_bloc.postModelForEditing = value;
print("server request");
_bloc.reportWidgetState = value.author.id == MainBloc.user.id
? ReportWidgetState.editing
: ReportWidgetState.report;
_panelController.open();
});
} else {
_bloc.reportWidgetState =
_bloc.postModelForEditing.author.id == MainBloc.user.id
? ReportWidgetState.editing
: ReportWidgetState.report;
_panelController.open();
}
}

Set BoxFit Cover for Lottie animation

Hi I want my Flutter App to have a fullscreen StartUp Animation.
I use Lottie to display the animation, however I can't get the framing and fit right.
I want the Animation to fit the whole screen. In case the screen is too big to have the animation fullscreen, I want to "zoom in" and crop the Edges off (Default BoxFit.cover behaviour).
This is my current Code:
Widget build(BuildContext context) {
return BaseWidget<StartUpViewModel>(
model: StartUpViewModel(),
builder: (context, model, child) {
return Scaffold(
body: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Lottie.asset(
"assets/lottie/dummy.json",
fit: BoxFit.cover,
controller: animationController,
onLoaded: (comp) {
animationController
..duration = comp.duration
..forward();
},
),
));
},
);
}
}
However, if I use this code, the animation is not centered. It scales up the animation but alignes the left side of the animation with the screen, and crops away the right side by doing so.
After some trial and error I found out! I'll leave this here because it is very unintuitive and someone might need this in the future:
Widget build(BuildContext context) {
return BaseWidget<StartUpViewModel>(
model: StartUpViewModel(),
builder: (context, model, child) {
return Scaffold(
body: SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: Lottie.asset(
"assets/lottie/dummy.json",
fit: BoxFit.fill,
controller: animationController,
onLoaded: (comp) {
animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
print("AnimationComnpleze");
setState(() {});
}
});
animationController
..duration = comp.duration
..forward();
},
),
),
),
);
},
);
}
}

Flutter async method keeps running even after the corresponding widget is removed

I have a list of image paths and I am using the List.generate method to display images and a cross icon to remove image from list. Upload method is called on each image and when I remove the image from the list the method still keeps running until the image is uploaded. The behavior I am expecting is when I remove the image from the list the method should also stop running. I am using a future builder to display the circular progress bar and error icons while uploading an image.
What should I be doing to make sure the future method associated to the current widget also stops when I remove the widget from the list?
This is the code where I am creating a list
List.generate(
files.length,
(index) {
var image = files[index];
return Container(
height: itemSize,
width: itemSize,
child: Stack(
children: <Widget>[
Container(
getImagePreview(image, itemSize)
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
uploadHandler(image, field),
InkWell(
onTap: () => removeFileAtIndex(index, field),
child: Container(
margin: EdgeInsets.all(3),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(.7),
shape: BoxShape.circle,
),
alignment: Alignment.center,
height: 22,
width: 22,
child: Icon(Icons.close, size: 18, color: Colors.white),
),
),
],
),
],
),
);
},
)
This is Upload Handler method.
Widget uploadHandler(file, field) {
return FutureBuilder(
future: upload(file),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data.statusCode == 201) {
return doneUpload();
} else {
logger.d(snapshot.error);
return error();
}
} else {
return uploading();
}
},
);
}
The lifecycle of the widget isn't attached to the async functions invoked by the widget.
You can check the mounted variable to check if the widget still mounted from your async function.

Better way to load images from network flutter

I am trying to load images from network and show them in a GridView. I am using a StatefulWidget and loading the images inside the build method. But according to my understanding its not good to make a network call inside the build method. How can I download images from the network inside my BLoC file and later pass the list of downloaded images to the widget? Below is my current implementation.
class MovieList extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MovieListState();
}
}
class MovieListState extends State<MovieList> {
#override
void initState() {
super.initState();
bloc.fetchAllMovies();
}
#override
void dispose() {
bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Popular Movies'),
),
body: StreamBuilder(
stream: bloc.allMovies,
builder: (context, AsyncSnapshot<ItemModel> snapshot) {
if (snapshot.hasData) {
return buildList(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Center(child: CircularProgressIndicator());
},
),
);
}
Widget buildList(AsyncSnapshot<ItemModel> snapshot) {
return GridView.builder(
itemCount: snapshot.data.results.length,
gridDelegate:
new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (BuildContext context, int index) {
return GridTile(
child: InkResponse(
enableFeedback: true,
child: Image.network(
'https://image.tmdb.org/t/p/w185${snapshot.data
.results[index].poster_path}',
fit: BoxFit.cover,
),
onTap: () => openDetailPage(snapshot.data, index),
),
);
});
}
openDetailPage(ItemModel data, int index) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return MovieDetailBlocProvider(
child: MovieDetail(
title: data.results[index].title,
posterUrl: data.results[index].backdrop_path,
description: data.results[index].overview,
releaseDate: data.results[index].release_date,
voteAverage: data.results[index].vote_average.toString(),
movieId: data.results[index].id,
),
);
}),
);
}
}
You can use loadingBuilder which is inbuilt feature from flutter for Image.Network
Image.network(
widget.networkUrl,
fit: BoxFit.fill,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
),
I would recommend you to use
https://pub.dartlang.org/packages/cached_network_image
It's really works good for my cases.
Simple code example from their r
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => new CircularProgressIndicator(),
errorWidget: (context, url, error) => new Icon(Icons.error),
),
or
Image(image: CachedNetworkImageProvider(url))
You should add to the pubspec file
cached_network_image: <actual version here>
into the dependencies section
If you want a circular shape for your image. You can use Circle Avatar in such a way that it will act as loader and displayer both....
Parent circle avatar will be having loader and If we put transparent color to child circle avatar it will show loading until it is loaded...
Plus point with this way is you can simply give border also by setting background color of parent circle avatar and increasing it's radius slightly.
CircleAvatar(
backgroundColor: Colors.red,
radius: 65,
backgroundImage: AssetImage('assets/bottombar/loading.gif'),
child: CircleAvatar(
radius: 65,
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(_url),
),
),
You can also use FadeInImage
https://flutter.dev/docs/cookbook/images/fading-in-images
FadeInImage.assetNetwork(
placeholder: 'assets/loading.gif',
image: 'https://picsum.photos/250?image=9',
),
It is a good practice to handle errors related to, for example, lack of Internet, when trying to load an image from the Internet. ErrorBuilder is really good if you use Image.network()
Image.network(
'https://example.does.not.exist/image.jpg',
errorBuilder: (BuildContext context, Object exception, StackTrace stackTrace) {
// Appropriate logging or analytics, e.g.
// myAnalytics.recordError(
// 'An error occurred loading "https://example.does.not.exist/image.jpg"',
// exception,
// stackTrace,
// );
return Text('😢');
},
),
Best way to Handle Network Images
The best way to load image from the network in flutter is to use flutter's built-in network function and customize it according to your requirements, I do not recommend/prefer to use any package like CachedNetworkImage because it sometimes works on mobile phone and give unexpected error on the web.
You can manage the Network image like this:
Image.network(
netWorkImageURL,
fit: BoxFit.fill,
// When image is loading from the server it takes some time
// So we will show progress indicator while loading
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
// When dealing with networks it completes with two states,
// complete with a value or completed with an error,
// So handling errors is very important otherwise it will crash the app screen.
// I showed dummy images from assets when there is an error, you can show some texts or anything you want.
errorBuilder: (context, exception, stackTrace) {
return Image.asset(
AppAssets.dummyPostImg,
fit: BoxFit.cover,
height: (widget.hideBelowImage == null ||
widget.hideBelowImage == false)
? 170.h
: 130.h,
width: double.infinity,
);
},
),