Better way to load images from network flutter - 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,
);
},
),

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.

What widget should I use?

This is what I am doing now.
Main Page:
I would like to make it same like this picture.Example:
I have tried couple ways and widget to build it but couldn't figure it out. Also, I want to retrieve the data from the Firebase and show them as the content.
Code 1: https://pastebin.com/A0nK1riQ
Code 2: https://pastebin.com/i1T7gBNy
Widget build(BuildContext context) {
return Container(
child: StreamBuilder(
stream: _products.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
if (streamSnapshot.hasData) {
return ListView.builder(
itemCount: streamSnapshot.data!.docs.length,
itemBuilder: (context, index) {
final DocumentSnapshot documentSnapshot =
streamSnapshot.data!.docs[index];
return Container(
margin: const EdgeInsets.all(10),
child: ListTile(
title: Text(documentSnapshot['name']),
subtitle: Text(documentSnapshot['price'].toString()),
trailing: SizedBox(
width: 100,
),
),
);
});
}
return SizedBox.shrink();
}),
);
}
You may use GridView.builder
https://api.flutter.dev/flutter/widgets/GridView-class.html
and in gridview build use column
According to me, first you have to check which NavigationRail icon clicked then put the condition on it's GestureDetector like
// global variable
String itemClickedValue = "";
then set the value in it according to user click
itemClickedValue = "first";
then check the condition while fetching data like
if(itemClickedValue.compareTo("first") == 0){
// pass that documentId or api and then show in list
}

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

What is the best way to display a list of network images while having them fullscreen and swipeable to switch images?

I would like to be able to display a list of NetworkImages that I get in full screen, and to be able to swipe between them. I can't seem to find a decent way to do so though. I currently have a GridView.builder to display the images, but I don't like the way it looks. Hence why I am trying to make it a fullscreen view of the images where you can swipe between images, if there are multiple.
This is what I am currently using, the gridview. I don't like it though, but it was decent for displaying the images. To actually display the images I believe I will always need NetworkImage, since I need to send over some Authorization headers to actually view the image.
class _ImageViewState extends State<ImageView> {
#override
Widget build(BuildContext context) {
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
return Container(
height: height,
width: width,
child: Scaffold(
body: FutureBuilder<dynamic>(
future: ImageDBProvider.imageDB.getUserimageData(widget.recordType, widget.recordData.id),
initialData: List(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('none');
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.active:
return Text('');
case ConnectionState.done:
if (snapshot.hasError) {
print(
'${snapshot.error}',
);
}
}
var userImageData = snapshot.data;
return GridView.builder(
itemCount: userImageData['images'].length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2),
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.all(5),
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(
userImageData['images'][index].url, headers: <String, String>{
'Authorization': '${userImageData['user_data']['token']}',
},),
fit: BoxFit.cover))));
});
}
),
),
);
}
}
You could use PageView
Official documentation here

Flutter FutureBuilder rebuilds unnecessarily when navigate between screens

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 ...
),