Slow Flutter GridView

2,450

You can create your own CustomGridView with CustomPainter widget, and draw all items + add one gesture detector and calculate the place of touch, if you need to add onTap behavior to blocs

enter image description here enter image description here

import 'package:flutter/material.dart';
import 'package:flutter/rendering.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(
        backgroundColor: Colors.black,
        body: SafeArea(
          child: MyHomePage(),
        ),
      ),
    );
  }
}

final int redCount = 728;
final int greyCount = 3021;
final int allCount = 4160;
final int crossAxisCount = 52;

enum BlockTypes {
  red,
  gray,
  green,
  yellow,
}

class MyHomePage extends StatefulWidget {
  MyHomePage()
      : blocks = List<BlockTypes>.generate(allCount, (index) {
          if (index < redCount) {
            return BlockTypes.red;
          } else if (index < redCount + greyCount) {
            return BlockTypes.gray;
          }
          return BlockTypes.green;
        });

  final List<BlockTypes> blocks;

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

class _MyHomePageState extends State<MyHomePage> {
  int columnsCount;
  double blocSize;

  int clickedIndex;
  Offset clickOffset;
  bool hasSizes = false;
  List<BlockTypes> blocks;
  final ScrollController scrollController = ScrollController();

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
    blocks = widget.blocks;
    super.initState();
  }

  void _afterLayout(_) {
    blocSize = context.size.width / crossAxisCount;
    columnsCount = (allCount / crossAxisCount).ceil();
    setState(() {
      hasSizes = true;
    });
  }

  void onTapDown(TapDownDetails details) {
    final RenderBox box = context.findRenderObject();
    clickOffset = box.globalToLocal(details.globalPosition);
  }

  void onTap() {
    final dx = clickOffset.dx;
    final dy = clickOffset.dy + scrollController.offset;
    final tapedRow = (dx / blocSize).floor();
    final tapedColumn = (dy / blocSize).floor();
    clickedIndex = tapedColumn * crossAxisCount + tapedRow;

    setState(() {
      blocks[clickedIndex] = BlockTypes.yellow;
    });
  }

  @override
  Widget build(BuildContext context) {
    print(blocSize);
    return hasSizes
        ? SingleChildScrollView(
            controller: scrollController,
            child: GestureDetector(
              onTapDown: onTapDown,
              onTap: onTap,
              child: CustomPaint(
                size: Size(
                  MediaQuery.of(context).size.width,
                  columnsCount * blocSize,
                ),
                painter: CustomGridView(
                  blocs: widget.blocks,
                  columnsCount: columnsCount,
                  blocSize: blocSize,
                ),
              ),
            ),
          )
        : Container();
  }
}

class CustomGridView extends CustomPainter {
  final double gap = 1;
  final Paint painter = Paint()
    ..strokeWidth = 1
    ..style = PaintingStyle.fill;

  final int columnsCount;
  final double blocSize;
  final List<BlockTypes> blocs;

  CustomGridView({this.columnsCount, this.blocSize, this.blocs});

  @override
  void paint(Canvas canvas, Size size) {
    blocs.asMap().forEach((index, bloc) {
      setColor(bloc);
      canvas.drawRRect(
          RRect.fromRectAndRadius(
              Rect.fromLTWH(
                getLeft(index),
                getTop(index),
                blocSize - gap,
                blocSize - gap,
              ),
              Radius.circular(1.0)),
          painter);
    });
  }

  double getTop(int index) {
    return (index / crossAxisCount).floor().toDouble() * blocSize;
  }

  double getLeft(int index) {
    return (index % crossAxisCount).floor().toDouble() * blocSize;
  }

  @override
  bool shouldRepaint(CustomGridView oldDelegate) => true;
  @override
  bool shouldRebuildSemantics(CustomGridView oldDelegate) => true;

  void setColor(BlockTypes bloc) {
    switch (bloc) {
      case BlockTypes.red:
        painter.color = Colors.red;
        break;
      case BlockTypes.gray:
        painter.color = Colors.grey;
        break;
      case BlockTypes.green:
        painter.color = Colors.green;
        break;
      case BlockTypes.yellow:
        painter.color = Colors.yellow;
        break;
    }
  }
}
Share:
2,450
eth0
Author by

eth0

Updated on December 20, 2022

Comments

  • eth0
    eth0 over 1 year

    I have a need to create a 52x80 grid of square blocks. It looks like this:

    enter image description here

    But performance is particularly slow whilst developing in an emulator (over 1s 'lag'). I understand that Flutter code runs faster in release mode on a physical device, which is also true in my case if the device is new-ish. But if the device is a few years old (i.e. Samsung Galaxy S8 or iPhone 8) then there is a frustratingly noticeable time in loading the view and whilst scrolling. And I can't release my app like that. I'm building my GridView like this:

      GridView.count(
        shrinkWrap: true,
        primary: false,
        padding: const EdgeInsets.all(5.0),
        crossAxisCount: 52,
        crossAxisSpacing: 1.0,
        mainAxisSpacing: 1.0,
        addAutomaticKeepAlives: true,
        children: blocks.map((block) => // blocks is just a list of 4160 objects
          FlatButton(
            child: null,
            color: block.backgroundColor,
            onPressed: () {
              // open a new route
            },
            splashColor: Colors.transparent,  
            highlightColor: Colors.transparent
          )
        ).toList()
      )
    

    I've tried switching out FlatButton for an Image or a SizedBox which helps a little. Any suggestions on how I could make this faster?

  • eth0
    eth0 about 4 years
    Amazing. This is 100x better. Thanks for pointing me in the right direction!
  • Rémi Rousselet
    Rémi Rousselet about 4 years
    SingleChildScrollView is not a very good idea (as it will paint the items that are not visible too). A custom RenderSliver would probably be better
  • Kherel
    Kherel about 4 years
    @Rémi Rousselet. In the question eth0 was talking about the grid 52x80, it can be showen in one screen.