Skip to content
Ahmedatef5O5Public

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

213 Commits

Folders and files

Repository files navigation


NewsWave Banner

NewsWave App Icon NewsWave

Your world, curated β€” in real time. Ψ¨Ω„ΨΊΨͺΩƒΨŒ ΩƒΩ…Ψ§ Ψͺريد.

A production-ready Flutter news application delivering breaking headlines, personalized feeds, and offline-first reading β€” fully bilingual (English / Arabic) with reactive RTL support, dependency-free network resilience, and an architecture built to survive real-world failure modes, not just demo conditions.


Flutter Dart Supabase Hive NewsAPI i18n Architecture License: MIT


Features Β· Showcase Β· Screenshots Β· Architecture Β· Localization Β· Tech Stack Β· Getting Started Β· Project Structure Β· Roadmap



🌊 What is NewsWave?

NewsWave is a full-featured, production-ready news reader built entirely in Flutter. It aggregates live news from NewsAPI.org, organizes articles by category, and keeps your reading experience fast β€” even when you're offline, thanks to intelligent Hive-backed caching and a custom, dependency-free connectivity layer.

Whether you're exploring as a guest, or signed in with a personalized profile, NewsWave adapts to you. Authenticated users get a tailored onboarding experience where they choose their country, hobbies, and preferred news categories β€” shaping a feed that's truly theirs.

Built to showcase senior-level Flutter engineering: strict Clean Architecture with enforced Dependency Inversion, reactive state management, secure authentication, offline-first data with locale-aware caching, fully localized UI (EN/AR) with RTL support, resilient third-party API integration with rate-limit handling, and a UI that users will actually want to come back to.


✨ Features at a Glance

Category Features
πŸ” Authentication Sign Up, Sign In, Forgot Password, Deep-Link Password Recovery, Guest Mode
πŸ“° News Feed Breaking Headlines Carousel, "For You" paginated feed, 7 categories
πŸ” Search Full-text search with 500ms debounce, infinite scroll, locale-aware results
🌍 Localization 100% localized (zero hardcoded strings), reactive RTL/LTR layout switching, locale-aware caching, translated Favorites & Search
πŸ’Ύ Offline Hive local cache for headlines & recommended articles, keyed per-locale to prevent cross-language cache pollution
πŸ›°οΈ Connectivity Custom, dependency-free network polling with multi-endpoint probing β€” no connectivity_plus, no third-party plugin
🎨 Theming Light / Dark mode with animated toggle, locale-aware typography (Poppins / Cairo)
🧭 Onboarding 3-step profile wizard (name, country/hobby, category preferences)
πŸ“Œ Favorites Save articles locally, dynamically re-translated to match the active locale
πŸ—‚οΈ Categories General, Business, Technology, Sports, Health, Entertainment, Science
πŸš€ Navigation Custom page transitions: Fade, Slide, Shared-Axis
πŸ‘€ Profile Update display name, theme preference, language preference, sign out

🎬 Showcase

πŸ“‚ To add your GIFs: Record with ScreenToGif or LICEcap, drop the files into docs/assets/, then replace the placeholder links below with the relative paths or hosted URLs.


🌍 RTL / LTR Language Switching

Language Switch RTL/LTR
Live locale toggle β€” layout direction, typography, and content all flip reactively with zero manual rebuilds.
πŸ“‘ Offline Resilience & Caching

Offline Resilience
Network loss is detected via custom polling, the offline banner appears automatically, and cached articles keep the feed readable.

![Language Switch RTL/LTR](insert_language_gif_link_here)
![Offline Resilience](insert_offline_gif_link_here)

πŸ” Authentication & Security

NewsWave uses Supabase Auth as the identity layer, with a clean AuthRepository abstraction so the data source can be swapped without touching the UI or state layers.

Auth flows supported:

  • βœ… Email + Password Sign Up (with optional phone number)
  • βœ… Email + Password Sign In
  • βœ… Forgot Password (email link)
  • βœ… Deep-link Password Recovery β€” the app intercepts AuthChangeEvent.passwordRecovery at the root level and routes directly to the Update Password screen
  • βœ… Guest Mode β€” browse the app without an account; guest state is a first-class AuthGuest state in the Cubit
  • βœ… Session persistence β€” restoreSession() on cold launch; user stays signed in across restarts
  • βœ… Profile creation is handled server-side via a Supabase trigger, ensuring atomicity between the auth.users table and the public profiles table
AuthUserState
 β”œβ”€β”€ AuthInitial
 β”œβ”€β”€ AuthLoading
 β”œβ”€β”€ AuthAuthenticated(user, profile)   ← may need onboarding
 β”œβ”€β”€ AuthGuest
 β”œβ”€β”€ AuthUnauthenticated
 └── AuthError(message)

πŸ“° Dynamic News Feed & Categories

The Home screen is split into two independent data streams:

