How to implement this complex view in the flutter?

I am trying to implement a GridView with n columns and the child should be of a certain aspect ratio(say 1.3) but the height of the child should be (wrap content in Android terminology).

I am stuck because as fas I understand GridView's childAspectRatio:1.3 (default:1) always lays out the child in same aspect ratio but not dynamic content.

Note: Child should expand its height according to the image's height

Use case: I am trying to implement a view like below, in which image is wrapped height = wrap content so that in case an image with stretched height can look good and form a StaggeredGridView like structure.

Sample Image


Solution 1: Shady Aziza

There are two things here:

  1. There is an existing package for doing such layout

  2. In order to make the images look good use BoxFit.cover on the DecorationImage widget.

There are tons of example in the package repo here

I just used on of the examples and modified it to include pictures:

enter image description here

class GridViewExample extends StatefulWidget {
  @override
  _GridViewExampleState createState() => new _GridViewExampleState();
}

class _GridViewExampleState extends State<GridViewExample> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Padding(
        padding: const EdgeInsets.all(8.0),
        child: new StaggeredGridView.countBuilder(
  crossAxisCount: 4,
  itemCount: 8,
  itemBuilder: (BuildContext context, int index) => new Container(
        decoration: new BoxDecoration(
          image: new DecorationImage(
            image: new NetworkImage("https://i.imgur.com/EVTkpZL.jpg"),
            fit: BoxFit.cover
          )
        )

        ),

  staggeredTileBuilder: (int index) =>
      new StaggeredTile.count(2, index.isEven ? 2 : 1),
  mainAxisSpacing: 4.0,
  crossAxisSpacing: 4.0,
),),

    );
  }
}


Solution 2: rmtmckenzie

For any of the relatively simple ways of doing this (i.e. without a deep understanding how layout in flutter works), you are going to need to get the sizes of the images before you build anything. This is an answer that describes how to do that by using ImageProvier and ImageStream.

You could then use @aziza's example of flutter_staggered_grid_view once you know the basic dimensions of the images.

An alternative could be to store the image size or at least aspect ratio wherever you store the list of images/urls (I don't know how you're populating the list of images so I can't help you there).

If you want it to be fully based on the size of the images and not grid-like at all, you might be able to do it with a Flow widget. There is a caveat to flow though - I believe that it won't handle a large amount of items very well as it would have to lay all of the children out each time, but I could be wrong about that. If you don't have a huge amount of items, you could use Flow + a SingleChildScrollView for the scrolling part.

If you are going to have a large amount of items (and/or want to do something like dynamic loading of new items), you might have to do something with a CustomMultiChildLayout - I think it would be more efficient but you'd still need to do something to know the sizes of the images.

A last possible solution (I don't know exactly how this would work though) would be to have two scrollable views side-by-side and synchronize their positions. You'd have to set shrinkwrap=true though so you could do the scrolling, and you'd still have to know the height of each image so you could decide which side to put each one in.

Hope that helps you get started at least!


Solution 3: Romain Rastel

Edit: I added the constructor StaggeredTile.fit in the 0.2.0. With that you should be able to build you app ;-).

Dynamic tile sizes

First comment: For now with StaggeredGridView, the layout and the children rendering are completely independant. So as @rmtmckenzie said, you will have to get the image size to create your tiles. Then you can use StaggeredTile.count constructor with a double value for the mainAxisCellCount parameter: new StaggeredTile.count(x, x*h/w) (where h is the height of your image and w its width. So that the tile with have the same aspect ratio as your image.

What you want to accomplish will need more work because you want to have an area below the image with some information. For that I think you will have to compute the real width of your tile before creating it and use the StaggeredTile.extent constructor.

I understand this is not ideal and I'm currently working on a new way to create the layout. I hope it will help to build scenarios like yours.


Solution 4: Blasanka

First let me tell you about how I ended up here:

In my application I wanted a grid view to display my ad cards and all the data coming from the server database and images are coming from the server and images are in different sizes. I used FutureBuilder to map those data to GridView. First I tried to use:

double cardWidth = MediaQuery.of(context).size.width / 3.3;
double cardHeight = MediaQuery.of(context).size.height / 3.6;
//....
GridView.count(
  childAspectRatio: cardWidth / cardHeight,
  //..

As you can see it will not dynamic to all cards. I came here like you and tried to use all answers those are great and you have to tackle a bit to understand how, but any of those answer completely solved my issue.

Using @RomainRastel answer and thanks to his StaggeredGridView package. I had to use StaggeredGridView.count as my constructor to map all cards and for the staggeredTiles property I had to again map all cards and add for each StaggeredTile.fit(2).

I'm sure you didn't get it still, so let's try a simple example so that you do not need to go somewhere else to find an answer:

First add dependency to pubspec.yaml, now version is 0.2.5. You can checkout the latest one here.

dependencies:
 flutter_staggered_grid_view: ^0.2.5

If you are fetching data from internet or if you are going to copy paste this example, you have to also ad this dependency: http: ^0.12.0.

import 'package:flutter/material.dart';

//this is what you need to have for flexible grid
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

//below two imports for fetching data from somewhere on the internet
import 'dart:convert';
import 'package:http/http.dart' as http;

//boilerplate that you use everywhere
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flexible GridView",
      home: HomePage(),
    );
  }
}

