Linked Questions

Popular Questions

My UI doesn't update anymore if I change the preferred language of the app. The app reads data written into a textfile using asyncprovider.

Snippets from home page and the tab section:

final budgetManagerData = ref.watch(asyncBudgetProvider);
final budgetIndex = ref.watch(indexValueProvider);
final transactionData = ref.watch(asyncTransactionProvider(budgetIndex));

var language = ref.watch(languagesProvider);
var index = ref.watch(languageIndexProvider);
var home = language[index]![Constants.homeText]!;
var transaction = language[index]![Constants.transactionText]!;
var file = language[index]![Constants.fileText]!;

List<String> budgetMonth = [];
List<String> budgetYear = [];
List<String> budgetFileName = [];
String currentFileName = '';
String budgetName = '';
int length = 0;

//* Uses the data once read and assigns it to the variables
budgetManagerData.when(
  data: (data) {
    if (data.isNotEmpty) {
      budgetYear = data[Constants.keyYear]!;
      budgetMonth = data[Constants.keyMonth]!;
      budgetFileName = data[Constants.keyFileName]!;
      currentFileName = budgetFileName[budgetIndex];
      budgetName = "${budgetMonth[budgetIndex]} ${budgetYear[budgetIndex]}";
      length = budgetFileName.length;
    }
  },
  loading: () => const SizedBox(
    height: 150,
    width: 150,
    child: Center(
      child: CircularProgressIndicator(),
    ),
  ),
  error: (error, stackTrace) => Text(error.toString()),
);


body: Padding(
  padding: const EdgeInsets.all(30.0),
  child: TabBarView(
    children: [
      HomeTab(
          readTransactionData: transactionData,
          currentFileName: currentFileName,
          index: budgetIndex),
      TransactionTab(
        data: transactionData,
        currentFileName: currentFileName,
        budgetIndex: budgetIndex,
      ),
      const ViewBudgetTab(),
    ],
  ),
),

I'm only going to show home page and how I watch the provider because tab pages do it the same way.

code:

class HomeTab extends ConsumerWidget {
  const HomeTab({
    super.key,
    required this.readTransactionData,
    required this.currentFileName,
    required this.index,
  });

  final AsyncValue<Map<String, List<String>>> readTransactionData;
  final String currentFileName;
  final int index;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    double totalIncome = 0.00, totalExpense = 0.00, totalBalance = 0.00;

    var language = ref.watch(languagesProvider);
    var index = ref.watch(languageIndexProvider);
    var getStarted = language[index]![Constants.getStartedHomeText]!;
    var income = language[index]![Constants.incomeText]!;
    var expense = language[index]![Constants.expenseText]!;
    var balance = language[index]![Constants.balanceText]!;

    return Column(
      children: [
        Expanded(
          child: readTransactionData.when(
            data: (transactionData) {
              if (transactionData.isEmpty) {
                return Text(getStarted);
              }
              if (transactionData.isNotEmpty) {
                //Calculates the total income,expense and total balance values
                for (int i = 0;
                    i < transactionData[Constants.keyAmount]!.length;
                    i++) {
                  if (transactionData[Constants.keyTransactionType]![i] ==
                      Constants.transactionType[0]) {
                    totalIncome +=
                        double.parse(transactionData[Constants.keyAmount]![i]);
                  } else if (transactionData[Constants.keyTransactionType]![
                          i] ==
                      Constants.transactionType[1]) {
                    totalExpense +=
                        double.parse(transactionData[Constants.keyAmount]![i]);
                  }

                  totalBalance = totalIncome - totalExpense;
                }
              }
              return Column(
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(income),
                      Text(totalIncome.toStringAsFixed(2)),
                    ],
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(expense),
                      Text(totalExpense.toStringAsFixed(2)),
                    ],
                  ),
                  const Divider(
                    color: Colors.black,
                    height: 25,
                    thickness: 0.5,
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(balance),
                      Text(totalBalance.toStringAsFixed(2))
                    ],
                  ),
                ],
              );
            },
            loading: () => const SizedBox(
              height: 150,
              width: 150,
              child: Center(
                child: CircularProgressIndicator(),
              ),
            ),
            error: (error, stackTrace) => Text(error.toString()),
          ),
        ),
        Expanded(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Column(
                children: [
                  Text(expense),
                  IconButton(
                    onPressed: () async {
                      if (context.mounted) {
                        Navigator.of(context)
                            .pushNamed('/add_expense', arguments: [
                          index,
                          currentFileName,
                        ]);
                      }
                    },
                    icon: const Icon(Icons.add, size: 45),
                  )
                ],
              ),
              Column(
                children: [
                  Text(income),
                  IconButton(
                    onPressed: () async {
                      if (context.mounted) {
                        Navigator.of(context)
                            .pushNamed('/add_income', arguments: [
                          index,
                          currentFileName,
                        ]);
                      }
                    },
                    icon: const Icon(Icons.add, size: 45),
                  )
                ],
              )
            ],
          ),
        ),
      ],
    );
  }
}

