Code Review of Cashew App: Lessons from a Flutter App with 100k+ Downloads

This video provides a comprehensive analysis of the Cashew budgeting app, an open-source Flutter application with over 100,000 downloads and exceptional user reviews. While the app demonstrates remarkable business success, the codebase reveals critical lessons about technical debt and software engineering practices.

Below is an AI summary of the video. Here's the GitHub repository.

App Overview and Success Metrics

Cashew stands as a testament to what a single developer can achieve. The finance budgeting app boasts:

  • Over 100,000 downloads on Google Play
  • 4.9-star rating from 15,700+ reviews
  • Similar high ratings on the App Store
  • Featured multiple times on YouTube channels with substantial followings
  • Cross-platform availability (iOS, Android, Web)

The app was actively developed from September 2021 to October 2024, spanning over three years of continuous development—far exceeding the typical lifespan of side projects. However, the last release occurred in July 2024, suggesting potential development challenges.

Getting the App Running: Version Compatibility Challenges

The first hurdle in reviewing this codebase was simply getting it to run. The project's extensive dependencies created immediate compatibility issues with current Flutter versions. Rather than upgrading dozens of packages individually, the solution involved downgrading to Flutter 3.22 using tools like Puro for version management.

This approach highlights a critical lesson: when inheriting legacy codebases, sometimes working backward to a compatible environment is more efficient than forcing everything forward. The experience underscores the importance of version management tools for developers working across multiple projects.

Android setup required manual Gradle script updates, while iOS proved more straightforward after removing the old Podfile.lock and running pod install. Interestingly, the repository includes Firebase configuration files—a security concern for open-source projects that should be addressed through proper backend security measures.

Architecture and Dependencies Analysis

Package Dependencies

The pubspec.yaml reveals a complex dependency structure:

  • Database: Drift with SQLite for local storage
  • Authentication: Google Sign-In integration
  • Backend: Firebase packages for cloud synchronization
  • State Management: Provider (notably absent: dedicated solutions like BLoC or Riverpod)
  • Development: Build Runner for code generation

The absence of dedicated state management frameworks suggests custom-built solutions, which can work effectively when implemented properly but require careful architectural planning.

Codebase Scale

Using the cloc tool reveals staggering numbers:

  • 103,000 lines of Dart code (excluding empty lines and generated files)
  • Largest files include database tables and schema versions
  • Individual pages reaching nearly 5,000 lines of code
  • The addTransactionsPage.dart file alone contains massive complexity

Testing Infrastructure

Perhaps the most concerning discovery: the project contains no meaningful tests beyond the default Flutter project template. For a production application of this scale serving thousands of users, the absence of automated testing represents significant risk. Any code changes could potentially break existing functionality without detection.

Code Architecture Deep Dive

Global State Management Issues

The application relies heavily on global variables for dependency management, declared in files like databaseGlobal.dart. Dependencies such as SharedPreferences, database instances, and notification payloads are initialized as global variables rather than through proper dependency injection systems.

This approach creates several problems:

  • Unclear initialization order: Functions like loadCurrencyJSON set global variables without explicit return values
  • Mutable global state: Variables can be modified from anywhere in the application
  • Testing difficulties: Global state makes unit testing nearly impossible
  • Maintenance challenges: Dependencies between components become opaque

Navigation and State Updates

The app employs a complex system of global navigation keys defined in navigationFramework.dart. These keys enable widgets to force rebuilds of other widgets throughout the application using patterns like:

homepageStateKey.currentState.refreshState();

This approach creates problematic interdependencies where widgets can trigger rebuilds of seemingly unrelated components. The refreshState method typically contains empty setState() calls that force complete widget rebuilds—a brute-force approach to UI updates that can cause performance issues.

Main Application Structure

The main.dart file demonstrates inconsistent initialization patterns:

  • Some dependencies initialized as global variables in the main method
  • Others handled through inherited widgets at the widget tree's root
  • Error handling relies on runZonedGuarded primarily for development logging
  • No integration with production error monitoring services like Crashlytics or Sentry

UI Implementation Analysis

Homepage Complexity

The homepage widget exemplifies the architectural challenges throughout the codebase:

  • Build method spans nearly 200 lines
  • Complex conditional logic based on global app settings
  • Dynamic widget mapping using string keys
  • For loops within the build method creating widgets based on context

This monolithic approach means any state change potentially rebuilds the entire homepage, creating performance bottlenecks and making the code difficult to reason about.

The 5,000-Line Monster: AddTransactionsPage

