Flutter. High GPU load with list of high resolution images - performance

I've started to learn Flutter Framework and high GPU load when I'm trying to render a list of images which come from the network and have high resolution.
I've created test project on github for demo https://github.com/troublediehard/flutter_imageList_high_gpu_load
Am I doing anything wrong? Is there way to optimise images before render?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Image loading test'),
),
body: buildLists(),
),
);
}
Widget buildLists() {
final imageUrls1 = [
'https://via.placeholder.com/2000',
'https://via.placeholder.com/2001',
'https://via.placeholder.com/2002',
];
final imageUrls2 = [
'https://via.placeholder.com/2003',
'https://via.placeholder.com/2004',
'https://via.placeholder.com/2005',
];
return Column(
children: <Widget>[
buildList(imageUrls1),
buildList(imageUrls2),
],
);
}
buildList(List<String> urls) {
return Container(
height: 150,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: urls.length,
itemBuilder: (context, index) {
return Image.network(
urls[index],
height: 130,
width: 130,
);
}),
);
}
}

There is an open issue in Flutter that characterizes this problem (see this github issue, in particular the links to three issues created from it) as of mid-January 2019. Hopefully it will be resolved sooner rather than later, as more people have been running into it, but I get the impression there's quite a lot of internal discussion around how to properly handle high-res images as it needs to be a flexible and robust solution that works for many use-cases. I believe the high GPU load you're seeing is probably the result of loading up the entire set of images into the GPUs texture buffer, although as a comment states you should expect much difference performance on a real device and in release mode.
The current advice from the flutter devs is to use lower resolution images (i.e. set up your server to serve thumbnails). While this is a slightly annoying response, it is actually something you should think about doing anyways as when you download high-res photos you're 1) using bandwidth, 2) using server memory/processing power, and 3) using additional phone memory (even if images are resized before showing this would still be true). Serving thumbnails should result in a better user experience, even if it means that the both the low-res and high res photos are downloaded separately (when the user taps on the thumbnail for example).

Related

Can I run "image.dart" functions in background

I want to display a loading widget while compressing the image selected.
I am using the "image.dart" package to select an image, using ImagePicker.GetImage().
Then I want to display a loading widget while the image is being compressed, and when it's finished display the compressed image.
My code fundamentally works, my loading widget is cool (thanks spinkit), I can select any image, compress it and display the new one, but there is a small freeze (2-3s) while the image is being compressed.
So I'm trying to put a loading widget to not make the user panic and tap 10 000 times to select the image wanted, but I'm not completely comfortable with asynchronous code yet.
Here's my code :
import 'package:image/image.dart' as Img;
#override
Widget build(BuildContext context) {
if (loading == true) {
return LoadingScreen();
} else {
return Scaffold(
backgroundColor: Colors.brown[300],
body: Column(
children: [
SizedBox(
height: 50,
),
RaisedButton(
child: Text('Select an image'),
onPressed: () async {
await getImage();
// You can see here that I'm trying to trigger the loading widget
setState(() {
loading = true;
});
compressImage();
// And I want to disable the loading widget here, after the compression
setState(() {
loading = false;
});
},
),
SizedBox(
height: 10,
),
pickedImage != null ? printSelectedImage() : Center(child: Text('Please select an image')),
],
),
);
}
}
getImage() async {
pickedFile = await picker.getImage(source: ImageSource.gallery);
}
compressImage() async {
Img.Image selectedImage = Img.decodeJpg(File(pickedFile.path).readAsBytesSync());
if (selectedImage.width > selectedImage.height) {
Img.Image compressedImage = Img.copyResize(selectedImage, width: 500);
File(pickedFile.path).writeAsBytesSync(Img.encodePng(compressedImage));
} else {
Img.Image compressedImage = Img.copyResize(selectedImage, height: 500);
File(pickedFile.path).writeAsBytesSync(Img.encodePng(compressedImage));
}
if (pickedFile.path != null) {
setState(() {
pickedImage = pickedFile.path;
});
}
}
But the result is the same, the screen is still stuck in the file selector while compressing, and then directly display the compressed image.
I started learning dart/Flutter 2 weeks ago, so am I missing some fundamental principles of the dart language ? Am I not seeing something obvious ?
Thanks for reading
Making something async doesn't move it into some magical background thread. Dart uses isolates which are basically an execution context in which dart code can run. Flutter has a single isolate which runs your app, if you do too much work in it, your app gets slow or skips frames.
You can create other isolates to do calculations and offload work and communicate between isolates by passing messages with primitive content back and forth. The drawback in Flutter is, that other isolates don't have access to Flutter plugins thus a lot of things don't work there.
There are ways to work around this but they are pretty involved.
The easiest way to start an isolate is the compute(callback, message) function.
Spawn an isolate, run callback on that isolate, passing it message, and
(eventually) return the value returned by callback.
I am not sure if the image/image.dart library uses a plugin or some other features that are not available (dart:ui for example), you can try offloading your image compression into the callback. If it fails with a MissingPluginException or something similar, then it won't work.
Remember, you can only pass primitive messages so you can not path the pickedFile, only the pass as argument.
For your problem however there may be an easier solution. The image picker accepts parameters for max width/height and quality. If you need the image to be encoded as PNG however then you need to create a complex solution yourself.

