Skip to content

feat(voice): add Flutter UI and platform channel for voice profile se…#772

Open
Arunodoy18 wants to merge 1 commit intoAOSSIE-Org:masterfrom
Arunodoy18:feat/684-voice-profile-ui
Open

feat(voice): add Flutter UI and platform channel for voice profile se…#772
Arunodoy18 wants to merge 1 commit intoAOSSIE-Org:masterfrom
Arunodoy18:feat/684-voice-profile-ui

Conversation

@Arunodoy18
Copy link

@Arunodoy18 Arunodoy18 commented Feb 22, 2026

Implements Issue #684.

  • Adds Flutter UI component for selecting predefined voice profiles
  • Adds preview mode toggle
  • Introduces MethodChannel for passing control signals to native layer
  • Keeps Flutter strictly as a control layer (no audio processing)

Closes #684

Summary by CodeRabbit

New Features

  • Introduced voice profile selection feature with multiple voice options available (Default, Deep, Soft, Energetic, Calm)
  • Added voice preview mode toggle to test selected voice profiles before applying

@Arunodoy18 Arunodoy18 requested a review from M4dhav as a code owner February 22, 2026 20:09
@github-actions
Copy link
Contributor

🎉 Welcome @Arunodoy18!
Thank you for your pull request! Our team will review it soon. 🔍

  • Please ensure your PR follows the contribution guidelines. ✅
  • All automated tests should pass before merging. 🔄
  • If this PR fixes an issue, link it in the description. 🔗

We appreciate your contribution! 🚀

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

Introduces a cross-platform voice control feature with Flutter UI components and platform channels. Adds Android and iOS MethodChannel handlers for voice profile and preview state management, paired with Dart controller, service, and screen implementations for voice profile selection.

Changes

Cohort / File(s) Summary
Native Platform Channels
android/app/src/main/kotlin/com/resonate/resonate/MainActivity.kt, ios/Runner/AppDelegate.swift
Implements MethodChannel "voice_control_channel" on both platforms with handlers for setVoiceProfile and setPreviewEnabled methods. Android overrides configureFlutterEngine to attach the channel; iOS creates channel in existing application method. Both include private helper methods (applyVoiceProfile, setVoicePreviewEnabled) with placeholder logging for native voice processing.
Dart Service & Controller
lib/services/voice_control_service.dart, lib/controllers/voice_profile_controller.dart
Introduces VoiceControlService as a static bridge to native MethodChannel and VoiceProfileController (GetxController) managing observable voice profile list, selected voice, and preview state. Controller dispatches user selections to service on init and during profile/preview changes.
UI & Routing
lib/views/screens/voice_profile_screen.dart, lib/routes/app_pages.dart, lib/routes/app_routes.dart
Adds VoiceProfileScreen with dropdown for voice selection and toggle for preview mode, both reactive via Obx bindings. Introduces new route constant and GetPage entry to make screen navigable.

Sequence Diagram

sequenceDiagram
    participant User as User (UI)
    participant Screen as VoiceProfileScreen
    participant Controller as VoiceProfileController
    participant Service as VoiceControlService
    participant Channel as MethodChannel
    participant Native as Native Platform<br/>(Android/iOS)

    User->>Screen: Select voice profile<br/>or toggle preview
    Screen->>Controller: onVoiceProfileChanged(voice)<br/>or onPreviewToggled(value)
    Controller->>Controller: Update observable<br/>(selectedVoice/<br/>isPreviewEnabled)
    
    Controller->>Service: setVoiceProfile(voice)<br/>or setPreviewEnabled(enabled)
    Service->>Channel: invokeMethod('setVoiceProfile')<br/>or ('setPreviewEnabled')
    Channel->>Native: Route to platform handler<br/>(MainActivity/AppDelegate)
    Native->>Native: applyVoiceProfile(profile)<br/>or setVoicePreviewEnabled(enabled)
    Native-->>Channel: Return success
    Channel-->>Service: Complete Future
    
    Note over Controller,Native: Initial state sent<br/>on controller onInit()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

app-update

Poem

A rabbit hops through Flutter's green,
With voices tuned to every scene,
Platforms channeled, left and right,
Preview toggles shine so bright,
Voice profiles blooming—what delight! 🎤🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly relates to the main changeset: adding Flutter UI and platform channels for voice profile selection, which is the primary focus of all changes.
Linked Issues check ✅ Passed All code changes meet issue #684 requirements: Flutter UI for voice profile selection [screens], preview toggle, platform channels for control signals [services, native code], and strict separation ensuring Flutter sends only control signals without audio processing [services/controller logic].
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #684: UI components, controller logic, routing, platform channel implementation, and native handlers—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
lib/routes/app_pages.dart (1)