1. Breaking Headlines Carousel

  • English locale fetches top-headlines from NewsAPI; Arabic locale fetches everything with curated Arabic-language category queries, since top-headlines has no reliable Arabic coverage
  • Displayed as a full-bleed horizontal carousel with glassmorphism overlays
  • Page size: 10 articles, cached in Hive under a locale-namespaced key (e.g. cached_headlines_business_ar) so switching languages never serves stale or mistranslated content

2. "For You" Recommended Feed

  • Mirrors the same locale-aware endpoint switch as the carousel, filtered by the active category
  • Rendered as a paginated list with a visible Page X of Y counter
  • Page size: 5 articles β€” each page individually cached (rec_page_N_<locale>)
  • Lazy-loads the next page when the user scrolls within 3 items of the bottom (infiniteScrollThreshold)

Categories are managed globally via CategoryCubit and surfaced as animated FilterChip pills, with category labels pulled from the active AppLocalizations. Switching categories triggers a fresh paginated fetch while preserving cached pages already loaded. Both the headlines and recommended feed cubits subscribe to LocaleCubit's stream and force a fresh fetch on every language change β€” guaranteeing the feed reflects the correct locale's source data, never a translated echo of the previous language's cache.


πŸ’Ύ Offline Support & Locale-Aware Caching

All caching is handled through a dedicated NewsCacheManager β€” a focused class extracted specifically to keep cache orchestration out of the repository layer (see Architecture Overview), backed by Hive (LocalDatabaseHive) in a single box keyed by articles_box.

Cache Key Pattern Contents
cached_headlines_<category>_<locale> Latest top-headlines articles, namespaced per category and language
rec_page_N_<locale> Individual page N of recommended articles, namespaced per language
tr_<md5(article_id)> Per-article translated title/description/content, cleared automatically on language switch
favorite_articles User-saved articles (persisted indefinitely, re-translated on read)

This locale-aware keying scheme was a deliberate architectural fix: in earlier iterations, switching the app language could surface cached English articles that were then machine-translated on top of stale data instead of fetching fresh, language-appropriate sources. Every cache key now carries the active locale, and every locale switch triggers a forced refetch β€” eliminating that entire class of bug.

Articles are serialized as @HiveType objects with generated adapters, ensuring blazing-fast reads and zero JSON overhead on the hot path. The Article model includes computed extensions (cleanContent, cleanDescription, formattedDate, shortAuthor) that strip NewsAPI's HTML fragments and truncation markers before any data hits the UI.


🌍 Localization & i18n

NewsWave is 100% localized into English and Arabic β€” zero hardcoded UI strings, anywhere β€” driven by a reactive LocaleCubit rather than a static, restart-required setting.

  • flutter_localizations + ARB files β€” every UI string (navigation, dialogs, errors, validation messages, category labels, hobby labels, even fallback titles for untitled articles) is generated through AppLocalizations. Hobby and category labels are modeled as exhaustive enums with a localize(AppLocalizations) method, so the compiler β€” not a runtime crash β€” catches any missing translation.
  • LocaleCubit β€” a Cubit<Locale> that persists the chosen language to a dedicated Hive settings_box, restores it on cold launch, and exposes a one-tap toggle() between English and Arabic. A defensive _initialized guard ensures the Hive box is always ready before the first write, even if setLocale() is called in a race with init().
  • Reactive RTL/LTR layout β€” Directionality flips automatically with the locale; layout-sensitive widgets use EdgeInsetsDirectional and locale-aware icon mirroring (e.g. back/forward chevrons) instead of hardcoded left/right values
  • Locale-aware typography β€” AppTheme.of(locale: ..., brightness: ...) swaps the active font family (Poppins for English, Cairo for Arabic) at the ThemeData level
  • Locale-aware data fetching, everywhere β€” HomeCubit, HeadlinesCubit, FavoritesCubit, and SearchCubit all subscribe to LocaleCubit's stream and react automatically when the language changes:
    • Headlines and the recommended feed force a fresh, correctly-sourced fetch (not a translated stale cache)
    • Favorites are dynamically re-translated on read β€” an article saved while browsing in English is transparently localized when you revisit your saved list in Arabic
    • Search results are locale-aware β€” the active language is passed as a query parameter to NewsAPI, and any in-progress search automatically re-runs when the language changes
  • On-device article translation β€” articles fetched for the Arabic locale are passed through an ArticleTranslationRepository, which translates and caches the title/description/content per article so repeat views don't re-hit the translation API, and purges its cache automatically on every locale switch to prevent unbounded growth
  • Language picker β€” a dedicated LanguagePickerDialog lets the user choose their language explicitly from the Drawer or Profile Settings, in addition to the quick toggle() shortcut
LocaleCubit
 β”œβ”€β”€ init()        β†’ reads persisted locale from Hive on app start
 β”œβ”€β”€ setLocale(_)  β†’ persists + clears translation cache + emits a new Locale
 └── toggle()      β†’ flips between Locale('en') and Locale('ar')

πŸ” Advanced Search & Pagination

