“What app should I build next?” This is a familiar and exciting question for every developer. It’s from this simple starting point that our project began. From a sea of ideas, we chose the concept of a “situational breathing coach app” and set out to bring it to life with Flutter.
This article is a practical development log that chronicles the entire journey of turning an idea into a finished application. It covers the core technologies applied, from UI animations with AnimationController
and asynchronous logic control with async/await
to plugin integration and real-world debugging.
1. UI & Animation: AnimationController
and Transition Widgets
The first technical challenge was to implement a smooth animation to visually guide the user’s breathing. For this, we utilized Flutter’s core animation APIs.
StatefulWidget
andTickerProviderStateMixin
: Since an animation’s state changes over time, we built the screen as aStatefulWidget
. TheTickerProviderStateMixin
was crucial as it provides the “ticks” for every frame, allowing theAnimationController
to run smoothly.AnimationController
: This was the core controller for managing the animation’s lifecycle, including its duration, playback, reversal, and repetition. It was initialized ininitState
and, importantly, disposed of indispose
to prevent memory leaks.ScaleTransition
andTween
: Instead of repeatedly callingsetState
to rebuild the widget, which can cause performance issues, we usedScaleTransition
. This transition widget efficiently rebuilds only thescale
property of our widget in response to value changes from theAnimationController
, ensuring high performance. ATween<double>(begin: 0.5, end: 1.0)
was used to define the animation’s value range.
2. Asynchronous State Logic with async/await
and Future.delayed
To move beyond a simple animation and implement structured breathing patterns like “4-7-8 breathing,” we heavily utilized asynchronous programming.
enum
for State Management: We defined clear application states likeenum BreathingState { inhale, hold, exhale }
. This replaced complexif-else
chains and significantly improved the code’s readability and maintainability, effectively creating a simple state machine.async/await
: The entire breathing cycle was defined within anasync
function (_startBreathingCycle
). This allowed us to write sequential logic that waited for certain tasks to complete, making complex asynchronous code read like synchronous code.Future.delayed
: For phases like “hold your breath,” where we needed to pause execution without any animation, we usedawait Future.delayed(Duration(seconds: 7))
. This is an effective way to asynchronously delay execution without blocking the UI thread.
3. App Architecture: Navigation, State Passing, and Data Modeling
As the app’s complexity grew, we moved from a single-screen structure to a more systematic architecture.
Navigator
andMaterialPageRoute
: We separated the UI into aMenuScreen
and aBreathingScreen
. We usedNavigator.push
to handle screen transitions. Data, such as the user’s selected breathing technique, was passed between screens via constructor arguments in theMaterialPageRoute
‘sbuilder
.- Data Modeling: We defined model classes like
BreathingSession
andCategory
. This was a crucial refactoring step that decoupled the data logic from the UI code. Through modeling, the data structure became clear, and the UI’s responsibility was narrowed to only representing the data, leading to a more scalable and modular architecture. AlertDialog
andTextFormField
: For the “Custom Mode” feature, we built a dynamic user interface usingAlertDialog
withTextFormField
s andTextEditingController
s to receive user input.
4. Lifecycle Management and Plugin Integration with audioplayers
To enhance the app’s polish, we added sound and managed the widget lifecycle to ensure session loops behaved correctly.
initState
&dispose
: We strictly followed the pattern of creating resource-heavy objects likeAnimationController
andAudioPlayer
ininitState
and cleaning them up indispose
. This is a fundamental principle for preventing memory leaks and unexpected errors.while
Loop and themounted
Property: The session repetition feature was implemented using awhile (_isLooping)
loop. The loop was safely terminated by setting the_isLooping
flag tofalse
in thedispose
method. Crucially, after everyawait
call inside the loop, we checked the widget’smounted
property withif (!mounted) break;
to prevent thesetState() called after dispose()
error.- Plugin Integration: We added the
audioplayers
package to ourpubspec.yaml
, ranflutter pub get
, and registered our audio files in anassets
folder to include them in the app’s resources.
5. Real-World Debugging: Asynchronous Issues and Dependency Management
Towards the end of the project, we encountered two significant technical hurdles, and solving them was a major growth experience.
PlatformException
and Race Conditions: We encountered an error when trying tostop()
anAudioPlayer
instance before it was fully initialized on the native side. This is a classic race condition issue in asynchronous programming.- Dependency Version Mismatch: More critically, we discovered the version of the
audioplayers
package we were using had a much older API. TheAudioCache
class lacked aplay
method entirely. To solve this, we had to adapt to the “by-the-book” pattern for that specific version: usingAudioCache
only to get a local file path vialoadPath()
, and then using a separateAudioPlayer
instance to play that path viaplay(DeviceFileSource(path))
. This experience highlighted the critical importance of carefully checking library documentation and writing code that is compatible with the specific dependency version.
Technical Retrospective
Through this project, we gained deep, practical experience with Flutter’s declarative UI, state management, asynchronous programming, architectural design, and real-world debugging. The process of tracking down and resolving the library versioning issue, in particular, provided a more valuable lesson than any technical document could offer.
Reference
https://heavenly.tistory.com/entry/Flutter-CLI-flutter-create-필수-옵션-가이드