skills/flutter/app-distribution/SKILL.md
# 📦 Skill: App Distribution & Deployment ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-app-distribution` | | **Nivel** | 🟡 Intermedio | | **Versión** | 1.0.0 | | **Keywords** | `app-distribution`, `testflight`, `play-console`, `firebase-distribution`, `fastlane`, `beta-testing` | | **Referencia** | [Flutter Deployment](https://docs.flutter.dev/deployment) | ## 🔑 Keywords para Invocación - `app-distribution` - `testflight` - `play-console` - `firebase-distrib
npx skillsauth add altrupets/monorepo skills/flutter/app-distributionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Atributo | Valor |
|----------|-------|
| ID | flutter-app-distribution |
| Nivel | 🟡 Intermedio |
| Versión | 1.0.0 |
| Keywords | app-distribution, testflight, play-console, firebase-distribution, fastlane, beta-testing |
| Referencia | Flutter Deployment |
app-distributiontestflightplay-consolefirebase-distributionfastlanebeta-testingapp-deployment@skill:app-distributionConfigura app-distribution con testflight y play-console
Implementa fastlane para automated deployment
Setup firebase-distribution para beta testing
@skill:app-distribution - Pipeline completo de distribución
Este skill cubre la distribución de aplicaciones Flutter en múltiples canales: TestFlight (iOS), Google Play Internal Testing, Firebase App Distribution, y automatización con Fastlane. Incluye configuración de flavors, signing, y CI/CD integration.
⚠️ IMPORTANTE: Todos los comandos de este skill deben ejecutarse desde la raíz del proyecto (donde existe el directorio mobile/). El skill incluye verificaciones para asegurar que se está en el directorio correcto antes de ejecutar cualquier comando.
⚠️ IMPORTANTE: Todos los comandos de este skill deben ejecutarse desde la raíz del proyecto (donde existe el directorio mobile/). El skill incluye verificaciones para asegurar que se está en el directorio correcto antes de ejecutar cualquier comando.
my_app/
├── lib/
│ └── config/
│ ├── flavor_config.dart
│ └── app_config.dart
│
├── android/
│ ├── app/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── dev/
│ │ ├── staging/
│ │ └── production/
│ ├── key.properties
│ └── fastlane/
│ ├── Fastfile
│ └── Appfile
│
├── ios/
│ ├── fastlane/
│ │ ├── Fastfile
│ │ └── Appfile
│ └── ExportOptions.plist
│
├── scripts/
│ ├── distribute.sh
│ └── build_flavors.sh
│
└── .github/
└── workflows/
├── distribute_ios.yml
└── distribute_android.yml
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_launcher_icons: ^0.13.1
flutter_native_splash: ^2.3.7
// lib/config/flavor_config.dart
enum Flavor {
dev,
staging,
production,
}
class FlavorConfig {
final Flavor flavor;
final String name;
final String apiBaseUrl;
final String appName;
final String bundleId;
final bool enableAnalytics;
final bool enableCrashlytics;
static FlavorConfig? _instance;
FlavorConfig._internal({
required this.flavor,
required this.name,
required this.apiBaseUrl,
required this.appName,
required this.bundleId,
required this.enableAnalytics,
required this.enableCrashlytics,
});
static FlavorConfig get instance => _instance!;
static bool get isProduction => _instance?.flavor == Flavor.production;
static bool get isStaging => _instance?.flavor == Flavor.staging;
static bool get isDevelopment => _instance?.flavor == Flavor.dev;
static void setFlavor(Flavor flavor) {
switch (flavor) {
case Flavor.dev:
_instance = FlavorConfig._internal(
flavor: Flavor.dev,
name: 'DEV',
apiBaseUrl: 'https://dev-api.example.com',
appName: 'MyApp DEV',
bundleId: 'com.example.myapp.dev',
enableAnalytics: false,
enableCrashlytics: false,
);
break;
case Flavor.staging:
_instance = FlavorConfig._internal(
flavor: Flavor.staging,
name: 'STAGING',
apiBaseUrl: 'https://staging-api.example.com',
appName: 'MyApp STAGING',
bundleId: 'com.example.myapp.staging',
enableAnalytics: true,
enableCrashlytics: true,
);
break;
case Flavor.production:
_instance = FlavorConfig._internal(
flavor: Flavor.production,
name: 'PRODUCTION',
apiBaseUrl: 'https://api.example.com',
appName: 'MyApp',
bundleId: 'com.example.myapp',
enableAnalytics: true,
enableCrashlytics: true,
);
break;
}
}
@override
String toString() => name;
}
// lib/main_dev.dart
import 'package:flutter/material.dart';
import 'config/flavor_config.dart';
import 'app.dart';
void main() {
FlavorConfig.setFlavor(Flavor.dev);
runApp(const MyApp());
}
// lib/main_staging.dart
import 'package:flutter/material.dart';
import 'config/flavor_config.dart';
import 'app.dart';
void main() {
FlavorConfig.setFlavor(Flavor.staging);
runApp(const MyApp());
}
// lib/main_production.dart
import 'package:flutter/material.dart';
import 'config/flavor_config.dart';
import 'app.dart';
void main() {
FlavorConfig.setFlavor(Flavor.production);
runApp(const MyApp());
}
// android/app/build.gradle
android {
defaultConfig {
applicationId "com.example.myapp"
// ...
}
signingConfigs {
release {
if (System.getenv("CI")) {
// CI/CD environment
storeFile file(System.getenv("KEYSTORE_PATH"))
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
} else {
// Local development
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
}
flavorDimensions "environment"
productFlavors {
dev {
dimension "environment"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
resValue "string", "app_name", "MyApp DEV"
buildConfigField "String", "API_BASE_URL", '"https://dev-api.example.com"'
}
staging {
dimension "environment"
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
resValue "string", "app_name", "MyApp STAGING"
buildConfigField "String", "API_BASE_URL", '"https://staging-api.example.com"'
}
production {
dimension "environment"
resValue "string", "app_name", "MyApp"
buildConfigField "String", "API_BASE_URL", '"https://api.example.com"'
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix ".debug"
}
}
}
# android/key.properties (NO commitar, agregar a .gitignore)
storeFile=../upload-keystore.jks
storePassword=your_keystore_password
keyAlias=upload
keyPassword=your_key_password
<!-- ios/Runner/Info.plist -->
<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
# Configurar schemes en Xcode:
# 1. Product > Scheme > Manage Schemes
# 2. Crear schemes: dev, staging, production
# 3. Para cada scheme, configurar Build Configuration
# En Xcode, agregar User-Defined Settings:
# DEV:
# APP_DISPLAY_NAME = MyApp DEV
# PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp.dev
# STAGING:
# APP_DISPLAY_NAME = MyApp STAGING
# PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp.staging
# PRODUCTION:
# APP_DISPLAY_NAME = MyApp
# PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp
# Instalar Fastlane
sudo gem install fastlane -NV
# Inicializar en carpeta ios/
cd ios
fastlane init
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
# Increment build number
increment_build_number(
xcodeproj: "Runner.xcodeproj",
build_number: latest_testflight_build_number + 1
)
# Build app
build_app(
scheme: "production",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.example.myapp" => "match AppStore com.example.myapp"
}
}
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false
)
# Send notification
slack(
message: "New iOS beta build uploaded to TestFlight! 🚀",
success: true
)
end
desc "Deploy to App Store"
lane :release do
increment_build_number(
xcodeproj: "Runner.xcodeproj"
)
build_app(
scheme: "production",
export_method: "app-store"
)
upload_to_app_store(
submit_for_review: false,
automatic_release: false,
skip_metadata: true,
skip_screenshots: true,
precheck_include_in_app_purchases: false
)
end
desc "Staging build"
lane :staging do
build_app(
scheme: "staging",
export_method: "development"
)
firebase_app_distribution(
app: "1:123456789:ios:abcd1234",
testers: "[email protected]",
release_notes: "Staging build for testing"
)
end
end
# ios/fastlane/Appfile
app_identifier("com.example.myapp")
apple_id("[email protected]")
team_id("TEAMID123")
cd android
fastlane init
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
desc "Deploy to Internal Testing track"
lane :internal do
# Build AAB
gradle(
task: "bundle",
flavor: "production",
build_type: "Release"
)
# Upload to Internal Testing
upload_to_play_store(
track: 'internal',
aab: '../build/app/outputs/bundle/productionRelease/app-production-release.aab',
skip_upload_screenshots: true,
skip_upload_images: true,
skip_upload_metadata: true
)
slack(
message: "New Android build uploaded to Internal Testing! 🎉",
success: true
)
end
desc "Promote Internal to Beta"
lane :promote_to_beta do
upload_to_play_store(
track: 'internal',
track_promote_to: 'beta',
skip_upload_aab: true,
skip_upload_screenshots: true,
skip_upload_images: true
)
end
desc "Deploy to Production (Staged Rollout)"
lane :production do
gradle(
task: "bundle",
flavor: "production",
build_type: "Release"
)
upload_to_play_store(
track: 'production',
rollout: '0.1', # 10% rollout
aab: '../build/app/outputs/bundle/productionRelease/app-production-release.aab'
)
end
desc "Staging build to Firebase"
lane :staging do
gradle(
task: "assemble",
flavor: "staging",
build_type: "Release"
)
firebase_app_distribution(
app: "1:123456789:android:abcd1234",
testers: "[email protected]",
release_notes: "Staging build for testing",
apk_path: "../build/app/outputs/apk/staging/release/app-staging-release.apk"
)
end
end
# android/fastlane/Appfile
json_key_file("service-account-key.json")
package_name("com.example.myapp")
# Instalar Firebase CLI
npm install -g firebase-tools
# Login
firebase login
# Configurar proyecto
firebase init
# scripts/distribute_firebase.sh
#!/bin/bash
# Verificar que estamos en la raíz del proyecto
if [ ! -d "mobile" ]; then
echo "Error: Ejecuta este comando desde la raíz del proyecto"
exit 1
fi
# Build Flutter app
cd mobile
flutter build apk --flavor staging --release
flutter build ios --flavor staging --release --no-codesign
cd ..
# Distribute Android
firebase appdistribution:distribute \
build/app/outputs/apk/staging/release/app-staging-release.apk \
--app 1:123456789:android:abcd1234 \
--groups "testers" \
--release-notes "Staging build $(date)"
# Distribute iOS (requiere .ipa firmado)
firebase appdistribution:distribute \
build/ios/ipa/Runner.ipa \
--app 1:123456789:ios:abcd1234 \
--groups "testers" \
--release-notes "Staging build $(date)"
# .github/workflows/distribute_ios.yml
name: iOS Distribution
on:
push:
branches: [ main, develop ]
workflow_dispatch:
jobs:
distribute_ios:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.35.0'
channel: 'stable'
- name: Install dependencies
run: flutter pub get
- name: Run tests
run: flutter test
- name: Setup Fastlane
run: |
cd ios
bundle install
- name: Configure Apple Certificates
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
run: |
cd ios
bundle exec fastlane match appstore
- name: Build and deploy to TestFlight
env:
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
run: |
cd ios
bundle exec fastlane beta
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: ios-build
path: ios/build/Runner.ipa
# .github/workflows/distribute_android.yml
name: Android Distribution
on:
push:
branches: [ main, develop ]
workflow_dispatch:
jobs:
distribute_android:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.35.0'
channel: 'stable'
- name: Install dependencies
run: flutter pub get
- name: Run tests
run: flutter test
- name: Configure Keystore
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
KEY_PROPERTIES: ${{ secrets.KEY_PROPERTIES }}
run: |
echo $KEYSTORE_BASE64 | base64 -d > android/app/upload-keystore.jks
echo "$KEY_PROPERTIES" > android/key.properties
- name: Build AAB
working-directory: mobile
run: flutter build appbundle --flavor production --release
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
working-directory: android
- name: Deploy to Play Console
env:
PLAY_STORE_CONFIG_JSON: ${{ secrets.PLAY_STORE_CONFIG_JSON }}
run: |
echo "$PLAY_STORE_CONFIG_JSON" > android/fastlane/service-account-key.json
cd android
bundle exec fastlane internal
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: android-build
path: build/app/outputs/bundle/productionRelease/app-production-release.aab
// lib/config/app_config.dart
class AppConfig {
static const String version = '1.0.0';
static const int buildNumber = 1;
static String get versionString => '$version+$buildNumber';
static String get fullVersionString {
final flavor = FlavorConfig.instance.name;
return '$version+$buildNumber ($flavor)';
}
}
# scripts/bump_version.sh
#!/bin/bash
# Bump version in pubspec.yaml
VERSION=$1
BUILD_NUMBER=$2
if [ -z "$VERSION" ] || [ -z "$BUILD_NUMBER" ]; then
echo "Usage: ./bump_version.sh <version> <build_number>"
echo "Example: ./bump_version.sh 1.2.0 42"
exit 1
fi
# Verificar que estamos en la raíz del proyecto
if [ ! -d "mobile" ]; then
echo "Error: Ejecuta este comando desde la raíz del proyecto"
exit 1
fi
# Update mobile/pubspec.yaml
sed -i "" "s/^version: .*/version: $VERSION+$BUILD_NUMBER/" mobile/pubspec.yaml
echo "✅ Version updated to $VERSION+$BUILD_NUMBER"
# Commit changes
git add mobile/pubspec.yaml
git commit -m "chore: bump version to $VERSION+$BUILD_NUMBER"
git tag "v$VERSION"
echo "📦 Created tag v$VERSION"
✅ DO:
❌ DON'T:
✅ DO: Genera release notes automáticas
# Generate changelog from git commits
git log --oneline --pretty=format:"- %s" v1.0.0..HEAD > release_notes.txt
✅ DO: Usa staged rollouts para production
# Start with 10%, monitor, then increase
upload_to_play_store(
track: 'production',
rollout: '0.1' # 10%
)
# Verificar que el path en key.properties es correcto
# Debe ser relativo a android/app/
storeFile=../upload-keystore.jks
<!-- Agregar a Info.plist -->
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
// Auto-increment version code
def buildNumber = System.getenv("BUILD_NUMBER") ?: "1"
versionCode buildNumber.toInteger()
Versión: 1.0.0 Última actualización: Diciembre 2025 Total líneas: 1,100+
development
# 🔧 Skill: Native Integration (Swift/Kotlin) ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-native-integration` | | **Nivel** | 🔴 Avanzado | | **Versión** | 1.0.0 | | **Keywords** | `native-integration`, `swift`, `kotlin`, `uikit`, `android-sdk`, `native-ui` | | **Referencia** | [Flutter Platform Integration](https://docs.flutter.dev/platform-integration) | ## 🔑 Keywords para Invocación Usa cualquiera de estos keywords en tus prompts para invocar este skill:
testing
# 🎨 Skill: MVVM Pattern ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-mvvm-pattern` | | **Nivel** | 🟡 Intermedio | | **Versión** | 1.0.0 | | **Keywords** | `mvvm`, `model-view-viewmodel`, `provider`, `changenotifier` | ## 🔑 Keywords para Invocación Usa cualquiera de estos keywords en tus prompts para invocar este skill: - `mvvm` - `model-view-viewmodel` - `provider` - `changenotifier` - `@skill:mvvm` ### Ejemplos de Prompts ``` Crea una app de lista de ta
development
# 🎨 Skill: Arquitectura Modular ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-modular-architecture` | | **Nivel** | 🔴 Avanzado | | **Versión** | 1.0.0 | | **Keywords** | `modular`, `modular-architecture`, `module`, `multi-module` | | **Referencia** | [Flutter Modular Package](https://pub.dev/packages/flutter_modular) | ## 🔑 Keywords para Invocación Usa cualquiera de estos keywords en tus prompts para invocar este skill: - `modular` - `modular-architecture`
tools
# 📱 Skill: Mobile Testing y Debugging con Flutter MCP ## 📋 Metadata | Atributo | Valor | |----------|-------| | **ID** | `flutter-mobile-testing` | | **Nivel** | 🔴 Avanzado | | **Versión** | 1.2.0 | | **Keywords** | `mobile-testing`, `integration-test`, `flutter-mcp`, `dart-mcp`, `debugging`, `logic-analysis`, `widget-inspection`, `device-testing` | | **Referencia** | [Dart and Flutter MCP server](https://docs.flutter.dev/ai/mcp-server) \| [Mobile MCP](https://github.com/mobile-next/mobile-