Reputation: 488
I'm trying to load images from network and than passing it to ExtendedRawImage as finished future Image
to crop it.
The problem is that every new image that comes in uses (I think) the same instance of future to check if image has been loaded and that's why it shows CircularProgressIndicator
on every new image.
The things I have tried and didn't solve my problem:
StatefulWidget
widget.The code and gif I'm working with right now:
AnimatedList(
key: _sListKey,
physics: BouncingScrollPhysics(),
controller: _sScrollCtrl,
padding: EdgeInsets.all(5),
initialItemCount: sData.length,
itemBuilder: (context, index, animation) {
final vehicle = sData[index];
final color = defaultRegions.contains(vehicle.region)
? Colors.grey
: Colors.blue;
return _dataContainer(
vehicle, animation);
},
)
class CropImage extends StatefulWidget {
final Vehicle vehicle;
CropImage({Key key, this.vehicle}) : super(key: key);
@override
_CropImageState createState() => _CropImageState();
}
class _CropImageState extends State<CropImage> {
Future<ImageInfo> _imageInfo;
@override
void initState() {
super.initState();
NetworkImage image = NetworkImage(widget.vehicle.image);
Completer<ImageInfo> completer = Completer();
image
.resolve(new ImageConfiguration())
.addListener(ImageStreamListener((ImageInfo info, bool _) {
completer.complete(info);
}));
_imageInfo = completer.future;
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: widget.vehicle.vehicleRegion.width /
widget.vehicle.vehicleRegion.height,
child: FutureBuilder(
// key: UniqueKey(),
future: _imageInfo,
builder: (context, AsyncSnapshot<ImageInfo> snapshot) {
if (snapshot.hasData) {
return ExtendedRawImage(
image: snapshot.data.image,
fit: BoxFit.cover,
soucreRect: Rect.fromLTWH(
widget.vehicle.vehicleRegion.x,
widget.vehicle.vehicleRegion.y,
widget.vehicle.vehicleRegion.width,
widget.vehicle.vehicleRegion.height,
),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
UPDATE_1: _dataContainer()
contains CropImage
widget that I have wrapped with regular Container. Also as a side note, I'm using websocket to pass data to the AnimatedList
by inserting it in the top of the list using _sListKey.currentState.insertItem(0)
.
UPDATE_2 I think I have narrowed down a problem. If I use any kind of FutureBuilder
inside AnimatedList
(as showed below), it creates the same problem (adding a 'Key' or moving to an other Statefull
widget doesn't solve the problem).
FutureBuilder(
future: http.get(vehicle.image),
builder: (context, AsyncSnapshot<http.Response> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Image.memory(snapshot.data.bodyBytes);
} else {
return CircularProgressIndicator();
}
},
),
UPDATE_3: It seams that the core problem comes from setState()
call. Because I'm using weboskcet
to pass data to view, every time new data comes in, setState()
is being called to notify the AnimatedList
or Listview
to reload the view, which forces the FutureBuilder
to refresh. But knowing this doesn't solve my problem.
@override
void initState() {
super.initState();
ws.dataCallback = (vehicle, status) {
switch (status) {
case 0:
setState(() {});
break;
}
};
}
NOTE: As a request, created sample repository of my problem (link to repo).
Upvotes: 2
Views: 3610
Reputation: 2224
See the documentation of FutureBuilder: https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
You can solve this problem by wrapping the FutureBuilder in your own StatefulWidget and calling fetchImage in e.g. initState and assigning it to a field.
EDIT: If all images still reload even though the loading is done inside the initState method, this must mean the State objects are being reinitialized. Given that you mention you add the item to the beginning, your problem might be related to the following issue https://github.com/flutter/flutter/issues/21023#issuecomment-510950338. It's mentioned that ListView.builder does not support changes in the list as it uses the index for tracking. Although the issue is about ListView.builder, the AnimatedList doc mentions:
This widget is similar to one created by [ListView.builder].
If your only requirement is to insert items to the top, you may consider adding items to the end and using a reverse shrinkWrap AnimatedList. A minimal example:
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Example(),
);
}
}
class Example extends StatefulWidget {
@override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
List<Vehicle> vehicles = new List();
GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
Random rng = new Random();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: AnimatedList(
shrinkWrap: true,
reverse: true,
key: listKey,
initialItemCount: vehicles.length,
itemBuilder: (context, index, animation) => SizeTransition(
axis: Axis.vertical,
sizeFactor: animation,
child: ListTile(
title: WidgetWithFutureBuilder('${vehicles[index].name}'),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
int id = rng.nextInt(5000);
vehicles.add(new Vehicle(name: "Vehicle $id"));
listKey.currentState.insertItem(vehicles.length - 1);
});
},
),
);
}
}
class WidgetWithFutureBuilder extends StatefulWidget {
final String name;
WidgetWithFutureBuilder(this.name);
@override
_WidgetWithFutureBuilderState createState() =>
_WidgetWithFutureBuilderState();
}
class _WidgetWithFutureBuilderState extends State<WidgetWithFutureBuilder> {
Future<String> future;
@override
void initState() {
super.initState();
this.future = Future.delayed(Duration(seconds: 1), () => widget.name);
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, state) {
if (state.hasData) {
return ListTile(
title: Text(state.data),
);
}
return Center(child: CircularProgressIndicator());
},
);
}
}
class Vehicle {
Vehicle({this.name});
String name;
}
Upvotes: 4
Reputation: 14205
Okay, I don't know how to create unique instance of the Future but I probably know how to solve your problem.
You can use FadeInImage to sort this out. FadeInImage puts a place-holder image and it replaces with the actual network image once it is loaded. The place-holder image can be gif loading indicator.
Widget _imageView(BuildContext context, String imageUrl) {
return FadeInImage.assetNetwork(
placeholder:'assets/giphy.gif' ,
image: imageUrl,
);
}
More information is available here and here.
Upvotes: 4