Widgets in Gridview lose state when off-screen

604

This behavior is working as intended in the sense that Flutter is designed to clean up your offscreen State. You can get the behavior you want by maintaining the isSelected boolean at a higher level of the tree, e.g. in CardHolder or in a model class. For simple cases a ValueNotifier may be sufficient. Here's an example of that.

import 'dart:collection';
import 'package:flutter/scheduler.dart';
import 'package:flutter/material.dart';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
        primaryColorBrightness: Brightness.light,
      ),
      home: new CardHolder(),
    );
  }
}

class CardHolder extends StatefulWidget {
  CardHolder({Key key}) : super(key: key);

  @override
  _CardHolderState createState() => new _CardHolderState();
}

class _CardHolderState extends State<CardHolder> {
  List _cardData;

  _getCards() async {
    String endpoint = 'https://jsonplaceholder.typicode.com/posts';
    var httpClient = createHttpClient();
    var response = await httpClient.read(endpoint);

    List data = JSON.decode(response);

    if (!mounted) return;

    setState(() {
      _cardData = data;
    });
  }

  @override
  void initState() {
    super.initState();

    _getCards();
  }

  @override
  Widget build(BuildContext context) {
    return new GridView.extent(
      maxCrossAxisExtent: 250.0,
      mainAxisSpacing: 4.0,
      crossAxisSpacing: 4.0,
      children: _buildGridList(_cardData)
    );
  }
}

List<Card> _buildGridList(data) {
  if (data == null) return [];
  List<Card> cards = [];
  for (var card in data) {
    cards.add(new Card(
      title: card['title'],
      isSelected: new ValueNotifier<bool>(false),
    ));
  }
  return cards;
}

class Card extends AnimatedWidget {
  Card({Key key, this.title, this.isSelected }) : super(key: key, listenable: isSelected);

  final String title;
  final ValueNotifier<bool> isSelected;

  @override
  Widget build(BuildContext context) {
    print('Rendering card: ' + title);
    return new GestureDetector(
      onTap: () {
        isSelected.value = !isSelected.value;
      },
      child: new Container(
        constraints: new BoxConstraints(minHeight: 120.0, minWidth: 100.0, maxWidth: 100.0),
        decoration: new BoxDecoration(
          color: isSelected.value ? Colors.red : Colors.white,
          borderRadius: new BorderRadius.all(new Radius.circular(2.5)),
          boxShadow: [new BoxShadow(color: Colors.black45, blurRadius: 5.0, spreadRadius: 0.0, offset: new Offset(0.0, 3.0))]
        ),
        margin: new EdgeInsets.all(5.0),
        padding: new EdgeInsets.all(10.0),
        child: new Text(title, style: new TextStyle(color: Colors.black))
      )
    );
  }
}
Share:
604
opticon
Author by

opticon

Updated on December 02, 2022

Comments

  • opticon
    opticon over 1 year

    I'm just starting to play around with Flutter/Dart, and I'm wondering why my Card widgets are losing their state when they scroll offscreen.

    _isSelected is toggled by a user tapping one one of the Card widgets. All is fine, until they disappear off screen - at which point they revert to normal. I'm assuming I have to take additional steps to preserve state but I'm not quite sure how to best go about this.

    import 'dart:convert';
    
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    class CardHolder extends StatefulWidget {
      CardHolder({Key key}) : super(key: key);
    
      @override
      _CardHolderState createState() => new _CardHolderState();
    }
    
    class _CardHolderState extends State<CardHolder> {
      List _cardData;
    
      _getCards() async {
        String endpoint = 'https://jsonplaceholder.typicode.com/posts';
        var httpClient = createHttpClient();
        var response = await httpClient.read(endpoint);
    
        List data = JSON.decode(response);
    
        if (!mounted) return;
    
        setState(() {
          _cardData = data;
        });
      }
    
      @override
      void initState() {
        super.initState();
    
        _getCards();
      }
    
      @override
      Widget build(BuildContext context) {
        return new GridView.extent(
            maxCrossAxisExtent: 250.0,
            mainAxisSpacing: 4.0,
            crossAxisSpacing: 4.0,
            children: _buildGridList(_cardData)
          );
      }
    }
    
    List<Card> _buildGridList(data) {
      if (data == null) return [];
      List<Card> cards = [];
      for (var card in data) {
        cards.add(new Card(title: card['title']));
      }
      return cards;
    }
    
    class Card extends StatefulWidget {
      Card({Key key, this.title}) : super(key: key);
    
      final String title;
    
      @override
      CardState createState() => new CardState();
    }
    
    class CardState extends State<Card> {
    
      String title;
      bool _isSelected = false;
    
      _toggleSelected() {
        setState(() {
          _isSelected = !_isSelected;
          print('Toggled to ' + _isSelected.toString());
        });
      }
    
      CardState({this.title = "No Title!"});
    
      @override
      Widget build(BuildContext context) {
        print('Rendering card: ' + widget.title);
        return new GestureDetector(
            onTap: _toggleSelected,
            child: new Container(
                constraints: new BoxConstraints(minHeight: 120.0, minWidth: 100.0, maxWidth: 100.0),
                decoration: new BoxDecoration(
                    color: _isSelected ? Colors.red : Colors.white,
                    borderRadius: new BorderRadius.all(new Radius.circular(2.5)),
                    boxShadow: [new BoxShadow(color: Colors.black45, blurRadius: 5.0, spreadRadius: 0.0, offset: new Offset(0.0, 3.0))]
                ),
                margin: new EdgeInsets.all(5.0),
                padding: new EdgeInsets.all(10.0),
                child: new Text(widget.title, style: new TextStyle(color: Colors.black))
            )
        );
      }
    }