5

I want to get attribute values, such as width and height, from NetworkImage or Image.network before displaying the image.

I found the following good posts, but it doesn't work. It got the size values, but the image is not loaded in FutureBuilder.

My code is as below;

import 'dart:async';

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: FutureBuilder(
          future: _getImage(),
          builder: (BuildContext context, AsyncSnapshot<Image> snapshot) {
            print(snapshot.hasData);
            if (snapshot.hasData) {
              return snapshot.data;
            } else {
              return Text('Loading...');
            }
          },
        ),
      ),
    );
  }

  Future<Image> _getImage() async {
    final Completer completer = Completer();
    final String url = 'http://images-jp.amazon.com/images/P/4101098018.09.MZZZZZZZ';
    final image = NetworkImage(url);

    image.resolve(ImageConfiguration())
          .addListener(ImageStreamListener((ImageInfo info, bool isSync) {
      print(info.image.width);
      completer.complete(info.image);
    }));

    return completer.future;
  }
}

The result is; - The screen only shows "Loading..." and the image is not loaded. - print output is as below. This should means, FutureBuilder is called twice before loading the image, and we can get the width but FutureBuilder is not called after that.

false
false
112

Environment:

  • Flutter 1.13.0 • channel dev (due to flutter web)
  • Chrome Version 79.0.3945.79
Hirotaka Nishimiya
  • 351
  • 1
  • 3
  • 10
  • Download the image yourself using your HTTP package of choice, extract the size information, then display it using `Image.memory`. – Abion47 Dec 12 '19 at 01:56
  • Thanks for your advice. I'll investigate that way. In parallel, I still would like to have answers why the above is not working. – Hirotaka Nishimiya Dec 12 '19 at 01:59

3 Answers3

1

Couple of observations based on the reference post you provided.

  1. You have mixed up ui.Image with Image widget.
  2. If you separate the info logic from widget building then you will not have access to the Image widget meaning you will have to recreate it. Instead you can create a widget and return it.
  3. In your http based answer response.body.length might not exactly represent the image dimension. You have to see if the response headers has any information about the image.
  4. Also note FutureBuilder's build method will be called more than once with different ConnectionState depending on the state of the future like waiting or done. Check here

Option 1: If you don't care about the Image widget then your current code can work with some slight modification. This is exactly identical to the original post but modified to match to the way you defined it in your post.

import 'dart:async';

import 'package:flutter/material.dart';
import 'dart:ui' as ui;

class ImageSizeTestWidget extends StatefulWidget {
  ImageSizeTestWidget({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _ImageSizeTestWidgetState createState() => _ImageSizeTestWidgetState();
}

class _ImageSizeTestWidgetState extends State<ImageSizeTestWidget> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: FutureBuilder<ui.Image>(
          future: _getImage(),
          builder: (BuildContext context, AsyncSnapshot<ui.Image> snapshot) {
            print(snapshot.hasData);
            if (snapshot.hasData) {
              return Text('${snapshot.data.width} X ${snapshot.data.height}');
            } else {
              return Text('Loading...');
            }
          },
        ),
      ),
    );
  }

  Future<ui.Image> _getImage() async {
    final Completer<ui.Image> completer = Completer<ui.Image>();
    final String url =
        'http://images-jp.amazon.com/images/P/4101098018.09.MZZZZZZZ';
    Image image = Image.network(url);

    image.image
        .resolve(ImageConfiguration())
        .addListener(ImageStreamListener((ImageInfo info, bool isSync) {
      print(info.image.width);
      completer.complete(info.image);
    }));

    return completer.future;
  }
}

Option 2: Just use the code as is in the original post bringing the Image widget creation and information extraction into the build method.

Abhilash Chandran
  • 6,803
  • 3
  • 32
  • 50
0

Based on the advice from Abion47, I successfully get the image with http package. But I still cannot get width and/ or height values even after getting the image. Alternatively, I use response.body.length to check whether the downloaded image is valid or not.

  Future<Image> _getImage() async {
    Image _image;
    final String url = 'http://images-jp.amazon.com/images/P/4101098018.09.MZZZZZZZ';
    var response = await http.get(url);

    print("Response status: ${response.statusCode}"); // 200
    _image = Image.memory(response.bodyBytes);
    print(_image.width); // still null
    print(response.body.length); // this shows numbers. I'll use this.

    return _image;
  }
Hirotaka Nishimiya
  • 351
  • 1
  • 3
  • 10
  • 1
    The length of `response.bodyBytes` is just the number of bytes in the response body - it will not line up with the dimensions of your image. For one, the pixels will represent each channel of the image, and there could be 3/4 channels, each pixel could have more than one byte per channel per pixel, etc. Then the image will likely be compressed (as a JPG or PNG, for instance), so the pixel data will be garbled for just casual perusal. There's also the header bytes to consider, which contain metadata regarding the image. In short, the length of `bodyBytes` will be nowhere near what you need. – Abion47 Dec 12 '19 at 21:05
0

You are halfway there with your self-answer code. From there, you can convert the bytes to a ui.Image object with instantiateImageCodec.

Future<Image> _getImage() async {
  Image _image;
  final String url = 'http://images-jp.amazon.com/images/P/4101098018.09.MZZZZZZZ';
  var response = await http.get(url);
  print("Response status: ${response.statusCode}"); // 200, ideally

  final bytes = response.bodyBytes);
  final codec = await instantiateImageCodec(bytes);
  final frame = await codec.getNextFrame();
  final uiImage = frame.image; // a ui.Image object, not to be confused with the Image widget

  print(uiImage.width); // The width of the image in pixels
  print(uiImage.height); // The heightof the image in pixels

  _image = Image.memory(bytes);

  return _image;
}

It kind of sucks that you have to do it this way as the image will get decoded twice, but short of creating your own custom Image widget that can take the ui.Image object directly, I don't think there's much that can be done about that.

Abion47
  • 22,211
  • 4
  • 65
  • 88