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.