Caching Image on flutter web, is it required? - flutter

Due to CachedNetworkImage not working on flutter web, upon porting, I have tried to use this, but my question is do we really need this? Or we just use Image. Network and the browser and service worker will handle the cache part (which is then set by server's response header through, for example, cache-control= "max-age=43200, public"
This is used on the food delivery project I am working on, https://www.santaiyamcha.com
Below are the classes I use to replace CachedNetworkImage which doesn't seem to work well.
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:http_extensions_cache/http_extensions_cache.dart';
import 'package:http_extensions/http_extensions.dart';
/// Builds a widget when the connectionState is none and waiting
typedef LoadingBuilder = Widget Function(BuildContext context);
/// Builds a if some error occurs
typedef ErrorBuilder = Widget Function(BuildContext context, Object error);
class MeetNetworkImage extends StatelessWidget {
/// Image url that you want to show in your app.
final String imageUrl;
/// When image data loading from the [imageUrl],
/// you can build specific widgets with [loadingBuilder]
final LoadingBuilder loadingBuilder;
/// When some error occurs,
/// you can build specific error widget with [errorBuilder]
final ErrorBuilder errorBuilder;
final double scale;
final double width;
final double height;
final Color color;
final FilterQuality filterQuality;
final BlendMode colorBlendMode;
final BoxFit fit;
final AlignmentGeometry alignment;
final ImageRepeat repeat;
final Rect centerSlice;
final bool matchTextDirection;
/// Whether to continue showing the old image (true), or briefly show nothing
/// (false), when the image provider changes.
final bool gaplessPlayback;
final String semanticLabel;
final bool excludeFromSemantics;
MeetNetworkImage({
#required this.imageUrl,
this.loadingBuilder = null,
this.errorBuilder = null,
this.scale = 1.0,
this.height,
this.width,
this.color = const Color(0xFDFFFF),
this.fit = BoxFit.fill,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.semanticLabel,
this.centerSlice,
this.colorBlendMode,
this.excludeFromSemantics = false,
this.filterQuality = FilterQuality.low,
this.matchTextDirection = false,
this.gaplessPlayback = false,
}) : assert(imageUrl != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null);
Future<Response> getUrlResponse() {
/*
//The caching part I tried, does not seems working
final client = ExtendedClient(
inner: Client(),
extensions: [
CacheExtension(
//logger: Logger("Cache"),
defaultOptions: CacheOptions(
expiry: const Duration(hours: 168),
// The duration after the cached result of the request will be expired.
//forceUpdate: false, // Forces to request a new value, even if an valid cache is available
//forceCache: false, // Forces to return the cached value if available (even if expired).
//ignoreCache: true, //Indicates whether the request should bypass all caching logic
//returnCacheOnError: true, //If [true], on error, if a value is available in the store if is returned as a successful response (even if expired).
keyBuilder: (request) => "${request.method}_${imageUrl.toString()}",
// Builds the unqie key used for indexing a request in cache.
store: MemoryCacheStore(),
// The store used for caching data.
shouldBeSaved: (response) =>
response.statusCode >= 200 && response.statusCode < 300,
),
)
],
);
return client.get(imageUrl);
*/
return get(imageUrl);
}
Widget getLoadingWidget(BuildContext context) {
if (loadingBuilder != null) {
return loadingBuilder(context);
} else
return Container(
height: height, width: width,
child: Center(
child: CircularProgressIndicator()
)
/*Image.asset(
'assets/img/loading4.gif',
height: height,
width: width,
fit: BoxFit.contain,
),*/
);
}
Widget getErrorWidget(BuildContext context, String error) {
if (errorBuilder != null) {
return errorBuilder(context, error);
} else
return Center(child: Icon(Icons.error));
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getUrlResponse(),
builder: (BuildContext context, AsyncSnapshot<Response> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return getLoadingWidget(context);
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.hasError)
return getErrorWidget(context, snapshot.error);
if (!snapshot.hasData)
return getErrorWidget(context, snapshot.error);
//return getLoadingWidget(context);
return Image.memory(
snapshot.data.bodyBytes,
scale: scale,
height: height,
width: width,
color: color,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
colorBlendMode: colorBlendMode,
excludeFromSemantics: excludeFromSemantics,
filterQuality: filterQuality,
gaplessPlayback: gaplessPlayback,
matchTextDirection: matchTextDirection,
semanticLabel: semanticLabel,
);
}
return Container();
},
);
}
}
What do you suggest?

I am using FadeInImage.memoryNetwork. It is working fine. The browser handles the cache part.

