Read this article carefully to learn how to write a plugin for the volume of a Flutter system that adjusts the system volume and listens for changes in the system volume. If there is any improper place please correct.

0, background

I am currently working on a plugin for Flutter video player fijkPlayer. If you are interested, check out my Github. Consider adding the ability to adjust system volume after version 0.1.0. Googled and found a related plugin for Flutter (the Flutter ecosystem is really fast). But after a closer look at the functionality of the plugin, I felt that it did not meet my needs, and since my FijkPlayer is a plugin itself, I wanted to avoid relying on additional plug-ins, so why not build one myself? This is much simpler than a player plugin. At the time of writing this article, the functions of volume adjustment and monitoring have been completed on fijkplayer plug-in. For the sake of clarity of the document, the related code has been separately extracted as a small project flutter_volume.

1. Environment introduction

Setting up the Flutter environment is not a topic here. Start with the Development environment for the Flutter plugin directly. (Channel stable, v1.9.1+hotfix.2, on Mac OS X 10.14.618G95, + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) locale zh-Hans-CN)

Create the plug-in

Create a Flutter plugin called flutter_volume: Flutter create –org com.befovy -t plugin -i objc flutter_volume The flutter create command uses the -t parameter to select the template. The optional value is app package plugin, which is used to create the flutter application, the flutter package (the function implemented by the dart code), The Flutter plugin (interacts with the host system).

When I started writing FijkPlayer, the default plug-in languages were Java and ObjC. Now, version 1.9 uses Kotlin and Swift by default. Swift I’m not familiar with yet, Kotlin knows a little bit, and Android Studio’s Java transformation Kotlin is very powerful, so my new little project Flutter_volume uses Kotlin and objC as well. If you want to change the programming language used to create the Flutter plug-in, you can use the parameters -i and -a. For example, flutter create -t plugin -a Java -i swift flutter_volume

Plug-in directory structure

Install the Flutter plugin and Dart plugin in Android Studio.

Then use Android Studio to open the plugin project directory flutter_volume that you just created. Note that you use the “Open an Existing Android Studio Project” menu in Android Studio.

When the Flutter project is opened using Android Studio, its structure is as follows. The function of the Flutter Plugin is basically that dart code calls android native Kotlin/Java and iOS native Swift/objC code. The code to do this is shown in the dart source file in the libs directory, the Java/Kotlin source file in the Android/SRC directory, and the objc/ Swift source file in the ios/Classes directory. When you Open a file in an Android directory in the Android Studio project, a clickable link will appear in the upper right corner of the editor to Open any file in the ios folder. There’s a clickable link like “Open for Editing in XCode.”

In my version of Flutter, there were some issues with opening new projects directly with Xcode. The solution is to run Pod Install in the example/ios folder first. Then click “Open for Editing in the XCode” Open XCode project, or use the XCode Open example/ios/Runner xcworkspace project. Focus on the example/ios folder, run Pod Install, and then open the Xcode project.

When you open Xcode, you can see that the objc/ Swift code of the plug-in is covered with a long path by pod using file links. The main purpose of writing iOS plug-ins is to implement functions in the code of this folder. (Screenshot is still a swift plug-in project, later changed to objC for progress, after all, I am not familiar with Swift)

Open a new Android Studio project and wait for Gradle to automatically synchronize. This is a complete Android App project, and the Flutter_volume plugin exists as a modue for the Android project. The implementation of the plug-in is mainly to modify the code in this Module.

The above Xcode project and the Android Studio project are all App projects that can run. The Flutter tool has been taken care of for us. By default, when creating a Flutter plugin, there is an example. The directory structure in the Example folder shown in Figure 1 above is just like that of a normal Flutter App directory, except that the Flutter App uses the flutter_volume plugin for the relative path-dependent outer folder. Large figures 2 and 3 open up the Android and iOS projects in the Example file.

The plugin directory structure that the Flutter tool automatically generates is actually very programmer friendly and can be seen immediately in the demo after the plugin is written.

2. Flutter Native communication

