The astrology app market is huge, but many apps suffer from bloated interfaces, generic copy, and clunky navigation. I wanted to build an experience that felt premium, hyper-personalized, and highly responsive.
The result is AstroStella — an iOS and Android companion app that offers a clean, editorial take on horoscopes and cosmic guidance.
The Architecture & Tech Stack
AstroStella is built exclusively on Flutter, optimized for both iOS and Android through a single codebase.
- Framework: Flutter with Dart
- State Management:
flutter_bloc+get_it(Service Locator) - Routing:
go_routerfor deep linking and declarative navigation - Backend / Auth: Firebase Core & Auth
- Push Notifications: OneSignal (
onesignal_flutter) - In-App Purchases: RevenueCat (
purchases_flutter) - Localization: Multiple languages standard with
flutter_localizations
Key Architecture Decisions
1. Robust State Management with BLoC
AstroStella has several complex state domains intersecting: user authentication, active streaks, deeply nested app settings, in-app purchases, and language preferences.
To handle this cleanly, I used the BLoC (Business Logic Component) pattern coupled with a service locator (sl()). Inside main.dart, I wrap the entire app in a MultiBlocProvider:
// lib/main.dart
runApp(
MultiBlocProvider(
providers: [
BlocProvider(create: (context) {
final bloc = sl<LanguageBloc>();
bloc.add(const LoadLanguageEvent());
return bloc;
}),
BlocProvider(create: (context) => sl<OnboardingBloc>()),
BlocProvider(create: (context) => sl<AuthBloc>()..add(CheckAuthStatusEvent())),
BlocProvider(create: (context) => sl<DashboardBloc>()..add(DashboardStarted())),
BlocProvider(create: (context) => sl<PurchasesBloc>()),
BlocProvider(create: (context) => sl<StreakBloc>()..add(const StreakLoaded())),
],
child: const AstroStellaApp(),
),
);This strict separation of events and states guarantees that when a user buys a Pro subscription via RevenueCat, or switches language from English to French, the entire UI tree reacts predictably without spaghetti setState calls.
2. Global "Cosmic" Aesthetics
To achieve a consistent, premium feel, I bypassed standard Material design containers in favor of a global custom theme. The entire MaterialApp.router is wrapped in a GradientBackgroundContainer:
builder: (context, child) {
// Gradient-only background for clean editorial layout.
return GradientBackgroundContainer(
showStars: false,
enableShootingStars: false,
useThrottling: true,
showGrain: true,
child: child ?? const SizedBox.shrink(),
);
},By adding a subtle grain effect over optimized gradients (with heavy animations like shooting stars toggled off programmatically for a cleaner look), it achieves a distinct, moody "space" vibe while maintaining 60fps performance on low-end Androids.
3. Retention & Habit Engine
Astrology apps live and die by daily active users. Rather than spamming generic push notifications, I integrated OneSignal and built a custom RetentionNotificationService.
Coupled with the StreakBloc, the app encourages users to check their daily transits. Tracking "Streaks" gamifies the experience, rewarding users for consecutive daily check-ins.
What I Learned
1. RevenueCat in Flutter is a lifesaver.
Handling the discrepancies between Apple's App Store and Google Play billing natively is notoriously difficult. Testing initialization using RevenueCatService.testInitialization() in debug mode gave me massive confidence that permissions, paywalls, and customer info sync worked flawlessly across both platforms.
2. Localizations Delegating is non-negotiable.
AstroStella supports multiple locales. Building language swapping dynamically into the LanguageBloc (listening to LanguageLoaded state changes to set the MaterialApp locale) means translating the app feels instantaneous to the user. Retrospectively adding i18n to an app this large would have been a nightmare, so architecting it first was the right call.
Try It
Discover what the stars have in store for you. AstroStella is available across multiple languages on both app stores.