The addTransactionsPage.dart file represents the extreme end of the complexity spectrum:

  • Over 5,000 lines in a single file
  • 44 setState() calls throughout the file
  • Massive build method spanning from line 1,050 to 2,218
  • Business logic, UI code, database operations, and animations all intertwined

The addTransactionLocked method demonstrates the architectural problems:

  1. Opens bottom sheets for user input
  2. Performs data validation
  3. Creates transaction objects
  4. Handles conditional logic for related transactions
  5. Updates the database
  6. Manages animations
  7. Sets notifications
  8. Updates global app settings
  9. Handles error cases

This single method mixing UI, business logic, database operations, and side effects makes testing and maintenance extremely challenging.

Database Implementation

Schema Complexity

The database implementation in tables.dart spans over 7,000 lines, containing:

  • Table definitions with extensive column specifications
  • Complex converter classes for data transformation
  • Migration strategies across multiple app versions
  • Intricate SQL queries for financial calculations

Migration Strategy Evolution

The migration code reveals evolving development practices:

  • Early migrations: Simple await calls without error handling
  • Later migrations: Try-catch blocks with print statements for debugging
  • Complex multi-step migrations for schema changes
  • Inconsistent error handling patterns

Query Complexity

Database queries demonstrate sophisticated financial calculations but raise maintainability concerns:

  • Methods like watchTotalNetBeforeStartsDateTransactionCategoryWithDay perform complex aggregations
  • Some queries depend on global app settings variables
  • Commented-out code suggests ongoing experimentation
  • Query optimization appears secondary to functionality

Folder Structure and Organization

The project's organization reflects organic growth rather than planned architecture:

  • struct folder: Contains miscellaneous utilities (data formatting, animations, Firebase auth, biometrics)
  • widgets/util folder: Another catch-all containing app links, debouncing, animations, mixins, file I/O
  • Inconsistent naming conventions
  • No clear separation between layers (presentation, domain, data)

This structure makes navigation difficult and suggests code was placed wherever convenient rather than following established architectural patterns.

Performance and Maintainability Concerns

State Management Issues

The combination of global mutable state, massive widget classes, and forced rebuilds creates several performance risks:

  • Unnecessary widget rebuilds due to global state changes
  • Complex interdependencies making optimization difficult
  • Memory leaks potential from global state retention
  • Difficult debugging due to unclear state flow

Code Maintainability

Several factors compound maintenance difficulties:

  • Massive files requiring extensive scrolling and mental mapping
  • Mixed concerns within single classes
  • Global dependencies making isolated testing impossible
  • Inconsistent architectural patterns throughout the codebase

Learning Opportunities and Best Practices

What Went Right

Despite architectural challenges, the Cashew app demonstrates several positive aspects:

  • Successful product delivery: The app serves real users effectively
  • Feature completeness: Comprehensive budgeting functionality
  • User satisfaction: Exceptional ratings indicate strong user experience
  • Open source contribution: Provides learning opportunities for the community
  • Solo developer achievement: Remarkable accomplishment for a single person

Critical Issues Identified

1. No error monitoring

  • Problem: Errors are silenced or only logged to console
  • Solution: Implement Crashlytics or Sentry for production error tracking

2. No automated tests

  • Problem: No automated tests for a 103,000-line codebase
  • Solution: Start with integration tests for critical user flows before major refactoring

3. No linter tool

  • Problem: No linting or static analysis
  • Solution: Implement Flutter lints package, consider DCM (Dart Code Metrics) for advanced analysis

4. Poor separation of concerns

  • Problem: Business logic, UI, and data access mixed throughout
  • Solution: Implement layered architecture (presentation, domain, data layers)

5. Lack of dependency injection system / service locator

  • Problem: Global variables for dependency injection
  • Solution: Adopt proper DI system (Riverpod, GetIt) for dependency management

6. Global mutable state

  • Problem: App settings and other state accessible/modifiable globally
  • Solution: Implement proper state management (Riverpod, BLoC) with immutable state

7. Widget rebuilds via global navigator keys

  • Problem: Widgets forcing other widgets to rebuild by directly accessing their navigator keys and resetting their state
  • Solution: Move to reactive state management solutions for better control of widget rebuilds

8. Massive widget classes

  • Problem: Massive widget classes with complex interdependencies
  • Solution: Break down into smaller, focused widgets with clear responsibilities

Refactoring Strategy

For a codebase of this scale, refactoring must be incremental:

  1. Establish safety nets: Add integration tests for critical paths
  2. Implement error monitoring: Gain visibility into production issues
  3. Add code quality tools: Enable automated detection of issues
  4. Gradual architectural improvements: Apply Boy Scout Rule—leave code better than found
  5. Dependency injection migration: Slowly replace global variables with proper DI
  6. State management evolution: Introduce reactive state management incrementally
  7. Widget decomposition: Break large widgets into smaller, testable components

