Two Years as a Flutter Developer: Insights and Experiences (Part 1)
Today, I've completed two years as a full-time Flutter developer. During this time, I've learned a lot, faced some challenges, and gained valuable experience. I want to share some of the key things I've learned, hoping they might help other developers (or at least someone like me!).
Handling State Management Effectively
Using State Management is essential for keeping our app's data in sync with the UI. As your app grows, managing the state directly with widgets like setState can get messy and lead to bugs or performance issues.
Using state management makes your code more organized and easier to debug.
Is There a Best State Management Library?
No, there isn't a single best state management library. It all depends on your app's needs. There are many state management solutions like Bloc/Cubit, Riverpod, Provider, and more. The key is to choose the one that best fits your app's use case. Always go through the documentation of the libraries first to decide which library suits your app.
What Do We Use in Our App?
We use Jetpack, an open-source library developed by one of our team members.
Jetpack includes tools like ViewModel and EventQueue, which help us manage state effectively.
Using an App Event Bus
We can create an App Event Bus using flutter streams.
What is a Stream?
A stream in Flutter is a sequence of asynchronous data that you can listen to, like a flow of events. It delivers data over time, which means you can react to updates as they happen.
Importance of Streams
Streams are essential for handling real-time data in Flutter apps. They allow your app to react instantly to changes, making the user experience smoother and more interactive. They're great for tasks like updating UI, handling user input, or managing data from APIs.
Why do we need app event bus?
The app event bus allows us to listen for changes from other screens in our app easily.
For example, consider a scenario where we show a user's feed based on their selected interests. If the user changes their interests on a different screen, we can fire a FeedInvalidated event.
On the MyFeed page, we would be listening to the app event stream. When the FeedInvalidated event is triggered, we simply refresh the feed.
Note: Avoid using the app event bus unnecessarily. It should be used primarily for communicating between different screens when needed.
Here is basic example of how to create AppEventBus.
@singleton
class AppEventBus {
// Create a StreamController that allows broadcasting events to multiple listeners
final StreamController<AppEvent> _appEventsStreamController =
StreamController.broadcast();
// Expose the stream for external listeners
Stream<AppEvent> get stream => _appEventsStreamController.stream;
// Method to fire an event
void fire(AppEvent event) {
_appEventsStreamController.add(event);
}
}
// Base class for app events
sealed class AppEvent {}
// Example event for feed invalidation
class FeedInvalidatedEvent extends AppEvent {}
The singleton pattern ensures that a class has only one instance throughout the application and provides a global point of access to it.
Stateless vs Stateful Widgets
Stateless Widgets
Stateless widgets are immutable and do not maintain any internal state. Once they are built, their properties cannot change, and they depend solely on the data passed to them.
Stateful Widgets
Stateful widgets are mutable and can change their internal state. They maintain a separate state object that holds data that can change during the widget's lifecycle.
Which to Use?
Make sure you need to use stateless widgets wherever possible with the help of state management. UI widgets should be dumb, they should simply re-render when the state they are listening to changes. This approach keeps your code cleaner, more organized, and easier to understand. Stateless widgets also help improve your app's performance since they are lightweight and don't need to manage state changes. If you find that stateless widgets won't fit your use case, then go ahead with stateful widgets. Use them sparingly to avoid complexity and potential performance issues.
Problems with setState
Using setState in Flutter is straightforward for managing state in simple cases, but as your app grows, it can lead to a few issues:
- Messy Code: When your widget tree becomes more complex, relying heavily on
setStatecan make your code harder to read and maintain. It can spread state logic all over your widget, making the code less organized. - Performance Issues:
setStatetriggers a full rebuild of the widget and its descendants. This can be inefficient, especially if only a small part of the UI needs to be updated, leading to unnecessary rendering. - Mount Check Issue: If a user interacts with the screen and performs an action that uses
setState, and then navigates back before thatsetStateaction completes, it can cause a mount check error. This happens becausesetStatetries to update a widget that no longer exists in the widget tree, leading to an error since the screen is not rendered anymore. - Lack of Scalability:
setStateis not ideal for managing state in large-scale apps. As your app grows, handling state withsetStatealone becomes unmanageable and prone to bugs.
My suggestion is to avoid using setState as much as possible and instead use state management solutions. Even if you're only updating a single variable on the screen, it's better to use state management. The problem with using setState is that once you start with it, you'll be tempted to use it again for another variable, which leads to messy and complicated code in the long run.
If you do choose to use setState, make sure to always check if the widget is still mounted before making any updates to prevent mount check errors.
Getting Familiar with Native Code
It's a good idea to get familiar with native code when working with Flutter. Even though Flutter lets you create apps with one codebase, sometimes you'll need to connect directly with the platform-specific features of iOS and Android. For example, you might have to write your own method channels to talk to the native code on both platforms. This helps you use device-specific features that Flutter might not handle on its own. Knowing some native code can make your app more powerful by letting you use features from iOS and Android that aren't always available in Flutter.
Maintaining a High Crash-Free Rate (99.1%+) and Crash-Free Sessions (99.7%+)
- User: A user is an individual installation of your app on a device. If a person has your app on multiple devices, each installation counts as a unique user.
- Session: A session is a continuous period when a user interacts with your app. A new session starts when the app is cold-started or resumed after being in the background for at least 30 minutes.
- Crash-Free Users: The crash-free users metric indicates the percentage of users who engaged with your app during a selected time period without encountering any crashes. It reflects the overall user experience.
- Crash-Free Sessions: The crash-free sessions metric measures the percentage of sessions that occurred without crashes during a specific time frame. It indicates the reliability of your app.
To make sure our app runs smoothly, we need to aim for a crash-free rate of 99.1% or higher and crash-free sessions of 99.7% or more.
What Do We Do to Improve Crash-Free Rate?
We use Firebase for Crashlytics, which helps us monitor app stability effectively. After every release, we check Crashlytics daily. If we find any fatal errors, we prioritize fixing them and quickly release a patch version. If the issues are minor—meaning they affect only a few users or result in just a few crashes—we'll address those in the next planned release. By consistently following this process, we gradually improve our app's crash-free rate, ensuring a better experience for our users.
Importance of Flutter Documentation
It's really important to go through the Flutter documentation thoroughly when you're building apps. The documentation is straightforward and clearly explains every property and feature you might need. I recommend that when you run into issues or want to check if something exists in a widget, take the time to read the Flutter documentation first. Before jumping onto Google or asking ChatGPT, try the official docs. This way, you'll not only find the answers you're looking for but also learn about all the properties of the widget. Understanding the documentation helps you become more familiar with Flutter and improves your coding skills over time. There are many widgets that you might not even know exist, and discovering them in the documentation can make your work much easier. Knowing the ins and outs of the widgets will give you more tools for future projects and make your development process smoother. So, always make the Flutter documentation your first stop!
Why Creating Our Own Library Can Be Beneficial
Dependency Issues
Relying on third-party libraries can create significant problems when upgrading your Flutter and Dart versions. If the library's developers haven't updated their code to be compatible with the latest versions, it can lead to errors and conflicts in your project. This can slow down your development process and prevent you from using the latest Flutter features by not being able to upgrade Flutter.
Control
When a library is crucial for your project, it's worth considering creating your own version tailored to your specific needs. By developing your own library, you have complete control over its features and updates. This way, you won't be reliant on other developers to make changes or keep it up to date, ensuring that it always meets your project's requirements.
Use Well-Maintained Libraries
It's perfectly fine to use third-party libraries, but make sure they are actively maintained. Libraries that receive regular updates are more likely to be compatible with the latest versions of Flutter and Dart. Avoid using libraries that are no longer maintained, as they can introduce risks to your project, leading to bugs and performance issues.
Simpler Alternatives
If your use case can be effectively addressed using Flutter's built-in features, it's often better to add the necessary code directly to your project instead of relying on external packages. Many third-party packages consist solely of Dart code, which means you can easily replicate their functionality within your project. This approach can simplify your project and reduce dependencies, making it easier to maintain in the long run.
The Importance of Staying Updated with Flutter
In one of the Flutter versions, I faced a frustrating issue with the Impeller rendering. The shader mask for text worked perfectly on Android devices but not on iOS, which affected my app's appearance. Thankfully, I learned that this issue was resolved in an upcoming Flutter version. By keeping my Flutter SDK up to date, I was able to benefit from this fix, ensuring a smoother experience for both myself and my users.
Here are some steps that we need to follow to stay updated with Flutter:
- Follow the Official Flutter Blog: The Flutter team frequently posts about updates, new features, and best practices on their official blog. Subscribing helps us stay informed about important changes.
- Join the Flutter Community: Engaging with the Flutter community through forums and social media allows us to gain insights, tips, and the latest news from other developers.
- Regularly Check Flutter Documentation: The official documentation is always updated, so it's a good habit to review it often. This helps us understand new features and best practices.
Staying updated has not only helped me overcome issues like the Impeller problem but has also improved my overall development skills and project quality. Flutter is gradually introducing many changes and adding new widgets and features that enhance the overall quality of our apps. By staying updated with Flutter, we'll be aware of these improvements and can use them in our projects.
Conclusion
That's enough for now, this has already become a long article! I'll come back with Part 2 soon, sharing more insights and tips based on my journey as a Flutter developer.