Flutter apps can run on iOS and Android platforms and must interact with native systems in a variety of ways. The interactive part is mainly in the Flutter Engine, as well as a large number of flutter plug-ins.

MethodChannel

The Flutter framework provides this interaction. Messages are passed between the client (UI) and the host (platform) through the Method Channel. In the official document, platform channels was used. In the translation, I used the more specific and direct expression Method Channel.


On the client side, a MethodChannel can send messages corresponding to a method call. On the platform side, MethodChannel on Android and FlutterMethodChannel on iOS allow method calls to be received and results sent back. These classes allow you to develop platform plug-ins with very little boilerplate code. Note: Method calls can also be sent backwards if needed, with the platform acting as a client for the methods implemented in Dart.

The figure above depicts the process of Flutter sending messages to the native end. Also, we need to note that this process can in turn actively send messages from the native end to the Flutter end. That is, the native side creates a MethodChannel and makes a method call, and the Flutter side processes the method and sends the result of the method call. More commonly used in practice is the encapsulation of EventChannel, a higher level of this pattern. The Native end sends events, and the Flutter end responds events. Both MethodChannel and EventChannel will be used later in the tutorial, so you’ll see how it works.

Data transfer on both the Flutter client and native platform requires encoding and decoding. The default way to code is to use StandardMethodCodec, in addition to JSONMethodCodec. StandardMethodCodec is more efficient.

Encoded data type

The data types supported by MethodCodec and their mappings in DART, iOS, and Android are shown in the following table.

Dart Android iOS
null null nil (NSNull when nested)
bool java.lang.Boolean NSNumber numberWithBool:
int java.lang.Integer NSNumber numberWithInt:
int, if 32 bits not enough java.lang.Long NSNumber numberWithLong:
double java.lang.Double NSNumber numberWithDouble:
String java.lang.String NSString
Uint8List byte[] FlutterStandardTypedData typedDataWithBytes:
Int32List int[] FlutterStandardTypedData typedDataWithInt32:
Int64List long[] FlutterStandardTypedData typedDataWithInt64:
Float64List double[] FlutterStandardTypedData typedDataWithFloat64:
List java.util.ArrayList NSArray
Map java.util.HashMap NSDictionary



Part of the code is as follows, please click the link to view the complete code.

class VolumeVal {
  final double vol;
  final int type;
}

typedef VolumeCallback = void Function(VolumeVal value);

class FlutterVolume {
  static const double _step = 1.0 / 16.0;
  static const MethodChannel _channel =
      const MethodChannel('com.befovy.flutter_volume');

  static _VolumeValueNotifier _notifier =
      _VolumeValueNotifier(VolumeVal(vol: 0, type: 0));

  static StreamSubscription _eventSubs;

  void enableWatcher() {
    if (_eventSubs == null) {
      _eventSubs = EventChannel('com.befovy.flutter_volume/event')
          .receiveBroadcastStream()
          .listen(_eventListener, onError: _errorListener);
      _channel.invokeMethod("enable_watch"); }}void disableWatcher() {
    _channel.invokeMethod("disable_watch"); _eventSubs? .cancel(); _eventSubs =null;
  }

  static void _eventListener(dynamic event) {
    final Map<dynamic.dynamic> map = event;
    switch (map['event']) {
      case 'vol':
        double vol = map['v'];
        int type = map['t'];
        _notifier.value = VolumeVal(vol: vol, type: type);
        break;
      default:
        break; }}static Future<double> up({double step = _step, int type = STREAM_MUSIC}) {
    return _channel.invokeMethod("up", <String.dynamic> {'step': step,
      'type': type,
    });
  }

  static voidaddVolListener(VoidCallback listener) { _notifier.addListener(listener); }}class VolumeWatcher extends StatefulWidget {
  final VolumeCallback watcher;
  final Widget child;

  VolumeWatcher({
    @required this.watcher,
    @required this.child,
  });

  @override
  _VolumeWatcherState createState() => _VolumeWatcherState();
}
Copy the code

