Problem with removing ListView's item with image from FutureBuilder

Viewed 78

I am creating an application where there is a list of items in a ListView. Each item has a description and a photo. Each item is downloaded by my api and the ListView builder generates a test widget "TestInternalWidget" for each item with a photo generated again by FutureBuilder. In a real application, the fetchPhoto method returns the Uint8list photo. For simulation purposes I simply return the url. It works fine. However, there is a problem when deleting an item from the list. The element is removed, but the photo is not. For example: I want to delete item 2: The description is deleted, but, the picture from item 2 jumps to item 3, etc.

before removing item 2:

after removing item 2:

I will be grateful for your help. I am using getx for state management, but I checked in standard configuration with setstate() and there was the same problem

main.dart


import 'package:camera_app/views/widgets/testwidget.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controllers/testcontroller.dart';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    final TestController testController = Get.put(TestController());
    return GetMaterialApp(
        title: 'test',
      home: Scaffold(
          appBar: AppBar(
            iconTheme: IconThemeData(color: Colors.black54),
            backgroundColor: Color(0xfffee6dc),
            centerTitle: true,
          ),
          backgroundColor: Color(0xfffff6f7),
          body: Obx(() => ListView.builder(
              cacheExtent: 100,
              addAutomaticKeepAlives: true,
              physics: BouncingScrollPhysics(),
              //physics : NeverScrollableScrollPhysics(),
              itemCount: testController.albumList.length,
              itemExtent: 120,
              itemBuilder: (context, index) {
                print("item build");
                return InkWell(
                    onDoubleTap: () {
                      testController.albumList.removeAt(index);
                    },
                    child: Obx(()=>  TestInternalWidget(testController.albumList[index]))
                );
              })
          )
      )
    );
  }

}

testcontroller.dart

import 'dart:convert';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;


//MODEL CLASS
class Album {
  final int userId;
  final int id;
  final String title;
   String ? url;

   Album({
    required this.userId,
    required this.id,
    required this.title,
    this.url
  });

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

List<Album> AlbumFromJson(String str) =>
    List<Album>.from(json.decode(str).map((x) => Album.fromJson(x)));


class TestController extends GetxController {
  var albumList = <Album>[].obs;

  @override
  void onInit() {
    print("init controller");
    fetchItems();
    super.onInit();
  }


  Future<void> fetchItems() async {
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/'));

    List<Album> listAlbum;
    if (response.statusCode == 200) {
      listAlbum = AlbumFromJson(response.body);

      //Adding sample url for every item
      for(int i = 0; i< listAlbum.length; i++){
        listAlbum[i].url = 'https://picsum.photos/250?image=' + i.toString();
      }
      albumList.addAll(listAlbum);

    } else {
      throw Exception('Failed to load album');
    }
  }
  
}

testwidget.dart


import 'package:flutter/material.dart';
import '../../controllers/testcontroller.dart';
class TestInternalWidget extends StatefulWidget {

  Album album;
  TestInternalWidget(this.album);

  @override
  TestInternalWidgetState createState() => TestInternalWidgetState();

}

class TestInternalWidgetState extends State<TestInternalWidget>
{
  Future<String?> fetchPhoto() async
  {
    //here for test I'm returning only string url, but in real Uint8List object from my backend api
    return widget.album.url;
  }
  late Future<String?> myFuture;

  @override
  void initState() {
    myFuture = fetchPhoto();
    super.initState();
  }
  @override
    Widget build(BuildContext context) {
      print("internal widget build");
      return  Container(
          child:Column(
            children: [
              FutureBuilder(
                future: myFuture,
                builder: (context, snapshot) {
                  String url = snapshot.data as String;
                  return snapshot.hasData && snapshot.data != null
                      ?
                  CircleAvatar(
                    radius: 30.0,
                    backgroundImage:
                    NetworkImage(url),
                    backgroundColor: Colors.transparent,
                  )
                   : CircularProgressIndicator();
                },
              ),
              Text(widget.album.title),
            ],
          ),
          decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(3),
              color: Color(0xffE4CFC6)
          )
      );
    }

}
1 Answers

You need to add a Key to the widgets you show in the ListView to help Flutter understand that the item at, say, index 2 is not the same as it was before. This article explains it well. Ideally you use an ObjectKey related to the contents of the item in your list. I'm a bit confused as to what is real versus test code in your code, but for example if each item you show has a unique URL then you could create the widget using MyWidget(key: ObjectKey(item.url)) or with additional constructor fields as you see fit.

Related