If you're developing a Flutter application and you're striving for robustness, scalability, and maintainability, you're in the right place.
The 12-Factor App methodology, though initially designed for web apps or SaaS applications, offers a set of best practices that many developers find invaluable for mobile app development as well.
Each of the twelve factors can serve as a guideline to help you avoid common pitfalls and build a better app.
Let's explore each of them and see how they can apply to a Flutter app.
Factor 1: Codebase
Your application should have one codebase tracked in revision control, with many deploys. In the context of Flutter development, this means:
Use a version control system like Git. This helps keep track of changes, facilitates collaboration, and makes it easier to revert changes if needed.
Each separate service should have its own repository. This separation allows for independent scaling and development. Different environments, such as production, staging, and development, should be managed through configurations. This ensures consistent behaviour across all environments.
Aim for codebase consistency across your team. Having a shared understanding of the codebase structure and coding conventions can significantly improve team productivity.
Factor 2: Dependencies
Your Flutter application should explicitly declare and isolate dependencies. This means:
Do not rely on the implicit existence of system-wide packages. Instead, specify all dependencies, like Flutter libraries, in a pubspec.yaml
file. This makes your app environment-agnostic and reduces the risk of discrepancies between different environments.
Ensure isolation of dependencies to prevent version conflicts. This can help you avoid issues arising from different services depending on different versions of the same package.
Factor 3: Config
Store your app's config in the environment, not in the code. This means:
Any aspect of your application that varies between deployments should be exposed as a configuration that can be read from the environment. This could include API endpoints, secret keys, etc.
Use Dart's dotenv
package to manage these configurations.
Keep your configs separate for each environment to avoid accidentally using the wrong config.
Factor 4: Backing Services
Your Flutter app should treat backing services as attached resources. This means:
Your app should make no distinction between local and third-party services. For example, if you're using a database, it should be able to switch between a local SQLite database and a remote Postgres database with just a change in configuration.
All services are resources that can be attached or detached at any time. This makes your app more flexible and allows for easy scaling and replacement of services.
Factor 5: Build, Release, Run
Strictly separate your build and run stages. This means:
Your build script should transform your codebase into an executable bundle.
A release should take the build and combine it with the current config. The run stage (in this case, the Flutter runtime) runs the app in the execution environment.
Each stage should be able to operate without dependence on the previous stages. This makes your deployment process more robust and makes it easier to identify and fix issues.
Factor 6: Processes
Execute your app as one or more stateless processes. For a Flutter app, this might mean:
Your app should not rely on saving state between invocations. Instead, save necessary state to a persistent data store.
Consider using Flutter's state management solutions like Provider or Riverpod to manage the state of your app effectively.
Stateless apps are easier to reason about and manage since they don't need to maintain a connection or remember previous interactions.
Factor 7: Port Binding
While this factor is more relevant for server-side services, for a Flutter app, it's important to think about how your app communicates with the network:
Having a specific port for network communication can help with testing and debugging.
Consider using a mock server for testing network interactions. This can help you test how your app behaves under different network conditions.
Factor 8: Concurrency
Your app should scale out via the process model, i.e., it should handle multiple users or sessions concurrently. For a Flutter app, this means:
Use Flutter's asynchronous capabilities effectively. This can help you handle multiple user inputs or network requests at the same time.
Keep your app responsive, no matter how many users you have. This includes handling long-running tasks without blocking the user interface.
Use Futures and Streams in Dart to handle asynchronous operations. This can help you manage complex asynchronous workflows in a more readable and maintainable way.
Factor 9: Disposability
Your app should be disposable, i.e., it should start quickly and shut down gracefully. This means:
Your app should be able to handle being shut down at any moment, without losing data. This is particularly relevant for mobile apps, as users may close your app at any time.
Fast startup and graceful shutdown are key. This makes your app more robust and improves the user experience.
Consider using persistent local storage like Hive or Shared Preferences for saving state. This allows your app to restore its state when it's started again.
Factor 10: Dev/prod Parity
Keep your development, staging, and production environments as similar as possible. This means:
The backing services you use in development should be as close as possible to the ones you use in production. This reduces the risk of bugs appearing in production that weren't present in development.
Use feature flags to test new features in production without impacting all users. This can help you get feedback on new features quickly and safely.
Factor 11: Logs
Treat logs as event streams. For a Flutter app, this means:
Each process writes its event stream, unbuffered, to stdout. This makes it easy to observe the behavior of your app in real-time.
Use logging packages like logger
in Dart to manage logs in a structured way. This can make it easier to search and analyze your logs.
Factor 12: Admin Processes
Run administrative tasks as one-off processes. This means:
Any one-off admin tasks, like database migrations or data analysis scripts, should be run in an environment as close to the production environment as possible.
Automate these tasks to avoid human error and to make them reproducible.
Remember, these twelve factors are guidelines, not strict rules. They're designed to help you build better, more maintainable apps. How you apply them will depend on the specifics of your app and your development and deployment environments. Use them as a guide, and watch your Flutter app flourish!
Before we go...
Thanks for reading!
If you loved this, consider following me :)
Got any doubt or wanna chat? React out to me on twitter or linkedin.