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.
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:
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.
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,
}
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.
}
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…
}
}
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;
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);
}
}
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);
}
}
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);
}
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.
This section provides an intuitive way to analyze income and expenses using a segmented button, a duration dropdown, and a Syncfusion® Flutter Doughnut Chart.
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.
The duration dropdown provides options to filter data based on:
Selecting a duration updates the data source, ensuring synchronized data visualization for income or expense categories within the chosen period.
A Doughnut Chart visually represents financial data and updates dynamically based on:
The chart displays
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,);
}
To ensure accuracy, data is processed through the following steps:
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);
}
A custom legend, designed using legendItemBuilder, ensures clear identification of financial data across desktop and mobile layouts. It displays:
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;
}),
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.
To enhance financial tracking, this section includes:
Using the SfCartesianChart widget, a Spline Area Series is plotted to depict income and expense trends:
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,
...);
}
Similarly, an income chart can be created by applying the same approach while updating the data source to reflect income details.
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.
This section utilizes SfCartesianChart with a Spline Area Series, similar to the account balance section:
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),
));
}
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?
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.”
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 forums, support portal, or feedback portal. We are always happy to assist you!