reis-mobile 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +12 -0
- package/CHANGELOG.md +103 -0
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/THIRD_PARTY_NOTICES.md +105 -0
- package/agents/flutter-architect.md +92 -0
- package/agents/flutter-performance-engineer.md +103 -0
- package/agents/flutter-test-engineer.md +100 -0
- package/agents/lead-mobile.md +107 -0
- package/agents/mobile-code-reviewer.md +66 -0
- package/agents/mobile-staff-engineer.md +85 -0
- package/agents/plugin-native-expert.md +97 -0
- package/bin/reis-mobile.mjs +256 -0
- package/commands/debate.md +135 -0
- package/commands/doctor.md +23 -0
- package/commands/reis-mobile.md +50 -0
- package/commands/review.md +53 -0
- package/core/config/language.mjs +83 -0
- package/core/context/context-engine.mjs +113 -0
- package/core/detection/project-probe.mjs +104 -0
- package/core/detection/stack-detector.mjs +204 -0
- package/core/diagnostics/doctor.mjs +181 -0
- package/core/install/claude-plugin.mjs +76 -0
- package/core/paths.mjs +4 -0
- package/core/registry/frontmatter.mjs +81 -0
- package/core/registry/registry.mjs +140 -0
- package/core/router/intent-detector.mjs +75 -0
- package/core/router/intents.mjs +91 -0
- package/core/router/router.mjs +83 -0
- package/core/security/redact.mjs +24 -0
- package/package.json +35 -0
- package/skills/architecture-feature-first/SKILL.md +116 -0
- package/skills/bloc/SKILL.md +354 -0
- package/skills/code-review/SKILL.md +89 -0
- package/skills/dart-3-updates/SKILL.md +223 -0
- package/skills/dart-add-unit-test/SKILL.md +126 -0
- package/skills/dart-collect-coverage/SKILL.md +145 -0
- package/skills/dart-fix-runtime-errors/SKILL.md +170 -0
- package/skills/dart-generate-test-mocks/SKILL.md +159 -0
- package/skills/dart-migrate-to-checks-package/SKILL.md +532 -0
- package/skills/dart-resolve-package-conflicts/SKILL.md +120 -0
- package/skills/dart-run-static-analysis/SKILL.md +108 -0
- package/skills/dart-use-pattern-matching/SKILL.md +240 -0
- package/skills/dart-use-pattern-matching/examples/json_patterns.dart +85 -0
- package/skills/detect-mobile-stack/SKILL.md +33 -0
- package/skills/effective-dart/SKILL.md +187 -0
- package/skills/firebase-ai/SKILL.md +93 -0
- package/skills/firebase-analytics/SKILL.md +129 -0
- package/skills/firebase-app-check/SKILL.md +124 -0
- package/skills/firebase-auth/SKILL.md +224 -0
- package/skills/firebase-cloud-firestore/SKILL.md +165 -0
- package/skills/firebase-cloud-functions/SKILL.md +148 -0
- package/skills/firebase-crashlytics/SKILL.md +182 -0
- package/skills/firebase-data-connect/SKILL.md +80 -0
- package/skills/firebase-database/SKILL.md +224 -0
- package/skills/firebase-in-app-messaging/SKILL.md +111 -0
- package/skills/firebase-messaging/SKILL.md +201 -0
- package/skills/firebase-remote-config/SKILL.md +131 -0
- package/skills/firebase-storage/SKILL.md +158 -0
- package/skills/flutter-add-integration-test/SKILL.md +167 -0
- package/skills/flutter-add-widget-preview/SKILL.md +149 -0
- package/skills/flutter-add-widget-test/SKILL.md +158 -0
- package/skills/flutter-adding-home-screen-widgets/SKILL.md +205 -0
- package/skills/flutter-animating-apps/SKILL.md +185 -0
- package/skills/flutter-app-architecture/SKILL.md +80 -0
- package/skills/flutter-apply-architecture-best-practices/SKILL.md +166 -0
- package/skills/flutter-build-responsive-layout/SKILL.md +143 -0
- package/skills/flutter-building-forms/SKILL.md +125 -0
- package/skills/flutter-building-layouts/SKILL.md +131 -0
- package/skills/flutter-building-plugins/SKILL.md +209 -0
- package/skills/flutter-caching-data/SKILL.md +171 -0
- package/skills/flutter-change-notifier/SKILL.md +138 -0
- package/skills/flutter-embedding-native-views/SKILL.md +200 -0
- package/skills/flutter-errors/SKILL.md +126 -0
- package/skills/flutter-fix-layout-issues/SKILL.md +134 -0
- package/skills/flutter-handling-concurrency/SKILL.md +182 -0
- package/skills/flutter-implement-json-serialization/SKILL.md +157 -0
- package/skills/flutter-improving-accessibility/SKILL.md +113 -0
- package/skills/flutter-interoperating-with-native-apis/SKILL.md +130 -0
- package/skills/flutter-login-usecase/SKILL.md +38 -0
- package/skills/flutter-managing-state/SKILL.md +206 -0
- package/skills/flutter-project-audit/SKILL.md +66 -0
- package/skills/flutter-reducing-app-size/SKILL.md +103 -0
- package/skills/flutter-secure-token-store/SKILL.md +35 -0
- package/skills/flutter-setting-up-on-macos/SKILL.md +78 -0
- package/skills/flutter-setup-declarative-routing/SKILL.md +259 -0
- package/skills/flutter-setup-localization/SKILL.md +214 -0
- package/skills/flutter-theming-apps/SKILL.md +163 -0
- package/skills/flutter-use-http-package/SKILL.md +178 -0
- package/skills/flutter-widget-review/SKILL.md +54 -0
- package/skills/flutter-working-with-databases/SKILL.md +203 -0
- package/skills/flutterfire-configure/SKILL.md +143 -0
- package/skills/mobile-security-audit/SKILL.md +101 -0
- package/skills/mockito/SKILL.md +149 -0
- package/skills/mocktail/SKILL.md +122 -0
- package/skills/patrol-e2e-testing/SKILL.md +224 -0
- package/skills/provider/SKILL.md +139 -0
- package/skills/riverpod/SKILL.md +246 -0
- package/skills/testing/SKILL.md +50 -0
- package/stacks/android/stack.json +8 -0
- package/stacks/flutter/stack.json +8 -0
- package/stacks/ios/stack.json +8 -0
- package/stacks/kotlin-multiplatform/stack.json +8 -0
- package/stacks/react-native/stack.json +8 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter-add-widget-preview
|
|
3
|
+
description: Adds interactive widget previews to the project using the previews.dart system. Use when creating new UI components or updating existing screens to ensure consistent design and interactive testing.
|
|
4
|
+
metadata:
|
|
5
|
+
model: models/gemini-3.1-pro-preview
|
|
6
|
+
last_modified: Tue, 21 Apr 2026 20:05:23 GMT
|
|
7
|
+
routing: manual
|
|
8
|
+
stacks: [flutter]
|
|
9
|
+
source: https://github.com/flutter/skills
|
|
10
|
+
license: BSD-3-Clause
|
|
11
|
+
---
|
|
12
|
+
# Previewing Flutter Widgets
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
- [Preview Guidelines](#preview-guidelines)
|
|
16
|
+
- [Handling Limitations](#handling-limitations)
|
|
17
|
+
- [Workflows](#workflows)
|
|
18
|
+
- [Examples](#examples)
|
|
19
|
+
|
|
20
|
+
## Preview Guidelines
|
|
21
|
+
|
|
22
|
+
Use the Flutter Widget Previewer to render widgets in real-time, isolated from the full application context.
|
|
23
|
+
|
|
24
|
+
- **Target Elements:** Apply the `@Preview` annotation to top-level functions, static methods within a class, or public widget constructors/factories that have no required arguments and return a `Widget` or `WidgetBuilder`.
|
|
25
|
+
- **Imports:** Always import `package:flutter/widget_previews.dart` to access the preview annotations.
|
|
26
|
+
- **Custom Annotations:** Extend the `Preview` class to create custom annotations that inject common properties (e.g., themes, wrappers) across multiple widgets.
|
|
27
|
+
- **Multiple Configurations:** Apply multiple `@Preview` annotations to a single target to generate multiple preview instances. Alternatively, extend `MultiPreview` to encapsulate common multi-preview configurations.
|
|
28
|
+
- **Runtime Transformations:** Override the `transform()` method in custom `Preview` or `MultiPreview` classes to modify preview configurations dynamically at runtime (e.g., generating names based on dynamic values, which is impossible in a `const` context).
|
|
29
|
+
|
|
30
|
+
## Handling Limitations
|
|
31
|
+
|
|
32
|
+
Adhere to the following constraints when authoring previewable widgets, as the Widget Previewer runs in a web environment:
|
|
33
|
+
|
|
34
|
+
- **No Native APIs:** Do not use native plugins or APIs from `dart:io` or `dart:ffi`. Widgets with transitive dependencies on `dart:io` or `dart:ffi` will throw exceptions upon invocation. Use conditional imports to mock or bypass these in preview mode.
|
|
35
|
+
- **Asset Paths:** Use package-based paths for assets loaded via `dart:ui` `fromAsset` APIs (e.g., `packages/my_package_name/assets/my_image.png` instead of `assets/my_image.png`).
|
|
36
|
+
- **Public Callbacks:** Ensure all callback arguments provided to preview annotations are public and constant to satisfy code generation requirements.
|
|
37
|
+
- **Constraints:** Apply explicit constraints using the `size` parameter in the `@Preview` annotation if your widget is unconstrained, as the previewer defaults to constraining them to approximately half the viewport.
|
|
38
|
+
|
|
39
|
+
## Workflows
|
|
40
|
+
|
|
41
|
+
### Creating a Widget Preview
|
|
42
|
+
Copy and track this checklist when implementing a new widget preview:
|
|
43
|
+
|
|
44
|
+
- [ ] Import `package:flutter/widget_previews.dart`.
|
|
45
|
+
- [ ] Identify a valid target (top-level function, static method, or parameter-less public constructor).
|
|
46
|
+
- [ ] Apply the `@Preview` annotation to the target.
|
|
47
|
+
- [ ] Configure preview parameters (`name`, `group`, `size`, `theme`, `brightness`, etc.) as needed.
|
|
48
|
+
- [ ] If applying the same configuration to multiple widgets, extract the configuration into a custom class extending `Preview`.
|
|
49
|
+
|
|
50
|
+
### Interacting with Previews
|
|
51
|
+
Follow the appropriate conditional workflow to launch and interact with the Widget Previewer:
|
|
52
|
+
|
|
53
|
+
**If using a supported IDE (Android Studio, IntelliJ, VS Code with Flutter 3.38+):**
|
|
54
|
+
1. Launch the IDE. The Widget Previewer starts automatically.
|
|
55
|
+
2. Open the "Flutter Widget Preview" tab in the sidebar.
|
|
56
|
+
3. Toggle "Filter previews by selected file" at the bottom left if you want to view previews outside the currently active file.
|
|
57
|
+
|
|
58
|
+
**If using the Command Line:**
|
|
59
|
+
1. Navigate to the Flutter project's root directory.
|
|
60
|
+
2. Run `flutter widget-preview start`.
|
|
61
|
+
3. View the automatically opened Chrome environment.
|
|
62
|
+
|
|
63
|
+
**Feedback Loop: Preview Iteration**
|
|
64
|
+
1. Modify the widget code or preview configuration.
|
|
65
|
+
2. Observe the automatic update in the Widget Previewer.
|
|
66
|
+
3. If global state (e.g., static initializers) was modified: Click the global hot restart button at the bottom right.
|
|
67
|
+
4. If only the local widget state needs resetting: Click the individual hot restart button on the specific preview card.
|
|
68
|
+
5. Review errors in the IDE/CLI console -> fix -> repeat.
|
|
69
|
+
|
|
70
|
+
## Examples
|
|
71
|
+
|
|
72
|
+
### Basic Preview
|
|
73
|
+
```dart
|
|
74
|
+
import 'package:flutter/widget_previews.dart';
|
|
75
|
+
import 'package:flutter/material.dart';
|
|
76
|
+
|
|
77
|
+
@Preview(name: 'My Sample Text', group: 'Typography')
|
|
78
|
+
Widget mySampleText() {
|
|
79
|
+
return const Text('Hello, World!');
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Custom Preview with Runtime Transformation
|
|
84
|
+
```dart
|
|
85
|
+
import 'package:flutter/widget_previews.dart';
|
|
86
|
+
import 'package:flutter/material.dart';
|
|
87
|
+
|
|
88
|
+
final class TransformativePreview extends Preview {
|
|
89
|
+
const TransformativePreview({
|
|
90
|
+
super.name,
|
|
91
|
+
super.group,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
PreviewThemeData _themeBuilder() {
|
|
95
|
+
return PreviewThemeData(
|
|
96
|
+
materialLight: ThemeData.light(),
|
|
97
|
+
materialDark: ThemeData.dark(),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
Preview transform() {
|
|
103
|
+
final originalPreview = super.transform();
|
|
104
|
+
final builder = originalPreview.toBuilder();
|
|
105
|
+
|
|
106
|
+
builder
|
|
107
|
+
..name = 'Transformed - ${originalPreview.name}'
|
|
108
|
+
..theme = _themeBuilder;
|
|
109
|
+
|
|
110
|
+
return builder.toPreview();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@TransformativePreview(name: 'Custom Themed Button')
|
|
115
|
+
Widget myButton() => const ElevatedButton(onPressed: null, child: Text('Click'));
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### MultiPreview Implementation
|
|
119
|
+
```dart
|
|
120
|
+
import 'package:flutter/widget_previews.dart';
|
|
121
|
+
import 'package:flutter/material.dart';
|
|
122
|
+
|
|
123
|
+
/// Creates light and dark mode previews automatically.
|
|
124
|
+
final class MultiBrightnessPreview extends MultiPreview {
|
|
125
|
+
const MultiBrightnessPreview({required this.name});
|
|
126
|
+
|
|
127
|
+
final String name;
|
|
128
|
+
|
|
129
|
+
@override
|
|
130
|
+
List<Preview> get previews => const [
|
|
131
|
+
Preview(brightness: Brightness.light),
|
|
132
|
+
Preview(brightness: Brightness.dark),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
@override
|
|
136
|
+
List<Preview> transform() {
|
|
137
|
+
final previews = super.transform();
|
|
138
|
+
return previews.map((preview) {
|
|
139
|
+
final builder = preview.toBuilder()
|
|
140
|
+
..group = 'Brightness'
|
|
141
|
+
..name = '$name - ${preview.brightness!.name}';
|
|
142
|
+
return builder.toPreview();
|
|
143
|
+
}).toList();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@MultiBrightnessPreview(name: 'Primary Card')
|
|
148
|
+
Widget cardPreview() => const Card(child: Padding(padding: EdgeInsets.all(8.0), child: Text('Content')));
|
|
149
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter-add-widget-test
|
|
3
|
+
description: Implement a component-level test using `WidgetTester` to verify UI rendering and user interactions (tapping, scrolling, entering text). Use when validating that a specific widget displays correct data and responds to events as expected.
|
|
4
|
+
metadata:
|
|
5
|
+
model: models/gemini-3.1-pro-preview
|
|
6
|
+
last_modified: Tue, 21 Apr 2026 21:15:41 GMT
|
|
7
|
+
intents: [test]
|
|
8
|
+
stacks: [flutter]
|
|
9
|
+
source: https://github.com/flutter/skills
|
|
10
|
+
license: BSD-3-Clause
|
|
11
|
+
---
|
|
12
|
+
# Writing Flutter Widget Tests
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
- [Setup & Configuration](#setup--configuration)
|
|
16
|
+
- [Core Components](#core-components)
|
|
17
|
+
- [Workflow: Implementing a Widget Test](#workflow-implementing-a-widget-test)
|
|
18
|
+
- [Interaction & State Management](#interaction--state-management)
|
|
19
|
+
- [Examples](#examples)
|
|
20
|
+
|
|
21
|
+
## Setup & Configuration
|
|
22
|
+
|
|
23
|
+
Ensure the testing environment is properly configured before authoring widget tests.
|
|
24
|
+
|
|
25
|
+
1. Add the `flutter_test` dependency to the `dev_dependencies` section of `pubspec.yaml`.
|
|
26
|
+
2. Place all test files in the `test/` directory at the root of the project.
|
|
27
|
+
3. Suffix all test file names with `_test.dart` (e.g., `widget_test.dart`).
|
|
28
|
+
|
|
29
|
+
## Core Components
|
|
30
|
+
|
|
31
|
+
Utilize the following `flutter_test` components to interact with and validate the widget tree:
|
|
32
|
+
|
|
33
|
+
* **`WidgetTester`**: The primary interface for building and interacting with widgets in the test environment. Provided automatically by the `testWidgets()` function.
|
|
34
|
+
* **`Finder`**: Locates widgets in the test environment (e.g., `find.text('Submit')`, `find.byType(TextField)`, `find.byKey(Key('submit_btn'))`).
|
|
35
|
+
* **`Matcher`**: Verifies the presence or state of widgets located by a `Finder` (e.g., `findsOneWidget`, `findsNothing`, `findsNWidgets(2)`, `matchesGoldenFile`).
|
|
36
|
+
|
|
37
|
+
## Workflow: Implementing a Widget Test
|
|
38
|
+
|
|
39
|
+
Copy the following checklist to track progress when implementing a new widget test.
|
|
40
|
+
|
|
41
|
+
### Task Progress
|
|
42
|
+
- [ ] **Step 1: Define the test.** Use `testWidgets('description', (WidgetTester tester) async { ... })`.
|
|
43
|
+
- [ ] **Step 2: Build the widget.** Call `await tester.pumpWidget(MyWidget())` to render the UI. Wrap the widget in a `MaterialApp` or `Directionality` widget if it requires inherited directional or theme data.
|
|
44
|
+
- [ ] **Step 3: Locate elements.** Instantiate `Finder` objects for the target widgets.
|
|
45
|
+
- [ ] **Step 4: Verify initial state.** Use `expect(finder, matcher)` to validate the initial render.
|
|
46
|
+
- [ ] **Step 5: Simulate interactions.** Execute gestures or inputs (e.g., `await tester.tap(buttonFinder)`).
|
|
47
|
+
- [ ] **Step 6: Rebuild the tree.** Call `await tester.pump()` or `await tester.pumpAndSettle()` to process state changes.
|
|
48
|
+
- [ ] **Step 7: Verify updated state.** Use `expect()` to validate the UI after the interaction.
|
|
49
|
+
- [ ] **Step 8: Run and validate.** Execute `flutter test test/your_test_file_test.dart`.
|
|
50
|
+
- [ ] **Step 9: Feedback Loop.** Review test output -> identify failing matchers -> adjust widget logic or test assertions -> re-run until passing.
|
|
51
|
+
|
|
52
|
+
## Interaction & State Management
|
|
53
|
+
|
|
54
|
+
Apply the following conditional logic based on the type of interaction or state change being tested:
|
|
55
|
+
|
|
56
|
+
* **If testing static rendering:** Call `await tester.pumpWidget()` once, then immediately run `expect()` assertions.
|
|
57
|
+
* **If testing standard state changes (e.g., button taps):**
|
|
58
|
+
1. Call `await tester.tap(finder)`.
|
|
59
|
+
2. Call `await tester.pump()` to trigger a single frame rebuild.
|
|
60
|
+
* **If testing animations, transitions, or asynchronous UI updates:**
|
|
61
|
+
1. Trigger the action (e.g., `await tester.drag(finder, Offset(500, 0))`).
|
|
62
|
+
2. Call `await tester.pumpAndSettle()` to repeatedly pump frames until no more frames are scheduled (animation completes).
|
|
63
|
+
* **If testing text input:** Call `await tester.enterText(textFieldFinder, 'Input string')`.
|
|
64
|
+
* **If testing items in a dynamic or long list:** Call `await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder)` to ensure the target widget is rendered before interacting with it.
|
|
65
|
+
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
### High-Fidelity Widget Test Implementation
|
|
69
|
+
|
|
70
|
+
**Target Widget (`lib/todo_list.dart`):**
|
|
71
|
+
```dart
|
|
72
|
+
import 'package:flutter/material.dart';
|
|
73
|
+
|
|
74
|
+
class TodoList extends StatefulWidget {
|
|
75
|
+
const TodoList({super.key});
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
State<TodoList> createState() => _TodoListState();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class _TodoListState extends State<TodoList> {
|
|
82
|
+
final todos = <String>[];
|
|
83
|
+
final controller = TextEditingController();
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
Widget build(BuildContext context) {
|
|
87
|
+
return MaterialApp(
|
|
88
|
+
home: Scaffold(
|
|
89
|
+
body: Column(
|
|
90
|
+
children: [
|
|
91
|
+
TextField(controller: controller),
|
|
92
|
+
Expanded(
|
|
93
|
+
child: ListView.builder(
|
|
94
|
+
itemCount: todos.length,
|
|
95
|
+
itemBuilder: (context, index) {
|
|
96
|
+
final todo = todos[index];
|
|
97
|
+
return Dismissible(
|
|
98
|
+
key: Key('$todo$index'),
|
|
99
|
+
onDismissed: (_) => setState(() => todos.removeAt(index)),
|
|
100
|
+
child: ListTile(title: Text(todo)),
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
],
|
|
106
|
+
),
|
|
107
|
+
floatingActionButton: FloatingActionButton(
|
|
108
|
+
onPressed: () {
|
|
109
|
+
setState(() {
|
|
110
|
+
todos.add(controller.text);
|
|
111
|
+
controller.clear();
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
child: const Icon(Icons.add),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Test Implementation (`test/todo_list_test.dart`):**
|
|
123
|
+
```dart
|
|
124
|
+
import 'package:flutter/material.dart';
|
|
125
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
126
|
+
import 'package:my_app/todo_list.dart';
|
|
127
|
+
|
|
128
|
+
void main() {
|
|
129
|
+
testWidgets('Add and remove a todo item', (WidgetTester tester) async {
|
|
130
|
+
// 1. Build the widget
|
|
131
|
+
await tester.pumpWidget(const TodoList());
|
|
132
|
+
|
|
133
|
+
// 2. Verify initial state
|
|
134
|
+
expect(find.byType(ListTile), findsNothing);
|
|
135
|
+
|
|
136
|
+
// 3. Enter text into the TextField
|
|
137
|
+
await tester.enterText(find.byType(TextField), 'Buy groceries');
|
|
138
|
+
|
|
139
|
+
// 4. Tap the add button
|
|
140
|
+
await tester.tap(find.byType(FloatingActionButton));
|
|
141
|
+
|
|
142
|
+
// 5. Rebuild the widget to reflect the new state
|
|
143
|
+
await tester.pump();
|
|
144
|
+
|
|
145
|
+
// 6. Verify the item was added
|
|
146
|
+
expect(find.text('Buy groceries'), findsOneWidget);
|
|
147
|
+
|
|
148
|
+
// 7. Swipe the item to dismiss it
|
|
149
|
+
await tester.drag(find.byType(Dismissible), const Offset(500, 0));
|
|
150
|
+
|
|
151
|
+
// 8. Build the widget until the dismiss animation ends
|
|
152
|
+
await tester.pumpAndSettle();
|
|
153
|
+
|
|
154
|
+
// 9. Verify the item was removed
|
|
155
|
+
expect(find.text('Buy groceries'), findsNothing);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
```
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flutter-adding-home-screen-widgets
|
|
3
|
+
description: Adds home screen widgets to a Flutter app for Android and iOS. Use when providing glanceable app information or quick actions on the device home screen.
|
|
4
|
+
metadata:
|
|
5
|
+
model: models/gemini-3.1-pro-preview
|
|
6
|
+
last_modified: Thu, 12 Mar 2026 22:23:50 GMT
|
|
7
|
+
routing: manual
|
|
8
|
+
stacks: [flutter]
|
|
9
|
+
source: https://github.com/flutter/skills
|
|
10
|
+
license: BSD-3-Clause
|
|
11
|
+
---
|
|
12
|
+
# Implementing Flutter Home Screen Widgets
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
- [Architecture & Data Flow](#architecture--data-flow)
|
|
16
|
+
- [Flutter Integration Workflow](#flutter-integration-workflow)
|
|
17
|
+
- [iOS Implementation Workflow](#ios-implementation-workflow)
|
|
18
|
+
- [Android Implementation Workflow](#android-implementation-workflow)
|
|
19
|
+
- [Advanced Techniques](#advanced-techniques)
|
|
20
|
+
- [Examples](#examples)
|
|
21
|
+
|
|
22
|
+
## Architecture & Data Flow
|
|
23
|
+
Home Screen Widgets require native UI implementation (SwiftUI for iOS, XML/Kotlin for Android). The Flutter app communicates with these native widgets via shared local storage (`UserDefaults` on iOS, `SharedPreferences` on Android) using the `home_widget` package.
|
|
24
|
+
|
|
25
|
+
- **Data Write:** Flutter app writes key-value pairs or renders images to a shared container.
|
|
26
|
+
- **Trigger:** Flutter app signals the native OS to update the widget.
|
|
27
|
+
- **Data Read:** Native widget wakes up, reads the key-value pairs or images from the shared container, and updates its UI.
|
|
28
|
+
|
|
29
|
+
## Flutter Integration Workflow
|
|
30
|
+
|
|
31
|
+
Use this checklist to implement the Dart side of the Home Screen Widget integration.
|
|
32
|
+
|
|
33
|
+
- [ ] **Step 1: Initialize the App Group.** Call `HomeWidget.setAppGroupId('<YOUR_APP_GROUP>')` in `initState()` or app startup.
|
|
34
|
+
- [ ] **Step 2: Save Data.** Use `HomeWidget.saveWidgetData<T>('key', value)` to write data to shared storage.
|
|
35
|
+
- [ ] **Step 3: Trigger Update.** Call `HomeWidget.updateWidget(iOSName: 'YourIOSWidget', androidName: 'YourAndroidWidget')` to notify the OS.
|
|
36
|
+
- [ ] **Step 4: Validate.** Run Flutter build -> review console for missing plugin registrations -> fix.
|
|
37
|
+
|
|
38
|
+
## iOS Implementation Workflow
|
|
39
|
+
|
|
40
|
+
If targeting iOS, implement the widget using Xcode and SwiftUI.
|
|
41
|
+
|
|
42
|
+
- [ ] **Step 1: Create Target.** Open `ios/Runner.xcworkspace` in Xcode. Add a new **Widget Extension** target. Disable "Include Live Activity" and "Include Configuration Intent" unless explicitly required.
|
|
43
|
+
- [ ] **Step 2: Configure App Groups.** Add the **App Groups** capability to *both* the Runner target and the Widget Extension target. Ensure the App Group ID matches the one used in Dart.
|
|
44
|
+
- [ ] **Step 3: Define TimelineEntry.** Create a struct conforming to `TimelineEntry` to hold the data passed from shared storage.
|
|
45
|
+
- [ ] **Step 4: Implement TimelineProvider.**
|
|
46
|
+
- In `getSnapshot` and `getTimeline`, instantiate `UserDefaults(suiteName: "<YOUR_APP_GROUP>")`.
|
|
47
|
+
- Extract values using `userDefaults?.string(forKey: "your_key")`.
|
|
48
|
+
- Return the populated `TimelineEntry`.
|
|
49
|
+
- [ ] **Step 5: Build UI.** Implement the SwiftUI `View` to display the data from the `TimelineEntry`.
|
|
50
|
+
- [ ] **Step 6: Validate.** Run Xcode build for the Widget Extension -> review provisioning/App Group errors -> fix.
|
|
51
|
+
|
|
52
|
+
## Android Implementation Workflow
|
|
53
|
+
|
|
54
|
+
If targeting Android, implement the widget using Android Studio and XML/Kotlin.
|
|
55
|
+
|
|
56
|
+
- [ ] **Step 1: Create App Widget.** Open the `android` folder in Android Studio. Right-click the app directory -> **New -> Widget -> App Widget**.
|
|
57
|
+
- [ ] **Step 2: Define Layout.** Edit `res/layout/<widget_name>.xml` to define the UI using standard Android XML layouts (e.g., `RelativeLayout`, `TextView`, `ImageView`).
|
|
58
|
+
- [ ] **Step 3: Implement AppWidgetProvider.**
|
|
59
|
+
- Open the generated Kotlin class extending `AppWidgetProvider`.
|
|
60
|
+
- In the `onUpdate` method, retrieve shared data using `HomeWidgetPlugin.getData(context)`.
|
|
61
|
+
- Extract values using `widgetData.getString("your_key", null)`.
|
|
62
|
+
- Update the UI using `RemoteViews` and `setTextViewText` or `setImageViewBitmap`.
|
|
63
|
+
- Call `appWidgetManager.updateAppWidget(appWidgetId, views)`.
|
|
64
|
+
- [ ] **Step 4: Validate.** Run Android build -> review Manifest registration errors -> fix.
|
|
65
|
+
|
|
66
|
+
## Advanced Techniques
|
|
67
|
+
|
|
68
|
+
### Rendering Flutter Widgets as Images
|
|
69
|
+
If the UI is too complex to recreate natively (e.g., custom charts), render the Flutter widget to an image and display the image in the native widget.
|
|
70
|
+
|
|
71
|
+
1. Wrap the target Flutter widget with a `GlobalKey`.
|
|
72
|
+
2. Call `HomeWidget.renderFlutterWidget()`, passing the widget, a filename, and the key.
|
|
73
|
+
3. **iOS:** Read the file path from `UserDefaults` and render using `UIImage(contentsOfFile:)` inside a SwiftUI `Image`.
|
|
74
|
+
4. **Android:** Read the file path from `SharedPreferences`, decode using `BitmapFactory.decodeFile()`, and render using `setImageViewBitmap()`.
|
|
75
|
+
|
|
76
|
+
### Using Custom Flutter Fonts (iOS Only)
|
|
77
|
+
If utilizing custom fonts defined in Flutter on iOS Home Screen Widgets:
|
|
78
|
+
|
|
79
|
+
1. Extract the Flutter asset bundle path in Swift.
|
|
80
|
+
2. Register the font using `CTFontManagerRegisterFontsForURL`.
|
|
81
|
+
3. Apply the font in SwiftUI using `Font.custom()`.
|
|
82
|
+
|
|
83
|
+
## Examples
|
|
84
|
+
|
|
85
|
+
### Example: Flutter Data Update
|
|
86
|
+
```dart
|
|
87
|
+
import 'package:home_widget/home_widget.dart';
|
|
88
|
+
|
|
89
|
+
const String appGroupId = 'group.com.example.app';
|
|
90
|
+
const String iOSWidgetName = 'NewsWidgets';
|
|
91
|
+
const String androidWidgetName = 'NewsWidget';
|
|
92
|
+
|
|
93
|
+
Future<void> updateWidgetData(String title, String description) async {
|
|
94
|
+
await HomeWidget.setAppGroupId(appGroupId);
|
|
95
|
+
await HomeWidget.saveWidgetData<String>('headline_title', title);
|
|
96
|
+
await HomeWidget.saveWidgetData<String>('headline_description', description);
|
|
97
|
+
await HomeWidget.updateWidget(
|
|
98
|
+
iOSName: iOSWidgetName,
|
|
99
|
+
androidName: androidWidgetName,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Example: iOS SwiftUI Provider & View
|
|
105
|
+
```swift
|
|
106
|
+
import WidgetKit
|
|
107
|
+
import SwiftUI
|
|
108
|
+
|
|
109
|
+
struct NewsArticleEntry: TimelineEntry {
|
|
110
|
+
let date: Date
|
|
111
|
+
let title: String
|
|
112
|
+
let description: String
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
struct Provider: TimelineProvider {
|
|
116
|
+
func placeholder(in context: Context) -> NewsArticleEntry {
|
|
117
|
+
NewsArticleEntry(date: Date(), title: "Loading...", description: "Loading...")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func getSnapshot(in context: Context, completion: @escaping (NewsArticleEntry) -> ()) {
|
|
121
|
+
let userDefaults = UserDefaults(suiteName: "group.com.example.app")
|
|
122
|
+
let title = userDefaults?.string(forKey: "headline_title") ?? "No Title"
|
|
123
|
+
let description = userDefaults?.string(forKey: "headline_description") ?? "No Description"
|
|
124
|
+
|
|
125
|
+
let entry = NewsArticleEntry(date: Date(), title: title, description: description)
|
|
126
|
+
completion(entry)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
|
130
|
+
getSnapshot(in: context) { (entry) in
|
|
131
|
+
let timeline = Timeline(entries: [entry], policy: .atEnd)
|
|
132
|
+
completion(timeline)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
struct NewsWidgetsEntryView : View {
|
|
138
|
+
var entry: Provider.Entry
|
|
139
|
+
|
|
140
|
+
var body: some View {
|
|
141
|
+
VStack(alignment: .leading) {
|
|
142
|
+
Text(entry.title).font(.headline)
|
|
143
|
+
Text(entry.description).font(.subheadline)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Example: Android Kotlin Provider
|
|
150
|
+
```kotlin
|
|
151
|
+
package com.example.app.widgets
|
|
152
|
+
|
|
153
|
+
import android.appwidget.AppWidgetManager
|
|
154
|
+
import android.appwidget.AppWidgetProvider
|
|
155
|
+
import android.content.Context
|
|
156
|
+
import android.widget.RemoteViews
|
|
157
|
+
import es.antonborri.home_widget.HomeWidgetPlugin
|
|
158
|
+
import com.example.app.R
|
|
159
|
+
|
|
160
|
+
class NewsWidget : AppWidgetProvider() {
|
|
161
|
+
override fun onUpdate(
|
|
162
|
+
context: Context,
|
|
163
|
+
appWidgetManager: AppWidgetManager,
|
|
164
|
+
appWidgetIds: IntArray,
|
|
165
|
+
) {
|
|
166
|
+
for (appWidgetId in appWidgetIds) {
|
|
167
|
+
val widgetData = HomeWidgetPlugin.getData(context)
|
|
168
|
+
val views = RemoteViews(context.packageName, R.layout.news_widget).apply {
|
|
169
|
+
val title = widgetData.getString("headline_title", "No Title")
|
|
170
|
+
setTextViewText(R.id.headline_title, title)
|
|
171
|
+
|
|
172
|
+
val description = widgetData.getString("headline_description", "No Description")
|
|
173
|
+
setTextViewText(R.id.headline_description, description)
|
|
174
|
+
}
|
|
175
|
+
appWidgetManager.updateAppWidget(appWidgetId, views)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
<details>
|
|
182
|
+
<summary>Example: iOS Custom Font Registration Helper</summary>
|
|
183
|
+
|
|
184
|
+
```swift
|
|
185
|
+
// Add this to your SwiftUI View struct
|
|
186
|
+
var bundle: URL {
|
|
187
|
+
let bundle = Bundle.main
|
|
188
|
+
if bundle.bundleURL.pathExtension == "appex" {
|
|
189
|
+
var url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent()
|
|
190
|
+
url.append(component: "Frameworks/App.framework/flutter_assets")
|
|
191
|
+
return url
|
|
192
|
+
}
|
|
193
|
+
return bundle.bundleURL
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
init(entry: Provider.Entry) {
|
|
197
|
+
self.entry = entry
|
|
198
|
+
CTFontManagerRegisterFontsForURL(
|
|
199
|
+
bundle.appending(path: "/fonts/YourCustomFont.ttf") as CFURL,
|
|
200
|
+
CTFontManagerScope.process,
|
|
201
|
+
nil
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
</details>
|