No. This is an issue with Google Chrome which prefers "cache-control" over E-Tags or Last Modified Headers. In my case I am using Firefox which caches on the basis of E-Tags. So here is the thing.
If you are using setState((){}) or the flutter engine calls the setState due to some reason, the images are rebuilt and if the images are not cached, it is again fetched. To prevent it, use the header cache-control: max-age=<any value greater than 0> and it will work fine in Chrome.
Or just build the app using web renderer canvaskit - flutter build web --web-renderer canvaskit.
I have come to this conclusion based on my experience and I could not find it anywhere, Hope it helps :)

Try to update your flutter version if you are using old one, and use from image providers interface.
And the browser will take care of the rest
Example:
Create a container add a decoration in the decoration add an image decoration
Then add from memory or network image.

Related

How do you update a value (derived from a remote source) that updates with state change, in a FutureBuilder?

Background
I have an image widget that loads an image remotely from the network.
Before/while the image is being retrieved I have a placeholder with a fixed height of 200.0 in its place.
Once the image is loaded, I update the Widget to display the image
The catch is I want to update the height of the container to the new height of the image. This is done in synchrony with changes to a scroll position so I can't just let the image container auto-adjust, I need the specific height of the newly downloaded image just before its displayed on screen.
My setup
I have a FutureBuilder as follows:
FutureBuilder(
future: getData(image:image, width:widget.width, height:widget.height),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If we got an error
if (snapshot.hasError) {
... placeholder
// if we got our data
} else if (snapshot.hasData) {
// Extracting data from snapshot object
final thisimage = snapshot.data as FutureImageTransporter;
final imageheight = thisimage.height
/// Do some stuff with image height here
return Container(
width: widget.width,
height:thisimage.height, ///This needs to have the height value retrieved when the image is downloaded
child: thisimage.imagewidget
);
}
else {
... placeholder
}
}
else
{
... placeholder
}
},
),
The future data retrieves data from getData, which is a sort of pass through to get the image from MyCustomImage which returns an image widget from the extended image package which is as follows:
Future<FutureImageTransporter> getData({image, width, height}) {
return Future.delayed(Duration(seconds: 6), () async {
var finalImageWidget = MyCustomImage(
input: image,
width:width,
height:height,
);
return finalImageWidget;
});
}
I'm using my own class called FutureImageTransporter to house the data, which consists of the actual image Widget and the height data (double).
class FutureImageTransporter{
late Widget imagewidget;
late double height;
}
The actual image widget is constructed as follows with MyCustomImage, and I also want to return back the height of the image, both pieces packaged in a FutureImageTransporter
FutureImageTransporter MyCustomImage ({
required input,
width,
height,
}) {
double _finalHeight = 0.0;
Widget finalImageWidget =
ExtendedImage.network(
input,
key: ValueKey(input),
width: width
height: height
enableMemoryCache: false,
handleLoadingProgress: true,
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading: return Container();
case LoadState.completed:
_finalHeight = state.extendedImageInfo?.image.height.toDouble() ?? 0.0;
debugPrint(_finalHeight.toString()); /// This is properly outputting the final height of the image
return ExtendedRawImage(
image: state.extendedImageInfo?.image,
fit: BoxFit.cover,
width: width
height: height
);
case LoadState.failed: return Container();
}
},
);
FutureImageTransporter tempvar = FutureImageTransporter();
tempvar.imagewidget = finalImageWidget;
tempvar.height = _finalHeight; ///This is not updating to the _finalHeight which updates above
return tempvar;
}
The Problem/Question
With the above setup, I'm using a FutureBuilder to successfully retrieve an image and then update the widget in my widget tree with the new image widget which is an ExtendedImage.network widget, which is something analogous to Image.Network().
Within ExtendedImage.network, the height of the image is successfully calculated _finalHeight = state.extendedImageInfo?.image.height.toDouble() ?? 0.0;.
However, which the widget does properly debugPrint the value of the image height, it never actually properly returns the value to my FutureBuilder. The FutureBuilder only sees the originally initialized value of 0.0 double _finalHeight = 0.0; (or whatever default value I stick in there).
This problem seems to have to do with how Im retrieving data from a future and setting it up to pass between the widgets, but I can't understand where I went wrong, or if this is simply impossible? This ends up being a TON of code, but what I originally thought would be a simple task. Any ideas?

How to uninitialize a variable In Flutter / Dart?

