Skip to content

Commit 2dac270

Browse files
authored
Quality improvements (#25)
* 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)
1 parent 4f69231 commit 2dac270

16 files changed

+452
-93
lines changed

.github/workflows/main.yml

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
quality:
11+
name: Quality Checks
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v2
16+
17+
- uses: subosito/flutter-action@v1.4.0
18+
with:
19+
channel: stable
20+
- name: Get Packages
21+
run: flutter pub get
22+
- name: Analyze
23+
run: flutter analyze
24+
- name: Format
25+
run: flutter format . --set-exit-if-changed
26+
- name: Publishability
27+
run: flutter pub publish --dry-run
28+
- name: Test
29+
run: flutter test --coverage
30+
- uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
31+
32+
android:
33+
name: Android Integration Tests
34+
runs-on: macos-latest
35+
36+
steps:
37+
- uses: actions/checkout@v2
38+
- uses: subosito/flutter-action@v1.4.0
39+
with:
40+
channel: stable
41+
- name: Run Android Integration Tests
42+
uses: reactivecircus/android-emulator-runner@v2
43+
with:
44+
api-level: 29
45+
working-directory: example
46+
script: flutter drive --driver=test_driver/integration_test.dart --target=integration_test/android_test.dart -d emulator-5554
47+
48+
# iOS Test based on https://medium.com/flutter-community/run-flutter-driver-tests-on-github-actions-13c639c7e4ab
49+
# by @kate_sheremet
50+
ios:
51+
name: iOS Integration Tests
52+
strategy:
53+
matrix:
54+
device:
55+
- "iPhone 12 Pro Max (14.4)"
56+
fail-fast: false
57+
runs-on: macOS-latest
58+
steps:
59+
- name: List Available Devices
60+
run: xcrun xctrace list devices 2>&1
61+
- name: Set Simulator Id
62+
run: |
63+
echo "::set-output name=UDID::$(xcrun xctrace list devices 2>&1 |
64+
awk -F '( |\\()' \
65+
-v 'device=${{ matrix.device }}' \
66+
'length($0) == length(device)+39 && substr($0,0,length(device)) == device { print substr($NF,0, length($NF) - 1)}')"
67+
id: udid
68+
- name: "Start Simulator"
69+
run: |
70+
xcrun simctl boot "${{steps.udid.outputs.UDID}}"
71+
- uses: actions/checkout@v2
72+
- uses: subosito/flutter-action@v1
73+
with:
74+
channel: stable
75+
- name: "Run iOS integration tests"
76+
run: flutter drive --driver=test_driver/integration_test.dart --target=integration_test/ios_test.dart -d ${{steps.udid.outputs.UDID}}
77+
working-directory: example