Both MethodChannel and EventChannel are used. Flutter uses MethodChannel to send a method call request to the native side and get the result of the method call. To avoid UI gridlock, method calls are made in asynchronous mode. EventChannel processes event notifications sent by Native on the Flutter side. In a Flutter, the names of all channels must be unique, otherwise messages will fail to be sent.

  • MethodChannelThe name parameter is used to construct oneMethodChannelAnd the use ofinvokeMethodSends messages and parameters and returns asynchronous results.
  • EventChannelIt’s a little more complicated to use, but it’s boilerplate code. structureEventChannelAnd listen for event broadcasts, registering event handlers and error handlers. Cancel the broadcast subscription when you are done.

In the interface design, I added the optional parameter type for different audio types, but in the initial implementation, only functions related to media sound types will be implemented. This optional parameter ensures that the interface does not change in the future.

The full code changes can be seen on github’s commit. Github.com/befovy/flut…

4. IOS function realization

FlutterPluginRegistrar

FlutterPluginRegistrar is the Context information of the Flutter plug-in in the iOS environment and provides the plugin context information as well as App callback event information. The FlutterPluginRegistrar instance object needs to be stored in the Plugin class member variables for further use. Adjust the no-argument init function of the FlutterVolumePlugin to initWithRegistrar.

@implementation FlutterVolumePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
  FlutterMethodChannel *channel =
      [FlutterMethodChannel methodChannelWithName:@"com.befovy.flutter_volume"
                                  binaryMessenger:[registrar messenger]];
  FlutterVolumePlugin *instance =
      [[FlutterVolumePlugin alloc] initWithRegistrar:registrar];
  [registrar addMethodCallDelegate:instance channel:channel];
}

- (instancetype)initWithRegistrar:
    (NSObject<FlutterPluginRegistrar> *)registrar {
  self = [super init];
  if (self) {
    _registrar = registrar;
  }
  return self;
}
@end
Copy the code

IOS listens for volume changes

The ios notification center broadcasts volume changes. You only need to register notifications in the notification center to listen to volume changes. According to the interface design, monitor the system volume changes, there are two interfaces to call the control function on or off. The main code for volume monitoring is as follows:

@implementation FlutterVolumePlugin
- (void)enableWatch {
  if (_eventListening == NO) {
    _eventListening = YES;

    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(volumeChange:)
               name:@"AVSystemController_SystemVolumeDidChangeNotification"
             object:nil];
      
    _eventChannel = [FlutterEventChannel
                    eventChannelWithName:@"com.befovy.flutter_volume/event"
                    binaryMessenger:[_registrar messenger]];
    [_eventChannel setStreamHandler:self]; }} - (void)disableWatch {
  if (_eventListening == YES) {
    _eventListening = NO;

    [[NSNotificationCenter defaultCenter]
        removeObserver:self
                  name:@"AVSystemController_SystemVolumeDidChangeNotification"
                object:nil];
    [_eventChannel setStreamHandler:nil];
    _eventChannel = nil; }} - (void)volumeChange:(NSNotification *)notification {
  NSString *style = [notification.userInfo
      objectForKey:@"AVSystemController_AudioCategoryNotificationParameter"];
  CGFloat value = [[notification.userInfo
      objectForKey:@"AVSystemController_AudioVolumeNotificationParameter"]
      doubleValue];
  if ([style isEqualToString:@"Audio/Video"{[])selfsendVolumeChange:value]; }} - (void)sendVolumeChange:(float)value {
  if (_eventListening) {
      NSLog(@"valume val %f\n", value);
    [_eventSink success:@{@"event" : @"volume".@"vol": @(value)}]; }}@end
Copy the code

EnableWatch registers handlers for volume changes in the notification center. Then construct the FlutterEventChannel and set up the handler. Remove the callback registered in the notification center in disableWatch, then remove the EventChannel handler, and remove the EventChannel object. It’s important to note that, Dart EventChannel(‘ XXX ‘).receiveBroadcastStream() must be called at the native end after executing the FlutterEventChannel setStreamHandler method. Otherwise, the onListen method will not find the error.

System Volume Modification