I define a variable ui.Image? previewImage; (ui refers to dart:ui).
I then start an image stream from the camera that performs YOLOX classifications and pairs them with an image, and updates the variable previewImage with the classification results with a function onDetected():
onDetected(
List<YoloxResults> results,
ui.Image image,
) {
setState(() {
yoloxResults = results;
previewImage = image;
});
}
The yoloxResults and previewImage are then displayed with a widget:
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (previewImage == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
final cameraWidget = FittedBox(
child: SizedBox(
width: previewImage!.width.toDouble(),
height: previewImage!.height.toDouble(),
child: CustomPaint(
painter: YoloxResultsPainter(
image: previewImage!,
results: results,
labels: labels,
),
),
),
);
return cameraWidget;
},
);
}
I have a page where I switch between the above widget and another body widget via a Ternary Operator...
The first time I load the cameraWidget, previewImage is null and displays the CircularProgressIndicator() before the previewImage is set via the onDetected() function (which is what I want).
The second time I open the cameraWidget previewImage is already defined and an old image is displayed before the onDetected() function updates... I have tried setState previewImage = null, which calls the CircleProgressIndicator() but then displays an old previewImage first, and before updating...
My Question: How can I reset the previewImage variable to being uninitialized?
How can I remove the data assigned to previewImage?

Flutter: Image.memory() fails with bytes != null': is not true -- when there IS data

I'm trying to display an image based on (base64) data coming from a backend, but I keep getting the error bytes != null': is not true.
Here's my code:
class _FuncState extends State<Func> {
Uint8List userIconData;
void initState() {
super.initState();
updateUI();
}
void updateUI() async {
await getUserIconData(1, 2, 3).then((value) => userIconData = value);
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
Container(
child: CircleAvatar(
backgroundImage: Image.memory(userIconData).image, // <--- problem here
maxRadius: 20,
),
),
),
);
}
}
Helper code:
Future<Uint8List> getUserIconData(
role,
id,
session,
) async {
var url = Uri.https(kMobileAppAPIURL, kMobileAppAPIFolder);
var response = await http.post(url, body: {
'method': 'getUserProfilePic',
'a': id.toString(),
'b': role.toString(),
'c': session.toString(),
});
if (response.statusCode == 200) {
Map data = jsonDecode(response.body);
return base64Decode(data['img']);
}
return null;
}
I have stepped through the code with a debugger and I have confirmed the helper function is returning the correct series of bytes for the image.
I'd appreciate any pointers.
Further note. The error also says:
Either the assertion indicates an error in the framework itself, or we
should provide substantially more information in this error message to
help you determine and fix the underlying cause. In either case,
please report this assertion by filing a bug on GitHub
This is quite simple; if you take a look at your code you should be able to follow through this sequence of operations.
The widget is created. No action. At this point userIconData is null.
initState is called. async http call is initiated. userIconData == null
build is called. build occurs, throws error. userIconData == null
http call returns. userIconData is set. userIconData == your image
Due to not calling setState, your build function won't run again. If you did, this would happen (but you'd still have had the exception earlier).
build is called. userIconData is set. userIconData == your image
The key here is understanding that asynchronous calls (anything that returns a future and optionally uses async and await) do not return immediately, but rather at some later point, and that you can't rely on them having set what you need in the meantime. If you had previously tried doing this with an image loaded from disk and it worked, that's only because flutter does some tricks that are only possible because loading from disk is synchronous.
Here are two options for how you can write your code instead.
class _FuncState extends State<Func> {
Uint8List? userIconData;
// if you're using any data from the `func` widget, use this instead
// of initState in case the widget changes.
// You could also check the old vs new and if there has been no change
// that would need a reload, not do the reload.
#override
void didUpdateWidget(Func oldWidget) {
super.didUpdateWidget(oldWidget);
updateUI();
}
void updateUI() async {
await getUserIconData(widget.role, widget.id, widget.session).then((value){
// this ensures that a rebuild happens
setState(() => userIconData = value);
});
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Container(
// this only uses your circle avatar if the image is loaded, otherwise
// show a loading indicator.
child: userIconData != null ? CircleAvatar(
backgroundImage: Image.memory(userIconData!).image,
maxRadius: 20,
) : CircularProgressIndicator(),
),
),
);
}
}
Another way to do the same thing is to use a FutureBuilder.
class _FuncState extends State<Func> {
// using late isn't entirely safe, but we can trust
// flutter to always call didUpdateWidget before
// build so this will work.
late Future<Uint8List> userIconDataFuture;
#override
void didUpdateWidget(Func oldWidget) {
super.didUpdateWidget(oldWidget);
userIconDataFuture =
getUserIconData(widget.role, widget.id, widget.session);
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Container(
child: FutureBuilder(
future: userIconDataFuture,
builder: (BuildContext context, AsyncSnapshot<Uint8List> snapshot) {
if (snapshot.hasData) {
return CircleAvatar(
backgroundImage: Image.memory(snapshot.data!).image,
maxRadius: 20);
} else {
return CircularProgressIndicator();
}
},
),
),
),
);
}
}
Note that the loading indicator is just one option; I'd actually recommend having a hard-coded default for your avatar (i.e. a grey 'user' image) that gets switched out when the image is loaded.
Note that I've used null-safe code here as that will make this answer have better longevity, but to switch back to non-null-safe code you can just remove the extraneous ?, ! and late in the code.
The error message is pretty clear to me. userIconData is null when you pass it to the Image.memory constructor.
Either use FutureBuilder or a condition to check if userIconData is null before rendering image, and manually show a loading indicator if it is, or something along these lines. Also you'd need to actually set the state to trigger a re-render. I'd go with the former, though.

