feat: use less Futures

This commit is contained in:
Matyáš Caras 2024-06-30 11:30:07 +02:00
parent 1855ed4e6b
commit 07908797d5
Signed by: hernik
GPG key ID: 2A3175F98820C5C6
16 changed files with 139 additions and 91 deletions

View file

@ -1,5 +1,6 @@
# newVersion # newVersion
- Upgrade dependencies - Upgrade dependencies
- Use less `await`s in WalletManager class
# 1.1.1 # 1.1.1
- Removed deprecated code - Removed deprecated code

View file

@ -18,11 +18,11 @@ void main() {
group("Test classes and API", () { group("Test classes and API", () {
test("Test wallet operations", () async { test("Test wallet operations", () async {
expect( expect(
(await WalletManager.listWallets()).length, WalletManager.listWallets().length,
equals(0), equals(0),
); // check that there are no other wallets ); // check that there are no other wallets
await WalletManager.saveWallet(Wallet.empty); WalletManager.saveWallet(Wallet.empty);
var w = (await WalletManager.listWallets()).firstOrNull; var w = WalletManager.listWallets().firstOrNull;
expect(w, isNotNull); // check that the wallet was successfully saved expect(w, isNotNull); // check that the wallet was successfully saved
expect(w!.categories.length, equals(1)); expect(w!.categories.length, equals(1));
w.categories.add( w.categories.add(
@ -43,8 +43,8 @@ void main() {
id: w.nextId, id: w.nextId,
), ),
); // create test entry ); // create test entry
await WalletManager.saveWallet(w); // save again WalletManager.saveWallet(w); // save again
w = await WalletManager.loadWallet(w.name); // try loading manually w = WalletManager.loadWallet(w.name); // try loading manually
final e = w.entries.where((element) => element.id == testId).firstOrNull; final e = w.entries.where((element) => element.id == testId).firstOrNull;
expect( expect(
e, e,
@ -55,9 +55,9 @@ void main() {
equals(1), equals(1),
); // check that the category exists too ); // check that the category exists too
await WalletManager.deleteWallet(w); WalletManager.deleteWallet(w);
expect( expect(
(await WalletManager.listWallets()).length, WalletManager.listWallets().length,
equals(0), equals(0),
); );
}); });
@ -66,7 +66,7 @@ void main() {
group("Test app functionality:", () { group("Test app functionality:", () {
testWidgets('First-time setup', (WidgetTester tester) async { testWidgets('First-time setup', (WidgetTester tester) async {
// Delete all data // Delete all data
await WalletManager.deleteAllData(); WalletManager.deleteAllData();
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget( await tester.pumpWidget(
const MyApp( const MyApp(
@ -113,13 +113,12 @@ void main() {
testWidgets('Test rendering of entries', (WidgetTester tester) async { testWidgets('Test rendering of entries', (WidgetTester tester) async {
// Delete all data // Delete all data
await WalletManager.deleteAllData(); WalletManager.deleteAllData();
expect((await WalletManager.listWallets()).length, equals(0)); expect(WalletManager.listWallets().length, equals(0));
// Create test data // Create test data
final w = Wallet.empty; Wallet.empty.createTestEntries();
await w.createTestEntries(); expect(WalletManager.listWallets().length, equals(1));
expect((await WalletManager.listWallets()).length, equals(1));
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget( await tester.pumpWidget(

View file

@ -147,7 +147,7 @@ class Wallet {
/// ///
/// All [WalletSingleEntry]s will have their category reassigned /// All [WalletSingleEntry]s will have their category reassigned
/// to the default *No category* /// to the default *No category*
Future<void> removeCategory(WalletCategory category) async { void removeCategory(WalletCategory category) {
// First remove the category from existing entries // First remove the category from existing entries
for (final entryToChange for (final entryToChange
in entries.where((element) => element.category.id == category.id)) { in entries.where((element) => element.category.id == category.id)) {
@ -157,7 +157,7 @@ class Wallet {
// Remove the category // Remove the category
categories.removeWhere((element) => element.id == category.id); categories.removeWhere((element) => element.id == category.id);
// Save // Save
await WalletManager.saveWallet(this); WalletManager.saveWallet(this);
} }
/// Returns the current balance /// Returns the current balance
@ -216,7 +216,7 @@ class Wallet {
); );
/// Creates test data used for debugging purposes /// Creates test data used for debugging purposes
Future<void> createTestEntries() async { void createTestEntries() {
entries.clear(); entries.clear();
recurringEntries.clear(); recurringEntries.clear();
final random = Random(); final random = Random();
@ -274,6 +274,6 @@ class Wallet {
); );
// save and reload // save and reload
await WalletManager.saveWallet(this); WalletManager.saveWallet(this);
} }
} }

View file

@ -11,10 +11,17 @@ import 'package:prasule/main.dart';
/// Used for [Wallet]-managing operations /// Used for [Wallet]-managing operations
class WalletManager { class WalletManager {
/// Currently selected wallet
static Wallet? selectedWallet;
/// Path to the directory with wallet files
///
/// Saved beforehand so we don't have to use async everywhere
static late String walletPath;
/// Returns a list of all [Wallet]s /// Returns a list of all [Wallet]s
static Future<List<Wallet>> listWallets() async { static List<Wallet> listWallets() {
final path = final path = Directory(walletPath);
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
if (!path.existsSync()) { if (!path.existsSync()) {
path.createSync(); path.createSync();
} }
@ -22,7 +29,7 @@ class WalletManager {
for (final w for (final w
in path.listSync().map((e) => e.path.split("/").last).toList()) { in path.listSync().map((e) => e.path.split("/").last).toList()) {
try { try {
wallets.add(await loadWallet(w)); wallets.add(loadWallet(w));
} catch (e) { } catch (e) {
logger.e(e); logger.e(e);
// TODO: do something with unreadable wallets // TODO: do something with unreadable wallets
@ -32,9 +39,8 @@ class WalletManager {
} }
/// Deletes all [Wallet]s /// Deletes all [Wallet]s
static Future<void> deleteAllData() async { static void deleteAllData() {
final path = final path = Directory(walletPath);
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
if (!path.existsSync()) { if (!path.existsSync()) {
return; return;
} }
@ -124,10 +130,10 @@ class WalletManager {
d = File(filePath).readAsStringSync(); d = File(filePath).readAsStringSync();
} }
final w = Wallet.fromJson(jsonDecode(d) as Map<String, dynamic>); final w = Wallet.fromJson(jsonDecode(d) as Map<String, dynamic>);
if (await WalletManager.exists(w.name)) { if (WalletManager.exists(w.name)) {
throw Exception("Wallet already exists!"); throw Exception("Wallet already exists!");
} }
await WalletManager.saveWallet( WalletManager.saveWallet(
w, w,
); );
} }
@ -166,15 +172,14 @@ class WalletManager {
} }
/// Loads and returns a single [Wallet] by name /// Loads and returns a single [Wallet] by name
static Future<Wallet> loadWallet(String name) async { static Wallet loadWallet(String name) {
final path = final path = Directory(walletPath);
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
final wallet = File("${path.path}/$name"); final wallet = File("${path.path}/$name");
if (!path.existsSync()) { if (!path.existsSync()) {
path.createSync(); path.createSync();
} }
if (!wallet.existsSync()) { if (!wallet.existsSync()) {
return Future.error("Wallet does not exist"); throw Exception("Wallet does not exist");
} }
return Wallet.fromJson( return Wallet.fromJson(
jsonDecode(wallet.readAsStringSync()) as Map<String, dynamic>, jsonDecode(wallet.readAsStringSync()) as Map<String, dynamic>,
@ -182,29 +187,26 @@ class WalletManager {
} }
/// Converts [Wallet] to JSON and saves it to AppData /// Converts [Wallet] to JSON and saves it to AppData
static Future<bool> saveWallet(Wallet w) async { static void saveWallet(Wallet w) {
final path = final path = Directory(walletPath);
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
final wallet = File("${path.path}/${w.name}"); final wallet = File("${path.path}/${w.name}");
if (!path.existsSync()) { if (!path.existsSync()) {
path.createSync(); path.createSync();
} }
// if (!wallet.existsSync()) return false; // if (!wallet.existsSync()) return false;
wallet.writeAsStringSync(jsonEncode(w.toJson())); wallet.writeAsStringSync(jsonEncode(w.toJson()));
return true;
} }
/// Deletes the corresponding [Wallet] file /// Deletes the corresponding [Wallet] file
static Future<void> deleteWallet(Wallet w) async { static void deleteWallet(Wallet w) {
final path = final path = Directory(walletPath);
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
File("${path.path}/${w.name}").deleteSync(); File("${path.path}/${w.name}").deleteSync();
} }
/// Checks if the wallet exists /// Checks if the wallet exists
static Future<bool> exists(String name) async { static bool exists(String name) {
return File( return File(
"${(await getApplicationDocumentsDirectory()).path}/wallets/$name", "$walletPath/$name",
).existsSync(); ).existsSync();
} }
} }

View file

@ -6,8 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/util/color_schemes.g.dart'; import 'package:prasule/util/color_schemes.g.dart';
import 'package:prasule/views/home.dart'; import 'package:prasule/views/home.dart';
import 'package:prasule/views/initialization_screen.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
var _materialYou = false; var _materialYou = false;
@ -20,6 +23,7 @@ void main() async {
} }
_materialYou = s.getBool("useMaterialYou") ?? true; _materialYou = s.getBool("useMaterialYou") ?? true;
runApp(const MyApp()); runApp(const MyApp());
} }
@ -39,6 +43,7 @@ class MyApp extends StatelessWidget {
/// Override locale, used for testing /// Override locale, used for testing
final Locale? locale; final Locale? locale;
// This widget is the root of your application. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return (Platform.isAndroid) return (Platform.isAndroid)
@ -66,7 +71,7 @@ class MyApp extends StatelessWidget {
colorScheme: colorScheme:
_materialYou ? dark ?? darkColorScheme : darkColorScheme, _materialYou ? dark ?? darkColorScheme : darkColorScheme,
), ),
home: const HomeView(), home: const InitializationScreen(),
); );
}, },
) )
@ -85,7 +90,7 @@ class MyApp extends StatelessWidget {
...GlobalCupertinoLocalizations.delegates, ...GlobalCupertinoLocalizations.delegates,
], ],
title: 'Prašule', title: 'Prašule',
home: HomeView(), home: InitializationScreen(),
), ),
); );
} }

View file

@ -236,9 +236,8 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
return; return;
} }
widget.w.entries.add(newEntry); widget.w.entries.add(newEntry);
WalletManager.saveWallet(widget.w).then( WalletManager.saveWallet(widget.w); // TODO loading circle?
(value) => Navigator.of(context).pop(widget.w), Navigator.of(context).pop(widget.w);
); // TODO loading circle?
}, },
), ),
], ],

View file

@ -323,9 +323,8 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
return; return;
} }
widget.w.recurringEntries.add(newEntry); widget.w.recurringEntries.add(newEntry);
WalletManager.saveWallet(widget.w).then( WalletManager.saveWallet(widget.w); // TODO loading circle?
(value) => Navigator.of(context).pop(widget.w), Navigator.of(context).pop(widget.w);
); // TODO loading circle?
}, },
), ),
], ],

View file

@ -67,8 +67,8 @@ class _GraphViewState extends State<GraphView> {
final availableYears = <WheelChoice<int>>[]; final availableYears = <WheelChoice<int>>[];
Future<void> loadWallet() async { void loadWallet() {
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
if (wallets.isEmpty && mounted) { if (wallets.isEmpty && mounted) {
unawaited( unawaited(
Navigator.of(context) Navigator.of(context)
@ -224,7 +224,7 @@ class _GraphViewState extends State<GraphView> {
), ),
), ),
); );
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
logger.i(wallets.length); logger.i(wallets.length);
selectedWallet = wallets.last; selectedWallet = wallets.last;
setState(() {}); setState(() {});
@ -250,7 +250,7 @@ class _GraphViewState extends State<GraphView> {
) )
.then((value) async { .then((value) async {
selectedWallet = selectedWallet =
await WalletManager.loadWallet(selectedWallet!.name); WalletManager.loadWallet(selectedWallet!.name);
final s = await SharedPreferences.getInstance(); final s = await SharedPreferences.getInstance();
chartType = s.getInt("monthlygraph") ?? 2; chartType = s.getInt("monthlygraph") ?? 2;
setState(() {}); setState(() {});

View file

@ -63,8 +63,8 @@ class _HomeViewState extends State<HomeView> {
loadWallet(); loadWallet();
} }
Future<void> loadWallet() async { void loadWallet() {
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
if (wallets.isEmpty && mounted) { if (wallets.isEmpty && mounted) {
unawaited( unawaited(
Navigator.of(context) Navigator.of(context)
@ -82,7 +82,7 @@ class _HomeViewState extends State<HomeView> {
return PopScope( return PopScope(
canPop: !_searchActive, // don't pop when we just want canPop: !_searchActive, // don't pop when we just want
// to deactivate searchfield // to deactivate searchfield
onPopInvoked: (b) { onPopInvokedWithResult: (b, d) {
if (b) return; if (b) return;
_searchActive = false; _searchActive = false;
_filter = ""; _filter = "";
@ -105,13 +105,12 @@ class _HomeViewState extends State<HomeView> {
onTap: () { onTap: () {
// debug option to quickly fill a wallet with data // debug option to quickly fill a wallet with data
if (selectedWallet == null) return; if (selectedWallet == null) return;
selectedWallet!.createTestEntries().then((_) { selectedWallet!.createTestEntries();
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
platformRoute( platformRoute(
(p0) => const HomeView(), (p0) => const HomeView(),
), ),
); );
});
}, },
), ),
SpeedDialChild( SpeedDialChild(
@ -194,7 +193,7 @@ class _HomeViewState extends State<HomeView> {
), ),
), ),
); );
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
selectedWallet = wallets.last; selectedWallet = wallets.last;
setState(() {}); setState(() {});
return; return;
@ -248,9 +247,9 @@ class _HomeViewState extends State<HomeView> {
(context) => const SettingsView(), (context) => const SettingsView(),
), ),
) )
.then((value) async { .then((value) {
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
selectedWallet = await WalletManager.loadWallet( selectedWallet = WalletManager.loadWallet(
selectedWallet!.name, selectedWallet!.name,
); );
setState(() {}); setState(() {});
@ -634,7 +633,10 @@ class _HomeViewState extends State<HomeView> {
(c) => const TessdataListView(), (c) => const TessdataListView(),
), ),
) )
.then((value) => Navigator.of(c).pop()); .then((value) {
if (!c.mounted) return;
Navigator.of(c).pop();
});
}, },
), ),
PlatformButton( PlatformButton(
@ -741,7 +743,7 @@ class _HomeViewState extends State<HomeView> {
); );
if (newEntry == null) return; if (newEntry == null) return;
selectedWallet!.entries.add(newEntry); selectedWallet!.entries.add(newEntry);
await WalletManager.saveWallet(selectedWallet!); WalletManager.saveWallet(selectedWallet!);
setState(() {}); setState(() {});
}, },
child: const Text("Ok"), child: const Text("Ok"),

View file

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/views/home.dart';
import 'package:prasule/views/setup.dart';
class InitializationScreen extends StatefulWidget {
const InitializationScreen({super.key});
@override
State<InitializationScreen> createState() => _InitializationScreenState();
}
class _InitializationScreenState extends State<InitializationScreen> {
@override
void initState() {
super.initState();
getApplicationDocumentsDirectory().then((v) {
WalletManager.walletPath = v.path;
if (!mounted) return;
final wallets = WalletManager.listWallets();
if (wallets.isEmpty && mounted) {
Navigator.of(context)
.pushReplacement(platformRoute((c) => const SetupView()));
return;
}
Navigator.of(context)
.pushReplacement(platformRoute((c) => const HomeView()));
});
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Align(
child: CircularProgressIndicator(),
),
);
}
}

View file

@ -44,8 +44,8 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
loadWallet(); loadWallet();
} }
Future<void> loadWallet() async { void loadWallet() {
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
if (wallets.isEmpty && mounted) { if (wallets.isEmpty && mounted) {
unawaited( unawaited(
Navigator.of(context) Navigator.of(context)
@ -91,7 +91,7 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
), ),
), ),
); );
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
selectedWallet = wallets.last; selectedWallet = wallets.last;
setState(() {}); setState(() {});
return; return;
@ -116,7 +116,7 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
) )
.then((value) async { .then((value) async {
selectedWallet = selectedWallet =
await WalletManager.loadWallet(selectedWallet!.name); WalletManager.loadWallet(selectedWallet!.name);
}); });
} else if (value == AppLocalizations.of(context).about) { } else if (value == AppLocalizations.of(context).about) {
showAbout(context); showAbout(context);

View file

@ -39,7 +39,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
} }
Future<void> loadWallet() async { Future<void> loadWallet() async {
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
if (wallets.isEmpty && mounted) { if (wallets.isEmpty && mounted) {
unawaited( unawaited(
Navigator.of(context) Navigator.of(context)
@ -82,7 +82,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
), ),
), ),
); );
wallets = await WalletManager.listWallets(); wallets = WalletManager.listWallets();
logger.i(wallets.length); logger.i(wallets.length);
selectedWallet = wallets.last; selectedWallet = wallets.last;
setState(() {}); setState(() {});
@ -122,7 +122,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
IconButton( IconButton(
onPressed: () async { onPressed: () {
selectedWallet!.categories.add( selectedWallet!.categories.add(
WalletCategory( WalletCategory(
name: AppLocalizations.of(context) name: AppLocalizations.of(context)
@ -137,7 +137,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
), ),
), ),
); );
await WalletManager.saveWallet(selectedWallet!); WalletManager.saveWallet(selectedWallet!);
setState(() {}); setState(() {});
}, },
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
@ -206,7 +206,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
), ),
), ),
); );
await WalletManager.saveWallet(selectedWallet!); WalletManager.saveWallet(selectedWallet!);
setState(() {}); setState(() {});
}, },
child: Container( child: Container(
@ -226,8 +226,8 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
), ),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.cancel), icon: const Icon(Icons.cancel),
onPressed: () async { onPressed: () {
await selectedWallet!.removeCategory( selectedWallet!.removeCategory(
selectedWallet!.categories[i], selectedWallet!.categories[i],
); );
setState(() {}); setState(() {});
@ -243,11 +243,11 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
builder: (c) => AlertDialog.adaptive( builder: (c) => AlertDialog.adaptive(
actions: [ actions: [
TextButton( TextButton(
onPressed: () async { onPressed: () {
if (controller.text.isEmpty) return; if (controller.text.isEmpty) return;
selectedWallet!.categories[i].name = selectedWallet!.categories[i].name =
controller.text; controller.text;
await WalletManager.saveWallet( WalletManager.saveWallet(
selectedWallet!, selectedWallet!,
); );
if (!context.mounted) return; if (!context.mounted) return;

View file

@ -117,7 +117,7 @@ class _SettingsViewState extends State<SettingsView> {
description: description:
Text(AppLocalizations.of(context).exportSingleDesc), Text(AppLocalizations.of(context).exportSingleDesc),
onPressed: (ctx) async { onPressed: (ctx) async {
final all = await WalletManager.listWallets(); final all = WalletManager.listWallets();
if (!ctx.mounted) return; if (!ctx.mounted) return;
final w = await showAdaptiveDialog<String>( final w = await showAdaptiveDialog<String>(
context: ctx, context: ctx,

View file

@ -131,7 +131,7 @@ class _SetupViewState extends State<SetupView> {
next: Text(AppLocalizations.of(context).next), next: Text(AppLocalizations.of(context).next),
back: Text(AppLocalizations.of(context).back), back: Text(AppLocalizations.of(context).back),
done: Text(AppLocalizations.of(context).finish), done: Text(AppLocalizations.of(context).finish),
onDone: () async { onDone: () {
if (_nameController.text.isEmpty) { if (_nameController.text.isEmpty) {
unawaited( unawaited(
showMessage( showMessage(
@ -141,7 +141,7 @@ class _SetupViewState extends State<SetupView> {
); );
return; return;
} }
if (await WalletManager.exists(_nameController.text) && if (WalletManager.exists(_nameController.text) &&
context.mounted) { context.mounted) {
unawaited( unawaited(
showMessage( showMessage(
@ -157,7 +157,7 @@ class _SetupViewState extends State<SetupView> {
categories: categories, categories: categories,
starterBalance: double.parse(_balanceController.text), starterBalance: double.parse(_balanceController.text),
); );
await WalletManager.saveWallet(wallet); WalletManager.saveWallet(wallet);
if (widget.newWallet && context.mounted) { if (widget.newWallet && context.mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();

View file

@ -409,10 +409,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_tesseract_ocr name: flutter_tesseract_ocr
sha256: f6f55804d2c1fe385dba150a2c1c8641e5add2694fdaa72d835aca051f9faa75 sha256: a45b76842f9670a3b69a4d1276367926aa8428d33e439e80541e699b7c5fb96b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.25" version: "0.4.23"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter

View file

@ -28,7 +28,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_slidable: ^3.0.0 flutter_slidable: ^3.0.0
flutter_speed_dial: ^7.0.0 flutter_speed_dial: ^7.0.0
flutter_tesseract_ocr: ^0.4.23 flutter_tesseract_ocr: 0.4.23
fluttertoast: ^8.2.4 fluttertoast: ^8.2.4
grouped_list: ^5.1.2 grouped_list: ^5.1.2
intl: any intl: any