Create Plugin - wurzelsand/flutter-memos GitHub Wiki

Create Plugin

There is (was?) a bug in SystemChrome.setEnabledSystemUIMode for Android versions before SDK level 30 where the navigation bar flickers when switching between normal and full screen. So I wrote a plugin for exactly these versions.

  1. Create Android and iOS plugin named fullscreen of fantasy company floatingcircle.com:

    flutter create --template=plugin --org com.floatingcircle --platforms=android,ios fullscreen
    

    Running on Android device:

    ./example/lib/main.dart:

    import 'package:flutter/material.dart';
    import 'dart:async';    
    
    import 'package:flutter/services.dart';
    import 'package:fullscreen/fullscreen.dart';    
    
    void main() {
      runApp(const MyApp());
    }    
    
    class MyApp extends StatefulWidget {
      const MyApp({super.key});    
    
      @override
      State<MyApp> createState() => _MyAppState();
    }    
    
    class _MyAppState extends State<MyApp> {
      String _platformVersion = 'Unknown';
      final _fullscreenPlugin = Fullscreen();    
    
      @override
      void initState() {
        super.initState();
        initPlatformState();
      }    
    
      // Platform messages are asynchronous, so we initialize in an async method.
      Future<void> initPlatformState() async {
        String platformVersion;
        // Platform messages may fail, so we use a try/catch PlatformException.
        // We also handle the message potentially returning null.
        try {
          platformVersion =
              await _fullscreenPlugin.getPlatformVersion() ?? 'Unknown platform version';
        } on PlatformException {
          platformVersion = 'Failed to get platform version.';
        }    
    
        // If the widget was removed from the tree while the asynchronous platform
        // message was in flight, we want to discard the reply rather than calling
        // setState to update our non-existent appearance.
        if (!mounted) return;    
    
        setState(() {
          _platformVersion = platformVersion;
        });
      }    
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              title: const Text('Plugin example app'),
            ),
            body: Center(
              child: Text('Running on: $_platformVersion\n'),
            ),
          ),
        );
      }
    }

    ./lib/fullscreen.dart:

    import 'fullscreen_platform_interface.dart';
    
    class Fullscreen {
      Future<String?> getPlatformVersion() {
        return FullscreenPlatform.instance.getPlatformVersion();
      }
    }

    ./lib/fullscreen_method_channel.dart:

    import 'package:flutter/foundation.dart';
    import 'package:flutter/services.dart';
    
    import 'fullscreen_platform_interface.dart';
    
    /// An implementation of [FullscreenPlatform] that uses method channels.
    class MethodChannelFullscreen extends FullscreenPlatform {
      /// The method channel used to interact with the native platform.
      @visibleForTesting
      final methodChannel = const MethodChannel('fullscreen');
    
      @override
      Future<String?> getPlatformVersion() async {
        final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
        return version;
      }
    }

    ./lib/fullscreen_platform_interface.dart:

    import 'package:plugin_platform_interface/plugin_platform_interface.dart';
    
    import 'fullscreen_method_channel.dart';
    
    abstract class FullscreenPlatform extends PlatformInterface {
      /// Constructs a FullscreenPlatform.
      FullscreenPlatform() : super(token: _token);
    
      static final Object _token = Object();
    
      static FullscreenPlatform _instance = MethodChannelFullscreen();
    
      /// The default instance of [FullscreenPlatform] to use.
      ///
      /// Defaults to [MethodChannelFullscreen].
      static FullscreenPlatform get instance => _instance;
    
      /// Platform-specific implementations should set this with their own
      /// platform-specific class that extends [FullscreenPlatform] when
      /// they register themselves.
      static set instance(FullscreenPlatform instance) {
        PlatformInterface.verifyToken(instance, _token);
        _instance = instance;
      }
    
      Future<String?> getPlatformVersion() {
        throw UnimplementedError('platformVersion() has not been implemented.');
      }
    }

    ./android/src/main/kotlin/com/floatingcircle/fullscreen/FullscreenPlugin.kt:

    package com.floatingcircle.fullscreen
    
    import androidx.annotation.NonNull
    
    import io.flutter.embedding.engine.plugins.FlutterPlugin
    import io.flutter.plugin.common.MethodCall
    import io.flutter.plugin.common.MethodChannel
    import io.flutter.plugin.common.MethodChannel.MethodCallHandler
    import io.flutter.plugin.common.MethodChannel.Result
    
    /** FullscreenPlugin */
    class FullscreenPlugin: FlutterPlugin, MethodCallHandler {
      /// The MethodChannel that will the communication between Flutter and native Android
      ///
      /// This local reference serves to register the plugin with the Flutter Engine and unregister it
      /// when the Flutter Engine is detached from the Activity
      private lateinit var channel : MethodChannel
    
      override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "fullscreen")
        channel.setMethodCallHandler(this)
      }
    
      override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        if (call.method == "getPlatformVersion") {
          result.success("Android ${android.os.Build.VERSION.RELEASE}")
        } else {
          result.notImplemented()
        }
      }
    
      override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
      }
    }

    fullscreen/ios/Classes/SwiftFullscreenPlugin.swift:

    import Flutter
    import UIKit
    
    public class SwiftFullscreenPlugin: NSObject, FlutterPlugin {
      public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "fullscreen", binaryMessenger: registrar.messenger())
        let instance = SwiftFullscreenPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
      }
    
      public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        result("iOS " + UIDevice.current.systemVersion)
      }
    }
  2. Open folder fullscreen/example/ios in Xcode and fullscreen/example/android in Android Studio, to edit the plugins. In Xcode you can find SwiftFullscreenPlugin.swift under Pods/Development Pods/fullscreen/.../Classes.

  3. We need the fullscreen plugin only for Android versions before API level 30 (Android.R).

    ./example/lib/main.dart:

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    import 'package:fullscreen/fullscreen.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatefulWidget {
      const MyApp({super.key});
    
      @override
      State<MyApp> createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      final _fullscreen = Fullscreen();
      var _isFullScreen = false;
    
      @override
      void initState() {
        super.initState();
        SystemChrome.setSystemUIChangeCallback((systemOverlaysAreVisible)  async =>
            setState(() => _isFullScreen = systemOverlaysAreVisible));
      }
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            floatingActionButton: FloatingActionButton(
              onPressed: toggleFullScreen,
              child: Icon(_isFullScreen ? Icons.fullscreen_exit :Icons.fullscreen),
            ),
            appBar: AppBar(
              title: const Text('Fullscreen plugin'),
            ),
            body: const Center(
              child: Text('Flutter full screen plugin'),
            ),
          ),
        );
      }
    
      void toggleFullScreen() {
        setState(() {
          if (_isFullScreen) {
            _fullscreen.exitFullScreen();
            _isFullScreen = false;
          } else {
            _fullscreen.enterFullScreen(FullScreenMode.leanBack);
            _isFullScreen = true;
          }
        });
      }
    }

    ./lib/fullscreen.dart:

    import 'fullscreen_platform_interface.dart';
    
    enum FullScreenMode { immersive, immersiveSticky, leanBack }
    
    class Fullscreen {
      Future<void> enterFullScreen(FullScreenMode mode) {
        return FullscreenPlatform.instance.enterFullScreen(mode);
      }
    
      Future<void> exitFullScreen() {
        return FullscreenPlatform.instance.exitFullScreen();
      }
    }

    ./lib/fullscreen_method_channel.dart:

    import 'dart:io';
    import 'package:flutter/foundation.dart';
    import 'package:flutter/services.dart';
    
    import 'fullscreen.dart';
    import 'fullscreen_platform_interface.dart';
    
    /// An implementation of [FullscreenPlatform] that uses method channels.
    class MethodChannelFullscreen extends FullscreenPlatform {
      /// The method channel used to interact with the native platform.
      @visibleForTesting
      final methodChannel = const MethodChannel('fullscreen');
    
      @override
      Future<void> exitFullScreen() async {
        if (Platform.isIOS) {
          await exitFullScreenFallBack();
        } else if (Platform.isAndroid) {
          try {
            await methodChannel.invokeMethod('exitFullScreen');
          } on PlatformException {
            await exitFullScreenFallBack();
          }
        }
      }
    
      Future<void> exitFullScreenFallBack() async {
        await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
            overlays: SystemUiOverlay.values);
      }
    
      @override
      Future<void> enterFullScreen(FullScreenMode mode) async {
        if (Platform.isIOS) {
          await enterFullScreenFallBack(mode);
        } else if (Platform.isAndroid) {
          try {
            await enterFullScreenAndroid(mode);
          } on PlatformException {
            await enterFullScreenFallBack(mode);
          }
        }
      }
    
      Future<void> enterFullScreenAndroid(FullScreenMode mode) async {
        switch (mode) {
          case FullScreenMode.immersive:
            await methodChannel.invokeMethod('enterImmersiveMode');
            break;
          case FullScreenMode.immersiveSticky:
            await methodChannel.invokeMethod('enterImmersiveStickyMode');
            break;
          case FullScreenMode.leanBack:
            await methodChannel.invokeMethod('enterLeanBackMode');
            break;
        }
      }
    
      Future<void> enterFullScreenFallBack(FullScreenMode mode) async {
        switch (mode) {
          case FullScreenMode.immersive:
            await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
            break;
          case FullScreenMode.immersiveSticky:
            await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
            break;
          case FullScreenMode.leanBack:
            await SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack);
            break;
        }
      }
    }

    ./lib/fullscreen_platform_interface.dart:

    import 'package:plugin_platform_interface/plugin_platform_interface.dart';
    
    import 'fullscreen.dart';
    import 'fullscreen_method_channel.dart';
        
    abstract class FullscreenPlatform extends PlatformInterface {
      /// Constructs a FullscreenPlatform.
      FullscreenPlatform() : super(token: _token);
    
      static final Object _token = Object();
    
      static FullscreenPlatform _instance = MethodChannelFullscreen();
    
      /// The default instance of [FullscreenPlatform] to use.
      ///
      /// Defaults to [MethodChannelFullscreen].
      static FullscreenPlatform get instance => _instance;
    
      /// Platform-specific implementations should set this with their own
      /// platform-specific class that extends [FullscreenPlatform] when
      /// they register themselves.
      static set instance(FullscreenPlatform instance) {
        PlatformInterface.verifyToken(instance, _token);
        _instance = instance;
      }
    
      Future<void> enterFullScreen(FullScreenMode mode) {
        throw UnimplementedError('enterFullScreen() has not been implemented');
      }
    
      Future<void> exitFullScreen() {
        throw UnimplementedError('exitFullScreen() has not been implemented');
      }
    }

    ./android/src/main/kotlin/com/floatingcircle/fullscreen/FullscreenPlugin.kt:

    package com.floatingcircle.fullscreen
    
    import android.app.Activity
    import android.os.Build
    import android.view.View
    import android.view.WindowManager.*
    import androidx.annotation.RequiresApi
    import io.flutter.embedding.engine.plugins.FlutterPlugin
    import io.flutter.embedding.engine.plugins.activity.ActivityAware
    import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
    import io.flutter.plugin.common.MethodCall
    import io.flutter.plugin.common.MethodChannel
    import io.flutter.plugin.common.MethodChannel.MethodCallHandler
    import io.flutter.plugin.common.MethodChannel.Result
    
    
    /** FullscreenPlugin */
    class FullscreenPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
        /// The MethodChannel that will the communication between Flutter and native Android
        ///
        /// This local reference serves to register the plugin with the Flutter Engine and unregister it
        /// when the Flutter Engine is detached from the Activity
        private lateinit var channel: MethodChannel
    
        private lateinit var activity: Activity
    
        override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
            channel = MethodChannel(flutterPluginBinding.binaryMessenger, "fullscreen")
            channel.setMethodCallHandler(this)
        }
    
        @RequiresApi(Build.VERSION_CODES.KITKAT)
        override fun onMethodCall(call: MethodCall, result: Result) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                return result.error(
                    "pluginRejected",
                    "method call cancelled",
                    "Flutter can handle method in Android > R itself"
                )
            }
            when (call.method) {
                "enterImmersiveMode" -> {
                    enterImmersiveMode()
                }
                "enterImmersiveStickyMode" -> {
                    enterImmersiveStickyMode()
                }
                "enterLeanBackMode" -> {
                    enterLeanBackMode()
                }
                "exitFullScreen" -> {
                    exitFullScreen()
                }
                else -> {
                    result.notImplemented()
                }
            }
        }
    
        override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
            channel.setMethodCallHandler(null)
        }
    
        override fun onAttachedToActivity(binding: ActivityPluginBinding) {
            activity = binding.activity
        }
    
        override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
            activity = binding.activity
        }
    
        override fun onDetachedFromActivity() {
            activity.finish()
        }
    
        override fun onDetachedFromActivityForConfigChanges() {
            activity.finish()
        }
    
        private fun showCutoutArea() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                // both landscape and portrait should use cutout area:
                activity.window.attributes.layoutInDisplayCutoutMode =
                    LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
            }
        }
    
        @RequiresApi(Build.VERSION_CODES.KITKAT)
        private fun enterImmersiveMode() {
            showCutoutArea()
            @Suppress("DEPRECATION")
            activity.window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                    or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
                    or View.SYSTEM_UI_FLAG_IMMERSIVE)
        }
    
        @RequiresApi(Build.VERSION_CODES.KITKAT)
        private fun enterImmersiveStickyMode() {
            showCutoutArea()
            @Suppress("DEPRECATION")
            activity.window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                    or View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
                    or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
        }
    
        @RequiresApi(Build.VERSION_CODES.KITKAT)
        private fun enterLeanBackMode() {
            showCutoutArea()
            @Suppress("DEPRECATION")
            activity.window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                    or View.SYSTEM_UI_FLAG_FULLSCREEN) // hide status bar
        }
    
        private fun exitFullScreen() {
            @Suppress("DEPRECATION")
            activity.window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
        }
    }

    fullscreen/ios/Classes/SwiftFullscreenPlugin.swift:

    not needed

  4. Create new Flutter project (flutterproject):

    └── Projects
        ├── fullscreen
        └── flutterproject
    

    pubspec.yaml:

    ...
    dependencies:
      flutter:
        sdk: flutter
    
      fullscreen:
        path: ../fullscreen
    ...

    ./lib/main.dart:

    import 'package:fullscreen/fullscreen.dart';
    ...
⚠️ **GitHub.com Fallback** ⚠️