Where I add data and update the UI:

//* This is the types of pages
enum AddPageType { addIncome, addExpense }

class AddTransactionPage extends ConsumerWidget {
  AddTransactionPage({super.key, required this.pageType});

  late final AddPageType pageType;
  final DatePicker _datePicker = DatePicker();
  final CustomSnackbar _customSnackbar = CustomSnackbar();
  final TextEditingController _dateTextEditing = TextEditingController();
  final TextEditingController _amountTextEditing = TextEditingController();
  final TextEditingController _noteTextEditing = TextEditingController();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final passedArguments = ModalRoute.of(context)!.settings.arguments as List;

    var pagesType = ref.watch(addPageTypeProvider);
    var language = ref.watch(languagesProvider);
    var index = ref.watch(languageIndexProvider);

    var date = language[index]![Constants.dateText]!;
    var amount = language[index]![Constants.amountText]!;
    var note = language[index]![Constants.noteText]!;
    var emptyFields = language[index]![Constants.emptyFieldsText]!;
    var save = language[index]![Constants.saveText]!;
    var page = pagesType[index]![pageType.index];

    return Scaffold(
      appBar: AppBar(
        backgroundColor: const Color(0xfffffbfe),
        shadowColor: Colors.white,
        elevation: 0,
        leading: IconButton(
          icon: const Icon(
            Icons.arrow_back,
            color: Colors.black,
          ),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(30),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Expanded(
              flex: 1,
              child: Align(
                alignment: Alignment.centerLeft,
                child: Text(
                  page,
                  style: const TextStyle(
                      fontSize: 30, fontWeight: FontWeight.bold),
                ),
              ),
            ),
            Expanded(
              flex: 5,
              child: Column(
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Expanded(
                        flex: 2,
                        child: Text(date),
                      ),
                      Expanded(
                        flex: 2,
                        child: TextField(
                          focusNode: AlwaysDisabledFocusNode(),
                          controller: _dateTextEditing,
                          onTap: () {
                            _datePicker.selectDate(context, _dateTextEditing);
                          },
                        ),
                      ),
                      Expanded(
                          flex: 0,
                          child: IconButton(
                            icon: const Icon(Icons.date_range_sharp),
                            // * When clicked it sets the date field to the current date
                            onPressed: () {
                              _datePicker.setCurrentDate(_dateTextEditing);
                            },
                          ))
                    ],
                  ),
                  const SizedBox(
                    height: 30,
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Expanded(
                        flex: 1,
                        child: Text(amount),
                      ),
                      Expanded(
                        flex: 3,
                        child: TextFormField(
                          inputFormatters: [
                            //* Formats the Text input and only allows numbers and 2 decimals
                            FilteringTextInputFormatter.allow(
                              RegExp(r'^\d*\.?\d{0,2}'),
                            ),
                          ],
                          keyboardType: const TextInputType.numberWithOptions(
                            decimal: true,
                          ),
                          controller: _amountTextEditing,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(
                    height: 30,
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Expanded(
                        flex: 1,
                        child: Text(note),
                      ),
                      Expanded(
                        flex: 3,
                        child: TextFormField(
                          controller: _noteTextEditing,
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            Expanded(
              flex: 1,
              child: Align(
                alignment: Alignment.bottomRight,
                child: ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    foregroundColor: Colors.white,
                    backgroundColor: const Color(0xff474747),
                    minimumSize: const Size(120, 45),
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    shape: const RoundedRectangleBorder(
                      borderRadius: BorderRadius.all(
                        Radius.circular(2),
                      ),
                    ),
                  ),
                  child: Text(
                    save,
                    style: const TextStyle(fontSize: 20),
                  ),
                  onPressed: () async {
                    // *: Saves info to textfile
                    if (_dateTextEditing.text.trim().isEmpty ||
                        _amountTextEditing.text.trim().isEmpty ||
                        _noteTextEditing.text.trim().isEmpty) {
                      _customSnackbar.showSnackBar(context, emptyFields, null);
                    } else {
                      var transactionData =
                          "${Constants.transactionType[pageType.index]} ${Constants.keySeparator} ${_dateTextEditing.text.trim()} ${Constants.keySeparator} ${_amountTextEditing.text.trim()} ${Constants.keySeparator} ${_noteTextEditing.text.trim()} \n";

                      await ref
                          .read(asyncTransactionProvider(passedArguments[0])
                              .notifier)
                          .addData(
                            transactionData,
                            passedArguments[1].toString(),
                            passedArguments[0],
                          );

                      if (context.mounted) {
                        Navigator.of(context).pop();
                      }
                    }
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Settings page where I change language:

class SettingsPage extends ConsumerWidget {
  const SettingsPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    NotificationManager notiManager = NotificationManager();

    var dropdownvalue = ref.watch(dropdownValueProvider);
    var indexValue = ref.watch(languageIndexProvider);
    var passcodeValue = ref.watch(passcodeValueProvider);
    var reminderValue = ref.watch(reminderValueProvider);
    var languages = ref.watch(languagesProvider);

    String language = languages[indexValue]![Constants.languageText]!;
    String passcode = languages[indexValue]![Constants.passText]!;
    String reminder = languages[indexValue]![Constants.reminderText]!;
    String exit = languages[indexValue]![Constants.exitText]!;

    return Scaffold(
        appBar: AppBar(
          backgroundColor: const Color(0xfffffbfe),
          elevation: 0,
          shadowColor: Colors.white,
          leading: IconButton(
            color: Colors.black,
            icon: const Icon(Icons.arrow_back_sharp),
            onPressed: () {
              Navigator.pop(context);
            },
          ),
        ),
        body: Padding(
          padding: const EdgeInsets.all(30.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              const SizedBox(
                height: 50,
              ),
              Expanded(
                flex: 10,
                child: Column(
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Expanded(
                          flex: 5,
                          child: Text(language),
                        ),
                        Expanded(
                          flex: 1,
                          child: DropdownButton<String>(
                            alignment: Alignment.center,
                            isExpanded: true,
                            value: dropdownvalue[indexValue],
                            icon: const Icon(Icons.arrow_drop_down),
                            elevation: 16,
                            style: const TextStyle(color: Colors.black),
                            underline: Container(
                              height: 2,
                              color: Colors.black,
                            ),
                            onChanged: (String? value) {
                              // This is called when the user selects an item.
                              if (value == 'EN') {
                                ref.read(languageIndexProvider.notifier).state =
                                    0;
                                box.put(Constants.prefLanguage, 0);
                              } else {
                                ref.read(languageIndexProvider.notifier).state =
                                    1;
                                box.put(Constants.prefLanguage, 1);
                              }
                            },
                            items: dropdownvalue
                                .map<DropdownMenuItem<String>>((String value) {
                              return DropdownMenuItem<String>(
                                value: value,
                                child: Text(
                                  value,
                                  style: const TextStyle(fontSize: 20),
                                ),
                              );
                            }).toList(),
                          ),
                        )
                      ],
                    ),
                    const SizedBox(
                      height: 50,
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(passcode),
                        Switch(
                            // thumb color (round icon)
                            activeColor: Colors.black,
                            activeTrackColor: const Color(0xff33b6e7),
                            inactiveThumbColor: Colors.black,
                            inactiveTrackColor: Colors.grey.shade400,
                            splashRadius: 20.0,
                            // boolean variable value
                            value: passcodeValue,
                            // changes the state of the switch
                            onChanged: (value) async {
                              if (value == true) {
                                if (context.mounted) {
                                  Navigator.of(context)
                                      .pushNamed('/create_passcode');
                                }
                              }

                              if (value == false) {
                                box.put(Constants.prefPassword, value);
                                ref.read(passcodeValueProvider.notifier).state =
                                    value;
                                await Hive.openBox(Constants.passName);
                                Hive.box(Constants.passName)
                                    .delete(Constants.passName);
                                log('Deleted');
                              }
                            }),
                      ],
                    ),
                    const SizedBox(
                      height: 50,
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(reminder),
                        Switch(
                          // thumb color (round icon)

                          activeColor: Colors.black,
                          activeTrackColor: const Color(0xff33b6e7),
                          inactiveThumbColor: Colors.black,
                          inactiveTrackColor: Colors.grey.shade400,
                          splashRadius: 20.0,
                          // boolean variable value
                          value: reminderValue,
                          // changes the state of the switch
                          onChanged: (value) async {
                            final androidInfo =
                                await DeviceInfoPlugin().androidInfo;
                            var allAccepted = false;

                            if (androidInfo.version.sdkInt > 32) {
                              final Map<Permission, PermissionStatus>
                                  permsStatus =
                                  await [Permission.notification].request();

                              permsStatus.forEach((permission, status) {
                                if (status == PermissionStatus.granted) {
                                  allAccepted = true;
                                }
                              });

                              if (await Permission.notification.isDenied) {
                                openAppSettings();
                              }
                            } else {
                              allAccepted = true;
                            }

                            if (allAccepted) {
                              allAccepted = false;

                              ref.read(reminderValueProvider.notifier).state =
                                  value;
                              if (value == true) {
                                await notiManager
                                    .scheduleReminderNotification();
                              }
                              box.put(Constants.prefNoties, value);
                            }
                          },
                        ),
                      ],
                    ),
                  ],
                ),
              ),
              Expanded(
                flex: 1,
                child: Align(
                  alignment: Alignment.centerRight,
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      foregroundColor: Colors.white,
                      backgroundColor: const Color(0xff474747),
                      minimumSize: const Size(120, 45),
                      padding: const EdgeInsets.symmetric(horizontal: 16),
                      shape: const RoundedRectangleBorder(
                        borderRadius: BorderRadius.all(
                          Radius.circular(2),
                        ),
                      ),
                    ),
                    child: Text(
                      exit,
                      style: const TextStyle(fontSize: 20),
                    ),
                    onPressed: () {
                      SystemNavigator.pop();
                    },
                  ),
                ),
              ),
            ],
          ),
        ));
  }
}

Here is where I check what language preferences user selected and get it from a constants.dart file.

code:

final box = Hive.box(Constants.prefSettings);
final dropdownValueProvider = StateProvider<List<String>>(
  (ref) => ['EN', 'SW'],
);
var languageIndexProvider = StateProvider<int>(
  (ref) => box.get(Constants.prefLanguage, defaultValue: 0),
);
var passcodeValueProvider = StateProvider<bool>(
  (ref) => box.get(Constants.prefPassword, defaultValue: false),
);
var reminderValueProvider = StateProvider<bool>(
  (ref) => box.get(Constants.prefNoties, defaultValue: false),
);
final languagesProvider = StateProvider<Map<int, Map<String, String>>>(
  (ref) => Constants.languages,
);
final addPageTypeProvider = StateProvider<Map<int, List<String>>>(
  (ref) => Constants.addPageType,
);
final editPageTypeProvider = StateProvider<Map<int, List<String>>>(
  (ref) => Constants.editPageType,
);

Here is where I handle main budget file state:

final fileManager = FileManager();
final indexValueProvider = StateProvider<int>((ref) => 0);

final asyncBudgetProvider =
    AsyncNotifierProvider<BudgetNotifier, Map<String, List<String>>>(
  () => BudgetNotifier(),
);

class BudgetNotifier extends AsyncNotifier<Map<String, List<String>>> {
  //* loads initial data if there is any
  Future<Map<String, List<String>>> readBudget() async {
    final budgetFile = await fileManager.readFromMainBudgetFile(
      Constants.keyMainFileName,
    );
    return budgetFile;
  }

  //*Uses the initial data to build the state
  @override
  FutureOr<Map<String, List<String>>> build() async {
    return readBudget();
  }

  //*Check if file exists
  Future<bool> isExist(String fileName) async {
    var isExist = await fileManager.checkIfFileExists(fileName);

    return isExist;
  }

  //* Saves budget
  Future<Map<String, List<String>>> saveBudget(
      {String? year, String? month}) async {
    year ??= DateFormat('yyyy').format(DateTime.now());
    month ??= DateFormat('MMMM').format(DateTime.now());

    String fileName = "$year$month.txt";
    String dataToWrite =
        "$year ${Constants.keySeparator} $month ${Constants.keySeparator} $fileName \n";
    fileManager.addNewData(
      dataToWrite: dataToWrite,
      fileName: Constants.keyMainFileName,
    );
    return readBudget();
  }

  //* Deletes a line from budget
  Future<void> deleteSpecificLine({
    required String fileName,
    required int lineNumber,
  }) async {
    state = const AsyncValue.loading();

    state = await AsyncValue.guard(() {
      fileManager.deleteSpecificLine(
        fileName: fileName,
        lineNumber: lineNumber,
      );
      return readBudget();
    });
  }

  // //* Deletes the budget from the device
  // Future<void> deleteBudget(String budgetName) async {

  //   fileManager.deleteFile(fileName: budgetName);
  //   log('deleted?');
  //   return readBudget();
  // }
}

Here is where I handle the separate file that stores all the transactions:

final fileManager = FileManager();

//*Provides a way to watch the data
final asyncTransactionProvider = AsyncNotifierProvider.autoDispose
    .family<TransactionNotifier, Map<String, List<String>>, int?>(
  () => TransactionNotifier(),
);

class TransactionNotifier
    extends AutoDisposeFamilyAsyncNotifier<Map<String, List<String>>, int?> {
  //* loads initial data if there is any
  Future<Map<String, List<String>>> _readData(int? index) async {
    final budgetFile =
        await fileManager.readFromMainBudgetFile(Constants.keyMainFileName);
    index ??= await fileManager.getCurrentMonthIndex(
      Constants.keyMainFileName,
    );

    final data = await fileManager.readFromBudgetFile(
      budgetFile[Constants.keyFileName]![index],
    );
    return data;
  }

  //* Uses the initial data to build the state
  @override
  FutureOr<Map<String, List<String>>> build(int? arg) {
    return _readData(arg);
  }

  //* add a new line of data
  Future<void> addData(String data, String fileName, int index) async {
    state = const AsyncValue.loading();

    state = await AsyncValue.guard(() {
      fileManager.addNewData(
        dataToWrite: data,
        fileName: fileName,
      );
      return _readData(index);
    });
  }

  //* edit a line of data
  Future<void> editALine({
    required String data,
    required String fileName,
    required int lineNumber,
    required int budgetIndex,
  }) async {
    state = const AsyncValue.loading();

    state = await AsyncValue.guard(() {
      fileManager.editSpecificLine(
        fileName: fileName,
        lineNumber: lineNumber,
        newData: data,
      );
      return _readData(budgetIndex);
    });
  }

  //* Delete a line of data
  Future<void> deleteSpecificLine({
    required String fileName,
    required int lineNumber,
    required int budgetIndex,
  }) async {
    state = const AsyncValue.loading();

    state = await AsyncValue.guard(() {
      fileManager.deleteSpecificLine(
        fileName: fileName,
        lineNumber: lineNumber,
      );
      return _readData(budgetIndex);
    });
  }

  //* Deletes the budget from the device
  Future<void> deleteBudget(String budgetName, int index) async {
    state = const AsyncValue.loading();

    state = await AsyncValue.guard(() {
      fileManager.deleteFile(fileName: budgetName);
      return _readData(index);
    });
  }
}

The UI updates just fine when the language is default.

I thought maybe the way I was updating the state was wrong, so I tried updating the state used the function .copyWithPrevious

Just clarifying that the language of the app does change when I change it, the problem is when I try adding a transaction afterwards.

Related Questions