How can I memoize a gradient mask in Flutter?

I made an Android app long time ago where the main focus were a bunch of differently colored icon buttons representing psychological emotions. I realized that a few people I had it tested with were slightly confused about the icons, as they unconsciously associated different colors to these emotions than the ones I had chosen.
So, now I'm rewriting the app with Flutter, and I've decided that these icon buttons will have gentle gradients instead of solid colors, as it seems that specific color relationships, rather than colors, are more consistently associated with the emotions, plus it encourages association by learning instead of usage of innate ideas, since it makes the appearance of each icon harder to completely describe but easier to distinguish from the others.
Anyway, I'm using a ShaderMask with a LinearGradient, and an IconButton as child. Obviously it eats up GPU and rendering time is slightly above 16 ms. I thought of memoizing the rendered icon with the gradient, by pre-rendering it to an image then using it in the buttons. I've seen a good increase in performance, but the resulting code felt so hackish and buggy that I just reverted everything.
Is there a way to do this without feeling like I'm fighting the framework? Like, an official way to pre-render widgets, or at least to tell Flutter when something can be re-used without changes (performance is terrible even when updating a widget that doesn't have anything to do with these buttons).
Edit: memoizing these gradient icons would also allow me to use them elsewhere, increasing overall consistency whenever these emotions must be represented. Doing this right now would simply nuke performance.
Edit: code:
class MyButton extends StatelessWidget {
MyButton({this.index, this.constraints});
final int index;
final BoxConstraints constraints;
#override
Widget build(BuildContext context) {
return GradientMask(
child: IconButton(
onPressed: () {},
iconSize: constraints.maxWidth,
padding: EdgeInsets.all(0.0),
color: Colors.white,
icon: iconList[index],
),
index: index,
);
}
}
class GradientMask extends StatelessWidget {
GradientMask({this.index, this.child});
final int index;
final Widget child;
#override
Widget build(BuildContext context) {
return ShaderMask(
shaderCallback: (Rect bounds) => LinearGradient(
begin: Alignment.bottomLeft,
end: Alignment.topRight,
colors: gradientList[index],
tileMode: TileMode.repeated,
).createShader(Rect.fromLTWH(0.0, 0.0, 100.0, 100.0)),
child: child,
);
}
}

Sync app on simulator from VS Code is very slow

I'm writing an App (1st time) with Flutter on MacOS from VS Code.
All works but when I run the app it is very slow with the synchronization, I mean that if I change something in the home screen, save then click "run on iOS device" it loads the old version, ignoring my edits but after a random time it loads the right version with the correct edits.
I'm working on Catalina 10.15.4 (macbook pro 13 inch early 2015, Xcode updated to the latest version).
This is the code wrote in the home screen, other files are unchanged from the original template.
(I added 3 Images to the LaunchScreen and an other for the Android splash screen).
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('BuddyLang'),
centerTitle: true,
backgroundColor: Colors.orange,
),
body: Center(
child: FlatButton(
onPressed: (){},
child: Text('click me'),
),
),
);
}
}
Try Flutter clean and then Restart your app and also check this happen is Flutter run --release

Flutter CircleAvatar - Using Image object as (background) image / typecasting it to ImageProvider