Edit flutter BasicTimeField

I added in my code this class:
class BasicTimeField extends StatelessWidget {
final format = asd.DateFormat("HH:mm");
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Text('Basic time field (${format.pattern})'),
DateTimeField(
format: format,
onShowPicker: (context, currentValue) async {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(currentValue ?? DateTime.now()),
);
return DateTimeField.convert(time);
},
),
]);
}}
And it is taken from this website:
https://pub.dev/packages/datetime_picker_formfield
I want to change the heights and border-radius of the time popup but I don't know from where I can do this. Where can I customize all the details of the widget?
You can change the dart_picker_dialog.dart file of the package. But it is not recommended as they mentioned in that comment line.
// Constrain the textScaleFactor to the largest supported value to prevent
// layout issues.
final double textScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 1.3);
Later, the textScaleFactor will be used to calculate the dialogSize.
final Size dialogSize = _dialogSize(context) * textScaleFactor;
For more, you can look here.

Flutter ShaderMask using an ImageShader

I'm trying to get a ShaderMask that uses an Image as a Shader. I'm reading the image from memory, so my image is a File. How do I create an ImageShader using an image from memory?
File imageFile;
Image image = Image.file(imageFile)
ShaderMask(
shaderCallback: (bounds) {
Float64List matrix4 = new Matrix4.identity().storage; // <--- DO I NEED THIS OR THE BOUNDS?
return ImageShader(image, TileMode.mirror, TileMode.mirror, matrix4);
},
child: child
)
There's an error with ImageShader as image is the wrong type (I need ui.Image, which I don't understand how to create).
How do I create the ImageShader from a File image?
PS: is matrix4 correct or should I use the bounds somehow?
The important thing to know is that ImageShader uses an Image from the dart:ui package and not an instance of the Image widget. There is no direct operation or constructor available to create an instance of ui.Image from a network location, a file or asset, so you need some code to get it done.
The best generic solution i came up with after looking up many resources and digging into the code how the Image widget is loading raw images, is to use an ImageProvider as source. The abstract ImageProvider has implementations like NetworkImage, FileImage, ExactAssetImage and MemoryImage to load your image from any resource you want.
First you get an ImageStream using the ImageProvider.resolve method. The resolve method takes an argument of type ImageConfiguration, that should be filled with as many information as you have available at the code location. You can use the global createLocalImageConfiguration function in most cases, but be aware that this will not work when you create the shader in the initState method of a StatefulWidget.
On the resolved ImageStream you can attach an ImageStreamListener, that takes an ImageListener callback as first parameter. Once the image is loaded, the callback will be called with an ImageInfo, which provides the requested image on the image property.
You can construct the ImageShader with both tile modes as TileMode.clamp and a simple identity matrix, which you can either create by hand or take that one offered by the Matrix4 class. If you need the image shader to be smaller than the size of the provided image, you can wrap your provider in an ResizeProvider and specify the desired width and height.
Below my implementation of an ImageMask widget as a reference, which can be used to mask widgets of any kind.
class ImageMask extends StatefulWidget {
final ImageProvider image;
final double width;
final double height;
final Widget child;
const ImageMask({#required this.image, this.width, this.height, #required this.child});
#override
_ImageMaskState createState() => _ImageMaskState();
}
class _ImageMaskState extends State<ImageMask> {
Future<Shader> _shader;
#override
void initState() {
super.initState();
_shader = _loadShader(context);
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _shader,
builder: (_, AsyncSnapshot<Shader> snapshot) {
return snapshot.connectionState != ConnectionState.done || snapshot.hasError
? SizedBox(width: widget.width, height: widget.height)
: ShaderMask(
blendMode: BlendMode.dstATop,
shaderCallback: (bounds) => snapshot.data,
child: widget.child,
);
},
);
}
Future<Shader> _loadShader(BuildContext context) async {
final completer = Completer<ImageInfo>();
// use the ResizeImage provider to resolve the image in the required size
ResizeImage(widget.image, width: widget.width.toInt(), height: widget.height.toInt())
.resolve(ImageConfiguration(size: Size(widget.width, widget.height)))
.addListener(ImageStreamListener((info, _) => completer.complete(info)));
final info = await completer.future;
return ImageShader(
info.image,
TileMode.clamp,
TileMode.clamp,
Float64List.fromList(Matrix4.identity().storage),
);
}
}