Skip to content

refactor(dining): Collapsable Favourites, Light/Dark Mode issue#657

Open
DaChelimo wants to merge 41 commits intomainfrom
add-favorites
Open

refactor(dining): Collapsable Favourites, Light/Dark Mode issue#657
DaChelimo wants to merge 41 commits intomainfrom
add-favorites

Conversation

@DaChelimo
Copy link
Contributor

Design Decisions: Dining Feature Refactor

This document outlines the architectural choices made during the refactoring of the Dining feature from legacy XML to modern Jetpack Compose. The goal is to explain the why behind these patterns, not just the what, to help the team understand and adopt them for future development.

1. The "Hybrid" Strategy: Using Compose inside a Fragment

A common question is, "Why are we writing Compose inside a Fragment instead of a pure Compose Activity?"

The answer is incremental migration.

Our application's navigation is built on an established Fragment-based system. Rewriting the entire navigation graph at once would be a massive, high-risk undertaking. By hosting our Jetpack Compose code within a Fragment (DiningFragment), we effectively create a container for the new UI.

How it Works:

  • The Wrapper: The DiningFragment acts as a bridge. It integrates with the existing navigation graph and lifecycle of the app.
  • The Content: Inside its onCreateView, it returns a ComposeView. This special view is the entry point where all our modern, declarative UI code lives.

This hybrid approach allows us to modernize one screen at a time, delivering value faster and reducing the risk of a "big bang" rewrite. Think of it as renovating one room in a house; we can complete the work in that room without having to tear down the entire house structure.

2. Unidirectional Data Flow (UDF) and StateFlow

To make our UI logic predictable and easy to debug, we've adopted a strict Unidirectional Data Flow (UDF) pattern. Data flows in only one direction, preventing the chaotic state management issues common in complex MVC architectures.

The Data Journey:

Repository -> ViewModel -> UI (Compose)

  1. Repository (DiningRepo): This is the single source of truth for our data. It fetches dining hall information from the network and abstracts away the data source from the rest of the app.
  2. ViewModel (DiningViewModel): This is the single source of truth for our UI state. It holds the data fetched from the repository, applies any necessary business logic (like sorting), and exposes it to the UI.
  3. UI (DiningHallListScreen): The UI is a passive observer. It receives state from the ViewModel and renders it. It never modifies the state directly. Instead, it sends user events (like a button click) up to the ViewModel to process.

Why StateFlow?
We use StateFlow in the ViewModel to hold the UI state. Think of StateFlow as a live whiteboard. The ViewModel writes the current state to the board, and the UI just reads it.

  • It's a "hot" flow: It always has a value (the latest state). When the screen rotates and the UI recomposes, it immediately gets the most recent state without needing to re-fetch anything.
  • Lifecycle-Aware Collection: When used with collectAsState(), the UI automatically subscribes and unsubscribes based on its lifecycle, preventing memory leaks.

This ensures the UI is always a simple, passive reflection of the state held in the ViewModel.

3. The "Side-Effect" Loop with LaunchedEffect

In our DiningHallListScreen, you'll see a LaunchedEffect block that listens to snackBarEvent from the ViewModel. This pattern is essential for handling one-time events that are not part of the core UI state, known as side-effects.

What is a Side-Effect?
A side-effect is an action that happens as a result of a state change but isn't part of the UI's declarative description. Examples include showing a Snackbar, navigating to another screen, or logging an analytics event.

How Our Snackbar Pattern Works:

  1. Event Trigger: A user action (e.g., toggleFavourite) calls a function on the DiningViewModel.
  2. State Update: The ViewModel performs its logic and updates the snackBarEvent StateFlow to a new value (e.g., SnackBarEvent.Success("Added to favourites")).
  3. Recomposition: The UI, collecting snackBarEvent as state, recomposes.
  4. Side-Effect Execution: The LaunchedEffect(snackBarEvent) block sees that its key has changed. This triggers its code to run, which calls snackBarHostState.showSnackbar().
  5. State Reset: After the snackbar is handled (shown or dismissed), we call viewModel.resetSnackBarEvent() to return the state to SnackBarEvent.None. This is crucial to prevent the snackbar from re-appearing every time the UI recomposes (e.g., on screen rotation).

This "bridge" is necessary because Compose is designed to convert state into UI. For fire-and-forget actions, LaunchedEffect provides a lifecycle-aware coroutine scope where we can safely perform these operations in response to a state change.

4. Dependency Injection with Hilt

We've introduced Hilt to manage dependencies automatically. For those new to Dependency Injection (DI), it's a pattern where objects receive their dependencies from an external source rather than creating them internally.

Analogy: Instead of a chef growing their own vegetables and raising their own cattle (manual instantiation), they simply request the ingredients they need from a supplier (Hilt).

How We Use It:

  • @HiltViewModel: This annotation tells Hilt how to create our DiningViewModel for us, making it available to any Hilt-enabled Fragment or Activity.
  • @Inject: We use this in the DiningViewModel's constructor to declare its "ingredients"—in this case, a DiningRepo and SharedPreferences. Hilt knows it must provide these dependencies whenever it creates a DiningViewModel.
  • @Module / @Provides: In NetworkModule.kt, we provide "recipes" that teach Hilt how to create complex objects like Retrofit or our DiningRepo interface, which it wouldn't know how to build otherwise.

The Benefits:

  1. Decoupling: The DiningViewModel doesn't know how to create a DiningRepo; it just knows it needs one. This makes our classes independent and modular.
  2. Simplified Testing: When testing the ViewModel, we can easily tell Hilt to inject a fake DiningRepo with sample data. This is impossible if the ViewModel creates the repository itself.
  3. Reduced Boilerplate: Hilt eliminates the need for manual ViewModel Factories and passing dependencies through long chains of constructors, cleaning up our codebase significantly.

Divak2004 and others added 30 commits August 31, 2025 21:30
◦Redesigned the 'Favorites' section as a collapsible animated header.
◦Centralized text styling and added new Cabin & Google Sans fonts.
◦Added dual light/dark mode previews for components.
◦Decoupled all business logic from the DiningFragment.
◦Split DiningRepo to better follow the Single Responsibility Principle.
◦Used combine to reactively derive UI state from multiple flows.
@baronhsieh2005 baronhsieh2005 self-requested a review February 9, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants