Copied RSS Feed

Flutter

Build a Responsive Flutter Expense Tracker Dashboard with Dynamic Charts

TL;DR: Struggling to build a comprehensive Flutter dashboard? This guide details implementing a powerful Flutter Expense Tracker Dashboard using custom layouts and dynamic Syncfusion® Flutter Charts to visualize all your financial insights, from income to savings, seamlessly across devices. It covers how to consolidate financial insights like income, expenses, and savings growth into a responsive, interactive layout. Key features include dynamic charts, recent transaction tracking, and an adaptable design for mobile and desktop views.

Welcome to our guide on building a responsive Flutter Expense Tracker Dashboard! So far, we have covered the Setup and Import Pages of the Expense Tracker Sample in the Onboarding Pages.  In this post, we’ll explore how to create an interactive dashboard that consolidates financial insights like income, expenses, and savings. Whether you’re developing for mobile or desktop, this step-by-step guide will help you implement dynamic charts and recent transaction tracking.

Now, let’s dive into the dashboard page, where we’ll walk through its implementation in the Flutter Expense Tracker Sample and explore its key features.

Consolidated details on the dashboard page

The dashboard page in the expense tracker sample provides a detailed summary of financial activity, offering a clear view of transactions and savings. It is structured into multiple sections, each highlighting key financial details:

  • Overall financial insights: Displaying key financial metrics like account balance, income, expenses, and savings.
  • Financial overview: An interactive chart visualizes income and expense trends based on the categories.
  • Recent transactions: Displays the most recently made transactions.
  • Account overview: An interactive chart visualizes income and expenses over different timeframes.
  • Savings growth: An interactive chart that visualizes savings growth over time.

Dashboard demo

Desktop view

Dashboard page on desktop with multiple financial insights

Mobile view

Expense tracker dashboard on mobile with stacked financial metrics

Designing the dashboard layout

To build a complex, custom layout and a well-structured dashboard, we used SlottedMultiChildRenderObjectWidget from the Flutter framework. This widget allows precise control over positioning while maintaining flexibility for mobile and desktop views. This approach ensures that each widget is placed in the correct slot, making the layout responsive and adaptable to different screen sizes.

Let’s break down how we have implemented the dashboard page step by step below.

Step 1: Defining slots with an enum

Before assigning each widget to its position, we defined all possible slots. This approach improves code maintainability and readability by ensuring each widget is mapped to a specific position.

/// Defines slots for different dashboard widgets.
enum DashboardWidgetsSlot {
   currentBalance,
   income,
   expense,
   savings,
   financialOverviewWidget,
   accountBalanceChart,
   recentTransactionWidget,
   savingGrowth,
}

Step 2: Create the slotted widget class

We created a widget by extending SlottedMultiChildRenderObjectWidget, the foundation for slot-based positioning.

class ETDashboardLayout extends SlottedMultiChildRenderObjectWidget<
      DashboardWidgetsSlot,
      RenderObject>{
   const DashboardWidget({
      required this.buildContext,
      // Other required properties..
      super.key,
   });
   final BuildContext buildContext;
   // Other required properties.
}

Step 3: Assigning widgets to slots

The childForSlot method maps each dashboard component to its respective slot. This method ensures that each widget is positioned correctly and efficiently.

@override
Widget? childForSlot(DashboardWidgetsSlot slot) {
  switch (slot) {
    case DashboardWidgetsSlot.currentBalance:
      return Padding(
         padding:
             isMobile(buildContext) ? mobileCardPadding : windowsCardPadding,
             child: OverallDetails(
                 insightTitle: 'Balance',
                 insightValue: toCurrency(
                 totalIncome - totalExpense,
                 userDetails.userProfile,
             ),
             // Other properties…  
     ),);
     case DashboardWidgetsSlot.income:
           // return income widget…  
     case DashboardWidgetsSlot.expense:
           // return expense widget…  
     case DashboardWidgetsSlot.savings:
           // return savings widget…  
     case DashboardWidgetsSlot.financialOverviewWidget:
           // return financial overview widget…  
     case DashboardWidgetsSlot.recentTransactionWidget:
           // return recent transaction widget…        
     case DashboardWidgetsSlot.accountBalanceChart:
           // return recent account balance widget…        
     case DashboardWidgetsSlot.savingGrowth:
           // return savings Growth widget…        
  }
}

Step 4: Defining available slots

We override the slots getter from the SlottedMultiChildRenderObjectWidget to specify the available slots, ensuring the widget knows which slots to expect.

@override
Iterable<DashboardWidgetsSlot> get slots => DashboardWidgetsSlot.values;

Step 5: Creating and updating RenderObject

The createRenderObject and updateRenderObject methods handle rendering logic. These methods ensure that widgets are properly initialized and updated when required.

