A Technical Deep Dive into Flutter: Building a Breathing Meditation App from Scratch

“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 and TickerProviderStateMixin: Since an animation’s state changes over time, we built the screen as a StatefulWidget. The TickerProviderStateMixin was crucial as it provides the “ticks” for every frame, allowing the AnimationController 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 in initState and, importantly, disposed of in dispose to prevent memory leaks.
  • ScaleTransition and Tween: Instead of repeatedly calling setState to rebuild the widget, which can cause performance issues, we used ScaleTransition. This transition widget efficiently rebuilds only the scale property of our widget in response to value changes from the AnimationController, ensuring high performance. A Tween<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 like enum BreathingState { inhale, hold, exhale }. This replaced complex if-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 an async 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 used await 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 and MaterialPageRoute: We separated the UI into a MenuScreen and a BreathingScreen. We used Navigator.push to handle screen transitions. Data, such as the user’s selected breathing technique, was passed between screens via constructor arguments in the MaterialPageRoute‘s builder.
  • Data Modeling: We defined model classes like BreathingSession and Category. 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 and TextFormField: For the “Custom Mode” feature, we built a dynamic user interface using AlertDialog with TextFormFields and TextEditingControllers 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 like AnimationController and AudioPlayer in initState and cleaning them up in dispose. This is a fundamental principle for preventing memory leaks and unexpected errors.
  • while Loop and the mounted Property: The session repetition feature was implemented using a while (_isLooping) loop. The loop was safely terminated by setting the _isLooping flag to false in the disposemethod. Crucially, after every await call inside the loop, we checked the widget’s mounted property with if (!mounted) break; to prevent the setState() called after dispose() error.
  • Plugin Integration: We added the audioplayers package to our pubspec.yaml, ran flutter pub get, and registered our audio files in an assets 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.

  1. PlatformException and Race Conditions: We encountered an error when trying to stop() an AudioPlayerinstance before it was fully initialized on the native side. This is a classic race condition issue in asynchronous programming.
  2. Dependency Version Mismatch: More critically, we discovered the version of the audioplayers package we were using had a much older API. The AudioCache class lacked a play method entirely. To solve this, we had to adapt to the “by-the-book” pattern for that specific version: using AudioCache only to get a local file path via loadPath(), and then using a separate AudioPlayer instance to play that path via play(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-필수-옵션-가이드

댓글 남기기