One of the best ways to process emotions, track personal growth, and gain clarity during challenging times. But let’s be honest, sometimes we don’t realize how we’re feeling until we look back on our words. A journal entry that felt neutral in the moment might reveal hidden stress, frustration, or joy when viewed later.
Well that’s why I decided to add sentiment analysis as a feature of the app. Instead of just relying on how you label your mood, AI can analyze the text of your entries and provide an unbiased sentiment score. This means that even if you select Neutral as your mood, but your writing contains words associated with sadness, or anger, the app can help detect pattersn you might not notice yourself.
In this post, I walk through how I integrated sentiment analysis into my mental health journal app. Let’s begin!
What is Sentiment Analysis?
Sentiment analysis is the process of analyzing a piece of text to determine its emotional tone. It’s a natural language processing (NLP) technique that works by analyzing words, phrases, and context to classify the text as is positive, negative, or neutral.
You’ve probably encountered sentiment analysis in action when
- Social media platforms use it to detect offensive content and categorize user reaction,
- Customer service bots analyze messages to gauge whether a customer is happy or frustrated,
- Product reviews on ecommerce sites are often summarized with sentiment scores like mostly positive, or negative
For the mental health journal app, sentiment analysis would provide value insights into emotions that might not be explicitly stated. A journal entry like, “I feel exhausted. Work has been overwhelming, and I just want a break” might be label as neutral when the user manually selects a mood, but sentiment analysis could detect negative sentiment based on words like exhausted and overwhelming.
By integrating sentiment analysis, the app can help users recognize emotional patterns even when they don’t consciously label their feelings. This can especially be useful for those who struggle with emotional awareness or simply want data-driven insights into their mental health.
Choosing the Right Sentiment Analysis Method
When I first planned the sentiment analysis feature, my initial choice was TensorFlow Lite (TFLite). The Idea was to use a pretrained machine learning model, specifically the Universal Sentence Encoder (TensorFlow 1 Lite v2), to analyze journal entries and generate sentiment scores. I thought
A trained AI model will provide more accurate and nuanced sentiment detection compared to simple keyword-based approach
However, after experimenting with this approach, I ran into several challenges that made me reconsider my choice.
- Complex Model Handling and Processing Overhead
- Converting and integrating the Universal Sentence Encoder model into TFLite proved to be tricky and error-prone.
- The model required a structured input format that didn’t align with standard text preprocessing in Flutter
- Converting raw journal text into a compatible format for TensorFlow Lite took extra steps, making it less efficient for real-time analysis.
- Deployment Issue and Compatibility
- Running a TFLite model inside a Flutter app requires integrating tflite_flutter, ensuring the correct model format, and handling dependencies properly.
- model size and inference time can impact performance, especially for larger neural networks like the Universal Sent
- Unexpected Errors and Debugging Challenges
- Despite making sure I have the latest C++ Redistributables and trying multiple conversion approaches, I faced persistent errors when loading the model in Google Colab and the Flutter project
- Debugging ML models inside a mobile app is tricky since most of the debugging tools are optimized for Python environments like TensorFlow and Keras
Why I Switched to dart_sentiment
After struggling with the TensorFlow Lite approach, I looked for a simpler and more lightweight alternative that would still provide meaningful sentiment analysis. That’s when I found dart_sentiment, a Dart-based sentiment analysis package.
Here’s why it turned out to be the better choice, at least for this phase of the project:
- No External ML Models Needed: It works entirely in Dart, so there’s no need to integrate TensorFlow Lite and and handle .tflite model files
- Fast & Lightweight: It processes text instantly, making it ideal for mobile apps where performance matters
- Simple to use: It provides a sentiment score based on a pretrained lexicon without requiring additional preprocessing
- Works Offline: Unlike cloud-based AI services, dart_sentiment doesn’t require an internet connection, ensuring privacy for user data.
Implementing Sentiment Analysis
After settling on using dart sentiment for sentiment analysis, the next step was to integrating it into the app. The goal was simple:
- Analyze the sentiment of each journal entry when it’s saved
- Store the sentiment score along with the user’s selected mood
- Visualize trends over time in the mood and sentiment trends dashboard
Installing dart_sentiment
First I added the package to my Flutter project in the pubspec.yaml file by running
flutter pub add dart_sentiment
Creating the Sentiment Analyzer Service
I then created a SentimentAnalyzer class where I encapsulated the sentiment analysis logic:
import 'package:dart_sentiment/dart_sentiment.dart';
class SentimentAnalyzer {
final _sentiment = Sentiment();
/// Analyzes the sentiment of a given text and returns a sentiment score.
double analyzeText(String entryContent) {
final result = _sentiment.analysis(entryContent, emoji: true);
final score = result['comparative'] as double? ?? 0.0;
return score;
}
/// Interprets the sentiment score into categories: Positive, Neutral, Negative.
String interpretResult(double score) {
if (score > 0.6) {
return 'Positive';
} else if (score < -0.4) {
return 'Negative';
} else {
return 'Neutral';
}
}
}
- The analyzeText() method takes in a journal entry’s content and returns a sentiment score
- The interpretResult() method maps that score into Positive, Neutral, or Negative sentiment categories.
Integrating Sentiment Analysis in Journal entries
Next, I modified the journal editor screen to analyze the sentiment whenever a new journal entry is saved
void _submitEntry() async {
final cubit = context.read<JournalCubit>();
final journalText = _contentController.document.toPlainText();
// Analyze the sentiment
final sentimentScore = await sentimentAnalyzer.analyzeText(journalText);
final entry = JournalEntryModel.empty().copyWith(
userId: context.currentUser!.uid,
title: _titleController.text.trim(),
titleLowercase: _titleController.text.trim().toLowerCase(),
content: jsonEncode(_contentController.document.toDelta().toJson()),
selectedMood: _selectedMood?.capitalizeFirstLetter(), // User selected mood
sentimentScore: sentimentScore, // AI analyzed sentiment
tags: _tags,
);
if (context.currentUser == null) {
CoreUtils.showSnackBar(context, 'User not logged in');
} else {
await cubit.createEntry(
entry: entry,
);
}
}
Key updates:
Before saving a journal entry, I now:
- Run the sentiment analysis on the journal content
- store the sentiment score in the entry
This ensure that every saved journal entry has both the user selected mood (e.g., Happy, Sad, Neutral) and analyzed sentiment (Positive, Neutral, Negative)
Displaying Sentiment Trends in the Dashboard
After storing sentiment data in journal entries, I created a trends dashboard where I used the package fl_chart to display the data in a graph.
Fetching Mood and Sentiment Data
Using the cubit InsightsCubit, I fetched the stored entries and extracted sentiment and mood data
class MoodTrendsDashboard extends StatelessWidget {
const MoodTrendsDashboard({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<InsightsCubit, InsightsState>(
builder: (context, state) {
if (state is DashboardDataFetched) {
final entries = state.entries;
// Extract mood and sentiment data points for the last 7 days
final moodData = <FlSpot>[];
final sentimentData = <FlSpot>[];
final moodCounts = <String, int>{
'Happy': 0,
'Neutral': 0,
'Sad': 0,
'Angry': 0,
};
final dayLabels =
_generateRotatedWeekLabels(); // Get reordered week labels/ Stores the day labels (e.g., 'Mon', 'Tue')
// Create a map for quick access to journal entries by day
final moodMap = <String, int>{};
final sentimentMap = <String, double>{};
for (final entry in entries) {
final entryDay = DateFormat.E().format(entry.dateCreated); // 'Mon', 'Tue'
// Convert user-selected mood into a numerical scale
final moodValue = _mapMoodToValue(entry.selectedMood);
final sentimentValue = entry.sentimentScore;
moodMap[entryDay] = moodValue;
sentimentMap[entryDay] = sentimentValue;
moodCounts[entry.selectedMood] = (moodCounts[entry.selectedMood] ?? 0) + 1;
}
// Assign values to spots, ensuring all weekdays are filled
for (var i = 0; i < dayLabels.length; i++) {
final day = dayLabels[i];
moodData.add(
FlSpot(i.toDouble(), moodMap[day]?.toDouble() ?? 4), // Default to Neutral if missing
);
sentimentData.add(
FlSpot(i.toDouble(), sentimentMap[day] ?? 0), // Default to Neutral if missing
);
}
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
).copyWith(top: 8, left: 4),
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// **Mood & Sentiment Graph**
if (entries.isEmpty)
const SizedBox(
height: 225,
child: Center(
child: Text(
'No mood trends available yet.',
style: TextStyle(
color: Colours.softGreyColor,
),
),
),
)
else
MoodTrendsChart(
userMoodSpots: moodData,
sentimentScoreSpots: sentimentData,
dayLabels: dayLabels,
),
],
),
);
} else if (state is DashboardLoading) {
return const LoadingWidget();
} else {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 16,
).copyWith(top: 8, bottom: 25),
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: const Center(
child: Text('Failed to load mood trends'),
),
);
}
},
);
}
}
Mapping User Selected Moods to Malues
The AI generated sentiment scores are numerical values, but the user selected moods are strings. In order to display them on the graph, I created the helper method _mapMoodToValue(), which converts entry moods to numerical values.
int _mapMoodToValue(String mood) {
switch (mood.toLowerCase()) {
case 'happy':
return 5;
case 'neutral':
return 4;
case 'sad':
return 3;
case 'angry':
return 2;
default:
return 4; // Default to neutral
}
}
Mood and Sentiment Trends Chart
I then created the widget MoodTrendsChart, which contains the actual graph.
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class MoodTrendsChart extends StatelessWidget {
const MoodTrendsChart({
required this.userMoodSpots,
required this.sentimentScoreSpots,
required this.dayLabels,
super.key,
});
final List<FlSpot> userMoodSpots;
final List<FlSpot> sentimentScoreSpots;
final List<String> dayLabels; // Labels like ['Mon', 'Tue', 'Wed']
@override
Widget build(BuildContext context) {
return Column(
children: [
/// **Mood & Sentiment Graph**
SizedBox(
height: 225,
child: LineChart(
LineChartData(
minY: -1,
maxY: 5,
minX: 0,
maxX: 6,
lineBarsData: [
/// 🔵 **Mood Line (User Selected)**
LineChartBarData(
spots: userMoodSpots,
isCurved: true,
color: Colors.blue,
// barWidth: 3,
// belowBarData: BarAreaData(show: true),
dotData: const FlDotData(show: true),
),
/// 🔴 **Sentiment Line (AI Generated)**
LineChartBarData(
spots: sentimentScoreSpots,
isCurved: true,
color: Colors.red,
// barWidth: 3,
// belowBarData: BarAreaData(show: true),
dotData: const FlDotData(show: true),
),
],
/// 🏷 **Axis Titles**
titlesData: FlTitlesData(
leftTitles: AxisTitles(
axisNameSize: 6,
sideTitles: SideTitles(
reservedSize: 75,
showTitles: true,
getTitlesWidget: (value, _) {
return _leftAxisLabels(value.toInt());
},
),
),
rightTitles: const AxisTitles(),
topTitles: const AxisTitles(),
/// **Bottom X-Axis Labels (Days)**
bottomTitles: AxisTitles(
drawBelowEverything: false,
sideTitles: SideTitles(
reservedSize: 35,
showTitles: true,
getTitlesWidget: (value, _) {
int index = value.toInt();
if (index >= 0 && index < dayLabels.length) {
return Column(
children: [
Text(
index == dayLabels.length - 1
? '\t\t\t${dayLabels[index]}\n(today)\t'
: '${dayLabels[index]}', // Label with day name
style: const TextStyle(fontSize: 10),
),
],
);
}
return const Text('');
},
),
),
),
),
),
),
/// **Legend for Colors**
_buildLegend(),
],
);
}
/// **Mood & Sentiment Labels (Y-Axis)**
Widget _leftAxisLabels(int value) {
switch (value) {
case 6:
return const Text(
'',
style: TextStyle(fontSize: 10, color: Colors.red),
);
case 5:
return const Text(
'😊 Happy',
style: TextStyle(fontSize: 10),
);
case 4:
return const Text(
'😐 Neutral',
style: TextStyle(fontSize: 10),
);
case 3:
return const Text(
'😢 Sad',
style: TextStyle(fontSize: 10),
);
case 2:
return const Text(
'😠 Angry',
style: TextStyle(fontSize: 10),
);
case 1:
return const Text(
'🟢 AI Positive',
style: TextStyle(fontSize: 10, color: Colors.green),
);
case 0:
return const Text(
'🟡 AI Neutral',
style: TextStyle(
fontSize: 10,
color: Colors.orange,
),
);
case -1:
return const Text(
'🔴 AI Negative',
style: TextStyle(
fontSize: 10,
color: Colors.red,
),
);
case -2:
return const Text(
'',
style: TextStyle(
fontSize: 10,
color: Colors.red,
),
);
default:
return const Text('');
}
}
/// **Chart Legend**
Widget _buildLegend() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_legendItem(Colors.blue, 'Mood'),
const SizedBox(width: 16),
_legendItem(Colors.red, 'Sentiment (AI)'),
],
);
}
/// **Legend Item (Color + Label)**
Widget _legendItem(Color color, String label) {
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(label, style: const TextStyle(fontSize: 12)),
],
);
}
}
Improving the Profile Stats with Sentiment Analysis
Finally, I enhance the profile screen to display mood and sentiment statistics in the user’s journal history
Updating the User Entity and Model
I started by Modifying UserEntity and UserModel to include mood and sentiment summary statistics
class UserEntity extends Equatable {
const UserEntity({
required this.uid,
required this.name,
required this.email,
required this.dateCreated,
required this.totalEntries,
required this.sentimentSummary,
required this.moodSummary,
required this.tagsFrequency,
required this.isVerified,
this.profilePictureUrl,
});
UserEntity.empty()
: this(
uid: '',
name: '',
email: '',
dateCreated: DateTime.now(),
totalEntries: 0, // New
sentimentSummary: const SentimentSummary.empty(), // New
moodSummary: const MoodSummary.empty(), // New
tagsFrequency: TagsFrequency.empty(),// New
profilePictureUrl: '',
isVerified: false,
);
final String uid;
final String name;
final String email;
final DateTime dateCreated;
final String? profilePictureUrl;
final bool isVerified;
final int totalEntries; // New
final SentimentSummary sentimentSummary;// New
final MoodSummary moodSummary;// New
final TagsFrequency tagsFrequency;// New
@override
List<Object?> get props => [
uid,
name,
email,
dateCreated,
profilePictureUrl,
isVerified,
totalEntries,
sentimentSummary,
moodSummary,
tagsFrequency,
];
@override
String toString() => '''
UserEntity {
uid: $uid,
name: $name,
email: $email,
dateCreated: $dateCreated,
profilePictureUrl: $profilePictureUrl,
isVerified: $isVerified,
totalEntries: $totalEntries,
sentimentSummary: $sentimentSummary,
moodSummary: $moodSummary,
tagFrequency: $tagsFrequency,
}
''';
}
Updating the profile screen
Since I’m now keeping track of the user’s mood and sentiment statistics, I modified the Profile screen to display them
class ProfileBody extends StatelessWidget {
const ProfileBody({super.key});
@override
Widget build(BuildContext context) {
return Consumer<UserProvider>(
builder: (context, provider, child) {
final user = context.currentUser;
final positive = user?.sentimentSummary.positive ?? 0;
final negative = user?.sentimentSummary.negative ?? 0;
final neutral = user?.sentimentSummary.neutral ?? 0;
final total = user?.totalEntries ?? 0;
final posPct = total == 0 ? 0 : (positive / total * 100).toStringAsFixed(0);
final negPct = total == 0 ? 0 : (negative / total * 100).toStringAsFixed(0);
final neutralPct = total == 0 ? 0 : (neutral / total * 100).toStringAsFixed(0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
sectionTitle: 'Your Stats',
fontSize: 16,
seeAll: false,
onSeeAll: () {},
),
const SizedBox(height: 16),
Container(
margin: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: const [
BoxShadow(
color: Colours.softGreyColor,
blurRadius: 5,
spreadRadius: 1,
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_statItem(
'Entries',
'${context.currentUser?.totalEntries ?? 0}',
),
const Divider(),
_statItem(
'Mood Trends',
'Positive: $posPct%, Neutral: $neutralPct%, '
'Negative: $negPct%',
),
const Divider(),
_statItem(
'Favorite Tags',
favTags,
),
],
),
),
),
const SizedBox(height: 32),
SectionHeader(
sectionTitle: 'Settings',
fontSize: 16,
seeAll: false,
onSeeAll: () {},
),
const SizedBox(height: 16),
_settingsItem(
context,
'Change Password',
Icons.lock,
),
_settingsItem(
context,
'Notification Preferences',
Icons.notifications,
),
_settingsItem(
context,
'Privacy Policy',
Icons.privacy_tip,
),
_settingsItem(
context,
'Sign Out',
Icons.logout,
),
const SizedBox(height: 16),
Center(
child: TextButton(
onPressed: () {
CoreUtils.displayDeleteAccountWarning(
context,
onDeletePressed: () {
Navigator.pop(context);
CoreUtils.showEnterPasswordDialog(context);
},
);
},
child: Text(
'Delete Account',
style: context.theme.textTheme.titleMedium?.copyWith(
color: Colors.red,
decoration: TextDecoration.underline,
decorationColor: Colors.red,
),
),
),
),
const SizedBox(height: 16),
],
);
},
);
}
// Helper Widget for Stats Items
Widget _statItem(String title, String value) {
var valueItems = <String>[];
if (value.contains(',')) {
valueItems = value.split(',');
}
return value.contains(',')
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Column(
children: [
for (final item in valueItems)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
item,
style: const TextStyle(
fontSize: 14,
// color: Colours.softGreyColor,
),
),
),
],
),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
value,
style: const TextStyle(
fontSize: 14,
// color: Colours.softGreyColor,
),
),
),
],
);
}
}
Now, the profile screen gives users an overview of their emotional trends over time, making it more insightful
You can checkout the full code and the rest of the project here
Conclusion
Integrating sentiment analysis into my mental health journal app was both a challenge and a breakthrough. I wanted to go beyond simple mood tracking and provide deeper insights into emotional trends. Initially, I explored TensorFlow Lite but switched to dart_sentiment for its simplicity and efficiency.
Storing and visualizing sentiment scores was a key challenge, but seeing the Mood Trends Dashboard come to life made it all worth it. Adding sentiment analysis to the profile stats gave users a clearer picture of their emotional patterns over time.
This feature transformed the app from just a journaling tool to something that helps users understand their emotions. While there’s room for improvement, I’m excited about how far it’s come and what’s next.