@override
SlottedContainerRenderObjectMixin<DashboardWidgetsSlot, RenderObject>
      createRenderObject(BuildContext context) {
    return ETRenderDashboardWidgetsLayout(context);
}

@override
ETRenderDashboardWidgetsLayout updateRenderObject(BuildContext context,
 covariant SlottedContainerRenderObjectMixin<DashboardWidgetsSlot,RenderObject>
  renderObject,){
     return ETRenderDashboardWidgetsLayout(context);
  }
}

Custom RenderObject for dashboard widgets

To define the custom layout behavior, we created a class named ETRenderDashboardWidgetsLayout, which extends the Flutter RenderBox widget. This class determines how widgets are arranged within the dashboard.

/// A custom render box that lays out dashboard widgets in a specific arrangement.
class ETRenderDashboardWidgetsLayout extends RenderBox
  with SlottedContainerRenderObjectMixin<DashboardWidgetsSlot, RenderBox> {
   /// Creates a new dashboard widget layout with the given build context.
   ETRenderDashboardWidgetsLayout(this._buildContext);
  
   /// The build context used for responsive layout calculations.
   final BuildContext _buildContext;

   /// Sets up the parent data for child render objects.
   @override
   void setupParentData(RenderObject child) {
      if(child.parentData is! DashboardWidgetParentData) {
         child.parentData = DashboardWidgetParentData();
      }
      super.setupParentData(child);
} }

Step 6: Handling layout with PerformLayout

The performLayout method in the RenderBox widget calculates the position of each child, dynamically adjusting the dashboard’s structure based on the available screen size.