There is no public interface for changing the system volume in iOS, but there are other ways to change the volume. By far the most widely used is to insert an invisible MPVolumeView into the UI and then adjust the MPVolumeSlider in it by simulating the UI operation.

@implementation FlutterVolumePlugin
- (void)initVolumeView {
  if (_volumeView == nil) {
    _volumeView =
        [[MPVolumeView alloc] initWithFrame:CGRectMake(- 100..- 100..10.10)];
    _volumeView.hidden = YES;
  }
  if (_volumeViewSlider == nil) {
    for (UIView *view in [_volumeView subviews]) {
      if ([view.class.description isEqualToString:@"MPVolumeSlider"]) {
        _volumeViewSlider = (UISlider *)view;
        break; }}}if(! _volumeInWindow) {UIWindow *window = UIApplication.sharedApplication.keyWindow;
    if(window ! =nil) {
      [window addSubview:_volumeView];
      _volumeInWindow = YES; }}} - (float)getVolume {
  [self initVolumeView];
  if (_volumeViewSlider == nil) {
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    CGFloat currentVol = audioSession.outputVolume;
    return currentVol;
  } else {
    return_volumeViewSlider.value; }} - (float)setVolume:(float)vol {
  [self initVolumeView];
  if (vol > 1.0) {
    vol = 1.0;
  } else if (vol < 0) {
    vol = 0.0;
  }
  [_volumeViewSlider setValue:vol animated:FALSE];
  vol = _volumeViewSlider.value;
  return vol;
}
@end
Copy the code

Complete iOS plugin code point I view

5. Android function realization

The Development of Android Flutter plug-in needs the interface Registrar in Flutter engine. Via the Registrar method, we can obtain the activity, context and other important objects in Android development.

Registrar

 public interface Registrar {
    Activity activity(a);
    Context context(a);
    Context activeContext(a); . }Copy the code

class FlutterVolumePlugin(registrar: Registrar): MethodCallHandler {
  companion object {
    @JvmStatic
    fun registerWith(registrar: Registrar) {
      val channel = MethodChannel(registrar.messenger(), "flutter_volume")
      channel.setMethodCallHandler(FlutterVolumePlugin(registrar))
    }
  }
  private val mRegistrar: Registrar = registrar
}
Copy the code

Modify the auto-generated Plugin class to add the mRegistrar member variable (see the code snippseabove) so that the activity, context and other important variables can be obtained when the method call is processed in the onMethodCall member function.

For example, the AudioManager used for volume changes in Android.

 class FlutterVolumePlugin(registrar: Registrar): MethodCallHandler {
   private fun audioManager(a): AudioManager {
     val activity = mRegistrar.activity()
     return activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager
   }
 }
Copy the code

The main implementation of the Volume adjustment function in Android is the API call of the AudioManager and the handling of the flutter onMethodCall method. Click on the source code for more details.

Listen for changes in volume

Android uses BroadcastReceiver to get volume changes using broadcast notifications. According to the interface design, monitor the system volume changes, there are two interfaces to call the control function on or off. In the enableWatch method, modify the marker variable mWatching, then create EventChannel and call the setStreamHandler method. Finally, register a broadcast receiver to be notified of changes in the system volume. Dart EventChannel(‘ XXX ‘).receiveBroadcastStream() must be called at the native end of the dart after the setStreamHandler method is executed. Otherwise, the onListen method will not find the error.

class FlutterVolumePlugin(registrar: Registrar) : MethodCallHandler {
	private fun enableWatch(a) {
        if(! mWatching) { mWatching =true
            mEventChannel = EventChannel(mRegistrar.messenger(), "com.befovy.flutter_volume/event") mEventChannel!! .setStreamHandler(object : EventChannel.StreamHandler {
                override fun onListen(o: Any? , eventSink:EventChannel.EventSink) {
                    mEventSink.setDelegate(eventSink)
                }

                override fun onCancel(o: Any?). {
                    mEventSink.setDelegate(null)
                }
            })

            mVolumeReceiver = VolumeReceiver(this)
            val filter = IntentFilter()
            filter.addAction(VOLUME_CHANGED_ACTION)
            mRegistrar.activeContext().registerReceiver(mVolumeReceiver, filter)
        }

    }

