0

i've built a card which will open a searchDelegate where users can search for cars. When selecting a car, the delegate screen will close onTap: () {close(context, carList[index].id);}. The id will then be updated on the main screen and added to a List.

However, i keep having the error setState() called after dispose()

List<String> selectedCar = [];
// Function to open SearchDelegate
void openCarSearchDelegate() async {
    String? selectedCarId = await showSearch(
    context: context,
      delegate: CompareCarSearchDelegate(_firebaseServices.carsRef),
    );
    print(selectedCarId) // <- Returns value when car is selected

    if (selectedCarId != null) {
      addSelectedCar(selectedCarId);
    }
}
// Update the state to add to the List
void addSelectedCar(String carId) {
    // ERROR occurs here
    setState(() {
      selectedCar.add(carId);
    });
}

@override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(20),
      child: SelectCarCard(
        onTap: openCarSearchDelegate,
      ),
    );
  }
class SelectCarCard extends StatelessWidget {
  const SelectCarCard({
    Key? key,
    required this.onTap,
  }) : super(key: key);

  final void Function() onTap;

  @override
  Widget build(BuildContext context) {
    return InkWell(
      borderRadius: const BorderRadius.all(Radius.circular(12)),
      onTap: onTap,
      child: Ink(
        height: 200,
        width: double.infinity,
        child: SizedBox(
          height: 200,
          width: double.infinity,
          child: const Text(
            "Select Car",
            style: TextStyle(
              color: Colors.black,
              fontSize: 18,
            ),
          ),
        ),
      ),
    );
  }
}
class CompareCarSearchDelegate extends SearchDelegate<String> {
  final Query searchQuery;

  CompareCarSearchDelegate(this.searchQuery);

  @override
  String get searchFieldLabel => 'Select car...';

  @override
  TextStyle get searchFieldStyle => const TextStyle(fontSize: 18);

  @override
  List<Widget> buildActions(BuildContext context) {
    // Clear field
  }

  @override
  Widget buildLeading(BuildContext context) {
    // Return to previous screen
  }

  @override
  Widget buildResults(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: searchQuery.snapshots(),
      builder: (context, snapshot) {

        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator(
            backgroundColor: Colors.white,
            color: kPrimaryColor,
            strokeWidth: 3,
          );
        }

        List<DocumentSnapshot> documents = snapshot.data!.docs;

        final carList = documents.where((doc) {
          // Get results based on search text
        }).toList();


        return ListView.separated(
          separatorBuilder: (context, index) {
            return const Divider();
          },
          shrinkWrap: true,
          padding:
              const EdgeInsets.only(top: 20, bottom: 20, left: 10, right: 10),
          itemCount: carList.length,
          itemBuilder: (context, index) {
            return ListTile(
              contentPadding: const EdgeInsets.only(right: 10),
              title: Text(
                carList[index]['name'],
                overflow: TextOverflow.ellipsis,
              ),
              onTap: () {
                close(context, carList[index].id);
              },
            );
          },
        );
      },
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    return Container();
  }
}
I/flutter (28352): setState() called after dispose(): _BodyState#27420(lifecycle state: defunct, not mounted)
I/flutter (28352): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
I/flutter (28352): The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
I/flutter (28352): This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().

I have tried adding if(mounted) {setState(() {selectedCar.add(carId);});} but the mounted keeps returning false.

Any idea of how i can implement it or any solution would be greatly appreciated. Thanks.

Rohan
  • 17
  • 5
  • please add reproducible error to investigate better such as CompareCarSearchDelegate code is not provided – Burhan Khanzada Jun 16 '23 at 13:32
  • @BurhanKhanzada The CompareCarSearchDelegate is fetching data from firebase and on this side it is working fine since the selectedCarId is returning data – Rohan Jun 16 '23 at 13:50
  • @BurhanKhanzada Please see the code for the CompareCarSearchDelegate in the description – Rohan Jun 16 '23 at 14:33

4 Answers4

0

Try this

