- FinGenie Repository Documentation
- Flutter Application Architecture
- Services
- BLoC Pattern Implementation
- Development Setup
- Contributing
- License
FinGenie is a not-so-sophisticated financial management application that combines expense tracking, group expense management, and AI-powered receipt scanning. The application uses Flutter for cross-platform development and follows clean architecture principles.
- Flutter for UI development
- BLoC pattern for state management
- Hive for local storage
- Dio for network requests
- Google ML Kit for OCR
- Gemini AI for receipt analysis
void main() async {
try {
// Initialize core services
// Initialize repositories and services
// Determine initial screen based on auth state and attach app to root
} catch (e) {
throw;
}
} WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await dotenv.load(fileName: ".env"); await ContactsService.initializeHive();
await AuthRepository.init();final userBox = await Hive.openBox<UserModel>('userBox');
final currentUser = userBox.get('current_user');
Widget initialScreen = currentUser?.isLoggedIn == true
? HomeScreen()
: IntroScreen();
runApp(MyApp(initialScreen: initialScreen));- User authentication (login/signup)
- Token management
- Local user data storage
- Profile updates
// In your main.dart or startup code
await AuthRepository.init();final authRepo = AuthRepository();
try {
final user = await authRepo.signUp(SignUpRequest(
email: '[email protected]',
password: 'password123',
name: 'John Doe',
phoneNumber: '+1234567890'
));
// User is now signed up and logged in
} catch (e) {
// Handle signup error
}try {
final user = await authRepo.login(LoginRequest(
email: '[email protected]',
password: 'password123'
));
// User is now logged in
} catch (e) {
// Handle login error
}final currentUser = authRepo.getCurrentUser();
if (currentUser != null) {
print('User is logged in: ${currentUser.name}');
}final updatedUser = await authRepo.updateProfileLocally(
currency: 'USD',
age: 25,
occupation: 'Developer',
monthlyIncome: 5000.0
);await authRepo.logout();
// User is now logged outRetrieves the currently logged-in user from local storage:
- Opens user box if not already open
- Returns stored user data or null if no user is found
Debug function to verify user data persistence:
- Checks if user data exists in storage
- Logs user details for verification
- Reports any persistence issues
Updates user profile information in local storage:
- Updates currency preferences
- Updates demographic information
- Updates financial information
- Persists changes to Hive storage
The Group Repository manages all group-related operations including creation, member management, and data synchronization.
Retrieves all groups associated with the current user:
- Fetches groups from
/api/v1/groups - Processes member data for each group
- Fetches user profiles for all group members
- Constructs complete group models with member information
sequenceDiagram
participant App
participant GroupRepo
participant API
participant AuthRepo
App->>GroupRepo: fetchGroups()
GroupRepo->>AuthRepo: getStoredToken()
AuthRepo-->>GroupRepo: token
GroupRepo->>API: GET /api/v1/groups
API-->>GroupRepo: groups data
loop For each group
loop For each member
GroupRepo->>API: GET user profile
API-->>GroupRepo: member data
end
end
GroupRepo-->>App: List<GroupModel>
Usage Example:
try {
final groups = await groupRepository.fetchGroups();
// Process retrieved groups
} catch (e) {
// Handle fetch error
}Adds new members to an existing group:
- Validates member IDs
- Makes individual requests for each member
- Handles partial success scenarios
- Updates group member list
Error Handling:
- Invalid member IDs
- Permission issues
- Network failures
- Partial success handling
Usage Example:
try {
await groupRepository.addGroupMembers(
groupId: 'group123',
memberIds: ['user1', 'user2']
);
// Members added successfully
} catch (e) {
// Handle member addition failure
}Searches for a user by phone number:
- Makes API request to user search endpoint
- Processes response to extract user ID
- Handles user not found scenarios
Usage Example:
try {
final userId = await groupRepository.findUserByPhone('+1234567890');
// Use found user ID
} catch (e) {
// Handle user not found
}Creates a new group with specified parameters:
- Validates input parameters
- Sets up default values based on user preferences
- Creates group on server
- Processes response and creates local group model
- Handles member addition if initial members provided
Error Handling:
- Input validation
- Server errors
- Member addition failures
- Currency validation
sequenceDiagram
participant App
participant GroupRepo
participant AuthRepo
participant API
App->>GroupRepo: createGroup(params)
GroupRepo->>AuthRepo: getCurrentUser()
AuthRepo-->>GroupRepo: user data
GroupRepo->>API: POST /api/v1/groups
API-->>GroupRepo: new group data
loop For each initial member
GroupRepo->>API: POST /members
API-->>GroupRepo: member added
end
GroupRepo-->>App: GroupModel
Usage Example:
try {
final group = await groupRepository.createGroup(
name: 'Weekend Trip',
tag: 'trip',
securityDepositRequired: true,
securityDeposit: 1000.0,
autoSettlement: true,
initialMembers: ['user1', 'user2']
);
// Handle successful group creation
} catch (e) {
// Handle creation failure
}Retrieves detailed user profile information:
- Makes authenticated request to profile endpoint
- Processes user data
- Returns formatted user profile
Fetches comprehensive group information:
- Retrieves basic group data
- Fetches all member profiles
- Constructs complete group model with member details
- Handles missing member data gracefully
The Split Share Repository manages expense sharing calculations and settlements.
Retrieves all split shares for the current user:
- Fetches shares from API
- Processes payment statuses
- Calculates interest if applicable
- Returns formatted share list
sequenceDiagram
participant App
participant SplitShareRepo
participant API
App->>SplitShareRepo: fetchSplitShares()
SplitShareRepo->>API: GET /split-shares
API-->>SplitShareRepo: shares data
SplitShareRepo->>SplitShareRepo: calculate interest
SplitShareRepo-->>App: List<SplitShare>
Usage Example:
try {
final shares = await splitShareRepository.fetchSplitShares();
// Process split shares
} catch (e) {
// Handle fetch error
}The Share Repository manages expense sharing between users, including split calculations, balances, and settlements.
Retrieves all active share splits for the current user:
- Fetches all splits from
/api/v1/split-shares - Calculates pending amounts
- Processes payment status
- Returns formatted split list
sequenceDiagram
participant App
participant ShareRepo
participant API
participant AuthRepo
App->>ShareRepo: fetchSplitShares()
ShareRepo->>AuthRepo: getStoredToken()
AuthRepo-->>ShareRepo: token
ShareRepo->>API: GET /split-shares
API-->>ShareRepo: shares data
ShareRepo->>ShareRepo: Calculate totals
ShareRepo-->>App: List<SplitShare>
Usage Example:
try {
final shareRepo = SplitShareRepository(dio: dio, apiUrl: apiUrl);
final shares = await shareRepo.fetchSplitShares();
// Process retrieved shares
} catch (e) {
// Handle fetch error
}The contact management system combines the ContactService for handling operations and the HiveContact adapter for data persistence. This system manages device contacts, local caching, and data synchronization.
graph TD
A[Device Contacts] -->|Read| B[Contact Service]
B -->|Convert| C[Contact Adapter]
C -->|Store| D[Hive Storage]
D -->|Retrieve| C
C -->|Convert Back| B
B -->|Display| E[App]
Manages contact operations and synchronization:
class ContactsService {
static const String contactsBoxName = 'contacts_box';
static const String settingsBoxName = 'settings_box';
// Initialize the service
static Future<void> initializeHive() async {
await Hive.initFlutter();
Hive.registerAdapter(HiveContactAdapter());
Hive.registerAdapter(HivePhoneAdapter());
Hive.registerAdapter(HiveEmailAdapter());
}
}Handles data conversion and storage format:
@HiveType(typeId: 10)
class HiveContact extends HiveObject {
@HiveField(0) late String id;
@HiveField(1) late String displayName;
@HiveField(2) late List<HivePhone> phones;
@HiveField(3) late List<HiveEmail> emails;
}// In Contact Service
Future<List<Contact>> fetchAndCacheContacts() async {
if (await shouldRefreshContacts()) {
final contacts = await FlutterContacts.getContacts(
withProperties: true
);
// Convert and cache using adapter
for (var contact in contacts) {
final hiveContact = HiveContact.fromContact(contact);
await contactsBox.put(hiveContact.id, hiveContact);
}
return contacts;
}
return getCachedContacts();
}// In HiveContact Adapter
factory HiveContact.fromContact(Contact contact) {
return HiveContact(
id: contact.id,
displayName: contact.displayName
)..phones = contact.phones
.map((p) => HivePhone(number: p.number))
.toList();
}// In Contact Service
Future<List<Contact>> getCachedContacts() async {
final contactsBox = Hive.box<HiveContact>(contactsBoxName);
return contactsBox.values
.map((hiveContact) => hiveContact.toContact())
.toList();
}// In your app initialization
await ContactsService.initializeHive();
final contactService = ContactsService();try {
final contacts = await contactService.fetchAndCacheContacts();
// Use contacts in UI
} catch (e) {
// Handle error
}final cachedContacts = await contactService.getCachedContacts();
// Use cached contactsif (await contactService.shouldRefreshContacts()) {
// Refresh contacts
await contactService.fetchAndCacheContacts();
}await contactService.clearCachedContacts();
// Cache is cleared, next fetch will get fresh datasequenceDiagram
participant User
participant ImagePicker
participant MLKit
participant Gemini
participant UI
User->>ImagePicker: Select/Capture Image
ImagePicker->>MLKit: Process Image
MLKit->>Gemini: Extract Text
Gemini->>UI: Structured JSON
UI->>User: Display Results
- User can select image from gallery or capture with camera
- Image is optimized (1800px width, 85% quality)
- Selected image is displayed in UI
Uses Google ML Kit to:
- Convert image to text
- Extract text blocks
- Combine lines into processable text
Sends text to Gemini with prompt to extract:
- Store name
- Date
- Total amount
- Individual items
- Payment method
Gemini returns structured JSON:
{
"store": "Store Name",
"date": "Purchase Date",
"total": "Total Amount",
"items": [
{
"name": "Item Name",
"price": "Price",
"quantity": "Quantity"
}
],
"paymentMethod": "Payment Method"
}- User opens receipt scanner
- Selects/captures receipt image
- System processes image for text
- Gemini analyzes text for receipt data
- UI displays formatted receipt with:
- Store details
- Item list
- Total amount
- Payment information
- Requires clear image quality
- Latin script support only
- Requires internet for Gemini analysis
- Depends on receipt format consistency
The application uses several BLoCs to manage state for different features:
- Authentication (Login/Signup)
- Contacts Management
- Group Management
- Expense Tracking
graph TD
A[UI Layer] -->|Events| B[BLoCs]
B -->|States| A
B -->|Requests| C[Repositories]
C -->|Data| B
Manages login state and authentication.
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthRepository _authRepository;
// Handle login submission
Future<void> _onLoginSubmitted(event, emit) async {
emit(LoginLoading());
try {
final user = await _authRepository.login(request);
emit(LoginSuccess(user));
} catch (error) {
emit(LoginFailure(error.toString()));
}
}
}States:
LoginInitial: Initial stateLoginLoading: During authenticationLoginSuccess: After successful loginLoginFailure: When login fails
Usage Example:
context.read<LoginBloc>().add(LoginSubmitted(
email: '[email protected]',
password: 'password'
));Handles new user registration.
class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
Future<void> _onSignUpSubmitted(event, emit) async {
emit(SignUpLoading());
try {
final user = await _authRepository.signUp(request);
emit(SignUpSuccess(user));
} catch (error) {
emit(SignUpFailure(error.toString()));
}
}
}States:
SignUpInitial: Starting stateSignUpLoading: During registrationSignUpSuccess: Registration successfulSignUpFailure: Registration failed
Manages device contacts and synchronization.
class ContactsBloc extends Bloc<ContactsEvent, ContactsState> {
Future<void> _onFetchContacts(event, emit) async {
if (!await Permission.contacts.request().isGranted) {
emit(state.copyWith(errorMessage: 'Permission denied'));
return;
}
// Fetch and process contacts
}
}Events:
FetchContactsEvent: Load contactsRefreshContactsEvent: Update contacts
States tracked:
- Loading state
- Contact list
- Error messages
Manages group operations and member management.
class GroupBloc extends Bloc<GroupEvent, GroupState> {
// Load groups
Future<void> _onLoadGroups(event, emit) async {
emit(state.copyWith(isLoading: true));
try {
final groups = await repository.fetchGroups();
emit(state.copyWith(groups: groups));
} catch (e) {
emit(state.copyWith(errorMessage: e.toString()));
}
}
// Create group
Future<void> _onCreateGroup(event, emit) async {
// Group creation logic
}
// Add members
Future<void> _onAddGroupMembers(event, emit) async {
// Member addition logic
}
}Events:
LoadGroups: Fetch groupsCreateGroup: Create new groupAddGroupMembers: Add members to group
States tracked:
- Group list
- Selected group
- Loading states
- Error messages
Manages expense tracking and settlements.
class ExpenseBloc extends Bloc<ExpenseEvent, ExpenseState> {
Future<void> _onLoadExpenses(event, emit) async {
emit(ExpenseLoading());
try {
final expenses = await loadExpenses();
emit(ExpenseLoaded(expenses));
} catch (e) {
emit(ExpenseError(e.toString()));
}
}
}Events:
LoadExpenses: Fetch expensesAddExpense: Create expenseSettleExpense: Settle expense
States:
ExpenseInitialExpenseLoadingExpenseLoadedExpenseError
BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
if (state is LoginLoading) {
return CircularProgressIndicator();
}
if (state is LoginSuccess) {
return HomeScreen();
}
return LoginForm();
},
)BlocBuilder<GroupBloc, GroupState>(
builder: (context, state) {
if (state.isLoading) {
return LoadingIndicator();
}
return ListView.builder(
itemCount: state.groups.length,
itemBuilder: (context, index) {
return GroupTile(group: state.groups[index]);
},
);
},
)- Flutter SDK (Latest stable version)
- Android Studio / VS Code
- Git
- A Google Cloud Platform account (for ML Kit)
- A Gemini API key
- Clone the repository:
git clone https://github.com/yourusername/fingenie.git
cd fingenie- Install dependencies:
flutter pub get- Run build runner for code generation:
# One-time build
flutter pub run build_runner build
# Watch for changes
flutter pub run build_runner watch
# Force rebuild if conflicts occur
flutter pub run build_runner build --delete-conflicting-outputs- Create a .env file in the root directory:
GEMINI_API_KEY=your_api_key_here
API_BASE_URL=your_api_base_url
- Run the development server:
flutter runflutter testWe welcome contributions to FinGenie! Please follow these steps:
- Fork the repository
- Create a new branch:
git checkout -b feature/your-feature-name - Make your changes
- Run tests:
flutter test - Commit your changes:
git commit -m 'Add some feature' - Push to the branch:
git push origin feature/your-feature-name - Submit a pull request
- Follow the official Dart style guide
- Use meaningful variable and function names
- Write comments for complex logic
- Include tests for new features
- Ensure your code follows the style guide
- Update the README.md with details of changes if needed
- The PR will be merged once you have the sign-off of at least one maintainer
This project is licensed under the MIT License - see the LICENSE file for details.