Search is built on a dedicated SearchCubit with these production-level details:

  • Debouncing β€” 500ms searchDebounce timer; no API call fires until the user pauses typing
  • Locale-aware β€” subscribes to LocaleCubit's stream and transparently re-runs the active query when the language changes, passing the current locale to NewsAPI as a language parameter
  • SearchService β€” a thin, plain-Dio service wired through the service locator (a @RestApi-annotated SearchServicesRetrofit interface also exists in the codebase as an alternative typed-client implementation, generated via build_runner)
  • Paginated results β€” page size 15, with a paginationWindowSize of 5 for the visible page controls
  • Max API results cap β€” hard limit of 100 results respects NewsAPI's free-tier constraints
  • Infinite scroll β€” triggers at 3 items from the bottom of the current page
  • Search scope β€” queries the /v2/everything endpoint with searchIn: 'title' for higher-precision results

πŸ›°οΈ Network Resilience β€” Zero Third-Party Connectivity Plugins

One of the most deliberate architectural decisions in NewsWave: no connectivity_plus, no platform connectivity plugin, no native channel β€” at all.

Most Flutter apps reach for a connectivity package that reports interface state (is Wi-Fi/cellular "on?") rather than actual internet reachability β€” meaning a phone connected to a Wi-Fi network with no real internet access (a common captive-portal scenario in airports, hotels, and cafΓ©s) gets falsely reported as "online."