    private fun disableWatch(a) {
        if (mWatching) {
            mWatching = falsemEventChannel!! .setStreamHandler(null)
            mEventChannel = null

            mRegistrar.activeContext().unregisterReceiver(mVolumeReceiver)
            mVolumeReceiver = null}}}Copy the code

In the onReceive method of getting the BroadcastReceiver notification of volume changes, use EventChannel to send the event content to the flutter side.


private class VolumeReceiver(plugin: FlutterVolumePlugin) : BroadcastReceiver() {
    private var mPlugin: WeakReference<FlutterVolumePlugin> = WeakReference(plugin)
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == "android.media.VOLUME_CHANGED_ACTION") {
            val plugin = mPlugin.get(a)if(plugin ! =null) {
                val volume = plugin.getVolume()
                val event: MutableMap<String, Any> = mutableMapOf()
                event["event"] = "vol"
                event["v"] = volume
                event["t"] = AudioManager.STREAM_MUSIC
                plugin.sink(event)
            }
        }
    }
}

class FlutterVolumePlugin(registrar: Registrar) : MethodCallHandler {
    fun sink(event: Any) {
        mEventSink.success(event)
    }
}
Copy the code

Click on the source code for more details.

Volume interval mapping

On Android, the maximum volume may be different. The range is not [0, 1]. After the plug-in gets the maximum volume, it maps the volume linearly to the range [0, 1]. Another point to note is that android volume adjustment is not stepless. There is a minimum unit of adjustment. Map this minimum unit to a delta value in the range [0, 1], and ensure that the step value of adjustment volume is greater than or equal to this minimum unit delta value, otherwise the volume adjustment is invalid. In the API implementation of the plug-in, if the step parameter value is less than delta when calling up or Down interfaces, it will be changed to the value of delta to ensure that the calls of up or Down interfaces are valid.

6. Plug-in Demo

The default folders created by the FLUTTER plugin all contain an example folder. Inside is a complete folder for the Flutter app project, referencing the flutter plug-ins in the outer folder using relative paths.

dev_dependencies:
  flutter_volume:
    path: ../
Copy the code

Dart: import ‘package:flutter_volume/flutter_volume.dart’;

Then simply write a few buttons and call the API in Flutter_Volume.dart in onPressed to complete the plug-in’s sample App. For more details, see the complete source code example/lib/main.dart

7. Release plug-ins

Once you’ve finished developing and testing your plug-in or Dart package, you can publish it to the Pub so that other developers can use it quickly and easily. Flutter dependency management PubSpec supports importing dependencies through local paths and Git, but pub makes it easier to version the plugin.

Volume flutter_volume has been corrupted, so I will not post to pub for now

To publish plugins to pub, you need to log in to Google account, please prepare ladder in advance.

Before publishing, check pubspec.yaml, readme. md and Changelog. md, LICENSE files for completeness and correctness. Pubspec. yaml contains some meta information about the plugin and its author in addition to the plugin dependencies, which need to be added:

name: flutter_volume
description: A Plugin for Volume Control and Monitoring, support iOS and Android
version: 0.01.
author: befovy
homepage: blog.befovy.com
Copy the code

Then, run the dry-run command to see if there are any other problems with the plug-in:

flutter packages pub publish --dry-run
Copy the code

If the command outputs Package has 0 warnings, everything is fine. Finally, run the publish command flutter Packages pub publish to prompt you to verify your Google account if this is the first release.

Looks great! Are you ready to upload your package (y/n)? y Pub needs your authorization to upload packages on your behalf. In a web browser, go to https://accounts.google.com/o/oauth2/auth?access_type=offline*****..... Then click"Allow access".

Waiting for your authorization...
Successfully authorized.
Uploading...
Successful uploaded package.
Copy the code

After the upload is Successful, the Successful event package is displayed. After the release, can be in pub.dartlang.org/packages/${… View releases.

The resources

Flutter. Dev/docs/develo…