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.
I figured out what my issue was, on the home tab page i was passing the wrong index to the add transaction page and so the state was refreshing something different which resulted in it not updating the UI
added the following variable on the HomeTab:
and passed it to the navigator':