Search code examples
flutterfirebasedartflutter-getx

GETX Problem setting new value of an RxBool inside GetxController


I just started learning about GetX in Flutter as i'm trying to improve my application states. The user has the option to select "Instant" or "Appointments" so if "Instant" is clicked a firebase document is created and the user is now in searching state. The main goal was to display a small widget at the bottom when the user returns to the main page, such as "Searching for user...".Thus, my introduction to state management.

Error

Exception has occurred.
FlutterError (setState() or markNeedsBuild() called during build.
This Obx widget cannot be marked as needing to build because the framework is already 
in the process of building widgets. A widget can be marked as needing to be built 
during the build phase only if one of its ancestors is currently building. This 
exception is allowed because the framework builds parent widgets before children, 
which means a dirty descendant will always be built. Otherwise, the framework might 
not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
  Obx
The widget which was currently being built when the offending call was made was:
  FindInstantWidget)

order_screen.dart

class FindInstantWidget extends GetWidget {
  const FindInstantWidget({Key? key, required this.text, required this.docId})
      : super(key: key);

  final String? text;
  final String? docId;

  @override
  Widget build(BuildContext context) {

    final controller = Get.put(SearchingController());

    if (controller.isSearching.isFalse) {
      controller.startSearch(docId);
    }

    return Obx(() => controller.isAccepted.isFalse
        ? Scaffold(
            body: Container(
              margin: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Container(
                    margin: const EdgeInsets.all(20),
                    child: const CircularProgressIndicator(
                      backgroundColor: Colors.grey,
                      color: Colors.purple,
                      strokeWidth: 5,
                    ),
                  ),
                  Container(
                    margin: const EdgeInsets.all(20),
                    child: Text("Searching for $text"),
                  ),
                  const SizedBox(height: 16),
                  ButtonWidget(
                      text: 'Cancel',
                      onClicked: () async {
                        //Database().deleteInstant(docId);
                        //Navigator.pop(context);
                        controller.stopSearch();
                        DocumentReference docRef =
                            _firestore.collection('jobs').doc(docId);
                        Database().deleteInstant(docRef);
                        Get.offAll(() => LandingPage(title: "Landing Page"));
                      })
                ],
              ),
            ),
          )
        : const Scaffold(
            body: Text("Found someone for you!"),
          ));
  }
}

searchingController.dart

import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:get/get.dart';

class SearchingController extends GetxController {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  RxBool isSearching = false.obs;
  RxBool isAccepted = false.obs;
  RxString? uidAcceptor;
  final jobType = Rxn<String>();
  final docId = Rxn<String>();
  StreamSubscription? _streamSubscription;

  void startSearch(String? id) {
    DocumentReference docRef = _firestore.collection('jobs').doc(id);

    docId.value = docRef.id;
    isSearching.value = true;

    try {
      _streamSubscription = docRef.snapshots().listen((e) async {
        Map a = e.data() as Map<String, dynamic>;
        uidAcceptor?.value = a['uidAccepted'];
        isAccepted.value = a['isAccepted'];
        jobType.value = a['jobType'];
      });
      update();
    } catch (e) {
      print(e);
    }
  }

  void stopSearch() {
    isSearching.value = false;
    _streamSubscription?.cancel();
  }
}

landing_screen.dart

class LandingPage extends GetWidget {
  LandingPage({Key? key, required this.title}) : super(key: key);
  final String title;

  BottomNavigationController bottomNavigationController =
      Get.put(BottomNavigationController());
  SearchingController searchingController = Get.put(SearchingController());

  final screens = [
    const HomePage(),
    const OrderPage(),
    const AccountPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Obx(() => Stack(alignment: Alignment.center, children: [
              IndexedStack(
                index: bottomNavigationController.selectedIndex.value,
                children: screens,
              ),
              searchingController.isSearching.isTrue
                  ? Positioned(
                      bottom: 10,
                      child: GestureDetector(
                          onTap: () {
                            Get.to(FindInstantWidget(
                                text: searchingController.jobType.value,
                                docId: searchingController.docId.value));
                          },
                          child: Container(
                            decoration: const BoxDecoration(color: Colors.grey),
                            width: 250,
                            child: Padding(
                              padding: const EdgeInsets.all(10),
                              child: Text(
                                "Searching for ${searchingController.jobType.value}...",
                                style: const TextStyle(
                                    fontFamily: 'Roboto',
                                    fontSize: 16,
                                    color: Colors.black,
                                    decoration: TextDecoration.none,
                                    fontWeight: FontWeight.normal),
                                textAlign: TextAlign.center,
                              ),
                            ),
                          )))
                  : const SizedBox(width: 0, height: 0)
            ])),
        bottomNavigationBar: Obx(
          () => BottomNavigationBar(
              currentIndex: bottomNavigationController.selectedIndex.value,
              fixedColor: Colors.green,
              items: const [
                BottomNavigationBarItem(
                  label: "Home",
                  icon: Icon(Icons.home),
                ),
                BottomNavigationBarItem(
                  label: "Orders",
                  icon: Icon(Icons.article_outlined),
                ),
                BottomNavigationBarItem(
                  label: "Account",
                  icon: Icon(Icons.account_circle_outlined),
                ),
              ],
              onTap: (index) => bottomNavigationController.changeIndex(index)),
        ));
  }
}

It was working before, when I returned to the landing screen the widget was displayed as it should, but as I was learning to removing it and cancelling the firebase listen subscription, it doesn't seem to work properly anymore. The widget would appear if I reload the landing widget by clicking on other icons on the nav bar. Seems GetX is not properly updating the views?

The reason that my head is spinning is probably this line (also highlighted) isSearching.value = true; inside searchingController.dart

SS of Exception(https://i.sstatic.net/AOnOV.png)

It barely works if I moved it into the try-catch block but it seems like prolonging my descend into madness from all the other issues that arises.

When initialising my controllers, this does appear on my debug console, it's been bothering me but I have little understand of the "GetLifeCycleBase?"

[GETX] Instance "GetMaterialController" has been created
[GETX] Instance "GetMaterialController" has been initialized
[GETX] Instance "GetLifeCycleBase?" is not registered.
[GETX] Instance "AuthController" has been created
[GETX] Instance "AuthController" has been initialized
[GETX] Instance "BottomNavigationController" has been created
[GETX] Instance "BottomNavigationController" has been initialized

Solution

  • Almost sure it's because that controller.startSearch(docId); inside the build function on your FindInstantWidget.

    Instead of this:

    if (controller.isSearching.isFalse) {
          controller.startSearch(docId);
        }
    

    Try this:

    WidgetsBinding.instance.addPostFrameCallback((_) {
          if (controller.isSearching.isFalse) {
            controller.startSearch(docId);
          }
        });