Technical Debt Impact

The Cashew app illustrates how technical debt accumulates in successful projects:

  • Initial velocity: Quick development enabled rapid feature delivery
  • Growing complexity: Each new feature became harder to implement
  • Maintenance burden: Changes risk breaking existing functionality
  • Developer fatigue: Complex codebase becomes overwhelming to maintain

The lack of recent releases (since July 2024) may indicate the technical debt has reached a tipping point where continued development becomes prohibitively difficult.

Architectural Recommendations

Immediate Priorities

  1. Error monitoring implementation: Essential for production app stability
  2. Critical path testing: Integration tests for core user journeys
  3. Dependency injection: Replace global variables with proper DI container
  4. State management: Introduce reactive patterns to reduce forced rebuilds

Long-term Improvements

  1. Layered architecture: Separate presentation, domain, and data concerns
  2. Widget decomposition: Break monolithic widgets into manageable components
  3. Code organization: Restructure folders following architectural principles
  4. Performance optimization: Address unnecessary rebuilds and memory issues

Development Process Improvements

  1. Code review practices: Implement standards for future changes
  2. Automated testing: Build comprehensive test suite incrementally
  3. Continuous integration: Automate quality checks and testing
  4. Documentation: Create architectural decision records and coding standards

Lessons for Flutter Developers

For Solo Developers

  • Balance speed with quality: Technical debt compounds quickly in successful projects
  • Invest in testing early: Manual testing doesn't scale with codebase growth
  • Establish architectural patterns: Consistency prevents complexity explosion
  • Plan for success: Consider maintenance burden as the app grows

For Teams

  • Code review importance: Multiple perspectives prevent architectural drift
  • Shared standards: Team conventions prevent inconsistent patterns
  • Refactoring time: Budget time for technical debt reduction
  • Knowledge sharing: Prevent single points of failure in understanding

Universal Principles

  • Separation of concerns: Keep UI, business logic, and data access distinct
  • Dependency management: Avoid global state; use proper injection patterns
  • Error handling: Plan for failure scenarios from the beginning
  • Performance considerations: Design for scale, even in early stages

Conclusion

The Cashew app represents a fascinating case study in Flutter development. While the business success is undeniable—serving over 100,000 users with exceptional satisfaction—the technical implementation reveals the challenges of scaling a codebase without proper architectural foundations.

The developer's achievement in creating a successful app single-handedly deserves recognition. However, the codebase serves as a cautionary tale about technical debt accumulation and the importance of sustainable development practices.

Key takeaways for the Flutter community:

  1. Technical debt is real: It accumulates silently and can eventually halt development
  2. Architecture matters: Even solo projects benefit from proper structural planning
  3. Testing is essential: Manual testing doesn't scale with complexity
  4. Refactoring is investment: Regular code improvement prevents future paralysis
  5. Tools help: Linting, error monitoring, and quality metrics provide early warnings

The open-source nature of Cashew provides invaluable learning opportunities. By examining both its successes and challenges, developers can make more informed decisions about their own projects, balancing delivery speed with long-term maintainability.

For developers facing similar technical debt situations, the path forward involves incremental improvement, establishing safety nets through testing, and gradually introducing better architectural patterns. The Boy Scout Rule—leaving code better than you found it—becomes essential for managing legacy codebases while continuing to deliver value to users.

The Cashew app ultimately demonstrates that while technical perfection isn't required for business success, sustainable development practices become crucial as projects scale and mature. The challenge lies in finding the right balance between shipping features and maintaining code quality—a balance that becomes increasingly important as applications grow in complexity and user base.

Want More?

Invest in yourself with my high-quality Flutter courses.

Flutter In Production

Flutter In Production

Learn about flavors, environments, error monitoring, analytics, release management, CI/CD, and finally ship your Flutter apps to the stores. 🚀

Flutter Foundations Course

Flutter Foundations Course

Learn about State Management, App Architecture, Navigation, Testing, and much more by building a Flutter eCommerce app on iOS, Android, and web.

Flutter & Firebase Masterclass

Flutter & Firebase Masterclass

Learn about Firebase Auth, Cloud Firestore, Cloud Functions, Stripe payments, and much more by building a full-stack eCommerce app with Flutter & Firebase.

The Complete Dart Developer Guide

The Complete Dart Developer Guide

Learn Dart Programming in depth. Includes: basic to advanced topics, exercises, and projects. Last updated to Dart 2.15.

Flutter Animations Masterclass

Flutter Animations Masterclass

Master Flutter animations and build a completely custom habit tracking application.