commit 3f5f894e86d6fe6e031221ed8abd795120a65589 Author: 4831c0 <4831c0@proton.me> Date: Fri Apr 4 17:04:55 2025 +0200 1.2.3 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6076988 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# 1.2.3 + +- Replaces deprecated native lifecycle observer methods (by [@Crdzbird](https://github.com/Crdzbird) in [#6](https://github.com/Rexios80/wear_plus/pull/6)) + +# 1.2.2 + +- Fixes build issue + +# 1.2.1 + +- Updates proguard rules for R8 + +# 1.2.0 + +- First release of `wear_plus`. Replace any references to the `wear` plugin with `wear_plus` to switch. +- `InheritedShape` is now private. This would be a breaking change if this wasn't the first release of a new plugin. + +# 1.1.0 + +- Fix issue with non-windows builds breaking. + +# 1.0.0 + +- Null Safety Migration (Finally!) + - Thanks to Rexios and Peter Ullrich. + - Min Dart 2.12 / Flutter 2.5 +- Updated native component versions: + - Gradle 6.5 + - Android Gradle Plugin 4.1.0 + - Android compileSdkVersion 31 + - AndroidX Wear 1.2.0 + - Kotlin 1.5.10 + - Removed `jcenter()` repo requirement. + +# 0.1.1 + +- Fix Kotlin/Android compileOnly dep on com.google.android.wearable:wearable. + +# 0.1.0 + +- Updated to AndroidX and Android embedding v2. +- Renamed `Shape` is now `WearShape`. +- Renamed `Mode` is now `WearMode`. +- Deprecated `InheritedShape`. + _Add `WatchShape` instead and use `WatchShape.of(context)` to get the shape value._ + +# 0.0.1 + +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7d72d53 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright 2018 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f80487 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Flutter Wear Plugin + +A plugin that offers Flutter support for Wear OS by Google (Android Wear). + +__To use this plugin you must set your `minSdkVersion` to `23`.__ + + +# Tutorial + +https://medium.com/flutter-community/flutter-building-wearos-app-fedf0f06d1b4 + + +# Widgets + +There currently three widgets provided by the plugin: + +* WatchShape: determines whether the watch is square or round. +* AmbientMode: builder that provides what mode the watch is in. The widget will rebuild whenever the watch changes mode. + + +## Setup + +If you are creating a standalone watch app, add the following to your manifest: + +```xml + + + +``` + +## Example + +```dart +class WatchScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return WatchShape( + builder: (BuildContext context, WearShape shape, Widget? child) { + return AmbientMode( + builder: (context, mode, child) { + return mode == Mode.active ? ActiveWatchFace() : AmbientWatchFace(); + }, + ); + }, + ); + } +} +``` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..5f6db13 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1 @@ +include: package:rexios_lints/flutter/package_extra.yaml \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..3a2d6c7 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,56 @@ +group 'dev.rexios.wear_plus' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.20' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +//apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 31 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 23 + consumerProguardFiles 'proguard-rules.pro' + } + lintOptions { + disable 'InvalidPackage' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } + namespace "dev.rexios.wear_plus" +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.wear:wear:1.3.0' + implementation 'com.google.android.support:wearable:2.9.0' + compileOnly 'com.google.android.wearable:wearable:2.9.0' +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..646c51b --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c46198 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 0000000..72bff7b --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1,3 @@ +-keep class dev.rexios.wear_plus.** { *; } +-dontwarn com.google.android.wearable.compat.WearableActivityController$AmbientCallback +-dontwarn com.google.android.wearable.compat.WearableActivityController \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..4adff7b --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'wear' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f885959 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/android/src/main/kotlin/dev/rexios/wear_plus/WearPlugin.kt b/android/src/main/kotlin/dev/rexios/wear_plus/WearPlugin.kt new file mode 100644 index 0000000..1937d61 --- /dev/null +++ b/android/src/main/kotlin/dev/rexios/wear_plus/WearPlugin.kt @@ -0,0 +1,147 @@ +package dev.rexios.wear_plus + +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import com.google.android.wearable.compat.WearableActivityController +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.embedding.engine.plugins.lifecycle.HiddenLifecycleReference +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 + + +class WearPlugin : FlutterPlugin, ActivityAware, MethodCallHandler, LifecycleEventObserver { + private var mAmbientCallback = WearableAmbientCallback() + private var mMethodChannel: MethodChannel? = null + private var mActivityBinding: ActivityPluginBinding? = null + private var mAmbientController: WearableActivityController? = null + + companion object { + const val TAG = "WearPlugin" + const val BURN_IN_PROTECTION = WearableActivityController.EXTRA_BURN_IN_PROTECTION + const val LOW_BIT_AMBIENT = WearableActivityController.EXTRA_LOWBIT_AMBIENT + } + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + mMethodChannel = MethodChannel(binding.binaryMessenger, "wear") + mMethodChannel!!.setMethodCallHandler(this) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + mMethodChannel?.setMethodCallHandler(this) + mMethodChannel = null + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + attachAmbientController(binding) + } + + override fun onDetachedFromActivityForConfigChanges() { + detachAmbientController() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + attachAmbientController(binding) + } + + override fun onDetachedFromActivity() { + detachAmbientController() + } + + private fun attachAmbientController(binding: ActivityPluginBinding) { + mActivityBinding = binding + mAmbientController = WearableActivityController(TAG, binding.activity, mAmbientCallback) + mAmbientController?.setAmbientEnabled() + val reference = (binding.lifecycle as HiddenLifecycleReference) + reference.lifecycle.addObserver(this) + } + + private fun detachAmbientController() { + mActivityBinding?.let { + val reference = (it.lifecycle as HiddenLifecycleReference) + reference.lifecycle.removeObserver(this) + } + mActivityBinding = null + + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "getShape" -> { + val activity = mActivityBinding?.activity + when { + activity == null -> { + result.error("no-activity", "No android activity available.", null) + } + activity.resources.configuration.isScreenRound -> { + result.success("round") + } + else -> { + result.success("square") + } + } + } + "isAmbient" -> { + result.success(mAmbientController?.isAmbient ?: false) + } + "setAutoResumeEnabled" -> { + val enabled = call.argument("enabled") + if (mAmbientController == null || enabled == null) { + result.error("not-ready", "Ambient mode controller not ready", null) + } else { + mAmbientController!!.setAutoResumeEnabled(enabled) + result.success(null) + } + } + "setAmbientOffloadEnabled" -> { + val enabled = call.argument("enabled") + if (mAmbientController == null || enabled == null) { + result.error("not-ready", "Ambient mode controller not ready", null) + } else { + mAmbientController!!.setAmbientOffloadEnabled(enabled) + result.success(null) + } + } + else -> result.notImplemented() + } + } + + inner class WearableAmbientCallback : WearableActivityController.AmbientCallback() { + override fun onEnterAmbient(ambientDetails: Bundle) { + val burnInProtection = ambientDetails.getBoolean(BURN_IN_PROTECTION, false) + val lowBitAmbient = ambientDetails.getBoolean(LOW_BIT_AMBIENT, false) + mMethodChannel?.invokeMethod("onEnterAmbient", mapOf( + "burnInProtection" to burnInProtection, + "lowBitAmbient" to lowBitAmbient + )) + } + + override fun onExitAmbient() { + mMethodChannel?.invokeMethod("onExitAmbient", null) + } + + override fun onUpdateAmbient() { + mMethodChannel?.invokeMethod("onUpdateAmbient", null) + } + + override fun onInvalidateAmbientOffload() { + mMethodChannel?.invokeMethod("onInvalidateAmbientOffload", null) + } + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_CREATE -> mAmbientController?.onCreate() + Lifecycle.Event.ON_RESUME -> mAmbientController?.onResume() + Lifecycle.Event.ON_PAUSE -> mAmbientController?.onPause() + Lifecycle.Event.ON_STOP -> mAmbientController?.onStop() + Lifecycle.Event.ON_DESTROY -> mAmbientController?.onDestroy() + else -> {} + } + } +} diff --git a/lib/src/ambient_widget.dart b/lib/src/ambient_widget.dart new file mode 100644 index 0000000..5840619 --- /dev/null +++ b/lib/src/ambient_widget.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:wear_plus/src/wear.dart'; + +/// Ambient modes for a Wear device +enum WearMode { + /// The screen is active + active, + + /// The screen is in ambient mode + ambient, +} + +/// Builds a child for [AmbientMode] +typedef AmbientModeWidgetBuilder = Widget Function( + BuildContext context, + WearMode mode, + Widget? child, +); + +/// Widget that listens for when a Wear device enters full power or ambient mode, +/// and provides this in a builder. It optionally takes an [onUpdate] function that's +/// called every time the wear device triggers an ambient update request. +@immutable +class AmbientMode extends StatefulWidget { + /// Constructor + const AmbientMode({ + super.key, + required this.builder, + this.child, + this.onUpdate, + }); + + /// Built when the mode changes + final AmbientModeWidgetBuilder builder; + + /// Optional child that will not get rebuilt when the mode changes + final Widget? child; + + /// Called each time the the wear device triggers an ambient update request. + final VoidCallback? onUpdate; + + /// Get current [WearMode]. + static WearMode wearModeOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType<_InheritedAmbientMode>()! + .mode; + } + + /// Get current [AmbientDetails]. + static AmbientDetails ambientDetailsOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType<_InheritedAmbientMode>()! + .details; + } + + @override + State createState() => _AmbientModeState(); +} + +class _AmbientModeState extends State with AmbientCallback { + var _ambientMode = WearMode.active; + final _ambientDetails = const AmbientDetails(false, false); + + @override + void initState() { + super.initState(); + Wear.instance.registerAmbientCallback(this); + _initState(); + } + + void _initState() async { + final isAmbient = await Wear.instance.isAmbient(); + _updateMode(isAmbient); + } + + @override + void dispose() { + Wear.instance.unregisterAmbientCallback(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _InheritedAmbientMode( + mode: _ambientMode, + details: _ambientDetails, + child: Builder( + builder: (context) { + return widget.builder(context, _ambientMode, widget.child); + }, + ), + ); + } + + void _updateMode(bool isAmbient) { + if (!mounted) return; + setState( + () => _ambientMode = isAmbient ? WearMode.ambient : WearMode.active, + ); + } + + @override + void onEnterAmbient(AmbientDetails ambientDetails) => _updateMode(true); + + @override + void onExitAmbient() => _updateMode(false); + + @override + void onUpdateAmbient() { + _updateMode(true); + widget.onUpdate?.call(); + } +} + +class _InheritedAmbientMode extends InheritedWidget { + const _InheritedAmbientMode({ + required this.mode, + required this.details, + required super.child, + }); + + final WearMode mode; + final AmbientDetails details; + + @override + bool updateShouldNotify(_InheritedAmbientMode old) { + return mode != old.mode || details != old.details; + } +} diff --git a/lib/src/shape_widget.dart b/lib/src/shape_widget.dart new file mode 100644 index 0000000..f846655 --- /dev/null +++ b/lib/src/shape_widget.dart @@ -0,0 +1,94 @@ +import 'package:flutter/widgets.dart'; +import 'package:wear_plus/src/wear.dart'; + +/// Shape of a Wear device +enum WearShape { + /// The display is square + square, + + /// The display is round + round, +} + +/// Builds a child for a [WatchShape] +typedef WatchShapeBuilder = Widget Function( + BuildContext context, + WearShape shape, + Widget? child, +); + +/// Builder widget for watch shapes +@immutable +class WatchShape extends StatefulWidget { + /// Constructor + const WatchShape({ + super.key, + required this.builder, + this.child, + }); + + /// Built when the shape changes + final WatchShapeBuilder builder; + + /// Optional child that will not get rebuilt when the shape changes + final Widget? child; + + /// Call [WatchShape.of(context)] to retrieve the shape further down + /// in the widget hierarchy. + static WearShape of(BuildContext context) { + return _InheritedShape.of(context).shape; + } + + @override + State createState() => _WatchShapeState(); +} + +class _WatchShapeState extends State { + // Default to round until the platform returns the shape + // round being the most common form factor for WearOS + var _shape = WearShape.round; + + @override + void initState() { + super.initState(); + _initState(); + } + + void _initState() async { + final shape = await Wear.instance.getShape(); + if (!mounted) return; + setState( + () => _shape = (shape == 'round' ? WearShape.round : WearShape.square), + ); + } + + @override + Widget build(BuildContext context) { + return _InheritedShape( + shape: _shape, + child: Builder( + builder: (context) { + return widget.builder(context, _shape, widget.child); + }, + ), + ); + } +} + +class _InheritedShape extends InheritedWidget { + /// Constructor + const _InheritedShape({ + required this.shape, + required super.child, + }); + + final WearShape shape; + + static _InheritedShape of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_InheritedShape>()!; + } + + @override + bool updateShouldNotify(_InheritedShape oldWidget) => + shape != oldWidget.shape; +} diff --git a/lib/src/wear.dart b/lib/src/wear.dart new file mode 100644 index 0000000..7c71c7e --- /dev/null +++ b/lib/src/wear.dart @@ -0,0 +1,152 @@ +import 'package:flutter/foundation.dart' show debugPrint; +import 'package:flutter/services.dart' + show MethodChannel, MethodCall, PlatformException; + +/// Provides access to Wearable features +class Wear { + static const _channel = MethodChannel('wear'); + + /// Get the [Wear] instance + factory Wear() => instance; + + /// Access to the singleton instance + static final instance = Wear._(); + + Wear._() { + _channel.setMethodCallHandler(_onMethodCallHandler); + } + + final _ambientCallbacks = []; + + /// Register callback for ambient notifications + void registerAmbientCallback(AmbientCallback callback) { + _ambientCallbacks.add(callback); + } + + /// Unregister callback for ambient notifications + void unregisterAmbientCallback(AmbientCallback callback) { + _ambientCallbacks.remove(callback); + } + + Future _onMethodCallHandler(MethodCall call) async { + switch (call.method) { + case 'onEnterAmbient': + final args = (call.arguments as Map).cast(); + final details = + AmbientDetails(args['burnInProtection']!, args['lowBitAmbient']!); + _notifyAmbientCallbacks((callback) => callback.onEnterAmbient(details)); + case 'onExitAmbient': + _notifyAmbientCallbacks((callback) => callback.onExitAmbient()); + case 'onUpdateAmbient': + _notifyAmbientCallbacks((callback) => callback.onUpdateAmbient()); + case 'onInvalidateAmbientOffload': + _notifyAmbientCallbacks( + (callback) => callback.onInvalidateAmbientOffload(), + ); + } + } + + void _notifyAmbientCallbacks(Function(AmbientCallback callback) fn) { + final callbacks = List.from(_ambientCallbacks); + for (final callback in callbacks) { + try { + fn(callback); + } catch (e, st) { + debugPrint('Failed callback: $callback\n$e\n$st'); + } + } + } + + /// Fetches the shape of the watch face + Future getShape() async { + try { + return (await _channel.invokeMethod('getShape'))!; + } on PlatformException catch (e, st) { + // Default to round + debugPrint('Error calling getShape: $e\n$st'); + return 'round'; + } + } + + /// Tells the application if we are currently in ambient mode + Future isAmbient() async { + try { + return (await _channel.invokeMethod('isAmbient'))!; + } on PlatformException catch (e, st) { + debugPrint('Error calling isAmbient: $e\n$st'); + return false; + } + } + + /// Sets whether this activity's task should be moved to the front when + /// the system exits ambient mode. + /// + /// If true, the activity's task may be moved to the front if it was the + /// last activity to be running when ambient started, depending on how + /// much time the system spent in ambient mode. + /// + Future setAutoResumeEnabled(bool enabled) async { + try { + await _channel.invokeMethod( + 'setAutoResumeEnabled', + {'enabled': enabled}, + ); + } on PlatformException catch (e, st) { + debugPrint('Error calling setAutoResumeEnabled: $e\n$st'); + rethrow; + } + } + + /// Sets whether this activity is currently in a state that supports ambient offload mode. + Future setAmbientOffloadEnabled(bool enabled) async { + try { + await _channel.invokeMethod( + 'setAmbientOffloadEnabled', + {'enabled': enabled}, + ); + } on PlatformException catch (e, st) { + debugPrint('Error calling setAmbientOffloadEnabled: $e\n$st'); + rethrow; + } + } +} + +/// Provides details of current ambient mode configuration. +class AmbientDetails { + /// Constructor + const AmbientDetails(this.burnInProtection, this.lowBitAmbient); + + /// Used to indicate whether burn-in protection is required. + /// + /// When this property is set to true, views must be shifted around + /// periodically in ambient mode. To ensure that content isn't + /// shifted off the screen, avoid placing content within 10 pixels + /// of the edge of the screen. Activities should also avoid solid + /// white areas to prevent pixel burn-in. Both of these + /// requirements only apply in ambient mode, and only when + /// this property is set to true. + final bool burnInProtection; + + /// Used to indicate whether the device has low-bit ambient mode. + /// + /// When this property is set to true, the screen supports fewer bits + /// for each color in ambient mode. In this case, activities should + /// disable anti-aliasing in ambient mode. + final bool lowBitAmbient; +} + +/// Callback to receive ambient mode state changes. +mixin AmbientCallback { + /// Called when an activity is entering ambient mode. + void onEnterAmbient(AmbientDetails ambientDetails) {} + + /// Called when an activity should exit ambient mode. + void onExitAmbient() {} + + /// Called when the system is updating the display for ambient mode. + void onUpdateAmbient() {} + + /// Called to inform an activity that whatever decomposition it has sent to + /// Sidekick is no longer valid and should be re-sent before enabling ambient offload. + void onInvalidateAmbientOffload() {} +} diff --git a/lib/wear_plus.dart b/lib/wear_plus.dart new file mode 100644 index 0000000..0e5e1de --- /dev/null +++ b/lib/wear_plus.dart @@ -0,0 +1,3 @@ +export 'src/ambient_widget.dart'; +export 'src/shape_widget.dart'; +export 'src/wear.dart'; diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..75461c3 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,22 @@ +name: wear_plus +description: An actively maintained plugin that offers Flutter support for Wear OS by Google +version: 1.2.3 +homepage: https://github.com/Rexios80/wear_plus + +environment: + sdk: ^3.0.0 + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + rexios_lints: ^10.1.0 + +flutter: + plugin: + platforms: + android: + package: dev.rexios.wear_plus + pluginClass: WearPlugin