This commit is contained in:
4831c0 2025-04-04 17:04:55 +02:00
commit 3f5f894e86
Signed by: 4831c0
GPG Key ID: 3F97EDDF98E45AA4
16 changed files with 753 additions and 0 deletions

49
CHANGELOG.md Normal file
View File

@ -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

27
LICENSE Normal file
View File

@ -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.

51
README.md Normal file
View File

@ -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
<application>
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true"
/>
</application>
```
## 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();
},
);
},
);
}
}
```

1
analysis_options.yaml Normal file
View File

@ -0,0 +1 @@
include: package:rexios_lints/flutter/package_extra.yaml

56
android/build.gradle Normal file
View File

@ -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'
}

View File

@ -0,0 +1,2 @@
android.useAndroidX=true
android.enableJetifier=true

View File

@ -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

3
android/proguard-rules.pro vendored Normal file
View File

@ -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

1
android/settings.gradle Normal file
View File

@ -0,0 +1 @@
rootProject.name = 'wear'

View File

@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.rexios.wear_plus"
>
<!-- Flags the app as a Wear app -->
<uses-feature android:name="android.hardware.type.watch" />
<!-- Required for ambient mode support -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>

View File

@ -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<Boolean>("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<Boolean>("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 -> {}
}
}
}

129
lib/src/ambient_widget.dart Normal file
View File

@ -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<StatefulWidget> createState() => _AmbientModeState();
}
class _AmbientModeState extends State<AmbientMode> 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;
}
}

94
lib/src/shape_widget.dart Normal file
View File

@ -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<StatefulWidget> createState() => _WatchShapeState();
}
class _WatchShapeState extends State<WatchShape> {
// 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;
}

152
lib/src/wear.dart Normal file
View File

@ -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 = <AmbientCallback>[];
/// 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<dynamic> _onMethodCallHandler(MethodCall call) async {
switch (call.method) {
case 'onEnterAmbient':
final args = (call.arguments as Map).cast<String, bool>();
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<AmbientCallback>.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<String> getShape() async {
try {
return (await _channel.invokeMethod<String>('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<bool> isAmbient() async {
try {
return (await _channel.invokeMethod<bool>('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<void> setAutoResumeEnabled(bool enabled) async {
try {
await _channel.invokeMethod<String>(
'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<void> setAmbientOffloadEnabled(bool enabled) async {
try {
await _channel.invokeMethod<String>(
'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() {}
}

3
lib/wear_plus.dart Normal file
View File

@ -0,0 +1,3 @@
export 'src/ambient_widget.dart';
export 'src/shape_widget.dart';
export 'src/wear.dart';

22
pubspec.yaml Normal file
View File

@ -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