The purpose of this repository to share the idea of how to structuring the application in a nice way. In my opinion, flutter_redux and redux_epics is a great combination to setup base for flutter application.
Nihad Delic also wrote a small application to understand how to use flutter_redux
with redux_epics
in the following
URL Redux and epics for better-organized code in Flutter apps.
This structure already listed on Flutter's official State management list page.
By utilizing Flutter Redux as a global application state management tool and Redux Epics for stream handling, you can recycle your Dart code.
When starting a new project, it's common to consider which architecture and structure to follow. With so many state management options available, it’s a bit challenging to pick one and you may be seeking a solution that is not only reusable but can also accommodate future product expansion.
I’m going to explain the structure I have been following and how it helped me to manage the state of the application in a better way.
The directory structure with their responsibilities are mentioned below:
- The API is synonymous with the repository, and we will use the interface to retrieve data from either the network or local storage.
- You have the flexibility to relocate this layer within the package to suit your specific needs.
- The objects of your data type will be included in the repository.
- During the App's
initState(){}
method, inject the completegraph
or modules, and then pass the Store to theStoreProvider
. There is no need for any external injections in this step. - Within the application's
build(BuildContext context){}
method, the modules will be transmitted to all the app's widgets using aStoreProvider()
. - Throughout the application's life cycle, the App module will monitor all the modules.
- This is the primary way to create any type of exception. It functions as a decorator on the
Exception
and will provide an example ofLowPriorityException
for now.
- All the helper
extensions
will be part of this directory. You can enhance as per your requirement, but you can find a number of extensions methods.
- You have the option to define your page's name and then execute the
build runner
command to automatically generate the corresponding route. - Once the route has been generated, it can be utilized for navigation purposes.
Note: I opted for Auto_router because it can tackle complicated navigation scenarios. However, you can substitute it with any other navigation tool of your choice.
-
This is where all the features, pages, screens, and so on, will be located.
-
Every feature will have a corresponding UI and Redux folder that contains its
action
,epics
,reducer
, andState
.- To begin, generate a State class for the corresponding feature. In the current instance,
LaunchesState
will contain two objects: the first will record the status of the launches obtained from the API, while the second will contain the actual list of launches. - Establish an
Action
class in a distinct file and define your actions by expanding from the core Action class ( outlined in Redux). Actions serve as the events that will be triggered, such asFetchLaunchesAction
,FetchLaunchesSucceededAction
,FetchLaunchesFailureAction
. - You can create a new file for the
epics
and read/catch your action/event in it. Access the repository and apply your business logic. Then,emit/yield
the nextaction/event
(either success or failure). Finally, combine your epics in a list and pass them back to the Middleware, which is connected with redux/core.
To clarify, the middleware in this context is essentially a list of all the epics for each feature, which functions as a middleware.
- In a new file, create a
reducer
to update the state of the feature based on the Action/Event. - Great, now you're all set and can display the data in the user interface!
NOTE: In this example, you will see the other classes. Those are decorators to manage the data layer by layer.
- To begin, generate a State class for the corresponding feature. In the current instance,
-
To receive data via
ViewModel
in the UI, you can useStoreConnector
.
- An abstract class named
Action
should be created, which will serve as the base class for all child actions/events. Additionally, anExceptionAction
should be declared as a child of Action, which will serve as the parentexception
action. - Next, create an
AppState
class that includes the initial values. This class should contain a factory method that maintains the initial state for all features. To update the state, include acopyWith()
method. - The app's core must be constructed, which involves creating a
Store
and injecting theinitialState()
of the app. The Store also needs to receive theMiddleware
of the app in the form of epics, as well as a reducer to identify the actions/events.Hint: Here, you can establish your logout process without any boilerplate code.
- Create a
reducer
to manage the actions that are related to the system's core. - Define a
DataState
class which can indicate the status of an object, such asloaded
,loading
,failure
, etc. This DataState will be utilized in the application to track the state of the corresponding object.
- Create app life cycle actions and their corresponding
observers
, along with business logic to handle object changes during the life cycle. - The module
initializer
needs to be set up in this architecture. It involves a list of modules that includes the initialization ofDio
,Repository
,Environment
,Local Storage
, and so on. - To build the values for the list of modules, create a
Graph
of the Module objects, which can be accomplished using the built_value package. Additionally, in this example, an abstract class has been included with adispose()
method that can be used to close all connections of the initialized objects. - In a separate initializer file, create a method named
initialize()
to set up thegraph
by providing the necessary values. - You can use the
FutureBuilder()
in the main file toasynchronously
receive the initialized graph. This allows you to display a splash page while the app/graph is being prepared. - To make high level values accessible throughout the app, use
InheritedWidget
to create anInjectorWidget
fordependency injection
via the context.
- This directory will contain utility functions and classes that can be useful in organizing the app.
- You can put your commonly used widgets in this directory, which can be accessed and utilized throughout the app. If needed, you can also move them to a separate layer or package for more organization.
- Set up your
router
's initialization here. - Build a
graph
while initializing the modules list. - During initializing, make sure to show your splash page.
- At this point, you don’t have the
MaterialApp()
access, and you can useDirectionality
widget to present the SplashScreen - Once
Graph
is ready, switch the UI with yourFutureBuilder()
's builder method.
- You may find this structure easy to test
- Test your API/Repositories in a separate layer
- Your Redux layer is testable in a separate directory and easy to test
- UI layer behaves as independently and easy to test
- Business logic
epic/reducer
will be managed separately to test Module
by module testing can make sure the app is bug free- In future if you need to change anything, you will be sure where to make changes and scalable
- You can add more feature without touching its architecture layer
- Easy to maintain the code and architecture
- Easy for learning for new people from web development (who have experience in Redux)
- Unit tests covers
- API
- redux
- system
- Common widgets
Note: Tests(Unit, Widget, and e2e_integration) are under development, and will try my best to cover 99%.