CHANGELOG.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
## 0.1.3
2+
3+
* Add GitHub Actions, Tests and Integration Tests to ensure further quality
4+
* Fix double and null handling on Android
5+
* Fix HomeWidget.updateWidget not completing on Android [#26](https://github.com/ABausG/home_widget/issues/26)
6+
17
## 0.1.2+1
28

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

511
## 0.1.2
612

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

1117
## 0.1.1+2
1218

1319
* Set sdk bound correctly
14-
* Woraround for analysis_options import error
20+
* Workaround for analysis_options import error
1521
* Cleanup Example
1622

1723
## 0.1.1+1

README.md

+12-11
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
[![Pub](https://img.shields.io/pub/v/home_widget.svg)](https://pub.dartlang.org/packages/home_widget)
44
[![likes](https://badges.bar/home_widget/likes)](https://pub.dev/packages/home_widget/score)
55
[![popularity](https://badges.bar/home_widget/popularity)](https://pub.dev/packages/home_widget/score)
6-
[![pub points](https://badges.bar/home_widget/pub%20points)](https://pub.dev/packages/home_widget/score)
6+
[![pub points](https://badges.bar/home_widget/pub%20points)](https://pub.dev/packages/home_widget/score)
7+
![Build](https://github.com/abausg/home_widget/actions/workflows/main.yml/badge.svg?branch=main)
78

89
HomeWidget is a Plugin to make it easier to create HomeScreen Widgets on Android and iOS.
9-
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
10+
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
1011

1112
| iOS | Android |
1213
| ----- | ----- |
1314
| <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">|
1415

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

1819
<details><summary>Android</summary>
1920

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

4647
### Write your WidgetProvider
47-
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.
48-
If you don't want to use the convenience Method you can access the Data using
48+
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.
49+
In case you don't want to use the convenience Method you can access the Data using
4950
```kotlin
5051
import es.antonborri.home_widget.HomeWidgetPlugin
5152
...
@@ -76,10 +77,10 @@ Add this group to you Runner and the Widget Extension inside XCode `Signing & Ca
7677

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

79-
(To swap between your App and the Extension change the Target)
80+
(To swap between your App, and the Extension change the Target)
8081

8182
### Sync CFBundleVersion (optional)
82-
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.
83+
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.
8384

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

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

9899
### Write your Widget
99100
Check the [Example App](example/ios/HomeWidgetExample/HomeWidgetExample.swift) for an Implementation of a Widget
100-
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)
101+
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).
101102
In order to access the Data send with Flutter can be access with
102103
```swift
103104
let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID")
@@ -107,7 +108,7 @@ let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID")
107108
## Usage
108109

109110
### Setup
110-
For iOS you need to call `HomeWidget.setAppGroupId('YOUR_GROUP_ID');`
111+
For iOS, you need to call `HomeWidget.setAppGroupId('YOUR_GROUP_ID');`
111112
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
112113

113114
### Save Data
@@ -124,7 +125,7 @@ HomeWidget.updateWidget(
124125
```
125126

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

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

148149
### Clicking
149-
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.
150+
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.
150151

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

analysis_options.yaml

+1-57
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,5 @@
1-
# include: package:pedantic/analysis_options.yaml
2-
# Directly specify rules to work around https://github.com/dart-lang/sdk/issues/42910
3-
4-
analyzer:
5-
exclude: example/**
1+
include: package:flutter_lints/flutter.yaml
62

73
linter:
84
rules:
9-
- always_declare_return_types
10-
- always_require_non_null_named_parameters
11-
- annotate_overrides
12-
- avoid_init_to_null
13-
- avoid_null_checks_in_equality_operators
14-
- avoid_relative_lib_imports
15-
- avoid_return_types_on_setters
16-
- avoid_shadowing_type_parameters
17-
- avoid_single_cascade_in_expression_statements
18-
- avoid_types_as_parameter_names
19-
- await_only_futures
20-
- camel_case_extensions
21-
- curly_braces_in_flow_control_structures
22-
- empty_catches
23-
- empty_constructor_bodies
24-
- library_names
25-
- library_prefixes
26-
- no_duplicate_case_values
27-
- null_closures
28-
- omit_local_variable_types
29-
- prefer_adjacent_string_concatenation
30-
- prefer_collection_literals
31-
- prefer_conditional_assignment
32-
- prefer_contains
33-
- prefer_equal_for_default_values
34-
- prefer_final_fields
35-
- prefer_for_elements_to_map_fromIterable
36-
- prefer_generic_function_type_aliases
37-
- prefer_if_null_operators
38-
- prefer_inlined_adds
39-
- prefer_is_empty
40-
- prefer_is_not_empty
41-
- prefer_iterable_whereType
42-
- prefer_single_quotes
43-
- prefer_spread_collections
44-
- recursive_getters
45-
- slash_for_doc_comments
46-
- sort_child_properties_last
47-
- type_init_formals
48-
- unawaited_futures
49-
- unnecessary_brace_in_string_interps
50-
- unnecessary_const
51-
- unnecessary_getters_setters
52-
- unnecessary_new
53-
- unnecessary_null_in_if_null_operators
54-
- unnecessary_this
55-
- unrelated_type_equality_checks
56-
- unsafe_html
57-
- use_full_hex_values_for_flutter_colors
58-
- use_function_type_syntax_for_parameters
59-
- use_rethrow_when_possible
60-
- valid_regexps
615
- public_member_api_docs

android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class HomeWidgetBackgroundService : MethodChannel.MethodCallHandler, JobIntentSe
4040
val callbackHandle = HomeWidgetPlugin.getDispatcherHandle(context)
4141

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

4646
val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle)

android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt

+18-9
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,17 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
4141
val id = call.argument<String>("id")
4242
val data = call.argument<Any>("data")
4343
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit()
44-
when (data) {
45-
is Boolean -> prefs.putBoolean(id, data)
46-
is Float -> prefs.putFloat(id, data)
47-
is String -> prefs.putString(id, data)
48-
is Double -> prefs.putLong(id, data.toLong())
49-
is Long -> prefs.putLong(id, data)
50-
is Int -> prefs.putInt(id, data)
51-
else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException())
44+
if(data != null) {
45+
when (data) {
46+
is Boolean -> prefs.putBoolean(id, data)
47+
is Float -> prefs.putFloat(id, data)
48+
is String -> prefs.putString(id, data)
49+
is Double -> prefs.putLong(id, java.lang.Double.doubleToRawLongBits(data))
50+
is Int -> prefs.putInt(id, data)
51+
else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException())
52+
}
53+
} else {
54+
prefs.remove(id);
5255
}
5356
result.success(prefs.commit())
5457
} else {
@@ -63,7 +66,12 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
6366
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
6467

6568
val value = prefs.all[id] ?: defaultValue
66-
result.success(value)
69+
70+
if(value is Long) {
71+
result.success(java.lang.Double.longBitsToDouble(value))
72+
} else {
73+
result.success(value)
74+
}
6775
} else {
6876
result.error("-2", "InvalidArguments getWidgetData must be called with id", IllegalArgumentException())
6977
}
@@ -77,6 +85,7 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
7785
val ids: IntArray = AppWidgetManager.getInstance(context.applicationContext).getAppWidgetIds(ComponentName(context, javaClass))
7886
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
7987
context.sendBroadcast(intent)
88+
result.success(true)
8089
} catch (classException: ClassNotFoundException) {
8190
result.error("-3", "No Widget found with Name $className. Argument 'name' must be the same as your AppWidgetProvider you wish to update", classException)
8291
}

example/analysis_options.yaml

Whitespace-only changes.
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:home_widget/home_widget.dart';
3+
import 'package:integration_test/integration_test.dart';
4+
5+
void main() {
6+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
7+
8+
final testData = <String, dynamic>{
9+
'stringKey': 'stringValue',
10+
'intKey': 12,
11+
'boolKey': true,
12+
'floatingNumberKey': 12.1,
13+
'nullValueKey': null,
14+
};
15+
16+
final defaultValue = MapEntry('defaultKey', 'defaultValue');
17+
18+
setUpAll(() {
19+
// Clear all Data
20+
for (final key in testData.keys) {
21+
HomeWidget.saveWidgetData(key, null);
22+
}
23+
});
24+
25+
group('Test Data operations', () {
26+
for (final testSet in testData.entries) {
27+
testWidgets('Test ${testSet.value?.runtimeType}', (tester) async {
28+
// Save Data
29+
await HomeWidget.saveWidgetData(testSet.key, testSet.value);
30+
31+
final retrievedData = await HomeWidget.getWidgetData(testSet.key);
32+
expect(retrievedData, testSet.value);
33+
});
34+
}
35+
36+
testWidgets('Delte Value successful', (tester) async {
37+
final initialData = await HomeWidget.getWidgetData(testData.keys.first);
38+
expect(initialData, testData.values.first);
39+
40+
await HomeWidget.saveWidgetData(testData.values.first, null);
41+
42+
final deletedData = await HomeWidget.getWidgetData(testData.keys.first);
43+
expect(deletedData, testData.values.first);
44+
});
45+
46+
testWidgets('Returns default Value', (tester) async {
47+
final returnValue = await HomeWidget.getWidgetData(defaultValue.key,
48+
defaultValue: defaultValue.value);
49+
50+
expect(returnValue, defaultValue.value);
51+
});
52+
});
53+
54+
testWidgets('Update Widget completes', (tester) async {
55+
final returnValue = await HomeWidget.updateWidget(
56+
name: 'HomeWidgetExampleProvider',
57+
).timeout(Duration(seconds: 5));
58+
59+
expect(returnValue, true);
60+
});
61+
}

0 commit comments

Comments
 (0)