refactor(dining): Collapsable Favourites, Light/Dark Mode issue#657
Open
refactor(dining): Collapsable Favourites, Light/Dark Mode issue#657
Conversation
◦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.
…into david/insights-refactoring
Dining Insights Refactor
added profile to about page
added cassieym profile
Added vkakar to about page
Bold some stuff
Move old members to Alumni section
…le-android into add-favorites
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
DiningFragmentacts as a bridge. It integrates with the existing navigation graph and lifecycle of the app.onCreateView, it returns aComposeView. 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
StateFlowTo 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)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.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.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
StateFlowin the ViewModel to hold the UI state. Think ofStateFlowas a live whiteboard. The ViewModel writes the current state to the board, and the UI just reads it.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
LaunchedEffectIn our
DiningHallListScreen, you'll see aLaunchedEffectblock that listens tosnackBarEventfrom 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:
toggleFavourite) calls a function on theDiningViewModel.snackBarEventStateFlowto a new value (e.g.,SnackBarEvent.Success("Added to favourites")).snackBarEventas state, recomposes.LaunchedEffect(snackBarEvent)block sees that itskeyhas changed. This triggers its code to run, which callssnackBarHostState.showSnackbar().viewModel.resetSnackBarEvent()to return the state toSnackBarEvent.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,
LaunchedEffectprovides 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 ourDiningViewModelfor us, making it available to any Hilt-enabled Fragment or Activity.@Inject: We use this in theDiningViewModel's constructor to declare its "ingredients"—in this case, aDiningRepoandSharedPreferences. Hilt knows it must provide these dependencies whenever it creates aDiningViewModel.@Module/@Provides: InNetworkModule.kt, we provide "recipes" that teach Hilt how to create complex objects likeRetrofitor ourDiningRepointerface, which it wouldn't know how to build otherwise.The Benefits:
DiningViewModeldoesn't know how to create aDiningRepo; it just knows it needs one. This makes our classes independent and modular.DiningRepowith sample data. This is impossible if the ViewModel creates the repository itself.