Skip to content

Commit

Permalink
Quality improvements (#25)
Browse files Browse the repository at this point in the history
* Add GitHub Actions, Tests and Integration Tests to ensure further quality
* Fix double and null handling on Android
* Fix HomeWidget.updateWidget not completing on Android (Fixes #26)
  • Loading branch information
ABausG committed Jul 5, 2021
1 parent 4f69231 commit 2dac270
Show file tree
Hide file tree
Showing 16 changed files with 452 additions and 93 deletions.
77 changes: 77 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Build

on:
push:
branches:
- main
pull_request:

jobs:
quality:
name: Quality Checks
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- uses: subosito/flutter-action@v1.4.0
with:
channel: stable
- name: Get Packages
run: flutter pub get
- name: Analyze
run: flutter analyze
- name: Format
run: flutter format . --set-exit-if-changed
- name: Publishability
run: flutter pub publish --dry-run
- name: Test
run: flutter test --coverage
- uses: VeryGoodOpenSource/very_good_coverage@v1.1.1

android:
name: Android Integration Tests
runs-on: macos-latest

steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1.4.0
with:
channel: stable
- name: Run Android Integration Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
working-directory: example
script: flutter drive --driver=test_driver/integration_test.dart --target=integration_test/android_test.dart -d emulator-5554

# iOS Test based on https://medium.com/flutter-community/run-flutter-driver-tests-on-github-actions-13c639c7e4ab
# by @kate_sheremet
ios:
name: iOS Integration Tests
strategy:
matrix:
device:
- "iPhone 12 Pro Max (14.4)"
fail-fast: false
runs-on: macOS-latest
steps:
- name: List Available Devices
run: xcrun xctrace list devices 2>&1
- name: Set Simulator Id
run: |
echo "::set-output name=UDID::$(xcrun xctrace list devices 2>&1 |
awk -F '( |\\()' \
-v 'device=${{ matrix.device }}' \
'length($0) == length(device)+39 && substr($0,0,length(device)) == device { print substr($NF,0, length($NF) - 1)}')"
id: udid
- name: "Start Simulator"
run: |
xcrun simctl boot "${{steps.udid.outputs.UDID}}"
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
channel: stable
- name: "Run iOS integration tests"
run: flutter drive --driver=test_driver/integration_test.dart --target=integration_test/ios_test.dart -d ${{steps.udid.outputs.UDID}}
working-directory: example
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
## 0.1.3

* Add GitHub Actions, Tests and Integration Tests to ensure further quality
* Fix double and null handling on Android
* Fix HomeWidget.updateWidget not completing on Android [#26](https://github.com/ABausG/home_widget/issues/26)

## 0.1.2+1

* Fix [#19](https://github.com/ABausG/home_widget/issues/19) Receiver not registered bug

## 0.1.2

* Add Click Listeners
* Detect if App was launched via a view from the HomeScreen Widget
* Detect if App has been launched via a view from the HomeScreen Widget
* Execute Background Dart Code when clicking on a view in HomeScreen Widget [Android only]

## 0.1.1+2

* Set sdk bound correctly
* Woraround for analysis_options import error
* Workaround for analysis_options import error
* Cleanup Example

## 0.1.1+1
Expand Down
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
[![Pub](https://img.shields.io/pub/v/home_widget.svg)](https://pub.dartlang.org/packages/home_widget)
[![likes](https://badges.bar/home_widget/likes)](https://pub.dev/packages/home_widget/score)
[![popularity](https://badges.bar/home_widget/popularity)](https://pub.dev/packages/home_widget/score)
[![pub points](https://badges.bar/home_widget/pub%20points)](https://pub.dev/packages/home_widget/score)
[![pub points](https://badges.bar/home_widget/pub%20points)](https://pub.dev/packages/home_widget/score)
![Build](https://github.com/abausg/home_widget/actions/workflows/main.yml/badge.svg?branch=main)

HomeWidget is a Plugin to make it easier to create HomeScreen Widgets on Android and iOS.
HomeWidget does **not** allow writing Widgets with Flutter itself. It still requires writing the Widgets with native code. However it provides a unified Interface for sending data, retrieving data and updating the Widgets
HomeWidget does **not** allow writing Widgets with Flutter itself. It still requires writing the Widgets with native code. However, it provides a unified Interface for sending data, retrieving data and updating the Widgets

| iOS | Android |
| ----- | ----- |
| <img src="https://github.com/ABausG/home_widget/blob/main/.github/assets/demo_ios.png?raw=true" width="500px"> | <img src="https://github.com/ABausG/home_widget/blob/main/.github/assets/demo_android.png?raw=true" width="608px">|

## Platform Setup
As stated there needs to be some platform specific setup. Check below on how to add support for Android and iOS
In order to work correctly there needs to be some platform specific setup. Check below on how to add support for Android and iOS

<details><summary>Android</summary>

Expand Down Expand Up @@ -44,8 +45,8 @@ As stated there needs to be some platform specific setup. Check below on how to
```

### Write your WidgetProvider
For convenience you can extend from [HomeWidgetProvider](android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt) which gives you access to a SharedPreferences Object with the Data in the `onUpdate` method.
If you don't want to use the convenience Method you can access the Data using
For convenience, you can extend from [HomeWidgetProvider](android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt) which gives you access to a SharedPreferences Object with the Data in the `onUpdate` method.
In case you don't want to use the convenience Method you can access the Data using
```kotlin
import es.antonborri.home_widget.HomeWidgetPlugin
...
Expand Down Expand Up @@ -76,10 +77,10 @@ Add this group to you Runner and the Widget Extension inside XCode `Signing & Ca

![Build Targets](https://github.com/ABausG/home_widget/blob/main/.github/assets/target.png?raw=true)

(To swap between your App and the Extension change the Target)
(To swap between your App, and the Extension change the Target)

### Sync CFBundleVersion (optional)
This step is optional, this will sync the widget extension build version with your app version so you don't get warnings of mismatch version from App Store Connect when uploading your app.
This step is optional, this will sync the widget extension build version with your app version, so you don't get warnings of mismatch version from App Store Connect when uploading your app.

![Build Phases](https://github.com/ABausG/home_widget/blob/main/.github/assets/build_phases.png?raw=true)

Expand All @@ -97,7 +98,7 @@ Replace `HomeExampleWidget` with the name of the widget extension folder that yo

### Write your Widget
Check the [Example App](example/ios/HomeWidgetExample/HomeWidgetExample.swift) for an Implementation of a Widget
A more detailed overview on how to write Widgets for iOS 14 can fbe found on the [Apple Developer documentation](https://developer.apple.com/documentation/swiftui/widget)
A more detailed overview on how to write Widgets for iOS 14 can fbe found on the [Apple Developer documentation](https://developer.apple.com/documentation/swiftui/widget).
In order to access the Data send with Flutter can be access with
```swift
let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID")
Expand All @@ -107,7 +108,7 @@ let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID")
## Usage

### Setup
For iOS you need to call `HomeWidget.setAppGroupId('YOUR_GROUP_ID');`
For iOS, you need to call `HomeWidget.setAppGroupId('YOUR_GROUP_ID');`
Without this you won't be able to share data between your App and the Widget and calls to `saveWidgetData` and `getWidgetData` will return an error

### Save Data
Expand All @@ -124,7 +125,7 @@ HomeWidget.updateWidget(
```

The name for Android will be chosen by checking `androidName` if that was not provided it will fallback to `name`.
This Name needs to be equal to the Classname of the [WidgetProvider](#-write-your-widgetprovider)
This Name needs to be equal to the Classname of the [WidgetProvider](#Write-your-Widget)

The name for iOS will be chosen by checking `iOSName` if that was not provided it will fallback to `name`.
This name needs to be equal to the Kind specified in you Widget
Expand All @@ -146,7 +147,7 @@ WorkmanagerPlugin.setPluginRegistrantCallback { registry in
to [AppDelegate.swift](example/ios/Runner/AppDelegate.swift)

### Clicking
To detect if the App was initially started by clicking the Widget you can call `HomeWidget.initiallyLaunchedFromHomeWidget()` if the App was already running in the Background you can receive these Events by listening to `HomeWidget.widgetClicked`. Both methods will provide Uris so you can easily send back data from the Widget to the App to for example navigate to a content page.
To detect if the App has been initially started by clicking the Widget you can call `HomeWidget.initiallyLaunchedFromHomeWidget()` if the App was already running in the Background you can receive these Events by listening to `HomeWidget.widgetClicked`. Both methods will provide Uris, so you can easily send back data from the Widget to the App to for example navigate to a content page.

In order for these methods to work you need to follow these steps:

Expand Down
58 changes: 1 addition & 57 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,61 +1,5 @@
# include: package:pedantic/analysis_options.yaml
# Directly specify rules to work around https://github.com/dart-lang/sdk/issues/42910

analyzer:
exclude: example/**
include: package:flutter_lints/flutter.yaml

linter:
rules:
- always_declare_return_types
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_relative_lib_imports
- avoid_return_types_on_setters
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_types_as_parameter_names
- await_only_futures
- camel_case_extensions
- curly_braces_in_flow_control_structures
- empty_catches
- empty_constructor_bodies
- library_names
- library_prefixes
- no_duplicate_case_values
- null_closures
- omit_local_variable_types
- prefer_adjacent_string_concatenation
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_contains
- prefer_equal_for_default_values
- prefer_final_fields
- prefer_for_elements_to_map_fromIterable
- prefer_generic_function_type_aliases
- prefer_if_null_operators
- prefer_inlined_adds
- prefer_is_empty
- prefer_is_not_empty
- prefer_iterable_whereType
- prefer_single_quotes
- prefer_spread_collections
- recursive_getters
- slash_for_doc_comments
- sort_child_properties_last
- type_init_formals
- unawaited_futures
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_new
- unnecessary_null_in_if_null_operators
- unnecessary_this
- unrelated_type_equality_checks
- unsafe_html
- use_full_hex_values_for_flutter_colors
- use_function_type_syntax_for_parameters
- use_rethrow_when_possible
- valid_regexps
- public_member_api_docs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class HomeWidgetBackgroundService : MethodChannel.MethodCallHandler, JobIntentSe
val callbackHandle = HomeWidgetPlugin.getDispatcherHandle(context)

if (callbackHandle == 0L) {
Log.e(TAG, "No callbackHandle saved. Did you call HomeWidgetPlugin.registerBackgroundCallback?")
Log.e(TAG, "No callbackHandle saved. Did you call HomeWidget.registerBackgroundCallback?")
}

val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val id = call.argument<String>("id")
val data = call.argument<Any>("data")
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit()
when (data) {
is Boolean -> prefs.putBoolean(id, data)
is Float -> prefs.putFloat(id, data)
is String -> prefs.putString(id, data)
is Double -> prefs.putLong(id, data.toLong())
is Long -> prefs.putLong(id, data)
is Int -> prefs.putInt(id, data)
else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException())
if(data != null) {
when (data) {
is Boolean -> prefs.putBoolean(id, data)
is Float -> prefs.putFloat(id, data)
is String -> prefs.putString(id, data)
is Double -> prefs.putLong(id, java.lang.Double.doubleToRawLongBits(data))
is Int -> prefs.putInt(id, data)
else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException())
}
} else {
prefs.remove(id);
}
result.success(prefs.commit())
} else {
Expand All @@ -63,7 +66,12 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)

val value = prefs.all[id] ?: defaultValue
result.success(value)

if(value is Long) {
result.success(java.lang.Double.longBitsToDouble(value))
} else {
result.success(value)
}
} else {
result.error("-2", "InvalidArguments getWidgetData must be called with id", IllegalArgumentException())
}
Expand All @@ -77,6 +85,7 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val ids: IntArray = AppWidgetManager.getInstance(context.applicationContext).getAppWidgetIds(ComponentName(context, javaClass))
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
context.sendBroadcast(intent)
result.success(true)
} catch (classException: ClassNotFoundException) {
result.error("-3", "No Widget found with Name $className. Argument 'name' must be the same as your AppWidgetProvider you wish to update", classException)
}
Expand Down
Empty file added example/analysis_options.yaml
Empty file.
61 changes: 61 additions & 0 deletions example/integration_test/android_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:home_widget/home_widget.dart';
import 'package:integration_test/integration_test.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

final testData = <String, dynamic>{
'stringKey': 'stringValue',
'intKey': 12,
'boolKey': true,
'floatingNumberKey': 12.1,
'nullValueKey': null,
};

final defaultValue = MapEntry('defaultKey', 'defaultValue');

setUpAll(() {
// Clear all Data
for (final key in testData.keys) {
HomeWidget.saveWidgetData(key, null);
}
});

group('Test Data operations', () {
for (final testSet in testData.entries) {
testWidgets('Test ${testSet.value?.runtimeType}', (tester) async {
// Save Data
await HomeWidget.saveWidgetData(testSet.key, testSet.value);

final retrievedData = await HomeWidget.getWidgetData(testSet.key);
expect(retrievedData, testSet.value);
});
}

testWidgets('Delte Value successful', (tester) async {
final initialData = await HomeWidget.getWidgetData(testData.keys.first);
expect(initialData, testData.values.first);

await HomeWidget.saveWidgetData(testData.values.first, null);

final deletedData = await HomeWidget.getWidgetData(testData.keys.first);
expect(deletedData, testData.values.first);
});

testWidgets('Returns default Value', (tester) async {
final returnValue = await HomeWidget.getWidgetData(defaultValue.key,
defaultValue: defaultValue.value);

expect(returnValue, defaultValue.value);
});
});

testWidgets('Update Widget completes', (tester) async {
final returnValue = await HomeWidget.updateWidget(
name: 'HomeWidgetExampleProvider',
).timeout(Duration(seconds: 5));

expect(returnValue, true);
});
}
Loading

0 comments on commit 2dac270

Please sign in to comment.