Flutter with Sqflite + Getx data update issues

I am currently using GetX and Sqflite within Flutter and encountering the issue of data update after insert.

Firstly, there is a LoadingScreen which checks if there are any entries in the User Database. If none, then it opens up the SignUp Page to be filled by the User. If there is already an entry in the User Database, then the SignIn Page is opened.

Once the SignUp Page is completed, the data is written into the User Database and the user is then taken to the Home Page where the User Name is displayed in the drawer.

The User is on .obs in the LoadingScreenController and I am using Obx for the username in the Home Page. Despite this, the data after completion of the insert command is not automatically updated.

However, if I call the fetch function again in the init of HomePage, then the fresh data is broght in.

My question is, do I have to run fetch function everytime there is an update to the database? Does it not beat the purpose of Getx? Obviously, I am wrong somewhere.

Below is the code to the Home Page, LoadingScreen, LoadingScreenController, SignUp Page and also Database Helper.

Would appreciate any help on this.

HomePage Code:

class Home extends StatefulWidget {
  const Home({Key? key}) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  LoadingController loadingController = Get.find();

  var notifyHelper;
  @override
  void initState() {
    super.initState();
    notifyHelper = NotifyHelper();
    notifyHelper.initializeNotification();
    notifyHelper.requestIOSPermissions();
    username = loadingController.users[0].name.toString();
    usermail = loadingController.users[0].mail.toString();
  }

  String name = '', usernames = '', usermail = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _appbar(context),
      body: SingleChildScrollView(
        scrollDirection: Axis.vertical,
        child: Center(
          child: Obx(() => Text(loadingController.users.length.toString())),
        ),
      ),
      drawer: MyDrawer(usernames, usermail),
    );
  }

  _appbar(BuildContext context) {
    return AppBar(
      //leading: Drawer(),
      actions: [
        const Icon(
          Icons.person,
          size: 20.0,
        ),
      ],
    );
  }
}

LoadingScreen

class LoadingScreen extends StatelessWidget {
  final loadingController = Get.put(LoadingController());
  final splashController = Get.put(SplashController());
  final TextEditingController passwordController = TextEditingController();
  int index = 0;

  LoadingScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //backgroundColor: Colors.black,
      body: SafeArea(
        child: SingleChildScrollView(
          child: Column(
            children: [
              const SizedBox(
                height: 20,
              ),
              _splash(context),
              GetBuilder<SplashController>(
                  builder: (_) => splashController.completes.value == false
                      ? MyTextContainer(
                          20.0,
                          'Counting Your Money ... \n Please wait',
                          .4,
                          true,
                          subTitleStyle)
                      : const Text('')),
              GetBuilder<LoadingController>(
                builder: (_) => loadingController.users.length > 0
                    ? _signin(
                        context, loadingController.users[0].name.toString())
                    : _signup(context),
              ),
            ],
          ),
        ),
      ),
    );
  }

  onTap() {
    if (passwordController.text !=
        loadingController.users[0].password.toString()) {
      Get.snackbar('Alert', 'Passwords do not match',
          snackPosition: SnackPosition.BOTTOM);
    } else {
      Get.to(() => const Home());
    }
  }

  Widget _splash(BuildContext context) {
    return Center(
        child: GetBuilder<SplashController>(
      builder: (_) => ScaleTransition(
        scale: splashController.anim,
        child: Container(
            margin: const EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0),
            padding: const EdgeInsets.only(bottom: 12.0),
            decoration: BoxDecoration(
              gradient: const LinearGradient(
                begin: Alignment.centerLeft,
                end: Alignment.centerRight,
                colors: [primaryClr, yellowClr],
              ),
              borderRadius: BorderRadius.circular(18),
              border: Border.all(color: Colors.white, width: 2.0),
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Lottie.asset('assets/loadingScreen.json',
                    height: MediaQuery.of(context).size.height * .40),
                Text('Welcome ', style: titleStyle),
              ],
            )),
      ),
    ));
  }

  Widget _signin(BuildContext context, String username) {
    return GetBuilder<SplashController>(
      builder: (_) => ScaleTransition(
        scale: splashController.anim,
        child: Container(
          margin: const EdgeInsets.only(left: 10.0, right: 10.0),
          child: Column(
            children: [
              Text(
                "Welome back $username \nEnter Password to Sign-In",
                textAlign: TextAlign.center,
              ),
              MyTextInputField(
                title: 'Password',
                hint: 'Enter Your Password',
                textStyle: subTitleStyle,
                textInputType: TextInputType.number,
                textCapital: TextCapitalization.none,
                controller: passwordController,
                isObscure: true,
              ),
              const SizedBox(
                height: 20.0,
              ),
              passwordController.text.isEmpty
                  ? Container()
                  : MyButton(
                      label: 'SignIn',
                      onTap: onTap,
                      textStyle: titleStyle,
                      colorScheme: primaryClr,
                    ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _signup(BuildContext context) {
    return GetBuilder<SplashController>(
      builder: (_) => ScaleTransition(
        scale: splashController.anim,
        child: Column(
          children: [
            Text(
              "This is your first Login \nClick here to Sign-Up",
              style: subTitleStyle,
            ),
            const SizedBox(
              height: 20.0,
            ),
            MyButton(
              label: 'Sign-Up',
              textStyle: titleStyle,
              onTap: () => Get.to(() => const SignUp()),
              colorScheme: primaryClr,
            ),
            
          ],
        ),
      ),
    );
  }
}

LoadingScreenController

class LoadingController extends GetxController {
  // ignore: deprecated_member_use
  //var allacc = List<AllAccounts>().obs;

  var users = <User>[].obs;
  var allaccounts = <AllAccounts>[].obs;
  List<AllAccounts> allacc = <AllAccounts>[].obs;

  @override
  void onInit() async {
    await fetchUsers();
    await fetchAllAccounts();
    super.onInit();
  }

  fetchUsers() async {
    var userList = await DatabaseHelper.db.getAllUsers();
    users.assignAll(userList);
    update();
  }

  fetchAllAccounts() async {
    await DatabaseHelper.db.getAllAccounts().then((accountList) {
      allaccounts.value = accountList.cast<AllAccounts>();
      update();
    });
  }

  Future<void> insertUser({User? user}) async {
    await DatabaseHelper.db.insertUser(user!);
    update();
  }
}

DatabaseHelper (without the DB creation code)

Future<int> insertUser(User user) async {
    Database dbase = await database;
    var res = dbase.insert(User.tblUser, user.toMap());
    return res;
  }

  Future<List<User>> getAllUsers() async {
    Database db = await database;
    List<Map<String, dynamic>> username = await db.rawQuery('''
        SELECT COALESCE(${User.colUserId},'') as 'id', COALESCE(${User.colUserName},'') as 'name', 
        COALESCE(${User.colUserPhone},'') as 'phone',  COALESCE(${User.colUserMail},'9999') as 'mail',
        COALESCE(${User.colUserPassword},'') as 'password', COALESCE(${User.colUserStartDate},'') as 'startdate'
        FROM ${User.tblUser}
    ''');
    //print('Printing from db $usernames');
    return username.isEmpty
        ? []
        : username.map((e) => User.fromMap(e)).toList();
  }

SignUp

class SignUp extends StatefulWidget {
  const SignUp({Key? key}) : super(key: key);

  @override
  State<SignUp> createState() => _SignUpState();
}

class _SignUpState extends State<SignUp> {
  final TextEditingController nameController = TextEditingController();
  final TextEditingController phoneController = TextEditingController();
  final TextEditingController mailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();
  final TextEditingController repasswordController = TextEditingController();
  final TextEditingController startdateController = TextEditingController();
  final LoadingController loadingController = Get.find();

  String alertmsg = '';

  Future<void> onTap() async {
    if (_validateText() == null) {
      _showSnackBar('Writing Data... Please wait');
      _addDataToDB();
    }

    //Get.to(() => Home());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: _appbar(context),
        body: SafeArea(
          child: Container(
            padding: const EdgeInsets.only(left: 20, top: 10, right: 20),
            child: SingleChildScrollView(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('Sign-Up', style: headingStyle),
                  MyTextInputField(
                    title: 'Name',
                    hint: 'Enter First Name and Last Name here',
                    textStyle: subTitleStyle,
                    textInputType: TextInputType.text,
                    textCapital: TextCapitalization.words,
                    controller: nameController,
                  ),
                  MyTextInputField(
                    title: 'Phone Number',
                    hint: 'Phone Number is to send reminders - Required',
                    textStyle: subTitleStyle,
                    textInputType: TextInputType.number,
                    textCapital: TextCapitalization.none,
                    controller: phoneController,
                  ),
                  MyTextInputField(
                    title: 'Mail ID',
                    hint: 'Mail ID is to back up the Data - Required',
                    textStyle: subTitleStyle,
                    textInputType: TextInputType.emailAddress,
                    textCapital: TextCapitalization.none,
                    controller: mailController,
                  ),
                  MyTextInputField(
                    title: 'Password',
                    hint: 'Enter Pasword Here - Minimum of 4 digits',
                    textStyle: subTitleStyle,
                    textInputType: TextInputType.number,
                    textCapital: TextCapitalization.none,
                    controller: passwordController,
                    isObscure: true,
                  ),
                  MyTextInputField(
                    title: 'Confirm Password',
                    hint: 'Re-enter Your Pasword Here',
                    textStyle: subTitleStyle,
                    textInputType: TextInputType.number,
                    textCapital: TextCapitalization.none,
                    controller: repasswordController,
                    isObscure: true,
                  ),
                  MyTextInputField(
                      title: 'Start Date',
                      hint: 'Date from when the Transactions start',
                      textStyle: subTitleStyle,
                      textInputType: TextInputType.number,
                      textCapital: TextCapitalization.none,
                      controller: startdateController,
                      widget: IconButton(
                        icon: const Icon(Icons.calendar_today_outlined),
                        onPressed: () {
                          print('Printed');
                        },
                      )),
                  const SizedBox(
                    height: 20.0,
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: [
                      MyButton(
                        label: 'Confirm',
                        onTap: onTap,
                        textStyle: titleStyle,
                        colorScheme: primaryClr,
                      )
                    ],
                  )
                ],
              ),
            ),
          ),
        ));
  }

  _addDataToDB() async {
    await loadingController
        .insertUser(
            user: User(
                name: nameController.text,
                phone: phoneController.text,
                mail: mailController.text,
                password: passwordController.text,
                startdate: DateTime.now().toString(),
                created: DateTime.now().toString()))
        .then((value) {
      _showSnackBar('Data Saved');
      Get.to(() => Home());
    });
  }

  _validateText() {
    if (nameController.text.isEmpty) {
      alertmsg = 'Name is Required';
      _showSnackBar(alertmsg);
      return alertmsg;
    } else if (_validatePhone(phoneController.text) != null) {
      _showSnackBar(alertmsg);
      return alertmsg;
    } else if (_validateMail(mailController.text) != null) {
      _showSnackBar(alertmsg);
      return alertmsg;
    } else if (_validatePassword(
            passwordController.text, repasswordController.text) !=
        null) {
      _showSnackBar(alertmsg);
      return alertmsg;
    }
    return null;
  }

  String? _validatePhone(String value) {
    if (value.isEmpty || value.length < 10) {
      alertmsg = 'Please enter valid Phone Number';
      // _showSnackBar(alertmsg);
      return alertmsg;
    }
    return null;
  }

  String? _validateMail(String value) {
    bool emailValid = RegExp(
            r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
        .hasMatch(value);
    if (!emailValid) {
      alertmsg = 'Please enter valid mail ID';

      return alertmsg;
    }
    return null;
  }

  String? _validatePassword(String value1, String value2) {
    print('$value1 $value2');
    if (!value1.isNum && value1.length < 4) {
      alertmsg = 'Password should be atleast a 4 digit Number ';
      _showSnackBar(alertmsg);
      return alertmsg;
    } else if (value2.isEmpty) {
      alertmsg = 'Confirm Password';
      _showSnackBar(alertmsg);
      return alertmsg;
    } else if (value1 != value2) {
      alertmsg = 'Passwords do not match';
      _showSnackBar(alertmsg);
      return alertmsg;
    }
  }

  _showSnackBar(String value) {
    Get.snackbar('Alert', value, snackPosition: SnackPosition.BOTTOM);
  }

  _appbar(BuildContext context) {
    return AppBar(
      backgroundColor: Theme.of(context).colorScheme.primary,
      leading: InkWell(
        onTap: () {
          Get.back();
        },
        child: const Icon(Icons.arrow_back),
      ),
    );
  }
}

Solution 1:

You have to call fetch data after each add/update/delete action unless your database somehow returns a stream. And it does not defeat the purpose of GetX. It's expected behaviour. User should explicitly define what they want to do in their code.

Another thing: You are using GetBuilder when using .obs. That won't work because GetBuilder is not reactive and will not react/update UI when an observable (.obs) changes. You should use GetX or Obx instead.