chore: move flutter client out of monorepo into _archive
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -1,440 +0,0 @@
|
||||
---
|
||||
name: accessibility
|
||||
description: Audit and improve web accessibility following WCAG 2.2 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible".
|
||||
license: MIT
|
||||
metadata:
|
||||
author: web-quality-skills
|
||||
version: "1.1"
|
||||
---
|
||||
|
||||
# Accessibility (a11y)
|
||||
|
||||
Comprehensive accessibility guidelines based on WCAG 2.2 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities.
|
||||
|
||||
## WCAG Principles: POUR
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| **P**erceivable | Content can be perceived through different senses |
|
||||
| **O**perable | Interface can be operated by all users |
|
||||
| **U**nderstandable | Content and interface are understandable |
|
||||
| **R**obust | Content works with assistive technologies |
|
||||
|
||||
## Conformance levels
|
||||
|
||||
| Level | Requirement | Target |
|
||||
|-------|-------------|--------|
|
||||
| **A** | Minimum accessibility | Must pass |
|
||||
| **AA** | Standard compliance | Should pass (legal requirement in many jurisdictions) |
|
||||
| **AAA** | Enhanced accessibility | Nice to have |
|
||||
|
||||
---
|
||||
|
||||
## Perceivable
|
||||
|
||||
### Text alternatives (1.1)
|
||||
|
||||
**Images require alt text:**
|
||||
```html
|
||||
<!-- ❌ Missing alt -->
|
||||
<img src="chart.png">
|
||||
|
||||
<!-- ✅ Descriptive alt -->
|
||||
<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales">
|
||||
|
||||
<!-- ✅ Decorative image (empty alt) -->
|
||||
<img src="decorative-border.png" alt="" role="presentation">
|
||||
|
||||
<!-- ✅ Complex image with longer description -->
|
||||
<figure>
|
||||
<img src="infographic.png" alt="2024 market trends infographic"
|
||||
aria-describedby="infographic-desc">
|
||||
<figcaption id="infographic-desc">
|
||||
<!-- Detailed description -->
|
||||
</figcaption>
|
||||
</figure>
|
||||
```
|
||||
|
||||
**Icon buttons need accessible names:**
|
||||
```html
|
||||
<!-- ❌ No accessible name -->
|
||||
<button><svg><!-- menu icon --></svg></button>
|
||||
|
||||
<!-- ✅ Using aria-label -->
|
||||
<button aria-label="Open menu">
|
||||
<svg aria-hidden="true"><!-- menu icon --></svg>
|
||||
</button>
|
||||
|
||||
<!-- ✅ Using visually hidden text -->
|
||||
<button>
|
||||
<svg aria-hidden="true"><!-- menu icon --></svg>
|
||||
<span class="visually-hidden">Open menu</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
**Visually hidden class:**
|
||||
```css
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Color contrast (1.4.3, 1.4.6)
|
||||
|
||||
| Text Size | AA minimum | AAA enhanced |
|
||||
|-----------|------------|--------------|
|
||||
| Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 |
|
||||
| Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 |
|
||||
| UI components & graphics | 3:1 | 3:1 |
|
||||
|
||||
```css
|
||||
/* ❌ Low contrast (2.5:1) */
|
||||
.low-contrast {
|
||||
color: #999;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ✅ Sufficient contrast (7:1) */
|
||||
.high-contrast {
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ✅ Focus states need contrast too */
|
||||
:focus-visible {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
**Don't rely on color alone:**
|
||||
```html
|
||||
<!-- ❌ Only color indicates error -->
|
||||
<input class="error-border">
|
||||
<style>.error-border { border-color: red; }</style>
|
||||
|
||||
<!-- ✅ Color + icon + text -->
|
||||
<div class="field-error">
|
||||
<input aria-invalid="true" aria-describedby="email-error">
|
||||
<span id="email-error" class="error-message">
|
||||
<svg aria-hidden="true"><!-- error icon --></svg>
|
||||
Please enter a valid email address
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Media alternatives (1.2)
|
||||
|
||||
```html
|
||||
<!-- Video with captions -->
|
||||
<video controls>
|
||||
<source src="video.mp4" type="video/mp4">
|
||||
<track kind="captions" src="captions.vtt" srclang="en" label="English" default>
|
||||
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions">
|
||||
</video>
|
||||
|
||||
<!-- Audio with transcript -->
|
||||
<audio controls>
|
||||
<source src="podcast.mp3" type="audio/mp3">
|
||||
</audio>
|
||||
<details>
|
||||
<summary>Transcript</summary>
|
||||
<p>Full transcript text...</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Operable
|
||||
|
||||
### Keyboard accessible (2.1)
|
||||
|
||||
**All functionality must be keyboard accessible:**
|
||||
```javascript
|
||||
// ❌ Only handles click
|
||||
element.addEventListener('click', handleAction);
|
||||
|
||||
// ✅ Handles both click and keyboard
|
||||
element.addEventListener('click', handleAction);
|
||||
element.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleAction();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**No keyboard traps.** Users must be able to Tab into and out of every component. Use the [modal focus trap pattern](references/A11Y-PATTERNS.md#modal-focus-trap) for dialogs—the native `<dialog>` element handles this automatically.
|
||||
|
||||
### Focus visible (2.4.7)
|
||||
|
||||
```css
|
||||
/* ❌ Never remove focus outlines */
|
||||
*:focus { outline: none; }
|
||||
|
||||
/* ✅ Use :focus-visible for keyboard-only focus */
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ✅ Or custom focus styles */
|
||||
button:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
### Focus not obscured (2.4.11) — new in 2.2
|
||||
|
||||
When an element receives keyboard focus, it must not be entirely hidden by other author-created content such as sticky headers, footers, or overlapping panels. At Level AAA (2.4.12), no part of the focused element may be hidden.
|
||||
|
||||
```css
|
||||
/* ✅ Account for sticky headers when scrolling to focused elements */
|
||||
:target {
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
/* ✅ Ensure focused items clear fixed/sticky bars */
|
||||
:focus {
|
||||
scroll-margin-top: 80px;
|
||||
scroll-margin-bottom: 60px;
|
||||
}
|
||||
```
|
||||
|
||||
### Skip links (2.4.1)
|
||||
|
||||
Provide a skip link so keyboard users can bypass repetitive navigation. See the [skip link pattern](references/A11Y-PATTERNS.md#skip-link) for full markup and styles.
|
||||
|
||||
### Target size (2.5.8) — new in 2.2
|
||||
|
||||
Interactive targets must be at least **24 × 24 CSS pixels** (AA). Exceptions: inline text links, elements where the browser controls the size, and targets where a 24px circle centered on the bounding box does not overlap another target.
|
||||
|
||||
```css
|
||||
/* ✅ Minimum target size */
|
||||
button,
|
||||
[role="button"],
|
||||
input[type="checkbox"] + label,
|
||||
input[type="radio"] + label {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
/* ✅ Comfortable target size (recommended 44×44) */
|
||||
.touch-target {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
```
|
||||
|
||||
### Dragging movements (2.5.7) — new in 2.2
|
||||
|
||||
Any action that requires dragging must have a single-pointer alternative (e.g., buttons, inputs). See the [dragging movements pattern](references/A11Y-PATTERNS.md#dragging-movements) for a sortable-list example.
|
||||
|
||||
### Timing (2.2)
|
||||
|
||||
```javascript
|
||||
// Allow users to extend time limits
|
||||
function showSessionWarning() {
|
||||
const modal = createModal({
|
||||
title: 'Session Expiring',
|
||||
content: 'Your session will expire in 2 minutes.',
|
||||
actions: [
|
||||
{ label: 'Extend session', action: extendSession },
|
||||
{ label: 'Log out', action: logout }
|
||||
],
|
||||
timeout: 120000
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Motion (2.3)
|
||||
|
||||
```css
|
||||
/* Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Understandable
|
||||
|
||||
### Page language (3.1.1)
|
||||
|
||||
```html
|
||||
<!-- ❌ No language specified -->
|
||||
<html>
|
||||
|
||||
<!-- ✅ Language specified -->
|
||||
<html lang="en">
|
||||
|
||||
<!-- ✅ Language changes within page -->
|
||||
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>
|
||||
```
|
||||
|
||||
### Consistent navigation (3.2.3)
|
||||
|
||||
```html
|
||||
<!-- Navigation should be consistent across pages -->
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
<li><a href="/" aria-current="page">Home</a></li>
|
||||
<li><a href="/products">Products</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Consistent help (3.2.6) — new in 2.2
|
||||
|
||||
If a help mechanism (contact info, chat widget, FAQ link, self-help option) is repeated across multiple pages, it must appear in the **same relative order** each time. Users who rely on consistent placement shouldn't have to hunt for help on every page.
|
||||
|
||||
### Form labels (3.3.2)
|
||||
|
||||
Every input needs a programmatically associated label. See the [form labels pattern](references/A11Y-PATTERNS.md#form-labels) for explicit, implicit, and instructional examples.
|
||||
|
||||
### Error handling (3.3.1, 3.3.3)
|
||||
|
||||
Announce errors to screen readers with `role="alert"` or `aria-live`, set `aria-invalid="true"` on invalid fields, and focus the first error on submit. See the [error handling pattern](references/A11Y-PATTERNS.md#error-handling) for full markup and JS.
|
||||
|
||||
### Redundant entry (3.3.7) — new in 2.2
|
||||
|
||||
Don't force users to re-enter information they already provided in the same session. Auto-populate from earlier steps, or let users select from previously entered values. Exceptions: security re-confirmation and content that has expired.
|
||||
|
||||
```html
|
||||
<!-- ✅ Auto-fill shipping address from billing -->
|
||||
<fieldset>
|
||||
<legend>Shipping address</legend>
|
||||
<label>
|
||||
<input type="checkbox" id="same-as-billing" checked>
|
||||
Same as billing address
|
||||
</label>
|
||||
<!-- Fields auto-populated when checked -->
|
||||
</fieldset>
|
||||
```
|
||||
|
||||
### Accessible authentication (3.3.8) — new in 2.2
|
||||
|
||||
Login flows must not rely on cognitive function tests (e.g., remembering a password, solving a puzzle) unless at least one of:
|
||||
- A copy-paste or autofill mechanism is available
|
||||
- An alternative method exists (e.g., passkey, SSO, email link)
|
||||
- The test uses object recognition or personal content (AA only; AAA removes this exception)
|
||||
|
||||
```html
|
||||
<!-- ✅ Allow paste in password fields -->
|
||||
<input type="password" id="password" autocomplete="current-password">
|
||||
|
||||
<!-- ✅ Offer passwordless alternatives -->
|
||||
<button type="button">Sign in with passkey</button>
|
||||
<button type="button">Email me a login link</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Robust
|
||||
|
||||
### ARIA usage (4.1.2)
|
||||
|
||||
**Prefer native elements:**
|
||||
```html
|
||||
<!-- ❌ ARIA role on div -->
|
||||
<div role="button" tabindex="0">Click me</div>
|
||||
|
||||
<!-- ✅ Native button -->
|
||||
<button>Click me</button>
|
||||
|
||||
<!-- ❌ ARIA checkbox -->
|
||||
<div role="checkbox" aria-checked="false">Option</div>
|
||||
|
||||
<!-- ✅ Native checkbox -->
|
||||
<label><input type="checkbox"> Option</label>
|
||||
```
|
||||
|
||||
**When ARIA is needed,** use the correct roles and states. See the [ARIA tabs pattern](references/A11Y-PATTERNS.md#aria-tabs) for a complete tablist example.
|
||||
|
||||
### Live regions (4.1.3)
|
||||
|
||||
Use `aria-live` regions to announce dynamic content changes without moving focus. See the [live regions pattern](references/A11Y-PATTERNS.md#live-regions-and-notifications) for markup and a `showNotification()` helper.
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
### Automated testing
|
||||
```bash
|
||||
# Lighthouse accessibility audit
|
||||
npx lighthouse https://example.com --only-categories=accessibility
|
||||
|
||||
# axe-core
|
||||
npm install @axe-core/cli -g
|
||||
axe https://example.com
|
||||
```
|
||||
|
||||
### Manual testing
|
||||
|
||||
- [ ] **Keyboard navigation:** Tab through entire page, use Enter/Space to activate
|
||||
- [ ] **Screen reader:** Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android)
|
||||
- [ ] **Zoom:** Content usable at 200% zoom
|
||||
- [ ] **High contrast:** Test with Windows High Contrast Mode
|
||||
- [ ] **Reduced motion:** Test with `prefers-reduced-motion: reduce`
|
||||
- [ ] **Focus order:** Logical and follows visual order
|
||||
- [ ] **Target size:** Interactive elements meet 24×24px minimum
|
||||
|
||||
See the [screen reader commands reference](references/A11Y-PATTERNS.md#screen-reader-commands) for VoiceOver and NVDA shortcuts.
|
||||
|
||||
---
|
||||
|
||||
## Common issues by impact
|
||||
|
||||
### Critical (fix immediately)
|
||||
1. Missing form labels
|
||||
2. Missing image alt text
|
||||
3. Insufficient color contrast
|
||||
4. Keyboard traps
|
||||
5. No focus indicators
|
||||
|
||||
### Serious (fix before launch)
|
||||
1. Missing page language
|
||||
2. Missing heading structure
|
||||
3. Non-descriptive link text
|
||||
4. Auto-playing media
|
||||
5. Missing skip links
|
||||
|
||||
### Moderate (fix soon)
|
||||
1. Missing ARIA labels on icons
|
||||
2. Inconsistent navigation
|
||||
3. Missing error identification
|
||||
4. Timing without controls
|
||||
5. Missing landmark regions
|
||||
|
||||
## References
|
||||
|
||||
- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/)
|
||||
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
||||
- [Deque axe Rules](https://dequeuniversity.com/rules/axe/)
|
||||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||
- [WCAG criteria reference](references/WCAG.md)
|
||||
- [Accessibility code patterns](references/A11Y-PATTERNS.md)
|
||||
@@ -1,233 +0,0 @@
|
||||
# Accessibility Code Patterns
|
||||
|
||||
Practical, copy-paste-ready patterns for common accessibility requirements. Each pattern is self-contained and linked from the main [SKILL.md](../SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Modal focus trap
|
||||
|
||||
Trap keyboard focus inside a modal dialog so Tab/Shift+Tab cycle through its focusable elements and Escape closes it.
|
||||
|
||||
```javascript
|
||||
function openModal(modal) {
|
||||
const focusableElements = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
modal.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
if (e.shiftKey && document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
firstElement.focus();
|
||||
}
|
||||
```
|
||||
|
||||
The native `<dialog>` element handles focus trapping automatically—prefer it when browser support allows.
|
||||
|
||||
---
|
||||
|
||||
## Skip link
|
||||
|
||||
Allows keyboard users to bypass repetitive navigation and jump straight to main content.
|
||||
|
||||
```html
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
<header><!-- navigation --></header>
|
||||
<main id="main-content" tabindex="-1">
|
||||
<!-- main content -->
|
||||
</main>
|
||||
</body>
|
||||
```
|
||||
|
||||
```css
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
Announce errors to screen readers and focus the first invalid field on submit.
|
||||
|
||||
```html
|
||||
<form novalidate>
|
||||
<div class="field" aria-live="polite">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email"
|
||||
aria-invalid="true"
|
||||
aria-describedby="email-error">
|
||||
<p id="email-error" class="error" role="alert">
|
||||
Please enter a valid email address (e.g., name@example.com)
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
```javascript
|
||||
form.addEventListener('submit', (e) => {
|
||||
const firstError = form.querySelector('[aria-invalid="true"]');
|
||||
if (firstError) {
|
||||
e.preventDefault();
|
||||
firstError.focus();
|
||||
|
||||
const errorSummary = document.getElementById('error-summary');
|
||||
errorSummary.textContent =
|
||||
`${errors.length} errors found. Please fix them and try again.`;
|
||||
errorSummary.focus();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Form labels
|
||||
|
||||
Every input needs an associated label—either explicit (`for`/`id`) or implicit (wrapping `<label>`).
|
||||
|
||||
```html
|
||||
<!-- ❌ No label association -->
|
||||
<input type="email" placeholder="Email">
|
||||
|
||||
<!-- ✅ Explicit label -->
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email"
|
||||
autocomplete="email" required>
|
||||
|
||||
<!-- ✅ Implicit label -->
|
||||
<label>
|
||||
Email address
|
||||
<input type="email" name="email" autocomplete="email" required>
|
||||
</label>
|
||||
|
||||
<!-- ✅ With instructions -->
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password"
|
||||
aria-describedby="password-requirements">
|
||||
<p id="password-requirements">
|
||||
Must be at least 8 characters with one number.
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dragging movements
|
||||
|
||||
Any action triggered by dragging must offer a single-pointer alternative (WCAG 2.5.7).
|
||||
|
||||
```html
|
||||
<!-- ❌ Drag-only reorder -->
|
||||
<ul class="sortable-list" draggable="true">
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</ul>
|
||||
|
||||
<!-- ✅ Drag + button alternatives -->
|
||||
<ul class="sortable-list">
|
||||
<li>
|
||||
<span>Item 1</span>
|
||||
<button aria-label="Move Item 1 up">↑</button>
|
||||
<button aria-label="Move Item 1 down">↓</button>
|
||||
</li>
|
||||
<li>
|
||||
<span>Item 2</span>
|
||||
<button aria-label="Move Item 2 up">↑</button>
|
||||
<button aria-label="Move Item 2 down">↓</button>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Also applies to sliders, map panning, colour pickers, and similar drag-based widgets—always provide an equivalent click/tap or keyboard path.
|
||||
|
||||
---
|
||||
|
||||
## ARIA tabs
|
||||
|
||||
Tabs require `role="tablist"`, `role="tab"`, and `role="tabpanel"` with proper `aria-selected`, `aria-controls`, and keyboard support.
|
||||
|
||||
```html
|
||||
<div role="tablist" aria-label="Product information">
|
||||
<button role="tab" id="tab-1" aria-selected="true"
|
||||
aria-controls="panel-1">Description</button>
|
||||
<button role="tab" id="tab-2" aria-selected="false"
|
||||
aria-controls="panel-2" tabindex="-1">Reviews</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
|
||||
<!-- Panel content -->
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
|
||||
<!-- Panel content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
Arrow keys should move focus between tabs; the active tab receives `tabindex="0"` while inactive tabs use `tabindex="-1"`.
|
||||
|
||||
---
|
||||
|
||||
## Live regions and notifications
|
||||
|
||||
Use `aria-live` to announce dynamic content changes to screen readers without moving focus.
|
||||
|
||||
```html
|
||||
<!-- Status updates (polite — waits for pause in speech) -->
|
||||
<div aria-live="polite" aria-atomic="true" class="status">
|
||||
<!-- Content updates announced to screen readers -->
|
||||
</div>
|
||||
|
||||
<!-- Urgent alerts (assertive — interrupts) -->
|
||||
<div role="alert" aria-live="assertive">
|
||||
<!-- Interrupts current announcement -->
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
function showNotification(message, type = 'polite') {
|
||||
const container = document.getElementById(`${type}-announcer`);
|
||||
container.textContent = '';
|
||||
requestAnimationFrame(() => {
|
||||
container.textContent = message;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Clear the container before writing to ensure the same message triggers a new announcement.
|
||||
|
||||
---
|
||||
|
||||
## Screen reader commands
|
||||
|
||||
Quick reference for the most common screen reader shortcuts.
|
||||
|
||||
| Action | VoiceOver (Mac) | NVDA (Windows) |
|
||||
|--------|-----------------|----------------|
|
||||
| Start/Stop | ⌘ + F5 | Ctrl + Alt + N |
|
||||
| Next item | VO + → | ↓ |
|
||||
| Previous item | VO + ← | ↑ |
|
||||
| Activate | VO + Space | Enter |
|
||||
| Headings list | VO + U, then arrows | H / Shift + H |
|
||||
| Links list | VO + U | K / Shift + K |
|
||||
@@ -1,191 +0,0 @@
|
||||
# WCAG 2.2 Quick Reference
|
||||
|
||||
## Success criteria by level
|
||||
|
||||
### Level A (minimum)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.1.1** Non-text Content | All images, icons have text alternatives |
|
||||
| **1.2.1** Audio-only/Video-only | Provide transcript or audio description |
|
||||
| **1.2.2** Captions | Video with audio has captions |
|
||||
| **1.2.3** Audio Description | Video has audio description |
|
||||
| **1.3.1** Info and Relationships | Information conveyed through presentation is available programmatically |
|
||||
| **1.3.2** Meaningful Sequence | Reading order is logical |
|
||||
| **1.3.3** Sensory Characteristics | Instructions don't rely solely on shape, color, size, location, orientation, or sound |
|
||||
| **1.4.1** Use of Color | Color is not the only visual means of conveying information |
|
||||
| **1.4.2** Audio Control | Audio playing automatically can be paused/stopped |
|
||||
| **2.1.1** Keyboard | All functionality available via keyboard |
|
||||
| **2.1.2** No Keyboard Trap | Keyboard focus can be moved away from any component |
|
||||
| **2.1.4** Character Key Shortcuts | Single-key shortcuts can be turned off or remapped |
|
||||
| **2.2.1** Timing Adjustable | Time limits can be extended |
|
||||
| **2.2.2** Pause, Stop, Hide | Moving/blinking content can be paused |
|
||||
| **2.3.1** Three Flashes | Nothing flashes more than 3 times per second |
|
||||
| **2.4.1** Bypass Blocks | Skip link or landmark navigation available |
|
||||
| **2.4.2** Page Titled | Pages have descriptive titles |
|
||||
| **2.4.3** Focus Order | Focus order preserves meaning |
|
||||
| **2.4.4** Link Purpose | Link purpose clear from link text or context |
|
||||
| **2.5.1** Pointer Gestures | Multi-point gestures have single-pointer alternatives |
|
||||
| **2.5.2** Pointer Cancellation | Down-event doesn't trigger action (use up-event or click) |
|
||||
| **2.5.3** Label in Name | Accessible name contains visible label text |
|
||||
| **2.5.4** Motion Actuation | Motion-triggered functions have alternatives |
|
||||
| **3.1.1** Language of Page | Default language specified in HTML |
|
||||
| **3.2.1** On Focus | Focus doesn't trigger unexpected changes |
|
||||
| **3.2.2** On Input | Input doesn't trigger unexpected changes |
|
||||
| **3.2.6** Consistent Help | Help mechanisms appear in the same relative order across pages |
|
||||
| **3.3.1** Error Identification | Input errors clearly described |
|
||||
| **3.3.2** Labels or Instructions | Form inputs have labels or instructions |
|
||||
| **3.3.7** Redundant Entry | Information previously entered is auto-populated or available to select |
|
||||
| **4.1.2** Name, Role, Value | UI components have accessible names and correct roles |
|
||||
|
||||
### Level AA (standard)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.2.4** Captions (Live) | Live audio has captions |
|
||||
| **1.2.5** Audio Description | Pre-recorded video has audio description |
|
||||
| **1.3.4** Orientation | Content doesn't restrict orientation |
|
||||
| **1.3.5** Identify Input Purpose | Input purpose can be programmatically determined |
|
||||
| **1.4.3** Contrast (Minimum) | 4.5:1 for normal text, 3:1 for large text |
|
||||
| **1.4.4** Resize Text | Text can be resized to 200% without loss of functionality |
|
||||
| **1.4.5** Images of Text | Text used instead of images of text |
|
||||
| **1.4.10** Reflow | Content reflows at 320px width without horizontal scroll |
|
||||
| **1.4.11** Non-text Contrast | UI components have 3:1 contrast |
|
||||
| **1.4.12** Text Spacing | Content adapts to text spacing changes |
|
||||
| **1.4.13** Content on Hover/Focus | Additional content is dismissible, hoverable, persistent |
|
||||
| **2.4.5** Multiple Ways | Multiple ways to find pages |
|
||||
| **2.4.6** Headings and Labels | Headings and labels are descriptive |
|
||||
| **2.4.7** Focus Visible | Focus indicator is visible |
|
||||
| **2.4.11** Focus Not Obscured (Minimum) | Focused element is not entirely hidden by author-created content |
|
||||
| **2.5.7** Dragging Movements | Dragging actions have single-pointer alternatives |
|
||||
| **2.5.8** Target Size (Minimum) | Interactive targets are at least 24×24 CSS pixels (with exceptions) |
|
||||
| **3.1.2** Language of Parts | Language changes are marked |
|
||||
| **3.2.3** Consistent Navigation | Navigation is consistent across pages |
|
||||
| **3.2.4** Consistent Identification | Same functionality uses same labels |
|
||||
| **3.3.3** Error Suggestion | Error corrections suggested when known |
|
||||
| **3.3.4** Error Prevention (Legal) | Actions can be reversed or confirmed |
|
||||
| **3.3.8** Accessible Authentication (Minimum) | No cognitive function test for login unless an alternative or assistance is provided |
|
||||
| **4.1.3** Status Messages | Status messages announced to screen readers |
|
||||
|
||||
### Level AAA (enhanced)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.4.6** Contrast (Enhanced) | 7:1 for normal text, 4.5:1 for large text |
|
||||
| **1.4.8** Visual Presentation | Foreground/background colors can be selected |
|
||||
| **1.4.9** Images of Text (No Exception) | No images of text |
|
||||
| **2.1.3** Keyboard (No Exception) | All functionality keyboard accessible |
|
||||
| **2.2.3** No Timing | No time limits |
|
||||
| **2.2.4** Interruptions | Interruptions can be postponed |
|
||||
| **2.2.5** Re-authenticating | Data preserved on re-authentication |
|
||||
| **2.2.6** Timeouts | Users warned about data loss from inactivity |
|
||||
| **2.3.2** Three Flashes | No content flashes more than 3 times |
|
||||
| **2.3.3** Animation from Interactions | Motion animation can be disabled |
|
||||
| **2.4.8** Location | User location within site is available |
|
||||
| **2.4.9** Link Purpose (Link Only) | Link purpose clear from link text alone |
|
||||
| **2.4.10** Section Headings | Sections have headings |
|
||||
| **2.4.12** Focus Not Obscured (Enhanced) | No part of the focused element is hidden by author-created content |
|
||||
| **2.4.13** Focus Appearance | Focus indicator has sufficient area, contrast, and is not obscured |
|
||||
| **3.1.3** Unusual Words | Definitions available for unusual words |
|
||||
| **3.1.4** Abbreviations | Abbreviations expanded |
|
||||
| **3.1.5** Reading Level | Alternative content for complex text |
|
||||
| **3.1.6** Pronunciation | Pronunciation available where needed |
|
||||
| **3.2.5** Change on Request | Changes initiated only by user |
|
||||
| **3.3.5** Help | Context-sensitive help available |
|
||||
| **3.3.6** Error Prevention (All) | All form submissions can be reviewed |
|
||||
| **3.3.9** Accessible Authentication (Enhanced) | No cognitive function test for login (no object or personal content recognition exceptions) |
|
||||
|
||||
## Common ARIA patterns
|
||||
|
||||
### Buttons
|
||||
```html
|
||||
<button>Label</button>
|
||||
<!-- or -->
|
||||
<button aria-label="Close dialog">×</button>
|
||||
```
|
||||
|
||||
### Links
|
||||
```html
|
||||
<a href="/page">Descriptive link text</a>
|
||||
<!-- External links -->
|
||||
<a href="https://external.com" target="_blank" rel="noopener">
|
||||
External site
|
||||
<span class="visually-hidden">(opens in new tab)</span>
|
||||
</a>
|
||||
```
|
||||
|
||||
### Form fields
|
||||
```html
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" aria-describedby="email-hint">
|
||||
<p id="email-hint">We'll never share your email.</p>
|
||||
```
|
||||
|
||||
### Error states
|
||||
```html
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
|
||||
<p id="email-error" role="alert">Please enter a valid email address.</p>
|
||||
```
|
||||
|
||||
### Navigation
|
||||
```html
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
<li><a href="/" aria-current="page">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Modals
|
||||
```html
|
||||
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
||||
<h2 id="dialog-title">Confirm Action</h2>
|
||||
<!-- content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Live regions
|
||||
```html
|
||||
<!-- Polite (waits for pause in speech) -->
|
||||
<div aria-live="polite">Status update here</div>
|
||||
|
||||
<!-- Assertive (interrupts immediately) -->
|
||||
<div aria-live="assertive" role="alert">Error message here</div>
|
||||
|
||||
<!-- Status (polite, implicit) -->
|
||||
<div role="status">Loading complete</div>
|
||||
```
|
||||
|
||||
## What changed from 2.1 to 2.2
|
||||
|
||||
| Change | Criterion | Level |
|
||||
|--------|-----------|-------|
|
||||
| **Removed** | 4.1.1 Parsing | A |
|
||||
| **Added** | 2.4.11 Focus Not Obscured (Minimum) | AA |
|
||||
| **Added** | 2.4.12 Focus Not Obscured (Enhanced) | AAA |
|
||||
| **Added** | 2.4.13 Focus Appearance | AAA |
|
||||
| **Added** | 2.5.7 Dragging Movements | AA |
|
||||
| **Added** | 2.5.8 Target Size (Minimum) | AA |
|
||||
| **Added** | 3.2.6 Consistent Help | A |
|
||||
| **Added** | 3.3.7 Redundant Entry | A |
|
||||
| **Added** | 3.3.8 Accessible Authentication (Minimum) | AA |
|
||||
| **Added** | 3.3.9 Accessible Authentication (Enhanced) | AAA |
|
||||
|
||||
## Testing tools
|
||||
|
||||
| Tool | Type | URL |
|
||||
|------|------|-----|
|
||||
| axe DevTools | Browser extension | [deque.com/axe](https://www.deque.com/axe/) |
|
||||
| WAVE | Browser extension | [wave.webaim.org](https://wave.webaim.org/) |
|
||||
| Lighthouse | Built into Chrome | DevTools → Lighthouse |
|
||||
| NVDA | Screen reader (Windows) | [nvaccess.org](https://www.nvaccess.org/) |
|
||||
| VoiceOver | Screen reader (Mac) | Built into macOS |
|
||||
| Colour Contrast Analyser | Desktop app | [tpgi.com](https://www.tpgi.com/color-contrast-checker/) |
|
||||
|
||||
## Sources
|
||||
|
||||
- [WCAG 2.2 W3C Recommendation](https://www.w3.org/TR/WCAG22/)
|
||||
- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/)
|
||||
- [What's New in WCAG 2.2](https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/)
|
||||
@@ -1,533 +0,0 @@
|
||||
---
|
||||
name: bash-defensive-patterns
|
||||
description: Master defensive Bash programming techniques for production-grade scripts. Use when writing robust shell scripts, CI/CD pipelines, or system utilities requiring fault tolerance and safety.
|
||||
---
|
||||
|
||||
# Bash Defensive Patterns
|
||||
|
||||
Comprehensive guidance for writing production-ready Bash scripts using defensive programming techniques, error handling, and safety best practices to prevent common pitfalls and ensure reliability.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Writing production automation scripts
|
||||
- Building CI/CD pipeline scripts
|
||||
- Creating system administration utilities
|
||||
- Developing error-resilient deployment automation
|
||||
- Writing scripts that must handle edge cases safely
|
||||
- Building maintainable shell script libraries
|
||||
- Implementing comprehensive logging and monitoring
|
||||
- Creating scripts that must work across different platforms
|
||||
|
||||
## Core Defensive Principles
|
||||
|
||||
### 1. Strict Mode
|
||||
|
||||
Enable bash strict mode at the start of every script to catch errors early.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail # Exit on error, unset variables, pipe failures
|
||||
```
|
||||
|
||||
**Key flags:**
|
||||
|
||||
- `set -E`: Inherit ERR trap in functions
|
||||
- `set -e`: Exit on any error (command returns non-zero)
|
||||
- `set -u`: Exit on undefined variable reference
|
||||
- `set -o pipefail`: Pipe fails if any command fails (not just last)
|
||||
|
||||
### 2. Error Trapping and Cleanup
|
||||
|
||||
Implement proper cleanup on script exit or error.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'echo "Error on line $LINENO"' ERR
|
||||
trap 'echo "Cleaning up..."; rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
# Script code here
|
||||
```
|
||||
|
||||
### 3. Variable Safety
|
||||
|
||||
Always quote variables to prevent word splitting and globbing issues.
|
||||
|
||||
```bash
|
||||
# Wrong - unsafe
|
||||
cp $source $dest
|
||||
|
||||
# Correct - safe
|
||||
cp "$source" "$dest"
|
||||
|
||||
# Required variables - fail with message if unset
|
||||
: "${REQUIRED_VAR:?REQUIRED_VAR is not set}"
|
||||
```
|
||||
|
||||
### 4. Array Handling
|
||||
|
||||
Use arrays safely for complex data handling.
|
||||
|
||||
```bash
|
||||
# Safe array iteration
|
||||
declare -a items=("item 1" "item 2" "item 3")
|
||||
|
||||
for item in "${items[@]}"; do
|
||||
echo "Processing: $item"
|
||||
done
|
||||
|
||||
# Reading output into array safely
|
||||
mapfile -t lines < <(some_command)
|
||||
readarray -t numbers < <(seq 1 10)
|
||||
```
|
||||
|
||||
### 5. Conditional Safety
|
||||
|
||||
Use `[[ ]]` for Bash-specific features, `[ ]` for POSIX.
|
||||
|
||||
```bash
|
||||
# Bash - safer
|
||||
if [[ -f "$file" && -r "$file" ]]; then
|
||||
content=$(<"$file")
|
||||
fi
|
||||
|
||||
# POSIX - portable
|
||||
if [ -f "$file" ] && [ -r "$file" ]; then
|
||||
content=$(cat "$file")
|
||||
fi
|
||||
|
||||
# Test for existence before operations
|
||||
if [[ -z "${VAR:-}" ]]; then
|
||||
echo "VAR is not set or is empty"
|
||||
fi
|
||||
```
|
||||
|
||||
## Fundamental Patterns
|
||||
|
||||
### Pattern 1: Safe Script Directory Detection
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Correctly determine script directory
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
SCRIPT_NAME="$(basename -- "${BASH_SOURCE[0]}")"
|
||||
|
||||
echo "Script location: $SCRIPT_DIR/$SCRIPT_NAME"
|
||||
```
|
||||
|
||||
### Pattern 2: Comprehensive Function Templat
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Prefix for functions: handle_*, process_*, check_*, validate_*
|
||||
# Include documentation and error handling
|
||||
|
||||
validate_file() {
|
||||
local -r file="$1"
|
||||
local -r message="${2:-File not found: $file}"
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo "ERROR: $message" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
process_files() {
|
||||
local -r input_dir="$1"
|
||||
local -r output_dir="$2"
|
||||
|
||||
# Validate inputs
|
||||
[[ -d "$input_dir" ]] || { echo "ERROR: input_dir not a directory" >&2; return 1; }
|
||||
|
||||
# Create output directory if needed
|
||||
mkdir -p "$output_dir" || { echo "ERROR: Cannot create output_dir" >&2; return 1; }
|
||||
|
||||
# Process files safely
|
||||
while IFS= read -r -d '' file; do
|
||||
echo "Processing: $file"
|
||||
# Do work
|
||||
done < <(find "$input_dir" -maxdepth 1 -type f -print0)
|
||||
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Safe Temporary File Handling
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'rm -rf -- "$TMPDIR"' EXIT
|
||||
|
||||
# Create temporary directory
|
||||
TMPDIR=$(mktemp -d) || { echo "ERROR: Failed to create temp directory" >&2; exit 1; }
|
||||
|
||||
# Create temporary files in directory
|
||||
TMPFILE1="$TMPDIR/temp1.txt"
|
||||
TMPFILE2="$TMPDIR/temp2.txt"
|
||||
|
||||
# Use temporary files
|
||||
touch "$TMPFILE1" "$TMPFILE2"
|
||||
|
||||
echo "Temp files created in: $TMPDIR"
|
||||
```
|
||||
|
||||
### Pattern 4: Robust Argument Parsing
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Default values
|
||||
VERBOSE=false
|
||||
DRY_RUN=false
|
||||
OUTPUT_FILE=""
|
||||
THREADS=4
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Options:
|
||||
-v, --verbose Enable verbose output
|
||||
-d, --dry-run Run without making changes
|
||||
-o, --output FILE Output file path
|
||||
-j, --jobs NUM Number of parallel jobs
|
||||
-h, --help Show this help message
|
||||
EOF
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-v|--verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
-d|--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-j|--jobs)
|
||||
THREADS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown option: $1" >&2
|
||||
usage 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required arguments
|
||||
[[ -n "$OUTPUT_FILE" ]] || { echo "ERROR: -o/--output is required" >&2; usage 1; }
|
||||
```
|
||||
|
||||
### Pattern 5: Structured Logging
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*" >&2
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
|
||||
}
|
||||
|
||||
log_debug() {
|
||||
if [[ "${DEBUG:-0}" == "1" ]]; then
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] DEBUG: $*" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage
|
||||
log_info "Starting script"
|
||||
log_debug "Debug information"
|
||||
log_warn "Warning message"
|
||||
log_error "Error occurred"
|
||||
```
|
||||
|
||||
### Pattern 6: Process Orchestration with Signals
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Track background processes
|
||||
PIDS=()
|
||||
|
||||
cleanup() {
|
||||
log_info "Shutting down..."
|
||||
|
||||
# Terminate all background processes
|
||||
for pid in "${PIDS[@]}"; do
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Wait for graceful shutdown
|
||||
for pid in "${PIDS[@]}"; do
|
||||
wait "$pid" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
trap cleanup SIGTERM SIGINT
|
||||
|
||||
# Start background tasks
|
||||
background_task &
|
||||
PIDS+=($!)
|
||||
|
||||
another_task &
|
||||
PIDS+=($!)
|
||||
|
||||
# Wait for all background processes
|
||||
wait
|
||||
```
|
||||
|
||||
### Pattern 7: Safe File Operations
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Use -i flag to move safely without overwriting
|
||||
safe_move() {
|
||||
local -r source="$1"
|
||||
local -r dest="$2"
|
||||
|
||||
if [[ ! -e "$source" ]]; then
|
||||
echo "ERROR: Source does not exist: $source" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -e "$dest" ]]; then
|
||||
echo "ERROR: Destination already exists: $dest" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv "$source" "$dest"
|
||||
}
|
||||
|
||||
# Safe directory cleanup
|
||||
safe_rmdir() {
|
||||
local -r dir="$1"
|
||||
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
echo "ERROR: Not a directory: $dir" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Use -I flag to prompt before rm (BSD/GNU compatible)
|
||||
rm -rI -- "$dir"
|
||||
}
|
||||
|
||||
# Atomic file writes
|
||||
atomic_write() {
|
||||
local -r target="$1"
|
||||
local -r tmpfile
|
||||
tmpfile=$(mktemp) || return 1
|
||||
|
||||
# Write to temp file first
|
||||
cat > "$tmpfile"
|
||||
|
||||
# Atomic rename
|
||||
mv "$tmpfile" "$target"
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 8: Idempotent Script Design
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Check if resource already exists
|
||||
ensure_directory() {
|
||||
local -r dir="$1"
|
||||
|
||||
if [[ -d "$dir" ]]; then
|
||||
log_info "Directory already exists: $dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$dir" || {
|
||||
log_error "Failed to create directory: $dir"
|
||||
return 1
|
||||
}
|
||||
|
||||
log_info "Created directory: $dir"
|
||||
}
|
||||
|
||||
# Ensure configuration state
|
||||
ensure_config() {
|
||||
local -r config_file="$1"
|
||||
local -r default_value="$2"
|
||||
|
||||
if [[ ! -f "$config_file" ]]; then
|
||||
echo "$default_value" > "$config_file"
|
||||
log_info "Created config: $config_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Rerunning script multiple times should be safe
|
||||
ensure_directory "/var/cache/myapp"
|
||||
ensure_config "/etc/myapp/config" "DEBUG=false"
|
||||
```
|
||||
|
||||
### Pattern 9: Safe Command Substitution
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Use $() instead of backticks
|
||||
name=$(<"$file") # Modern, safe variable assignment from file
|
||||
output=$(command -v python3) # Get command location safely
|
||||
|
||||
# Handle command substitution with error checking
|
||||
result=$(command -v node) || {
|
||||
log_error "node command not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
# For multiple lines
|
||||
mapfile -t lines < <(grep "pattern" "$file")
|
||||
|
||||
# NUL-safe iteration
|
||||
while IFS= read -r -d '' file; do
|
||||
echo "Processing: $file"
|
||||
done < <(find /path -type f -print0)
|
||||
```
|
||||
|
||||
### Pattern 10: Dry-Run Support
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
DRY_RUN="${DRY_RUN:-false}"
|
||||
|
||||
run_cmd() {
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "[DRY RUN] Would execute: $*"
|
||||
return 0
|
||||
fi
|
||||
|
||||
"$@"
|
||||
}
|
||||
|
||||
# Usage
|
||||
run_cmd cp "$source" "$dest"
|
||||
run_cmd rm "$file"
|
||||
run_cmd chown "$owner" "$target"
|
||||
```
|
||||
|
||||
## Advanced Defensive Techniques
|
||||
|
||||
### Named Parameters Pattern
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
process_data() {
|
||||
local input_file=""
|
||||
local output_dir=""
|
||||
local format="json"
|
||||
|
||||
# Parse named parameters
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--input=*)
|
||||
input_file="${1#*=}"
|
||||
;;
|
||||
--output=*)
|
||||
output_dir="${1#*=}"
|
||||
;;
|
||||
--format=*)
|
||||
format="${1#*=}"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown parameter: $1" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Validate required parameters
|
||||
[[ -n "$input_file" ]] || { echo "ERROR: --input is required" >&2; return 1; }
|
||||
[[ -n "$output_dir" ]] || { echo "ERROR: --output is required" >&2; return 1; }
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Checking
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
check_dependencies() {
|
||||
local -a missing_deps=()
|
||||
local -a required=("jq" "curl" "git")
|
||||
|
||||
for cmd in "${required[@]}"; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing_deps+=("$cmd")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing_deps[@]} -gt 0 ]]; then
|
||||
echo "ERROR: Missing required commands: ${missing_deps[*]}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_dependencies
|
||||
```
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Always use strict mode** - `set -Eeuo pipefail`
|
||||
2. **Quote all variables** - `"$variable"` prevents word splitting
|
||||
3. **Use [[]] conditionals** - More robust than [ ]
|
||||
4. **Implement error trapping** - Catch and handle errors gracefully
|
||||
5. **Validate all inputs** - Check file existence, permissions, formats
|
||||
6. **Use functions for reusability** - Prefix with meaningful names
|
||||
7. **Implement structured logging** - Include timestamps and levels
|
||||
8. **Support dry-run mode** - Allow users to preview changes
|
||||
9. **Handle temporary files safely** - Use mktemp, cleanup with trap
|
||||
10. **Design for idempotency** - Scripts should be safe to rerun
|
||||
11. **Document requirements** - List dependencies and minimum versions
|
||||
12. **Test error paths** - Ensure error handling works correctly
|
||||
13. **Use `command -v`** - Safer than `which` for checking executables
|
||||
14. **Prefer printf over echo** - More predictable across systems
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
name: dart-best-practices
|
||||
description: |-
|
||||
General best practices for Dart development.
|
||||
Covers code style, effective Dart, and language features.
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Dart Best Practices
|
||||
|
||||
## 1. When to use this skill
|
||||
Use this skill when:
|
||||
- Writing or reviewing Dart code.
|
||||
- Looking for guidance on idiomatic Dart usage.
|
||||
|
||||
## 2. Best Practices
|
||||
|
||||
### Multi-line Strings
|
||||
Prefer using multi-line strings (`'''`) over concatenating strings with `+` and
|
||||
`\n`, especially for large blocks of text like SQL queries, HTML, or
|
||||
PEM-encoded keys. This improves readability and avoids
|
||||
`lines_longer_than_80_chars` lint errors by allowing natural line breaks.
|
||||
|
||||
**Avoid:**
|
||||
```dart
|
||||
final pem = '-----BEGIN RSA PRIVATE KEY-----\n' +
|
||||
base64Encode(fullBytes) +
|
||||
'\n-----END RSA PRIVATE KEY-----';
|
||||
```
|
||||
|
||||
**Prefer:**
|
||||
```dart
|
||||
final pem = '''
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
${base64Encode(fullBytes)}
|
||||
-----END RSA PRIVATE KEY-----''';
|
||||
```
|
||||
|
||||
### Line Length
|
||||
Avoid lines longer than 80 characters, even in Markdown files and comments.
|
||||
This ensures code is readable in split-screen views and on smaller screens
|
||||
without horizontal scrolling.
|
||||
|
||||
**Prefer:**
|
||||
Target 80 characters for wrapping text. Exceptions are allowed for long URLs
|
||||
or identifiers that cannot be broken.
|
||||
|
||||
## Discovery
|
||||
|
||||
### Multi-line Strings
|
||||
To find candidates for multi-line strings, search for string concatenation
|
||||
with `+` involving newlines:
|
||||
- **Regex**: `['"]\s*\+\s*['"]`
|
||||
- **Regex**: `\+\s*['"].*\\n`
|
||||
|
||||
### Line Length
|
||||
- Rely on the `lines_longer_than_80_chars` lint from the analyzer.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **[dart-modern-features]**: For idiomatic
|
||||
usage of modern Dart features like Pattern Matching (useful for deep JSON
|
||||
extraction), Records, and Switch Expressions.
|
||||
|
||||
[dart-modern-features]: https://github.com/kevmoo/dash_skills/blob/main/.agent/skills/dart-modern-features/SKILL.md
|
||||
@@ -1,116 +0,0 @@
|
||||
---
|
||||
name: flutter-animations
|
||||
description: >-
|
||||
Add, fix, refactor, debug, test, or explain Flutter animations and motion
|
||||
effects. Use when working with implicit animations such as AnimatedContainer,
|
||||
AnimatedOpacity, AnimatedSwitcher, and TweenAnimationBuilder; explicit
|
||||
animations using AnimationController, Tween, CurvedAnimation, AnimatedWidget,
|
||||
AnimatedBuilder, and built-in transitions; Hero/shared-element route
|
||||
transitions; staggered or sequenced animations; physics-based motion,
|
||||
gestures, springs, flings, scroll physics, curves, performance, accessibility,
|
||||
reduced motion, and animation lifecycle bugs.
|
||||
metadata:
|
||||
author: Stanislav [MADTeacher] Chernyshev
|
||||
version: "2.0"
|
||||
---
|
||||
|
||||
# Flutter Animations
|
||||
|
||||
You are a Flutter motion implementation specialist. Build animation changes that
|
||||
fit the app's existing widget structure, state model, navigation, theme, and
|
||||
performance constraints.
|
||||
|
||||
## Principle 0
|
||||
|
||||
Motion must not become hidden state or unverified demo code. Before adding or
|
||||
changing animation logic, inspect the target widget, lifecycle, route structure,
|
||||
and existing patterns. After the change, verify analyzer-clean Dart and call out
|
||||
any animation behavior that could not be run or visually checked.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify the user's actual request: add a new animation, fix a broken one,
|
||||
refactor animation code, debug jank/lifecycle behavior, explain a pattern, or
|
||||
provide a standalone example.
|
||||
2. Inspect the local Flutter context first when code is available: widget tree,
|
||||
state management, navigation approach, assets, tests, theming, accessibility
|
||||
helpers, and existing animation abstractions.
|
||||
3. Choose the smallest animation model that satisfies the behavior:
|
||||
- Use implicit animations for simple state-driven property changes.
|
||||
- Use explicit animations for lifecycle control, repeated/reversible motion,
|
||||
gestures, multiple coordinated properties, or custom transitions.
|
||||
- Use Hero for shared elements across route transitions.
|
||||
- Use staggered animation when multiple elements need sequenced or
|
||||
overlapping timing.
|
||||
- Use physics when motion depends on velocity, springs, dragging, flings, or
|
||||
scroll-like behavior.
|
||||
4. Implement in the app's style. Preserve existing public APIs unless the user
|
||||
asked for a refactor. Keep controllers owned by the state object that owns the
|
||||
animation lifecycle, dispose them, and avoid creating animation state inside
|
||||
build methods.
|
||||
5. Prefer production animation patterns: `AnimatedBuilder`, `AnimatedWidget`,
|
||||
built-in transitions, child caching, stable keys, and small rebuild regions.
|
||||
Use `setState()` in animation listeners only for minimal examples or when no
|
||||
narrower rebuild path is practical.
|
||||
6. Respect accessibility and user settings. Check `MediaQuery.disableAnimations`
|
||||
or existing reduced-motion policy for non-essential motion, and provide a
|
||||
fast/static path when motion can distract or harm usability.
|
||||
7. Validate with the strongest available local checks. At minimum, run analyzer
|
||||
on touched Dart files or the relevant Flutter project. Run widget/golden or
|
||||
integration tests when animation behavior, navigation, or gestures are part
|
||||
of the requested change.
|
||||
|
||||
## Decision Guide
|
||||
|
||||
| Need | Default approach |
|
||||
|---|---|
|
||||
| One property or a small set of state-driven visual changes | `AnimatedContainer`, `AnimatedOpacity`, `AnimatedAlign`, `AnimatedPadding`, `AnimatedSwitcher`, or `TweenAnimationBuilder` |
|
||||
| Full lifecycle control, repeat/reverse/status, or gesture-driven values | `AnimationController` with `Tween`, `CurvedAnimation`, `AnimatedBuilder`, or `AnimatedWidget` |
|
||||
| Shared visual element between routes | `Hero` with stable unique tags and compatible source/destination widget trees |
|
||||
| List/menu/card reveal with offset timings | One controller with `Interval`s, or per-item animation only when lifecycle truly differs |
|
||||
| Spring/fling/drag or platform scroll feel | `fling`, `animateWith`, `SpringSimulation`, gesture velocity, or platform-specific `ScrollPhysics` |
|
||||
| Motion feels wrong but code works | Tune duration, curve, interval, easing, or reduced-motion behavior before adding complexity |
|
||||
|
||||
## Resource Routing
|
||||
|
||||
Read only the resources needed for the current task:
|
||||
|
||||
| Task | Read/use | Purpose |
|
||||
|---|---|---|
|
||||
| Simple state-driven animation or AnimatedSwitcher/TweenAnimationBuilder choice | `references/implicit.md`; optionally `assets/templates/implicit_animation.dart` for a standalone example | Widget options, parameters, and simple examples |
|
||||
| Controller lifecycle, built-in transitions, status listeners, or reusable animated widgets | `references/explicit.md`; optionally `assets/templates/explicit_animation.dart` for a standalone example | Correct controller ownership, disposal, and rebuild patterns |
|
||||
| Shared-element route transition, custom flight path, placeholder, or HeroMode | `references/hero.md`; optionally `assets/templates/hero_transition.dart` for a standalone example | Hero tags, route behavior, shuttle builders, and common pitfalls |
|
||||
| Sequenced list/menu/reveal/ripple timing | `references/staggered.md`; optionally `assets/templates/staggered_animation.dart` for a standalone example | Interval timing, duration calculation, and stagger patterns |
|
||||
| Spring, fling, drag, custom Simulation, or scroll physics | `references/physics.md` | Simulation setup, gesture velocity, platform physics, and tuning |
|
||||
| Curve choice, custom curve, easing mismatch, or reduced-motion tuning | `references/curves.md` | Curve selection, custom curves, and accessibility notes |
|
||||
|
||||
Templates are complete demo files, not drop-in production modules. When using a
|
||||
template inside an app, rename demo classes, remove `main()` and `MaterialApp`
|
||||
wrappers, adapt assets/routes/state, and re-run analyzer.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not invent missing routes, asset paths, theme tokens, gesture behavior, or
|
||||
state-management APIs. Inspect them or ask the user if they are absent.
|
||||
- Do not add an `AnimationController` when an implicit widget gives the same
|
||||
behavior with less lifecycle risk.
|
||||
- Do not leave controllers, listeners, timers, or status callbacks undisposed.
|
||||
- Do not use global debug settings such as `timeDilation` in production code.
|
||||
Mention them only as a local debugging aid.
|
||||
- Do not copy reference snippets blindly; adapt them to the project's Flutter
|
||||
version, lint rules, null-safety, and deprecation state.
|
||||
- Do not make accessibility optional for user-facing motion. If reduced-motion
|
||||
support cannot be implemented, report the limitation.
|
||||
|
||||
## Validation
|
||||
|
||||
- Run `dart format` or the project's formatter on edited Dart files when edits
|
||||
are made.
|
||||
- Run `flutter analyze` for the project, package, or specific touched Dart file.
|
||||
- Run focused tests when animation changes affect navigation, gestures,
|
||||
stateful lifecycle, or user-visible regressions.
|
||||
- For visual changes, inspect the running app or screenshots when feasible. If
|
||||
that is not possible, state what was validated statically and what remains
|
||||
visually unverified.
|
||||
- When updating templates, verify each template as `lib/main.dart` in a minimal
|
||||
Flutter project with `flutter analyze lib/main.dart`.
|
||||
@@ -1,345 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Explicit animation example using AnimationController
|
||||
///
|
||||
/// This example demonstrates:
|
||||
/// - AnimationController setup and lifecycle
|
||||
/// - Tween interpolation
|
||||
/// - AnimatedWidget pattern for reusable animations
|
||||
/// - AnimatedBuilder pattern for complex widgets
|
||||
/// - Animation status monitoring
|
||||
|
||||
void main() => runApp(const ExplicitAnimationApp());
|
||||
|
||||
class ExplicitAnimationApp extends StatelessWidget {
|
||||
const ExplicitAnimationApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Explicit Animation',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const LogoAnimationDemo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic explicit animation with addListener and setState
|
||||
class BasicExplicitAnimation extends StatefulWidget {
|
||||
const BasicExplicitAnimation({super.key});
|
||||
|
||||
@override
|
||||
State<BasicExplicitAnimation> createState() => _BasicExplicitAnimationState();
|
||||
}
|
||||
|
||||
class _BasicExplicitAnimationState extends State<BasicExplicitAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0, end: 300).animate(_controller)
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Basic Explicit')),
|
||||
body: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
height: _animation.value,
|
||||
width: _animation.value,
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// AnimatedWidget pattern - best for reusable animated widgets
|
||||
class AnimatedLogo extends AnimatedWidget {
|
||||
const AnimatedLogo({super.key, required Animation<double> animation})
|
||||
: super(listenable: animation);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animation = listenable as Animation<double>;
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
height: animation.value,
|
||||
width: animation.value,
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedWidgetDemo extends StatefulWidget {
|
||||
const AnimatedWidgetDemo({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedWidgetDemo> createState() => _AnimatedWidgetDemoState();
|
||||
}
|
||||
|
||||
class _AnimatedWidgetDemoState extends State<AnimatedWidgetDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0, end: 300).animate(_controller);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Animated Widget')),
|
||||
body: AnimatedLogo(animation: _animation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// AnimatedBuilder pattern - best for complex widgets
|
||||
class LogoWidget extends StatelessWidget {
|
||||
const LogoWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: const FlutterLogo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GrowTransition extends StatelessWidget {
|
||||
const GrowTransition({
|
||||
required this.child,
|
||||
required this.animation,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return SizedBox(
|
||||
height: animation.value,
|
||||
width: animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedBuilderDemo extends StatefulWidget {
|
||||
const AnimatedBuilderDemo({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
|
||||
}
|
||||
|
||||
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(begin: 0, end: 300).animate(_controller);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Animated Builder')),
|
||||
body: GrowTransition(animation: _animation, child: const LogoWidget()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiple simultaneous animations with status monitoring
|
||||
class MultiPropertyAnimation extends AnimatedWidget {
|
||||
const MultiPropertyAnimation({
|
||||
super.key,
|
||||
required Animation<double> animation,
|
||||
}) : super(listenable: animation);
|
||||
|
||||
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
|
||||
static final _sizeTween = Tween<double>(begin: 0, end: 300);
|
||||
static final _colorTween = ColorTween(begin: Colors.red, end: Colors.blue);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animation = listenable as Animation<double>;
|
||||
return Center(
|
||||
child: Opacity(
|
||||
opacity: _opacityTween.evaluate(animation),
|
||||
child: Container(
|
||||
height: _sizeTween.evaluate(animation),
|
||||
width: _sizeTween.evaluate(animation),
|
||||
decoration: BoxDecoration(
|
||||
color: _colorTween.evaluate(animation),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiPropertyDemo extends StatefulWidget {
|
||||
const MultiPropertyDemo({super.key});
|
||||
|
||||
@override
|
||||
State<MultiPropertyDemo> createState() => _MultiPropertyDemoState();
|
||||
}
|
||||
|
||||
class _MultiPropertyDemoState extends State<MultiPropertyDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeIn)
|
||||
..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reverse();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Multi Property')),
|
||||
body: MultiPropertyAnimation(animation: _animation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Main demo widget showing all examples
|
||||
class LogoAnimationDemo extends StatelessWidget {
|
||||
const LogoAnimationDemo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Explicit Animations')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const BasicExplicitAnimation(),
|
||||
),
|
||||
),
|
||||
child: const Text('Basic Explicit'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const AnimatedWidgetDemo(),
|
||||
),
|
||||
),
|
||||
child: const Text('Animated Widget'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const AnimatedBuilderDemo(),
|
||||
),
|
||||
),
|
||||
child: const Text('Animated Builder'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const MultiPropertyDemo(),
|
||||
),
|
||||
),
|
||||
child: const Text('Multi Property'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Hero animation example for shared element transitions
|
||||
///
|
||||
/// This example demonstrates:
|
||||
/// - Basic hero animation with matching tags
|
||||
/// - Custom PhotoHero widget
|
||||
/// - Navigation between screens with hero transition
|
||||
/// - MaterialRectCenterArcTween for aspect ratio preservation
|
||||
|
||||
void main() => runApp(const HeroAnimationApp());
|
||||
|
||||
class HeroAnimationApp extends StatelessWidget {
|
||||
const HeroAnimationApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Hero Animation',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const GalleryScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom hero widget for images
|
||||
class PhotoHero extends StatelessWidget {
|
||||
const PhotoHero({
|
||||
super.key,
|
||||
required this.photo,
|
||||
this.onTap,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
final String photo;
|
||||
final VoidCallback? onTap;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Hero(
|
||||
tag: photo,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Image.asset(
|
||||
photo,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: width,
|
||||
color: Colors.grey,
|
||||
child: const Icon(Icons.broken_image),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gallery screen showing multiple hero images
|
||||
class GalleryScreen extends StatelessWidget {
|
||||
const GalleryScreen({super.key});
|
||||
|
||||
static const List<String> _photos = [
|
||||
'images/photo1.png',
|
||||
'images/photo2.png',
|
||||
'images/photo3.png',
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Hero Gallery')),
|
||||
body: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
itemCount: _photos.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: PhotoHero(
|
||||
photo: _photos[index],
|
||||
width: 100,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => DetailScreen(photo: _photos[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Detail screen showing hero image in fullscreen
|
||||
class DetailScreen extends StatelessWidget {
|
||||
const DetailScreen({super.key, required this.photo});
|
||||
|
||||
final String photo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Detail')),
|
||||
body: Center(
|
||||
child: PhotoHero(
|
||||
photo: photo,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Example with circular hero (using placeholder instead of asset)
|
||||
class CircularHeroExample extends StatefulWidget {
|
||||
const CircularHeroExample({super.key});
|
||||
|
||||
@override
|
||||
State<CircularHeroExample> createState() => _CircularHeroExampleState();
|
||||
}
|
||||
|
||||
class _CircularHeroExampleState extends State<CircularHeroExample> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Circular Hero')),
|
||||
body: ListView(
|
||||
children: List.generate(3, (index) {
|
||||
final tag = 'circle-$index';
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Hero(
|
||||
tag: tag,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) =>
|
||||
CircularDetailScreen(tag: tag, index: index),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.blue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CircularDetailScreen extends StatelessWidget {
|
||||
const CircularDetailScreen({
|
||||
super.key,
|
||||
required this.tag,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
final String tag;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Circular Detail')),
|
||||
body: Center(
|
||||
child: Hero(
|
||||
tag: tag,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(150),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Image $index',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Implicit animation example showing basic fade animation
|
||||
///
|
||||
/// This example demonstrates:
|
||||
/// - AnimatedOpacity for fade in/out
|
||||
/// - State-driven animation triggers
|
||||
/// - Simple, declarative animation code
|
||||
|
||||
void main() => runApp(const ImplicitAnimationApp());
|
||||
|
||||
class ImplicitAnimationApp extends StatelessWidget {
|
||||
const ImplicitAnimationApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Implicit Animation',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const FadeExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FadeExample extends StatefulWidget {
|
||||
const FadeExample({super.key});
|
||||
|
||||
@override
|
||||
State<FadeExample> createState() => _FadeExampleState();
|
||||
}
|
||||
|
||||
class _FadeExampleState extends State<FadeExample> {
|
||||
bool _visible = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Implicit Fade')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: _visible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
child: const FlutterLogo(size: 100),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () => setState(() => _visible = !_visible),
|
||||
child: Text(_visible ? 'Fade Out' : 'Fade In'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// AnimatedContainer example - animating multiple properties
|
||||
class AnimatedContainerExample extends StatefulWidget {
|
||||
const AnimatedContainerExample({super.key});
|
||||
|
||||
@override
|
||||
State<AnimatedContainerExample> createState() =>
|
||||
_AnimatedContainerExampleState();
|
||||
}
|
||||
|
||||
class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Animated Container')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: _expanded ? 200 : 100,
|
||||
height: _expanded ? 200 : 100,
|
||||
decoration: BoxDecoration(
|
||||
color: _expanded ? Colors.blue : Colors.red,
|
||||
borderRadius: BorderRadius.circular(_expanded ? 20 : 8),
|
||||
),
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () => setState(() => _expanded = !_expanded),
|
||||
child: Text(_expanded ? 'Shrink' : 'Expand'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// TweenAnimationBuilder example - custom tween without boilerplate
|
||||
class TweenAnimationBuilderExample extends StatefulWidget {
|
||||
const TweenAnimationBuilderExample({super.key});
|
||||
|
||||
@override
|
||||
State<TweenAnimationBuilderExample> createState() =>
|
||||
_TweenAnimationBuilderExampleState();
|
||||
}
|
||||
|
||||
class _TweenAnimationBuilderExampleState
|
||||
extends State<TweenAnimationBuilderExample> {
|
||||
bool _animated = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Tween Animation Builder')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.scale(scale: value, child: child),
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(size: 100),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () => setState(() => _animated = !_animated),
|
||||
child: const Text('Animate'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Staggered animation example with multiple property animations
|
||||
///
|
||||
/// This example demonstrates:
|
||||
/// - Single AnimationController driving multiple animations
|
||||
/// - Interval-based timing for sequential/overlapping animations
|
||||
/// - Multiple tweens for different properties
|
||||
/// - Status monitoring for animation loops
|
||||
|
||||
void main() => runApp(const StaggeredAnimationApp());
|
||||
|
||||
class StaggeredAnimationApp extends StatelessWidget {
|
||||
const StaggeredAnimationApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Staggered Animation',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const StaggeredDemo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Staggered animation with fade, scale, and color changes
|
||||
class StaggeredAnimation extends StatelessWidget {
|
||||
StaggeredAnimation({super.key, required this.animation});
|
||||
|
||||
late final Animation<double> opacity = Tween<double>(begin: 0.0, end: 1.0)
|
||||
.animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.0, 0.1, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
late final Animation<double> width = Tween<double>(begin: 50.0, end: 150.0)
|
||||
.animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.125, 0.25, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
late final Animation<double> height = Tween<double>(begin: 50.0, end: 150.0)
|
||||
.animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.25, 0.375, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
late final Animation<BorderRadius?> borderRadius =
|
||||
BorderRadiusTween(
|
||||
begin: BorderRadius.circular(4),
|
||||
end: BorderRadius.circular(75),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.375, 0.5, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
late final Animation<Color?> color =
|
||||
ColorTween(begin: Colors.red, end: Colors.orange).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.5, 0.625, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
Widget _buildAnimation(BuildContext context, Widget? child) {
|
||||
return Center(
|
||||
child: Opacity(
|
||||
opacity: opacity.value,
|
||||
child: Container(
|
||||
width: width.value,
|
||||
height: height.value,
|
||||
decoration: BoxDecoration(
|
||||
color: color.value,
|
||||
border: Border.all(color: Colors.indigo[300]!, width: 3),
|
||||
borderRadius: borderRadius.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(animation: animation, builder: _buildAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
class StaggeredDemo extends StatefulWidget {
|
||||
const StaggeredDemo({super.key});
|
||||
|
||||
@override
|
||||
State<StaggeredDemo> createState() => _StaggeredDemoState();
|
||||
}
|
||||
|
||||
class _StaggeredDemoState extends State<StaggeredDemo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _playAnimation() async {
|
||||
try {
|
||||
await _controller.forward().orCancel;
|
||||
await _controller.reverse().orCancel;
|
||||
} on TickerCanceled {
|
||||
// Animation was canceled
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Staggered Animation')),
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _playAnimation,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 300,
|
||||
height: 300,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
border: Border.all(color: Colors.black.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: StaggeredAnimation(animation: _controller.view),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Staggered menu animation example
|
||||
class StaggeredMenuExample extends StatefulWidget {
|
||||
const StaggeredMenuExample({super.key});
|
||||
|
||||
@override
|
||||
State<StaggeredMenuExample> createState() => _StaggeredMenuExampleState();
|
||||
}
|
||||
|
||||
class _StaggeredMenuExampleState extends State<StaggeredMenuExample>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const _menuTitles = [
|
||||
'Declarative Style',
|
||||
'Premade Widgets',
|
||||
'Stateful Hot Reload',
|
||||
'Native Performance',
|
||||
'Great Community',
|
||||
];
|
||||
|
||||
static const _initialDelayTime = Duration(milliseconds: 50);
|
||||
static const _itemSlideTime = Duration(milliseconds: 250);
|
||||
static const _staggerTime = Duration(milliseconds: 50);
|
||||
|
||||
late AnimationController _controller;
|
||||
|
||||
final _animationDuration =
|
||||
_initialDelayTime + (_staggerTime * _menuTitles.length) + _itemSlideTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: _animationDuration,
|
||||
vsync: this,
|
||||
);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Staggered Menu')),
|
||||
body: ListView.builder(
|
||||
itemCount: _menuTitles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final start =
|
||||
_initialDelayTime.inMilliseconds.toDouble() +
|
||||
(_staggerTime.inMilliseconds.toDouble() * index);
|
||||
final end = start + _itemSlideTime.inMilliseconds.toDouble();
|
||||
final intervalStart =
|
||||
start / _animationDuration.inMilliseconds.toDouble();
|
||||
final intervalEnd =
|
||||
end / _animationDuration.inMilliseconds.toDouble();
|
||||
|
||||
final animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(intervalStart, intervalEnd, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(1, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: FadeTransition(opacity: animation, child: child),
|
||||
);
|
||||
},
|
||||
child: ListTile(title: Text(_menuTitles[index])),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,566 +0,0 @@
|
||||
# Curves Reference
|
||||
|
||||
Curves define the easing function for animations, controlling how values interpolate from start to end.
|
||||
|
||||
## Core Concept
|
||||
|
||||
A curve takes an input `t` (0.0 to 1.0, representing time) and outputs a value (typically 0.0 to 1.0, representing progress).
|
||||
|
||||
## Built-in Curves
|
||||
|
||||
### Linear
|
||||
|
||||
```dart
|
||||
Curves.linear
|
||||
```
|
||||
|
||||
No easing - straight line from 0 to 1. Use when you want constant speed.
|
||||
|
||||
### Ease (Sigmoid)
|
||||
|
||||
```dart
|
||||
Curves.ease
|
||||
Curves.easeIn
|
||||
Curves.easeOut
|
||||
Curves.easeInOut
|
||||
```
|
||||
|
||||
**Curves.ease** - Slow start and end, fast middle (most common)
|
||||
|
||||
**Curves.easeIn** - Slow start, fast end (accelerating)
|
||||
|
||||
**Curves.easeOut** - Fast start, slow end (decelerating)
|
||||
|
||||
**Curves.easeInOut** - Slow start and end, fast middle (combination)
|
||||
|
||||
### Cubic (Stronger Ease)
|
||||
|
||||
```dart
|
||||
Curves.easeInCubic
|
||||
Curves.easeOutCubic
|
||||
Curves.easeInOutCubic
|
||||
```
|
||||
|
||||
Stronger easing than basic ease. More dramatic slow/fast zones.
|
||||
|
||||
### Elastic (Bouncy)
|
||||
|
||||
```dart
|
||||
Curves.elasticIn
|
||||
Curves.elasticOut
|
||||
Curves.elasticInOut
|
||||
```
|
||||
|
||||
**Curves.elasticIn** - Bounces in (starts behind, shoots forward)
|
||||
|
||||
**Curves.elasticOut** - Bounces out (goes past, comes back)
|
||||
|
||||
**Curves.elasticInOut** - Bounces in and out
|
||||
|
||||
Example: Good for playful UI, notifications
|
||||
|
||||
### Bounce
|
||||
|
||||
```dart
|
||||
Curves.bounceIn
|
||||
Curves.bounceOut
|
||||
Curves.bounceInOut
|
||||
```
|
||||
|
||||
**Curves.bounceIn** - Bounces in (less bouncy than elastic)
|
||||
|
||||
**Curves.bounceOut** - Bounces out (less bouncy than elastic)
|
||||
|
||||
**Curves.bounceInOut** - Bounces in and out
|
||||
|
||||
Example: Good for feedback animations
|
||||
|
||||
### Back (Overshoot)
|
||||
|
||||
```dart
|
||||
Curves.backIn
|
||||
Curves.backOut
|
||||
Curves.backInOut
|
||||
```
|
||||
|
||||
**Curves.backIn** - Backs up before going in
|
||||
|
||||
**Curves.backOut** - Backs up before coming out
|
||||
|
||||
**Curves.backInOut** - Backs up on both ends
|
||||
|
||||
Example: Good for revealing content
|
||||
|
||||
### Decelerate (Fast then Slow)
|
||||
|
||||
```dart
|
||||
Curves.decelerate
|
||||
```
|
||||
|
||||
Fast start, very slow end. Similar to easeOut but more dramatic.
|
||||
|
||||
### Fast Out Slow In
|
||||
|
||||
```dart
|
||||
Curves.fastOutSlowIn
|
||||
```
|
||||
|
||||
Very fast start, very slow middle, very fast end. Dramatic.
|
||||
|
||||
### Custom Curves in Flutter API
|
||||
|
||||
Flutter includes specialized curves:
|
||||
|
||||
```dart
|
||||
// Material design curves
|
||||
Curves.fastLinearToSlowEaseIn
|
||||
Curves.slowMiddle
|
||||
|
||||
// Specific curves for certain transitions
|
||||
Curves.ease
|
||||
```
|
||||
|
||||
## Using Curves
|
||||
|
||||
### With Implicit Animation
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: _expanded ? 200 : 100,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### With Explicit Animation (CurvedAnimation)
|
||||
|
||||
```dart
|
||||
animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
```
|
||||
|
||||
### With Interval
|
||||
|
||||
```dart
|
||||
animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(
|
||||
0.0,
|
||||
0.5,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### With Curves.elasticOut
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
curve: Curves.elasticOut,
|
||||
transform: Matrix4.rotationZ(_rotated ? 0.5 : 0),
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
## Custom Curves
|
||||
|
||||
### Creating a Custom Curve
|
||||
|
||||
```dart
|
||||
import 'dart:math' as math;
|
||||
|
||||
class CustomCurve extends Curve {
|
||||
@override
|
||||
double transform(double t) {
|
||||
// t is 0.0 to 1.0
|
||||
// Return 0.0 to 1.0
|
||||
return math.pow(t, 2); // Quadratic ease in
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
curve: CustomCurve(),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: _expanded ? 200 : 100,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### Shake Curve
|
||||
|
||||
```dart
|
||||
class ShakeCurve extends Curve {
|
||||
@override
|
||||
double transform(double t) {
|
||||
return sin(t * pi * 2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Smooth Step Curve
|
||||
|
||||
```dart
|
||||
class SmoothStep extends Curve {
|
||||
final double steps;
|
||||
|
||||
const SmoothStep({this.steps = 5});
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
return (t * steps).floor() / steps;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Exponential Curve
|
||||
|
||||
```dart
|
||||
class ExponentialCurve extends Curve {
|
||||
final double exponent;
|
||||
|
||||
const ExponentialCurve({this.exponent = 2});
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
return math.pow(t, exponent);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Curve Composition
|
||||
|
||||
### Cubic Bezier
|
||||
|
||||
Flutter doesn't have built-in cubic bezier, but you can approximate:
|
||||
|
||||
```dart
|
||||
class CubicBezier extends Curve {
|
||||
final double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
|
||||
|
||||
const CubicBezier({
|
||||
required this.p0x,
|
||||
required this.p0y,
|
||||
required this.p1x,
|
||||
required this.p1y,
|
||||
required this.p2x,
|
||||
required this.p2y,
|
||||
required this.p3x,
|
||||
required this.p3y,
|
||||
});
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
// Simplified cubic bezier formula
|
||||
final u = 1 - t;
|
||||
return (u * u * u * p0y +
|
||||
3 * u * u * t * p1y +
|
||||
3 * u * t * t * p2y +
|
||||
t * t * t * p3y);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Combining Curves
|
||||
|
||||
```dart
|
||||
class CombinedCurve extends Curve {
|
||||
final Curve first;
|
||||
final Curve second;
|
||||
final double threshold;
|
||||
|
||||
const CombinedCurve({
|
||||
required this.first,
|
||||
required this.second,
|
||||
required this.threshold,
|
||||
});
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
if (t < threshold) {
|
||||
return first.transform(t / threshold) * threshold;
|
||||
}
|
||||
return threshold +
|
||||
second.transform((t - threshold) / (1 - threshold)) * (1 - threshold);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
curve: CombinedCurve(
|
||||
first: Curves.easeIn,
|
||||
second: Curves.easeOut,
|
||||
threshold: 0.5,
|
||||
),
|
||||
duration: const Duration(milliseconds: 500),
|
||||
width: _expanded ? 200 : 100,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
## Choosing the Right Curve
|
||||
|
||||
### Motion Design Guidelines
|
||||
|
||||
| Animation Type | Recommended Curve | Why |
|
||||
|---------------|-------------------|-----|
|
||||
| Fade in/out | easeInOut | Natural opacity change |
|
||||
| Size change | easeOut | Decelerate feels natural for growth |
|
||||
| Position slide | easeInOut | Smooth start and stop |
|
||||
| Rotation | easeInOut | Natural angular acceleration |
|
||||
| Color change | linear | Uniform color transition |
|
||||
| Scale | elasticOut | Playful, bouncy feel |
|
||||
| Reveal | backOut | Dramatic reveal effect |
|
||||
| Loading | linear or ease | Consistent speed |
|
||||
| Success | elasticOut | Celebratory feel |
|
||||
|
||||
### Platform Conventions
|
||||
|
||||
**iOS:**
|
||||
- Prefers subtle curves (ease, easeInOut)
|
||||
- Limited use of elastic/bounce
|
||||
- Consistent with system animations
|
||||
|
||||
**Android:**
|
||||
- Wider variety of curves
|
||||
- More use of elastic/bounce for feedback
|
||||
- Material design curves (easeIn, fastOutSlowIn)
|
||||
|
||||
**Web:**
|
||||
- CSS-like curves (ease, ease-in, ease-out, ease-in-out)
|
||||
- Limited use of complex curves
|
||||
|
||||
## Curve Combinations
|
||||
|
||||
### Multi-Stage Animation
|
||||
|
||||
```dart
|
||||
// Stage 1: Ease in (0.0 - 0.5)
|
||||
// Stage 2: Ease out (0.5 - 1.0)
|
||||
|
||||
animation1 = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
);
|
||||
|
||||
animation2 = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
||||
);
|
||||
```
|
||||
|
||||
### Staggered with Different Curves
|
||||
|
||||
```dart
|
||||
// Item 1: Bouncy
|
||||
item1Animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.3, curve: Curves.elasticOut),
|
||||
);
|
||||
|
||||
// Item 2: Smooth
|
||||
item2Animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.1, 0.4, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
// Item 3: Linear
|
||||
item3Animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.2, 0.5, curve: Curves.linear),
|
||||
);
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Curve Complexity
|
||||
|
||||
Simple curves (linear, ease) are faster:
|
||||
- Fewer calculations
|
||||
- Less GPU work
|
||||
- Consistent timing
|
||||
|
||||
Complex curves (elastic, bounce) are slower:
|
||||
- More calculations
|
||||
- Potential for frame drops
|
||||
- Variable timing
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
- Use simpler curves on low-end devices
|
||||
- Cache curve calculations if needed
|
||||
- Test performance on target devices
|
||||
- Profile with Flutter DevTools
|
||||
|
||||
## Debugging Curves
|
||||
|
||||
### Visualize Curve
|
||||
|
||||
```dart
|
||||
class CurveVisualizer extends StatelessWidget {
|
||||
final Curve curve;
|
||||
|
||||
const CurveVisualizer({super.key, required this.curve});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: _CurvePainter(curve: curve),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CurvePainter extends CustomPainter {
|
||||
final Curve curve;
|
||||
|
||||
const CurvePainter({required this.curve});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = Colors.blue
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final path = Path();
|
||||
for (double t = 0; t <= 1; t += 0.01) {
|
||||
final x = t * size.width;
|
||||
final y = size.height - (curve.transform(t) * size.height);
|
||||
if (t == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_CurvePainter oldDelegate) => false;
|
||||
}
|
||||
```
|
||||
|
||||
### Print Curve Values
|
||||
|
||||
```dart
|
||||
void printCurveValues(Curve curve) {
|
||||
print('Curve: $curve');
|
||||
for (double t = 0; t <= 1; t += 0.1) {
|
||||
print('t=$t, output=${curve.transform(t)}');
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
printCurveValues(Curves.easeInOut);
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Respecting User Preferences
|
||||
|
||||
```dart
|
||||
Curve getAdaptiveCurve(BuildContext context) {
|
||||
if (MediaQuery.of(context).disableAnimations) {
|
||||
return Curves.linear; // No easing when animations disabled
|
||||
}
|
||||
return Curves.easeInOut;
|
||||
}
|
||||
```
|
||||
|
||||
### Reduced Motion
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: MediaQuery.of(context).disableAnimations
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 300),
|
||||
curve: getAdaptiveCurve(context),
|
||||
width: _expanded ? 200 : 100,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Loading Spinner
|
||||
|
||||
```dart
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.linear, // Constant speed
|
||||
);
|
||||
```
|
||||
|
||||
### Success Checkmark
|
||||
|
||||
```dart
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.elasticOut, // Bouncy celebration
|
||||
);
|
||||
```
|
||||
|
||||
### Page Transition
|
||||
|
||||
```dart
|
||||
_enterAnimation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeIn, // Accelerate in
|
||||
);
|
||||
|
||||
_exitAnimation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut, // Decelerate out
|
||||
);
|
||||
```
|
||||
|
||||
### Modal Popup
|
||||
|
||||
```dart
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOutBack, // Dramatic reveal
|
||||
);
|
||||
```
|
||||
|
||||
## Curve Comparison
|
||||
|
||||
| Curve | Start | Middle | End | Feel |
|
||||
|-------|-------|--------|-----|------|
|
||||
| linear | Fast | Fast | Fast | Mechanical |
|
||||
| ease | Slow | Fast | Slow | Natural |
|
||||
| easeIn | Slow | Fast | Fast | Accelerating |
|
||||
| easeOut | Fast | Slow | Slow | Decelerating |
|
||||
| easeInOut | Slow | Fast | Slow | Smooth |
|
||||
| elasticIn | Back | Fast | Normal | Bouncy start |
|
||||
| elasticOut | Normal | Fast | Back | Bouncy end |
|
||||
| bounceIn | Bounce | Fast | Normal | Bouncy start |
|
||||
| bounceOut | Normal | Fast | Bounce | Bouncy end |
|
||||
| backIn | Back | Fast | Normal | Overshoot start |
|
||||
| backOut | Normal | Fast | Back | Overshoot end |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use appropriate curves for animation type
|
||||
- Test on real devices
|
||||
- Consider platform conventions
|
||||
- Respect accessibility settings
|
||||
- Profile performance with DevTools
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use elastic/bounce for everything (distracting)
|
||||
- Over-combine curves (confusing motion)
|
||||
- Ignore device performance
|
||||
- Use same curve for all animations (boring)
|
||||
- Forget to handle edge cases (t < 0 or t > 1)
|
||||
@@ -1,553 +0,0 @@
|
||||
# Explicit Animations Reference
|
||||
|
||||
Explicit animations provide full control using AnimationController, Tween, and animation status listeners.
|
||||
|
||||
## Core Components
|
||||
|
||||
### AnimationController
|
||||
|
||||
The heart of explicit animations. Manages animation lifecycle, value, and timing.
|
||||
|
||||
```dart
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
vsync: this, // Required TickerProvider
|
||||
lowerBound: 0.0, // Optional: default 0.0
|
||||
upperBound: 1.0, // Optional: default 1.0
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose(); // ALWAYS dispose!
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
**Lifecycle methods:**
|
||||
|
||||
```dart
|
||||
// Start animation forward
|
||||
_controller.forward();
|
||||
|
||||
// Start animation backward
|
||||
_controller.reverse();
|
||||
|
||||
// Stop animation
|
||||
_controller.stop();
|
||||
|
||||
// Repeat animation
|
||||
_controller.repeat();
|
||||
|
||||
// Animate to specific value
|
||||
await _controller.animateTo(0.5);
|
||||
|
||||
// Reset to beginning
|
||||
_controller.reset();
|
||||
|
||||
// Animate with physics simulation
|
||||
_controller.fling(velocity: 2.0);
|
||||
|
||||
// Animate with custom simulation
|
||||
_controller.animateWith(SpringSimulation(...));
|
||||
```
|
||||
|
||||
**Listening to changes:**
|
||||
|
||||
```dart
|
||||
_controller.addListener(() {
|
||||
// Value changed - rebuild needed
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
_controller.addStatusListener((status) {
|
||||
// Status changed
|
||||
switch (status) {
|
||||
case AnimationStatus.dismissed:
|
||||
print('Animation dismissed (at 0)');
|
||||
break;
|
||||
case AnimationStatus.forward:
|
||||
print('Animation moving forward');
|
||||
break;
|
||||
case AnimationStatus.reverse:
|
||||
print('Animation moving backward');
|
||||
break;
|
||||
case AnimationStatus.completed:
|
||||
print('Animation completed (at 1)');
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Tween
|
||||
|
||||
Interpolates between begin and end values.
|
||||
|
||||
```dart
|
||||
// Simple tween
|
||||
animation = Tween<double>(begin: 0, end: 300).animate(_controller);
|
||||
|
||||
// Color tween
|
||||
animation = ColorTween(
|
||||
begin: Colors.red,
|
||||
end: Colors.blue,
|
||||
).animate(_controller);
|
||||
|
||||
// Border radius tween
|
||||
animation = BorderRadiusTween(
|
||||
begin: BorderRadius.circular(4),
|
||||
end: BorderRadius.circular(75),
|
||||
).animate(_controller);
|
||||
|
||||
// Rect tween
|
||||
animation = RectTween(
|
||||
begin: Rect.fromLTWH(0, 0, 100, 100),
|
||||
end: Rect.fromLTWH(100, 100, 200, 200),
|
||||
).animate(_controller);
|
||||
```
|
||||
|
||||
**Common Tweens:**
|
||||
- `Tween<T>` - Generic tween (use for most types)
|
||||
- `ColorTween` - Color interpolation
|
||||
- `RectTween` - Rectangle interpolation
|
||||
- `IntTween` - Integer interpolation
|
||||
- `SizeTween` - Size interpolation
|
||||
- `OffsetTween` - Offset interpolation
|
||||
- `BorderRadiusTween` - Border radius interpolation
|
||||
|
||||
### CurvedAnimation
|
||||
|
||||
Applies a curve to transform animation values.
|
||||
|
||||
```dart
|
||||
animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
reverseCurve: Curves.easeIn, // Optional: different curve for reverse
|
||||
);
|
||||
```
|
||||
|
||||
**With Interval:**
|
||||
|
||||
```dart
|
||||
animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(
|
||||
0.25, // Start at 25% of controller
|
||||
0.75, // End at 75% of controller
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### ReverseAnimation
|
||||
|
||||
Reverses the parent animation.
|
||||
|
||||
```dart
|
||||
final forwardAnimation = Tween<double>(begin: 0, end: 1).animate(_controller);
|
||||
final reverseAnimation = ReverseAnimation(forwardAnimation);
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
### AnimatedWidget Pattern
|
||||
|
||||
Best for reusable animated widgets. Automatically handles rebuilds.
|
||||
|
||||
```dart
|
||||
class AnimatedLogo extends AnimatedWidget {
|
||||
const AnimatedLogo({super.key, required Animation<double> animation})
|
||||
: super(listenable: animation);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animation = listenable as Animation<double>;
|
||||
return Center(
|
||||
child: Container(
|
||||
height: animation.value,
|
||||
width: animation.value,
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedLogo(animation: _animation);
|
||||
}
|
||||
```
|
||||
|
||||
### AnimatedBuilder Pattern
|
||||
|
||||
Best for complex widgets. Separates animation logic from widget logic.
|
||||
|
||||
```dart
|
||||
class GrowTransition extends StatelessWidget {
|
||||
const GrowTransition({
|
||||
required this.child,
|
||||
required this.animation,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return SizedBox(
|
||||
height: animation.value,
|
||||
width: animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GrowTransition(
|
||||
animation: _animation,
|
||||
child: const LogoWidget(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Why pass child twice?**
|
||||
The `child` parameter to `AnimatedBuilder` is passed to the `builder` function, allowing the child to be built once and reused, rather than rebuilt every animation frame.
|
||||
|
||||
### Multiple Animations Pattern
|
||||
|
||||
Animate multiple properties from single controller.
|
||||
|
||||
```dart
|
||||
class MultiPropertyAnimation extends AnimatedWidget {
|
||||
const MultiPropertyAnimation({super.key, required Animation<double> animation})
|
||||
: super(listenable: animation);
|
||||
|
||||
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
|
||||
static final _sizeTween = Tween<double>(begin: 0, end: 300);
|
||||
static final _colorTween = ColorTween(
|
||||
begin: Colors.red,
|
||||
end: Colors.blue,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animation = listenable as Animation<double>;
|
||||
return Center(
|
||||
child: Opacity(
|
||||
opacity: _opacityTween.evaluate(animation),
|
||||
child: Container(
|
||||
height: _sizeTween.evaluate(animation),
|
||||
width: _sizeTween.evaluate(animation),
|
||||
decoration: BoxDecoration(
|
||||
color: _colorTween.evaluate(animation),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Transitions
|
||||
|
||||
Flutter provides pre-built transitions for common use cases.
|
||||
|
||||
### FadeTransition
|
||||
|
||||
```dart
|
||||
FadeTransition(
|
||||
opacity: _animation,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### ScaleTransition
|
||||
|
||||
```dart
|
||||
ScaleTransition(
|
||||
scale: _animation,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### SlideTransition
|
||||
|
||||
```dart
|
||||
SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: Offset(0, 1),
|
||||
end: Offset.zero,
|
||||
).animate(_animation),
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### SizeTransition
|
||||
|
||||
```dart
|
||||
SizeTransition(
|
||||
sizeFactor: _animation,
|
||||
axis: Axis.vertical,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### RotationTransition
|
||||
|
||||
```dart
|
||||
RotationTransition(
|
||||
turns: _animation,
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### DecoratedBoxTransition
|
||||
|
||||
```dart
|
||||
DecoratedBoxTransition(
|
||||
decoration: DecorationTween(
|
||||
begin: BoxDecoration(color: Colors.red),
|
||||
end: BoxDecoration(color: Colors.blue),
|
||||
).animate(_animation),
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### PositionedTransition
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
children: [
|
||||
PositionedTransition(
|
||||
rect: RelativeRectTween(
|
||||
begin: RelativeRect.fill(calculateRect(context, startPosition)),
|
||||
end: RelativeRect.fill(calculateRect(context, endPosition)),
|
||||
).animate(_animation),
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Animation Status Handling
|
||||
|
||||
### Basic Status Loop
|
||||
|
||||
```dart
|
||||
_animation.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reverse();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### One-way Animation
|
||||
|
||||
```dart
|
||||
_animation.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.stop(); // Or navigate, show dialog, etc.
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Reset and Repeat
|
||||
|
||||
```dart
|
||||
_animation.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reset();
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Always dispose AnimationController
|
||||
- Use `AnimatedWidget` or `AnimatedBuilder` instead of `setState()` in listeners
|
||||
- Minimize widget rebuilds during animation
|
||||
- Use static Tweens when possible (avoid recreating)
|
||||
- Profile with Flutter DevTools
|
||||
- Use `timeDilation` for debugging
|
||||
|
||||
### DON'T
|
||||
|
||||
- Forget to dispose controllers (memory leak)
|
||||
- Call `setState()` in animation listeners unnecessarily
|
||||
- Create complex widget trees inside animation builds
|
||||
- Animate too many widgets simultaneously
|
||||
- Use animation values directly in expensive operations
|
||||
|
||||
### Performance Pattern: RepaintBoundary
|
||||
|
||||
```dart
|
||||
RepaintBoundary(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return ExpensiveWidget();
|
||||
},
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Slow Animations
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
timeDilation = 10.0; // 10x slower
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Print Animation Values
|
||||
|
||||
```dart
|
||||
_controller.addListener(() {
|
||||
print('Animation value: ${_controller.value}');
|
||||
});
|
||||
|
||||
_animation.addStatusListener((status) {
|
||||
print('Animation status: $status');
|
||||
});
|
||||
```
|
||||
|
||||
### Flutter DevTools Performance Overlay
|
||||
|
||||
1. Run app in debug mode
|
||||
2. Press 'p' to toggle performance overlay
|
||||
3. Look for "GPU UI" and "GPU Raster" graphs
|
||||
4. Aim for 60fps (16.67ms per frame)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Fade In Then Slide Up
|
||||
|
||||
```dart
|
||||
_animation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: Offset(0, 0.5),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.3, 1.0, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Bounce Effect
|
||||
|
||||
```dart
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.elasticOut,
|
||||
);
|
||||
```
|
||||
|
||||
### Shake Effect
|
||||
|
||||
```dart
|
||||
class ShakeCurve extends Curve {
|
||||
@override
|
||||
double transform(double t) {
|
||||
return sin(t * pi * 2);
|
||||
}
|
||||
}
|
||||
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: ShakeCurve(),
|
||||
);
|
||||
```
|
||||
|
||||
### Parallax Scrolling
|
||||
|
||||
```dart
|
||||
class ParallaxWidget extends StatelessWidget {
|
||||
const ParallaxWidget({
|
||||
required this.animation,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Animation<double> animation;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, animation.value * 50),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison: Implicit vs Explicit
|
||||
|
||||
| Feature | Implicit | Explicit |
|
||||
|---------|----------|-----------|
|
||||
| Controller | None needed | AnimationController required |
|
||||
| Setup | Simple | More complex |
|
||||
| Control | Limited (duration, curve) | Full (lifecycle, status, physics) |
|
||||
| Multiple Properties | AnimatedContainer only | Unlimited |
|
||||
| Performance | Good | Excellent (with patterns) |
|
||||
| Reusability | Widget-based | Component-based |
|
||||
| State Monitoring | Limited (onEnd) | Full (status listeners) |
|
||||
|
||||
## When to Use Explicit Animations
|
||||
|
||||
Use explicit animations when:
|
||||
- Need full control over animation lifecycle
|
||||
- Animating multiple properties
|
||||
- Need to react to animation state changes
|
||||
- Creating reusable animation components
|
||||
- Need complex timing or sequencing
|
||||
- Performance is critical
|
||||
- Want physics-based or custom animations
|
||||
|
||||
Use implicit animations when:
|
||||
- Simple, one-off animations
|
||||
- Animation triggered by state change
|
||||
- No need for fine-grained control
|
||||
- Want simple, declarative code
|
||||
@@ -1,724 +0,0 @@
|
||||
# Hero Animations Reference
|
||||
|
||||
Hero animations create shared element transitions between screens, making elements appear to "fly" from one route to another.
|
||||
|
||||
## Core Concept
|
||||
|
||||
Use two `Hero` widgets with matching `tag` in different routes. Flutter automatically animates the transition between them.
|
||||
|
||||
## Basic Hero Animation
|
||||
|
||||
### Simple Image Transition
|
||||
|
||||
**Source route (list screen):**
|
||||
```dart
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const DetailScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
tag: 'hero-image',
|
||||
child: Image.asset('images/thumbnail.png'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
**Destination route (detail screen):**
|
||||
```dart
|
||||
Scaffold(
|
||||
appBar: AppBar(title: const Text('Detail')),
|
||||
body: GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Hero(
|
||||
tag: 'hero-image', // Same tag!
|
||||
child: Image.asset('images/thumbnail.png'),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Custom PhotoHero Widget
|
||||
|
||||
Reusable hero widget for images:
|
||||
|
||||
```dart
|
||||
class PhotoHero extends StatelessWidget {
|
||||
const PhotoHero({
|
||||
super.key,
|
||||
required this.photo,
|
||||
this.onTap,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
final String photo;
|
||||
final VoidCallback? onTap;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Hero(
|
||||
tag: photo,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Image.asset(
|
||||
photo,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in source route:**
|
||||
```dart
|
||||
class SourceScreen extends StatelessWidget {
|
||||
const SourceScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Gallery')),
|
||||
body: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return PhotoHero(
|
||||
photo: 'images/photo_$index.png',
|
||||
width: 100,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => DetailScreen(photo: 'images/photo_$index.png'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in destination route:**
|
||||
```dart
|
||||
class DetailScreen extends StatelessWidget {
|
||||
const DetailScreen({super.key, required this.photo});
|
||||
|
||||
final String photo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Detail')),
|
||||
body: Center(
|
||||
child: PhotoHero(
|
||||
photo: photo,
|
||||
width: 300,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hero Tag Best Practices
|
||||
|
||||
### Using Object as Tag
|
||||
|
||||
For unique, consistent tags:
|
||||
|
||||
```dart
|
||||
// Data model
|
||||
class Photo {
|
||||
final String url;
|
||||
final String id;
|
||||
|
||||
Photo({required this.url, required this.id});
|
||||
}
|
||||
|
||||
// Source
|
||||
Hero(
|
||||
tag: photo.id, // Use unique identifier
|
||||
child: Image.network(photo.url),
|
||||
)
|
||||
|
||||
// Destination
|
||||
Hero(
|
||||
tag: photo.id, // Same unique identifier
|
||||
child: Image.network(photo.url),
|
||||
)
|
||||
```
|
||||
|
||||
### Using Data Object as Tag
|
||||
|
||||
When data object is consistent:
|
||||
|
||||
```dart
|
||||
// Source
|
||||
Hero(
|
||||
tag: photo, // Photo object must be same instance or implement ==
|
||||
child: Image.network(photo.url),
|
||||
)
|
||||
|
||||
// Destination
|
||||
Hero(
|
||||
tag: photo, // Same Photo object
|
||||
child: Image.network(photo.url),
|
||||
)
|
||||
```
|
||||
|
||||
**Important:** If using object as tag, ensure proper `==` and `hashCode` implementation.
|
||||
|
||||
## Custom Hero Flight Path
|
||||
|
||||
### MaterialRectArcTween (Default)
|
||||
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: animation.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
createRectTween: (begin, end) {
|
||||
return MaterialRectArcTween(begin: begin, end: end);
|
||||
},
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
### MaterialRectCenterArcTween
|
||||
|
||||
Center-based interpolation (good for maintaining aspect ratio):
|
||||
|
||||
```dart
|
||||
static RectTween _createRectTween(Rect? begin, Rect? end) {
|
||||
return MaterialRectCenterArcTween(begin: begin, end: end);
|
||||
}
|
||||
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
createRectTween: _createRectTween,
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
### Custom RectTween
|
||||
|
||||
For complete control:
|
||||
|
||||
```dart
|
||||
class LinearRectTween extends Tween<Rect> {
|
||||
LinearRectTween({required Rect begin, required Rect end})
|
||||
: super(begin: begin, end: end);
|
||||
|
||||
@override
|
||||
Rect lerp(double t) => Rect.lerp(begin!, end!, t);
|
||||
}
|
||||
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
createRectTween: (begin, end) => LinearRectTween(begin: begin, end: end),
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
## Radial Hero Animation
|
||||
|
||||
Transform from circle to rectangle during transition.
|
||||
|
||||
### RadialExpansion Widget
|
||||
|
||||
```dart
|
||||
import 'dart:math' as math;
|
||||
|
||||
class RadialExpansion extends StatelessWidget {
|
||||
const RadialExpansion({
|
||||
super.key,
|
||||
required this.maxRadius,
|
||||
this.child,
|
||||
}) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);
|
||||
|
||||
final double maxRadius;
|
||||
final double clipRectSize;
|
||||
final Widget? child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipOval(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: clipRectSize,
|
||||
height: clipRectSize,
|
||||
child: ClipRect(child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Radial Photo Widget
|
||||
|
||||
```dart
|
||||
class RadialPhoto extends StatelessWidget {
|
||||
const RadialPhoto({
|
||||
super.key,
|
||||
required this.photo,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String photo;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).primaryColor.withValues(alpha: 0.25),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Image.asset(
|
||||
photo,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complete Radial Hero Example
|
||||
|
||||
```dart
|
||||
class RadialHeroAnimation extends StatelessWidget {
|
||||
const RadialHeroAnimation({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Radial Hero')),
|
||||
body: ListView(
|
||||
children: List.generate(6, (index) {
|
||||
final photo = 'images/photo_$index.png';
|
||||
return Hero(
|
||||
tag: photo,
|
||||
createRectTween: _createRectTween,
|
||||
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: RadialExpansion(
|
||||
maxRadius: 120,
|
||||
child: RadialPhoto(
|
||||
photo: photo,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => DetailScreen(photo: photo),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static RectTween _createRectTween(Rect? begin, Rect? end) {
|
||||
return MaterialRectCenterArcTween(begin: begin, end: end);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailScreen extends StatelessWidget {
|
||||
const DetailScreen({super.key, required this.photo});
|
||||
|
||||
final String photo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Detail')),
|
||||
body: Center(
|
||||
child: Hero(
|
||||
tag: photo,
|
||||
createRectTween: RadialHeroAnimation._createRectTween,
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
height: 300,
|
||||
child: RadialPhoto(
|
||||
photo: photo,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Placeholder During Flight
|
||||
|
||||
### flightShuttleBuilder
|
||||
|
||||
Customize hero appearance during flight:
|
||||
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
|
||||
// fromContext - source hero's context
|
||||
// toContext - destination hero's context
|
||||
// direction - HeroFlightDirection.push or pop
|
||||
// animation - Animation<double> for the flight
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: animation.value * math.pi,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
### Replace Entire Hero During Flight
|
||||
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'hero-image',
|
||||
flightShuttleBuilder: (flightContext, animation, direction, fromContext, toContext) {
|
||||
// Show different widget during flight
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: Colors.blue,
|
||||
);
|
||||
},
|
||||
child: Image.asset('image.png'),
|
||||
)
|
||||
```
|
||||
|
||||
## Transition Settings
|
||||
|
||||
### Animation Duration
|
||||
|
||||
```dart
|
||||
MaterialApp(
|
||||
// Set global hero animation duration
|
||||
theme: ThemeData(
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Custom PageTransitionBuilder
|
||||
|
||||
```dart
|
||||
class CustomHeroTransitionBuilder extends PageTransitionsBuilder {
|
||||
@override
|
||||
Widget buildTransitions<T>(
|
||||
PageRoute<T> route,
|
||||
BuildContext? secondaryContext,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeUpwardsPageTransitionsBuilder().buildTransitions(
|
||||
route,
|
||||
secondaryContext,
|
||||
child,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hero Mode
|
||||
|
||||
### Disable Hero Animations
|
||||
|
||||
```dart
|
||||
HeroMode(
|
||||
enabled: false, // Disable all child hero animations
|
||||
child: ListView(
|
||||
children: [
|
||||
Hero(tag: 'image1', child: Image.asset('1.png')),
|
||||
Hero(tag: 'image2', child: Image.asset('2.png')),
|
||||
],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Conditional Hero Mode
|
||||
|
||||
```dart
|
||||
HeroMode(
|
||||
enabled: !_disableAnimations,
|
||||
child: Hero(tag: 'image', child: Image.asset('image.png')),
|
||||
)
|
||||
```
|
||||
|
||||
## Debugging Hero Animations
|
||||
|
||||
### Slow Animation
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
timeDilation = 10.0; // 10x slower
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Visualize Hero Bounds
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
debugPaintSizeEnabled = true;
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Print Hero Tags
|
||||
|
||||
```dart
|
||||
class DebugHero extends StatelessWidget {
|
||||
const DebugHero({
|
||||
super.key,
|
||||
required this.tag,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Object tag;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print('Building Hero with tag: $tag');
|
||||
return Hero(tag: tag, child: child);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use unique, consistent tags (often the data object itself)
|
||||
- Keep hero widget trees similar between routes
|
||||
- Wrap images in `Material` with transparent color for "pop" effect
|
||||
- Use `timeDilation` to debug transitions
|
||||
- Consider `createRectTween` for custom flight paths
|
||||
- Use `flightShuttleBuilder` for custom flight appearance
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use duplicate tags (conflicts!)
|
||||
- Change hero structure significantly between routes (jarring transition)
|
||||
- Forget `Material` wrapper (no splash effect)
|
||||
- Use very large images in heroes (performance)
|
||||
- Ignore aspect ratio during transition (distortion)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Grid to Fullscreen Image
|
||||
|
||||
**Grid item:**
|
||||
```dart
|
||||
PhotoHero(
|
||||
photo: photo.url,
|
||||
width: 150, // Smaller in grid
|
||||
onTap: () => Navigator.push(..., detailScreen),
|
||||
)
|
||||
```
|
||||
|
||||
**Fullscreen:**
|
||||
```dart
|
||||
PhotoHero(
|
||||
photo: photo.url,
|
||||
width: MediaQuery.of(context).size.width, // Full width
|
||||
onTap: () => Navigator.pop(),
|
||||
)
|
||||
```
|
||||
|
||||
### List Header to Page Header
|
||||
|
||||
**List:**
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'header',
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Image.asset('header.jpg'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
**Page:**
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'header',
|
||||
child: SizedBox(
|
||||
height: 300, // Larger on detail page
|
||||
child: Image.asset('header.jpg'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Shared Element with Content Update
|
||||
|
||||
```dart
|
||||
// In both routes
|
||||
Hero(
|
||||
tag: 'card',
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset('image.png'),
|
||||
Text(showDetails ? 'Full description...' : 'Brief description'),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
- Optimize hero images (compress, lazy load)
|
||||
- Use `RepaintBoundary` around hero children if needed
|
||||
- Test on low-end devices
|
||||
- Profile with Flutter DevTools Performance overlay
|
||||
- Avoid complex widget trees inside hero
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Respect `MediaQuery.disableAnimations` setting
|
||||
- Consider alternative navigation for users who prefer no animations
|
||||
- Ensure hero content remains accessible during transition
|
||||
- Test with screen readers
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Multiple Heroes on Same Route
|
||||
|
||||
```dart
|
||||
class PhotoDetail extends StatelessWidget {
|
||||
const PhotoDetail({super.key, required this.photos});
|
||||
|
||||
final List<String> photos;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Main photo hero
|
||||
Positioned.fill(
|
||||
child: Hero(
|
||||
tag: photos[0],
|
||||
child: Image.asset(photos[0]),
|
||||
),
|
||||
),
|
||||
// Overlay hero (e.g., like button)
|
||||
Positioned(
|
||||
top: 16,
|
||||
right: 16,
|
||||
child: Hero(
|
||||
tag: 'like-button',
|
||||
child: Icon(Icons.favorite),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Heroes
|
||||
|
||||
```dart
|
||||
Hero(
|
||||
tag: 'parent',
|
||||
child: Card(
|
||||
child: Column(
|
||||
children: [
|
||||
Hero(
|
||||
tag: 'child-image',
|
||||
child: Image.asset('image.png'),
|
||||
),
|
||||
Hero(
|
||||
tag: 'child-title',
|
||||
child: Text('Title'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Hero with Scroll Views
|
||||
|
||||
```dart
|
||||
class ScrollableHero extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 300,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Hero(
|
||||
tag: 'header-image',
|
||||
child: Image.asset('header.jpg', fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Content
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,368 +0,0 @@
|
||||
# Implicit Animations Reference
|
||||
|
||||
Implicit animations automatically handle animations when widget properties change. No AnimationController or explicit state management needed.
|
||||
|
||||
## Core Concept
|
||||
|
||||
Implicitly animated widgets extend `ImplicitlyAnimatedWidget`. When you change a property (color, size, etc.), the widget automatically animates to the new value using a specified duration and curve.
|
||||
|
||||
## Available Widgets
|
||||
|
||||
### AnimatedContainer
|
||||
|
||||
Animates multiple properties simultaneously.
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: _expanded ? 200 : 100,
|
||||
height: _expanded ? 200 : 100,
|
||||
decoration: BoxDecoration(
|
||||
color: _expanded ? Colors.blue : Colors.red,
|
||||
borderRadius: BorderRadius.circular(_expanded ? 20 : 8),
|
||||
),
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
**Animatable properties:**
|
||||
- `width`, `height` - Size
|
||||
- `color` - Background color
|
||||
- `decoration` - BoxDecoration (includes border, shadow, image, etc.)
|
||||
- `padding` - Padding
|
||||
- `margin` - Margin (via decoration)
|
||||
- `transform` - Matrix4 transformation
|
||||
|
||||
### AnimatedOpacity
|
||||
|
||||
Fades a widget in and out.
|
||||
|
||||
```dart
|
||||
AnimatedOpacity(
|
||||
opacity: _visible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
onEnd: () => print('Animation complete'),
|
||||
child: const Text('Hello'),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedPadding
|
||||
|
||||
Animates padding changes.
|
||||
|
||||
```dart
|
||||
AnimatedPadding(
|
||||
padding: EdgeInsets.all(_padded ? 16.0 : 8.0),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.ease,
|
||||
child: const Card(child: Text('Padded content')),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedPositioned
|
||||
|
||||
Animates position within a Stack.
|
||||
|
||||
```dart
|
||||
Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
top: _top ? 0 : 100,
|
||||
left: _left ? 0 : 100,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedAlign
|
||||
|
||||
Animates alignment changes.
|
||||
|
||||
```dart
|
||||
AnimatedAlign(
|
||||
alignment: _aligned ? Alignment.topLeft : Alignment.bottomRight,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
curve: Curves.easeInOut,
|
||||
child: const Text('Align me'),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedContainer (Multiple Properties)
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
transform: Matrix4.rotationZ(_rotated ? 0.5 : 0),
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### TweenAnimationBuilder
|
||||
|
||||
Custom tween animation without creating a custom widget.
|
||||
|
||||
```dart
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0, end: 1),
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.scale(
|
||||
scale: value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedSwitcher
|
||||
|
||||
Cross-fades between two widgets.
|
||||
|
||||
```dart
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _showFirst
|
||||
? const Text('First')
|
||||
: const Text('Second'),
|
||||
)
|
||||
```
|
||||
|
||||
With custom size transition:
|
||||
```dart
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axis: Axis.vertical,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _expanded
|
||||
? const Text('Expanded content')
|
||||
: const Text('Collapsed content'),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedDefaultTextStyle
|
||||
|
||||
Animates text style changes.
|
||||
|
||||
```dart
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
style: TextStyle(
|
||||
fontSize: _large ? 32 : 16,
|
||||
color: _colored ? Colors.blue : Colors.black,
|
||||
fontWeight: _bold ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
child: const Text('Animated text'),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedPhysicalModel
|
||||
|
||||
Animates elevation, color, and shape with physical shadow.
|
||||
|
||||
```dart
|
||||
AnimatedPhysicalModel(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
shape: BoxShape.rectangle,
|
||||
elevation: _elevated ? 12 : 0,
|
||||
color: _elevated ? Colors.blue : Colors.grey[300]!,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text('Physical model'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### AnimatedTheme
|
||||
|
||||
Animates theme changes.
|
||||
|
||||
```dart
|
||||
AnimatedTheme(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
data: ThemeData(
|
||||
brightness: _darkMode ? Brightness.dark : Brightness.light,
|
||||
primaryColor: _darkMode ? Colors.blue[700] : Colors.blue,
|
||||
),
|
||||
child: const MyChildWidget(),
|
||||
)
|
||||
```
|
||||
|
||||
## Common Parameters
|
||||
|
||||
All implicit animations support these parameters:
|
||||
|
||||
```dart
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300), // Required: Animation duration
|
||||
curve: Curves.easeInOut, // Optional: Animation curve
|
||||
onEnd: () {}, // Optional: Completion callback
|
||||
child: Widget(), // The child widget to animate
|
||||
)
|
||||
```
|
||||
|
||||
### Duration
|
||||
|
||||
```dart
|
||||
const Duration(milliseconds: 300) // 0.3 seconds
|
||||
const Duration(seconds: 1) // 1 second
|
||||
const Duration(days: 1) // 1 day
|
||||
```
|
||||
|
||||
### Curves
|
||||
|
||||
Common curves from `Curves` class:
|
||||
|
||||
```dart
|
||||
Curves.linear // Linear (no easing)
|
||||
Curves.ease // Simple ease in and out
|
||||
Curves.easeIn // Slow start, fast end
|
||||
Curves.easeOut // Fast start, slow end
|
||||
Curves.easeInOut // Slow start and end, fast middle
|
||||
Curves.easeInCubic // Cubic ease in
|
||||
Curves.easeOutCubic // Cubic ease out
|
||||
Curves.easeInOutCubic // Cubic ease in and out
|
||||
Curves.elasticIn // Bounces in
|
||||
Curves.elasticOut // Bounces out
|
||||
Curves.elasticInOut // Bounces in and out
|
||||
Curves.bounceIn // Bounces in (more subtle)
|
||||
Curves.bounceOut // Bounces out (more subtle)
|
||||
```
|
||||
|
||||
Custom curve:
|
||||
```dart
|
||||
class ShakeCurve extends Curve {
|
||||
@override
|
||||
double transform(double t) => sin(t * pi * 2);
|
||||
}
|
||||
|
||||
// Usage
|
||||
AnimatedContainer(
|
||||
curve: ShakeCurve(),
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
### onEnd Callback
|
||||
|
||||
Trigger action when animation completes:
|
||||
|
||||
```dart
|
||||
AnimatedOpacity(
|
||||
opacity: _visible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
onEnd: () {
|
||||
if (!_visible) {
|
||||
// Animation finished fading out
|
||||
// Remove widget, navigate, etc.
|
||||
}
|
||||
},
|
||||
child: const Text('Hello'),
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use implicit animations for simple, one-off animations
|
||||
- Set appropriate durations for natural feel (200-500ms typical)
|
||||
- Use curves to make animations feel natural
|
||||
- Use `onEnd` callback for post-animation actions
|
||||
- Test animations on various devices
|
||||
- Consider `MediaQuery.disableAnimations` for accessibility
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use implicit animations for complex, multi-property sequences (use staggered instead)
|
||||
- Forget to set duration (defaults to instant change)
|
||||
- Use extremely long durations (> 1s) without good reason
|
||||
- Animate too many properties simultaneously (performance impact)
|
||||
- Nest implicit animations deeply (can cause jank)
|
||||
|
||||
## Performance Tips
|
||||
|
||||
- Implicit animations are optimized but still rebuild during animation
|
||||
- Avoid animating complex widget trees
|
||||
- Use `RepaintBoundary` around animated children to isolate repaints
|
||||
- Test performance with Flutter DevTools Performance overlay
|
||||
- Consider using `AnimatedBuilder` for complex widgets to minimize rebuilds
|
||||
|
||||
## Debugging
|
||||
|
||||
### Slow Animations
|
||||
|
||||
```dart
|
||||
// Slow all animations by 10x during development
|
||||
timeDilation = 10.0;
|
||||
```
|
||||
|
||||
### Show Repaint Rainbows
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
debugPaintSizeEnabled = true;
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Overlay
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
// In debug mode, press 'p' to toggle performance overlay
|
||||
```
|
||||
|
||||
## Comparison: Implicit vs Explicit
|
||||
|
||||
| Feature | Implicit | Explicit |
|
||||
|---------|----------|-----------|
|
||||
| Controller needed | No | Yes (AnimationController) |
|
||||
| Setup complexity | Low | Medium/High |
|
||||
| Reusability | Widget-based | Controller-based |
|
||||
| Multiple properties | AnimatedContainer only | Unlimited |
|
||||
| Lifecycle control | Limited | Full |
|
||||
| State monitoring | Limited (onEnd) | Full (addStatusListener) |
|
||||
| Performance | Good | Excellent (with proper patterns) |
|
||||
|
||||
## When to Use Implicit Animations
|
||||
|
||||
Use implicit animations when:
|
||||
- Animating a single property
|
||||
- Animation is triggered by state change
|
||||
- No need for fine-grained control
|
||||
- Want simple, declarative code
|
||||
- Animation is one-time or simple toggle
|
||||
|
||||
Use explicit animations when:
|
||||
- Need full control over animation lifecycle
|
||||
- Animating multiple properties
|
||||
- Need to react to animation state
|
||||
- Creating reusable animation components
|
||||
- Need complex timing or sequencing
|
||||
- Performance is critical
|
||||
@@ -1,700 +0,0 @@
|
||||
# Physics-Based Animations Reference
|
||||
|
||||
Physics-based animations create natural-feeling motion using simulations like springs, gravity, and momentum.
|
||||
|
||||
## Core Concept
|
||||
|
||||
Instead of linear interpolation, use physics simulations for realistic motion that responds naturally to forces like velocity and damping.
|
||||
|
||||
## Fling Animation
|
||||
|
||||
### Basic Fling
|
||||
|
||||
```dart
|
||||
class FlingWidget extends StatefulWidget {
|
||||
@override
|
||||
State<FlingWidget> createState() => _FlingWidgetState();
|
||||
}
|
||||
|
||||
class _FlingWidgetState extends State<FlingWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 1),
|
||||
vsync: this,
|
||||
);
|
||||
_controller.fling(velocity: 2.0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(_controller.value * 100, 0),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fling with Gesture
|
||||
|
||||
```dart
|
||||
class DraggableFling extends StatefulWidget {
|
||||
@override
|
||||
State<DraggableFling> createState() => _DraggableFlingState();
|
||||
}
|
||||
|
||||
class _DraggableFlingState extends State<DraggableFling>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
double _dragX = 0;
|
||||
Offset? _startPosition;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController.unbounded(vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handlePanStart(DragStartDetails details) {
|
||||
_controller.stop();
|
||||
_startPosition = details.globalPosition;
|
||||
}
|
||||
|
||||
void _handlePanUpdate(DragUpdateDetails details) {
|
||||
if (_startPosition != null) {
|
||||
setState(() {
|
||||
_dragX += details.delta.dx;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePanEnd(DragEndDetails details) {
|
||||
_controller.fling(velocity: details.velocity.pixelsPerSecond.dx / 1000);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onPanStart: _handlePanStart,
|
||||
onPanUpdate: _handlePanUpdate,
|
||||
onPanEnd: _handlePanEnd,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(_dragX + _controller.value, 0),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Spring Simulation
|
||||
|
||||
### Basic Spring
|
||||
|
||||
```dart
|
||||
class SpringAnimation extends StatefulWidget {
|
||||
@override
|
||||
State<SpringAnimation> createState() => _SpringAnimationState();
|
||||
}
|
||||
|
||||
class _SpringAnimationState extends State<SpringAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController.unbounded(vsync: this);
|
||||
|
||||
_controller.animateWith(
|
||||
SpringSimulation(
|
||||
spring: const SpringDescription(
|
||||
mass: 1.0,
|
||||
stiffness: 200.0,
|
||||
damping: 10.0,
|
||||
),
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
velocity: 0.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _controller.value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Spring Description Parameters
|
||||
|
||||
```dart
|
||||
SpringDescription(
|
||||
mass: 1.0, // Mass of the object (higher = more inertia)
|
||||
stiffness: 200.0, // Spring stiffness (higher = faster, stiffer)
|
||||
damping: 10.0, // Damping ratio (higher = less oscillation)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Mass:**
|
||||
- How "heavy" the object feels
|
||||
- Typical values: 0.5 - 5.0
|
||||
- Higher mass = more momentum, slower response
|
||||
|
||||
**Stiffness:**
|
||||
- How stiff the spring is
|
||||
- Typical values: 100 - 500
|
||||
- Higher stiffness = faster oscillation
|
||||
- Lower stiffness = slower, more "bouncy"
|
||||
|
||||
**Damping:**
|
||||
- How quickly oscillation stops
|
||||
- Typical values: 5 - 20
|
||||
- Higher damping = less bounce
|
||||
- Lower damping = more bounce
|
||||
- Critical damping: ~15-18
|
||||
|
||||
### Spring Presets
|
||||
|
||||
**Bouncy:**
|
||||
```dart
|
||||
SpringDescription(
|
||||
mass: 0.5,
|
||||
stiffness: 300,
|
||||
damping: 8,
|
||||
)
|
||||
```
|
||||
|
||||
**Snappy:**
|
||||
```dart
|
||||
SpringDescription(
|
||||
mass: 1.0,
|
||||
stiffness: 400,
|
||||
damping: 18,
|
||||
)
|
||||
```
|
||||
|
||||
**Gentle:**
|
||||
```dart
|
||||
SpringDescription(
|
||||
mass: 2.0,
|
||||
stiffness: 150,
|
||||
damping: 20,
|
||||
)
|
||||
```
|
||||
|
||||
## Gravity Simulation
|
||||
|
||||
### Falling Animation
|
||||
|
||||
```dart
|
||||
class GravityAnimation extends StatefulWidget {
|
||||
@override
|
||||
State<GravityAnimation> createState() => _GravityAnimationState();
|
||||
}
|
||||
|
||||
class _GravityAnimationState extends State<GravityAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController.unbounded(vsync: this);
|
||||
|
||||
_controller.animateWith(
|
||||
GravitySimulation(
|
||||
acceleration: 980, // pixels/s²
|
||||
distance: 500, // pixels to fall
|
||||
startDistance: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, _controller.value),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scroll Physics
|
||||
|
||||
### Bouncing Scroll
|
||||
|
||||
```dart
|
||||
CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Items
|
||||
]),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Clamping Scroll
|
||||
|
||||
```dart
|
||||
CustomScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Items
|
||||
]),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Fixed Scroll
|
||||
|
||||
```dart
|
||||
CustomScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Items
|
||||
]),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Platform-Adaptive Scroll
|
||||
|
||||
```dart
|
||||
CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Items
|
||||
]),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Custom Simulation
|
||||
|
||||
### Creating Custom Simulation
|
||||
|
||||
```dart
|
||||
class CustomSimulation extends Simulation {
|
||||
final double target;
|
||||
|
||||
CustomSimulation({required this.target});
|
||||
|
||||
@override
|
||||
double x(double time) {
|
||||
// Calculate position at given time
|
||||
return target * (1 - math.exp(-time / 0.5));
|
||||
}
|
||||
|
||||
@override
|
||||
double dx(double time) {
|
||||
// Calculate velocity (derivative) at given time
|
||||
return target * math.exp(-time / 0.5) / 0.5;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isDone(double time) {
|
||||
return dx(time).abs() < 0.01;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
_controller.animateWith(
|
||||
CustomSimulation(target: 1.0),
|
||||
);
|
||||
```
|
||||
|
||||
### Decay Simulation
|
||||
|
||||
```dart
|
||||
class DecaySimulation extends Simulation {
|
||||
final double velocity;
|
||||
|
||||
DecaySimulation({required this.velocity});
|
||||
|
||||
@override
|
||||
double x(double time) {
|
||||
return velocity * (1 - math.exp(-time));
|
||||
}
|
||||
|
||||
@override
|
||||
double dx(double time) {
|
||||
return velocity * math.exp(-time);
|
||||
}
|
||||
|
||||
@override
|
||||
bool isDone(double time) {
|
||||
return dx(time).abs() < 0.1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Combining Physics and Animation
|
||||
|
||||
### Spring with Fallback
|
||||
|
||||
```dart
|
||||
void animateWithSpring({
|
||||
required AnimationController controller,
|
||||
required double start,
|
||||
required double end,
|
||||
}) {
|
||||
try {
|
||||
controller.animateWith(
|
||||
SpringSimulation(
|
||||
spring: const SpringDescription(
|
||||
mass: 1.0,
|
||||
stiffness: 200.0,
|
||||
damping: 15.0,
|
||||
),
|
||||
start: start,
|
||||
end: end,
|
||||
velocity: 0.0,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// Fallback to linear animation
|
||||
controller.animateTo(end);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Spring with Completion Callback
|
||||
|
||||
```dart
|
||||
_controller.animateWith(
|
||||
SpringSimulation(
|
||||
spring: const SpringDescription(
|
||||
mass: 1.0,
|
||||
stiffness: 200.0,
|
||||
damping: 15.0,
|
||||
),
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
velocity: 0.0,
|
||||
),
|
||||
);
|
||||
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
// Animation complete
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Physics for Gestures
|
||||
|
||||
### Swipe to Dismiss with Physics
|
||||
|
||||
```dart
|
||||
class DismissibleWithPhysics extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
const DismissibleWithPhysics({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DismissibleWithPhysics> createState() => _DismissibleWithPhysicsState();
|
||||
}
|
||||
|
||||
class _DismissibleWithPhysicsState extends State<DismissibleWithPhysics>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
double _dragX = 0;
|
||||
bool _isDismissing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController.unbounded(vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handlePanEnd(DragEndDetails details) {
|
||||
final velocity = details.velocity.pixelsPerSecond.dx;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
if (velocity.abs() > 500 || _dragX.abs() > screenWidth / 3) {
|
||||
// Dismiss
|
||||
_controller.fling(velocity: velocity / screenWidth);
|
||||
_isDismissing = true;
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed && _isDismissing) {
|
||||
widget.onDismiss();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Spring back
|
||||
_controller.animateWith(
|
||||
SpringSimulation(
|
||||
spring: const SpringDescription(
|
||||
mass: 1.0,
|
||||
stiffness: 300.0,
|
||||
damping: 15.0,
|
||||
),
|
||||
start: _dragX,
|
||||
end: 0,
|
||||
velocity: velocity,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
if (!_controller.isAnimating) {
|
||||
setState(() {
|
||||
_dragX += details.delta.dx;
|
||||
});
|
||||
}
|
||||
},
|
||||
onPanEnd: _handlePanEnd,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(_dragX + _controller.value, 0),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Simulation Complexity
|
||||
|
||||
Physics simulations can be computationally expensive. Optimize by:
|
||||
|
||||
- Using simpler simulations when possible (e.g., linear interpolation)
|
||||
- Reducing simulation resolution
|
||||
- Caching simulation results
|
||||
- Using built-in physics widgets
|
||||
|
||||
### Spring Tuning
|
||||
|
||||
Finding the right spring parameters can be trial and error:
|
||||
|
||||
1. Start with moderate values (mass: 1.0, stiffness: 200, damping: 15)
|
||||
2. Adjust for feel:
|
||||
- Too slow? Increase stiffness or decrease mass
|
||||
- Too bouncy? Increase damping
|
||||
- Not bouncy enough? Decrease damping or stiffness
|
||||
3. Test on real devices
|
||||
|
||||
## Debugging
|
||||
|
||||
### Visualize Physics
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
timeDilation = 5.0; // Slow down physics
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Print Simulation Values
|
||||
|
||||
```dart
|
||||
_controller.addListener(() {
|
||||
print('Position: ${_controller.value}');
|
||||
});
|
||||
|
||||
_controller.addStatusListener((status) {
|
||||
print('Status: $status');
|
||||
});
|
||||
```
|
||||
|
||||
### Physics Inspector
|
||||
|
||||
Use Flutter DevTools to profile physics animations:
|
||||
1. Open Performance overlay
|
||||
2. Look for frame drops during physics simulations
|
||||
3. Identify expensive calculations
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use physics simulations for natural-feeling motion
|
||||
- Tune spring parameters for desired feel
|
||||
- Test on various devices and screen sizes
|
||||
- Profile performance with DevTools
|
||||
- Provide fallback animations for physics failures
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use complex physics when simple curves suffice
|
||||
- Forget to handle simulation completion
|
||||
- Use overly bouncy animations (distracting)
|
||||
- Ignore accessibility (respect disable animations preference)
|
||||
- Assume physics simulation completes instantly
|
||||
|
||||
## Common Physics Patterns
|
||||
|
||||
### Bouncy Button Press
|
||||
|
||||
```dart
|
||||
_controller.animateWith(
|
||||
SpringSimulation(
|
||||
spring: const SpringDescription(
|
||||
mass: 0.5,
|
||||
stiffness: 500,
|
||||
damping: 10,
|
||||
),
|
||||
start: 1.0,
|
||||
end: 0.9,
|
||||
velocity: 0,
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Swipeable Card
|
||||
|
||||
```dart
|
||||
_controller.fling(
|
||||
velocity: details.velocity.pixelsPerSecond.dx / 1000,
|
||||
);
|
||||
```
|
||||
|
||||
### Pull to Refresh
|
||||
|
||||
```dart
|
||||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
// Refresh data
|
||||
},
|
||||
child: ListView(...),
|
||||
)
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Respect `MediaQuery.disableAnimations` setting
|
||||
- Provide non-physics alternatives
|
||||
- Ensure physics animations don't cause motion sickness
|
||||
- Test with reduced motion settings
|
||||
|
||||
## Platform-Specific Physics
|
||||
|
||||
### Adaptive Scroll Physics
|
||||
|
||||
```dart
|
||||
class AdaptiveScrollPhysics extends ScrollPhysics {
|
||||
const AdaptiveScrollPhysics({super.parent});
|
||||
|
||||
@override
|
||||
AdaptiveScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return AdaptiveScrollPhysics(parent: buildParent(ancestor));
|
||||
}
|
||||
|
||||
@override
|
||||
SpringDescription get spring => Platform.isIOS
|
||||
? const SpringDescription(
|
||||
mass: 0.5,
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
)
|
||||
: const SpringDescription(
|
||||
mass: 0.8,
|
||||
stiffness: 150,
|
||||
damping: 15,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```dart
|
||||
ListView(
|
||||
physics: const AdaptiveScrollPhysics(),
|
||||
children: [...],
|
||||
)
|
||||
```
|
||||
@@ -1,726 +0,0 @@
|
||||
# Staggered Animations Reference
|
||||
|
||||
Staggered animations run multiple animations with different timing offsets, creating sequential or overlapping visual effects.
|
||||
|
||||
## Core Concept
|
||||
|
||||
All animations share one `AnimationController`. Each animation has an `Interval` defining when it starts and ends within the controller's timeline.
|
||||
|
||||
## Basic Staggered Animation
|
||||
|
||||
### Two-Property Stagger
|
||||
|
||||
```dart
|
||||
class StaggeredFadeSlide extends StatelessWidget {
|
||||
const StaggeredFadeSlide({super.key, required this.controller});
|
||||
|
||||
final AnimationController controller;
|
||||
|
||||
// Fade in first (0.0 - 0.5 of controller)
|
||||
late final Animation<double> opacity = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
|
||||
// Slide in later (0.25 - 1.0 of controller)
|
||||
late final Animation<Offset> slide = Tween<Offset>(
|
||||
begin: Offset(0, 0.5),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.25, 1.0, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: opacity,
|
||||
child: SlideTransition(
|
||||
position: slide,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Staggered animation'),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Intervals Example
|
||||
|
||||
```dart
|
||||
class MultiPropertyStagger extends StatelessWidget {
|
||||
const MultiPropertyStagger({super.key, required this.controller});
|
||||
|
||||
final AnimationController controller;
|
||||
|
||||
// Opacity: 0.0 - 0.1 (10%)
|
||||
late final Animation<double> opacity = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.1, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
// Width: 0.125 - 0.25 (12.5% - 25%)
|
||||
late final Animation<double> width = Tween<double>(begin: 50, end: 150).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.125, 0.25, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
// Height: 0.25 - 0.375 (25% - 37.5%)
|
||||
late final Animation<double> height = Tween<double>(begin: 50, end: 150).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.25, 0.375, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
// Border radius: 0.375 - 0.5 (37.5% - 50%)
|
||||
late final Animation<BorderRadius?> borderRadius = BorderRadiusTween(
|
||||
begin: BorderRadius.circular(4),
|
||||
end: BorderRadius.circular(75),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.375, 0.5, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
// Color: 0.5 - 0.625 (50% - 62.5%)
|
||||
late final Animation<Color?> color = ColorTween(
|
||||
begin: Colors.red,
|
||||
end: Colors.orange,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.5, 0.625, curve: Curves.ease),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: opacity.value,
|
||||
child: Container(
|
||||
width: width.value,
|
||||
height: height.value,
|
||||
decoration: BoxDecoration(
|
||||
color: color.value,
|
||||
borderRadius: borderRadius.value,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FlutterLogo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Controller Setup
|
||||
|
||||
### Single Controller
|
||||
|
||||
```dart
|
||||
class _StaggerState extends State<StaggerWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
vsync: this,
|
||||
);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StaggerAnimation(controller: _controller.view);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Controllers
|
||||
|
||||
For independent animation sequences:
|
||||
|
||||
```dart
|
||||
class _ComplexStaggerState extends State<ComplexStaggerWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _fadeInController;
|
||||
late AnimationController _slideController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_fadeInController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Start fade, then slide
|
||||
_fadeInController.forward().then((_) {
|
||||
_slideController.forward();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fadeInController.dispose();
|
||||
_slideController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interval Timing
|
||||
|
||||
### Understanding Interval
|
||||
|
||||
```dart
|
||||
Interval(
|
||||
0.25, // Start at 25% of controller duration
|
||||
0.75, // End at 75% of controller duration
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
```
|
||||
|
||||
**Example:** If controller duration is 2000ms:
|
||||
- Animation starts at 500ms (0.25 * 2000)
|
||||
- Animation ends at 1500ms (0.75 * 2000)
|
||||
- Animation takes 1000ms (1500 - 500)
|
||||
|
||||
### Overlapping Intervals
|
||||
|
||||
```dart
|
||||
// Animation 1: 0.0 - 0.5 (first half)
|
||||
anim1 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.5),
|
||||
),
|
||||
);
|
||||
|
||||
// Animation 2: 0.3 - 0.8 (starts before anim1 ends)
|
||||
anim2 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.3, 0.8),
|
||||
),
|
||||
);
|
||||
|
||||
// Animation 3: 0.6 - 1.0 (starts after anim1 ends)
|
||||
anim3 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.6, 1.0),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Gaps Between Animations
|
||||
|
||||
```dart
|
||||
// Animation 1: 0.0 - 0.4
|
||||
// Gap: 0.4 - 0.5
|
||||
// Animation 2: 0.5 - 0.9
|
||||
|
||||
anim1 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.4),
|
||||
),
|
||||
);
|
||||
|
||||
anim2 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.5, 0.9),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
## Staggered List Animation
|
||||
|
||||
### Menu Items Animation
|
||||
|
||||
```dart
|
||||
class StaggeredMenu extends StatefulWidget {
|
||||
const StaggeredMenu({super.key});
|
||||
|
||||
static const _menuItems = [
|
||||
'Home',
|
||||
'Profile',
|
||||
'Settings',
|
||||
'About',
|
||||
];
|
||||
|
||||
@override
|
||||
State<StaggeredMenu> createState() => _StaggeredMenuState();
|
||||
}
|
||||
|
||||
class _StaggeredMenuState extends State<StaggeredMenu>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const _itemDelayTime = Duration(milliseconds: 50);
|
||||
static const _itemAnimationTime = Duration(milliseconds: 250);
|
||||
|
||||
late AnimationController _controller;
|
||||
late List<Animation<double>> _itemAnimations;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: _itemAnimationTime +
|
||||
(_itemDelayTime * (StaggeredMenu._menuItems.length - 1)),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_itemAnimations = StaggeredMenu._menuItems.map((item) {
|
||||
final index = StaggeredMenu._menuItems.indexOf(item);
|
||||
return Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
index * 0.1, // Start later for each item
|
||||
(index * 0.1) + 0.4, // Each takes 40% of controller
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: StaggeredMenu._menuItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: Offset(1.0, 0),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
index * 0.1,
|
||||
(index * 0.1) + 0.4,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
)),
|
||||
child: FadeTransition(
|
||||
opacity: _itemAnimations[index],
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(StaggeredMenu._menuItems[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Grid Animation
|
||||
|
||||
```dart
|
||||
class StaggeredGrid extends StatefulWidget {
|
||||
const StaggeredGrid({super.key, required this.itemCount});
|
||||
|
||||
final int itemCount;
|
||||
|
||||
@override
|
||||
State<StaggeredGrid> createState() => _StaggeredGridState();
|
||||
}
|
||||
|
||||
class _StaggeredGridState extends State<StaggeredGrid>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late List<Animation<double>> _animations;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animations = List.generate(widget.itemCount, (index) {
|
||||
return Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(
|
||||
index / widget.itemCount,
|
||||
(index / widget.itemCount) + 0.3,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
itemCount: widget.itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: _animations[index],
|
||||
child: ScaleTransition(
|
||||
scale: _animations[index],
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
child: Center(child: Text('Item $index')),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Staggered Patterns
|
||||
|
||||
### Sequential Completion
|
||||
|
||||
```dart
|
||||
animation1 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.0, 0.3),
|
||||
),
|
||||
);
|
||||
|
||||
animation2 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.3, 0.6),
|
||||
),
|
||||
);
|
||||
|
||||
animation3 = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: const Interval(0.6, 1.0),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### Ripple Effect
|
||||
|
||||
```dart
|
||||
// Center expands first, then outward
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
final distance = (i - centerIndex).abs();
|
||||
animations[i] = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
distance * 0.1,
|
||||
(distance * 0.1) + 0.3,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Staggered Reveal
|
||||
|
||||
```dart
|
||||
// Reveal items one by one from top
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
animations[i] = CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
i * 0.1,
|
||||
(i * 0.1) + 0.15,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Use for opacity or transform
|
||||
Opacity(opacity: animations[i].value)
|
||||
```
|
||||
|
||||
## Duration Calculation
|
||||
|
||||
### Calculate Total Duration
|
||||
|
||||
```dart
|
||||
const itemDelay = Duration(milliseconds: 50);
|
||||
const itemAnimationTime = Duration(milliseconds: 250);
|
||||
const itemCount = 10;
|
||||
|
||||
final totalDuration = itemAnimationTime +
|
||||
(itemDelay * (itemCount - 1));
|
||||
|
||||
// 250ms + (50ms * 9) = 700ms
|
||||
```
|
||||
|
||||
### Using in Controller
|
||||
|
||||
```dart
|
||||
_controller = AnimationController(
|
||||
duration: totalDuration,
|
||||
vsync: this,
|
||||
);
|
||||
```
|
||||
|
||||
### Dynamic Duration
|
||||
|
||||
```dart
|
||||
late AnimationController _controller;
|
||||
late Duration _totalDuration;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final itemCount = _items.length;
|
||||
const itemDelay = Duration(milliseconds: 50);
|
||||
const itemAnimationTime = Duration(milliseconds: 250);
|
||||
|
||||
_totalDuration = itemAnimationTime + (itemDelay * (itemCount - 1));
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: _totalDuration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Create animations...
|
||||
_controller.forward();
|
||||
}
|
||||
```
|
||||
|
||||
## Repeating Staggered Animations
|
||||
|
||||
### Loop on Completion
|
||||
|
||||
```dart
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reset();
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Ping-Pong (Forward and Reverse)
|
||||
|
||||
```dart
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_controller.reverse();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Staggered Animations
|
||||
|
||||
### Slow Animation
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
timeDilation = 10.0; // 10x slower
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Print Interval Ranges
|
||||
|
||||
```dart
|
||||
for (int i = 0; i < _animations.length; i++) {
|
||||
final anim = _animations[i] as CurvedAnimation;
|
||||
print('Animation $i: ${anim.curve}');
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Animation 0: Interval(0.0, 0.4, Curves.easeOut)
|
||||
// Animation 1: Interval(0.1, 0.5, Curves.easeOut)
|
||||
// ...
|
||||
```
|
||||
|
||||
### Visualize Animation State
|
||||
|
||||
```dart
|
||||
class DebugStaggeredWidget extends StatelessWidget {
|
||||
const DebugStaggeredWidget({super.key, required this.controller, required this.animations});
|
||||
|
||||
final AnimationController controller;
|
||||
final List<Animation<double>> animations;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
for (int i = 0; i < animations.length; i++)
|
||||
Text('Animation $i: ${animations[i].value.toStringAsFixed(2)}'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use one controller when animations are related
|
||||
- Calculate total duration correctly
|
||||
- Use `AnimatedBuilder` for optimal rebuilds
|
||||
- Profile with Flutter DevTools
|
||||
- Test on various devices
|
||||
- Consider reducing animation complexity on low-end devices
|
||||
|
||||
### DON'T
|
||||
|
||||
- Create too many controllers unnecessarily
|
||||
- Forget to dispose controllers
|
||||
- Use complex widget trees inside staggered animations
|
||||
- Animate too many items simultaneously (jank)
|
||||
- Use very long durations without good reason
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Loading Animation
|
||||
|
||||
```dart
|
||||
// Dot 1: 0.0 - 0.3
|
||||
// Dot 2: 0.1 - 0.4
|
||||
// Dot 3: 0.2 - 0.5
|
||||
// Repeat forever
|
||||
```
|
||||
|
||||
### Success Animation Sequence
|
||||
|
||||
```dart
|
||||
// Checkmark appears: 0.0 - 0.3
|
||||
// "Success!" text fades in: 0.2 - 0.5
|
||||
// Confetti falls: 0.3 - 1.0
|
||||
```
|
||||
|
||||
### Onboarding Steps
|
||||
|
||||
```dart
|
||||
// Step 1: 0.0 - 0.25
|
||||
// Step 2: 0.25 - 0.5
|
||||
// Step 3: 0.5 - 0.75
|
||||
// Step 4: 0.75 - 1.0
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Respect `MediaQuery.disableAnimations` setting
|
||||
- Provide alternative to complex staggered animations
|
||||
- Ensure content remains accessible during animation
|
||||
- Test with screen readers
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Conditional Staggering
|
||||
|
||||
```dart
|
||||
// Show animations based on device performance
|
||||
final isLowEnd = Platform.isAndroid && deviceInfo.version.sdkInt < 21;
|
||||
|
||||
final staggerDelay = isLowEnd
|
||||
? Duration(milliseconds: 100) // Slower on low-end
|
||||
: Duration(milliseconds: 50);
|
||||
```
|
||||
|
||||
### Adaptive Staggering
|
||||
|
||||
```dart
|
||||
// Adjust based on screen size
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final itemsPerRow = screenWidth ~/ 150;
|
||||
|
||||
// Calculate stagger based on grid position
|
||||
final row = index ~/ itemsPerRow;
|
||||
final col = index % itemsPerRow;
|
||||
final startDelay = (row * 0.2) + (col * 0.05);
|
||||
```
|
||||
|
||||
### Physics-Influenced Staggering
|
||||
|
||||
```dart
|
||||
// Use spring physics for staggered elements
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
animations[i] = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Interval(
|
||||
i * 0.05,
|
||||
(i * 0.05) + 0.5,
|
||||
curve: Curves.elasticOut,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1,140 +0,0 @@
|
||||
---
|
||||
name: flutter-expert
|
||||
description: Use when building cross-platform applications with Flutter 3+ and Dart. Invoke for widget development, Riverpod/Bloc state management, GoRouter navigation, platform-specific implementations, performance optimization.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: https://github.com/Jeffallan
|
||||
version: "1.1.0"
|
||||
domain: frontend
|
||||
triggers: Flutter, Dart, widget, Riverpod, Bloc, GoRouter, cross-platform
|
||||
role: specialist
|
||||
scope: implementation
|
||||
output-format: code
|
||||
related-skills: react-native-expert, test-master, fullstack-guardian
|
||||
---
|
||||
|
||||
# Flutter Expert
|
||||
|
||||
Senior mobile engineer building high-performance cross-platform applications with Flutter 3 and Dart.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building cross-platform Flutter applications
|
||||
- Implementing state management (Riverpod, Bloc)
|
||||
- Setting up navigation with GoRouter
|
||||
- Creating custom widgets and animations
|
||||
- Optimizing Flutter performance
|
||||
- Platform-specific implementations
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. **Setup** — Scaffold project, add dependencies (`flutter pub get`), configure routing
|
||||
2. **State** — Define Riverpod providers or Bloc/Cubit classes; verify with `flutter analyze`
|
||||
- If `flutter analyze` reports issues: fix all lints and warnings before proceeding; re-run until clean
|
||||
3. **Widgets** — Build reusable, const-optimized components; run `flutter test` after each feature
|
||||
- If tests fail: inspect widget tree with Flutter DevTools, fix failing assertions, re-run `flutter test`
|
||||
4. **Test** — Write widget and integration tests; confirm with `flutter test --coverage`
|
||||
- If coverage drops or tests fail: identify untested branches, add targeted tests, re-run before merging
|
||||
5. **Optimize** — Profile with Flutter DevTools (`flutter run --profile`), eliminate jank, reduce rebuilds
|
||||
- If jank persists: check rebuild counts in the Performance overlay, isolate expensive `build()` calls, apply `const` or move state closer to consumers
|
||||
|
||||
## Reference Guide
|
||||
|
||||
Load detailed guidance based on context:
|
||||
|
||||
| Topic | Reference | Load When |
|
||||
|-------|-----------|-----------|
|
||||
| Riverpod | `references/riverpod-state.md` | State management, providers, notifiers |
|
||||
| Bloc | `references/bloc-state.md` | Bloc, Cubit, event-driven state, complex business logic |
|
||||
| GoRouter | `references/gorouter-navigation.md` | Navigation, routing, deep linking |
|
||||
| Widgets | `references/widget-patterns.md` | Building UI components, const optimization |
|
||||
| Structure | `references/project-structure.md` | Setting up project, architecture |
|
||||
| Performance | `references/performance.md` | Optimization, profiling, jank fixes |
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Riverpod Provider + ConsumerWidget (correct pattern)
|
||||
|
||||
```dart
|
||||
// provider definition
|
||||
final counterProvider = StateNotifierProvider<CounterNotifier, int>(
|
||||
(ref) => CounterNotifier(),
|
||||
);
|
||||
|
||||
class CounterNotifier extends StateNotifier<int> {
|
||||
CounterNotifier() : super(0);
|
||||
void increment() => state = state + 1; // new instance, never mutate
|
||||
}
|
||||
|
||||
// consuming widget — use ConsumerWidget, not StatefulWidget
|
||||
class CounterView extends ConsumerWidget {
|
||||
const CounterView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final count = ref.watch(counterProvider);
|
||||
return Text('$count');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Before / After — State Management
|
||||
|
||||
```dart
|
||||
// ❌ WRONG: app-wide state in setState
|
||||
class _BadCounterState extends State<BadCounter> {
|
||||
int _count = 0;
|
||||
void _inc() => setState(() => _count++); // causes full subtree rebuild
|
||||
}
|
||||
|
||||
// ✅ CORRECT: scoped Riverpod consumer
|
||||
class GoodCounter extends ConsumerWidget {
|
||||
const GoodCounter({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final count = ref.watch(counterProvider);
|
||||
return IconButton(
|
||||
onPressed: () => ref.read(counterProvider.notifier).increment(),
|
||||
icon: const Icon(Icons.add), // const on static widgets
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
### MUST DO
|
||||
- Use `const` constructors wherever possible
|
||||
- Implement proper keys for lists
|
||||
- Use `Consumer`/`ConsumerWidget` for state (not `StatefulWidget`)
|
||||
- Follow Material/Cupertino design guidelines
|
||||
- Profile with DevTools, fix jank
|
||||
- Test widgets with `flutter_test`
|
||||
|
||||
### MUST NOT DO
|
||||
- Build widgets inside `build()` method
|
||||
- Mutate state directly (always create new instances)
|
||||
- Use `setState` for app-wide state
|
||||
- Skip `const` on static widgets
|
||||
- Ignore platform-specific behavior
|
||||
- Block UI thread with heavy computation (use `compute()`)
|
||||
|
||||
## Troubleshooting Common Failures
|
||||
|
||||
| Symptom | Likely Cause | Recovery |
|
||||
|---------|-------------|----------|
|
||||
| `flutter analyze` errors | Unresolved imports, missing `const`, type mismatches | Fix flagged lines; run `flutter pub get` if imports are missing |
|
||||
| Widget test assertion failures | Widget tree mismatch or async state not settled | Use `tester.pumpAndSettle()` after state changes; verify finder selectors |
|
||||
| Build fails after adding package | Incompatible dependency version | Run `flutter pub upgrade --major-versions`; check pub.dev compatibility |
|
||||
| Jank / dropped frames | Expensive `build()` calls, uncached widgets, heavy main-thread work | Use `RepaintBoundary`, move heavy work to `compute()`, add `const` |
|
||||
| Hot reload not reflecting changes | State held in `StateNotifier` not reset | Use hot restart (`R` in terminal) to reset full app state |
|
||||
|
||||
## Output Templates
|
||||
|
||||
When implementing Flutter features, provide:
|
||||
1. Widget code with proper `const` usage
|
||||
2. Provider/Bloc definitions
|
||||
3. Route configuration if needed
|
||||
4. Test file structure
|
||||
|
||||
[Documentation](https://jeffallan.github.io/claude-skills/skills/frontend/flutter-expert/)
|
||||
@@ -1,259 +0,0 @@
|
||||
# Bloc State Management
|
||||
|
||||
## When to Use Bloc
|
||||
|
||||
Use **Bloc/Cubit** when you need:
|
||||
|
||||
* Explicit event → state transitions
|
||||
* Complex business logic
|
||||
* Predictable, testable flows
|
||||
* Clear separation between UI and logic
|
||||
|
||||
| Use Case | Recommended |
|
||||
| ---------------------- | ----------- |
|
||||
| Simple mutable state | Riverpod |
|
||||
| Event-driven workflows | Bloc |
|
||||
| Forms, auth, wizards | Bloc |
|
||||
| Feature modules | Bloc |
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Description |
|
||||
| ------- | ---------------------- |
|
||||
| Event | User/system input |
|
||||
| State | Immutable UI state |
|
||||
| Bloc | Event → State mapper |
|
||||
| Cubit | State-only (no events) |
|
||||
|
||||
---
|
||||
|
||||
## Basic Bloc Setup
|
||||
|
||||
### Event
|
||||
|
||||
```dart
|
||||
sealed class CounterEvent {}
|
||||
|
||||
final class CounterIncremented extends CounterEvent {}
|
||||
|
||||
final class CounterDecremented extends CounterEvent {}
|
||||
```
|
||||
|
||||
### State
|
||||
|
||||
```dart
|
||||
class CounterState {
|
||||
final int value;
|
||||
|
||||
const CounterState({required this.value});
|
||||
|
||||
CounterState copyWith({int? value}) {
|
||||
return CounterState(value: value ?? this.value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bloc
|
||||
|
||||
```dart
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CounterBloc extends Bloc<CounterEvent, CounterState> {
|
||||
CounterBloc() : super(const CounterState(value: 0)) {
|
||||
on<CounterIncremented>((event, emit) {
|
||||
emit(state.copyWith(value: state.value + 1));
|
||||
});
|
||||
|
||||
on<CounterDecremented>((event, emit) {
|
||||
emit(state.copyWith(value: state.value - 1));
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cubit (Recommended for Simpler Logic)
|
||||
|
||||
```dart
|
||||
class CounterCubit extends Cubit<int> {
|
||||
CounterCubit() : super(0);
|
||||
|
||||
void increment() => emit(state + 1);
|
||||
void decrement() => emit(state - 1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Providing Bloc to the Widget Tree
|
||||
|
||||
```dart
|
||||
BlocProvider(
|
||||
create: (_) => CounterBloc(),
|
||||
child: const CounterScreen(),
|
||||
);
|
||||
```
|
||||
|
||||
Multiple blocs:
|
||||
|
||||
```dart
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => AuthBloc()),
|
||||
BlocProvider(create: (_) => ProfileBloc()),
|
||||
],
|
||||
child: const AppRoot(),
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using Bloc in Widgets
|
||||
|
||||
### BlocBuilder (UI rebuilds)
|
||||
|
||||
```dart
|
||||
class CounterScreen extends StatelessWidget {
|
||||
const CounterScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CounterBloc, CounterState>(
|
||||
buildWhen: (prev, curr) => prev.value != curr.value,
|
||||
builder: (context, state) {
|
||||
return Text(
|
||||
state.value.toString(),
|
||||
style: Theme.of(context).textTheme.displayLarge,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### BlocListener (Side Effects)
|
||||
|
||||
```dart
|
||||
BlocListener<AuthBloc, AuthState>(
|
||||
listenWhen: (prev, curr) => curr is AuthFailure,
|
||||
listener: (context, state) {
|
||||
if (state is AuthFailure) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(state.message)));
|
||||
}
|
||||
},
|
||||
child: const LoginForm(),
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### BlocConsumer (Builder + Listener)
|
||||
|
||||
```dart
|
||||
BlocConsumer<FormBloc, FormState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == FormStatus.success) {
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return ElevatedButton(
|
||||
onPressed: state.isValid
|
||||
? () => context.read<FormBloc>().add(FormSubmitted())
|
||||
: null,
|
||||
child: const Text('Submit'),
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessing Bloc Without Rebuilds
|
||||
|
||||
```dart
|
||||
context.read<CounterBloc>().add(CounterIncremented());
|
||||
```
|
||||
|
||||
⚠️ **Never use `watch` inside callbacks**
|
||||
|
||||
---
|
||||
|
||||
## Async Bloc Pattern (API Calls)
|
||||
|
||||
```dart
|
||||
on<UserRequested>((event, emit) async {
|
||||
emit(const UserState.loading());
|
||||
|
||||
try {
|
||||
final user = await repository.fetchUser();
|
||||
emit(UserState.success(user));
|
||||
} catch (e) {
|
||||
emit(UserState.failure(e.toString()));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bloc + GoRouter (Auth Guard Example)
|
||||
|
||||
```dart
|
||||
redirect: (context, state) {
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
|
||||
if (authState is Unauthenticated) {
|
||||
return '/login';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Bloc
|
||||
|
||||
```dart
|
||||
blocTest<CounterBloc, CounterState>(
|
||||
'emits incremented value',
|
||||
build: () => CounterBloc(),
|
||||
act: (bloc) => bloc.add(CounterIncremented()),
|
||||
expect: () => [
|
||||
const CounterState(value: 1),
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices (MUST FOLLOW)
|
||||
|
||||
✅ Immutable states
|
||||
✅ Small, focused blocs
|
||||
✅ One feature = one bloc
|
||||
✅ Use Cubit when possible
|
||||
✅ Test all blocs
|
||||
|
||||
❌ No UI logic inside blocs
|
||||
❌ No context usage inside blocs
|
||||
❌ No mutable state
|
||||
❌ No massive “god blocs”
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Widget | Purpose |
|
||||
| ----------------- | -------------------- |
|
||||
| BlocBuilder | UI rebuild |
|
||||
| BlocListener | Side effects |
|
||||
| BlocConsumer | Both |
|
||||
| BlocProvider | Dependency injection |
|
||||
| MultiBlocProvider | Multiple blocs |
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
# GoRouter Navigation
|
||||
|
||||
## Basic Setup
|
||||
|
||||
```dart
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final goRouter = GoRouter(
|
||||
initialLocation: '/',
|
||||
redirect: (context, state) {
|
||||
final isLoggedIn = /* check auth */;
|
||||
if (!isLoggedIn && !state.matchedLocation.startsWith('/auth')) {
|
||||
return '/auth/login';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'details/:id',
|
||||
builder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
return DetailsScreen(id: id);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/login',
|
||||
builder: (context, state) => const LoginScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// In app.dart
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: goRouter,
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Navigation Methods
|
||||
|
||||
```dart
|
||||
// Navigate and replace history
|
||||
context.go('/details/123');
|
||||
|
||||
// Navigate and add to stack
|
||||
context.push('/details/123');
|
||||
|
||||
// Go back
|
||||
context.pop();
|
||||
|
||||
// Replace current route
|
||||
context.pushReplacement('/home');
|
||||
|
||||
// Navigate with extra data
|
||||
context.push('/details/123', extra: {'title': 'Item'});
|
||||
|
||||
// Access extra in destination
|
||||
final extra = GoRouterState.of(context).extra as Map<String, dynamic>?;
|
||||
```
|
||||
|
||||
## Shell Routes (Persistent UI)
|
||||
|
||||
```dart
|
||||
final goRouter = GoRouter(
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) {
|
||||
return ScaffoldWithNavBar(child: child);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
|
||||
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
|
||||
GoRoute(path: '/settings', builder: (_, __) => const SettingsScreen()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
## Query Parameters
|
||||
|
||||
```dart
|
||||
GoRoute(
|
||||
path: '/search',
|
||||
builder: (context, state) {
|
||||
final query = state.uri.queryParameters['q'] ?? '';
|
||||
final page = int.tryParse(state.uri.queryParameters['page'] ?? '1') ?? 1;
|
||||
return SearchScreen(query: query, page: page);
|
||||
},
|
||||
),
|
||||
|
||||
// Navigate with query params
|
||||
context.go('/search?q=flutter&page=2');
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Method | Behavior |
|
||||
|--------|----------|
|
||||
| `context.go()` | Navigate, replace stack |
|
||||
| `context.push()` | Navigate, add to stack |
|
||||
| `context.pop()` | Go back |
|
||||
| `context.pushReplacement()` | Replace current |
|
||||
| `:param` | Path parameter |
|
||||
| `?key=value` | Query parameter |
|
||||
@@ -1,99 +0,0 @@
|
||||
# Performance Optimization
|
||||
|
||||
## Profiling Commands
|
||||
|
||||
```bash
|
||||
# Run in profile mode
|
||||
flutter run --profile
|
||||
|
||||
# Analyze performance
|
||||
flutter analyze
|
||||
|
||||
# DevTools
|
||||
flutter pub global activate devtools
|
||||
flutter pub global run devtools
|
||||
```
|
||||
|
||||
## Common Optimizations
|
||||
|
||||
### Const Widgets
|
||||
```dart
|
||||
// ❌ Rebuilds every time
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(16), // Creates new object
|
||||
child: Text('Hello'),
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Const prevents rebuilds
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: const Text('Hello'),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Selective Provider Watching
|
||||
```dart
|
||||
// ❌ Rebuilds on any user change
|
||||
final user = ref.watch(userProvider);
|
||||
return Text(user.name);
|
||||
|
||||
// ✅ Only rebuilds when name changes
|
||||
final name = ref.watch(userProvider.select((u) => u.name));
|
||||
return Text(name);
|
||||
```
|
||||
|
||||
### RepaintBoundary
|
||||
```dart
|
||||
// Isolate expensive widgets
|
||||
RepaintBoundary(
|
||||
child: ComplexAnimatedWidget(),
|
||||
)
|
||||
```
|
||||
|
||||
### Image Optimization
|
||||
```dart
|
||||
// Use cached_network_image
|
||||
CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
placeholder: (_, __) => const CircularProgressIndicator(),
|
||||
errorWidget: (_, __, ___) => const Icon(Icons.error),
|
||||
)
|
||||
|
||||
// Resize images
|
||||
Image.network(
|
||||
url,
|
||||
cacheWidth: 200, // Resize in memory
|
||||
cacheHeight: 200,
|
||||
)
|
||||
```
|
||||
|
||||
### Compute for Heavy Operations
|
||||
```dart
|
||||
// ❌ Blocks UI thread
|
||||
final result = heavyComputation(data);
|
||||
|
||||
// ✅ Runs in isolate
|
||||
final result = await compute(heavyComputation, data);
|
||||
```
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
| Check | Solution |
|
||||
|-------|----------|
|
||||
| Unnecessary rebuilds | Add `const`, use `select()` |
|
||||
| Large lists | Use `ListView.builder` |
|
||||
| Image loading | Use `cached_network_image` |
|
||||
| Heavy computation | Use `compute()` |
|
||||
| Jank in animations | Use `RepaintBoundary` |
|
||||
| Memory leaks | Dispose controllers |
|
||||
|
||||
## DevTools Metrics
|
||||
|
||||
- **Frame rendering time**: < 16ms for 60fps
|
||||
- **Widget rebuilds**: Minimize unnecessary rebuilds
|
||||
- **Memory usage**: Watch for leaks
|
||||
- **CPU profiler**: Identify bottlenecks
|
||||
@@ -1,118 +0,0 @@
|
||||
# Project Structure
|
||||
|
||||
## Feature-Based Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart
|
||||
├── app.dart
|
||||
├── core/
|
||||
│ ├── constants/
|
||||
│ │ ├── colors.dart
|
||||
│ │ └── strings.dart
|
||||
│ ├── theme/
|
||||
│ │ ├── app_theme.dart
|
||||
│ │ └── text_styles.dart
|
||||
│ ├── utils/
|
||||
│ │ ├── extensions.dart
|
||||
│ │ └── validators.dart
|
||||
│ └── errors/
|
||||
│ └── failures.dart
|
||||
├── features/
|
||||
│ ├── auth/
|
||||
│ │ ├── data/
|
||||
│ │ │ ├── repositories/
|
||||
│ │ │ └── datasources/
|
||||
│ │ ├── domain/
|
||||
│ │ │ ├── entities/
|
||||
│ │ │ └── usecases/
|
||||
│ │ ├── presentation/
|
||||
│ │ │ ├── screens/
|
||||
│ │ │ └── widgets/
|
||||
│ │ └── providers/
|
||||
│ │ └── auth_provider.dart
|
||||
│ └── home/
|
||||
│ ├── data/
|
||||
│ ├── domain/
|
||||
│ ├── presentation/
|
||||
│ └── providers/
|
||||
├── shared/
|
||||
│ ├── widgets/
|
||||
│ │ ├── buttons/
|
||||
│ │ ├── inputs/
|
||||
│ │ └── cards/
|
||||
│ ├── services/
|
||||
│ │ ├── api_service.dart
|
||||
│ │ └── storage_service.dart
|
||||
│ └── models/
|
||||
│ └── user.dart
|
||||
└── routes/
|
||||
└── app_router.dart
|
||||
```
|
||||
|
||||
## pubspec.yaml Essentials
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
# State Management
|
||||
flutter_riverpod: ^2.5.0
|
||||
riverpod_annotation: ^2.3.0
|
||||
# Navigation
|
||||
go_router: ^14.0.0
|
||||
# Networking
|
||||
dio: ^5.4.0
|
||||
# Code Generation
|
||||
freezed_annotation: ^2.4.0
|
||||
json_annotation: ^4.8.0
|
||||
# Storage
|
||||
shared_preferences: ^2.2.0
|
||||
hive_flutter: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.4.0
|
||||
riverpod_generator: ^2.4.0
|
||||
freezed: ^2.5.0
|
||||
json_serializable: ^6.8.0
|
||||
flutter_lints: ^4.0.0
|
||||
```
|
||||
|
||||
## Feature Layer Responsibilities
|
||||
|
||||
| Layer | Responsibility |
|
||||
|-------|----------------|
|
||||
| **data/** | API calls, local storage, DTOs |
|
||||
| **domain/** | Business logic, entities, use cases |
|
||||
| **presentation/** | UI screens, widgets |
|
||||
| **providers/** | Riverpod providers for feature |
|
||||
|
||||
## Main Entry Point
|
||||
|
||||
```dart
|
||||
// main.dart
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Hive.initFlutter();
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
|
||||
// app.dart
|
||||
class MyApp extends ConsumerWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
routerConfig: router,
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
themeMode: ThemeMode.system,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,130 +0,0 @@
|
||||
# Riverpod State Management
|
||||
|
||||
## Provider Types
|
||||
|
||||
```dart
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
// Simple state
|
||||
final counterProvider = StateProvider<int>((ref) => 0);
|
||||
|
||||
// Async state (API calls)
|
||||
final usersProvider = FutureProvider<List<User>>((ref) async {
|
||||
final api = ref.read(apiProvider);
|
||||
return api.getUsers();
|
||||
});
|
||||
|
||||
// Stream state (real-time)
|
||||
final messagesProvider = StreamProvider<List<Message>>((ref) {
|
||||
return ref.read(chatServiceProvider).messagesStream;
|
||||
});
|
||||
```
|
||||
|
||||
## Notifier Pattern (Riverpod 2.0)
|
||||
|
||||
```dart
|
||||
@riverpod
|
||||
class TodoList extends _$TodoList {
|
||||
@override
|
||||
List<Todo> build() => [];
|
||||
|
||||
void add(Todo todo) {
|
||||
state = [...state, todo];
|
||||
}
|
||||
|
||||
void toggle(String id) {
|
||||
state = [
|
||||
for (final todo in state)
|
||||
if (todo.id == id) todo.copyWith(completed: !todo.completed) else todo,
|
||||
];
|
||||
}
|
||||
|
||||
void remove(String id) {
|
||||
state = state.where((t) => t.id != id).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Async Notifier
|
||||
@riverpod
|
||||
class UserProfile extends _$UserProfile {
|
||||
@override
|
||||
Future<User> build() async {
|
||||
return ref.read(apiProvider).getCurrentUser();
|
||||
}
|
||||
|
||||
Future<void> updateName(String name) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
final updated = await ref.read(apiProvider).updateUser(name: name);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage in Widgets
|
||||
|
||||
```dart
|
||||
// ConsumerWidget (recommended)
|
||||
class TodoScreen extends ConsumerWidget {
|
||||
const TodoScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final todos = ref.watch(todoListProvider);
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: todos.length,
|
||||
itemBuilder: (context, index) {
|
||||
final todo = todos[index];
|
||||
return ListTile(
|
||||
title: Text(todo.title),
|
||||
leading: Checkbox(
|
||||
value: todo.completed,
|
||||
onChanged: (_) => ref.read(todoListProvider.notifier).toggle(todo.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Selective rebuilds with select
|
||||
class UserAvatar extends ConsumerWidget {
|
||||
const UserAvatar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final avatarUrl = ref.watch(userProvider.select((u) => u?.avatarUrl));
|
||||
|
||||
return CircleAvatar(
|
||||
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Async state handling
|
||||
class UserProfileScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final userAsync = ref.watch(userProfileProvider);
|
||||
|
||||
return userAsync.when(
|
||||
data: (user) => Text(user.name),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (err, stack) => Text('Error: $err'),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Provider | Use Case |
|
||||
|----------|----------|
|
||||
| `Provider` | Computed/derived values |
|
||||
| `StateProvider` | Simple mutable state |
|
||||
| `FutureProvider` | Async operations (one-time) |
|
||||
| `StreamProvider` | Real-time data streams |
|
||||
| `NotifierProvider` | Complex state with methods |
|
||||
| `AsyncNotifierProvider` | Async state with methods |
|
||||
@@ -1,123 +0,0 @@
|
||||
# Widget Patterns
|
||||
|
||||
## Optimized Widget Pattern
|
||||
|
||||
```dart
|
||||
// Use const constructors
|
||||
class OptimizedCard extends StatelessWidget {
|
||||
final String title;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const OptimizedCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Layout
|
||||
|
||||
```dart
|
||||
class ResponsiveLayout extends StatelessWidget {
|
||||
final Widget mobile;
|
||||
final Widget? tablet;
|
||||
final Widget desktop;
|
||||
|
||||
const ResponsiveLayout({
|
||||
super.key,
|
||||
required this.mobile,
|
||||
this.tablet,
|
||||
required this.desktop,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth >= 1100) return desktop;
|
||||
if (constraints.maxWidth >= 650) return tablet ?? mobile;
|
||||
return mobile;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Hooks (flutter_hooks)
|
||||
|
||||
```dart
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
class CounterWidget extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final counter = useState(0);
|
||||
final controller = useTextEditingController();
|
||||
|
||||
useEffect(() {
|
||||
// Setup
|
||||
return () {
|
||||
// Cleanup
|
||||
};
|
||||
}, []);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text('Count: ${counter.value}'),
|
||||
ElevatedButton(
|
||||
onPressed: () => counter.value++,
|
||||
child: const Text('Increment'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sliver Patterns
|
||||
|
||||
```dart
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 200,
|
||||
pinned: true,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: const Text('Title'),
|
||||
background: Image.network(imageUrl, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => ListTile(title: Text('Item $index')),
|
||||
childCount: 100,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Key Optimization Patterns
|
||||
|
||||
| Pattern | Implementation |
|
||||
|---------|----------------|
|
||||
| **const widgets** | Add `const` to static widgets |
|
||||
| **keys** | Use `Key` for list items |
|
||||
| **select** | `ref.watch(provider.select(...))` |
|
||||
| **RepaintBoundary** | Isolate expensive repaints |
|
||||
| **ListView.builder** | Lazy loading for lists |
|
||||
| **const constructors** | Always use when possible |
|
||||
@@ -1,106 +0,0 @@
|
||||
---
|
||||
name: flutter-testing
|
||||
description: >-
|
||||
Write, fix, review, debug, and validate Flutter tests for apps, packages,
|
||||
and plugins. Use when adding unit tests, widget tests, integration tests,
|
||||
MethodChannel or plugin mocks, Mockito or mocktail test doubles, golden or
|
||||
accessibility checks, CI test commands, test failures, MissingPluginException,
|
||||
pump or pumpAndSettle problems, finder errors, build_runner mock generation,
|
||||
device integration testing, web integration testing, or flaky Flutter tests.
|
||||
metadata:
|
||||
author: Stanislav [MADTeacher] Chernyshev
|
||||
version: "2.0"
|
||||
---
|
||||
|
||||
# Flutter Testing
|
||||
|
||||
You are a Flutter testing engineer for app, package, and plugin projects.
|
||||
|
||||
## Principle 0
|
||||
|
||||
Do not write Flutter tests from memory. First inspect the project, choose the
|
||||
right test layer, read the routed reference for the scenario, then run the
|
||||
closest validation command. Broken or flaky tests waste more time than missing
|
||||
tests because they create false confidence and slow future changes.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the project before changing tests: `pubspec.yaml`, existing `test/`,
|
||||
`integration_test/`, `test_driver/`, generated mock files, state management,
|
||||
platform abstractions, plugin usage, and CI commands.
|
||||
2. Choose the test layer:
|
||||
- Pure Dart functions, repositories, services, state reducers, and view
|
||||
models: unit tests.
|
||||
- Single widgets, forms, navigation shells, semantics, gestures, responsive
|
||||
layout, and UI state: widget tests.
|
||||
- Complete user flows, real device behavior, screenshots, performance
|
||||
reports, native/plugin bridges, and browser/device targets: integration
|
||||
tests.
|
||||
- Flutter plugins or app code using platform channels: plugin and mocking
|
||||
guidance.
|
||||
3. Read only the reference files needed for the chosen scenario.
|
||||
4. Implement the smallest deterministic test or test fix using the project's
|
||||
existing test style, dependency injection pattern, keys, fixtures, mocks, and
|
||||
CI constraints.
|
||||
5. Run mandatory validation. If validation cannot run, report the exact blocker
|
||||
and the concrete risk instead of presenting the change as verified.
|
||||
|
||||
## Resource Routing
|
||||
|
||||
| Task | Read or run | Why |
|
||||
|---|---|---|
|
||||
| Write or fix pure Dart tests, async tests, stream tests, matchers, exceptions, or test organization | `references/unit-testing.md` | Unit-test patterns that compile under Dart null safety |
|
||||
| Write or fix widget tests, finders, gestures, forms, navigation, semantics, scrolling, animations, or layout-size tests | `references/widget-testing.md` | `flutter_test` APIs and widget-specific pitfalls |
|
||||
| Add or fix integration tests, device/browser runs, performance reports, screenshots, persistence flows, platform scenarios, or CI integration | `references/integration-testing.md` | Current `integration_test` APIs and target commands |
|
||||
| Mock dependencies, repositories, platform channels, generated Mockito mocks, manual fakes, or state-management collaborators | `references/mocking.md` | Deterministic test-double patterns and mock generation |
|
||||
| Diagnose failing tests, layout errors, `MissingPluginException`, finder failures, timeouts, async hangs, or debugging output | `references/common-errors.md` | Error-to-fix mapping without guessing |
|
||||
| Test Flutter plugin packages, native Android/iOS code, example-app integration tests, or plugin registration/error paths | `references/plugin-testing.md` | Plugin package layout and native/Dart test split |
|
||||
| Edit this skill, references, or examples | `scripts/verify-examples.sh` | Deterministic smoke check for stale patterns and broken links |
|
||||
|
||||
## Mandatory Validation
|
||||
|
||||
- After editing Dart tests or production code, run the narrowest relevant
|
||||
command first, such as `flutter test test/my_widget_test.dart`,
|
||||
`flutter test --plain-name "subtree"`, or `dart test` for pure Dart packages.
|
||||
- After adding or changing generated Mockito mocks, run
|
||||
`dart run build_runner build` or the repository's established build command
|
||||
before running tests.
|
||||
- For integration tests on mobile or desktop targets, run `flutter test -d
|
||||
<device-id> integration_test/<test_file>.dart` when a device target is
|
||||
required; otherwise run the documented project command.
|
||||
- For browser integration tests that require `integrationDriver`, run
|
||||
`flutter drive --driver=test_driver/integration_test.dart --target=integration_test/<test_file>.dart -d chrome`
|
||||
or the project's web driver command.
|
||||
- After fixing flaky tests, run the specific test more than once or use the
|
||||
repository's repeat/randomization option if one exists.
|
||||
- After editing this skill or its references, run
|
||||
`bash flutter-testing/scripts/verify-examples.sh`.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Prefer `package:flutter_test/flutter_test.dart` for widget and integration
|
||||
tests, and `package:test/test.dart` only for pure Dart tests that do not need
|
||||
Flutter bindings.
|
||||
- Test user-visible behavior and public contracts. Do not assert private method
|
||||
calls, widget internals, or incidental rebuild counts unless performance work
|
||||
explicitly requires it.
|
||||
- Prefer dependency injection, fake repositories, fake platform interfaces, or
|
||||
MethodChannel handlers over real network, real storage, timers, permissions,
|
||||
or OS dialogs.
|
||||
- Do not use fixed `Future.delayed` waits to hide async uncertainty. Use
|
||||
deterministic fakes, explicit pumps, `pumpAndSettle` only when animations can
|
||||
settle, or bounded custom pumps.
|
||||
- Do not use obsolete commands or APIs: `--no-sound-null-safety`,
|
||||
`flutter test --platform ...`, `tester.trace`, `tester.takeScreenshot`,
|
||||
`captureNamed`, or `flutter pub run build_runner build`.
|
||||
- Do not simulate connectivity, permissions, or persistence by changing the
|
||||
test surface size or by re-pumping the app with hidden state. Use explicit
|
||||
fakes, test hooks, or real integration targets.
|
||||
|
||||
## Fallback
|
||||
|
||||
If the repository lacks enough context to choose the test layer, ask for the
|
||||
target behavior and preferred test level. If devices, browsers, native tooling,
|
||||
network access, code generation, or dependency downloads block validation,
|
||||
finish with the exact command that failed, what was not verified, and the risk
|
||||
left for the user.
|
||||
@@ -1,507 +0,0 @@
|
||||
# Common Testing Errors
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers frequently encountered Flutter testing errors and their solutions.
|
||||
|
||||
## Layout Errors
|
||||
|
||||
### 'A RenderFlex overflowed...'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
The following assertion was thrown during layout:
|
||||
A RenderFlex overflowed by 1146 pixels on the right.
|
||||
```
|
||||
|
||||
**Cause:** Yellow and black stripes indicate overflow when a child widget is too large for its parent (Row/Column).
|
||||
|
||||
**Solution:** Wrap the overflowing widget in `Expanded` or `Flexible`.
|
||||
|
||||
```dart
|
||||
// Problem
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.message),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
|
||||
Text('Very long text that overflows the available space...'),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
// Solution
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.message),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Title', style: Theme.of(context).textTheme.headlineMedium),
|
||||
Text('Very long text that fits in the available space...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### 'Vertical viewport was given unbounded height'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
Vertical viewport was given unbounded height.
|
||||
Viewports expand in the scrolling direction to fill their container.
|
||||
This situation typically happens when a scrollable widget is nested inside another scrollable widget.
|
||||
```
|
||||
|
||||
**Cause:** ListView or other scrollable widget inside Column without height constraints.
|
||||
|
||||
**Solution:** Wrap in `Expanded` or use `shrinkWrap: true`.
|
||||
|
||||
```dart
|
||||
// Problem
|
||||
Column(
|
||||
children: [
|
||||
Text('Header'),
|
||||
ListView(
|
||||
children: [
|
||||
ListTile(leading: Icon(Icons.map), title: Text('Map')),
|
||||
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
// Solution 1: Expanded
|
||||
Column(
|
||||
children: [
|
||||
Text('Header'),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(leading: Icon(Icons.map), title: Text('Map')),
|
||||
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
// Solution 2: shrinkWrap
|
||||
Column(
|
||||
children: [
|
||||
Text('Header'),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
ListTile(leading: Icon(Icons.map), title: Text('Map')),
|
||||
ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### 'An InputDecorator...cannot have an unbounded width'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
An InputDecorator, which is typically created by a TextField, cannot have an unbounded width.
|
||||
This happens when the parent widget does not provide a finite width constraint.
|
||||
```
|
||||
|
||||
**Cause:** TextField or TextFormField inside Row without width constraints.
|
||||
|
||||
**Solution:** Wrap in `Expanded` or `SizedBox`.
|
||||
|
||||
```dart
|
||||
// Problem
|
||||
Row(
|
||||
children: [TextField()],
|
||||
)
|
||||
|
||||
// Solution 1: Expanded
|
||||
Row(
|
||||
children: [Expanded(child: TextFormField())],
|
||||
)
|
||||
|
||||
// Solution 2: SizedBox
|
||||
Row(
|
||||
children: [SizedBox(width: 200, child: TextFormField())],
|
||||
)
|
||||
```
|
||||
|
||||
## Widget Lifecycle Errors
|
||||
|
||||
### 'setState called during build'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
setState() or markNeedsBuild() called during build.
|
||||
This Overlay widget cannot be marked as needing to build because the framework
|
||||
is already in the process of building widgets.
|
||||
```
|
||||
|
||||
**Cause:** Calling `setState` (or methods that call it like `showDialog`) directly in the `build` method.
|
||||
|
||||
**Solution:** Use `WidgetsBinding.instance.addPostFrameCallback` or Navigator API.
|
||||
|
||||
```dart
|
||||
// Problem
|
||||
Widget build(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(title: Text('Error!')),
|
||||
);
|
||||
return Center(child: Text('Show Dialog'));
|
||||
}
|
||||
|
||||
// Solution 1: Post-frame callback
|
||||
Widget build(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(title: Text('Success!')),
|
||||
);
|
||||
});
|
||||
return Center(child: Text('Show Dialog'));
|
||||
}
|
||||
|
||||
// Solution 2: Navigator API (for initial navigation)
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) => DialogScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('Show Dialog'),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Parent Widget Errors
|
||||
|
||||
### 'Incorrect use of ParentData widget'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
The following assertion was thrown while looking for parent data:
|
||||
Incorrect use of ParentDataWidget.
|
||||
Usually, this indicates that at least one of the offending ParentDataWidgets
|
||||
listed above is not placed directly inside a compatible ancestor widget.
|
||||
```
|
||||
|
||||
**Cause:** Using widgets that require specific parent widgets in wrong context.
|
||||
|
||||
**Common Solutions:**
|
||||
|
||||
| Widget | Expected Parent | Solution |
|
||||
|---------------------|---------------------|-----------------------------------|
|
||||
| `Flexible` | `Row`, `Column`, `Flex` | Ensure parent is Row/Column/Flex |
|
||||
| `Expanded` | `Row`, `Column`, `Flex` | Ensure parent is Row/Column/Flex |
|
||||
| `Positioned` | `Stack` | Wrap in Stack |
|
||||
| `TableCell` | `Table` | Ensure parent is Table |
|
||||
|
||||
```dart
|
||||
// Problem
|
||||
Column(
|
||||
children: [
|
||||
Expanded(child: Text('Wrong!')), // Expanded needs Row/Column parent, not Column children
|
||||
],
|
||||
)
|
||||
|
||||
// Solution
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Text('Correct!')),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Testing-Specific Errors
|
||||
|
||||
### 'WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp.
|
||||
```
|
||||
|
||||
**Cause:** Pumping widget without MaterialApp context.
|
||||
|
||||
**Solution:** Wrap widget in MaterialApp.
|
||||
|
||||
```dart
|
||||
// Problem
|
||||
testWidgets('test without MaterialApp', (tester) async {
|
||||
await tester.pumpWidget(MyWidget());
|
||||
});
|
||||
|
||||
// Solution
|
||||
testWidgets('test with MaterialApp', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MyWidget(),
|
||||
),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 'No Finder found'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
No Finder found.
|
||||
Test failed: No elements found matching the finder.
|
||||
```
|
||||
|
||||
**Cause:** Widget not found in tree at time of test.
|
||||
|
||||
**Common Solutions:**
|
||||
|
||||
```dart
|
||||
// 1. Wrong text/content
|
||||
expect(find.text('Wrong Text'), findsNothing); // Error
|
||||
|
||||
// Solution: Use correct text
|
||||
expect(find.text('Correct Text'), findsOneWidget);
|
||||
|
||||
// 2. Widget not built yet
|
||||
testWidgets('widget not built', (tester) async {
|
||||
await tester.pumpWidget(MyWidget());
|
||||
expect(find.text('Async Text'), findsOneWidget); // Error
|
||||
});
|
||||
|
||||
// Solution: Pump and settle for async operations
|
||||
testWidgets('widget not built', (tester) async {
|
||||
await tester.pumpWidget(MyWidget());
|
||||
await tester.pumpAndSettle(); // Wait for async operations
|
||||
expect(find.text('Async Text'), findsOneWidget);
|
||||
});
|
||||
|
||||
// 3. Wrong finder type
|
||||
testWidgets('wrong finder', (tester) async {
|
||||
await tester.pumpWidget(TextButton(child: Text('Button')));
|
||||
await tester.tap(find.byType(ElevatedButton)); // Error
|
||||
});
|
||||
|
||||
// Solution: Use correct type or use Text finder
|
||||
testWidgets('correct finder', (tester) async {
|
||||
await tester.pumpWidget(TextButton(child: Text('Button')));
|
||||
await tester.tap(find.byType(TextButton)); // Correct
|
||||
});
|
||||
```
|
||||
|
||||
## Plugin/Platform Errors
|
||||
|
||||
### 'MissingPluginException'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
MissingPluginException(No implementation found for method MethodName on channel channel.name)
|
||||
```
|
||||
|
||||
**Cause:** Plugin not registered or mock not set up in tests.
|
||||
|
||||
**Solution:** Mock the method channel in tests.
|
||||
|
||||
```dart
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('your.plugin.channel'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'getPlatformVersion') {
|
||||
return 'Android 12';
|
||||
}
|
||||
throw MissingPluginException();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('your.plugin.channel'),
|
||||
null,
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Async Errors
|
||||
|
||||
### 'TimeoutException'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
TimeoutException after 0:00:05.000000: Test timed out after 5 seconds.
|
||||
```
|
||||
|
||||
**Cause:** Async operation not completing or `pumpAndSettle()` waiting indefinitely.
|
||||
|
||||
**Solution:** Set timeout or fix infinite loops.
|
||||
|
||||
```dart
|
||||
// Solution 1: Increase timeout
|
||||
testWidgets(
|
||||
'long-running test',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(MyWidget());
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
},
|
||||
timeout: const Timeout(Duration(minutes: 1)),
|
||||
);
|
||||
|
||||
// Solution 2: Fix infinite pumpAndSettle
|
||||
testWidgets('fix infinite settle', (tester) async {
|
||||
await tester.pumpWidget(MyWidget());
|
||||
await tester.pump(); // Use pump() instead of pumpAndSettle() if animation loops
|
||||
});
|
||||
```
|
||||
|
||||
## Data Errors
|
||||
|
||||
### 'RangeError'
|
||||
|
||||
**Error Message:**
|
||||
```
|
||||
RangeError: Index out of range: index should be less than 5, but is 5
|
||||
```
|
||||
|
||||
**Cause:** Accessing list/array with invalid index.
|
||||
|
||||
**Solution:** Check bounds before access or handle empty cases.
|
||||
|
||||
```dart
|
||||
// Problem
|
||||
testWidgets('range error', (tester) async {
|
||||
final items = [1, 2, 3, 4, 5];
|
||||
expect(items[5], 6); // Error: index 5 out of range
|
||||
});
|
||||
|
||||
// Solution 1: Check bounds
|
||||
testWidgets('safe access', (tester) async {
|
||||
final items = [1, 2, 3, 4, 5];
|
||||
if (items.length > 5) {
|
||||
expect(items[5], 6);
|
||||
}
|
||||
});
|
||||
|
||||
// Solution 2: Handle empty lists
|
||||
testWidgets('handle empty list', (tester) async {
|
||||
final items = <int>[];
|
||||
if (items.isEmpty) {
|
||||
expect(find.text('No items'), findsOneWidget);
|
||||
} else {
|
||||
expect(items.first, 1);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Enable Verbose Logging
|
||||
|
||||
```bash
|
||||
flutter test --verbose
|
||||
```
|
||||
|
||||
### Take Screenshots in Integration Tests
|
||||
|
||||
```dart
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
testWidgets('debug with screenshot', (tester) async {
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
|
||||
as IntegrationTestWidgetsFlutterBinding;
|
||||
|
||||
await tester.pumpWidget(MyWidget());
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
await tester.pump();
|
||||
await binding.takeScreenshot('debug-state');
|
||||
});
|
||||
```
|
||||
|
||||
### Print Widget Tree
|
||||
|
||||
```dart
|
||||
testWidgets('print tree', (tester) async {
|
||||
await tester.pumpWidget(MyWidget());
|
||||
debugDumpApp();
|
||||
});
|
||||
```
|
||||
|
||||
### Use Debug Flags
|
||||
|
||||
```dart
|
||||
testWidgets('debug flags', (tester) async {
|
||||
debugPrint('Current state: ...');
|
||||
await tester.pumpWidget(MyWidget());
|
||||
debugPrint('Widget tree: ${tester.widgetList(find.byType(MyWidget))}');
|
||||
});
|
||||
```
|
||||
|
||||
## Prevention
|
||||
|
||||
### Use Type Safety
|
||||
|
||||
```dart
|
||||
// Good: Use type-safe access
|
||||
final text = tester.widget<Text>(find.byType(Text));
|
||||
|
||||
// Avoid: Unsafe casts
|
||||
final text = find.byType(Text).evaluate().first.widget as Text;
|
||||
```
|
||||
|
||||
### Check Before Actions
|
||||
|
||||
```dart
|
||||
testWidgets('safe tap', (tester) async {
|
||||
await tester.pumpWidget(MyWidget());
|
||||
|
||||
final button = find.byType(ElevatedButton);
|
||||
if (button.evaluate().isNotEmpty) {
|
||||
await tester.tap(button);
|
||||
} else {
|
||||
fail('Button not found');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Handle All Cases
|
||||
|
||||
```dart
|
||||
testWidgets('comprehensive', (tester) async {
|
||||
await tester.pumpWidget(MyWidget());
|
||||
|
||||
// Check all possible states
|
||||
if (find.text('Loading').evaluate().isNotEmpty) {
|
||||
// Handle loading state
|
||||
} else if (find.text('Error').evaluate().isNotEmpty) {
|
||||
// Handle error state
|
||||
} else {
|
||||
// Handle success state
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -1,562 +0,0 @@
|
||||
# Integration Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Integration tests test complete apps or large parts of apps on real devices or emulators. They verify that all widgets and services work together as expected and can measure performance.
|
||||
|
||||
## When to Write Integration Tests
|
||||
|
||||
- Testing complete user flows across multiple screens
|
||||
- Verifying navigation between pages
|
||||
- Testing state persistence
|
||||
- Validating real device behavior (sensors, camera, etc.)
|
||||
- Performance profiling and benchmarking
|
||||
- Testing platform-specific features
|
||||
|
||||
## Setup
|
||||
|
||||
### Add Dependency
|
||||
|
||||
Add to `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
```
|
||||
|
||||
### Create Test Driver
|
||||
|
||||
Create `test_driver/integration_test.dart` when using `flutter drive`, browser
|
||||
integration tests, screenshots, or response data from performance traces:
|
||||
|
||||
```dart
|
||||
import 'package:integration_test/integration_test_driver.dart';
|
||||
|
||||
Future<void> main() => integrationDriver();
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Basic Integration Test
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:my_app/main.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('app launches and displays home', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
expect(find.text('Home Screen'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Running Integration Tests
|
||||
|
||||
```bash
|
||||
# Run on all devices
|
||||
flutter test integration_test/
|
||||
|
||||
# Run on specific device
|
||||
flutter test -d <device-id> integration_test/my_test.dart
|
||||
|
||||
# Run specific test file
|
||||
flutter test integration_test/my_test.dart
|
||||
|
||||
# Run a browser/driver integration test
|
||||
flutter drive \
|
||||
--driver=test_driver/integration_test.dart \
|
||||
--target=integration_test/my_test.dart \
|
||||
-d chrome
|
||||
```
|
||||
|
||||
## Testing User Flows
|
||||
|
||||
### Multi-Screen Navigation
|
||||
|
||||
```dart
|
||||
testWidgets('complete user flow from login to dashboard', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Login screen
|
||||
expect(find.text('Login'), findsOneWidget);
|
||||
|
||||
// Enter credentials
|
||||
await tester.enterText(find.byKey(const Key('username')), 'user@example.com');
|
||||
await tester.enterText(find.byKey(const Key('password')), 'password123');
|
||||
|
||||
// Tap login
|
||||
await tester.tap(find.byKey(const Key('login-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Dashboard screen
|
||||
expect(find.text('Dashboard'), findsOneWidget);
|
||||
expect(find.text('Welcome, user@example.com'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Tab Navigation
|
||||
|
||||
```dart
|
||||
testWidgets('navigate between tabs', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Initial tab
|
||||
expect(find.text('Home'), findsOneWidget);
|
||||
|
||||
// Tap second tab
|
||||
await tester.tap(find.text('Profile'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Profile'), findsOneWidget);
|
||||
|
||||
// Navigate back
|
||||
await tester.tap(find.text('Home'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Home'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Forms
|
||||
|
||||
### Complete Form Submission
|
||||
|
||||
```dart
|
||||
testWidgets('submit registration form', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Navigate to registration
|
||||
await tester.tap(find.text('Sign Up'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Fill form
|
||||
await tester.enterText(find.byKey(const Key('name')), 'John Doe');
|
||||
await tester.enterText(find.byKey(const Key('email')), 'john@example.com');
|
||||
await tester.enterText(find.byKey(const Key('password')), 'password123');
|
||||
await tester.enterText(find.byKey(const Key('confirm-password')), 'password123');
|
||||
|
||||
// Submit
|
||||
await tester.tap(find.byKey(const Key('submit')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify success
|
||||
expect(find.text('Registration Successful'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Form Validation
|
||||
|
||||
```dart
|
||||
testWidgets('form shows validation errors', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
await tester.tap(find.text('Sign Up'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Submit empty form
|
||||
await tester.tap(find.byKey(const Key('submit')));
|
||||
await tester.pump();
|
||||
|
||||
// Verify errors
|
||||
expect(find.text('Name is required'), findsOneWidget);
|
||||
expect(find.text('Email is required'), findsOneWidget);
|
||||
expect(find.text('Password is required'), findsOneWidget);
|
||||
|
||||
// Fill with invalid email
|
||||
await tester.enterText(find.byKey(const Key('email')), 'invalid-email');
|
||||
await tester.tap(find.byKey(const Key('submit')));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Invalid email format'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Lists and Data
|
||||
|
||||
### Loading and Displaying Data
|
||||
|
||||
```dart
|
||||
testWidgets('load and display items from API', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Wait for data load
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify items loaded
|
||||
expect(find.byType(ListTile), findsWidgets);
|
||||
expect(find.text('Item 1'), findsOneWidget);
|
||||
expect(find.text('Item 2'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Pull-to-Refresh
|
||||
|
||||
```dart
|
||||
testWidgets('pull to refresh updates list', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Pull down
|
||||
await tester.drag(
|
||||
find.byType(RefreshIndicator),
|
||||
const Offset(0, 300),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify refreshed data
|
||||
expect(find.text('Updated Item 1'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Infinite Scrolling
|
||||
|
||||
```dart
|
||||
testWidgets('load more items on scroll', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Initial items
|
||||
expect(find.text('Item 20'), findsNothing);
|
||||
|
||||
// Scroll to bottom to trigger load more
|
||||
await tester.fling(
|
||||
find.byType(ListView),
|
||||
const Offset(0, -2000),
|
||||
10000,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// New items loaded
|
||||
expect(find.text('Item 20'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing State Persistence
|
||||
|
||||
### Persisted Settings
|
||||
|
||||
```dart
|
||||
testWidgets('user preference persists across app rebuild', (tester) async {
|
||||
final preferences = FakePreferencesStore();
|
||||
|
||||
await tester.pumpWidget(MyApp(preferences: preferences));
|
||||
|
||||
// Change setting
|
||||
await tester.tap(find.text('Dark Mode'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Rebuild the app with the same injected storage.
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pumpWidget(MyApp(preferences: preferences));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify setting persisted
|
||||
expect(find.text('Light Mode'), findsNothing);
|
||||
expect(find.text('Dark Mode'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Authentication State
|
||||
|
||||
```dart
|
||||
testWidgets('user stays logged in', (tester) async {
|
||||
final authStore = FakeAuthStore();
|
||||
|
||||
await tester.pumpWidget(MyApp(authStore: authStore));
|
||||
|
||||
// Login
|
||||
await tester.enterText(find.byKey(const Key('email')), 'user@example.com');
|
||||
await tester.enterText(find.byKey(const Key('password')), 'password');
|
||||
await tester.tap(find.byKey(const Key('login')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Dashboard'), findsOneWidget);
|
||||
|
||||
// Rebuild the app with the same injected auth store.
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pumpWidget(MyApp(authStore: authStore));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Still logged in
|
||||
expect(find.text('Dashboard'), findsOneWidget);
|
||||
expect(find.text('Login'), findsNothing);
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
### Measuring Frame Time
|
||||
|
||||
```dart
|
||||
testWidgets('measure scrolling performance', (tester) async {
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
|
||||
as IntegrationTestWidgetsFlutterBinding;
|
||||
|
||||
await tester.pumpWidget(const MyApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await binding.watchPerformance(() async {
|
||||
await tester.fling(
|
||||
find.byType(ListView),
|
||||
const Offset(0, -1000),
|
||||
5000,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}, reportKey: 'scrolling_performance');
|
||||
});
|
||||
```
|
||||
|
||||
### Tracking Build Count
|
||||
|
||||
```dart
|
||||
testWidgets('widget doesn't rebuild unnecessarily', (tester) async {
|
||||
int buildCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
buildCount++;
|
||||
return const MyWidget();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(buildCount, 1);
|
||||
|
||||
// Perform unrelated action
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should not rebuild
|
||||
expect(buildCount, 1);
|
||||
});
|
||||
```
|
||||
|
||||
### Memory Tracking on Native Targets
|
||||
|
||||
```dart
|
||||
import 'dart:io' show ProcessInfo;
|
||||
|
||||
testWidgets('monitor memory usage during scrolling', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
final initialMemory = ProcessInfo.currentRss;
|
||||
|
||||
await tester.fling(
|
||||
find.byType(ListView),
|
||||
const Offset(0, -5000),
|
||||
10000,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final finalMemory = ProcessInfo.currentRss;
|
||||
final memoryIncrease = finalMemory - initialMemory;
|
||||
|
||||
// Memory increase should be reasonable (< 10MB)
|
||||
expect(memoryIncrease, lessThan(10 * 1024 * 1024));
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Platform-Specific Features
|
||||
|
||||
### Permissions
|
||||
|
||||
```dart
|
||||
testWidgets('request and handle camera permission', (tester) async {
|
||||
final permissions = FakePermissionGateway(
|
||||
camera: PermissionStatus.granted,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MyApp(permissions: permissions));
|
||||
|
||||
// Request camera access
|
||||
await tester.tap(find.text('Take Photo'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify camera screen appears
|
||||
expect(find.byType(CameraPreview), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Deep Links
|
||||
|
||||
```dart
|
||||
testWidgets('handle deep link to product page', (tester) async {
|
||||
await tester.pumpWidget(const MyApp(initialRoute: '/product/123'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Product #123'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### In-App Purchases
|
||||
|
||||
```dart
|
||||
testWidgets('complete purchase flow', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
await tester.tap(find.text('Premium'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Subscribe'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify purchase completed
|
||||
expect(find.text('Premium Active'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Network
|
||||
|
||||
### Offline Behavior
|
||||
|
||||
```dart
|
||||
testWidgets('handle offline state gracefully', (tester) async {
|
||||
final connectivity = FakeConnectivity(isOnline: false);
|
||||
final api = FakeApiClient(networkError: const SocketException('offline'));
|
||||
|
||||
await tester.pumpWidget(MyApp(connectivity: connectivity, apiClient: api));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify offline message
|
||||
expect(find.text('No internet connection'), findsOneWidget);
|
||||
|
||||
// Simulate back online
|
||||
connectivity.isOnline = true;
|
||||
api.networkError = null;
|
||||
await tester.tap(find.byKey(const Key('retry-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Reload data
|
||||
expect(find.text('Data loaded'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```dart
|
||||
testWidgets('handle network errors', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Trigger API call
|
||||
await tester.tap(find.text('Load Data'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify error message displayed
|
||||
expect(find.text('Failed to load data'), findsOneWidget);
|
||||
expect(find.byKey(const Key('retry-button')), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Animations
|
||||
|
||||
### Smooth Animations
|
||||
|
||||
```dart
|
||||
testWidgets('animation runs smoothly', (tester) async {
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
|
||||
as IntegrationTestWidgetsFlutterBinding;
|
||||
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
await binding.traceAction(() async {
|
||||
await tester.tap(find.byKey(const Key('animate')));
|
||||
await tester.pumpAndSettle();
|
||||
}, reportKey: 'animation_timeline');
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test critical user paths** - Focus on important flows
|
||||
2. **Keep tests independent** - Each test should be standalone
|
||||
3. **Use descriptive names** - Explain what the test does
|
||||
4. **Wait for async operations** - Use `pumpAndSettle()`
|
||||
5. **Test real behavior** - Verify actual device behavior, not mocks
|
||||
6. **Measure performance** - Track frame times, memory, and build counts
|
||||
7. **Test edge cases** - Offline, errors, empty states
|
||||
8. **Keep tests fast** - Avoid unnecessary delays
|
||||
|
||||
## Debugging Integration Tests
|
||||
|
||||
### Take Screenshots
|
||||
|
||||
```dart
|
||||
testWidgets('debug test with screenshot', (tester) async {
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
|
||||
as IntegrationTestWidgetsFlutterBinding;
|
||||
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// On Android, call convertFlutterSurfaceToImage and pump before screenshots.
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
await tester.pump();
|
||||
await binding.takeScreenshot('test-screenshot');
|
||||
|
||||
// Perform actions
|
||||
await tester.tap(find.text('Button'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Take another screenshot
|
||||
await binding.takeScreenshot('after-tap');
|
||||
});
|
||||
```
|
||||
|
||||
### Print Widget Tree
|
||||
|
||||
```dart
|
||||
testWidgets('print widget tree for debugging', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Print widget tree
|
||||
debugDumpApp();
|
||||
});
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Integration Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.19.0'
|
||||
|
||||
- name: Get dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Run integration tests
|
||||
run: flutter test integration_test/
|
||||
```
|
||||
|
||||
### Test Reports
|
||||
|
||||
```bash
|
||||
# Generate test report
|
||||
flutter test integration_test/ --reporter expanded
|
||||
|
||||
# Generate JSON report
|
||||
flutter test integration_test/ --reporter json > test-results.json
|
||||
```
|
||||
@@ -1,588 +0,0 @@
|
||||
# Mocking Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Mocking replaces real dependencies with test doubles to isolate code and make tests deterministic. This guide covers mocking strategies for Flutter tests.
|
||||
|
||||
## When to Mock
|
||||
|
||||
- Testing code that depends on external services (APIs, databases)
|
||||
- Isolating components for unit tests
|
||||
- Simulating error conditions and edge cases
|
||||
- Making tests faster and more reliable
|
||||
- Testing code that uses plugins
|
||||
|
||||
## Using Mockito
|
||||
|
||||
### Setup
|
||||
|
||||
Add to `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dev_dependencies:
|
||||
build_runner: ^2.10.4
|
||||
mockito: ^5.6.1
|
||||
```
|
||||
|
||||
### Generate Mocks
|
||||
|
||||
Create a test file with a Mockito annotation and import the generated file:
|
||||
|
||||
```dart
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'user_repository_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([ApiClient, UserRepository])
|
||||
void main() {}
|
||||
```
|
||||
|
||||
Generate mocks:
|
||||
|
||||
```bash
|
||||
dart run build_runner build
|
||||
```
|
||||
|
||||
### Basic Mocking
|
||||
|
||||
```dart
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'my_service_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([ApiService])
|
||||
void main() {
|
||||
late MockApiService mockApiService;
|
||||
late MyService myService;
|
||||
|
||||
setUp(() {
|
||||
mockApiService = MockApiService();
|
||||
myService = MyService(mockApiService);
|
||||
});
|
||||
|
||||
test('calls API and returns data', () async {
|
||||
// Arrange
|
||||
when(mockApiService.fetchData())
|
||||
.thenAnswer((_) async => {'data': 'value'});
|
||||
|
||||
// Act
|
||||
final result = await myService.getData();
|
||||
|
||||
// Assert
|
||||
expect(result['data'], 'value');
|
||||
verify(mockApiService.fetchData()).called(1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Mocking Different Types
|
||||
|
||||
### Mocking Classes
|
||||
|
||||
```dart
|
||||
class DataService {
|
||||
Future<String> fetchData() async => 'real data';
|
||||
}
|
||||
|
||||
// In a real test file, generate MockDataService with @GenerateMocks([DataService]).
|
||||
test('mocks class method', () async {
|
||||
final mockService = MockDataService();
|
||||
|
||||
when(mockService.fetchData())
|
||||
.thenAnswer((_) async => 'mocked data');
|
||||
|
||||
final result = await mockService.fetchData();
|
||||
expect(result, 'mocked data');
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking Interfaces
|
||||
|
||||
```dart
|
||||
abstract class Storage {
|
||||
void save(String key, String value);
|
||||
String? load(String key);
|
||||
}
|
||||
|
||||
class FakeStorage implements Storage {
|
||||
final values = <String, String>{};
|
||||
|
||||
@override
|
||||
void save(String key, String value) {
|
||||
values[key] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
String? load(String key) => values[key];
|
||||
}
|
||||
|
||||
// Test
|
||||
test('uses a fake storage implementation', () {
|
||||
final storage = FakeStorage();
|
||||
|
||||
storage.save('key', 'value');
|
||||
|
||||
expect(storage.load('key'), 'value');
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking Streams
|
||||
|
||||
```dart
|
||||
class StreamService {
|
||||
Stream<int> get counterStream => StreamController<int>().stream;
|
||||
}
|
||||
|
||||
// Test
|
||||
test('mocks stream', () async {
|
||||
final mockService = MockStreamService();
|
||||
|
||||
final controller = StreamController<int>();
|
||||
when(mockService.counterStream).thenAnswer((_) => controller.stream);
|
||||
|
||||
expectLater(
|
||||
mockService.counterStream,
|
||||
emitsInOrder([1, 2, 3]),
|
||||
);
|
||||
|
||||
controller.add(1);
|
||||
controller.add(2);
|
||||
controller.add(3);
|
||||
await controller.close();
|
||||
});
|
||||
```
|
||||
|
||||
## Mockito Features
|
||||
|
||||
### Returning Values
|
||||
|
||||
```dart
|
||||
when(mockService.getData()).thenReturn('value');
|
||||
|
||||
when(mockService.getUser(id)).thenReturn(User(id: id, name: 'Test'));
|
||||
|
||||
when(mockService.getList()).thenReturn([1, 2, 3]);
|
||||
```
|
||||
|
||||
### Throwing Errors
|
||||
|
||||
```dart
|
||||
when(mockService.getData())
|
||||
.thenThrow(Exception('Network error'));
|
||||
|
||||
when(mockService.saveData(data))
|
||||
.thenThrow(FormatException('Invalid data'));
|
||||
```
|
||||
|
||||
### Async Responses
|
||||
|
||||
```dart
|
||||
when(mockService.fetchData())
|
||||
.thenAnswer((_) async => 'async data');
|
||||
|
||||
when(mockService.loadUser(id))
|
||||
.thenAnswer((_) async => Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() => User(id: id),
|
||||
));
|
||||
```
|
||||
|
||||
### Sequential Returns
|
||||
|
||||
```dart
|
||||
when(mockService.getData())
|
||||
.thenReturn('first')
|
||||
.thenReturn('second')
|
||||
.thenThrow(Exception('error'));
|
||||
|
||||
// First call returns 'first'
|
||||
expect(await mockService.getData(), 'first');
|
||||
|
||||
// Second call returns 'second'
|
||||
expect(await mockService.getData(), 'second');
|
||||
|
||||
// Third call throws
|
||||
await expectLater(mockService.getData(), throwsException);
|
||||
```
|
||||
|
||||
### Conditional Returns
|
||||
|
||||
```dart
|
||||
when(mockService.getUser(argThat(allOf(isA<String>(), contains('@')))))
|
||||
.thenReturn(User(email: 'test@example.com'));
|
||||
|
||||
when(mockService.getUser(argThat(predicate<String>((arg) => arg.length > 5))))
|
||||
.thenReturn(User(name: 'Long Name'));
|
||||
|
||||
// Matches email format
|
||||
expect(mockService.getUser('test@example.com').email, 'test@example.com');
|
||||
|
||||
// Matches length > 5
|
||||
expect(mockService.getUser('Long Name').name, 'Long Name');
|
||||
```
|
||||
|
||||
### Capturing Arguments
|
||||
|
||||
```dart
|
||||
mockService.save('key1', 'value1');
|
||||
mockService.save('key2', 'value2');
|
||||
|
||||
final captured = verify(mockService.save(captureAny, captureAny)).captured;
|
||||
|
||||
expect(captured, ['key1', 'value1', 'key2', 'value2']);
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
### Verify Calls
|
||||
|
||||
```dart
|
||||
mockService.getData();
|
||||
|
||||
verify(mockService.getData()).called(1);
|
||||
verify(mockService.getData()).called(greaterThan(0));
|
||||
verifyNever(mockService.otherMethod());
|
||||
```
|
||||
|
||||
### Verify In Order
|
||||
|
||||
```dart
|
||||
mockService.first();
|
||||
mockService.second();
|
||||
mockService.third();
|
||||
|
||||
verifyInOrder([
|
||||
mockService.first(),
|
||||
mockService.second(),
|
||||
mockService.third(),
|
||||
]);
|
||||
```
|
||||
|
||||
### Verify Specific Arguments
|
||||
|
||||
```dart
|
||||
mockService.save('key', 'value');
|
||||
|
||||
verify(mockService.save('key', 'value')).called(1);
|
||||
verify(mockService.save(argThat(isA<String>()), any)).called(1);
|
||||
```
|
||||
|
||||
### Verify No More Interactions
|
||||
|
||||
```dart
|
||||
mockService.getData();
|
||||
|
||||
verify(mockService.getData()).called(1);
|
||||
verifyNoMoreInteractions(mockService);
|
||||
|
||||
verifyNoInteractions(otherMockService);
|
||||
```
|
||||
|
||||
## Mocking Platform Channels
|
||||
|
||||
### Basic Platform Channel Mock
|
||||
|
||||
```dart
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.example.app/channel'),
|
||||
(MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case 'getBatteryLevel':
|
||||
return 85;
|
||||
case 'openUrl':
|
||||
return true;
|
||||
default:
|
||||
throw MissingPluginException();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.example.app/channel'),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('gets battery level', () async {
|
||||
final platform = MethodChannel('com.example.app/channel');
|
||||
final result = await platform.invokeMethod('getBatteryLevel');
|
||||
expect(result, 85);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Platform Channel with Arguments
|
||||
|
||||
```dart
|
||||
setUp(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.example.app/storage'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'save') {
|
||||
final args = methodCall.arguments as Map;
|
||||
storage[args['key']] = args['value'];
|
||||
return true;
|
||||
} else if (methodCall.method == 'load') {
|
||||
final args = methodCall.arguments as Map;
|
||||
return storage[args['key']];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('saves and loads from platform storage', () async {
|
||||
final platform = MethodChannel('com.example.app/storage');
|
||||
|
||||
await platform.invokeMethod('save', {'key': 'test', 'value': 'data'});
|
||||
final result = await platform.invokeMethod('load', {'key': 'test'});
|
||||
|
||||
expect(result, 'data');
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking Repositories
|
||||
|
||||
### Data Repository
|
||||
|
||||
```dart
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'user_repository_test.mocks.dart';
|
||||
|
||||
class UserRepository {
|
||||
final ApiClient apiClient;
|
||||
|
||||
UserRepository(this.apiClient);
|
||||
|
||||
Future<User> getUser(String id) async {
|
||||
final data = await apiClient.get('/users/$id');
|
||||
return User.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
@GenerateMocks([ApiClient])
|
||||
void main() {
|
||||
late MockApiClient mockApiClient;
|
||||
late UserRepository userRepository;
|
||||
|
||||
setUp(() {
|
||||
mockApiClient = MockApiClient();
|
||||
userRepository = UserRepository(mockApiClient);
|
||||
});
|
||||
|
||||
test('fetches and parses user', () async {
|
||||
when(mockApiClient.get('/users/123'))
|
||||
.thenAnswer((_) async => {
|
||||
'id': '123',
|
||||
'name': 'John Doe',
|
||||
'email': 'john@example.com',
|
||||
});
|
||||
|
||||
final user = await userRepository.getUser('123');
|
||||
|
||||
expect(user.id, '123');
|
||||
expect(user.name, 'John Doe');
|
||||
verify(mockApiClient.get('/users/123')).called(1);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Mocking State Management
|
||||
|
||||
### BLoC Mock
|
||||
|
||||
```dart
|
||||
class CounterBloc extends Bloc<CounterEvent, CounterState> {
|
||||
CounterBloc() : super(CounterInitial()) {
|
||||
on<IncrementEvent>((event, emit) => emit(CounterIncremented()));
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
test('emits states on increment', () {
|
||||
final bloc = CounterBloc();
|
||||
|
||||
expectLater(bloc, emitsInOrder([
|
||||
CounterInitial(),
|
||||
CounterIncremented(),
|
||||
]));
|
||||
|
||||
bloc.add(IncrementEvent());
|
||||
});
|
||||
```
|
||||
|
||||
### Provider Mock
|
||||
|
||||
```dart
|
||||
class AuthProvider extends ChangeNotifier {
|
||||
bool _isLoggedIn = false;
|
||||
bool get isLoggedIn => _isLoggedIn;
|
||||
|
||||
void login() {
|
||||
_isLoggedIn = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
test('notifies listeners on login', () {
|
||||
final provider = AuthProvider();
|
||||
bool notified = false;
|
||||
|
||||
provider.addListener(() {
|
||||
notified = true;
|
||||
});
|
||||
|
||||
provider.login();
|
||||
|
||||
expect(notified, true);
|
||||
expect(provider.isLoggedIn, true);
|
||||
});
|
||||
```
|
||||
|
||||
## Manual Mocks
|
||||
|
||||
### Simple Manual Mock
|
||||
|
||||
```dart
|
||||
class ApiService {
|
||||
Future<String> fetchData() async => 'real data';
|
||||
}
|
||||
|
||||
class MockApiService implements ApiService {
|
||||
String? _mockedResponse;
|
||||
|
||||
void setMockResponse(String response) {
|
||||
_mockedResponse = response;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> fetchData() async => _mockedResponse ?? 'default mock';
|
||||
}
|
||||
|
||||
// Test
|
||||
test('uses manual mock', () async {
|
||||
final mockService = MockApiService();
|
||||
mockService.setMockResponse('test data');
|
||||
|
||||
final result = await mockService.fetchData();
|
||||
expect(result, 'test data');
|
||||
});
|
||||
```
|
||||
|
||||
### State-Based Mock
|
||||
|
||||
```dart
|
||||
class StatefulMockService implements ApiService {
|
||||
final List<String> responses = [];
|
||||
int _callCount = 0;
|
||||
|
||||
@override
|
||||
Future<String> fetchData() async {
|
||||
if (_callCount < responses.length) {
|
||||
return responses[_callCount++];
|
||||
}
|
||||
throw Exception('No more responses');
|
||||
}
|
||||
|
||||
void addResponse(String response) {
|
||||
responses.add(response);
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
test('returns sequential responses', () async {
|
||||
final mock = StatefulMockService();
|
||||
mock.addResponse('first');
|
||||
mock.addResponse('second');
|
||||
|
||||
expect(await mock.fetchData(), 'first');
|
||||
expect(await mock.fetchData(), 'second');
|
||||
|
||||
await expectLater(mock.fetchData(), throwsException);
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Mock at boundaries** - Mock external dependencies, not internal logic
|
||||
2. **Avoid over-mocking** - Only mock what's necessary for the test
|
||||
3. **Verify interactions** - Ensure mocks are called correctly
|
||||
4. **Use specific matchers** - Avoid `any` when possible
|
||||
5. **Clear expectations** - Make test intentions clear
|
||||
6. **Reset mocks** - Use setUp/tearDown to clean up
|
||||
7. **Test real behavior** - Verify expected outcomes, not implementation
|
||||
8. **Keep mocks simple** - Complex mocks indicate design issues
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Mock Chain
|
||||
|
||||
```dart
|
||||
when(mockService.first())
|
||||
.thenReturn(mockSecondService);
|
||||
when(mockSecondService.getData())
|
||||
.thenReturn('result');
|
||||
|
||||
final result = mockService.first().getData();
|
||||
expect(result, 'result');
|
||||
```
|
||||
|
||||
### Mock Repository
|
||||
|
||||
```dart
|
||||
class MockRepository {
|
||||
final Map<String, dynamic> _data = {};
|
||||
|
||||
T get<T>(String key) => _data[key] as T;
|
||||
void set<T>(String key, T value) => _data[key] = value;
|
||||
}
|
||||
|
||||
// Test
|
||||
test('uses mock repository', () {
|
||||
final repo = MockRepository();
|
||||
repo.set('name', 'John');
|
||||
|
||||
expect(repo.get<String>('name'), 'John');
|
||||
});
|
||||
```
|
||||
|
||||
### Mock Factory
|
||||
|
||||
```dart
|
||||
class ServiceFactory {
|
||||
static ApiService create() => RealApiService();
|
||||
}
|
||||
|
||||
class MockServiceFactory {
|
||||
ApiService? mockService;
|
||||
|
||||
ApiService create() => mockService ?? RealApiService();
|
||||
}
|
||||
|
||||
// Test
|
||||
test('uses factory mock', () {
|
||||
final factory = MockServiceFactory();
|
||||
factory.mockService = MockApiService();
|
||||
|
||||
when(factory.mockService!.getData()).thenReturn('mocked');
|
||||
|
||||
final service = factory.create();
|
||||
expect(service.getData(), 'mocked');
|
||||
});
|
||||
```
|
||||
@@ -1,493 +0,0 @@
|
||||
# Plugin Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Flutter plugins require special testing strategies because they include native code. This guide covers testing plugin packages with Dart, native, and integration tests.
|
||||
|
||||
## Test Types for Plugins
|
||||
|
||||
| Type | Tests | Runs On | Purpose |
|
||||
|----------------|---------------------------|------------------|-----------------------------------|
|
||||
| Dart Unit | Single classes/functions | Dart VM | Test Dart code in isolation |
|
||||
| Dart Widget | UI components | Test environment | Test widget behavior |
|
||||
| Dart Integration | Dart + Native bridge | Device/Emulator | Test full plugin functionality |
|
||||
| Native Unit | Native code | Native test env | Test native code in isolation |
|
||||
| Native UI | Native UI + Flutter UI | Device/Emulator | Test native UI interactions |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
my_plugin/
|
||||
├── lib/ # Dart code
|
||||
│ └── my_plugin.dart
|
||||
├── android/ # Android native code
|
||||
│ └── src/test/ # Android unit tests
|
||||
├── ios/ # iOS native code
|
||||
├── example/ # Example app
|
||||
│ ├── integration_test/ # Integration tests
|
||||
│ ├── ios/RunnerTests/ # iOS tests
|
||||
│ └── lib/
|
||||
├── test/ # Dart unit tests
|
||||
│ └── my_plugin_test.dart
|
||||
└── pubspec.yaml
|
||||
```
|
||||
|
||||
## Dart Unit Tests
|
||||
|
||||
### Basic Plugin Unit Test
|
||||
|
||||
```dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:my_plugin/my_plugin.dart';
|
||||
|
||||
void main() {
|
||||
test('plugin initializes correctly', () {
|
||||
final plugin = MyPlugin();
|
||||
expect(plugin.initialized, false);
|
||||
|
||||
plugin.initialize();
|
||||
expect(plugin.initialized, true);
|
||||
});
|
||||
|
||||
test('processes data correctly', () {
|
||||
final plugin = MyPlugin();
|
||||
final result = plugin.processData([1, 2, 3]);
|
||||
|
||||
expect(result, [2, 4, 6]);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Mocking Platform Channels in Unit Tests
|
||||
|
||||
```dart
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:my_plugin/my_plugin.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late MyPlugin plugin;
|
||||
|
||||
setUp(() {
|
||||
// Mock platform channel
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.example.my_plugin'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'getNativeValue') {
|
||||
return 42;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
plugin = MyPlugin();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.example.my_plugin'),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('gets value from native side', () async {
|
||||
final result = await plugin.getNativeValue();
|
||||
expect(result, 42);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Dart Integration Tests
|
||||
|
||||
### Setup
|
||||
|
||||
Add to `example/pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
my_plugin:
|
||||
path: ../
|
||||
```
|
||||
|
||||
### Basic Integration Test
|
||||
|
||||
```dart
|
||||
// example/integration_test/my_plugin_test.dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:my_plugin_example/main.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('plugin works in real app', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
final plugin = MyPlugin();
|
||||
final result = await plugin.getNativeValue();
|
||||
|
||||
expect(result, greaterThan(0));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Create Test Driver
|
||||
|
||||
```dart
|
||||
// test_driver/integration_test.dart
|
||||
import 'package:integration_test/integration_test_driver.dart';
|
||||
|
||||
Future<void> main() => integrationDriver();
|
||||
```
|
||||
|
||||
### Run Integration Tests
|
||||
|
||||
```bash
|
||||
# From example directory
|
||||
cd example
|
||||
flutter test integration_test/
|
||||
|
||||
# Or with driver
|
||||
flutter drive --target=integration_test/my_plugin_test.dart
|
||||
```
|
||||
|
||||
## Android Unit Tests
|
||||
|
||||
### Setup
|
||||
|
||||
Create test in `android/src/test/java/com/example/my_plugin/MyPluginTest.java`:
|
||||
|
||||
```java
|
||||
package com.example.my_plugin;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class MyPluginTest {
|
||||
|
||||
@Test
|
||||
public void testNativeMethod() {
|
||||
MyPlugin plugin = new MyPlugin();
|
||||
int result = plugin.calculate(10, 20);
|
||||
|
||||
assertEquals(30, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringProcessing() {
|
||||
MyPlugin plugin = new MyPlugin();
|
||||
String result = plugin.process("hello");
|
||||
|
||||
assertEquals("HELLO", result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Run Android Tests
|
||||
|
||||
```bash
|
||||
# From android directory
|
||||
cd example/android
|
||||
|
||||
# Run all tests
|
||||
./gradlew testDebugUnitTest
|
||||
|
||||
# Run specific test
|
||||
./gradlew test --tests MyPluginTest.testNativeMethod
|
||||
```
|
||||
|
||||
## iOS Unit Tests
|
||||
|
||||
### Setup
|
||||
|
||||
Create test in `example/ios/RunnerTests/RunnerTests.m`:
|
||||
|
||||
```objectivec
|
||||
#import <XCTest/XCTest.h>
|
||||
#import "MyPlugin.h"
|
||||
|
||||
@interface MyPluginTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation MyPluginTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
// Setup code
|
||||
}
|
||||
|
||||
- (void)testNativeMethod {
|
||||
MyPlugin *plugin = [[MyPlugin alloc] init];
|
||||
NSInteger result = [plugin calculateWithA:10 b:20];
|
||||
|
||||
XCTAssertEqual(result, 30);
|
||||
}
|
||||
|
||||
- (void)testStringProcessing {
|
||||
MyPlugin *plugin = [[MyPlugin alloc] init];
|
||||
NSString *result = [plugin processString:@"hello"];
|
||||
|
||||
XCTAssertEqualObjects(result, @"HELLO");
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
### Run iOS Tests
|
||||
|
||||
```bash
|
||||
# From ios directory
|
||||
cd example/ios
|
||||
|
||||
# Run tests
|
||||
xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug
|
||||
|
||||
# Or run from Xcode
|
||||
# Product > Test
|
||||
```
|
||||
|
||||
## Native UI Tests (Espresso/XCUITest)
|
||||
|
||||
### Android Espresso Test
|
||||
|
||||
```java
|
||||
// android/src/androidTest/java/com/example/my_plugin/UiTest.java
|
||||
package com.example.my_plugin;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class UiTest {
|
||||
|
||||
@Rule
|
||||
public ActivityTestRule<MainActivity> activityRule =
|
||||
new ActivityTestRule<>(MainActivity.class);
|
||||
|
||||
@Test
|
||||
public void testPluginButton() {
|
||||
onView(withText("Plugin Button"))
|
||||
.check(matches(isDisplayed()));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### iOS XCUITest
|
||||
|
||||
```objectivec
|
||||
// example/ios/RunnerUITests/RunnerUITests.m
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
@interface RunnerUITests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation RunnerUITests
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
XCUIApplication *app = [[XCUIApplication alloc] init];
|
||||
[app launch];
|
||||
}
|
||||
|
||||
- (void)testPluginButtonExists {
|
||||
XCUIApplication *app = [[XCUIApplication alloc] init];
|
||||
XCUIElement *button = app.buttons[@"Plugin Button"];
|
||||
|
||||
XCTAssertTrue(button.exists);
|
||||
}
|
||||
|
||||
- (void)testPluginButtonClick {
|
||||
XCUIApplication *app = [[XCUIApplication alloc] init];
|
||||
XCUIElement *button = app.buttons[@"Plugin Button"];
|
||||
|
||||
[button tap];
|
||||
|
||||
XCUIElement *result = app.staticTexts[@"Success"];
|
||||
XCTAssertTrue(result.exists);
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
## Testing Plugin Initialization
|
||||
|
||||
### Test Plugin Registration
|
||||
|
||||
```dart
|
||||
testWidgets('plugin registers correctly', (tester) async {
|
||||
final calls = <MethodCall>[];
|
||||
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.example.my_plugin'),
|
||||
(MethodCall methodCall) async {
|
||||
calls.add(methodCall);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
expect(calls.any((call) => call.method == 'initialize'), true);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Error Handling
|
||||
|
||||
### Native Error Handling
|
||||
|
||||
```dart
|
||||
testWidgets('handles native errors', (tester) async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
const MethodChannel('com.example.my_plugin'),
|
||||
(MethodCall methodCall) async {
|
||||
if (methodCall.method == 'failingMethod') {
|
||||
throw PlatformException(
|
||||
code: 'ERROR',
|
||||
message: 'Native error occurred',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
final plugin = MyPlugin();
|
||||
|
||||
expect(
|
||||
() => plugin.failingMethod(),
|
||||
throwsA(isA<PlatformException>()),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Platform-Specific Features
|
||||
|
||||
### Android-Specific Tests
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void testAndroidOnlyFeature() {
|
||||
MyPlugin plugin = new MyPlugin();
|
||||
|
||||
// Test Android-specific API
|
||||
Context context = InstrumentationRegistry.getTargetContext();
|
||||
String deviceModel = plugin.getDeviceModel(context);
|
||||
|
||||
assertNotNull(deviceModel);
|
||||
}
|
||||
```
|
||||
|
||||
### iOS-Specific Tests
|
||||
|
||||
```objectivec
|
||||
- (void)testIOSOnlyFeature {
|
||||
MyPlugin *plugin = [[MyPlugin alloc] init];
|
||||
|
||||
// Test iOS-specific API
|
||||
NSString *deviceName = plugin.getDeviceName();
|
||||
|
||||
XCTAssertNotNil(deviceName);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Multiple Platforms
|
||||
|
||||
### Platform Detection Test
|
||||
|
||||
```dart
|
||||
testWidgets('works on all platforms', (tester) async {
|
||||
final plugin = MyPlugin();
|
||||
final platform = await plugin.getPlatform();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
expect(platform, contains('Android'));
|
||||
} else if (Platform.isIOS) {
|
||||
expect(platform, contains('iOS'));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Performance
|
||||
|
||||
### Native Performance Test
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void testPerformance() {
|
||||
MyPlugin plugin = new MyPlugin();
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
plugin.heavyOperation();
|
||||
long endTime = System.nanoTime();
|
||||
|
||||
long duration = endTime - startTime;
|
||||
long maxDuration = 100_000_000; // 100ms
|
||||
|
||||
assertTrue("Operation too slow", duration < maxDuration);
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test at each layer** - Dart, native, and integration
|
||||
2. **Mock platform channels** - In Dart unit tests
|
||||
3. **Test error cases** - Native failures, network issues
|
||||
4. **Use integration tests** - Test full plugin functionality
|
||||
5. **Test on real devices** - For platform-specific features
|
||||
6. **Keep tests isolated** - Each test should be independent
|
||||
7. **Test all platforms** - Android, iOS, web, desktop
|
||||
8. **Document platform limitations** - Note platform-specific behavior
|
||||
|
||||
## CI/CD for Plugins
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
name: Plugin Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
dart-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v2
|
||||
- run: flutter test
|
||||
|
||||
android-tests:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run Android tests
|
||||
run: |
|
||||
cd example/android
|
||||
./gradlew testDebugUnitTest
|
||||
|
||||
ios-tests:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run iOS tests
|
||||
run: |
|
||||
cd example/ios
|
||||
xcodebuild test -workspace Runner.xcworkspace \
|
||||
-scheme Runner -configuration Debug
|
||||
```
|
||||
@@ -1,433 +0,0 @@
|
||||
# Unit Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Unit tests test individual functions, methods, or classes in isolation. They are the foundation of a well-tested Flutter app, providing fast feedback and high maintainability.
|
||||
|
||||
## When to Write Unit Tests
|
||||
|
||||
- Testing business logic functions
|
||||
- Validating data transformations
|
||||
- Testing state management logic (Bloc, Provider, Riverpod)
|
||||
- Mocking external services/API calls
|
||||
- Testing utility functions and helpers
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Basic Test
|
||||
|
||||
```dart
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('description', () {
|
||||
// Arrange
|
||||
final subject = MyClass();
|
||||
|
||||
// Act
|
||||
final result = subject.myMethod();
|
||||
|
||||
// Assert
|
||||
expect(result, expectedValue);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Group Tests
|
||||
|
||||
```dart
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('Counter', () {
|
||||
late Counter counter;
|
||||
|
||||
setUp(() {
|
||||
counter = Counter();
|
||||
});
|
||||
|
||||
test('value starts at 0', () {
|
||||
expect(counter.value, 0);
|
||||
});
|
||||
|
||||
test('increment increases value', () {
|
||||
counter.increment();
|
||||
expect(counter.value, 1);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
counter.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Testing Pure Functions
|
||||
|
||||
```dart
|
||||
// Function to test
|
||||
int add(int a, int b) => a + b;
|
||||
|
||||
// Test
|
||||
test('add returns sum of two numbers', () {
|
||||
expect(add(2, 3), 5);
|
||||
expect(add(-1, 1), 0);
|
||||
});
|
||||
```
|
||||
|
||||
### Testing State Changes
|
||||
|
||||
```dart
|
||||
class Counter {
|
||||
int _value = 0;
|
||||
int get value => _value;
|
||||
|
||||
void increment() => _value++;
|
||||
void decrement() => _value--;
|
||||
void reset() => _value = 0;
|
||||
}
|
||||
|
||||
// Tests
|
||||
group('Counter state changes', () {
|
||||
late Counter counter;
|
||||
|
||||
setUp(() => counter = Counter());
|
||||
|
||||
test('initial value is 0', () {
|
||||
expect(counter.value, 0);
|
||||
});
|
||||
|
||||
test('increment increases value by 1', () {
|
||||
counter.increment();
|
||||
expect(counter.value, 1);
|
||||
});
|
||||
|
||||
test('decrement decreases value by 1', () {
|
||||
counter.decrement();
|
||||
expect(counter.value, -1);
|
||||
});
|
||||
|
||||
test('multiple increments work correctly', () {
|
||||
counter.increment();
|
||||
counter.increment();
|
||||
counter.increment();
|
||||
expect(counter.value, 3);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Async Operations
|
||||
|
||||
```dart
|
||||
class DataService {
|
||||
Future<String> fetchData() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
return 'data';
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
test('fetchData returns data after delay', () async {
|
||||
final service = DataService();
|
||||
final result = await service.fetchData();
|
||||
expect(result, 'data');
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Streams
|
||||
|
||||
```dart
|
||||
class CounterStream {
|
||||
final _controller = StreamController<int>();
|
||||
Stream<int> get stream => _controller.stream;
|
||||
|
||||
void increment() => _controller.sink.add(1);
|
||||
void dispose() => _controller.close();
|
||||
}
|
||||
|
||||
// Test
|
||||
test('stream emits values', () async {
|
||||
final counter = CounterStream();
|
||||
|
||||
expectLater(
|
||||
counter.stream,
|
||||
emitsInOrder([1, 1, 1]),
|
||||
);
|
||||
|
||||
counter.increment();
|
||||
counter.increment();
|
||||
counter.increment();
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
counter.dispose();
|
||||
});
|
||||
```
|
||||
|
||||
## Matchers
|
||||
|
||||
### Common Matchers
|
||||
|
||||
```dart
|
||||
// Equality
|
||||
expect(actual, equals(expected));
|
||||
|
||||
// Not equal
|
||||
expect(actual, isNot(equals(expected)));
|
||||
|
||||
// Null checks
|
||||
expect(value, isNull);
|
||||
expect(value, isNotNull);
|
||||
|
||||
// Numeric comparisons
|
||||
expect(5, greaterThan(3));
|
||||
expect(5, lessThan(10));
|
||||
expect(5, greaterThanOrEqualTo(5));
|
||||
expect(5, lessThanOrEqualTo(5));
|
||||
|
||||
// Type checking
|
||||
expect(obj, isA<String>());
|
||||
expect(obj, isNot(isA<int>()));
|
||||
|
||||
// String matching
|
||||
expect(text, contains('substring'));
|
||||
expect(text, startsWith('prefix'));
|
||||
expect(text, endsWith('suffix'));
|
||||
expect(text, matches(RegExp(r'\d+')));
|
||||
|
||||
// Lists
|
||||
expect(list, isEmpty);
|
||||
expect(list, isNotEmpty);
|
||||
expect(list, hasLength(3));
|
||||
expect(list, contains(item));
|
||||
expect(list, orderedEquals([1, 2, 3]));
|
||||
```
|
||||
|
||||
### Custom Matchers
|
||||
|
||||
```dart
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class HasLength extends Matcher {
|
||||
final int expectedLength;
|
||||
HasLength(this.expectedLength);
|
||||
|
||||
@override
|
||||
bool matches(item, Map matchState) {
|
||||
return (item as List).length == expectedLength;
|
||||
}
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
return description.add('has length $expectedLength');
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
test('list has custom length', () {
|
||||
final list = [1, 2, 3];
|
||||
expect(list, HasLength(3));
|
||||
});
|
||||
```
|
||||
|
||||
## Exception Testing
|
||||
|
||||
```dart
|
||||
class Calculator {
|
||||
int divide(int a, int b) {
|
||||
if (b == 0) throw ArgumentError('Cannot divide by zero');
|
||||
return a ~/ b;
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
test('divide throws when dividing by zero', () {
|
||||
final calculator = Calculator();
|
||||
expect(
|
||||
() => calculator.divide(10, 0),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
|
||||
test('divide throws with specific message', () {
|
||||
final calculator = Calculator();
|
||||
expect(
|
||||
() => calculator.divide(10, 0),
|
||||
throwsA(isA<ArgumentError>()
|
||||
.having((e) => e.message, 'message', contains('zero'))),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking
|
||||
|
||||
### Using Mockito
|
||||
|
||||
```dart
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'user_service_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([ApiClient])
|
||||
void main() {
|
||||
group('UserService', () {
|
||||
late MockApiClient mockApiClient;
|
||||
late UserService userService;
|
||||
|
||||
setUp(() {
|
||||
mockApiClient = MockApiClient();
|
||||
userService = UserService(mockApiClient);
|
||||
});
|
||||
|
||||
test('fetches user data', () async {
|
||||
when(mockApiClient.getUser('123'))
|
||||
.thenAnswer((_) async => User(id: '123', name: 'John'));
|
||||
|
||||
final user = await userService.getUser('123');
|
||||
|
||||
expect(user.name, 'John');
|
||||
verify(mockApiClient.getUser('123')).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Generate the mock file with:
|
||||
|
||||
```bash
|
||||
dart run build_runner build
|
||||
```
|
||||
|
||||
### Manual Mocking
|
||||
|
||||
```dart
|
||||
class MockStorage implements Storage {
|
||||
String? savedData;
|
||||
|
||||
@override
|
||||
void save(String data) {
|
||||
savedData = data;
|
||||
}
|
||||
|
||||
@override
|
||||
String? load() => savedData;
|
||||
}
|
||||
|
||||
// Test
|
||||
test('saves and loads data', () {
|
||||
final mockStorage = MockStorage();
|
||||
final service = DataService(mockStorage);
|
||||
|
||||
service.saveData('test');
|
||||
expect(mockStorage.savedData, 'test');
|
||||
|
||||
final loaded = service.loadData();
|
||||
expect(loaded, 'test');
|
||||
});
|
||||
```
|
||||
|
||||
## Testing with Fake Clock
|
||||
|
||||
```dart
|
||||
import 'package:clock/clock.dart';
|
||||
|
||||
test('time-based operations', () {
|
||||
withClock(Clock(() => DateTime(2024, 1, 1)), () {
|
||||
final timestamp = DateTime.now();
|
||||
expect(timestamp.year, 2024);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
counter.dart
|
||||
test/
|
||||
counter/
|
||||
counter_test.dart
|
||||
counter_value_test.dart
|
||||
counter_operations_test.dart
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- Test files: `*_test.dart`
|
||||
- Test groups: Group related functionality
|
||||
- Test names: Describe what and why
|
||||
|
||||
```dart
|
||||
// Good
|
||||
test('value increments when increment is called', () {});
|
||||
|
||||
// Avoid
|
||||
test('increment', () {});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Arrange-Act-Assert** - Structure tests with clear sections
|
||||
2. **One assertion per test** - When possible, keep tests focused
|
||||
3. **Descriptive names** - Explain what and why, not just what
|
||||
4. **Avoid test interdependence** - Each test should be independent
|
||||
5. **Use setUp/tearDown** - Avoid code duplication
|
||||
6. **Test edge cases** - Test boundaries, nulls, and errors
|
||||
7. **Keep tests fast** - Avoid slow operations in unit tests
|
||||
8. **Mock external dependencies** - Tests should be deterministic
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Testing Implementation Details
|
||||
|
||||
```dart
|
||||
// Bad - tests implementation
|
||||
test('counter calls _update', () {
|
||||
verify(counter._update()).called(1);
|
||||
});
|
||||
|
||||
// Good - tests behavior
|
||||
test('counter value increases after increment', () {
|
||||
expect(counter.value, 1);
|
||||
});
|
||||
```
|
||||
|
||||
### Fragile Tests
|
||||
|
||||
```dart
|
||||
// Bad - depends on exact timing
|
||||
test('completes within 100ms', () async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
await operation();
|
||||
expect(stopwatch.elapsedMilliseconds, lessThan(100));
|
||||
});
|
||||
|
||||
// Good - tests completion
|
||||
test('completes successfully', () async {
|
||||
await operation();
|
||||
// operation completed without error
|
||||
});
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
flutter test
|
||||
|
||||
# Run specific file
|
||||
flutter test test/counter/counter_test.dart
|
||||
|
||||
# Run with coverage
|
||||
flutter test --coverage
|
||||
|
||||
# Run verbose
|
||||
flutter test --verbose
|
||||
|
||||
# Run specific test
|
||||
flutter test --name "value increments"
|
||||
```
|
||||
@@ -1,670 +0,0 @@
|
||||
# Widget Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Widget tests (component tests) verify that widgets render correctly and interact as expected. They run in a test environment that simulates widget lifecycle without a full UI system.
|
||||
|
||||
## When to Write Widget Tests
|
||||
|
||||
- Verifying widget rendering
|
||||
- Testing user interactions (taps, drags, scrolling)
|
||||
- Validating widget state changes
|
||||
- Testing different orientations and screen sizes
|
||||
- Testing form inputs and validation
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Basic Widget Test
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('widget description', (tester) async {
|
||||
// Build widget
|
||||
await tester.pumpWidget(const MyWidget());
|
||||
|
||||
// Find and verify
|
||||
expect(find.text('Hello'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Using MaterialApp
|
||||
|
||||
```dart
|
||||
testWidgets('widget with MaterialApp', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: MyWidget(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(MyWidget), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Finding Widgets
|
||||
|
||||
### Common Finders
|
||||
|
||||
```dart
|
||||
// By text
|
||||
find.text('Hello')
|
||||
find.textContaining('ello')
|
||||
find.textRegExp(RegExp(r'\d+'))
|
||||
|
||||
// By widget type
|
||||
find.byType(Text)
|
||||
find.byType(ElevatedButton)
|
||||
find.byType(MyWidget)
|
||||
|
||||
// By key
|
||||
find.byKey(const Key('my-button'))
|
||||
find.byKey(const ValueKey('counter'))
|
||||
|
||||
// By icon
|
||||
find.byIcon(Icons.add)
|
||||
|
||||
// By widget instance
|
||||
find.byWidget(myWidgetInstance)
|
||||
|
||||
// By specific widget ancestor
|
||||
find.ancestor(
|
||||
of: find.text('Child'),
|
||||
matching: find.byType(Column),
|
||||
)
|
||||
```
|
||||
|
||||
### Finder Matchers
|
||||
|
||||
```dart
|
||||
// Find exactly one
|
||||
expect(find.text('Hello'), findsOneWidget);
|
||||
|
||||
// Find multiple
|
||||
expect(find.text('Hello'), findsWidgets);
|
||||
|
||||
// Find nothing
|
||||
expect(find.text('Missing'), findsNothing);
|
||||
|
||||
// Find at least N
|
||||
expect(find.text('Item'), findsNWidgets(3));
|
||||
```
|
||||
|
||||
## User Interactions
|
||||
|
||||
### Tapping
|
||||
|
||||
```dart
|
||||
// Tap a widget
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
|
||||
// Trigger a frame
|
||||
await tester.pump();
|
||||
|
||||
// Wait for all animations
|
||||
await tester.pumpAndSettle();
|
||||
```
|
||||
|
||||
### Dragging
|
||||
|
||||
```dart
|
||||
// Drag a widget
|
||||
await tester.drag(
|
||||
find.byType(MyWidget),
|
||||
const Offset(0, -300),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
```
|
||||
|
||||
### Entering Text
|
||||
|
||||
```dart
|
||||
// Enter text into TextField
|
||||
await tester.enterText(
|
||||
find.byType(TextField),
|
||||
'Hello World',
|
||||
);
|
||||
|
||||
// Tap to focus, then enter
|
||||
await tester.tap(find.byType(TextField));
|
||||
await tester.enterText(find.byType(TextField), 'Text');
|
||||
|
||||
// Enter by key
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('email-field')),
|
||||
'test@example.com',
|
||||
);
|
||||
```
|
||||
|
||||
### Scrolling
|
||||
|
||||
```dart
|
||||
// Scroll a ListView
|
||||
await tester.fling(
|
||||
find.byType(ListView),
|
||||
const Offset(0, -500),
|
||||
10000,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Scroll until widget is visible
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Target Item'),
|
||||
500.0,
|
||||
);
|
||||
```
|
||||
|
||||
### Long Press
|
||||
|
||||
```dart
|
||||
await tester.longPress(find.byType(IconButton));
|
||||
await tester.pumpAndSettle();
|
||||
```
|
||||
|
||||
## Testing Widget Properties
|
||||
|
||||
### Text Content
|
||||
|
||||
```dart
|
||||
testWidgets('displays correct text', (tester) async {
|
||||
await tester.pumpWidget(const MyWidget(text: 'Hello'));
|
||||
|
||||
expect(find.text('Hello'), findsOneWidget);
|
||||
expect(find.text('Goodbye'), findsNothing);
|
||||
});
|
||||
```
|
||||
|
||||
### Widget State
|
||||
|
||||
```dart
|
||||
testWidgets('updates state on button press', (tester) async {
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Initial state
|
||||
expect(find.text('Count: 0'), findsOneWidget);
|
||||
|
||||
// Tap increment button
|
||||
await tester.tap(find.byKey(const Key('increment')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Updated state
|
||||
expect(find.text('Count: 1'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Conditional Rendering
|
||||
|
||||
```dart
|
||||
testWidgets('shows loading indicator when loading', (tester) async {
|
||||
await tester.pumpWidget(MyWidget(isLoading: true));
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
expect(find.byType(ElevatedButton), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows button when not loading', (tester) async {
|
||||
await tester.pumpWidget(MyWidget(isLoading: false));
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
expect(find.byType(ElevatedButton), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing with Arguments
|
||||
|
||||
```dart
|
||||
testWidgets('receives and displays title', (tester) async {
|
||||
const title = 'My Title';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MyWidget(title: title),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text(title), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('receives and uses callback', (tester) async {
|
||||
bool callbackCalled = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MyWidget(
|
||||
onPressed: () => callbackCalled = true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
expect(callbackCalled, true);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Orientation
|
||||
|
||||
```dart
|
||||
testWidgets('widget in portrait mode', (tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(400, 800));
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify portrait layout
|
||||
expect(find.byType(Column), findsOneWidget);
|
||||
|
||||
// Clean up
|
||||
addTearDown(tester.binding.setSurfaceSize(null));
|
||||
});
|
||||
|
||||
testWidgets('widget in landscape mode', (tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(800, 400));
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify landscape layout
|
||||
expect(find.byType(Row), findsOneWidget);
|
||||
|
||||
// Clean up
|
||||
addTearDown(tester.binding.setSurfaceSize(null));
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Scrollable Widgets
|
||||
|
||||
### ListView Scrolling
|
||||
|
||||
```dart
|
||||
testWidgets('scrolling in ListView', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ListView.builder(
|
||||
itemCount: 100,
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
title: Text('Item $index'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Initial item visible
|
||||
expect(find.text('Item 0'), findsOneWidget);
|
||||
expect(find.text('Item 50'), findsNothing);
|
||||
|
||||
// Scroll down
|
||||
await tester.fling(
|
||||
find.byType(ListView),
|
||||
const Offset(0, -5000),
|
||||
10000,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Later item now visible
|
||||
expect(find.text('Item 50'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Scroll Until Visible
|
||||
|
||||
```dart
|
||||
testWidgets('scroll until specific item is visible', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ListView.builder(
|
||||
itemCount: 100,
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
key: ValueKey('item-$index'),
|
||||
title: Text('Item $index'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Scroll until item 75 is visible
|
||||
await tester.scrollUntilVisible(
|
||||
find.byKey(const ValueKey('item-75')),
|
||||
500.0,
|
||||
);
|
||||
|
||||
expect(find.text('Item 75'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Forms
|
||||
|
||||
### TextField Validation
|
||||
|
||||
```dart
|
||||
testWidgets('validates email field', (tester) async {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: const Key('email'),
|
||||
validator: (value) {
|
||||
if (value == null || !value.contains('@')) {
|
||||
return 'Invalid email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
key: const Key('submit'),
|
||||
onPressed: () => formKey.currentState!.validate(),
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Enter invalid email
|
||||
await tester.enterText(find.byKey(const Key('email')), 'invalid');
|
||||
await tester.tap(find.byKey(const Key('submit')));
|
||||
await tester.pump();
|
||||
|
||||
// Should show error
|
||||
expect(find.text('Invalid email'), findsOneWidget);
|
||||
|
||||
// Enter valid email
|
||||
await tester.enterText(find.byKey(const Key('email')), 'test@example.com');
|
||||
await tester.tap(find.byKey(const Key('submit')));
|
||||
await tester.pump();
|
||||
|
||||
// Should hide error
|
||||
expect(find.text('Invalid email'), findsNothing);
|
||||
});
|
||||
```
|
||||
|
||||
### Multiple Form Fields
|
||||
|
||||
```dart
|
||||
testWidgets('form with multiple fields', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Form(
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: const Key('name'),
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
),
|
||||
TextFormField(
|
||||
key: const Key('email'),
|
||||
decoration: const InputDecoration(labelText: 'Email'),
|
||||
),
|
||||
ElevatedButton(
|
||||
key: const Key('submit'),
|
||||
onPressed: () {},
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Fill form
|
||||
await tester.enterText(find.byKey(const Key('name')), 'John Doe');
|
||||
await tester.enterText(find.byKey(const Key('email')), 'john@example.com');
|
||||
|
||||
// Submit
|
||||
await tester.tap(find.byKey(const Key('submit')));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Animations
|
||||
|
||||
```dart
|
||||
testWidgets('animation plays correctly', (tester) async {
|
||||
await tester.pumpWidget(const AnimatedWidget());
|
||||
|
||||
// Initial state
|
||||
expect(find.byType(Opacity), findsOneWidget);
|
||||
|
||||
// Trigger animation
|
||||
await tester.tap(find.byType(ElevatedButton));
|
||||
|
||||
// Pump animation frames
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Animation complete
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify final state
|
||||
final opacity = tester.widget<Opacity>(find.byType(Opacity));
|
||||
expect(opacity.opacity, 0.0);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Custom Widgets
|
||||
|
||||
### Testing InheritedWidgets
|
||||
|
||||
```dart
|
||||
class MyInheritedWidget extends InheritedWidget {
|
||||
final String data;
|
||||
|
||||
const MyInheritedWidget({
|
||||
super.key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(child: child);
|
||||
|
||||
static MyInheritedWidget of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(MyInheritedWidget oldWidget) =>
|
||||
data != oldWidget.data;
|
||||
}
|
||||
|
||||
testWidgets('inherited widget provides data', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MyInheritedWidget(
|
||||
data: 'Test Data',
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Text(MyInheritedWidget.of(context).data);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test Data'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Testing StatefulWidget
|
||||
|
||||
```dart
|
||||
class CounterWidget extends StatefulWidget {
|
||||
@override
|
||||
State<CounterWidget> createState() => _CounterWidgetState();
|
||||
}
|
||||
|
||||
class _CounterWidgetState extends State<CounterWidget> {
|
||||
int _count = 0;
|
||||
|
||||
void _increment() => setState(() => _count++);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text('Count: $_count'),
|
||||
ElevatedButton(
|
||||
key: const Key('increment'),
|
||||
onPressed: _increment,
|
||||
child: const Text('Increment'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('CounterWidget increments correctly', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: CounterWidget(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Initial count
|
||||
expect(find.text('Count: 0'), findsOneWidget);
|
||||
|
||||
// Increment
|
||||
await tester.tap(find.byKey(const Key('increment')));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Count: 1'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing with Mock Data
|
||||
|
||||
```dart
|
||||
class ProductList extends StatelessWidget {
|
||||
final List<Product> products;
|
||||
|
||||
const ProductList({super.key, required this.products});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: products.length,
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
title: Text(products[index].name),
|
||||
subtitle: Text('\$${products[index].price}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('displays list of products', (tester) async {
|
||||
final products = [
|
||||
Product(name: 'Product 1', price: 10.99),
|
||||
Product(name: 'Product 2', price: 20.99),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ProductList(products: products),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Product 1'), findsOneWidget);
|
||||
expect(find.text('\$10.99'), findsOneWidget);
|
||||
expect(find.text('Product 2'), findsOneWidget);
|
||||
expect(find.text('\$20.99'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Accessibility
|
||||
|
||||
```dart
|
||||
testWidgets('widget has proper semantics', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Semantics(
|
||||
label: 'Submit button',
|
||||
button: true,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final semantics = tester.getSemantics(find.byType(ElevatedButton));
|
||||
expect(semantics.label, 'Submit button');
|
||||
expect(semantics.hasAction(SemanticsAction.tap), true);
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use descriptive test names** - Explain what and why
|
||||
2. **Keep tests independent** - Each test should be standalone
|
||||
3. **Use pumpAndSettle()** - Wait for all animations
|
||||
4. **Test user behavior, not implementation** - Focus on what users see and do
|
||||
5. **Group related tests** - Use `group()` for organization
|
||||
6. **Test edge cases** - Empty lists, null values, errors
|
||||
7. **Use keys for widgets** - Makes finding easier and more reliable
|
||||
8. **Test accessibility** - Verify semantic labels and actions
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Testing Navigation
|
||||
|
||||
```dart
|
||||
testWidgets('navigates to detail screen', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
'/': (context) => const HomeScreen(),
|
||||
'/detail': (context) => const DetailScreen(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Tap navigation button
|
||||
await tester.tap(find.text('Go to Detail'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify navigation
|
||||
expect(find.text('Detail Screen'), findsOneWidget);
|
||||
expect(find.text('Home Screen'), findsNothing);
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Loading States
|
||||
|
||||
```dart
|
||||
testWidgets('shows loading then content', (tester) async {
|
||||
bool isLoading = true;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const Text('Content Loaded');
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
|
||||
// Simulate data load
|
||||
isLoading = false;
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
expect(find.text('Content Loaded'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SKILL_FILE="$ROOT_DIR/SKILL.md"
|
||||
|
||||
fail() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -f "$SKILL_FILE" ]] || fail "Missing SKILL.md"
|
||||
|
||||
ruby -ryaml -e '
|
||||
path = ARGV.fetch(0)
|
||||
text = File.read(path)
|
||||
unless text.start_with?("---\n")
|
||||
abort "ERROR: SKILL.md must start with YAML frontmatter"
|
||||
end
|
||||
parts = text.split(/^---\s*$/, 3)
|
||||
abort "ERROR: SKILL.md frontmatter is not closed" unless parts.length == 3
|
||||
frontmatter = YAML.safe_load(parts[1], permitted_classes: [], aliases: false)
|
||||
abort "ERROR: frontmatter must be a map" unless frontmatter.is_a?(Hash)
|
||||
name = frontmatter["name"]
|
||||
description = frontmatter["description"]
|
||||
abort "ERROR: missing frontmatter name" if name.to_s.empty?
|
||||
abort "ERROR: missing frontmatter description" if description.to_s.empty?
|
||||
expected = File.basename(File.dirname(path))
|
||||
abort "ERROR: frontmatter name #{name.inspect} does not match #{expected.inspect}" unless name == expected
|
||||
required_terms = %w[unit widget integration mock plugin]
|
||||
missing = required_terms.reject { |term| description.downcase.include?(term) }
|
||||
abort "ERROR: description missing trigger terms: #{missing.join(", ")}" unless missing.empty?
|
||||
' "$SKILL_FILE"
|
||||
|
||||
ruby -e '
|
||||
root = ARGV.fetch(0)
|
||||
failed = false
|
||||
Dir[File.join(root, "**/*.md")].sort.each do |file|
|
||||
File.readlines(file).each_with_index do |line, index|
|
||||
line.scan(/\[[^\]]+\]\(([^)]+)\)/).flatten.each do |target|
|
||||
next if target.match?(/\A(?:https?:|mailto:|#)/)
|
||||
path = target.split("#", 2).first
|
||||
next if path.empty?
|
||||
full_path = File.expand_path(path, File.dirname(file))
|
||||
unless File.exist?(full_path)
|
||||
warn "ERROR: missing local link #{file}:#{index + 1} -> #{target}"
|
||||
failed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
exit(failed ? 1 : 0)
|
||||
' "$ROOT_DIR"
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
count="$(grep -c '^```' "$file" || true)"
|
||||
if (( count % 2 != 0 )); then
|
||||
fail "Unbalanced fenced code blocks in $file"
|
||||
fi
|
||||
done < <(find "$ROOT_DIR" -name '*.md' -print0)
|
||||
|
||||
[[ -z "$(find "$ROOT_DIR" -name '.DS_Store' -print)" ]] || fail "Remove .DS_Store files from flutter-testing"
|
||||
|
||||
grep -q 'scripts/verify-examples.sh' "$SKILL_FILE" || fail "SKILL.md must route to scripts/verify-examples.sh"
|
||||
|
||||
for pattern in \
|
||||
'tester\.trace\(' \
|
||||
'tester\.takeScreenshot\(' \
|
||||
'flutter test --platform' \
|
||||
'--no-sound-null-safety' \
|
||||
'captureNamed' \
|
||||
'flutter pub run build_runner build' \
|
||||
'setSurfaceSize\(Size\.zero\)'
|
||||
do
|
||||
if rg -n --glob '*.md' -- "$pattern" "$ROOT_DIR/references"; then
|
||||
fail "Forbidden stale pattern found: $pattern"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "flutter-testing skill checks passed"
|
||||
@@ -1,177 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
@@ -1,513 +0,0 @@
|
||||
---
|
||||
name: seo
|
||||
description: Optimize for search engine visibility and ranking. Use when asked to "improve SEO", "optimize for search", "fix meta tags", "add structured data", "sitemap optimization", or "search engine optimization".
|
||||
license: MIT
|
||||
metadata:
|
||||
author: web-quality-skills
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# SEO optimization
|
||||
|
||||
Search engine optimization based on Lighthouse SEO audits and Google Search guidelines. Focus on technical SEO, on-page optimization, and structured data.
|
||||
|
||||
## SEO fundamentals
|
||||
|
||||
Search ranking factors (approximate influence):
|
||||
|
||||
| Factor | Influence | This Skill |
|
||||
|--------|-----------|------------|
|
||||
| Content quality & relevance | ~40% | Partial (structure) |
|
||||
| Backlinks & authority | ~25% | ✗ |
|
||||
| Technical SEO | ~15% | ✓ |
|
||||
| Page experience (Core Web Vitals) | ~10% | See [Core Web Vitals](../core-web-vitals/SKILL.md) |
|
||||
| On-page SEO | ~10% | ✓ |
|
||||
|
||||
---
|
||||
|
||||
## Technical SEO
|
||||
|
||||
### Crawlability
|
||||
|
||||
**robots.txt:**
|
||||
```text
|
||||
# /robots.txt
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Block admin/private areas
|
||||
Disallow: /admin/
|
||||
Disallow: /api/
|
||||
Disallow: /private/
|
||||
|
||||
# Don't block resources needed for rendering
|
||||
# ❌ Disallow: /static/
|
||||
|
||||
Sitemap: https://example.com/sitemap.xml
|
||||
```
|
||||
|
||||
**Meta robots:**
|
||||
```html
|
||||
<!-- Default: indexable, followable -->
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<!-- Noindex specific pages -->
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<!-- Indexable but don't follow links -->
|
||||
<meta name="robots" content="index, nofollow">
|
||||
|
||||
<!-- Control snippets -->
|
||||
<meta name="robots" content="max-snippet:150, max-image-preview:large">
|
||||
```
|
||||
|
||||
**Canonical URLs:**
|
||||
```html
|
||||
<!-- Prevent duplicate content issues -->
|
||||
<link rel="canonical" href="https://example.com/page">
|
||||
|
||||
<!-- Self-referencing canonical (recommended) -->
|
||||
<link rel="canonical" href="https://example.com/current-page">
|
||||
|
||||
<!-- For paginated content -->
|
||||
<link rel="canonical" href="https://example.com/products">
|
||||
<!-- Or use rel="prev" / rel="next" for explicit pagination -->
|
||||
```
|
||||
|
||||
### XML sitemap
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://example.com/</loc>
|
||||
<lastmod>2024-01-15</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://example.com/products</loc>
|
||||
<lastmod>2024-01-14</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
```
|
||||
|
||||
**Sitemap best practices:**
|
||||
- Maximum 50,000 URLs or 50MB per sitemap
|
||||
- Use sitemap index for larger sites
|
||||
- Include only canonical, indexable URLs
|
||||
- Update `lastmod` when content changes
|
||||
- Submit to Google Search Console
|
||||
|
||||
### URL structure
|
||||
|
||||
```
|
||||
✅ Good URLs:
|
||||
https://example.com/products/blue-widget
|
||||
https://example.com/blog/how-to-use-widgets
|
||||
|
||||
❌ Poor URLs:
|
||||
https://example.com/p?id=12345
|
||||
https://example.com/products/item/category/subcategory/blue-widget-2024-sale-discount
|
||||
```
|
||||
|
||||
**URL guidelines:**
|
||||
- Use hyphens, not underscores
|
||||
- Lowercase only
|
||||
- Keep short (< 75 characters)
|
||||
- Include target keywords naturally
|
||||
- Avoid parameters when possible
|
||||
- Use HTTPS always
|
||||
|
||||
### HTTPS & security
|
||||
|
||||
```html
|
||||
<!-- Ensure all resources use HTTPS -->
|
||||
<img src="https://example.com/image.jpg">
|
||||
|
||||
<!-- Not: -->
|
||||
<img src="http://example.com/image.jpg">
|
||||
```
|
||||
|
||||
**Security headers for SEO trust signals:**
|
||||
```
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## On-page SEO
|
||||
|
||||
### Title tags
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing or generic -->
|
||||
<title>Page</title>
|
||||
<title>Home</title>
|
||||
|
||||
<!-- ✅ Descriptive with primary keyword -->
|
||||
<title>Blue Widgets for Sale | Premium Quality | Example Store</title>
|
||||
```
|
||||
|
||||
**Title tag guidelines:**
|
||||
- 50-60 characters (Google truncates ~60)
|
||||
- Primary keyword near the beginning
|
||||
- Unique for every page
|
||||
- Brand name at end (unless homepage)
|
||||
- Action-oriented when appropriate
|
||||
|
||||
### Meta descriptions
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing or duplicate -->
|
||||
<meta name="description" content="">
|
||||
|
||||
<!-- ✅ Compelling and unique -->
|
||||
<meta name="description" content="Shop premium blue widgets with free shipping. 30-day returns. Rated 4.9/5 by 10,000+ customers. Order today and save 20%.">
|
||||
```
|
||||
|
||||
**Meta description guidelines:**
|
||||
- 150-160 characters
|
||||
- Include primary keyword naturally
|
||||
- Compelling call-to-action
|
||||
- Unique for every page
|
||||
- Matches page content
|
||||
|
||||
### Heading structure
|
||||
|
||||
```html
|
||||
<!-- ❌ Poor structure -->
|
||||
<h2>Welcome to Our Store</h2>
|
||||
<h4>Products</h4>
|
||||
<h1>Contact Us</h1>
|
||||
|
||||
<!-- ✅ Proper hierarchy -->
|
||||
<h1>Blue Widgets - Premium Quality</h1>
|
||||
<h2>Product Features</h2>
|
||||
<h3>Durability</h3>
|
||||
<h3>Design</h3>
|
||||
<h2>Customer Reviews</h2>
|
||||
<h2>Pricing</h2>
|
||||
```
|
||||
|
||||
**Heading guidelines:**
|
||||
- Single `<h1>` per page (the main topic)
|
||||
- Logical hierarchy (don't skip levels)
|
||||
- Include keywords naturally
|
||||
- Descriptive, not generic
|
||||
|
||||
### Image SEO
|
||||
|
||||
```html
|
||||
<!-- ❌ Poor image SEO -->
|
||||
<img src="IMG_12345.jpg">
|
||||
|
||||
<!-- ✅ Optimized image -->
|
||||
<img src="blue-widget-product-photo.webp"
|
||||
alt="Blue widget with chrome finish, side view showing control panel"
|
||||
width="800"
|
||||
height="600"
|
||||
loading="lazy">
|
||||
```
|
||||
|
||||
**Image guidelines:**
|
||||
- Descriptive filenames with keywords
|
||||
- Alt text describes the image content
|
||||
- Compressed and properly sized
|
||||
- WebP/AVIF with fallbacks
|
||||
- Lazy load below-fold images
|
||||
|
||||
### Internal linking
|
||||
|
||||
```html
|
||||
<!-- ❌ Non-descriptive -->
|
||||
<a href="/products">Click here</a>
|
||||
<a href="/widgets">Read more</a>
|
||||
|
||||
<!-- ✅ Descriptive anchor text -->
|
||||
<a href="/products/blue-widgets">Browse our blue widget collection</a>
|
||||
<a href="/guides/widget-maintenance">Learn how to maintain your widgets</a>
|
||||
```
|
||||
|
||||
**Linking guidelines:**
|
||||
- Descriptive anchor text with keywords
|
||||
- Link to relevant internal pages
|
||||
- Reasonable number of links per page
|
||||
- Fix broken links promptly
|
||||
- Use breadcrumbs for hierarchy
|
||||
|
||||
---
|
||||
|
||||
## Structured data (JSON-LD)
|
||||
|
||||
### Organization
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Example Company",
|
||||
"url": "https://example.com",
|
||||
"logo": "https://example.com/logo.png",
|
||||
"sameAs": [
|
||||
"https://twitter.com/example",
|
||||
"https://linkedin.com/company/example"
|
||||
],
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+1-555-123-4567",
|
||||
"contactType": "customer service"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Article
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "How to Choose the Right Widget",
|
||||
"description": "Complete guide to selecting widgets for your needs.",
|
||||
"image": "https://example.com/article-image.jpg",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Jane Smith",
|
||||
"url": "https://example.com/authors/jane-smith"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Example Blog",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://example.com/logo.png"
|
||||
}
|
||||
},
|
||||
"datePublished": "2024-01-15",
|
||||
"dateModified": "2024-01-20"
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Product
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"name": "Blue Widget Pro",
|
||||
"image": "https://example.com/blue-widget.jpg",
|
||||
"description": "Premium blue widget with advanced features.",
|
||||
"brand": {
|
||||
"@type": "Brand",
|
||||
"name": "WidgetCo"
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "49.99",
|
||||
"priceCurrency": "USD",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"url": "https://example.com/products/blue-widget"
|
||||
},
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.8",
|
||||
"reviewCount": "1250"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### FAQ
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What colors are available?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Our widgets come in blue, red, and green."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What is the warranty?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "All widgets include a 2-year warranty."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Breadcrumbs
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "https://example.com"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Products",
|
||||
"item": "https://example.com/products"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 3,
|
||||
"name": "Blue Widgets",
|
||||
"item": "https://example.com/products/blue-widgets"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Test structured data at:
|
||||
- [Google Rich Results Test](https://search.google.com/test/rich-results)
|
||||
- [Schema.org Validator](https://validator.schema.org/)
|
||||
|
||||
---
|
||||
|
||||
## Mobile SEO
|
||||
|
||||
### Responsive design
|
||||
|
||||
```html
|
||||
<!-- ❌ Not mobile-friendly -->
|
||||
<meta name="viewport" content="width=1024">
|
||||
|
||||
<!-- ✅ Responsive viewport -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
```
|
||||
|
||||
### Tap targets
|
||||
|
||||
```css
|
||||
/* ❌ Too small for mobile */
|
||||
.small-link {
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ✅ Adequate tap target */
|
||||
.mobile-friendly-link {
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
}
|
||||
```
|
||||
|
||||
### Font sizes
|
||||
|
||||
```css
|
||||
/* ❌ Too small on mobile */
|
||||
body {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ✅ Readable without zooming */
|
||||
body {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## International SEO
|
||||
|
||||
### Hreflang tags
|
||||
|
||||
```html
|
||||
<!-- For multi-language sites -->
|
||||
<link rel="alternate" hreflang="en" href="https://example.com/page">
|
||||
<link rel="alternate" hreflang="es" href="https://example.com/es/page">
|
||||
<link rel="alternate" hreflang="fr" href="https://example.com/fr/page">
|
||||
<link rel="alternate" hreflang="x-default" href="https://example.com/page">
|
||||
```
|
||||
|
||||
### Language declaration
|
||||
|
||||
```html
|
||||
<html lang="en">
|
||||
<!-- or -->
|
||||
<html lang="es-MX">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SEO audit checklist
|
||||
|
||||
### Critical
|
||||
- [ ] HTTPS enabled
|
||||
- [ ] robots.txt allows crawling
|
||||
- [ ] No `noindex` on important pages
|
||||
- [ ] Title tags present and unique
|
||||
- [ ] Single `<h1>` per page
|
||||
|
||||
### High priority
|
||||
- [ ] Meta descriptions present
|
||||
- [ ] Sitemap submitted
|
||||
- [ ] Canonical URLs set
|
||||
- [ ] Mobile-responsive
|
||||
- [ ] Core Web Vitals passing
|
||||
|
||||
### Medium priority
|
||||
- [ ] Structured data implemented
|
||||
- [ ] Internal linking strategy
|
||||
- [ ] Image alt text
|
||||
- [ ] Descriptive URLs
|
||||
- [ ] Breadcrumb navigation
|
||||
|
||||
### Ongoing
|
||||
- [ ] Fix crawl errors in Search Console
|
||||
- [ ] Update sitemap when content changes
|
||||
- [ ] Monitor ranking changes
|
||||
- [ ] Check for broken links
|
||||
- [ ] Review Search Console insights
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Use |
|
||||
|------|-----|
|
||||
| Google Search Console | Monitor indexing, fix issues |
|
||||
| Google PageSpeed Insights | Performance + Core Web Vitals |
|
||||
| Rich Results Test | Validate structured data |
|
||||
| Lighthouse | Full SEO audit |
|
||||
| Screaming Frog | Crawl analysis |
|
||||
|
||||
## References
|
||||
|
||||
- [Google Search Central](https://developers.google.com/search)
|
||||
- [Schema.org](https://schema.org/)
|
||||
- [Core Web Vitals](../core-web-vitals/SKILL.md)
|
||||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||
@@ -1,45 +0,0 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
@@ -1,45 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "924134a44c189315be2148659913dda1671cbe99"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
base_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
- platform: android
|
||||
create_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
base_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
- platform: ios
|
||||
create_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
base_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
- platform: linux
|
||||
create_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
base_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
- platform: macos
|
||||
create_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
base_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
- platform: web
|
||||
create_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
base_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
- platform: windows
|
||||
create_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
base_revision: 924134a44c189315be2148659913dda1671cbe99
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -1,3 +0,0 @@
|
||||
# musify_flutter
|
||||
|
||||
A new Flutter project.
|
||||
@@ -1,9 +0,0 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
prefer_const_constructors: true
|
||||
prefer_const_constructors_in_immutables: true
|
||||
prefer_final_locals: true
|
||||
avoid_print: true
|
||||
use_key_in_widget_constructors: true
|
||||
@@ -1,14 +0,0 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -1,45 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.musify_flutter"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.musify_flutter"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="musify_flutter"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.example.musify_flutter
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
Before Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,7 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -1,24 +0,0 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# This newDsl flag was added by the Flutter template
|
||||
android.newDsl=false
|
||||
# This builtInKotlin flag was added by the Flutter template
|
||||
android.builtInKotlin=false
|
||||
@@ -1,5 +0,0 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
|
||||
@@ -1,26 +0,0 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "9.0.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
@@ -1,3 +0,0 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -1,34 +0,0 @@
|
||||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1 +0,0 @@
|
||||
#include "Generated.xcconfig"
|
||||
@@ -1 +0,0 @@
|
||||
#include "Generated.xcconfig"
|
||||
@@ -1,644 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.musifyFlutter;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.musifyFlutter.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.musifyFlutter.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.musifyFlutter.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.musifyFlutter;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.musifyFlutter;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,119 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,16 +0,0 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 762 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
@@ -1,5 +0,0 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
@@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,70 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Musify Flutter</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>musify_flutter</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1 +0,0 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -1,6 +0,0 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: FlutterSceneDelegate {
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'src/app.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const ProviderScope(child: MusifyApp()));
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'core/router/app_router.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
|
||||
class MusifyApp extends StatelessWidget {
|
||||
const MusifyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Musify',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.light(),
|
||||
darkTheme: AppTheme.dark(),
|
||||
themeMode: ThemeMode.system,
|
||||
routerConfig: appRouter,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:oidc/oidc.dart';
|
||||
|
||||
import 'auth_repository.dart';
|
||||
|
||||
final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
||||
final repository = AuthRepository();
|
||||
ref.onDispose(repository.dispose);
|
||||
return repository;
|
||||
});
|
||||
|
||||
final authUserProvider = StreamProvider<OidcUser?>((ref) async* {
|
||||
final repository = ref.watch(authRepositoryProvider);
|
||||
await repository.init();
|
||||
yield* repository.userChanges;
|
||||
});
|
||||
|
||||
final isAuthenticatedProvider = Provider<bool>((ref) {
|
||||
return ref.watch(authUserProvider).valueOrNull != null;
|
||||
});
|
||||
|
||||
final currentUserIdProvider = Provider<String?>((ref) {
|
||||
return ref.watch(authUserProvider).valueOrNull?.uid;
|
||||
});
|
||||