Figma design of flow chart - https://www.figma.com/board/oyPZGn7pFEZciS5zQR3bzv/Untitled?node-id=0-1&t=hCvKgekUWtBBRIiK-1
-
UI Layer Initiates a Request:
- The user interacts with the app, triggering an action that requires data from the network (e.g., fetching weather information for a city).
- This action is captured in the Presentation Layer, specifically in the UI widgets or screens.
-
State Notifier Handles the Request:
- The UI calls a method in the Notifier (e.g.,
getWeather
inWeatherNotifier
). - The notifier is part of the Application Layer and is responsible for managing state and business logic.
- It updates the state to a loading state to reflect that a network request is in progress.
- The UI calls a method in the Notifier (e.g.,
-
Notifier Interacts with the Repository:
- The notifier calls the appropriate method in the Repository (e.g.,
getWeather
inWeatherRepositoryImpl
). - This call goes through the Domain Layer, which defines the abstract
WeatherRepository
interface. The notifier depends on this abstraction, not on the concrete implementation.
- The notifier calls the appropriate method in the Repository (e.g.,
-
Repository Makes Network Call via NetworkApiService:
- The repository uses the NetworkApiService (Retrofit interface) to make the actual network call.
- It constructs the request with necessary parameters (city name, API key, units).
-
Dio and Interceptors Process the Request:
- The Dio HTTP client sends the request.
- The ApiInterceptor intercepts the request to:
- Log request details (URL, headers, method).
- Add or modify headers if needed (e.g., authentication tokens).
- When the response comes back, the interceptor:
- Logs response details.
- Checks for HTTP error codes (e.g., 4xx or 5xx) and converts them into exceptions using
handler.reject
.
-
Handling Responses and Errors in Repository:
- The repository receives the response or catches an exception.
- In the
try
block:- If the response is successful, it logs the response and returns a
Right(response)
wrapped in anEither
type, indicating success.
- If the response is successful, it logs the response and returns a
- In the
catch
block:- If an exception occurs, it uses
FailureMapper.getFailures(e)
to convert the exception into a well-definedAppFailure
. - Returns a
Left(failure)
indicating an error.
- If an exception occurs, it uses
-
Notifier Updates State Based on Repository Result:
- The notifier receives the
Either<AppFailure, WeatherDTO>
result from the repository. - It uses pattern matching (
fold
) to handle both cases:- On Success (
Right
):- Converts the
WeatherDTO
into a domain entity (WeatherFullEntity
) using a factory constructor or mapper. - Updates the state to
ApiRequestState.data(data: weatherEntity)
.
- Converts the
- On Failure (
Left
):- Updates the state to
ApiRequestState.failed(reason: failure)
. - Logs the failure for debugging purposes.
- Updates the state to
- On Success (
- The notifier receives the
-
UI Reacts to State Changes:
- The UI listens to changes in the notifier's state using Riverpod's
ref.watch
. - Depending on the state:
- Loading: Shows a loading indicator.
- Data: Displays the weather information to the user.
- Failed: Shows an error message, possibly with options to retry.
- The UI listens to changes in the notifier's state using Riverpod's
-
Error Handling and User Feedback:
- When a failure occurs, the UI can use the
FailureHandler
to display error dialogs or messages. - The error messages are user-friendly and can be specific based on the type of failure (network error, server error, etc.).
- When a failure occurs, the UI can use the
-
Presentation Layer:
- Handles UI components and user interactions.
- Should be free of business logic and data-fetching code.
- Reacts to state changes provided by the notifier.
-
Application Layer (Notifiers and Providers):
- Manages state and contains business logic related to state changes.
- Interacts with the domain layer to perform actions like fetching data.
- Updates the state based on the result of these actions.
-
Domain Layer (Repositories and Entities):
- Defines the core business logic and models (entities).
- Contains abstract repositories that define what operations are available.
- Entities are pure Dart objects representing the data in your app.
-
Infrastructure Layer (Data Access and DTOs):
- Implements the repositories defined in the domain layer.
- Handles data fetching from external sources (APIs, databases).
- Converts data transfer objects (DTOs) into domain entities.
-
Core Layer:
- Contains shared utilities, such as error handling, logging, and networking setup.
- Provides consistent mechanisms for dealing with failures and logging throughout the app.
-
Single Responsibility Principle (SRP):
- Each class and module has one responsibility.
- For example,
WeatherRepositoryImpl
is only responsible for fetching weather data.
-
Open-Closed Principle (OCP):
- Classes are open for extension but closed for modification.
- New features can be added without changing existing code, reducing the risk of introducing bugs.
-
Liskov Substitution Principle (LSP):
- Subclasses or implementations should be substitutable for their base types.
WeatherRepositoryImpl
can be used anywhereWeatherRepository
is expected.
-
Interface Segregation Principle (ISP):
- Clients should not be forced to depend on interfaces they do not use.
- By defining specific interfaces for repositories, notifiers depend only on what they need.
-
Dependency Inversion Principle (DIP):
- High-level modules should not depend on low-level modules; both should depend on abstractions.
- The notifier depends on the abstract
WeatherRepository
interface, not the concrete implementation.
-
Maintainability:
- Clear separation of concerns makes the code easier to understand and maintain.
- Changes in one layer have minimal impact on others.
-
Testability:
- Each component can be tested in isolation.
- Mock implementations can be provided for repositories during testing.
-
Scalability:
- The app can grow in features without becoming unmanageable.
- New features can follow the same structural patterns.
-
Reusability:
- Common utilities and patterns in the core layer can be reused across different features.
Let's walk through a real-life example using your app:
-
User Action:
- The user enters a city name and taps a button to fetch the weather.
-
UI Calls Notifier:
- The UI calls
getWeather(cityName)
on theWeatherNotifier
.
- The UI calls
-
Notifier Updates State to Loading:
- The
WeatherNotifier
sets the state toApiRequestState.loading()
to show a loading indicator.
- The
-
Notifier Requests Data from Repository:
- The notifier calls
getWeather(cityName)
on theWeatherRepositoryImpl
.
- The notifier calls
-
Repository Fetches Data from API:
- The repository uses
NetworkApiService
to make an API call via Retrofit and Dio.
- The repository uses
-
ApiInterceptor Processes Request:
- The
ApiInterceptor
logs the request and adds any necessary headers.
- The
-
Response Handling:
- On receiving the response:
- If successful, the data is returned to the repository.
- If there's an error (e.g., 404 Not Found), the interceptor rejects the request, and an exception is thrown.
- On receiving the response:
-
Repository Handles Exceptions:
- The repository catches exceptions and uses
FailureMapper
to convert them intoAppFailure
instances.
- The repository catches exceptions and uses
-
Notifier Updates State Based on Result:
- If data is received, the notifier updates the state to
ApiRequestState.data()
with the weather entity. - If an error occurs, the notifier updates the state to
ApiRequestState.failed()
with the failure reason.
- If data is received, the notifier updates the state to
-
UI Reacts to State Change:
- The UI observes the notifier's state and updates accordingly:
- Displays weather information on success.
- Shows an error message on failure.
- The UI observes the notifier's state and updates accordingly:
Your app's architecture effectively separates concerns, handles errors gracefully, and ensures that network data flows smoothly from the API to the UI. By adhering to clean architecture principles and SOLID design patterns, you've created a scalable and maintainable app structure that can be easily understood and extended by developers at any level.
Next Steps or Further Assistance
- Documentation: Consider adding comments and documentation throughout your code to help new developers understand each component's role.
- Testing: Implement unit tests for your notifiers, repositories, and other critical components to ensure reliability.
- Error Messages: Enhance user-facing error messages to be more descriptive and helpful.
If you have any specific questions or need clarification on any part of this flow or architecture, feel free to ask!
Images |
---|
Figure 1: Architecture Overview |
Figure 2: Router |
Figure 3: single feature structure |
Figure 4: Core structure |
Figure 5: Full app feature folder structure |
Landing Page | Landing Page Tap to see more | Search by city |
Search Result 1 | Search Result 2 | |
Search with wrong cityname | Error Search Result | |
Soln: git add . , then check the path of the tracked file you don't want to push. copy paste to .gitignore
Soln: Restart the device
Solved: Use Different function that returs different state. Riverpod Video Tutorial
- AsyncValue with FutureProvider
- https://stackoverflow.com/questions/67582335/how-to-get-the-old-state-before-updating-the-state-in-state-notifier-riverpod
- my git queries - rrousselGit/riverpod#212
- await on asyncValue - https://stackoverflow.com/questions/66411312/riverpod-how-to-await-using-futureprovider-on-asyncvalue-not-in-widget/66955043#66955043
- cached refresh indicator rrousselGit/riverpod#461
- Unhandled Exception: setState() or markNeedsBuild() called during build
rrousselGit/riverpod#177
https://codewithandrea.com/videos/flutter-state-management-riverpod/