NewsWave's NetworkInfo abstraction solves this properly:

  • Multi-endpoint HTTP probing β€” NetworkInfoImpl races a small Dio client against several independent endpoints simultaneously (the app's own API host, plus two well-known low-latency probes), resolving true the instant any one of them responds β€” and false only if all of them fail
  • Captive-portal aware β€” because the check is a real HTTP round-trip rather than a raw socket or DNS lookup, it correctly detects networks that appear connected but silently block traffic
  • In-flight de-duplication β€” concurrent callers share a single pending check via a cached Future, so a burst of isConnected calls never fires redundant network requests
  • ConnectivityCubit β€” a thin Cubit<bool> that performs an immediate check on construction, then polls NetworkInfo on a Timer.periodic interval, emitting only on actual state changes; the timer is cleanly cancelled in close()
  • Fully unit-tested β€” NetworkInfoImpl ships with a dedicated test suite covering first-probe-success, total-failure, partial-failure fallthrough, and concurrent-call de-duplication, using a fake HttpClientAdapter β€” no real network access required to run the suite
NetworkInfo (abstract)
 └── NetworkInfoImpl
       β”œβ”€β”€ races N probe URLs via Dio, completes on first success
       β”œβ”€β”€ in-flight check de-duplication (single shared Future)
       └── consumed by ConnectivityCubit via Timer.periodic polling

🎨 Theming & UI/UX

  • Light & Dark themes fully defined via AppTheme.light / AppTheme.dark using Material 3 ColorScheme
  • Brand palette: Primary #1A73E8 (Google Blue), Dark #0D47A1, Accent #FF6B35
  • Typography: locale-aware font selection β€” Poppins for English, Cairo for Arabic β€” applied at the ThemeData level via AppTheme.of(locale: ...)
  • Animated shimmer β€” custom ShimmerBox with AnimationController for skeleton loading states on both carousel cards and list rows
  • Theme persistence β€” ThemeCubit saves and restores the user's mode preference via Hive
  • Animated background β€” auth screens feature a subtle _AnimatedBackground widget using SingleTickerProviderStateMixin
  • Custom page transitions β€” Fade (auth flows), Slide (list views), Shared-Axis (article detail)
  • Optimized widget rebuilds β€” the root MaterialApp is wrapped in BlocBuilder<ThemeCubit> / BlocBuilder<LocaleCubit> with explicit buildWhen predicates, so a theme or locale change rebuilds only what actually depends on it instead of tearing down the entire navigation tree
  • Isolated dev tooling β€” DevicePreview lives behind its own dedicated main_dev.dart entry point, completely absent from the production main.dart build target β€” multi-device preview during development with zero risk of shipping debug-only overhead

πŸ“± Screenshots


sign_up_view
Sign Up
sign_in_view
Sign In
reset_password_view
Forgot Password
set_new_password
Update Password
onboarding_step_one
Onboarding β€” Step 1
Name & Details
onboarding_step_two
Onboarding β€” Step 2
Country & Hobby
onboarding_step_three
Onboarding β€” Step 3
News Categories
home_view
Home View
(Light Mode)
home_feed_view
Home View
(Dark Mode)
article_details_view
Article Details
search_view
Search View
saved_articles_view
Saved Articles
drawer_app
App Drawer
headlines_view_all
Headlines (View All)
for_you_pagination
For You Section
profile_settings_view
Profile Settings

πŸ—οΈ Architecture Overview

NewsWave follows a strict, feature-first Clean Architecture with a shared core module and enforced Dependency Inversion (DIP) at every repository boundary:

lib/
β”œβ”€β”€ core/                          # Shared infrastructure
β”‚   β”œβ”€β”€ constants/                 # AppConstants, NewsCategory enum
β”‚   β”œβ”€β”€ cubits/                    # CategoryCubit (global)
β”‚   β”œβ”€β”€ cache/                     # NewsCacheManager β€” extracted cache orchestration (SRP)
β”‚   β”œβ”€β”€ di/                        # service_locator.dart (GetIt setup)
β”‚   β”œβ”€β”€ exceptions/                # NewsException sealed hierarchy + NewsErrorCode
β”‚   β”œβ”€β”€ locale/                    # LocaleCubit, LanguagePickerDialog, LanguageOption
β”‚   β”œβ”€β”€ translation/                # TranslationService (interface) + LibreTranslate / MyMemory impls
β”‚   β”œβ”€β”€ network/                   # NetworkInfo (interface), NetworkInfoImpl (HTTP-probe), ConnectivityCubit
β”‚   β”œβ”€β”€ browser/                   # In-app WebView (InAppBrowserView)
β”‚   β”œβ”€β”€ repositories/               # HomeRepository β€” depends only on abstractions
β”‚   β”œβ”€β”€ models/                    # Article, Source (Hive + JSON)
β”‚   β”œβ”€β”€ pagination/                # PaginationMeta, pagination bar widgets
β”‚   β”œβ”€β”€ router/                    # AppRouter, AppRoutes
β”‚   β”œβ”€β”€ secrets/                   # AppSecrets (reads .env)
β”‚   β”œβ”€β”€ services/                  # LocalDatabaseHive (Hive init, box helpers)
β”‚   β”œβ”€β”€ supabase/                  # AuthLocalDataSource, AuthRemoteDataSource, exceptions
β”‚   β”œβ”€β”€ theme/                     # AppTheme, AppColors, ThemeCubit
β”‚   β”œβ”€β”€ helpers/                    # EmptyState, ErrorState, ShimmerBox, category l10n extension
β”‚   └── widgets/                   # Shared: ArticleCard, SaveButton, ShareButton,
β”‚                                  #         OfflineBanner, AppDrawer
β”‚
β”œβ”€β”€ l10n/                          # ARB files + generated AppLocalizations + context.l10n extension
β”‚                                  #   + news_error_x.dart (NewsException β†’ localized message)
β”‚
└── features/
    β”œβ”€β”€ auth/                      # AuthCubit, AuthRepository, Auth + Onboarding views
    β”‚   β”œβ”€β”€ cubit/                 #   auth_cubit.dart Β· auth_listener_cubit.dart
    β”‚   β”œβ”€β”€ data/                  #   auth_repository.dart (interface + impl)
    β”‚   └── views/                 #   SignIn, SignUp, ForgotPassword, UpdatePassword, Onboarding
    β”œβ”€β”€ home/                      # HomeView, HomeCubit (headlines + paginated "For You" feed)
    β”‚   β”œβ”€β”€ domain/repositories/   #   home_repository_contract.dart β€” the DIP boundary
    β”‚   β”œβ”€β”€ cubit/                 #   home_cubit.dart (single cubit drives both data streams)
    β”‚   β”œβ”€β”€ services/              #   home_services.dart β€” implements the contract, Dio-backed
    β”‚   └── views/                 #   home_view.dart, article_detail_view.dart
    β”œβ”€β”€ Headlines/                 # HeadlinesCubit + HeadlinesView (full-screen, paginated list)
    β”œβ”€β”€ search/                    # SearchCubit (locale-aware), SearchService, SearchView
    β”œβ”€β”€ favorites/                 # FavoritesCubit (locale-aware, Hive-backed), FavoritesView
    β”œβ”€β”€ profile/                   # ProfileSettingsView (theme, language, account)
    └── splash/                    # SplashView (session restore + routing logic)

Data flow per feature:

UI (View)
  └─► Cubit (state logic)
        └─► Repository (interface)
              └─► Impl (Supabase / Dio / Hive)

Dependency Inversion, enforced β€” not just described:

  • HomeRepository depends on four abstractions, never a concrete class: HomeRepositoryContract (the remote data-source boundary, implemented by HomeServices), NewsCacheManager (cache orchestration, extracted from the repository to keep it focused on a single responsibility), NetworkInfo, and ArticleTranslationRepository. Swapping the news provider, the cache backend, or the translation engine requires touching exactly one binding in service_locator.dart β€” nothing else in the codebase needs to know.
  • TranslationService is an abstract interface with two interchangeable implementations shipped side by side β€” LibreTranslationService and MyMemoryTranslationService β€” proving the abstraction isn't theoretical. The active implementation, plus its dispose() callback for clean resource teardown, is selected in exactly one place.
  • Domain errors are modeled as a sealed NewsException hierarchy (OfflineNoCacheException, NetworkTimeoutException, ServerException, …) instead of raw strings β€” keeping user-facing copy entirely out of the data layer and resolved to localized text only at the presentation boundary via a single resolveErrorMessage() helper.

The core layer has zero dependency on any feature. Features may only depend on core, never on each other. All cross-cutting singletons β€” Dio, NetworkInfo, NewsCacheManager, LocaleCubit, ThemeCubit, repositories, and services (each registered with an explicit dispose: callback where a resource needs closing) β€” are wired through a single setupServiceLocator() entry point using GetIt.


πŸ› οΈ Tech Stack

Layer Technology Purpose
Framework Flutter 3.x Cross-platform UI
Language Dart 3.x Null-safe, strong-typed
State Management flutter_bloc / Cubit Predictable reactive state, with buildWhen-optimized rebuilds
Dependency Injection get_it Centralized service locator (setupServiceLocator()), with explicit dispose: callbacks for resource cleanup
Backend / Auth Supabase Auth, Postgres DB, Realtime
News Data NewsAPI v2 Headlines, everything, and search endpoints
Localization flutter_localizations + ARB 100% of UI strings, reactive locale switching, locale-aware caching
Translation TranslationService interface β€” LibreTranslate (primary) / MyMemory (alternate impl) Chunked, field-merged article translation for the Arabic locale, Hive-cached
HTTP Client Dio Shared instance registered as a lazy singleton in the DI container; also powers the connectivity probe
Local Database Hive NoSQL key-value, locale-namespaced offline cache, theme & locale persistence
Environment flutter_dotenv Secrets management via .env
Routing onGenerateRoute Named routes with custom transitions
Fonts Poppins (EN) / Cairo (AR) Locale-aware brand typography
Dev Tools DevicePreview Isolated in a dedicated main_dev.dart entry point β€” zero footprint in production builds
Testing flutter_test Unit tests for network probing logic with a fake HttpClientAdapter
Code Generation build_runner Hive adapters, generated AppLocalizations

πŸš€ Getting Started

Prerequisites

  • Flutter SDK >=3.0.0
  • Dart SDK >=3.0.0
  • A NewsAPI.org API key (free tier)
  • A Supabase project with Auth enabled

1. Clone the Repository

git clone https://github.com/<your-username>/news_app.git
cd news_app

2. Configure Environment Variables

Create a .env file in the project root:

# .env
NEWS_API_KEY=your_newsapi_key_here
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_ANON_KEY=your_supabase_anon_key_here

⚠️ Never commit .env to version control. It is already listed in .gitignore.

3. Supabase Setup

In your Supabase dashboard:

  1. Enable Email Auth under Authentication β†’ Providers.
  2. Create a profiles table with at least these columns:
create table public.profiles (
  id          uuid references auth.users on delete cascade primary key,
  first_name  text,
  last_name   text,
  country     text,
  hobby       text,
  categories  text[],
  is_onboarded boolean default false
);
  1. Create a trigger to auto-insert a profile row on new user sign-up:
create or replace function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id)
  values (new.id);
  return new;
end;
$$ language plpgsql security definer;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();
  1. Enable deep-link redirect for password recovery (add your app's redirect URL in Authentication β†’ URL Configuration).

4. Install Dependencies

flutter pub get

5. Run Code Generation

Required for Hive TypeAdapters:

dart run build_runner build --delete-conflicting-outputs

Generated AppLocalizations classes are produced automatically by the Flutter toolchain from the ARB files under lib/l10n/ on flutter pub get / flutter run β€” no extra command needed, as long as generate: true is set under flutter: in pubspec.yaml.

6. Run the App

flutter run

To run on a specific device:

flutter run -d <device_id>

πŸ’‘ To preview the app with DevicePreview for rapid multi-device testing, run the isolated dev entry point instead: flutter run -t lib/main_dev.dart. The production target (lib/main.dart) never imports DevicePreview at all.


πŸ“ Project Structure

news_app/
β”œβ”€β”€ .env                          # ← create this (see setup)
β”œβ”€β”€ pubspec.yaml
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ main.dart                 # Production entry: Supabase, Hive, DI, BlocProviders, MaterialApp
β”‚   β”œβ”€β”€ main_dev.dart             # Dev-only entry: wraps NewsWave() in DevicePreview, isolated from prod
β”‚   β”œβ”€β”€ core/
β”‚   β”‚   β”œβ”€β”€ constants/
β”‚   β”‚   β”‚   β”œβ”€β”€ app_constants.dart        # API urls, pagination sizes, cache key prefixes, NewsCategory enum
β”‚   β”‚   β”‚   └── app_images.dart           # Asset path constants
β”‚   β”‚   β”œβ”€β”€ cubits/
β”‚   β”‚   β”‚   └── category_cubit.dart       # Global active-category state
β”‚   β”‚   β”œβ”€β”€ cache/
β”‚   β”‚   β”‚   └── news_cache_manager.dart   # Locale-aware cache key generation + read/write (extracted SRP)
β”‚   β”‚   β”œβ”€β”€ di/
β”‚   β”‚   β”‚   └── service_locator.dart      # GetIt setup β€” every singleton/factory + dispose callback lives here
β”‚   β”‚   β”œβ”€β”€ exceptions/
β”‚   β”‚   β”‚   └── news_exceptions.dart      # Sealed NewsException hierarchy + Dio β†’ NewsException mapper
β”‚   β”‚   β”œβ”€β”€ locale/
β”‚   β”‚   β”‚   β”œβ”€β”€ locale_cubit.dart         # LocaleCubit (persists to Hive `settings_box`, purges translation cache)
β”‚   β”‚   β”‚   β”œβ”€β”€ language_option.dart
β”‚   β”‚   β”‚   └── language_picker_dialog.dart
β”‚   β”‚   β”œβ”€β”€ translation/
β”‚   β”‚   β”‚   β”œβ”€β”€ translation_service.dart            # Abstract interface
β”‚   β”‚   β”‚   β”œβ”€β”€ libre_translation_service.dart       # LibreTranslate implementation (primary)
β”‚   β”‚   β”‚   β”œβ”€β”€ my_memory_translation_service.dart   # MyMemory implementation (alternate)
β”‚   β”‚   β”‚   β”œβ”€β”€ translation_exceptions.dart          # Rate-limit / network / offline exception types
β”‚   β”‚   β”‚   └── article_translation_repository.dart  # Chunked, field-merged translate + Hive cache
β”‚   β”‚   β”œβ”€β”€ network/
β”‚   β”‚   β”‚   β”œβ”€β”€ network_info.dart         # Abstract NetworkInfo + Dio multi-probe NetworkInfoImpl
β”‚   β”‚   β”‚   └── connectivity_cubit.dart   # Timer.periodic polling, no 3rd-party plugin
β”‚   β”‚   β”œβ”€β”€ browser/
β”‚   β”‚   β”‚   β”œβ”€β”€ view/in_app_browser_view.dart
β”‚   β”‚   β”‚   └── widgets/                  # error_page_widget.dart, nav_icon_widget.dart
β”‚   β”‚   β”œβ”€β”€ repositories/
β”‚   β”‚   β”‚   └── home_repository.dart      # Orchestrates 4 injected abstractions β€” zero concrete deps
β”‚   β”‚   β”œβ”€β”€ models/
β”‚   β”‚   β”‚   β”œβ”€β”€ article_model.dart        # @HiveType Article + Source + extensions
β”‚   β”‚   β”‚   β”œβ”€β”€ article_model.g.dart      # Generated Hive adapter
β”‚   β”‚   β”‚   β”œβ”€β”€ article_detail_args.dart
β”‚   β”‚   β”‚   └── news_api_response.dart
β”‚   β”‚   β”œβ”€β”€ pagination/
β”‚   β”‚   β”‚   β”œβ”€β”€ helpers/nav_button.dart
β”‚   β”‚   β”‚   └── widgets/                  # pagination_bar_widget.dart, load_more_footer.dart
β”‚   β”‚   β”œβ”€β”€ router/
β”‚   β”‚   β”‚   β”œβ”€β”€ app_router.dart           # onGenerateRoute + transition builders
β”‚   β”‚   β”‚   └── app_routes.dart           # Route name constants
β”‚   β”‚   β”œβ”€β”€ secrets/
β”‚   β”‚   β”‚   └── app_secrets.dart          # Reads .env values
β”‚   β”‚   β”œβ”€β”€ services/
β”‚   β”‚   β”‚   └── local_database_hive.dart  # Hive init, box helpers
β”‚   β”‚   β”œβ”€β”€ supabase/
β”‚   β”‚   β”‚   β”œβ”€β”€ auth_local_data_source.dart
β”‚   β”‚   β”‚   β”œβ”€β”€ auth_remote_data_source.dart
β”‚   β”‚   β”‚   └── auth_exception.dart       # Sealed AuthUserException hierarchy
β”‚   β”‚   β”œβ”€β”€ theme/
β”‚   β”‚   β”‚   β”œβ”€β”€ app_colors.dart           # Brand color palette
β”‚   β”‚   β”‚   β”œβ”€β”€ app_theme.dart            # Light + Dark ThemeData, locale-aware fonts
β”‚   β”‚   β”‚   β”œβ”€β”€ theme_cubit.dart          # ThemeCubit (persists to Hive)
β”‚   β”‚   β”‚   └── model/theme_model.dart
β”‚   β”‚   β”œβ”€β”€ helpers/
β”‚   β”‚   β”‚   β”œβ”€β”€ category_localization_x.dart  # NewsCategory β†’ localized label extension
β”‚   β”‚   β”‚   β”œβ”€β”€ empty_state.dart
β”‚   β”‚   β”‚   β”œβ”€β”€ error_state.dart
β”‚   β”‚   β”‚   └── shimmer_box.dart
β”‚   β”‚   └── widgets/
β”‚   β”‚       β”œβ”€β”€ drawer/
β”‚   β”‚       β”‚   β”œβ”€β”€ authenticated_header_widget.dart
β”‚   β”‚       β”‚   β”œβ”€β”€ guest_header_widget.dart
β”‚   β”‚       β”‚   β”œβ”€β”€ category_item.dart
β”‚   β”‚       β”‚   └── drawer_item.dart
β”‚   β”‚       β”œβ”€β”€ article_card_widget.dart
β”‚   β”‚       β”œβ”€β”€ save_button_widget.dart
β”‚   β”‚       β”œβ”€β”€ share_button_widget.dart
β”‚   β”‚       β”œβ”€β”€ offline_banner.dart
β”‚   β”‚       └── app_snack_bar.dart
β”‚   β”œβ”€β”€ l10n/
β”‚   β”‚   β”œβ”€β”€ app_en.arb                    # English source strings
β”‚   β”‚   β”œβ”€β”€ app_ar.arb                    # Arabic translations
β”‚   β”‚   β”œβ”€β”€ app_localizations_x.dart      # `context.l10n` extension
β”‚   β”‚   β”œβ”€β”€ news_error_x.dart             # NewsException β†’ localized message extension
β”‚   β”‚   └── gen_l10n/app_localizations.dart  # Auto-generated by Flutter
β”‚   └── features/
β”‚       β”œβ”€β”€ auth/
β”‚       β”‚   β”œβ”€β”€ cubit/
β”‚       β”‚   β”‚   β”œβ”€β”€ auth_cubit.dart       # signIn/Up/Out, guest, password reset
β”‚       β”‚   β”‚   └── auth_listener_cubit.dart
β”‚       β”‚   β”œβ”€β”€ data/
β”‚       β”‚   β”‚   β”œβ”€β”€ auth_repository.dart
β”‚       β”‚   β”‚   └── auth_repository_impl.dart
β”‚       β”‚   └── views/
β”‚       β”‚       β”œβ”€β”€ sign_in_view.dart
β”‚       β”‚       β”œβ”€β”€ sign_up_view.dart
β”‚       β”‚       β”œβ”€β”€ forgot_password_view.dart
β”‚       β”‚       β”œβ”€β”€ update_password_view.dart
β”‚       β”‚       └── onboarding_view.dart  # 3-page PageView wizard
β”‚       β”œβ”€β”€ home/
β”‚       β”‚   β”œβ”€β”€ domain/repositories/
β”‚       β”‚   β”‚   └── home_repository_contract.dart  # Abstraction HomeRepository depends on (DIP)
β”‚       β”‚   β”œβ”€β”€ cubit/home_cubit.dart     # Drives both headlines + paginated "For You" feed
β”‚       β”‚   β”œβ”€β”€ services/home_services.dart  # Implements the contract β€” Dio calls under the hood
β”‚       β”‚   └── views/
β”‚       β”‚       β”œβ”€β”€ home_view.dart
β”‚       β”‚       └── article_detail_view.dart
β”‚       β”œβ”€β”€ Headlines/
β”‚       β”‚   β”œβ”€β”€ cubit/headlines_cubit.dart
β”‚       β”‚   β”œβ”€β”€ widgets/                  # glass_category_row.dart, glass_category_card.dart
β”‚       β”‚   └── views/headlines_view.dart
β”‚       β”œβ”€β”€ search/
β”‚       β”‚   β”œβ”€β”€ cubit/search_cubit.dart   # Locale-aware: subscribes to LocaleCubit, re-queries on change
β”‚       β”‚   β”œβ”€β”€ services/
β”‚       β”‚   β”‚   β”œβ”€β”€ search_services.dart            # Plain-Dio SearchService (used by SearchCubit)
β”‚       β”‚   β”‚   └── search_services_retrofit.dart    # @RestApi alternative client
β”‚       β”‚   └── views/search_view.dart
β”‚       β”œβ”€β”€ favorites/
β”‚       β”‚   β”œβ”€β”€ cubit/favorite_cubit.dart # Locale-aware: re-translates saved articles on language switch
β”‚       β”‚   β”œβ”€β”€ services/favorite_services.dart
β”‚       β”‚   └── views/favorites_view.dart
β”‚       β”œβ”€β”€ profile/
β”‚       β”‚   └── views/profile_settings_view.dart  # Theme, language, account
β”‚       └── splash/
β”‚           └── view/splash_view.dart
β”œβ”€β”€ assets/
β”‚   └── images/icons/             # App icon assets
└── docs/
    β”œβ”€β”€ screenshots/              # ← add your screenshots here
    └── assets/                   # ← add your showcase GIFs here

βš™οΈ Key Implementation Details

Locale-aware cache architecture: NewsCacheManager was extracted from HomeRepository specifically to own cache-key generation and read/write logic in one place. Every key it produces is namespaced by both category and locale (e.g. cached_headlines_business_ar), and every HomeCubit/HeadlinesCubit locale-change handler forces a fresh fetch (forceRefresh: true) β€” together eliminating an entire category of "stale-language cache" bugs by construction.

Chunked, throttle-safe translation pipeline: Rather than firing one HTTP request per article field (which can mean 20–30+ simultaneous calls against a single feed), ArticleTranslationRepository first merges an article's title, description, and content into a single delimited request, then processes articles in small concurrent chunks (batches of 3) instead of firing the entire feed at once. Combined, these two optimizations cut outbound translation API calls by well over 80% compared to a naive per-field, per-article implementation β€” directly addressing the rate-limit constraints of free-tier translation services.

Resilient translation layer with typed exceptions: TranslationService is a true interface with interchangeable backends. The active implementation, LibreTranslationService, wraps every failure mode in a specific exception type β€” rate-limit (429) responses, network/offline failures, and unexpected non-200 responses are never silently swallowed or shown to the user as raw English error text. They're caught, classified, and resolved to a localized message via resolveErrorMessage() at the presentation layer.

Memory-safe resource lifecycle: Every http.Client and Dio-backed service that owns a connection pool exposes an explicit close(), and is registered in service_locator.dart with a matching dispose: callback so GetIt tears it down correctly. Every StreamSubscription created by a Cubit (locale changes, category changes, auth state changes) is cancelled in that Cubit's close() override β€” verified across FavoritesCubit, HomeCubit, HeadlinesCubit, SearchCubit, and AuthListenerCubit.

Optimized rebuild boundaries: The root NewsWave widget nests BlocBuilder<ThemeCubit> and BlocBuilder<LocaleCubit> with explicit buildWhen predicates (comparing languageCode rather than the full Locale object), so a theme toggle or language switch only rebuilds the MaterialApp subtree that actually needs new theme/locale data β€” not the entire navigator and route stack.

Pagination architecture: Each page of recommended articles is independently cached using a locale-namespaced key prefix (rec_page_N_<locale>). On cache hit, the UI renders instantly; on miss, a Dio call is made and the result is written to Hive before emission. This means pages the user has already seen are always available offline, in the correct language.

Auth deep-link handling: The navigatorKey is registered at the MaterialApp level and used inside main() to intercept AuthChangeEvent.passwordRecovery from Supabase's auth stream β€” routing the user to UpdatePasswordView regardless of which screen they're currently on.

Dio via dependency injection: A single Dio instance is built once inside service_locator.dart (base URL, auth header, conditional logging interceptor in non-release builds) and registered as a lazy singleton with GetIt. Every feature service (HomeServices, SearchService, the network probe, etc.) receives that same instance β€” or a purpose-built one with its own timeout policy β€” through constructor injection rather than instantiating its own client ad hoc.

Article model cleanliness: The ArticleExtension strips HTML tags, resolves HTML entities, and removes NewsAPI's [+N chars] truncation suffix from content β€” so the UI always receives clean, renderable text, in either language.


πŸ—ΊοΈ Roadmap

Based on the current codebase, these are the most impactful improvements that would push NewsWave even further past production-ready:

  • Push Notifications β€” Supabase Realtime or FCM integration for breaking news alerts, with locale-aware topic subscriptions
  • Social Auth β€” Google / Apple Sign-In via Supabase OAuth providers
  • Native Share Sheet polish β€” refine in-app browser navigation guards and share result messaging
  • Personalized Feed Algorithm β€” Weight recommended articles by onboarding category preferences
  • Self-Hosted Translation Backend β€” point LibreTranslationService at a self-hosted instance (Railway / Fly.io) instead of the public endpoint, with an automatic fallback to the alternate TranslationService implementation if the primary is unreachable
  • Cache TTL & Eviction β€” time- and size-bound eviction for both the news cache and the translation cache to cap long-term on-device storage growth
  • Broader Test Coverage β€” extend the existing NetworkInfoImpl unit-test suite to Cubit state tests (including locale-switch behavior) and widget golden tests for core components
  • CI/CD Pipeline β€” GitHub Actions for lint, test, and Flutter build on each PR
  • Additional Locales β€” extend the existing ARB-based localization beyond English / Arabic
  • Accessibility β€” Semantic labels, sufficient color contrast audit, dynamic text scaling

βœ… Connectivity Banner, Localization (i18n), Dependency Inversion on repositories, and Translation Rate-Limiting β€” all shipped: connectivity is detected via a dependency-free, multi-endpoint HTTP-probing NetworkInfo (zero third-party plugins), the app is 100% localized into English and Arabic with reactive RTL support and locale-aware caching, every repository depends on injected interfaces rather than concrete implementations, and article translation is chunked, field-merged, and protected by typed rate-limit exceptions.


🀝 Contributing

Contributions are welcome! Please open an issue first to discuss what you'd like to change. For pull requests:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/your-feature-name
  3. Commit your changes: git commit -m 'feat: add your feature'
  4. Push to the branch: git push origin feature/your-feature-name
  5. Open a Pull Request

πŸ“„ License

Distributed under the MIT License. See LICENSE for more information.


Built with ❀️ using Flutter


If this project helped you, please consider giving it a ⭐ β€” it means a lot!

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages