Happy is an AI-powered structured day planner designed for productivity.
Core features:
- Daily tasks and routines steps generation by integrating Vertex AI
- Daily tracking for consistency and routines creation
- Progress monitoring to build habits and achieve goals
This project contains 3 flavors:
- development
- staging
- production
Before you run the app you must add projects "happy-day-dev", "happy-day-stg", and "happy-day-prod" to your Firebase console.
Now, install Firebase & FlutterFire CLI and configure projects for each flavor:
# Install CLI
npm install -g firebase-tools
dart pub global activate flutterfire_cli
# Login to Firebase
firebase login
# Get projects configuration
./flutterfire_config.sh dev
./flutterfire_config.sh stg
./flutterfire_config.sh prodTo run the desired flavor either use the launch configuration in VSCode/Android Studio or use the following commands:
# Development
$ flutter run --flavor development --target lib/main_development.dart
# Staging
$ flutter run --flavor staging --target lib/main_staging.dart
# Production
$ flutter run --flavor production --target lib/main_production.dart*Happy works on iOS, Android, and Web.
If you are getting an error that says "Failed to list Firebase projects error", run firebase logout, then firebase login, and try again.
To run all unit and widget tests use the following command:
$ flutter test --coverage --test-randomize-ordering-seed randomTo view the generated coverage report you can use lcov.
# Generate Coverage Report
$ genhtml coverage/lcov.info -o coverage/
# Open Coverage Report
$ open coverage/index.htmlThis project relies on flutter_localizations and follows the official internationalization guide for Flutter.
- To add a new localizable string, open the
app_en.arbfile atlib/l10n/arb/app_en.arb.
{
"@@locale": "en",
"counterAppBarTitle": "Counter",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
}
}
- Then add a new key/value and description
{
"@@locale": "en",
"counterAppBarTitle": "Counter",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
},
"helloWorld": "Hello World",
"@helloWorld": {
"description": "Hello World Text"
}
}
- Use the new string
import 'package:happy_day/l10n/l10n.dart';
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Text(l10n.helloWorld);
}Update the CFBundleLocalizations array in the Info.plist at ios/Runner/Info.plist to include the new locale.
...
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>es</string>
</array>
...- For each supported locale, add a new ARB file in
lib/l10n/arb.
├── l10n
│ ├── arb
│ │ ├── app_en.arb
│ │ └── app_es.arb
- Add the translated strings to each
.arbfile:
app_en.arb
{
"@@locale": "en",
"counterAppBarTitle": "Counter",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
}
}
app_es.arb
{
"@@locale": "es",
"counterAppBarTitle": "Contador",
"@counterAppBarTitle": {
"description": "Texto mostrado en la AppBar de la página del contador"
}
}
To use the latest translations changes, you will need to generate them:
- Generate localizations for the current project:
flutter gen-l10n --arb-dir="lib/l10n/arb"Alternatively, run flutter run and code generation will take place automatically.
Happy Day is an AI-powered structured day planner built with Flutter. It helps users manage daily structures (routines/tasks) and increase productivity. The application uses Flutter's bloc pattern for state management and follows modern architecture patterns.
- Daily structures management (routines/tasks)
- AI-assisted step generation (using Vertex AI)
- Dark mode support
- Multi-language support
- State Management: Uses
flutter_blocwith both Cubit and Bloc classes - Immutability: Uses
freezedfor creating immutable state classes - Navigation: Implements
go_routerfor routing - Storage: Uses repository pattern with local storage
- Theming: Supports both light and dark themes with dynamic switching
- Error Handling: Uses
flutter_fimberfor logging - Notifications: Uses
toastificationfor user feedback - Localization: Supports multiple languages (English, German, Spanish, Polish)
- Firebase Integration: Analytics, Crashlytics, and Vertex AI
Happy Day uses Flutter flavors to manage different environments (development, staging, production). Each flavor has its own entry point and configuration.
Main Entry Point Example (main_development.dart):
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fimber/flutter_fimber.dart';
import 'package:happy_day/bootstrap.dart';
import 'package:happy_day/firebase_options_dev.dart';
import 'package:happy_day/shared/logging.dart';
import 'package:local_storage_structures_api/local_storage_structures_api.dart';
import 'package:onboarding_repository/onboarding_repository.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:steps_generation_repository/steps_generation_repository.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
final onboardingRepository = OnboardingRepository(
sharedPreferences: sharedPreferences,
);
final structuresApi = LocalStorageStructuresApi(
plugin: sharedPreferences,
);
Bloc.observer = const LoggingBlocObserver();
final logTree = DebugTree(
logLevels: DebugTree.defaultLevels.toList()..add('V'),
useColors: true,
);
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
const stepsGenerationRepository = FakeStepsGenerationRepository();
return bootstrap(
structuresApi: structuresApi,
stepsGenerationRepository: stepsGenerationRepository,
onboardingRepository: onboardingRepository,
onFatalError: (details) {
Fimber.e(details.exceptionAsString(), stacktrace: details.stack);
},
onError: (error, stackTrace) {
Fimber.e(error.toString(), stacktrace: stackTrace);
},
logTree: logTree,
sendCrashlyticsReports: false,
);
}Bootstrap Function:
Future<void> bootstrap({
required StructuresApi structuresApi,
required StepsGenerationRepository stepsGenerationRepository,
required OnboardingRepository onboardingRepository,
required void Function(FlutterErrorDetails) onFatalError,
required void Function(Object, StackTrace) onError,
required LogTree logTree,
required bool sendCrashlyticsReports,
}) async {
FlutterError.onError = onFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
onError(error, stack);
return true;
};
Fimber.plantTree(logTree);
final structuresRepository =
StructuresRepository(structuresApi: structuresApi);
const emailRepository = EmailRepository();
if (sendCrashlyticsReports) {
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
}
const wiredashProjectId = String.fromEnvironment('WIREDASH_PROJECT_ID');
const wiredashSecret = String.fromEnvironment('WIREDASH_SECRET');
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorageDirectory.web
: HydratedStorageDirectory((await getTemporaryDirectory()).path),
);
runApp(
Wiredash(
projectId: wiredashProjectId,
secret: wiredashSecret,
child: App(
structuresRepository: structuresRepository,
stepsGenerationRepository: stepsGenerationRepository,
onboardingRepository: onboardingRepository,
emailRepository: emailRepository,
),
),
);
}The app follows the repository pattern to provide a clean abstraction layer over the data sources. Here's an example from the EditStructureBloc:
class EditStructureBloc extends Bloc<EditStructureEvent, EditStructureState> {
EditStructureBloc({
required StructuresRepository structuresRepository,
required StepsGenerationRepository stepsGenerationRepository,
required Structure? initialStructure,
String? languageCode,
}) : _structuresRepository = structuresRepository,
_stepsGenerationRepository = stepsGenerationRepository,
super(
EditStructureState(
// Initial state setup with data from repository
structureId: initialStructure?.id ?? Structure.newId,
initialStructure: initialStructure,
title: initialStructure?.title ?? '',
description: initialStructure?.description ?? '',
color: initialStructure != null
? initialStructure.color
: Colors.deepPurple,
steps: initialStructure != null
? structuresRepository.getSteps(initialStructure.id)
: [],
languageCode: languageCode ?? 'en',
weekDays: initialStructure != null
? initialStructure.weekDays
: [true, true, true, true, true, false, false],
),
) {
// Event handlers
}
final StructuresRepository _structuresRepository;
final StepsGenerationRepository _stepsGenerationRepository;
// Example of repository method usage
Future<void> _onSubmitted(Emitter<EditStructureState> emit) async {
emit(state.copyWith(editStatus: EditStructureStatus.loading));
final structure = (state.initialStructure ?? Structure.empty()).copyWith(
id: state.structureId,
title: state.title,
description: state.description,
// More fields...
);
try {
await _structuresRepository.saveStructure(structure, state.steps);
emit(state.copyWith(editStatus: EditStructureStatus.success));
} catch (error, stackTrace) {
Fimber.e(
'Failed to submit structure',
ex: error,
stacktrace: stackTrace,
);
emit(state.copyWith(editStatus: EditStructureStatus.failure));
}
}
}The app uses go_router for navigation with named routes and redirection logic:
Router Configuration:
final goRouter = GoRouter(
initialLocation: '/${RoutesNames.onboarding}',
redirect: redirectToOnboardingIfNotCompleted,
routes: [
_onboardingRoute,
_dailyStructuresRoute,
_structureDetailsRoute,
_editStructureRoute,
_settingsRoute,
],
);
FutureOr<String?> redirectToOnboardingIfNotCompleted(
BuildContext context,
GoRouterState state,
) {
final isOnboardingCompleted =
context.read<OnboardingCubit>().state.isCompleted;
final isOnboardingRoute =
state.matchedLocation == '/${RoutesNames.onboarding}';
if (isOnboardingCompleted && isOnboardingRoute) {
return '/${RoutesNames.dailyStructures}';
}
if (!isOnboardingCompleted && !isOnboardingRoute) {
return '/${RoutesNames.onboarding}';
}
return null;
}Route Definition Example:
final _editStructureRoute = GoRoute(
name: RoutesNames.editStructure,
path: '/${RoutesNames.editStructure}',
builder: (context, state) => EditStructurePage(
initialStructure: state.extra as Structure?,
),
);The app integrates Wiredash for gathering user feedback, which is crucial for analytics and understanding user needs:
Wiredash Setup in Bootstrap:
runApp(
Wiredash(
projectId: wiredashProjectId,
secret: wiredashSecret,
child: App(
structuresRepository: structuresRepository,
stepsGenerationRepository: stepsGenerationRepository,
onboardingRepository: onboardingRepository,
emailRepository: emailRepository,
),
),
);Feedback Button in Settings:
ListTile(
key: const Key('sendFeedback'),
title: Text(l10n.sendFeedback),
trailing: const Icon(Icons.feedback_outlined),
onTap: () => Wiredash.of(context).show(inheritMaterialTheme: true),
),The app uses Flutter Fimber for logging and Firebase Crashlytics for error reporting in production:
LoggingBlocObserver:
class LoggingBlocObserver extends BlocObserver {
const LoggingBlocObserver();
@override
void onCreate(BlocBase<dynamic> bloc) {
Fimber.v('Creating instance of ${bloc.runtimeType}');
super.onCreate(bloc);
}
@override
void onEvent(Bloc<dynamic, dynamic> bloc, Object? event) {
Fimber.v(event.toString());
super.onEvent(bloc, event);
}
@override
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
Fimber.v('${change.nextState}');
super.onChange(bloc, change);
}
@override
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
Fimber.e(
'Error in ${bloc.runtimeType}',
ex: error,
stacktrace: stackTrace,
);
super.onError(bloc, error, stackTrace);
}
@override
void onClose(BlocBase<dynamic> bloc) {
Fimber.v('Closing instance of ${bloc.runtimeType}');
super.onClose(bloc);
}
}CrashlyticsTree for Logging:
class CrashlyticsTree extends LogTree {
@override
List<String> getLevels() => ['I', 'W', 'E'];
@override
void log(
String level,
String message, {
String? tag,
dynamic ex,
StackTrace? stacktrace,
}) {
if (level == 'E') {
FirebaseCrashlytics.instance.recordError(
ex ?? message,
stacktrace ?? _getCustomStackTrace(),
reason: ex != null ? message : null,
);
} else {
FirebaseCrashlytics.instance.log(message);
if (ex != null || stacktrace != null) {
FirebaseCrashlytics.instance
.log('exception: $ex, stacktrace: $stacktrace');
}
}
}
}The app uses Freezed for immutable state management, combined with BLoC pattern:
State Class Example:
@freezed
class EditStructureState with _$EditStructureState {
const factory EditStructureState({
required String structureId,
@Default(EditStructureStatus.initial) EditStructureStatus editStatus,
@Default(StepsGenerationStatus.initial)
StepsGenerationStatus stepsGenerationStatus,
@Default(StructureDeletionStatus.initial)
StructureDeletionStatus deletionStatus,
@Default('en') String languageCode,
Structure? initialStructure,
@Default('') String title,
@Default('') String description,
@Default([]) List<StructureStep> steps,
@Default(Colors.deepPurple) Color color,
@Default([true, true, true, true, true, false, false]) List<bool> weekDays,
}) = _EditStructureState;
}
extension EditStructureStateX on EditStructureState {
bool get isNewStructure => initialStructure == null;
bool get isTitleValid => title.isNotEmpty;
}Event Class with Freezed:
@freezed
class EditStructureEvent with _$EditStructureEvent {
const factory EditStructureEvent.submitted() = _Submitted;
const factory EditStructureEvent.titleChanged(String title) = _TitleChanged;
const factory EditStructureEvent.descriptionChanged(String description) =
_DescriptionChanged;
const factory EditStructureEvent.colorChanged(Color color) = _ColorChanged;
// More events...
}
extension EditStructureEventX on EditStructureEvent {
bool get isSubmitted => this is _Submitted;
bool get isTitleChanged => this is _TitleChanged;
bool get isDescriptionChanged => this is _DescriptionChanged;
// More getters...
String get title => (this as _TitleChanged).title;
String get description => (this as _DescriptionChanged).description;
// More field accessors...
}This implementation with Freezed provides:
- Immutable state with copyWith functionality
- Type-safe pattern matching
- Extension methods for easier state checking and property access
- Generated equality and toString methods
- Reduced boilerplate code
The app demonstrates a well-structured Flutter application using modern state management techniques with BLoC/Cubit patterns, clean architecture principles, and proper error handling. It's designed to be maintainable, testable, and scalable for future features.