void addSelectedCar(String carId) {
  if (mounted) {
    setState(() {
      selectedCar.add(carId);
    });
  }
}
Rationalist
  • 116
  • 9
  • I've tried but the same error is produced. – Rohan Jun 16 '23 at 13:58
  • try above correction, I feel like since you are using async, object has to be mounted. https://api.flutter.dev/flutter/widgets/State/mounted.html – Rationalist Jun 16 '23 at 14:04
  • Yes i also tried this solution before posting on Stackoverflow (check last paragraph on description). :( The mounted value keeps getting equal to false....it won't go in the setState. I've added a print($mounted); to check its value – Rohan Jun 16 '23 at 14:11
  • do you somehow dispose context inside your showSearch()? check it – Rationalist Jun 16 '23 at 14:21
  • I have added the content of the CompareCarSearchDelegate in the description – Rohan Jun 16 '23 at 14:32
0

Is your close(context, carList[index].id); function async? If it is then replace
onTap: () { close(context, carList[index].id)},
with
onTap: () async {await close(context, carList[index].id)},.
If not, then your error indicates that there is some async call which is responsible for the call of setState()

Different context can also be an issue.Try this change

void openCarSearchDelegate(BuildContext context) async {
    String? selectedCarId = await showSearch(
    context: context,
      delegate: CompareCarSearchDelegate(_firebaseServices.carsRef),
    );
    print(selectedCarId) // <- Returns value when car is selected

    if (selectedCarId != null) {
      addSelectedCar(selectedCarId);
    }
}

and

 onTap:() { openCarSearchDelegate(context);},

Edit:
Try mounted at this area.

if (selectedCarId != null && mounted) {
           addSelectedCar(selectedCarId);
    }

Or because of StreamBuilder<QuerySnapshot> your code is still in aync state,(may be that's the reason it never shows mounted). Try replacing above with the following.(if above doesn't works)

Future((){
  if (selectedCarId != null && mounted) {
     addSelectedCar(selectedCarId);
  }
});
Mearaj
  • 1,828
  • 11
  • 14
  • I have added the `async` but the in-buit close() function is not a Future. I've tried but i'm really stuck on this issue – Rohan Jun 16 '23 at 16:20
  • Sorry but the issue with the setstate still persists – Rohan Jun 16 '23 at 16:51
  • Ok, I will try your code and will try to debug and get back to you. – Mearaj Jun 16 '23 at 17:00
  • Your code is not working. Also your SelectCarCard's definition and call mismatches. You can try creating a minimal code to reproduce your issue. – Mearaj Jun 16 '23 at 17:12
  • Yes sorry i had to remove some code before posting to minimize it. I just updated the code above. But for the CompareCarSearchDelegate, i even removed the firebase query to test, and added a simple ListView with `onTap: () { close(context, "Test"); };....still the same – Rohan Jun 16 '23 at 17:18
0

A simple test code with similar scenario

import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String selectedText = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Selected Text: $selectedText',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                showSearch(
                  context: context,
                  delegate: CustomSearchDelegate(),
                ).then((selectedValue) {
                  if (selectedValue != null) {
                    setState(() {
                      selectedText = selectedValue;
                    });
                  }
                });
              },
              child: Text('Select Text'),
            ),
          ],
        ),
      ),
    );
  }
}

class CustomSearchDelegate extends SearchDelegate<String> {
  final List<String> textList = ['Text 1', 'Text 2', 'Text 3'];

  @override
  List<Widget> buildActions(BuildContext context) {
    return [
      IconButton(
        onPressed: () {
          query = '';
        },
        icon: Icon(Icons.clear),
      ),
    ];
  }

  @override
  Widget buildLeading(BuildContext context) {
    return IconButton(
      onPressed: () {
        close(context, '');
      },
      icon: Icon(Icons.arrow_back),
    );
  }

  @override
  Widget buildResults(BuildContext context) {
    final List<String> filteredList = textList
        .where((text) => text.toLowerCase().contains(query.toLowerCase()))
        .toList();

    return ListView.builder(
      itemCount: filteredList.length,
      itemBuilder: (context, index) {
        final String item = filteredList[index];

        return ListTile(
          title: Text(item),
          onTap: () {
            close(context, item);
          },
        );
      },
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    final List<String> filteredList = textList
        .where((text) => text.toLowerCase().contains(query.toLowerCase()))
        .toList();

    return ListView.builder(
      itemCount: filteredList.length,
      itemBuilder: (context, index) {
        final String item = filteredList[index];

        return ListTile(
          title: Text(item),
          onTap: () {
            query = item;
            close(context, item);
          },
        );
      },
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: MyHomePage(),
  ));
}

Rohan
  • 17
  • 5
  • 1
    This code doesn't produce any error. I have again updated my answer with two more suggestions. – Mearaj Jun 16 '23 at 18:06
  • Have you checked the debug log? Because the `$selectedText` is not being updated on my side – Rohan Jun 16 '23 at 18:13
  • 1
    Yeah, it's working fine. Only this issue close(context, null); should be close(context, ""); Rest is working fine. – Mearaj Jun 16 '23 at 18:19
  • 1
    @Mearaj I created a new application and it worked. Thanks a lot for your help. I'll try to find the issue with my current application – Rohan Jun 16 '23 at 19:08
  • Great. That's good to hear. :) – Mearaj Jun 16 '23 at 19:09
0

Update (in case you're having the same issue):

After removing and re-installing the app, the problem stopped. However when i restarted it, the error occurred again even if i didn't made any changes.

Solution: Instead of using the setState(() {});, i used the Get Library which has enabled me to update the selectedText without refreshing the screen. I hope this has helped you!

Rohan
  • 17
  • 5