Search code examples
androidflutterlistview

How to remove an item from a ListView in Flutter


I am looking for a way to remove a ListView item from outside of ListView.builder. Item name and index are known. I was hopping to achieve this via ListView.removeAt(itemIndex); but unfortunately this does not work.

In general... there are two FloatingActionButtons: fab1 and fab2. Once fab2 is pressed removeItem(itemName) function is called in order to remove data from database (this works OK) and once data is removed item should be removed from ListView (this does not work since I do not know how to refer to ListView).

Could you please advise on how to remove an item from a ListView in this case?

The full code is attached bellow:

  Future<List<PostListDetails>> postsFuture = getPosts();
  bool showButton = false;
  String itemName ="";
  int itemIndex = 0;


  static Future<List<PostListDetails>> getPosts() async {

    SharedPreferences prefs = await SharedPreferences.getInstance();
    final token = await prefs.getString('token');
    final listID = await prefs.getString('listID');
    final lista = await prefs.getString('lista');

    Response response = await post(Uri.parse('someURL'),
      headers: <String, String>{
        'Accept': 'application/json',
        'Authorization': 'Bearer $token',
      },
      body: <String, String>{
                  'listID': '$listID',
      },
    );

    if(response.statusCode != 200){
      print('Błąd połączenia z bazą danych...  status[${response.statusCode}]');
    }

    final List body = json.decode(response.body);
    return body.map((e) => PostListDetails.fromJson(e)).toList();
  }