153-156: Missing binding: for the voice profile route — pairs with the Get.put lifecycle issue in VoiceProfileScreen.

Without a Binding, GetX does not automatically create or dispose VoiceProfileController as part of route navigation. Add a VoiceProfileBinding (see the diff proposed in voice_profile_screen.dart) to properly wire controller lifecycle to route entry/exit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/routes/app_pages.dart` around lines 153 - 156, The GetPage for
AppRoutes.voiceProfileScreen is missing a binding so the VoiceProfileController
isn't created/disposed with navigation; update the GetPage declaration for
AppRoutes.voiceProfileScreen to include binding: VoiceProfileBinding (so GetX
will instantiate VoiceProfileController per route) and ensure
VoiceProfileBinding registers the controller (e.g., via Get.put/Get.lazyPut
inside VoiceProfileBinding), matching the controller lifecycle expected by
VoiceProfileScreen.
ios/Runner/AppDelegate.swift (1)

27-67: Store voiceChannel as a property to make the lifecycle explicit.

voiceChannel is a local let that goes out of scope after didFinishLaunchingWithOptions returns. While Flutter's binary messenger internally retains the handler block, this is an implementation detail. The conventional pattern for iOS platform channels stores the channel as a class-level property (e.g., private var voiceChannel: FlutterMethodChannel?) so it can be explicitly torn down and so the pattern is unambiguously safe when the channel later needs to invoke methods from native→Flutter.

♻️ Suggested refactor
 `@objc` class AppDelegate: FlutterAppDelegate {

   private let voiceControlChannelName = "voice_control_channel"
+  private var voiceChannel: FlutterMethodChannel?

   // ...inside didFinishLaunchingWithOptions:
-    let voiceChannel = FlutterMethodChannel(
+    voiceChannel = FlutterMethodChannel(
       name: voiceControlChannelName,
       binaryMessenger: controller.binaryMessenger
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Runner/AppDelegate.swift` around lines 27 - 67, Make voiceChannel a
class-level property (e.g., private var voiceChannel: FlutterMethodChannel?)
instead of a local let in didFinishLaunchingWithOptions so its lifecycle is
explicit; initialize it there using voiceControlChannelName and
controller.binaryMessenger, assign the handler that calls applyVoiceProfile and
setVoicePreviewEnabled as shown, and ensure you clear/tear down voiceChannel
(set to nil) when appropriate (e.g., on deinit or when the Flutter engine is
invalidated) to avoid relying on messenger retention details.
android/app/src/main/kotlin/com/resonate/resonate/MainActivity.kt (2)

15-49: Consider storing the MethodChannel as a member property and cleaning it up in cleanUpFlutterEngine.

Without a stored reference, you can't call setMethodCallHandler(null) on teardown. For a single-engine FlutterActivity this is low-impact, but it deviates from the Android embedding best-practice and will matter if this activity is ever used with multiple engine instances.