/// Performs layout calculations for all child widgets.
@override
void performLayout() {
   final Size availableSize = constraints.biggest;
   final double commonInsightTileMinimumHeight =
   isMobile(buildContext) ? 76.0 : 108.0;
   const double commonInsightWidthFactor = 0.25;
   const double commonInsightMobileWidthFactor = 0.5;
   final bool isMobileOrTablet = isMobile(buildContext) || isTablet(buildContext);
    
 // Calculate sizes for insight boxes and dashboard widgets.
final Size insightBoxSize = Size(
    isMobileOrTablet
    ? (availableSize.width * commonInsightMobileWidthFactor)
    : (availableSize.width * commonInsightWidthFactor),
    commonInsightTileMinimumHeight,
);
 
final Size dashboardWidgetBoxSize = Size(
    availableSize.width,
    MediaQuery.of(buildContext).size.height -
    (insightBoxSize.height + 24.0 + AppBar().preferredSize.height),
);
 
final RenderBox? currentBalanceRenderBox = childForSlot(
    DashboardWidgetsSlot.currentBalance,
);
 
if(currentBalanceRenderBox != null)
{
    currentBalanceRenderBox.layout( BoxConstraints.tight(Size(width,     height)), parentUsesSize: true, ); 
 
    // Set the position of the child. 
    final parentData = currentBalanceRenderBox.parentData! as DashboardWidgetParentData;     
    parentData.offset =    Offset(x, y); 
}
    
// repeat for other children… // Set the final size of the render object. size = Size(constraints.maxWidth, height); }

Dashboard layout across different devices

Desktop view

  • Overall details: Displayed horizontally, each section occupying 25% of the screen width.
  • Financial overview and recent transactions: Placed side by side, both the financial overview covers 65% and the recent transactions cover 35% of the total width.
  • Account balance and savings growth: Placed side by side, sharing equal widths.

Mobile and tablet view

  • Overall details: Arranged in two rows, with two widgets per row.
  • Other sections: Stacked vertically for better readability.

Dashboard slots

Section 1: Overall financial insights

The overall financial insights section provides a quick financial summary, displaying account balance, income, expenses, and savings at the top of the dashboard. Each metric uses a custom ETCommonBox for a consistent and clean layout.

Calculation logic

  • Balance = total income – total expense
  • Income = sum of all income sources
  • Expense = sum of all expenses
  • Savings = sum of all saved amounts

Desktop view

Overall financial metrics are displayed in a horizontal layout on desktop

Mobile view

Overall financial metrics in two rows on mobile view

Section 2: Financial overview

This section provides an intuitive way to analyze income and expenses using a segmented button, a duration dropdown, and a Syncfusion® Flutter Doughnut Chart.

Segmented button

Flutter’s built-in SegmentedButton widget allows users to toggle between income and expense. Based on the selection, the chart updates dynamically to display the respective categories.

Duration dropdown

The duration dropdown provides options to filter data based on:

  • This month
  • This year
  • Last 6 months
  • Last year

Selecting a duration updates the data source, ensuring synchronized data visualization for income or expense categories within the chosen period.

Syncfusion® Flutter Doughnut Chart

A Doughnut Chart visually represents financial data and updates dynamically based on:

  • The selected financial type (income/expense).
  • The selected duration.

The chart displays

  • Each category’s total expense or income.
  • Another section for categories contributing less than 8%.
DoughnutSeries<FinancialDetails, String> _buildDoughnutSeries(
 BuildContext context,
 List<FinancialDetails> currentDetails,
 String financialViewType,
 bool isExpense,
){
   return DoughnutSeries<FinancialDetails, String>(
      xValueMapper: (FinancialDetails details, int index) => details.category,
      yValueMapper: (FinancialDetails details, int index) => details.amount,
      dataLabelSettings: const DataLabelSettings(
      isVisible: true,
      labelIntersectAction: LabelIntersectAction.hide,
   ),
   radius: '100%',
   name: 'Expense',
   dataSource: currentDetails,
   animationDuration: 500,);
}

Data processing and grouping

To ensure accuracy, data is processed through the following steps:

  • Filter transactions based on the selected duration.
  • Group transactions by category and calculate total amounts.
  • Consolidate minor categories into other categories if they contribute less than 8%.
  • Updating the chart’s data source accordingly.
void _dataGrouping(
 List<ExpenseDetails> expenseDetailsReference,
 List<IncomeDetails> incomeDetailsReference,
){
 final List<ExpenseDetails> expenseData = _filterByTimeFrame(
  expenseDetailsReference,
  filteredTimeFrame,
 );
 final List<IncomeDetails> incomeData = _filterByTimeFrame(
  incomeDetailsReference,
  filteredTimeFrame,
 );

 Map<String, double> expenseMap = {};
 Map<String, double> incomeMap = {};

 for (final ExpenseDetails detail in expenseData) {
  expenseMap.update(
   detail.category,
   (value) => value + detail.amount,
   ifAbsent: () => detail.amount,
  );
 }
 for (final IncomeDetails detail in incomeData) {
  incomeMap.update(
   detail.category,
   (value) => value + detail.amount,
   ifAbsent: () => detail.amount,
  );
 }

 if (expenseMap.isNotEmpty) {
  expenseMap = _othersValueMapping(expenseMap);
 }
 if (incomeMap.isNotEmpty) {
  incomeMap = _othersValueMapping(incomeMap);
 }

 incomeDetailsReference.clear();
 expenseDetailsReference.clear();

 final List<ExpenseDetails> expenseDetails = expenseMap.entries.map((entry) {
  return ExpenseDetails(
   category: entry.key,
   amount: entry.value,
   date: DateTime.now(),
   budgetAmount: 0.0,
  );
 }).toList();

 final List<IncomeDetails> incomeDetails = incomeMap.entries.map((entry) {
  return IncomeDetails(
   category: entry.key,
   amount: entry.value,
   date: DateTime.now(),
  );
 }).toList();

 expenseDetailsReference.addAll(expenseDetails);
 incomeDetailsReference.addAll(incomeDetails);
}

Custom legend

A custom legend, designed using legendItemBuilder, ensures clear identification of financial data across desktop and mobile layouts. It displays:

  • Category icon (colored to match the chart segment).
  • Category name.
  • Total amount.

Desktop view

A Doughnut chart showing income and expenses on a desktop

Mobile view

Doughnut chart and segmented controls in mobile layout

Section 3: Recent transactions

The recent transactions widget provides users with an overview of their latest transactions. This section displays the most recently made transactions, including the category, subcategory, transaction amount, and transaction date. Each transaction entry is dynamically updated as new transactions are added.

To enhance user experience, a View More option is included, allowing users to navigate to the full transactions page to see all their past transactions.

buildViewMoreButton(context, () {
 pageNavigatorNotifier.value = NavigationPagesSlot.transaction;
}),

Desktop view

Recent transactions on the desktop

Mobile view

Recent transaction list optimized for mobile view

Section 4: Account overview

The account overview section provides an insightful and visually appealing representation of income and expenses over different timeframes. This enables users to track their financial trends with ease using Syncfusion® Flutter Cartesian Charts with Spline Area Series.

Visualizing income and expenses

To enhance financial tracking, this section includes:

  • Account balance display: A textual representation of the current balance.
  • Duration selection: A dropdown menu to switch between different timeframes (this year, last year, this month, last six months), dynamically updating the chart.
  • Spline Area Charts: Two overlapping area charts representing income and expenses, offering a clear financial overview.
  • Customized chart axes: The X and Y axes are customized using Syncfusion® axis types, ensuring clear readability and a professional appearance.

Implementation overview

Using the SfCartesianChart widget, a Spline Area Series is plotted to depict income and expense trends:

  • X-axis: Represents the time period (months or years) based on the selected duration.
  • Y-axis: Represents the monetary values of income and expenses.
  • Spline Area Series: Provides a smooth, visually appealing representation of financial fluctuations.

Key features:

  • Dynamic data update: The chart refreshes when a user selects a different duration from the dropdown.
  • Gradient fill: The income and expense areas use soft gradient fills to distinguish them visually.
  • Axis customization: Enhances clarity by formatting labels, gridlines, and scaling options using Syncfusion® axis types.
  • This section ensures users gain insights into financial patterns with a clear, interactive, and engaging visualization.

Expense chart

SplineAreaSeries<ExpenseDetails, DateTime> _buildExpenseSplineAreaSeries(
 BuildContext context,
 List<ExpenseDetails> expenseDetails,)
{ return SplineAreaSeries<ExpenseDetails, DateTime>( xValueMapper: (ExpenseDetails data, _) => data.date, yValueMapper: (ExpenseDetails data, _) => data.amount, splineType: SplineType.monotonic, dataSource: expenseDetails, ...); }

Income chart

Similarly, an income chart can be created by applying the same approach while updating the data source to reflect income details.

Desktop view demo

Spline area chart comparing income and expenses on a desktop

Mobile view demo

Spline area chart showing financial trends on mobile

Section 5: Savings growth

The Savings growth section visually represents how savings accumulate over time, allowing users to track their financial progress. It follows the same Spline Area Series visualization approach used in the account balance section.

Key features:

  • Duration selection: Users can filter savings growth over different periods (this year, last year, last six months, this month).
  • Spline Area Series: Displays how savings fluctuate over time with smooth transitions.
  • Gradient fill & axis customization: Ensures a clear and polished presentation of data.
  • View more navigation: A button directs users to the savings page for a detailed breakdown.

Implementation overview

This section utilizes SfCartesianChart with a Spline Area Series, similar to the account balance section:

  • X-axis: Represents the selected time duration.
  • Y-axis: Displays the accumulated savings amount.
  • Dynamic updates: The chart refreshes automatically after a different duration is selected.

By following this approach, we can seamlessly track savings trends.

/// Creates a spline area chart series for savings data. 
SplineAreaSeries<Saving, DateTime> _buildSavingsSplineAreaSeries( 
  BuildContext context, 
  List<Saving> savings, 
){ 
savings.sort((Saving a, Saving b) => a.savingDate.compareTo(b.savingDate)); 
 
return SplineAreaSeries<Saving, DateTime>( 
   xValueMapper: (Saving data, int index) => data.savingDate, 
   yValueMapper: (Saving data, int index) => data.savedAmount, 
   splineType: SplineType.monotonic, 
   dataSource: savings, 
   markerSettings: const MarkerSettings( 
    isVisible: true, 
    borderColor: Color.fromRGBO(134, 24, 252, 1), 
   )); 
} 

Desktop Demo

Savings growth chart over time on desktop

Mobile Demo

Savings chart showing accumulated savings on mobile

Unlock the power of Syncfusion’s highly customizable Flutter widgets.

FAQs

Q1: What is the Dashboard Page’s purpose?
It consolidates financial data, displaying balance, income, expenses, savings, and interactive charts for trends and transactions.

Q2: How does the Dashboard adapt to devices?
Using SlottedMultiChildRenderObjectWidget, it shows insights horizontally on desktop and stacks them in rows on mobile for readability.

Q3: What are the main Dashboard sections?

  • Financial Insights: Balance, income, expenses, savings.
  • Financial Overview: Doughnut Chart for category trends. Recent Transactions: Latest transactions with “View More.”
  • Savings Growth: Spline Area Chart for savings trends.

Q4: How are charts implemented?
Syncfusion widgets create dynamic Doughnut and Spline Area Charts, updating with timeframe selections and grouping minor categories (<8%) into “Others.”

Conclusion

In conclusion, building a responsive Flutter Expense Tracker Dashboard is a powerful way to offer a comprehensive, interactive, and user-friendly experience for tracking financial activities. By leveraging Syncfusion® Flutter widgets, you can create a smooth, interactive, visually appealing interface. This guide empowers you to analyze financial data effortlessly, make informed decisions, and track your financial health with confidence. Ready to build your dashboard? Start today with Syncfusion Flutter widgets!

The new version is available for current customers to download from the license and downloads page. If you are not a Syncfusion® customer, you can try our 30-day free trial for our newest features.

You can also contact us through our support forumssupport portal, or feedback portal. We are always happy to assist you!

Meet the Author

Kompelli Sravan Kumar Kompelli Lakshman

Kompelli Sravan Kumar is a proficient software engineer at Syncfusion, specializing in high-performance cross-platform component development. With a passion for cutting-edge technologies, he crafts innovative and scalable solutions that elevate user experiences. His deep expertise in modern cross-platform development empowers him to build efficient, adaptable applications that cater to diverse needs.