//here is the flexible grid in FutureBuilder that map each and every item and add to a gridview with ad card
class HomePage extends StatelessWidget {
  //this is should be somewhere else but to keep things simple for you,
  Future<List> fetchAds() async {
    //the link you want to data from, goes inside get
    final response = await http
        .get('https://blasanka.github.io/watch-ads/lib/data/ads.json');

    if (response.statusCode == 200) return json.decode(response.body);
    return [];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Dynamic height GridView Demo"),
      ),
      body: FutureBuilder<List>(
          future: fetchAds(),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            if (snapshot.hasData) {
              return new Padding(
                padding: const EdgeInsets.all(4.0),
                //this is what you actually need
                child: new StaggeredGridView.count(
                  crossAxisCount: 4, // I only need two card horizontally
                  padding: const EdgeInsets.all(2.0),
                  children: snapshot.data.map<Widget>((item) {
                    //Do you need to go somewhere when you tap on this card, wrap using InkWell and add your route
                    return new AdCard(item);
                  }).toList(),

                  //Here is the place that we are getting flexible/ dynamic card for various images
                  staggeredTiles: snapshot.data
                      .map<StaggeredTile>((_) => StaggeredTile.fit(2))
                      .toList(),
                  mainAxisSpacing: 3.0,
                  crossAxisSpacing: 4.0, // add some space
                ),
              );
            } else {
              return Center(
                  child:
                      new CircularProgressIndicator()); // If there are no data show this
            }
          }),
    );
  }
}

//This is actually not need to be a StatefulWidget but in case, I have it
class AdCard extends StatefulWidget {
  AdCard(this.ad);

  final ad;

  _AdCardState createState() => _AdCardState();
}

class _AdCardState extends State<AdCard> {
  //to keep things readable
  var _ad;
  String _imageUrl;
  String _title;
  String _price;
  String _location;

  void initState() {
    setState(() {
      _ad = widget.ad;
      //if values are not null only we need to show them
      _imageUrl = (_ad['imageUrl'] != '')
          ? _ad['imageUrl']
          : 'https://uae.microless.com/cdn/no_image.jpg';
      _title = (_ad['title'] != '') ? _ad['title'] : '';
      _price = (_ad['price'] != '') ? _ad['price'] : '';
      _location = (_ad['location'] != '') ? _ad['location'] : '';
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      semanticContainer: false,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(4.0)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Image.network(_imageUrl),
          Text(_title),
          Text('\$ $_price'),
          Text(_location),
        ],
      ),
    );
  }
}

If you have any issue, here is complete example in a git repository.

Flutter flexible grid view example

Good luck!


Solution 5: Chandupa Lasith

crossAxisCount:

This defines how many columns present and,

StaggeredTile.fit(value)

represent how may children to fit in a single column vertically

Example:

crossAxisCount: 2,
staggeredTileBuilder: (int index) => new StaggeredTile.fit(1),

will give you the desired output above


Solution 6: pritam parab

