chore: bring flutter client, docs, deploy and tools into monorepo

This commit is contained in:
2026-07-05 03:22:24 +02:00
parent fcc81e2a74
commit 7a931d2d6f
189 changed files with 19124 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
services:
seaweedfs:
image: chrislusf/seaweedfs:latest
container_name: seaweedfs
# All-in-one dev server: master + volume + filer + S3 gateway.
command: >
server
-ip.bind=0.0.0.0
-dir=/data
-master.volumeSizeLimitMB=1024
-volume.max=0
-filer
-s3
-s3.config=/etc/seaweedfs/s3.conf
ports:
- "9333:9333" # master (admin/UI)
- "8888:8888" # filer HTTP -> used by the Media Gateway
- "8333:8333" # S3 API -> used by the WebApi (InfrastructureStorage:Address)
# Volume server (8080) is left unpublished on purpose: it only needs to be
# reachable internally, and 8080 clashes with Zitadel on the host.
volumes:
- seaweedfs_data:/data
- ./s3.conf:/etc/seaweedfs/s3.conf:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:9333/cluster/healthz"]
interval: 10s
timeout: 5s
retries: 10
volumes:
seaweedfs_data:
+24
View File
@@ -0,0 +1,24 @@
{
"identities": [
{
"name": "admin_user",
"credentials": [
{
"accessKey": "admin_access_key",
"secretKey": "admin_secret_key"
}
],
"actions": ["Admin", "Read", "Write"]
},
{
"name": "read_only_user",
"credentials": [
{
"accessKey": "readonly_access_key",
"secretKey": "readonly_secret_key"
}
],
"actions": ["Read"]
}
]
}
+45
View File
@@ -0,0 +1,45 @@
# Musify — Documentación
Musify es un backend de streaming de música: subir pistas, procesarlas (transcodificar el audio a `.m4a` y generar miniaturas), organizarlas en playlists y reproducirlas en streaming sin que los bytes pasen por la API.
Esta carpeta explica **qué hace cada pieza, su propósito y por qué está diseñada así**.
## Índice
- [architecture.md](architecture.md) — Visión global: componentes, cómo encajan y los flujos principales.
- [projects.md](projects.md) — Qué es y para qué sirve cada proyecto/solución.
- [authentication.md](authentication.md) — Zitadel (OIDC), validación de JWT, provisioning/sync de usuarios.
- [storage.md](storage.md) — SeaweedFS (S3), estructura de claves, URLs prefirmadas, qué es público y qué privado.
- [streaming.md](streaming.md) — Streaming del audio (`.m4a` por HTTP range) con stream-ticket (RS256) y el reverse proxy (StreamingGateway).
- [media-processing.md](media-processing.md) — El worker y los workflows de MassTransit (transcode de audio, miniaturas).
- [development.md](development.md) — Cómo levantar todo en local: contenedores, puertos y comandos.
## Mapa de componentes (alto nivel)
```
┌─────────────────────────────────────────────┐
Cliente ───► │ WebApi (:5111) API REST + emite tickets │
└─────────────────────────────────────────────┘
│ EF Core │ presigned (subidas) │ MassTransit (eventos)
▼ ▼ ▼
PostgreSQL SeaweedFS S3 (:8333) RabbitMQ ──► Worker (procesa)
(metadatos) (objetos) audio→.m4a, imágenes
▲
Cliente ───► StreamingGateway (:8081) ┘ sirve audio .m4a (valida ticket, proxy al filer)
Login: Cliente ◄──► Zitadel (:8080, OIDC) → WebApi valida el JWT
```
## Stack
- **.NET 10**, C#. Minimal APIs (WebApi), Worker Service (Worker), ASP.NET + YARP (StreamingGateway).
- **CQRS** con [Mediator] (source-generator) y resultados con **Ardalis.Result**.
- **EF Core 10** + **PostgreSQL** (metadatos).
- **MassTransit 8** + **RabbitMQ** (procesado asíncrono con routing slips/sagas).
- **SeaweedFS** (almacenamiento de objetos compatible con S3, vía AWSSDK.S3).
- **ffmpeg** (transcode de audio a AAC/`.m4a`), **SixLabors.ImageSharp** (miniaturas).
- **Zitadel** (OIDC/OAuth2) para identidad.
## Repositorios git
Cada proyecto de nivel superior es su propio repo git: `MusifyBackend`, `Musify.WebApi`, `Musify.StreamingGateway`, `Musify.Worker`. La raíz del workspace (`docs/`, `deploy/`, `player.html`) no está versionada.
+55
View File
@@ -0,0 +1,55 @@
# Arquitectura
## Principios
- **Clean architecture / capas** en `MusifyBackend`: el dominio y la aplicación no dependen de infraestructura; la infraestructura implementa las interfaces (`Application/Contracts`).
- **CQRS**: cada operación es un `Command` o `Query` con su handler (Mediator). Los handlers devuelven `Ardalis.Result` (Ok/NotFound/Unauthorized/Conflict/Invalid…), que la WebApi traduce a códigos HTTP.
- **Los bytes pesados no pasan por la API**: subidas con URLs prefirmadas (cliente → S3) y reproducción vía un reverse proxy dedicado (gateway → S3). La WebApi solo mueve metadatos y emite permisos.
- **Procesado asíncrono**: crear una pista no transcodifica en caliente; publica un evento y un worker hace el trabajo pesado con compensación (sagas/routing slips de MassTransit).
## Componentes
| Componente | Responsabilidad | Por qué separado |
|---|---|---|
| **WebApi** | API REST (usuarios, pistas, playlists), autoriza, emite presigned URLs y stream-tickets | Punto de entrada fino; no procesa medios ni sirve bytes |
| **MusifyBackend** | Dominio + casos de uso + infraestructura compartida (EF, S3, MassTransit, ffmpeg) | Lógica reutilizada por WebApi y Worker |
| **StreamingGateway** | Reverse proxy (YARP) que valida el ticket y sirve el audio (`.m4a`) desde SeaweedFS | Escala con el tráfico de bytes, independiente de la API; SeaweedFS queda privado detrás |
| **Worker** | Consume eventos de MassTransit: transcodifica audio a `.m4a` y genera miniaturas | Trabajo CPU/IO intensivo fuera del request HTTP |
| **PostgreSQL** | Metadatos (usuarios, pistas, playlists, upload intents) | — |
| **RabbitMQ** | Cola de eventos + outbox transaccional | Desacopla creación de procesado |
| **SeaweedFS** | Almacenamiento de objetos (S3 + filer) | Guarda originales y derivados (audio `.m4a`, miniaturas) |
| **Zitadel** | Identidad (OIDC/OAuth2) | Login y emisión de JWT |
## Flujos principales
### 1. Subida de una pista
1. Cliente pide `POST /tracks/upload-urls` → la WebApi crea **UploadIntents** (reservas) y devuelve **URLs prefirmadas PUT** (imagen + audio).
2. Cliente sube los ficheros **directo a SeaweedFS** con esas URLs (no pasan por la API).
3. Cliente llama `POST /tracks` con los `intentId`s → la WebApi valida los intents, crea el `Track` (+ `UserHasTrack`) y publica `CreateTrackResourcesEvent`.
4. El **Worker** consume el evento: mueve los originales a su sitio, genera miniaturas y transcodifica el audio a `.m4a`, y actualiza el estado del track.
Ver detalle en [media-processing.md](media-processing.md) y [storage.md](storage.md).
### 2. Reproducción (streaming)
1. Cliente pide `GET /tracks/{id}/stream` (autenticado) → la WebApi autoriza y devuelve `{ manifestUrl, ticket }` (URL al `.m4a` + ticket RS256 acotado a la carpeta de esa pista).
2. El reproductor abre la URL del audio añadiendo `?t=<ticket>` (o cabecera `X-Stream-Ticket`) y descarga por HTTP range.
3. El **StreamingGateway** valida el ticket y reenvía al filer de SeaweedFS; los bytes van storage → cliente.
Ver [streaming.md](streaming.md).
### 3. Autenticación
1. Cliente hace login OIDC contra **Zitadel** y obtiene un access token (JWT).
2. La WebApi valida la firma/iss/aud del JWT.
3. En cada token validado, se **sincroniza** el usuario local (crea/actualiza nombre y foto) — provisioning just-in-time.
Ver [authentication.md](authentication.md).
## Modelo de datos (entidades)
- **User** — `Id` es el `sub` numérico de Zitadel (`long`), más nombre y `ProfilePictureUrl`.
- **Track** — pista: nombres de originales/derivados, `AudioFolderName` (carpeta del `.m4a` transcodificado), estados de procesado (`ProcessingStatus`), `LifeCycleStatus`.
- **PlayList** — lista del usuario (`UserId`).
- **PlayListHasTrack** — pistas dentro de una playlist (con `Position`).
- **UserHasTrack** — propiedad: qué usuario posee qué pista.
- **UploadIntent** — reserva de subida (clave, bucket, tamaño esperado, estado, expiración) para validar y aplicar cuota.
- **Upload** — registro de objetos subidos/transferidos al storage.
+40
View File
@@ -0,0 +1,40 @@
# Autenticación e identidad
## Proveedor: Zitadel (OIDC/OAuth2)
El login y la emisión de tokens los hace **Zitadel** (no la WebApi). La WebApi es un **resource server**: solo **valida** el access token (JWT) que le llega en `Authorization: Bearer`.
- Config en la sección `Authentication` (`AuthenticationConfiguration`): metadata address, issuer, audience, client id, endpoints de authorize/token, scopes.
- `JwtBearerOptionsSetup` configura la validación (issuer, audience, claves vía metadata OIDC).
- En dev, `RequireHttpsMetadata = false`.
### Requisito clave en la app de Zitadel
- **Auth Token Type = JWT** (si no, Zitadel emite un token **opaco** sin puntos y `JwtBearer` no lo puede validar → 401 en todo lo protegido).
- **User Info inside Token** activado si quieres que el access token traiga claims de perfil (`name`, `email`, `picture`). Por defecto el access token es mínimo (`sub`, `aud`, `iss`, `exp`…), sin perfil.
## El `sub` es numérico → `User.Id` es `long`
El `sub` de Zitadel es un entero de 64 bits en string (p. ej. `371953080444977155`), **no un GUID**. Por eso:
- `User.Id` y todas las FKs de usuario (`PlayList.UserId`, `UserHasTrack.UserId`, `UploadIntent.UserId`) son **`long`**.
- `User.Id` lleva `[DatabaseGenerated(None)]` para que EF **no** lo autogenere: se asigna el `sub`.
- `CurrentUser.Id` es `long?` (parsea con `long.TryParse`); `RequiredId` lanza si falta.
**Por qué**: el código antiguo hacía `Guid.Parse(sub)` y reventaba (FormatException) → 401 en cada endpoint protegido. Se decidió guardar el `sub` tal cual como número.
## Provisioning / sincronización de usuarios
En cada token validado, `JwtBearerEventsHandler.TokenValidated`:
1. Lee `sub` (id), `name`/`preferred_username`/`email` (con fallback) y el claim OIDC `picture`.
2. Envía un `SyncUserCommand`.
`SyncUserCommandHandler` hace un **upsert idempotente**:
- Si el usuario no existe → lo crea.
- Si existe → actualiza nombre/apellidos/`ProfilePictureUrl` **solo si cambiaron** (escribe en DB únicamente cuando hay cambios).
**Por qué upsert con "escribe solo si cambió"**: `TokenValidated` se ejecuta en **cada request**; sin ese guardado condicional estaríamos escribiendo en cada petición. Y debe ser resiliente: si el provisioning falla, se loguea pero **no** tumba la autenticación.
> Nota: `CreateUserCommand` (endpoint manual `POST /users`) sigue siendo create-only; el sync del login usa `SyncUserCommand`.
## Foto de perfil
`User.ProfilePictureUrl` se rellena desde el claim `picture` del token. Si Zitadel no mete la info de perfil en el access token (ver arriba), llega `null` — el código es correcto, pero no hay nada que sincronizar hasta activar esa opción.
+91
View File
@@ -0,0 +1,91 @@
# Entorno de desarrollo
Cómo levantar todo en local y probarlo.
## Dependencias (Docker)
```powershell
# SeaweedFS (almacenamiento)
docker compose -f deploy\seaweedfs\docker-compose.yml up -d
# PostgreSQL + pgAdmin
docker compose -f "C:\Users\XShin\OneDrive\Desktop\postgresql\docker-compose.yml" up -d
# Zitadel (auth)
docker compose -f "C:\Users\XShin\OneDrive\Pictures\zitadel-compose\docker-compose.yml" up -d
# RabbitMQ (contenedor suelto)
docker start rabbitmq
```
> Nota: usa `docker compose up` (no `docker start`) para Postgres/Zitadel: si sus redes se borraron, compose las recrea.
## Aplicaciones (.NET)
```powershell
dotnet run --project Musify.WebApi\WebApi # :5111
dotnet run --project Musify.StreamingGateway\StreamingGateway # :8081
dotnet run --project Musify.Worker\Worker # background
```
## Mapa de puertos
| Servicio | Puerto |
|---|---|
| WebApi | 5111 (`/scalar/v1`, `/openapi/v1.json`) |
| StreamingGateway | 8081 (`/health`, `/media/...`) |
| SeaweedFS S3 / filer / master | 8333 / 8888 / 9333 |
| Zitadel | 8080 |
| PostgreSQL | 59000 (→ 5432 en el contenedor) |
| RabbitMQ AMQP / management | 5672 / 15672 |
| pgAdmin | 5050 |
DB: `musify_db`, usuario `postgres`/`postgres`. Bucket S3: `webapi-storage` (credenciales `admin_access_key`/`admin_secret_key`).
## Migraciones
No hay migración automática al arrancar. Para aplicar cambios de esquema:
```powershell
cd MusifyBackend\Infrastructure
dotnet ef database update --context Database
```
La cadena de conexión para `dotnet ef` se lee de `DesignSettings.json` / user-secrets del proyecto Infrastructure.
## Stream-tickets (claves RS256)
La WebApi firma los stream-tickets con una clave privada y el gateway valida con la pública:
```powershell
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out stream_private.pem
openssl rsa -in stream_private.pem -pubout -out stream_public.pem
```
- WebApi: `StreamTicket:PrivateKeyPath` → `stream_private.pem`.
- Gateway: `StreamTicket:PublicKeyPath` → `stream_public.pem`.
## Consola de pruebas: `player.html`
Un único archivo HTML para probar todo sin front:
1. Sírvelo o ábrelo directo (el CORS está abierto en Development).
2. Pega tu **access token** de Zitadel y la API base (`http://localhost:5111`).
3. Permite: crear/actualizar/eliminar playlists, listar canciones, añadir/quitar canciones de una playlist, ver las canciones de una playlist y **reproducir** (pide el ticket y abre el `.m4a` con `?t=<ticket>`).
> Nota: `player.html` todavía usa dash.js; tras el cambio a `.m4a` debe actualizarse para reproducir el fichero directo (elemento `<audio>` por HTTP range).
Para obtener un token: login OAuth2 en Scalar (`/scalar/v1`) o cópialo de DevTools.
## Config (AppSettings)
Convención: `AppSettings.json` = plantilla con valores vacíos/por defecto; `AppSettings.Development.json` = valores reales de dev. Secretos sensibles (claves, connection strings) van idealmente en user-secrets.
Secciones clave: `Authentication` (Zitadel), `Database`, `MassTransit`, `InfrastructureStorage` (S3), `ApplicationStorage` (bucket), `Track`, `PlayList`, `UploadIntent`, `AudioTranscoder`, `Workers`, `StreamGateway`/`StreamTicket`.
## Requisitos del host
- .NET 10 SDK.
- **ffmpeg** en el PATH (transcode de audio del Worker).
- Docker Desktop.
- Directorio temporal de trabajo del Worker (`Workers:Routes:TemporaryFilesDirectory`, p. ej. `D:\tempsFilesDev`).
+46
View File
@@ -0,0 +1,46 @@
# Procesado de medios (Worker + MassTransit)
El procesado pesado (transcode de audio, miniaturas, transferencias en el bucket) ocurre **fuera del request HTTP**, en el **Worker**, orquestado con **MassTransit** sobre RabbitMQ.
## Por qué asíncrono y con routing slips
- Transcodificar audio y redimensionar imágenes es CPU/IO intensivo: no debe bloquear una llamada a la API.
- Es un proceso **multi-paso con efectos secundarios** (descargar de S3 → transcodificar → subir resultados → actualizar DB). Si un paso falla, hay que **deshacer** los anteriores. Para eso se usan **routing slips** (patrón Saga de MassTransit): cada paso es una *activity* con su **compensación**.
- **Outbox transaccional** (EF + MassTransit): el evento se publica de forma consistente con el cambio en la DB (no se pierde ni se duplica).
## Disparo
Al crear/actualizar una pista o playlist, el handler publica un evento (p. ej. `CreateTrackResourcesEvent`). Un **consumer** (`CreateTrackConsumer`, `UpdateTrackPictureConsumer`, …) construye un **routing slip** con la secuencia de activities y lo lanza.
## Builders de routing slip
- `CreateTrackRoutingSlipBuilder` — alta de pista (consume intents, mueve ficheros, dispara procesado).
- `PictureWorkflowRoutingSlipBuilder` — pipeline de imágenes (descargar → redimensionar → subir → actualizar).
- `AudioWorkflowRoutingSlipBuilder` — pipeline de audio (descargar → transcodificar a `.m4a` → subir carpeta → actualizar).
- `DeleteTrackRoutingSlipBuilder`, `DeletePlayListRoutingSlipBuilder`, `PlayListPictureSourceRoutingSlipBuilder`.
## Activities (pasos)
Agrupadas por área:
- **Files**: `DownloadFileFromBucketActivity`, `UploadFileToBucketActivity`, `TransferFilesToBucketActivity`, `CopyFileInBucketActivity`, `RemoveFileFromBucketActivity`.
- **Audio**: `GenerateAudioWorkflowPathsActivity`, `TranscodeAudioActivity` (ffmpeg → `.m4a`), `UpdateTrackAudioActivity` (guarda `AudioFolderName` y marca `Completed`).
- **Pictures**: `GeneratePictureWorkflowPathsActivity`, `ResizePictureActivity` (ImageSharp), `UpdateTrackPictureActivity`, `UpdatePlayListPictureActivity`.
- **Tracks/PlayLists**: `MarkTrackAsRemovingActivity`, `DeleteTrackFromDbActivity`, `PublishTrackProcessingEventsActivity`, y equivalentes de playlist.
- **UploadIntents**: `ConsumeUploadIntentsActivity`.
`RoutingSlipCleanUpConsumer` reacciona a `Completed`/`Faulted` para limpiar (p. ej. borrar el directorio temporal de trabajo).
## Transcode de audio
`AudioTranscoderService` invoca **ffmpeg** leyendo el original por stdin y produciendo en un directorio de trabajo un único `audio.m4a`: AAC con codec/bitrate/sample-rate/profile configurables y `-movflags +faststart` (moov al inicio → seek por range). Luego `TransferFilesToBucketActivity` sube esa carpeta a `Tracks/ProcessedAudios/{folder}/` y `UpdateTrackAudioActivity` guarda el `AudioFolderName` (la carpeta **de destino en S3**, no la local) y pone el estado en `Completed`.
## Configuración que necesita el Worker
El Worker registra: storage (S3), transcoder (ffmpeg), pictures, DB, los consumers de MassTransit y los jobs de upload intents. Su `AppSettings` debe tener las secciones correctas: `Bucket` (= `webapi-storage`, el mismo que la API), `PlayList`, `Track`, `MassTransit`, `AudioTranscoder`, `UploadIntent`, `Workers` (directorio temporal), `InfrastructureStorage`, `Database`.
## Jobs de fondo
- `UploadIntentExpirationJob` — marca intents caducados como expirados (libera cuota) cada `ExpirationJobIntervalSeconds`.
- `TemporalUploadsCleanUpJob` — limpia subidas temporales antiguas.
Corren en el host del Worker (`AddUploadIntentJobs`). Si no se registran en ningún proceso, los intents colgados no se limpian solos.
+59
View File
@@ -0,0 +1,59 @@
# Proyectos
Cada carpeta de nivel superior es una solución/repo independiente.
## Musify.WebApi/WebApi (`:5111`)
API REST pública (Minimal APIs, .NET 10). Es la fachada fina del sistema.
- **Endpoints**: `UserEndpoints`, `TrackEndpoints`, `PlayListEndpoints`.
- **Autenticación**: JWT Bearer contra Zitadel (`Authentication/`), con `JwtBearerEventsHandler` que sincroniza el usuario en cada token validado.
- **`CurrentUser`**: abstracción que se enlaza como parámetro de endpoint (`IBindableFromHttpContext`) y expone los claims básicos (`Id` como `long?`, `RequiredId`, nombre, etc.) sin tener que leer `ClaimsPrincipal` a mano.
- **Emite**: presigned URLs de subida (vía `IStorageService`) y **stream-tickets** (vía `IStreamTicketService`, RS256).
- **OpenAPI + Scalar** para documentación/prueba; OAuth2 (PKCE) configurado para login desde Scalar.
- **CORS** totalmente abierto **solo en Development** (para el `player.html` y front local).
- Traduce `Ardalis.Result` → HTTP en `ResultHttpExtensions`.
**Propósito/porqué**: separar la cara pública (autorización + metadatos) del procesado y del servido de bytes.
## MusifyBackend (Domain + Application + Infrastructure)
El núcleo, en tres proyectos (clean architecture):
### Domain
Entidades (`Entities/`) y value objects/enums (`ValueObjects/`: `ProcessingStatus`, `LifeCycleStatus`, `UploadIntentStatus`, …). Sin dependencias de infraestructura.
### Application
Casos de uso (CQRS con Mediator):
- `Tracks/`, `PlayLists/`, `Users/` → `Commands`, `Queries`, `Handlers`, `Responses`.
- `Contracts/` → interfaces que implementa la infraestructura (`IDatabase`, `IStorageService`, `IAudioTranscoderService`, `IPictureService`, `IEventBus`, `IStreamTicketService`).
- `Configuration/` → opciones tipadas (Track, PlayList, ApplicationStorage, UploadIntent…) bindeadas y validadas al arranque.
- `Shared/` → helpers transversales (`StorageKey.Combine`, `ImageSize`).
- `Pagination/` → `PaginatedResponse<T>`.
**Porqué**: la lógica de negocio no conoce EF, S3 ni RabbitMQ; solo interfaces. Reutilizable por WebApi y Worker.
### Infrastructure
Implementaciones concretas:
- `Persistence/` → `Database` (EF Core/Npgsql), migraciones.
- `Services/` → `StorageService` (AWSSDK.S3 contra SeaweedFS), `AudioTranscoderService` (ffmpeg → `.m4a`), `PictureService` (ImageSharp), `StreamTicketService` (firma RS256).
- `MassTransit/` → consumers, activities y routing slips (ver [media-processing.md](media-processing.md)).
- `Jobs/` → `UploadIntentExpirationJob`, `TemporalUploadsCleanUpJob`.
## Musify.StreamingGateway/StreamingGateway (`:8081`)
Reverse proxy (ASP.NET + **YARP**) que sirve el audio (`.m4a`) desde SeaweedFS. Valida el stream-ticket (RS256, clave pública) y, si el objeto pedido cae bajo el prefijo autorizado, reenvía al filer.
**Porqué un proyecto aparte**: escala con el tráfico de bytes independientemente de la API, tiene superficie mínima (solo sabe validar un JWT y proxiar) y permite que SeaweedFS quede en red privada. Ver [streaming.md](streaming.md).
## Musify.Worker/Worker
Worker Service (host de fondo) que consume los eventos de MassTransit y ejecuta el procesado pesado: transcode de audio a `.m4a` (ffmpeg), generación de miniaturas (ImageSharp), transferencias de ficheros en el bucket, y los jobs de expiración/limpieza de upload intents.
**Porqué aparte**: el trabajo CPU/IO no debe ocurrir dentro de un request HTTP; se escala y reinicia por separado. (Antes se llamaba `Musify.PictureWorker`; se renombró porque hace más que imágenes.)
## Raíz del workspace (no versionada)
- **`deploy/seaweedfs/`** — `docker-compose.yml` + `s3.conf` para levantar SeaweedFS.
- **`player.html`** — consola de pruebas (un solo archivo): login con token, CRUD de playlists, añadir/quitar canciones, listar y reproducir. Ver [development.md](development.md). (Pendiente: hoy usa dash.js; con el cambio a `.m4a` debe reproducir el fichero directo por `<audio>`/range.)
- **`docs/`** — esta documentación.
+48
View File
@@ -0,0 +1,48 @@
# Almacenamiento (SeaweedFS / S3)
## Qué es
**SeaweedFS** es el almacén de objetos, compatible con la API S3. Guarda los originales subidos y los derivados (audio `.m4a`, miniaturas). Se accede vía `AWSSDK.S3` desde `StorageService` (`IStorageService`).
Expone:
- **S3 API** (`:8333`) — operaciones S3 (presigned URLs, put/get/list…).
- **filer HTTP** (`:8888`) — sirve los ficheros por ruta; lo usa el **StreamingGateway**.
- master (`:9333`) y volume server — internos.
Bucket de la app: **`webapi-storage`** (`ApplicationStorage:Bucket`). Importante: la WebApi y el Worker usan **el mismo bucket** (el worker procesa lo que sube la API).
## Estructura de claves (layout)
- `temp/{userId}/Tracks/{guid}.{ext}` — subidas temporales (objetivo de las presigned PUT, antes de consumir el intent).
- `uploads/{userId}/Tracks/OriginalPictures|OriginalAudios/...` — originales ya consumidos.
- `Tracks/SmallPictures|MediumPictures|LargePictures/...` — miniaturas generadas.
- `Tracks/ProcessedAudios/{AudioFolderName}/` — audio transcodificado (`audio.m4a`, AAC con faststart).
Las claves se construyen **siempre** con el helper `StorageKey.Combine(...)` (en `Application/Shared`), que une segmentos con `/` limpiando espacios y barras. Está centralizado para estandarizar y no repetir lógica.
> En el filer, los objetos S3 viven bajo `/buckets/{bucket}/{key}` — de ahí que el gateway reescriba `/media/{key}` → `/buckets/webapi-storage/{key}`.
## URLs prefirmadas (presigned)
Para **no** hacer pasar ficheros grandes por la API, el cliente habla directo con S3 mediante URLs firmadas de corta vida:
- **Subidas (PUT)**: `RequestTrackUploadUrls` / `RequestPlayListPictureUpload` generan presigned PUT (con `Content-Type` y `If-None-Match: *` firmados, expiración ~120s). El cliente sube directo a S3.
- **Imágenes (GET)**: las miniaturas se sirven con presigned GET (un objeto = una URL).
**Por qué presigned y no proxiar**: subir un audio de ~100 MB a través de la API consumiría memoria/ancho de banda de los servidores de aplicación y escalaría mal. La presigned PUT directa a S3 es el patrón estándar y correcto.
### Upload intents y cuota
Cada `upload-urls` crea **UploadIntents** (reservas en estado `Issued`) y devuelve las URLs. Al crear la pista/playlist se **consumen**. Hay cuota por usuario (`MaxActiveUploadIntentsPerUser`, bytes…) para evitar abuso. El `UploadIntentExpirationJob` (en el Worker) marca como expirados los intents caducados y libera la cuota; si ese job no corre, los intents colgados se acumulan y bloquean nuevas subidas.
## Qué es público y qué privado (despliegue)
| Exponer a internet | Mantener privado |
|---|---|
| S3 API `:8333` (tras TLS) — necesario para presigned PUT/GET del cliente | filer `:8888` (solo el gateway, en red interna) |
| StreamingGateway `:8081` | master `:9333`, volume server, UI de admin |
- **El audio** se sirve por el gateway → SeaweedFS puede quedar privado para ese flujo.
- **Subidas e imágenes** usan presigned → el cliente toca el endpoint S3 directamente, así que `:8333` debe ser alcanzable (pero inútil sin firma válida; es el modelo de cualquier bucket S3/MinIO).
- En **producción**: `InfrastructureStorage:Address` debe ser el **endpoint S3 público** (la firma se calcula para ese host); si los servidores no lo alcanzan internamente, se usa config dividida (host público para firmar / interno para operaciones server-side).
**Regla**: el gateway y, como mucho, el `:8333` firmado son la única superficie pública; master/volume/filer/admin **nunca** se exponen.
+55
View File
@@ -0,0 +1,55 @@
# Streaming de audio
## Objetivo
Reproducir el audio **sin que los bytes pasen por la WebApi**, pero con control de acceso. La WebApi solo emite un permiso corto; el reverse proxy sirve los bytes desde SeaweedFS.
## Formato: fichero único AAC (.m4a) por HTTP range
El audio se transcodifica a **un único `audio.m4a`** (AAC, contenedor MP4 con `+faststart` para que el átomo `moov` quede al inicio y el seek por range funcione), dentro de la carpeta `Tracks/ProcessedAudios/{AudioFolderName}/`. No hay manifest ni segmentos: el reproductor descarga el fichero por **HTTP range requests** (modelo tipo Spotify para VOD musical).
Multi-bitrate no está implementado hoy; el camino para añadirlo es transcodificar varias calidades (`audio_96.m4a`, …) y que el cliente elija una al iniciar (sin ABR fluido a mitad de pista, igual que Spotify).
## El acceso: stream-ticket de prefijo
Se autoriza **el prefijo de la carpeta** de la pista con un *stream-ticket* de corta vida, y el proxy lo valida en cada petición. Aunque hoy sea un único objeto, el ticket sigue acotado al prefijo `Tracks/ProcessedAudios/<folder>/` (cubre el fichero actual y cualquier calidad futura).
## Flujo
1. `GET /tracks/{id}/stream` (autenticado) → la WebApi autoriza y devuelve:
```json
{ "manifestUrl": "http://<gateway>/media/Tracks/ProcessedAudios/<folder>/audio.m4a",
"ticket": "<JWT RS256>", "expiresInSeconds": 3600 }
```
El ticket lleva el claim `prefix = "Tracks/ProcessedAudios/<folder>/"`.
2. El reproductor abre la URL del audio añadiéndole `?t=<ticket>` (o la cabecera `X-Stream-Ticket`).
3. El **StreamingGateway** (YARP) valida el ticket y reenvía al filer; los bytes van SeaweedFS → cliente (con range/seek).
## El ticket (RS256)
- Lo firma la WebApi con clave **privada** (`StreamTicketService`); el gateway valida con la **pública** (`TicketValidator`). Así el gateway **no puede emitir** tickets, solo verificarlos.
- Claims: `sub`, `prefix`, `aud = media-gateway`, `iss = musify-webapi`, `exp` (TTL ~1h).
**Por qué RS256 (asimétrica)**: separa responsabilidades — emisor (WebApi) y verificador (gateway) no comparten secreto; el gateway, aunque se comprometa, no puede crear permisos.
## El gateway (validación + proxy)
`TicketValidationMiddleware` en cada petición a `/media/**`:
1. Lee `?t=`, valida firma/aud/iss/exp.
2. Comprueba que la clave pedida **empieza por** el `prefix` del ticket (`StartsWith`). Si no → 403.
3. Quita el `?t=` y deja pasar a YARP, que reescribe `/media/{key}` → `/buckets/webapi-storage/{key}` del filer.
- Sin ticket → **401**; fuera de prefijo → **403**; ok → **200** (y **206** en range requests, así que el seek funciona).
- Pasa `Range`/`Accept-Ranges` y expone esos headers por CORS.
## ¿Un ticket por canción?
Sí. Cada ticket está acotado a **una** carpeta (una pista). Para reproducir otra, se pide otro ticket. Es barato (la WebApi solo firma un JWT) y es lo más seguro: cada permiso abre solo lo que vas a reproducir.
- Si algún día molesta (muchísimos cambios de pista), el mismo mecanismo del claim `prefix` permite ampliar el alcance: a nivel de playlist o de toda la biblioteca (`Tracks/ProcessedAudios/`). Trade-off seguridad ↔ comodidad. Por defecto, **por canción**.
## Pendiente / notas de seguridad
- La política de quién puede pedir el ticket es hoy "cualquier autenticado" (TODO: restringir con `UserHasTrack`/visibilidad).
- El check de prefijo es `StartsWith` literal; conviene endurecer rechazando claves con `..` (path traversal), aunque navegadores y ASP.NET normalizan `..`.
- El ticket viaja en la query (`?t=`); el gateway lo elimina antes de reenviar (no llega a los logs del filer).
@@ -0,0 +1,440 @@
---
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)
@@ -0,0 +1,233 @@
# 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 |
@@ -0,0 +1,191 @@
# 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/)
@@ -0,0 +1,533 @@
---
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
@@ -0,0 +1,65 @@
---
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
@@ -0,0 +1,116 @@
---
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`.
@@ -0,0 +1,345 @@
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'),
),
],
),
),
);
}
}
@@ -0,0 +1,223 @@
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),
),
),
),
),
),
),
),
);
}
}
@@ -0,0 +1,149 @@
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'),
),
],
),
),
);
}
}
@@ -0,0 +1,239 @@
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])),
);
},
),
);
}
}
@@ -0,0 +1,566 @@
# 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)
@@ -0,0 +1,553 @@
# 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
@@ -0,0 +1,724 @@
# 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
]),
),
],
);
}
}
```
@@ -0,0 +1,368 @@
# 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
@@ -0,0 +1,700 @@
# 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: [...],
)
```
@@ -0,0 +1,726 @@
# 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,
),
),
);
}
```
@@ -0,0 +1,140 @@
---
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/)
@@ -0,0 +1,259 @@
# 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 |
@@ -0,0 +1,119 @@
# 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 |
@@ -0,0 +1,99 @@
# 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
@@ -0,0 +1,118 @@
# 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,
);
}
}
```
@@ -0,0 +1,130 @@
# 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 |
@@ -0,0 +1,123 @@
# 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 |
@@ -0,0 +1,106 @@
---
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.
@@ -0,0 +1,507 @@
# 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
}
});
```
@@ -0,0 +1,562 @@
# 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
```
@@ -0,0 +1,588 @@
# 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');
});
```
@@ -0,0 +1,493 @@
# 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
```
@@ -0,0 +1,433 @@
# 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"
```
@@ -0,0 +1,670 @@
# 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);
});
```
@@ -0,0 +1,80 @@
#!/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"
@@ -0,0 +1,177 @@
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
@@ -0,0 +1,42 @@
---
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.
+513
View File
@@ -0,0 +1,513 @@
---
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)
+45
View File
@@ -0,0 +1,45 @@
# 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
+45
View File
@@ -0,0 +1,45 @@
# 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'
+3
View File
@@ -0,0 +1,3 @@
# musify_flutter
A new Flutter project.
+9
View File
@@ -0,0 +1,9 @@
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
+14
View File
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,45 @@
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 = "../.."
}
@@ -0,0 +1,45 @@
<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>
@@ -0,0 +1,5 @@
package com.example.musify_flutter
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?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>
@@ -0,0 +1,12 @@
<?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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?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>
@@ -0,0 +1,18 @@
<?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>
@@ -0,0 +1,7 @@
<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>
+24
View File
@@ -0,0 +1,24 @@
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)
}
+6
View File
@@ -0,0 +1,6 @@
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
@@ -0,0 +1,5 @@
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
@@ -0,0 +1,26 @@
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")
+3
View File
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+34
View File
@@ -0,0 +1,34 @@
**/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
@@ -0,0 +1,24 @@
<?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>
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1,644 @@
// !$*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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?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>
@@ -0,0 +1,8 @@
<?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>
@@ -0,0 +1,119 @@
<?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 &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<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>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?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>
@@ -0,0 +1,8 @@
<?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>
@@ -0,0 +1,16 @@
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)
}
}
@@ -0,0 +1,122 @@
{
"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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Some files were not shown because too many files have changed in this diff Show More