reis-mobile 0.5.0 → 0.7.0
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 +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +33 -0
- package/README.md +52 -19
- package/agents/android-architect.md +14 -70
- package/agents/android-performance-engineer.md +14 -73
- package/agents/android-test-engineer.md +23 -76
- package/agents/flutter-architect.md +17 -73
- package/agents/flutter-performance-engineer.md +14 -83
- package/agents/flutter-test-engineer.md +18 -82
- package/agents/ios-architect.md +14 -74
- package/agents/ios-performance-engineer.md +14 -73
- package/agents/ios-test-engineer.md +18 -75
- package/agents/lead-mobile.md +40 -67
- package/agents/mobile-accessibility-auditor.md +31 -0
- package/agents/mobile-code-reviewer.md +19 -48
- package/agents/mobile-release-engineer.md +33 -0
- package/agents/mobile-staff-engineer.md +19 -66
- package/agents/plugin-native-expert.md +14 -78
- package/agents/rn-architect.md +13 -76
- package/agents/rn-performance-engineer.md +14 -74
- package/agents/rn-test-engineer.md +16 -77
- package/bin/reis-mobile.mjs +30 -5
- package/commands/reis-mobile.md +7 -2
- package/commands/test.md +71 -0
- package/core/install/claude-plugin.mjs +9 -2
- package/docs/agent-context.md +57 -0
- package/package.json +2 -1
- package/skills/mobile-accessibility/SKILL.md +69 -0
- package/skills/mobile-accessibility/references/android.md +54 -0
- package/skills/mobile-accessibility/references/flutter.md +59 -0
- package/skills/mobile-accessibility/references/ios.md +50 -0
- package/skills/mobile-accessibility/references/react-native.md +38 -0
- package/skills/mobile-debug/SKILL.md +2 -1
- package/skills/mobile-debug/references/android/anr.md +59 -0
- package/skills/mobile-debug/references/android/memory-leaks.md +43 -0
- package/skills/mobile-debug/references/android.md +6 -0
- package/skills/mobile-debug/references/deep-links.md +60 -0
- package/skills/mobile-release/SKILL.md +70 -0
- package/skills/mobile-release/references/android.md +47 -0
- package/skills/mobile-release/references/ci-cd.md +94 -0
- package/skills/mobile-release/references/flutter.md +37 -0
- package/skills/mobile-release/references/ios.md +48 -0
- package/skills/mobile-release/references/react-native.md +36 -0
- package/skills/mobile-release/references/store-copy.md +52 -0
- package/skills/mobile-test/SKILL.md +21 -6
- package/skills/mobile-test/references/android.md +67 -55
- package/skills/mobile-test/references/ios.md +76 -53
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-accessibility
|
|
3
|
+
description: Audits and fixes accessibility in mobile apps on every stack — screen reader labels, roles, states and reading order for TalkBack and VoiceOver, decorative elements, grouping, touch target size, text scaling and Dynamic Type, color contrast, focus in dialogs and announcements for dynamic content — with the exact APIs for Flutter (Semantics), native Android (Jetpack Compose semantics and Views), native iOS (SwiftUI and UIKit) and React Native, plus automated checks in tests. Use when building or reviewing UI for accessibility, preparing an accessibility audit, fixing TalkBack or VoiceOver issues, or when the user mentions a11y, WCAG, screen readers, font scaling or contrast, even if they only point at one screen.
|
|
4
|
+
intents: [accessibility]
|
|
5
|
+
stacks: ["*"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Mobile Accessibility
|
|
9
|
+
|
|
10
|
+
One accessibility process for every mobile stack. This file holds what is true everywhere; each platform's APIs, tools and pitfalls live in its reference.
|
|
11
|
+
|
|
12
|
+
## 1. Pick the reference
|
|
13
|
+
|
|
14
|
+
| Project | Read |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Flutter | [references/flutter.md](references/flutter.md) |
|
|
17
|
+
| Native Android: Jetpack Compose or Views | [references/android.md](references/android.md) |
|
|
18
|
+
| Native iOS: SwiftUI or UIKit | [references/ios.md](references/ios.md) |
|
|
19
|
+
| React Native or Expo | [references/react-native.md](references/react-native.md) |
|
|
20
|
+
|
|
21
|
+
In Flutter and React Native apps, the native reference applies only to screens or components written natively (platform views, native modules with UI).
|
|
22
|
+
|
|
23
|
+
## 2. What a screen reader user needs
|
|
24
|
+
|
|
25
|
+
Every interactive element exposes four things to the accessibility tree. Check them in this order:
|
|
26
|
+
|
|
27
|
+
1. **Name.** What it is or does, in words: "Delete task", not "trash icon" or nothing. Text children usually provide it; icon-only controls, images that carry meaning and custom-drawn controls need an explicit label.
|
|
28
|
+
2. **Role.** Button, checkbox, switch, slider, link, header, image. A tappable container without a role is read as plain text, and the user does not know it can be activated.
|
|
29
|
+
3. **State and value.** Checked, selected, expanded, disabled, the slider's value. When the state changes, the new state is read.
|
|
30
|
+
4. **Order and grouping.** Reading order follows the visual order. Related pieces (an avatar, a name and a timestamp in a list row) are one element, not three swipes.
|
|
31
|
+
|
|
32
|
+
And the tree must not contain noise: decorative images, dividers and duplicated text are hidden from assistive technology.
|
|
33
|
+
|
|
34
|
+
## 3. Checks (all stacks)
|
|
35
|
+
|
|
36
|
+
**Perceivable**
|
|
37
|
+
- [ ] Icon-only buttons, image buttons and meaningful images have a label that describes the action or content.
|
|
38
|
+
- [ ] Decorative images and icons next to text that already says the same are excluded from the accessibility tree.
|
|
39
|
+
- [ ] Text contrast is at least 4.5:1, or 3:1 for large text; icons and control borders that carry meaning reach 3:1 (WCAG 1.4.3 and 1.4.11). Contrast cannot be proven from code alone when colors come from themes or images: say so and name the tool that measures it.
|
|
40
|
+
- [ ] Information is not conveyed by color only (an error shown only as a red border).
|
|
41
|
+
|
|
42
|
+
**Operable**
|
|
43
|
+
- [ ] Touch targets are at least 48×48 dp on Android and 44×44 pt on iOS, even when the visible icon is smaller. The layout extends the tappable area instead of the icon.
|
|
44
|
+
- [ ] Gestures (swipe to delete, long press, drag) have an alternative: a visible button or a custom accessibility action.
|
|
45
|
+
- [ ] Modal dialogs and bottom sheets move focus inside when they open and return it when they close; content behind them is not reachable.
|
|
46
|
+
- [ ] Timeouts, auto-advancing carousels and animations can be paused or respect the system's reduce motion setting.
|
|
47
|
+
|
|
48
|
+
**Understandable**
|
|
49
|
+
- [ ] Form fields have a persistent label, not only a placeholder; errors are announced and say how to fix the input.
|
|
50
|
+
- [ ] Screen titles and section headers are marked as headers, so users can jump between them.
|
|
51
|
+
- [ ] Dynamic changes the user did not trigger by moving focus — a snackbar, a validation error, "3 results", loading finished — are announced.
|
|
52
|
+
|
|
53
|
+
**Robust**
|
|
54
|
+
- [ ] Text scales with the system font size setting up to the largest accessibility sizes without truncation, overlap or clipped containers. Fixed heights on text containers are the usual cause.
|
|
55
|
+
- [ ] Custom-drawn controls (canvas, custom painters, gesture detectors) expose name, role, state and actions explicitly.
|
|
56
|
+
- [ ] The layout works in landscape and with the display size (screen zoom) setting increased.
|
|
57
|
+
|
|
58
|
+
## 4. Evidence and severity
|
|
59
|
+
|
|
60
|
+
Point to `file:line` for every finding and give the fix in the platform's own API, not pseudocode.
|
|
61
|
+
|
|
62
|
+
- **Critical:** a screen reader user cannot complete the flow — an unlabeled primary action, a control that cannot be activated with the screen reader, focus trapped or lost, a custom control invisible to the tree.
|
|
63
|
+
- **High:** the flow is possible but misleading or very hard — wrong role, missing state, targets far below the minimum, text that clips at larger sizes, unannounced errors.
|
|
64
|
+
- **Medium:** noise and friction — decorative elements read aloud, rows split into many swipes, missing headers.
|
|
65
|
+
- **Low:** polish — hints, custom action names, reading order inside a well-labeled group.
|
|
66
|
+
|
|
67
|
+
## 5. Verify on a device
|
|
68
|
+
|
|
69
|
+
Code review finds most problems; only assistive technology confirms the experience. Recommend the automated checks listed in the reference for the test suite, then a manual pass: TalkBack on Android and VoiceOver on iOS through the flow, the largest font size, and the platform's scanner (Accessibility Scanner on Android, Accessibility Inspector in Xcode). Say which findings came from code only.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Accessibility on native Android
|
|
2
|
+
|
|
3
|
+
TalkBack reads the accessibility tree built from Compose semantics or from View properties. Material components fill in roles and states; custom composables with `Modifier.clickable`, `pointerInput` or `Canvas`, and custom Views, need them explicitly.
|
|
4
|
+
|
|
5
|
+
## Jetpack Compose
|
|
6
|
+
|
|
7
|
+
| Need | API |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Label an icon-only control | `Icon(Icons.Default.Delete, contentDescription = "Delete task")` inside `IconButton` |
|
|
10
|
+
| Hide a decorative icon or image | `contentDescription = null` |
|
|
11
|
+
| Role for a custom clickable | `Modifier.clickable(role = Role.Button, onClickLabel = "Open details") { }` |
|
|
12
|
+
| Toggle with role and state | `Modifier.toggleable(value = checked, role = Role.Checkbox, onValueChange = ...)` on the whole row |
|
|
13
|
+
| Merge a row into one element | `Modifier.semantics(mergeDescendants = true) {}` |
|
|
14
|
+
| Replace children's semantics | `Modifier.clearAndSetSemantics { contentDescription = "..." }` |
|
|
15
|
+
| Header | `Modifier.semantics { heading() }` |
|
|
16
|
+
| Custom state text | `Modifier.semantics { stateDescription = "Expanded" }` |
|
|
17
|
+
| Live updates | `Modifier.semantics { liveRegion = LiveRegionMode.Polite }` |
|
|
18
|
+
| Gesture alternative | `Modifier.semantics { customActions = listOf(CustomAccessibilityAction("Archive") { archive(); true }) }` |
|
|
19
|
+
| Dialog or pane title | `Modifier.semantics { paneTitle = "Filters" }` |
|
|
20
|
+
| Reading order | `Modifier.semantics { traversalIndex = 1f }` with `isTraversalGroup = true` on the container |
|
|
21
|
+
|
|
22
|
+
Pitfalls:
|
|
23
|
+
|
|
24
|
+
- A `Checkbox` or `Switch` next to a `Text` in a `Row` produces two elements and a checkbox without a name. Make the `Row` `toggleable` and pass `onCheckedChange = null` to the inner control.
|
|
25
|
+
- `Modifier.size(24.dp)` on an `IconButton` shrinks the touch target. Material components apply `Modifier.minimumInteractiveComponentSize()` (48 dp); keep the icon small, not the button.
|
|
26
|
+
- `Modifier.pointerInput` and `detectTapGestures` add no semantics at all; prefer `clickable`, or add `onClick` in `semantics`.
|
|
27
|
+
- Text sizes in `dp` instead of `sp` do not scale with the font size setting.
|
|
28
|
+
|
|
29
|
+
## Views
|
|
30
|
+
|
|
31
|
+
| Need | API |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Label | `android:contentDescription`, or `android:labelFor` on the `TextView` that labels an input |
|
|
34
|
+
| Hide decorative | `android:importantForAccessibility="no"` (or `contentDescription="@null"` on `ImageView`) |
|
|
35
|
+
| Group | `android:focusable="true"` on the container with `android:screenReaderFocusable="true"` |
|
|
36
|
+
| Header | `android:accessibilityHeading="true"` / `ViewCompat.setAccessibilityHeading` |
|
|
37
|
+
| Live updates | `android:accessibilityLiveRegion="polite"` |
|
|
38
|
+
| Role, state, actions | `ViewCompat.setAccessibilityDelegate` with `onInitializeAccessibilityNodeInfo`, or `ViewCompat.addAccessibilityAction` |
|
|
39
|
+
| Text field hint and error | `TextInputLayout` with `hint` and `error` |
|
|
40
|
+
|
|
41
|
+
`View.announceForAccessibility` is discouraged in recent Android releases in favor of live regions and pane titles; check the `compileSdk` and the method's documentation before recommending it.
|
|
42
|
+
|
|
43
|
+
## Touch targets and text
|
|
44
|
+
|
|
45
|
+
- 48×48 dp minimum. For small icons use padding or `TouchDelegate` in Views.
|
|
46
|
+
- Text in `sp`; containers with `wrap_content` or minimum heights, not fixed heights.
|
|
47
|
+
- Test at the largest font size and display size in Settings, including the nonlinear font scaling of Android 14 and later.
|
|
48
|
+
|
|
49
|
+
## Automated checks
|
|
50
|
+
|
|
51
|
+
- Espresso: `AccessibilityChecks.enable()` from `androidx.test.espresso:espresso-accessibility` runs the Accessibility Test Framework on every view action.
|
|
52
|
+
- Compose UI tests can assert semantics: `onNodeWithContentDescription("Delete task").assertHasClickAction()`, `assert(hasStateDescription("Expanded"))`.
|
|
53
|
+
- Lint flags missing `contentDescription` on `ImageView` and `ImageButton` (`ContentDescription`) and small touch targets in some layouts.
|
|
54
|
+
- Manual: TalkBack, the Accessibility Scanner app, and Settings → Accessibility → font and display size.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Accessibility in Flutter
|
|
2
|
+
|
|
3
|
+
Flutter builds its own semantics tree from widgets and hands it to TalkBack and VoiceOver. Material and Cupertino widgets already fill in most of it; custom widgets built from `GestureDetector`, `InkWell` without children text, `CustomPaint` and images do not. For the broader Flutter guide (including localization), see `mobile-flutter` → `references/accessibility-i18n.md`.
|
|
4
|
+
|
|
5
|
+
## Labels, roles and states
|
|
6
|
+
|
|
7
|
+
| Need | API |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Label an icon button | `IconButton(tooltip: 'Delete task', ...)` — the tooltip becomes the label |
|
|
10
|
+
| Label a meaningful image | `Image.asset(..., semanticLabel: 'Profile photo of Ana')` |
|
|
11
|
+
| Hide a decorative image | `Image.asset(..., excludeFromSemantics: true)`, or wrap in `ExcludeSemantics` |
|
|
12
|
+
| Custom tappable widget | `Semantics(button: true, label: '...', onTap: ..., child: ...)` |
|
|
13
|
+
| Toggle state | `Semantics(toggled: value, ...)` or `checked:` for checkbox-like controls |
|
|
14
|
+
| Header | `Semantics(header: true, child: Text('Settings'))` |
|
|
15
|
+
| Replace children's semantics | `Semantics(label: '...', excludeSemantics: true, child: ...)` |
|
|
16
|
+
| Merge a row into one element | `MergeSemantics(child: ListTile(...))` |
|
|
17
|
+
| Extra actions for gestures | `Semantics(customSemanticsActions: {CustomSemanticsAction(label: 'Archive'): _archive})` |
|
|
18
|
+
| Text field label | `TextField(decoration: InputDecoration(labelText: 'Email', errorText: ...))` |
|
|
19
|
+
|
|
20
|
+
Pitfalls:
|
|
21
|
+
|
|
22
|
+
- `GestureDetector` and `InkWell` around an `Icon` expose a tap action but no label. Prefer `IconButton`, or add `Semantics`.
|
|
23
|
+
- `Icon(semanticLabel: ...)` labels the icon, but it is still not a button. Put the label on the tappable widget.
|
|
24
|
+
- `Semantics` without `container: true` may merge into an ancestor; set it when a widget must be its own node.
|
|
25
|
+
- `CustomPaint` is invisible to screen readers unless its painter implements `semanticsBuilder` or it is wrapped in `Semantics`.
|
|
26
|
+
|
|
27
|
+
## Announcements and focus
|
|
28
|
+
|
|
29
|
+
- Mark text that changes on its own (a counter, a status) with `Semantics(liveRegion: true, ...)`.
|
|
30
|
+
- For one-off messages, `SemanticsService.announce(message, textDirection)` exists in `package:flutter/semantics.dart`; check its status in the Flutter version in `pubspec.lock`'s SDK constraint before recommending it, since the announcement APIs have been evolving for multi-view support.
|
|
31
|
+
- `SnackBar`s shown through `ScaffoldMessenger` are announced by the framework.
|
|
32
|
+
- Dialogs opened with `showDialog` and routes block the semantics of what is behind them; custom overlays built with `Stack` and `Positioned` do not — use `BlockSemantics` or a route.
|
|
33
|
+
- Control reading order with `Semantics(sortKey: OrdinalSortKey(n))` only when the visual order and the widget order genuinely differ.
|
|
34
|
+
|
|
35
|
+
## Touch targets and text scaling
|
|
36
|
+
|
|
37
|
+
- Material's minimum is `kMinInteractiveDimension` (48). `ThemeData.materialTapTargetSize: MaterialTapTargetSize.shrinkWrap` shrinks it — flag it on interactive components.
|
|
38
|
+
- Small icons: keep the visual size and give the tappable widget `constraints: BoxConstraints(minWidth: 48, minHeight: 48)` or padding.
|
|
39
|
+
- Text scaling comes from `MediaQuery.textScalerOf(context)`. Flag code that overrides it for the whole app (`MediaQuery(data: ...copyWith(textScaler: TextScaler.noScaling))`) and fixed-height containers around text.
|
|
40
|
+
- Respect reduced motion with `MediaQuery.disableAnimationsOf(context)`.
|
|
41
|
+
|
|
42
|
+
## Automated checks
|
|
43
|
+
|
|
44
|
+
Widget tests can assert the guidelines that `flutter_test` ships:
|
|
45
|
+
|
|
46
|
+
```dart
|
|
47
|
+
testWidgets('login screen meets accessibility guidelines', (tester) async {
|
|
48
|
+
final handle = tester.ensureSemantics();
|
|
49
|
+
await tester.pumpWidget(const MaterialApp(home: LoginScreen()));
|
|
50
|
+
|
|
51
|
+
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
|
52
|
+
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
|
|
53
|
+
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
|
54
|
+
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
55
|
+
handle.dispose();
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Find semantics nodes in tests with `find.bySemanticsLabel('Delete task')`. On a device, use TalkBack and VoiceOver, and the `showSemanticsDebugger: true` flag of `MaterialApp` to see the tree.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Accessibility on native iOS
|
|
2
|
+
|
|
3
|
+
VoiceOver reads accessibility elements built from SwiftUI modifiers or UIKit properties. Standard controls come with traits and values; custom views, gesture recognizers and drawn content need them explicitly. For the broader iOS UI guide, see `mobile-ios` → `references/ui-navigation.md`.
|
|
4
|
+
|
|
5
|
+
## SwiftUI
|
|
6
|
+
|
|
7
|
+
| Need | API |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Label an icon-only button | `Button { } label: { Image(systemName: "trash") }.accessibilityLabel("Delete task")`, or `Label("Delete task", systemImage: "trash").labelStyle(.iconOnly)` |
|
|
10
|
+
| Hide a decorative image | `Image(decorative: "divider")` or `.accessibilityHidden(true)` |
|
|
11
|
+
| Trait for a custom tappable view | Use `Button` instead of `.onTapGesture`; otherwise `.accessibilityAddTraits(.isButton)` |
|
|
12
|
+
| Header | `.accessibilityAddTraits(.isHeader)` |
|
|
13
|
+
| Group a row | `.accessibilityElement(children: .combine)` |
|
|
14
|
+
| Replace children | `.accessibilityElement(children: .ignore).accessibilityLabel("...")` |
|
|
15
|
+
| Value | `.accessibilityValue("3 of 5")` |
|
|
16
|
+
| Hint | `.accessibilityHint("Opens the order details")` — only when the label is not enough |
|
|
17
|
+
| Gesture alternative | `.accessibilityAction(named: "Archive") { archive() }` |
|
|
18
|
+
| Adjustable control | `.accessibilityAdjustableAction { direction in ... }` |
|
|
19
|
+
| Reading order | `.accessibilitySortPriority(_:)` inside a container |
|
|
20
|
+
| Announcement | `AccessibilityNotification.Announcement("Saved").post()` (iOS 17+) or `UIAccessibility.post(notification: .announcement, argument: "Saved")` |
|
|
21
|
+
|
|
22
|
+
Pitfalls:
|
|
23
|
+
|
|
24
|
+
- `.onTapGesture` on an `HStack` does not add the button trait, so VoiceOver does not say the row can be activated; `Button` with a custom style keeps the look and the trait.
|
|
25
|
+
- `.font(.system(size: 17))` does not scale with Dynamic Type. Use text styles (`.font(.body)`) and `@ScaledMetric` for custom sizes and spacing.
|
|
26
|
+
- `.frame(height:)` on text containers clips at accessibility sizes; use `minHeight`, and switch `HStack` to `VStack` with `@Environment(\.dynamicTypeSize)` when `dynamicTypeSize.isAccessibilitySize`.
|
|
27
|
+
- Respect `@Environment(\.accessibilityReduceMotion)` for animations.
|
|
28
|
+
|
|
29
|
+
## UIKit
|
|
30
|
+
|
|
31
|
+
| Need | API |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Element and label | `isAccessibilityElement = true`, `accessibilityLabel` |
|
|
34
|
+
| Hide decorative | `isAccessibilityElement = false` or `accessibilityElementsHidden = true` on a container |
|
|
35
|
+
| Traits | `accessibilityTraits = [.button]`, `.header`, `.selected`, `.notEnabled` |
|
|
36
|
+
| Group a cell | set `isAccessibilityElement = true` and a combined label on the cell |
|
|
37
|
+
| Custom actions | `accessibilityCustomActions = [UIAccessibilityCustomAction(name: "Archive") { _ in ... }]` |
|
|
38
|
+
| Modal | `accessibilityViewIsModal = true` on the presented view |
|
|
39
|
+
| Screen or layout change | `UIAccessibility.post(notification: .screenChanged, argument: firstElement)` / `.layoutChanged` |
|
|
40
|
+
| Dynamic Type | `UIFont.preferredFont(forTextStyle:)`, `UIFontMetrics`, `adjustsFontForContentSizeCategory = true` |
|
|
41
|
+
|
|
42
|
+
## Touch targets
|
|
43
|
+
|
|
44
|
+
Apple's Human Interface Guidelines recommend at least 44×44 points. In SwiftUI, extend the hit area with `.frame(minWidth: 44, minHeight: 44)` and `.contentShape(Rectangle())`; in UIKit, override `point(inside:with:)` or enlarge the button's frame, not the image.
|
|
45
|
+
|
|
46
|
+
## Automated checks
|
|
47
|
+
|
|
48
|
+
- XCUITest: `try app.performAccessibilityAudit()` (Xcode 15, iOS 17 and later) checks contrast, hit regions, element descriptions, Dynamic Type clipping and traits; pass a set of audit types or a handler to ignore known issues.
|
|
49
|
+
- Query by label in UI tests (`app.buttons["Delete task"]`): a test that finds elements by label breaks when a label disappears.
|
|
50
|
+
- Manual: VoiceOver on a device, Accessibility Inspector in Xcode (audit and inspection), and Environment Overrides in the debug bar for Dynamic Type, contrast and reduce motion.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Accessibility in React Native
|
|
2
|
+
|
|
3
|
+
React Native maps accessibility props to the native TalkBack and VoiceOver APIs. Core components such as `Button` and `Switch` fill in roles; `Pressable`, `TouchableOpacity` and `View` do not. Recent React Native versions also accept ARIA-style props (`role`, `aria-label`, `aria-checked`...) that map to the same native properties; follow whichever style the project already uses. For the broader guide, see `mobile-rn` → `references/ui-navigation.md`.
|
|
4
|
+
|
|
5
|
+
## Labels, roles and states
|
|
6
|
+
|
|
7
|
+
| Need | API |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Label an icon-only control | `<Pressable accessibilityRole="button" accessibilityLabel="Delete task">` |
|
|
10
|
+
| Hide decorative content | `accessible={false}` on images; for subtrees `accessibilityElementsHidden` (iOS) **and** `importantForAccessibility="no-hide-descendants"` (Android) |
|
|
11
|
+
| Group a row | `accessible` on the container, with a combined `accessibilityLabel` when children's text is not enough |
|
|
12
|
+
| State | `accessibilityState={{ checked, selected, disabled, expanded, busy }}` |
|
|
13
|
+
| Value | `accessibilityValue={{ min: 0, max: 100, now: 40 }}` or `{{ text: '3 of 5' }}` |
|
|
14
|
+
| Header | `accessibilityRole="header"` |
|
|
15
|
+
| Hint | `accessibilityHint="Opens the order details"` |
|
|
16
|
+
| Gesture alternative | `accessibilityActions={[{ name: 'archive', label: 'Archive' }]}` with `onAccessibilityAction` |
|
|
17
|
+
| Live updates | `accessibilityLiveRegion="polite"` (Android); `AccessibilityInfo.announceForAccessibility('Saved')` for both |
|
|
18
|
+
| Modal | `<Modal>` handles focus; custom overlays need `accessibilityViewIsModal` (iOS) and `importantForAccessibility="no-hide-descendants"` on the content behind (Android) |
|
|
19
|
+
| Move focus | `AccessibilityInfo.setAccessibilityFocus(findNodeHandle(ref.current))` |
|
|
20
|
+
|
|
21
|
+
Pitfalls:
|
|
22
|
+
|
|
23
|
+
- A `Pressable` without `accessibilityRole` is read as plain text: the user does not know it is tappable.
|
|
24
|
+
- A checkbox built from `Pressable` and an icon needs `accessibilityRole="checkbox"` and `accessibilityState={{ checked }}`.
|
|
25
|
+
- The iOS-only and Android-only props are silently ignored on the other platform; hiding and modal behavior need both.
|
|
26
|
+
- `TextInput` without a visible label: add `accessibilityLabel`; placeholders are not labels.
|
|
27
|
+
|
|
28
|
+
## Touch targets and text scaling
|
|
29
|
+
|
|
30
|
+
- Keep interactive elements at 44×44 pt / 48×48 dp. Use `hitSlop` or padding to extend the area of small icons; `hitSlop` does not extend beyond the parent's bounds on Android.
|
|
31
|
+
- Text scales with the system font size by default. Flag `allowFontScaling={false}` on body text; prefer `maxFontSizeMultiplier` when a layout has a real limit. Avoid fixed `height` on text containers.
|
|
32
|
+
- Respect reduced motion with `AccessibilityInfo.isReduceMotionEnabled()` or Reanimated's `useReducedMotion()`.
|
|
33
|
+
|
|
34
|
+
## Automated checks
|
|
35
|
+
|
|
36
|
+
- React Native Testing Library queries by role and label (`screen.getByRole('button', { name: 'Delete task' })`), so tests fail when a role or label disappears. `toBeChecked`, `toBeDisabled` and `toBeExpanded` matchers read the accessibility state.
|
|
37
|
+
- ESLint: `eslint-plugin-react-native-a11y` flags missing roles and labels, when the project uses ESLint.
|
|
38
|
+
- Manual: TalkBack and VoiceOver on devices, the Accessibility Scanner app on Android and Accessibility Inspector in Xcode on the iOS build.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mobile-debug
|
|
3
|
-
description: Finds the cause of failing builds and runtime errors in mobile apps on every stack — Flutter (pub version solving, Dart compilation, generated code, layout overflows, runtime exceptions), native Android (Gradle, AGP, JDK and Kotlin mismatches, dependency conflicts, manifest merger, R8, logcat crashes and
|
|
3
|
+
description: Finds the cause of failing builds and runtime errors in mobile apps on every stack — Flutter (pub version solving, Dart compilation, generated code, layout overflows, runtime exceptions), native Android (Gradle, AGP, JDK and Kotlin mismatches, dependency conflicts, manifest merger, R8, logcat crashes, ANRs and memory leaks), native iOS (Xcode signing and provisioning, deployment targets, CocoaPods, Swift Package Manager, crash logs) and React Native (Metro, native modules not found, Hermes, the Android and iOS builds underneath), plus deep links, App Links and Universal Links that do not open the app. Use whenever a build, `flutter run`, `pod install`, Gradle task, Xcode archive or app launch fails, when the user pastes a stack trace, "BUILD FAILED" or a red screen, or runs /reis-mobile:debug, even if they only paste the error.
|
|
4
4
|
intents: [debug, build, dependency, migration]
|
|
5
5
|
stacks: ["*"]
|
|
6
6
|
---
|
|
@@ -19,6 +19,7 @@ Choose by the stack **and** by the layer the error comes from — a Flutter or R
|
|
|
19
19
|
| Gradle, the Android build, logcat crashes or ANRs (native Android, or the `android/` folder of any app) | [references/android.md](references/android.md) |
|
|
20
20
|
| Xcode, signing, CocoaPods, Swift packages or iOS crash logs (native iOS, or the `ios/` folder of any app) | [references/ios.md](references/ios.md) |
|
|
21
21
|
| A React Native or Expo app: Metro, native modules, JavaScript errors | [references/react-native.md](references/react-native.md) |
|
|
22
|
+
| A deep link, App Link or Universal Link that opens the browser, the wrong screen or crashes (any stack) | [references/deep-links.md](references/deep-links.md) |
|
|
22
23
|
|
|
23
24
|
When `reis-mobile debug` (or `/reis-mobile:debug`) ran, its `area` line points at the layer: `gradle`, `manifest`, `proguard` → Android; `xcode`, `signing`, `cocoapods`, `spm` → iOS; `pub` → Flutter; `metro` → React Native.
|
|
24
25
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Investigating Android ANRs
|
|
2
|
+
|
|
3
|
+
An ANR (Application Not Responding) is reported when the main thread cannot process an input event for about five seconds, or when a broadcast receiver, service or job does not finish its callback in the time the system allows. The top frame of the main thread is often the victim, not the cause: find what the main thread was waiting for.
|
|
4
|
+
|
|
5
|
+
## 1. Get the evidence
|
|
6
|
+
|
|
7
|
+
| Source | What it gives |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Play Console → Android vitals → ANRs | Clusters with the main thread's stack, affected versions, devices and Android versions |
|
|
10
|
+
| Crashlytics or another reporter with ANR support | Main thread stack; other threads depend on the SDK |
|
|
11
|
+
| `ApplicationExitInfo` (Android 11+) | `REASON_ANR` exits of previous processes, with `getTraceInputStream()` returning the full thread dump — log or upload it on the next launch |
|
|
12
|
+
| `adb bugreport` | ANR traces of all threads when reproduced on a test device |
|
|
13
|
+
| Local reproduction | `adb logcat` shows `ANR in <package>` with the reason; `StrictMode` flags disk and network on the main thread before it becomes an ANR |
|
|
14
|
+
|
|
15
|
+
High confidence needs the **whole thread dump**, not only the main thread. With a single stack, say the conclusion is tentative and ask for the full trace.
|
|
16
|
+
|
|
17
|
+
## 2. Read the reason
|
|
18
|
+
|
|
19
|
+
- `Input dispatching timed out` → the main thread did not handle a touch or key event in time.
|
|
20
|
+
- `Broadcast of Intent { ... }` / `executing service` / `ContentProvider not responding` → a component callback ran too long on the main thread.
|
|
21
|
+
- `Context.startForegroundService() did not then call Service.startForeground()` → the service did not promote itself in time. On recent Android versions this is reported as a crash (`ForegroundServiceDidNotStartInTimeException`) as well.
|
|
22
|
+
|
|
23
|
+
## 3. Read the main thread's state
|
|
24
|
+
|
|
25
|
+
In the dump, find `"main" prio=5 tid=1 <State>`:
|
|
26
|
+
|
|
27
|
+
| State | Look for |
|
|
28
|
+
|---|---|
|
|
29
|
+
| **Runnable** | Work on the main thread: JSON parsing, database queries, `SharedPreferences.commit()`, file I/O, bitmap decoding, crypto, class loading of large SDKs, a loop in layout or composition |
|
|
30
|
+
| **Blocked** | `- waiting to lock <0x...> held by thread N`: open thread N and see what it does while holding the lock |
|
|
31
|
+
| **Waiting / TimedWaiting** | `Object.wait`, `Future.get`, `CountDownLatch.await`, `Thread.join`, `runBlocking`, a `Semaphore` or `Mutex`: find who should release it |
|
|
32
|
+
| **Native** | Binder calls into system services (`BinderProxy.transact`), `SharedPreferences` `apply()` flushing in `QueuedWork.waitToFinish` during `onPause`/`onStop`, file system or graphics calls |
|
|
33
|
+
|
|
34
|
+
For a deadlock, write down the cycle: thread A holds lock 1 and waits for lock 2; thread B holds lock 2 and waits for lock 1.
|
|
35
|
+
|
|
36
|
+
## 4. Match the code
|
|
37
|
+
|
|
38
|
+
- `runBlocking` anywhere reachable from the main thread, including inside interceptors, `ContentProvider`s and DI factories.
|
|
39
|
+
- `@Synchronized` or `synchronized` blocks that also do network or disk work: the lock is held for the whole I/O.
|
|
40
|
+
- Eager initialization in `Application.onCreate`, `ContentProvider.onCreate` (including library initializers) or the first Activity.
|
|
41
|
+
- Room queries without `suspend`/`Flow`, or with `allowMainThreadQueries()`.
|
|
42
|
+
- `BroadcastReceiver.onReceive` doing work beyond starting a `WorkManager` job; `goAsync()` without calling `finish()`.
|
|
43
|
+
- `SharedPreferences.apply()` writes piling up and flushed on lifecycle transitions: move hot data to DataStore.
|
|
44
|
+
- Third-party SDK calls that are synchronous on first use (ads, analytics, feature flags).
|
|
45
|
+
|
|
46
|
+
## 5. Fix
|
|
47
|
+
|
|
48
|
+
- Remove the blocking path that the evidence proves, not the top frame. Moving work to `Dispatchers.IO` fixes main-thread I/O; it does not fix a lock held across I/O — shorten the critical section instead.
|
|
49
|
+
- Replace `runBlocking` with a suspending call chain, or with an in-memory value that is refreshed asynchronously.
|
|
50
|
+
- Defer SDK initialization (App Startup with lazy initializers, or initialization after the first frame).
|
|
51
|
+
- Keep receivers, services and job callbacks short; hand long work to `WorkManager`.
|
|
52
|
+
- Do not trade an ANR for lost work or a race: if the result is needed, expose loading state instead of blocking.
|
|
53
|
+
|
|
54
|
+
## 6. Verify
|
|
55
|
+
|
|
56
|
+
- Reproduce with the slow dependency made slower (a delay in the network call or the lock owner) on a low-end device or emulator with a cold start.
|
|
57
|
+
- `StrictMode.ThreadPolicy` with `detectAll()` in debug builds shows no violations on the path.
|
|
58
|
+
- A system trace (Perfetto, Android Studio's CPU profiler) shows the main thread free during the flow. See `mobile-android` → the profiler guide.
|
|
59
|
+
- After release, watch the cluster and the user-perceived ANR rate in Android vitals for the new version.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Investigating Android memory leaks
|
|
2
|
+
|
|
3
|
+
A leak is an object that should have been garbage collected — usually an Activity, Fragment view, View or Context — kept alive by a reference from something that lives longer. Symptoms: memory growing each time a screen opens, `OutOfMemoryError` after long sessions, and slow GC pauses.
|
|
4
|
+
|
|
5
|
+
## 1. Get the evidence
|
|
6
|
+
|
|
7
|
+
- **LeakCanary** (`debugImplementation("com.squareup.leakcanary:leakcanary-android:<version>")`, no code needed) detects retained Activities, Fragments, Fragment views, ViewModels and Services in debug builds and prints the **leak trace**: the chain of references from a GC root to the leaked object. Check the version in the project before adding it.
|
|
8
|
+
- **Android Studio Memory Profiler:** capture a heap dump after opening and closing the screen a few times and forcing GC; filter by the Activity class and look for more instances than screens on the back stack. *Show activity/fragment leaks* highlights destroyed ones.
|
|
9
|
+
- **`OutOfMemoryError` reports** show where the last allocation failed, which is rarely the leak itself; use them to know which flow to profile.
|
|
10
|
+
|
|
11
|
+
## 2. Read the leak trace
|
|
12
|
+
|
|
13
|
+
LeakCanary marks each reference as leaking, not leaking or unknown. The bug is in the references between the last **not leaking** object and the first **leaking** one. Typical shape:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
GC Root: static field
|
|
17
|
+
↓ SessionManager.INSTANCE
|
|
18
|
+
↓ SessionManager.listeners ← likely cause: never unregistered
|
|
19
|
+
↓ ProfileActivity$1 (anonymous listener)
|
|
20
|
+
↓ ProfileActivity ← destroyed but retained
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 3. Common causes and fixes
|
|
24
|
+
|
|
25
|
+
| Cause | Fix |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Singleton, `object` or static field holding an Activity, View or Context | Hold `applicationContext`, or do not store it; pass it per call |
|
|
28
|
+
| Listener or callback registered in `onStart`/`onCreate` and never removed | Unregister in the symmetric callback, or use lifecycle-aware APIs (`repeatOnLifecycle`, `LifecycleObserver`) |
|
|
29
|
+
| Fragment keeping a binding or views after `onDestroyView` | Clear the binding in `onDestroyView`, or use `viewLifecycleOwner` for observers |
|
|
30
|
+
| Coroutine launched in `GlobalScope` or a custom scope that captures the Activity | Use `lifecycleScope`/`viewModelScope`; never capture views in long-running work |
|
|
31
|
+
| ViewModel holding an Activity, Fragment, View or its Context | Keep only data; use `AndroidViewModel` or an injected application context when a Context is needed |
|
|
32
|
+
| Inner or anonymous classes (Handler, Runnable, AsyncTask) outliving the Activity | Static nested class with a weak reference, or cancel pending work in `onDestroy` |
|
|
33
|
+
| `Handler.postDelayed` pending when the screen closes | `removeCallbacksAndMessages(null)` in the teardown |
|
|
34
|
+
| Third-party SDK holding the Activity passed at init | Pass the application context, or report it upstream and add it to LeakCanary's known library leaks |
|
|
35
|
+
| Compose: objects captured in `remember` that hold an Activity, or `DisposableEffect` without cleanup in `onDispose` | Use `LocalContext` at call time; release in `onDispose` |
|
|
36
|
+
|
|
37
|
+
Not every growth is a leak: image caches and pools are bounded by design. Check whether memory stabilizes after several repetitions before calling it a leak.
|
|
38
|
+
|
|
39
|
+
## 4. Verify
|
|
40
|
+
|
|
41
|
+
- Run the flow again with LeakCanary: no retained instance reported.
|
|
42
|
+
- Heap dump after repeating the flow and forcing GC: one or zero instances of the Activity.
|
|
43
|
+
- For large bitmaps and caches, compare the Java and native heap in the profiler before and after.
|
|
@@ -24,3 +24,9 @@ In Flutter and React Native apps, run Gradle from `android/` to see the full out
|
|
|
24
24
|
- An ANR means the main thread was blocked for about five seconds (input) or a broadcast receiver or service ran too long.
|
|
25
25
|
- Get the main thread's stack from Play Console vitals, or from `adb bugreport` (the ANR traces are included), and look at what the main thread was waiting on: disk, network, a lock, `runBlocking`, a binder call.
|
|
26
26
|
- `StrictMode` in debug builds flags disk and network access on the main thread before it becomes an ANR.
|
|
27
|
+
|
|
28
|
+
Full investigation — evidence sources, reading thread states, locks and deadlocks, fixes and verification: [android/anr.md](android/anr.md).
|
|
29
|
+
|
|
30
|
+
## Memory leaks and OutOfMemoryError
|
|
31
|
+
|
|
32
|
+
Retained Activities, Fragments and Contexts, reading LeakCanary traces and heap dumps: [android/memory-leaks.md](android/memory-leaks.md).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Debugging deep links
|
|
2
|
+
|
|
3
|
+
Deep links fail in three places: the **domain verification** (the link opens the browser), the **app's registration** (the system does not know the app handles the URL) and the **in-app routing** (the app opens on the wrong screen or crashes). Find which one before changing anything. Security checks for deep links (validating parameters, not acting without confirmation) are in `mobile-security`.
|
|
4
|
+
|
|
5
|
+
## 1. Classify the symptom
|
|
6
|
+
|
|
7
|
+
| Symptom | Layer to check first |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Opens the browser instead of the app | Verification: `assetlinks.json` / `apple-app-site-association`, entitlement, fingerprint |
|
|
10
|
+
| Works in debug, not from the store build | Verification with the release signing key (Android) or the distribution Team ID (iOS) |
|
|
11
|
+
| Shows a chooser dialog on Android | App Links not verified; the link is treated as a plain intent filter |
|
|
12
|
+
| Opens the app on the home screen | In-app routing: the URL reaches the app but no route matches, or it is lost during cold start |
|
|
13
|
+
| Crashes on open | In-app routing: navigation before the navigator or the auth state is ready |
|
|
14
|
+
| Custom scheme (`myapp://`) does nothing | The scheme is not registered, or the test tool is quoting the URL wrong |
|
|
15
|
+
|
|
16
|
+
## 2. Android App Links
|
|
17
|
+
|
|
18
|
+
**Checklist**
|
|
19
|
+
- [ ] `<intent-filter android:autoVerify="true">` with `ACTION_VIEW`, categories `DEFAULT` and `BROWSABLE`, `android:scheme="https"` and the exact `android:host`.
|
|
20
|
+
- [ ] `https://<host>/.well-known/assetlinks.json` returns 200 over HTTPS without redirects, with `Content-Type: application/json`, for **every host** in the filters (including `www` and subdomains).
|
|
21
|
+
- [ ] The file lists the package name and the SHA-256 fingerprint of the certificate that signs the installed build. For Play builds that is the **app signing key** from the Play Console (App integrity), not the upload key; debug builds use the debug keystore fingerprint (`keytool -list -v -keystore ~/.android/debug.keystore`).
|
|
22
|
+
|
|
23
|
+
**Commands**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Verification state per domain (Android 12+)
|
|
27
|
+
adb shell pm get-app-links com.example.app
|
|
28
|
+
# Re-run verification after fixing the server file
|
|
29
|
+
adb shell pm verify-app-links --re-verify com.example.app
|
|
30
|
+
# Open a link as the system would
|
|
31
|
+
adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/product/42" com.example.app
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`verified` means the domain passed; `legacy_failure` or other states mean the server file or the fingerprint is wrong. Google's Statement List Generator and Tester checks a published `assetlinks.json`.
|
|
35
|
+
|
|
36
|
+
## 3. iOS Universal Links
|
|
37
|
+
|
|
38
|
+
**Checklist**
|
|
39
|
+
- [ ] The **Associated Domains** capability is on for the App ID and the target, with `applinks:example.com` (and each subdomain) in the entitlements of the build being tested.
|
|
40
|
+
- [ ] `https://<domain>/.well-known/apple-app-site-association` (no file extension) returns 200 over HTTPS without redirects, as JSON.
|
|
41
|
+
- [ ] The file's `applinks.details` contains `<TeamID>.<BundleID>` of that build and `components` (or legacy `paths`) that match the URL's path, query and fragment.
|
|
42
|
+
- [ ] Apple fetches the file through its CDN, not from the device: after changing it, check what the CDN serves at `https://app-site-association.cdn-apple.com/a/v1/<domain>`. To test against your server directly during development, use `applinks:example.com?mode=developer` with Developer Mode enabled on the device.
|
|
43
|
+
|
|
44
|
+
**Behavior that looks like a bug but is not**
|
|
45
|
+
- Typing or pasting a Universal Link in Safari's address bar opens the website. Test by tapping the link in Notes, Messages or a page on **another** domain.
|
|
46
|
+
- If the user chose to open the site in Safari from the banner, iOS remembers it for that domain; long-press the link and choose *Open in "App"* to reset.
|
|
47
|
+
|
|
48
|
+
**Tools:** Settings → Developer → Universal Links → Diagnostics on a device with Developer Mode checks a URL against the installed app. For custom schemes on the simulator: `xcrun simctl openurl booted "myapp://product/42"`.
|
|
49
|
+
|
|
50
|
+
## 4. In-app routing
|
|
51
|
+
|
|
52
|
+
- **Cold start vs warm start.** The URL arrives through different paths: Android `onCreate` intent vs `onNewIntent`; iOS `scene(_:willConnectTo:options:)` (`connectionOptions.userActivities`/`urlContexts`) vs `scene(_:continue:)`/`scene(_:openURLContexts:)`, or `.onOpenURL` in SwiftUI. A handler implemented only for the warm path loses links that launch the app.
|
|
53
|
+
- **Timing.** Links handled before the navigator, the auth state or remote config is ready either crash or are dropped. Store the pending link and route after initialization; send users to login first and resume the link afterward.
|
|
54
|
+
- **Route matching.** Log the received URL at the entry point and compare with the route patterns: trailing slashes, case, encoded characters and query parameters are frequent mismatches.
|
|
55
|
+
- **Flutter:** with `go_router`, the Flutter deep linking flag (`flutter_deeplinking_enabled` meta-data in `AndroidManifest.xml`, `FlutterDeepLinkingEnabled` in `Info.plist`) lets the router receive links; when a plugin such as `app_links` handles them instead, follow the plugin's instructions for that flag. Check the Flutter version's default before changing it.
|
|
56
|
+
- **React Native:** the `linking` config of React Navigation (`prefixes` and `config.screens`) or Expo Router's file routes decide the screen; test with `npx uri-scheme open "myapp://product/42" --android` (or `--ios`). Expo Go uses its own scheme; test real links in a development build.
|
|
57
|
+
|
|
58
|
+
## 5. Verify
|
|
59
|
+
|
|
60
|
+
Test on a device with the build type that will ship, for each link type and host, from cold start and from background, logged in and logged out.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-release
|
|
3
|
+
description: Prepares and checks mobile app releases on every stack — release readiness (go/no-go) checklist, version name and build number, signing, crash symbols (R8 mappings, dSYMs, Dart split debug info, JavaScript source maps), store requirements, staged rollout and rollback, release notes and store listing copy with the Google Play and App Store character limits, and CI/CD pipelines with GitHub Actions, fastlane and EAS — for Flutter, native Android, native iOS and React Native. Use when preparing a version for Google Play or the App Store, auditing whether a build is ready to ship, writing release notes or store descriptions from the git history, setting up or reviewing a build and deploy pipeline, or planning a rollout, even if the user only says "we ship on Friday".
|
|
4
|
+
intents: [release, deployment]
|
|
5
|
+
stacks: ["*"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Mobile Release
|
|
9
|
+
|
|
10
|
+
One release process for every mobile stack. This file holds the process and what is true everywhere; each platform's versioning, signing, symbols and store details live in its reference.
|
|
11
|
+
|
|
12
|
+
## 1. Pick the references
|
|
13
|
+
|
|
14
|
+
| Need | Read |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Flutter app | [references/flutter.md](references/flutter.md) |
|
|
17
|
+
| Native Android, or the Android build of any app | [references/android.md](references/android.md) |
|
|
18
|
+
| Native iOS, or the iOS build of any app | [references/ios.md](references/ios.md) |
|
|
19
|
+
| React Native or Expo app | [references/react-native.md](references/react-native.md) |
|
|
20
|
+
| Release notes and store listing text | [references/store-copy.md](references/store-copy.md) |
|
|
21
|
+
| A CI/CD pipeline that builds, signs and uploads | [references/ci-cd.md](references/ci-cd.md) |
|
|
22
|
+
|
|
23
|
+
Flutter and React Native apps also need the Android and iOS references for signing and store details. Deeper platform guides live in the platform skills: Google Play policy and Play Billing in `mobile-android`, TestFlight and App Store Connect in `mobile-ios`, EAS in `mobile-rn`. Security hardening before release is in `mobile-security`.
|
|
24
|
+
|
|
25
|
+
## 2. Readiness checklist
|
|
26
|
+
|
|
27
|
+
Check each item against the project, not against memory. Mark each one **pass**, **fail** (with `file:line` or the missing artifact) or **unknown** (needs information only the team has, such as the store console state).
|
|
28
|
+
|
|
29
|
+
**Version**
|
|
30
|
+
- [ ] The version name follows the project's scheme and changed since the last release tag (`git describe --tags --abbrev=0`).
|
|
31
|
+
- [ ] The build number is higher than any build already uploaded. Both stores reject a build number they have seen.
|
|
32
|
+
- [ ] Android, iOS and any cross-platform manifest (`pubspec.yaml`, `app.json`) agree on the version.
|
|
33
|
+
|
|
34
|
+
**Build quality**
|
|
35
|
+
- [ ] CI is green on the release commit: lint or analysis, tests, and a release build for each platform.
|
|
36
|
+
- [ ] The release build is the one tested: release configuration, production environment, real signing — not a debug build.
|
|
37
|
+
- [ ] No debug leftovers: logging of sensitive data, debug menus, test endpoints, `debuggable` or development entitlements, feature flags pointing at staging.
|
|
38
|
+
- [ ] Crash symbols for this exact build are uploaded or archived: R8 mapping, dSYMs, Dart debug info, JavaScript source maps.
|
|
39
|
+
|
|
40
|
+
**Store and compliance**
|
|
41
|
+
- [ ] The target SDK, minimum OS and build toolchain meet the store's current requirements. These change every year: tell the user to confirm them in the Play Console and Apple's upload requirements instead of stating dates from memory.
|
|
42
|
+
- [ ] New permissions, tracking or data collection are reflected in the Play Data safety form, the App Store privacy details, privacy manifests and the privacy policy.
|
|
43
|
+
- [ ] Reviewers can reach every feature: a demo account and notes for anything behind login, hardware or location.
|
|
44
|
+
- [ ] Release notes and, when features changed, screenshots and description are ready in every listed language.
|
|
45
|
+
|
|
46
|
+
**Rollout**
|
|
47
|
+
- [ ] Staged rollout (Play) or phased release (App Store) planned for updates, with the crash-free and ANR metrics to watch and the threshold that halts it.
|
|
48
|
+
- [ ] Rollback plan: stores do not roll back a binary. Halting a rollout stops new installs; users who updated need a new build with a higher build number, a server-side kill switch or a remote config flag. Know which one applies before shipping.
|
|
49
|
+
- [ ] The release commit is tagged and the tag matches the version.
|
|
50
|
+
|
|
51
|
+
## 3. Report
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
## Release readiness — <app> <version> (<build>)
|
|
55
|
+
Verdict: GO | GO WITH RISKS | NO-GO
|
|
56
|
+
|
|
57
|
+
### Blockers
|
|
58
|
+
- item — evidence (file:line or missing artifact) — fix
|
|
59
|
+
|
|
60
|
+
### Risks
|
|
61
|
+
- item — why it matters — mitigation
|
|
62
|
+
|
|
63
|
+
### Unknown
|
|
64
|
+
- item — who or what can confirm it
|
|
65
|
+
|
|
66
|
+
### Rollout plan
|
|
67
|
+
Stages, metrics to watch, halt threshold, rollback path.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A single blocker makes the verdict NO-GO. Do not bump versions, create tags, upload builds or change store settings unless the user asks; propose the exact commands instead.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Releasing on Android
|
|
2
|
+
|
|
3
|
+
Google Play specifics: policy compliance and Play Billing live in `mobile-android` → `references/play.md`; R8 keep rules in its build and performance guides.
|
|
4
|
+
|
|
5
|
+
## Version
|
|
6
|
+
|
|
7
|
+
- `versionCode` (integer) must increase with every upload to Play, across all tracks. `versionName` is what users see.
|
|
8
|
+
- Set them in `defaultConfig` of `app/build.gradle(.kts)`, or from CI (`-PversionCode=...` read in the build script). Flutter and Expo generate them from their own manifests; see their references.
|
|
9
|
+
- When several APKs or bundles share a version (Wear OS, TV, different ABIs), each needs a distinct `versionCode`.
|
|
10
|
+
|
|
11
|
+
## Build and signing
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
./gradlew bundleRelease # app/build/outputs/bundle/release/app-release.aab
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- Google Play requires an Android App Bundle (`.aab`) for new apps. Use APKs only for other stores or direct distribution.
|
|
18
|
+
- With **Play App Signing**, Google holds the app signing key and you sign uploads with the **upload key**. A lost upload key can be reset through the Play Console; a lost app signing key outside Play App Signing cannot. Check in *Test and release → App integrity* which setup the app uses.
|
|
19
|
+
- Keystore passwords and the keystore itself never live in the repository. Read them from environment variables or an untracked `keystore.properties`, and check `.gitignore`.
|
|
20
|
+
- Fingerprints for App Links (`assetlinks.json`), Google Sign-In and Firebase come from the **app signing key** shown in the Play Console for Play builds, not from the upload or debug key.
|
|
21
|
+
|
|
22
|
+
## Symbols
|
|
23
|
+
|
|
24
|
+
- **R8 mapping** (`app/build/outputs/mapping/release/mapping.txt`) deobfuscates stack traces. Check in the Play Console's App bundle explorer that the deobfuscation file is attached to the version, and upload it when it is not. Crashlytics uploads it through its Gradle plugin. Archive it with the build either way.
|
|
25
|
+
- **Native symbols** for apps with C/C++ code: `android { buildTypes { release { ndk { debugSymbolLevel = "FULL" } } } }` (or `"SYMBOL_TABLE"`) adds them to the bundle.
|
|
26
|
+
|
|
27
|
+
## Before uploading
|
|
28
|
+
|
|
29
|
+
- [ ] `targetSdk` meets Google Play's current target API level requirement (it moves every year; confirm in the Play Console or the Play policy guide).
|
|
30
|
+
- [ ] `isMinifyEnabled` and `isShrinkResources` enabled for release, and the release build tested with them.
|
|
31
|
+
- [ ] `android:debuggable` not set, no `usesCleartextTraffic="true"` without a scoped network security config, `android:allowBackup` decided on purpose.
|
|
32
|
+
- [ ] New permissions justified; sensitive ones (location in background, SMS, call log, all files access, exact alarms) have the Play declaration they need.
|
|
33
|
+
- [ ] Data safety form updated for new SDKs or data collection.
|
|
34
|
+
- [ ] Release notes (500 characters per language) ready — see [store-copy.md](store-copy.md).
|
|
35
|
+
|
|
36
|
+
## Tracks and rollout
|
|
37
|
+
|
|
38
|
+
- **Internal testing** for fast team builds, **closed testing** for a group of testers, **open testing** for a public beta, **production**.
|
|
39
|
+
- New personal developer accounts must run a closed test with a minimum number of testers for a minimum period before applying for production access; check the current numbers in the Play Console.
|
|
40
|
+
- Production updates go out as a **staged rollout** (a percentage of users). Increase it while the Android vitals (user-perceived crash rate and ANR rate) stay at the baseline of the previous version; **halt** the rollout when they regress. Halting stops new installs but does not revert users who already updated: the fix is a new build with a higher `versionCode`.
|
|
41
|
+
- **Managed publishing** holds approved changes until you publish them, useful to coordinate with a backend release.
|
|
42
|
+
|
|
43
|
+
## Automation
|
|
44
|
+
|
|
45
|
+
- `fastlane supply` (`upload_to_play_store`) uploads bundles, notes and listing text with a service account JSON key.
|
|
46
|
+
- The Gradle Play Publisher plugin (`com.github.triplet.play`) does the same from Gradle.
|
|
47
|
+
- The service account needs access granted in the Play Console (*Users and permissions*), limited to the app and the tracks it publishes to.
|