This might be too late for a reply, but somehow I achieved this, thanks to the @Romani letsar and his flutter_staggered_grid_view package. Everyone already explained how this library works, I'll get straight to the point.
I was able to make StaggeredTiles of height similar to images, but I had to extra content below it, which is why I added extra 0.5 height to the calculated height. Fortunately API I was playing with was returning Image with it's height and width property.

Quick and simple calculations.
crossAxisCount: 4 //here I defined how many columns I want.

StaggeredTile.count(2,height + 0.5); //This constructor takes 2 arguments, first argument is how many columns should 1 tile take(Since I have 4 columns, and I want 2 tiles in a row, Argument value is 2. In other words 1 tile would take 2 columns). Second argument is height. I wanted height with resect to tile width(i.e. 2 columns).

I used simple formula:
Tile width/title height=image width/image height
//here I took tile width as two, since it is taking 2 columns. I added hard coded value 0.5 to calculated tile height, and so far it is working.

Here is my code:

class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: FutureBuilder(
              future: NetworkHelper.getData(),
              builder: (BuildContext context, AsyncSnapshot snapshot) {
                if (snapshot.data == null)
                  return Center(child: CircularProgressIndicator());
                else {
                  var data = snapshot.data;
                  print(data);
                  List<dynamic> jsonData = jsonDecode(snapshot.data);
                  return StaggeredGridView.countBuilder(
                    crossAxisCount: 4,
                    itemCount: jsonData.length,
                    itemBuilder: (BuildContext context, int index) {
                      JsonModelClass models =
                          JsonModelClass.fromJson(jsonData[index]);
                      String url = models.urls.regular;
                      return GestureDetector(
                        onTap: () {
                          print(url);
                        },
                        child: Card(
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                            children: [
                              Image.network(url),
                              Container(
                                child: Row(
                                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                                  crossAxisAlignment: CrossAxisAlignment.center,
                                  children: [
                                    Text("${models.user.name}"),
                                    Card(
                                      child: Padding(
                                        padding: const EdgeInsets.all(3.0),
                                        child: Row(
                                          mainAxisAlignment:
                                              MainAxisAlignment.spaceEvenly,
                                          children: [
                                            Icon(
                                              Icons.favorite,
                                              color: Colors.red,
                                            ),
                                            SizedBox(
                                              width: 6,
                                            ),
                                            Text("${models.likes}")
                                          ],
                                        ),
                                      ),
                                    ),
                                  ],
                                ),
                              )
                            ],
                          ),
                        ),
                      );
                    },
                    staggeredTileBuilder: (int index) {
                      JsonModelClass models =
                          JsonModelClass.fromJson(jsonData[index]);
                      var height = (models.height * 2) /
                          models.width; //calculating respective 'height' of image in view, 'model.height' is image's original height received from json.
                      return StaggeredTile.count(
                          2,
                          height +
                              0.5); //Adding extra 0.5 space for other content under the image
                    },
                    mainAxisSpacing: 4,
                    crossAxisSpacing: 4,
                  );
                }
              },
            ),
          ),
        );
      }
    }

My Final Output


Solution 7: Kamal Bunkar

The simple solution of this problem is add "key: number of column count". I am using version StaggeredGridview 0.4.0 . All you need to add is Key parameter. The widget will reload again on you resize the browser window. You can find example code here.

For example browser is in full width and you resize the window. I am also using responsive design to detect - is it mobile view for small screen (widget < 800 ), medium size screen ( > 800 & < 1200), Large Screen (> 1200).

StaggeredGridView.countBuilder(
      primary: false,
      shrinkWrap: true,
      key: ObjectKey(ResponsiveWidget.isSmallScreen(context) ? 2 :4), // **add this line**
      crossAxisCount: ResponsiveWidget.isSmallScreen(context) ? 4 : 8,
      itemCount: ResponsiveWidget.isSmallScreen(context) ? 4 : _productsList.length,
      itemBuilder: (BuildContext context, int index) {
        BannersResult product = _productsList.elementAt(index);
        return Home_CustBanner_twobytwo_Item(
          home_banner_list_item: product,
          heroTag: 'categorized_products_grid',
        );
      },

      staggeredTileBuilder: (int index) => new StaggeredTile.fit(2),
      mainAxisSpacing: 15.0,
      crossAxisSpacing: 15.0,
    ),