I am trying to show a CircleAvatar at the top of the main screen of my App, but I am running into a rather silly issue. I am unable to figure out how to provide an object of the class Image (that is a property of a user) as an ImageProvider, which is what CircleAvatar expects. Is there an easy way to "typecast" here or some other way people have resolved this?
class _HomePageState extends State<PictureShowcaseHome>{
Appuser appuser = new Appuser();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: InkWell(
child: CircleAvatar(
backgroundImage: appuser.img,
child: Text(appuser.name.substring(0,1).toUpperCase()),
) ,
),
...
To simplify things, assume an Appuser has just the following properties:
class Appuser{
String name;
Image img;
Appuser(){
this.name = "Unknown user";
this.img = Image.asset('assets/imgs/default1.jpg');
}
}
My idea is that I have a default image that is included in the assets, but the user can replace it with their own image (seems like a rather standard idea). I considered switching to AssetImage img; and img = new AssetImage('assets/imgs/default1.jpg'); in the constructor for Appuser, but that sort of seems backwards. It also gets me into the "opposite" problem, when I display the user image on a user information details page (I thought of just having a ListTile( title: appuser.img,),) or when I let the user take a new user picture with the camera. For those purposes, it is easiest to directly work with an object of class Image, not an AssetImage.
Or am I somehow looking at this the wrong way?
This
Image.asset('assets/imgs/default1.jpg').image
should work.
For the sake of clarity, the Image class has an ImageProvider property. Just get the ImageProvider using .image with any given Image class. This works for Image.asset, .file, .network, and .memory
.image will give you the ImageProvider that is needed.
CircleAvatar(
backgroundColor: Colors.red,
radius: radius,
child: CircleAvatar(
radius: radius - 5,
backgroundImage: Image.file(
profileImage,
fit: BoxFit.cover,
).image,
),
)

Performant blur/opacity in Flutter?

I love the blurry frost effect using a BackdropFilter (see this).
However, because the BackdropFilter has Opacity and because the widget I'm blurring also has Opacity, the performance is horrendous. This is also because I redraw my widgets a few times a second, but that shouldn't be an issue given Flutter can go 60fps?
I turned on checkerboardOffscreenLayers and see checkerboards for miles. :O
The checkerboards happen due to blurScreen, not due to widgetToBlur but widgetToBlur does slow down performance probably because (in my real code, not this example) it's calling setState() multiple times a second.
Is there a more performant way to make blurs/opacities? The link above says to apply opacity to widgets individually. I can't do that with the blur though (blurScreen below), because the BackdropFilter has to be stacked on top of my widget-that-does-the-redrawing.
I removed the blur effect and my performance is way better (no checkerboards, app doesn't crash).
build() code in question:
final widgetToBlur = Container(
child: Opacity(
opacity: 0.3,
// In my actual code, this is a Stateful widget.
child: Text('Spooky blurry semi-transparent text!'),
),
);
final blurScreen = BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
child: Container(
decoration: BoxDecoration(
color: _backgroundColor.withOpacity(0.3),
),
),
);
return Container(
child: Stack(
children: <Widget>[
widgetToBlur,
blurScreen,
Text('This is in front of the blurred background'),
],
),
);
I ended up drawing the widgetToBlur once, blurred, with opacity, using Paint and Canvas.
This means it only runs the blur and opacity operations once, in initState(), and never has to be re-rendered-with-blur throughout the lifecycle of the widget.
If anyone else ends up stuck with this, you can leave a comment and I can help out more.
I have a similar problem and it also boils down to using canvas and paint.
The only problem now is, if I apply a MaskFilter to the image, not much happens despite the enormously high sigma... Only the edge is a little blurred.
Now the question is why is that so? How did you solved this issue?
canvas.drawImageRect(
image,
Offset(0, 0) & srcSize,
Offset(-delta, 0) & dstSize,
Paint()..maskFilter = MaskFilter.blur(
BlurStyle.normal, 100.0
)
);
For those of you who are interested, I have loaded the image in the init function as follows:
rootBundle.load("assets/gift_1.png").then((bd) {
Uint8List lst = new Uint8List.view(bd.buffer);
Ui.instantiateImageCodec(lst).then((codec) {
codec.getNextFrame().then((frameInfo) {
image = frameInfo.image;
});
});
});
P.s. Unfortunately, I can't write any comments yet; therefore here as a contribution, including solution proposal
you can use this. but I do not know speed.
var pr = ui.PictureRecorder();
var canvas = ui.Canvas(pr);
canvas.drawImage(
img,
ui.Offset(0, 0),
ui.Paint()..maskFilter = ui.MaskFilter.blur(
BlurStyle.normal, 100.0
)
);
var pic = pr.endRecording();
return pic.toImage(img.width, img.height);

Resources