♻️ Suggested refactor
 class MainActivity : FlutterActivity() {
     private val channelName = "voice_control_channel"
+    private var voiceChannel: MethodChannel? = null

     override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
         super.configureFlutterEngine(flutterEngine)
-        MethodChannel(
+        voiceChannel = MethodChannel(
             flutterEngine.dartExecutor.binaryMessenger,
             channelName
         ).setMethodCallHandler { call, result ->
             // ...
         }
     }

+    override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
+        voiceChannel?.setMethodCallHandler(null)
+        super.cleanUpFlutterEngine(flutterEngine)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/kotlin/com/resonate/resonate/MainActivity.kt` around
lines 15 - 49, Store the MethodChannel instance as a MainActivity member (e.g.,
a private lateinit var methodChannel) instead of creating it inline, assign it
where you now call MethodChannel(...).setMethodCallHandler and use that member
in your call handling for methods like "setVoiceProfile" (applyVoiceProfile) and
"setPreviewEnabled" (setVoicePreviewEnabled); then override/implement
cleanUpFlutterEngine to call methodChannel.setMethodCallHandler(null) to release
the handler on teardown. Ensure the member is initialized with
flutterEngine.dartExecutor.binaryMessenger and that cleanUpFlutterEngine checks
for initialization/null before clearing.

10-10: Channel name lacks reverse-domain namespacing.

The Flutter docs recommend a reverse-domain prefix (e.g., "com.resonate.resonate/voice_control") to avoid collisions with third-party plugins that may register a channel with the same generic name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/kotlin/com/resonate/resonate/MainActivity.kt` at line
10, The channel name constant channelName in MainActivity.kt uses a generic
string and should be changed to a reverse-domain namespaced identifier (e.g.,
"com.resonate.resonate/voice_control"); update the private val channelName
declaration accordingly and then update any Flutter/Dart code that references
the old "voice_control_channel" string to use the new namespaced value so both
sides match (ensure MainActivity's channel registration and the Dart
MethodChannel/BasicMessageChannel use the identical new name).
lib/views/screens/voice_profile_screen.dart (1)

6-10: Get.put in a StatelessWidget field bypasses GetX route lifecycle management.

When navigating back, GetX automatically calls onClose() and deletes controllers that were registered via a Binding. Because VoiceProfileController is registered with Get.put inside the widget's field rather than through a Binding, GetX's routing system does not track it and the controller is not cleaned up when the route is popped — it stays in memory for the app's lifetime.

The idiomatic fix is a dedicated Binding class (also registered in app_pages.dart) and Get.find in the screen:

♻️ Suggested refactor

Create lib/bindings/voice_profile_binding.dart:

import 'package:get/get.dart';
import '../controllers/voice_profile_controller.dart';

class VoiceProfileBinding extends Bindings {
  `@override`
  void dependencies() {
    Get.lazyPut<VoiceProfileController>(() => VoiceProfileController());
  }
}

In voice_profile_screen.dart:

-class VoiceProfileScreen extends StatelessWidget {
-  VoiceProfileScreen({super.key});
-
-  final VoiceProfileController controller =
-      Get.put(VoiceProfileController());
+class VoiceProfileScreen extends StatelessWidget {
+  const VoiceProfileScreen({super.key});
+
+  `@override`
+  Widget build(BuildContext context) {
+    final controller = Get.find<VoiceProfileController>();

In app_pages.dart:

-    GetPage(
-      name: AppRoutes.voiceProfileScreen,
-      page: () => VoiceProfileScreen(),
-    ),
+    GetPage(
+      name: AppRoutes.voiceProfileScreen,
+      page: () => const VoiceProfileScreen(),
+      binding: VoiceProfileBinding(),
+    ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/views/screens/voice_profile_screen.dart` around lines 6 - 10, Replace the
direct Get.put usage in the VoiceProfileScreen field with lifecycle-managed
binding: create a VoiceProfileBinding class that implements Bindings and
registers VoiceProfileController via Get.lazyPut (or Get.put) in its
dependencies(), register that Binding with the VoiceProfileScreen route in your
routing setup (app_pages.dart), and update VoiceProfileScreen to retrieve the
controller via Get.find<VoiceProfileController>() (not Get.put) so GetX will
track and call onClose() when the route is popped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/Runner/AppDelegate.swift`:
- Around line 46-47: The calls using optional self
(self?.applyVoiceProfile(selectedVoice) and
self?.setVoicePreviewEnabled(isPreviewEnabled)) must not silently noop while
still returning success; update both handlers (the applyVoiceProfile and
setVoicePreviewEnabled call sites in AppDelegate.swift) to unwrap self with a
guard (or if let) and return a Flutter error via result(...) when self is nil,
so Flutter receives failure instead of unconditional success; ensure you use the
same pattern for both the applyVoiceProfile(selectedVoice) block and the
setVoicePreviewEnabled(isPreviewEnabled) block (and the analogous occurrences
mentioned around the second block) so the method returns early with a
descriptive FlutterError when self cannot be obtained.

In `@lib/controllers/voice_profile_controller.dart`:
- Around line 29-39: Both handlers currently mutate reactive state before
awaiting the platform/channel call which can desync UI if the call fails; move
the assignments so the observable updates only after the await completes
successfully. Specifically, in onVoiceProfileChanged, call await
_sendVoiceProfile() first and only set selectedVoice.value = voice after that
succeeds (catch PlatformException/any error and avoid changing selectedVoice or
revert it). Likewise, in onPreviewToggled, call await _sendPreviewState() before
setting isPreviewEnabled.value = value and handle errors to keep UI and native
audio engine consistent. Ensure both methods catch and handle exceptions from
_sendVoiceProfile and _sendPreviewState to prevent stale UI state.
- Around line 20-26: onInit currently fires _sendVoiceProfile() and
_sendPreviewState() without handling their returned Futures, so any
PlatformException is unobserved; update onInit to call each future and attach
proper error handling (e.g., _sendVoiceProfile().catchError(...) and
_sendPreviewState().catchError(...)) to log the PlatformException and apply a
safe fallback state; do not rely on making onInit async (GetX won't await it) —
explicitly handle errors on the Futures returned by _sendVoiceProfile and
_sendPreviewState so the native layer isn't left in an unknown state.

In `@lib/services/voice_control_service.dart`:
- Around line 11-23: The setVoiceProfile and setPreviewEnabled methods call
_channel.invokeMethod without handling PlatformException, which allows errors to
propagate and cause silent unhandled Futures and UI/native state desync; wrap
each invokeMethod call in a try/catch that catches PlatformException, log/report
the error via your logger, and surface failure to callers (e.g., return a bool
or rethrow a wrapped exception) so callers like VoiceProfileController
(_sendVoiceProfile, _sendPreviewState, onVoiceProfileChanged, onPreviewToggled)
can revert reactive state or show an error; ensure the methods' signatures and
return values are adjusted so callers can act on failure.

---

Nitpick comments:
In `@android/app/src/main/kotlin/com/resonate/resonate/MainActivity.kt`:
- Around line 15-49: Store the MethodChannel instance as a MainActivity member
(e.g., a private lateinit var methodChannel) instead of creating it inline,
assign it where you now call MethodChannel(...).setMethodCallHandler and use
that member in your call handling for methods like "setVoiceProfile"
(applyVoiceProfile) and "setPreviewEnabled" (setVoicePreviewEnabled); then
override/implement cleanUpFlutterEngine to call
methodChannel.setMethodCallHandler(null) to release the handler on teardown.
Ensure the member is initialized with flutterEngine.dartExecutor.binaryMessenger
and that cleanUpFlutterEngine checks for initialization/null before clearing.
- Line 10: The channel name constant channelName in MainActivity.kt uses a
generic string and should be changed to a reverse-domain namespaced identifier
(e.g., "com.resonate.resonate/voice_control"); update the private val
channelName declaration accordingly and then update any Flutter/Dart code that
references the old "voice_control_channel" string to use the new namespaced
value so both sides match (ensure MainActivity's channel registration and the
Dart MethodChannel/BasicMessageChannel use the identical new name).

In `@ios/Runner/AppDelegate.swift`:
- Around line 27-67: Make voiceChannel a class-level property (e.g., private var
voiceChannel: FlutterMethodChannel?) instead of a local let in
didFinishLaunchingWithOptions so its lifecycle is explicit; initialize it there
using voiceControlChannelName and controller.binaryMessenger, assign the handler
that calls applyVoiceProfile and setVoicePreviewEnabled as shown, and ensure you
clear/tear down voiceChannel (set to nil) when appropriate (e.g., on deinit or
when the Flutter engine is invalidated) to avoid relying on messenger retention
details.

In `@lib/routes/app_pages.dart`:
- Around line 153-156: The GetPage for AppRoutes.voiceProfileScreen is missing a
binding so the VoiceProfileController isn't created/disposed with navigation;
update the GetPage declaration for AppRoutes.voiceProfileScreen to include
binding: VoiceProfileBinding (so GetX will instantiate VoiceProfileController
per route) and ensure VoiceProfileBinding registers the controller (e.g., via
Get.put/Get.lazyPut inside VoiceProfileBinding), matching the controller
lifecycle expected by VoiceProfileScreen.

In `@lib/views/screens/voice_profile_screen.dart`:
- Around line 6-10: Replace the direct Get.put usage in the VoiceProfileScreen
field with lifecycle-managed binding: create a VoiceProfileBinding class that
implements Bindings and registers VoiceProfileController via Get.lazyPut (or
Get.put) in its dependencies(), register that Binding with the
VoiceProfileScreen route in your routing setup (app_pages.dart), and update
VoiceProfileScreen to retrieve the controller via
Get.find<VoiceProfileController>() (not Get.put) so GetX will track and call
onClose() when the route is popped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Add Flutter Voice Selection UI and Platform Channels for DSP Control

1 participant