void removeItem(String nazwa) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    final token = await prefs.getString('token');
    final listID = await prefs.getString('listID');

    try{
      Response response = await post(Uri.parse('SomeURL'),
        headers: <String, String>{
          'Accept': 'application/json',
          'Authorization': 'Bearer $token',
        },
        body: <String, String>{
          'listID': '$listID',
          'nazwa': nazwa,
        },
      );

      if(response.statusCode == 200) {
        //ListView.removeAt(itemIndex);  <--- I do not know how to do this
        
      }else{
        print('Błąd połączenia z bazą danych...  status[${response.statusCode}]');
      }
    }catch(e){
      print(e.toString());
    }
  }



  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          FloatingActionButton.extended(
            heroTag: "fab1",
            onPressed: () {
              print('---> addList()');
              // addList();
            },
            icon: const Icon(Icons.add_circle_outline),
            label: const Text('Dodaj'),
          ),
          if (showButton)
            FloatingActionButton.extended(
              heroTag: "fab2",
              onPressed: () {
                print('---> removing $itemName');
                removeItem(itemName);  // this function removes data from database
                setState(() => showButton = false);
              },
              icon: const Icon(Icons.delete),
              label: const Text('Usuń'),
            ),
        ],
      ),
      appBar: AppBar(
        backgroundColor: Colors.grey[900],
        title: const Text('Lista zakupowa'),
        actions: [
          IconButton(
            onPressed: signUserOut,
            icon: const Icon(Icons.logout),
          ),
        ],
      ),
      body: Center(
        child: FutureBuilder<List<PostListDetails>>(
          future: postsFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const CircularProgressIndicator();
            } else if (snapshot.hasData) {
              return buildPosts(snapshot.data!);
            } else {
              return const Text("Brak danych...");
            }
          },
        ),
      ),
    );
  }

  @override
  Widget buildPosts(List<PostListDetails> posts) {
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        final post = posts[index];
        int customCompare(PostListDetails a, PostListDetails b) {
          if (a.status != b.status) {
            return a.status!.toInt() - b.status!.toInt();
          }
          final int categoryCompare = a.kategoria.toString().compareTo(b.kategoria.toString());
          if (categoryCompare != 0) {
            return categoryCompare;
          }
          return a.nazwa.toString().compareTo(b.nazwa.toString());
        }
        return GestureDetector(
          onLongPress: () {
            itemName = post.nazwa!;
            itemIndex = index;
            setState(() => showButton = true);
          },
          child: Container(
            color: post.status! % 2 == 1 ? Colors.grey[100] : Colors.white,
            margin: const EdgeInsets.symmetric(vertical: 0.5, horizontal: 0),
            padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
            height: 50,
            width: double.maxFinite,
            child: Row(
              children: [
                Expanded(
                  flex: 8,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        post.nazwa!,
                        textAlign: TextAlign.left,
                        style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: Colors.black),
                      ),
                      Text(
                        '${post.waga!.toString()} g.',
                        textAlign: TextAlign.left,
                        style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 15, color: Colors.grey),
                      ),
                    ],
                  ),
                ),
                Expanded(
                  flex: 2,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Expanded(
                        flex: 0,
                        child: Row(),
                      ),
                      Expanded(
                        flex: 10,
                        child: Row(
                          children: [
                            CatIcon(imagePath: (Cat2Ico(post.kategoria!.toString()))),
                            Checkbox(
                              value: Int2Bool(post.status!.toInt()),
                              onChanged: (value) {
                                setState(() {
                                  post.status = Bool2Int(value!);
                                });
                                saveStatus(post.nazwa!, post.status!, index);
                                posts.sort(customCompare);
                              },
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }

UPDATE 1 I have updated the source code and moved ListView.remove... into removeItem function.


UPDATE 2

Bellow final code after the changes:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          FloatingActionButton.extended(
            heroTag: "fab1",
            onPressed: () {
              print('---> addList()');
              // addList();
            },
            icon: const Icon(Icons.add_circle_outline),
            label: const Text('Dodaj'),
          ),
          if (showButton)
            FloatingActionButton.extended(
              heroTag: "fab2",
              onPressed: () {
                removeItem(itemName).then((value){
                  setState((){
                    itemToRemove = true;
                  });
                });
                setState(() => showButton = false);
              },
              icon: const Icon(Icons.delete),
              label: const Text('Usuń'),
            ),
        ],
      ),
      appBar: AppBar(
        backgroundColor: Colors.grey[900],
        title: const Text('Lista zakupowa'),
        actions: [
          IconButton(
            onPressed: signUserOut,
            icon: const Icon(Icons.logout),
          ),
        ],
      ),
      body: Center(
        child: FutureBuilder<List<PostListDetails>>(
          future: postsFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const CircularProgressIndicator();
            } else if (snapshot.hasData) {
              return buildPosts(snapshot.data!);
            } else {
              return const Text("Brak danych...");
            }
          },
        ),
      ),
    );
  }

  @override
  Widget buildPosts(List<PostListDetails> posts) {
    if (itemToRemove) {
      posts.removeWhere((element) => element.nazwa == itemName);
      itemToRemove=false;
    }
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        final post = posts[index];
        int customCompare(PostListDetails a, PostListDetails b) {
          if (a.status != b.status) {
            return a.status!.toInt() - b.status!.toInt();
          }
          final int categoryCompare = a.kategoria.toString().compareTo(b.kategoria.toString());
          if (categoryCompare != 0) {
            return categoryCompare;
          }
          return a.nazwa.toString().compareTo(b.nazwa.toString());
        }
        return GestureDetector(
          onLongPress: () {
            itemName = post.nazwa!;
            itemIndex = index;
            setState(() => showButton = true);
          },
          child: Container(
            color: post.status! % 2 == 1 ? Colors.grey[100] : Colors.white,
            margin: const EdgeInsets.symmetric(vertical: 0.5, horizontal: 0),
            padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
            height: 50,
            width: double.maxFinite,
            child: Row(
              children: [
                Expanded(
                  flex: 8,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        post.nazwa!,
                        textAlign: TextAlign.left,
                        style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: Colors.black),
                      ),
                      Text(
                        '${post.waga!.toString()} g.',
                        textAlign: TextAlign.left,
                        style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 15, color: Colors.grey),
                      ),
                    ],
                  ),
                ),
                Expanded(
                  flex: 2,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Expanded(
                        flex: 0,
                        child: Row(),
                      ),
                      Expanded(
                        flex: 10,
                        child: Row(
                          children: [
                            CatIcon(imagePath: (Cat2Ico(post.kategoria!.toString()))),
                            Checkbox(
                              value: Int2Bool(post.status!.toInt()),
                              onChanged: (value) {
                                setState(() {
                                  post.status = Bool2Int(value!);
                                });
                                saveStatus(post.nazwa!, post.status!, index);
                                posts.sort(customCompare);
                              },
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }

Solution

  • This could be achieved in many ways.

    1. Wait until the item is successfully deleted from the database then updated the app. This will use loaders to await the database transaction and setState to update the UI once completed.

    2. By getting the index of the list item and deleting it by updating the state of the app. posts.removeAt(itemIndex);

    I would suggest you use way 1. Because you'll be able to tell if something in the DB went wrong simply by using Future and .then()

    Do this

    void removeItem(String nazwa) async {
        SharedPreferences prefs = await SharedPreferences.getInstance();
        final token = await prefs.getString('token');
        final listID = await prefs.getString('listID');
    
        try{
          Response response = await post(Uri.parse('SomeURL'),
            headers: <String, String>{
              'Accept': 'application/json',
              'Authorization': 'Bearer $token',
            },
            body: <String, String>{
              'listID': '$listID',
              'nazwa': nazwa,
            },
          );
    
          if(response.statusCode == 200) {
            //ListView.removeAt(itemIndex); Don't need this
           // just refresh UI here
           setState(() { });//<==remove this if you do not want to refresh the entire screen
            
          }else{
            print('Błąd połączenia z bazą danych...  status[${response.statusCode}]');
          }
        }catch(e){
          print(e.toString());
        }
      }
    

    Then for the UI to update based on the change of DB

    Center(
            child: FutureBuilder<List<PostListDetails>>(
               future: postsFuture,//Replace with getPosts() to directly/actively read from DB 
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const CircularProgressIndicator();
                } else if (snapshot.hasData) {
                  return buildPosts(snapshot.data!);
                } else {
                  return const Text("Brak danych...");
                }
              },
            ),
    
    

    For you to show change in progress use CircularProgressIndicator() like this

    ...
    bool isLoading = false;
    ...
    FloatingActionButton.extended(
                  heroTag: "fab2",
                  onPressed: () {
                    print('---> removing $itemName');
                    //loading
                    setState((){
                        isLoading = true;
                      });
                    removeItem(itemName).then((value){
                         setState((){
    posts.removeWhere((element) => element.itemName == itemName);//<==please confirm that element.itemName is in your model class else correct it
                        isLoading = false;
                      });};  // this function removes data from database
                    setState(() => showButton = false);
                  },
                  icon: const Icon(Icons.delete),
                  label: const Text('Usuń'),
                ),
    

    use this in you body like this

     body: isLoading ? Center(child: CircularProgressIndicator()) : Center(
            child: FutureBuilder<List<PostListDetails>>(
              future: postsFuture,
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const CircularProgressIndicator();
                } else if (snapshot.hasData) {
                  return buildPosts(snapshot.data!);
                } else {
                  return const Text("Brak danych...");
                }
              },
            ),
          ),