feat: add flutter-dev skill

Add comprehensive Flutter cross-platform development guide covering:
- Widget patterns and const optimization
- Riverpod/Bloc state management
- GoRouter navigation
- Performance optimization
- Testing strategies
- Platform-specific implementations

Made-with: Cursor
This commit is contained in:
mljxxx
2026-03-26 17:35:55 +08:00
parent 33cdc32a8e
commit 2995582a5e
15 changed files with 4723 additions and 0 deletions

View File

@@ -0,0 +1,510 @@
# Localization
Internationalization (i18n) patterns using flutter_localizations and intl package for Flutter applications.
## Setup
### Dependencies
```yaml
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: ^0.19.0
flutter:
generate: true
```
### l10n Configuration
```yaml
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
```
## ARB Files
### English (Template)
```json
// lib/l10n/app_en.arb
{
"@@locale": "en",
"appTitle": "My App",
"@appTitle": {
"description": "The application title"
},
"hello": "Hello",
"welcome": "Welcome, {name}!",
"@welcome": {
"description": "Welcome message with user name",
"placeholders": {
"name": {
"type": "String",
"example": "John"
}
}
},
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
"@itemCount": {
"description": "Number of items",
"placeholders": {
"count": {
"type": "int"
}
}
},
"lastUpdated": "Last updated: {date}",
"@lastUpdated": {
"description": "Last update timestamp",
"placeholders": {
"date": {
"type": "DateTime",
"format": "yMMMd"
}
}
},
"price": "Price: {amount}",
"@price": {
"description": "Product price",
"placeholders": {
"amount": {
"type": "double",
"format": "currency",
"optionalParameters": {
"symbol": "$",
"decimalDigits": 2
}
}
}
},
"gender": "{gender, select, male{He} female{She} other{They}} liked this",
"@gender": {
"description": "Gender-specific message",
"placeholders": {
"gender": {
"type": "String"
}
}
}
}
```
### Chinese
```json
// lib/l10n/app_zh.arb
{
"@@locale": "zh",
"appTitle": "我的应用",
"hello": "你好",
"welcome": "欢迎,{name}",
"itemCount": "{count, plural, =0{没有项目} other{{count} 个项目}}",
"lastUpdated": "最后更新:{date}",
"price": "价格:{amount}",
"gender": "{gender, select, male{他} female{她} other{Ta}}喜欢了这个"
}
```
### Japanese
```json
// lib/l10n/app_ja.arb
{
"@@locale": "ja",
"appTitle": "マイアプリ",
"hello": "こんにちは",
"welcome": "ようこそ、{name}さん!",
"itemCount": "{count, plural, =0{アイテムなし} other{{count}件}}",
"lastUpdated": "最終更新:{date}",
"price": "価格:{amount}",
"gender": "{gender, select, male{彼} female{彼女} other{その人}}がいいねしました"
}
```
## App Configuration
```dart
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('zh'),
Locale('ja'),
],
locale: const Locale('en'),
home: const HomePage(),
);
}
}
```
## Using Translations
```dart
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(l10n.appTitle)),
body: Column(
children: [
Text(l10n.hello),
Text(l10n.welcome('John')),
Text(l10n.itemCount(5)),
Text(l10n.lastUpdated(DateTime.now())),
Text(l10n.price(29.99)),
Text(l10n.gender('female')),
],
),
);
}
}
```
### Extension for Convenience
```dart
extension LocalizationExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}
// Usage
Text(context.l10n.hello)
```
## Dynamic Locale Switching
### With Riverpod
```dart
@riverpod
class LocaleNotifier extends _$LocaleNotifier {
@override
Locale build() {
final saved = ref.watch(sharedPreferencesProvider).getString('locale');
if (saved != null) {
return Locale(saved);
}
return const Locale('en');
}
void setLocale(Locale locale) {
ref.read(sharedPreferencesProvider).setString('locale', locale.languageCode);
state = locale;
}
}
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeNotifierProvider);
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: locale,
home: const HomePage(),
);
}
}
```
### Language Selector
```dart
class LanguageSelector extends ConsumerWidget {
const LanguageSelector({super.key});
static const languages = [
(Locale('en'), 'English'),
(Locale('zh'), '中文'),
(Locale('ja'), '日本語'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentLocale = ref.watch(localeNotifierProvider);
return PopupMenuButton<Locale>(
initialValue: currentLocale,
onSelected: (locale) {
ref.read(localeNotifierProvider.notifier).setLocale(locale);
},
itemBuilder: (context) => languages.map((lang) {
return PopupMenuItem(
value: lang.$1,
child: Row(
children: [
if (currentLocale == lang.$1)
const Icon(Icons.check, size: 18),
const SizedBox(width: 8),
Text(lang.$2),
],
),
);
}).toList(),
child: const Icon(Icons.language),
);
}
}
```
## Date and Number Formatting
```dart
import 'package:intl/intl.dart';
class FormattingUtils {
static String formatDate(DateTime date, String locale) {
return DateFormat.yMMMd(locale).format(date);
}
static String formatDateTime(DateTime dateTime, String locale) {
return DateFormat.yMMMd(locale).add_jm().format(dateTime);
}
static String formatRelativeTime(DateTime dateTime, String locale) {
final now = DateTime.now();
final diff = now.difference(dateTime);
if (diff.inDays > 7) {
return DateFormat.yMMMd(locale).format(dateTime);
} else if (diff.inDays > 0) {
return '${diff.inDays}d ago';
} else if (diff.inHours > 0) {
return '${diff.inHours}h ago';
} else if (diff.inMinutes > 0) {
return '${diff.inMinutes}m ago';
} else {
return 'Just now';
}
}
static String formatCurrency(double amount, String locale, {String? symbol}) {
return NumberFormat.currency(
locale: locale,
symbol: symbol,
decimalDigits: 2,
).format(amount);
}
static String formatNumber(num number, String locale) {
return NumberFormat.decimalPattern(locale).format(number);
}
static String formatPercent(double value, String locale) {
return NumberFormat.percentPattern(locale).format(value);
}
static String formatCompact(num number, String locale) {
return NumberFormat.compact(locale: locale).format(number);
}
}
```
### Usage with Locale
```dart
class FormattedContent extends StatelessWidget {
const FormattedContent({super.key});
@override
Widget build(BuildContext context) {
final locale = Localizations.localeOf(context).toString();
return Column(
children: [
Text(FormattingUtils.formatDate(DateTime.now(), locale)),
Text(FormattingUtils.formatCurrency(1234.56, locale, symbol: '\$')),
Text(FormattingUtils.formatNumber(1234567, locale)),
Text(FormattingUtils.formatPercent(0.75, locale)),
Text(FormattingUtils.formatCompact(1500000, locale)),
],
);
}
}
```
## RTL Support
```dart
class RtlAwareWidget extends StatelessWidget {
const RtlAwareWidget({super.key});
@override
Widget build(BuildContext context) {
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Row(
children: [
Icon(isRtl ? Icons.arrow_back : Icons.arrow_forward),
const Expanded(child: Text('Content')),
Padding(
padding: EdgeInsetsDirectional.only(start: 16),
child: const Icon(Icons.settings),
),
],
);
}
}
```
### Directional Widgets
| Standard | Directional |
|----------|-------------|
| `EdgeInsets` | `EdgeInsetsDirectional` |
| `Padding` | `Padding` with `EdgeInsetsDirectional` |
| `Align` | `AlignmentDirectional` |
| `Positioned` | `PositionedDirectional` |
| `BorderRadius` | `BorderRadiusDirectional` |
```dart
// Use directional
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
child: child,
)
Container(
alignment: AlignmentDirectional.centerStart,
child: child,
)
Container(
decoration: const BoxDecoration(
borderRadius: BorderRadiusDirectional.only(
topStart: Radius.circular(8),
bottomStart: Radius.circular(8),
),
),
)
```
## Organized Translations
### Split by Feature
```
lib/
l10n/
app_en.arb # Common translations
app_zh.arb
features/
auth_en.arb # Auth feature translations
auth_zh.arb
settings_en.arb # Settings feature translations
settings_zh.arb
```
### Namespaced Keys
```json
// app_en.arb
{
"auth_login": "Login",
"auth_logout": "Logout",
"auth_forgotPassword": "Forgot Password?",
"settings_title": "Settings",
"settings_language": "Language",
"settings_theme": "Theme",
"error_network": "Network error. Please try again.",
"error_unknown": "An unknown error occurred."
}
```
## Testing
```dart
void main() {
testWidgets('shows localized text', (tester) async {
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('en'),
home: const HomePage(),
),
);
expect(find.text('Hello'), findsOneWidget);
});
testWidgets('switches locale', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('zh'),
home: const HomePage(),
),
),
);
expect(find.text('你好'), findsOneWidget);
});
}
```
## ARB Placeholders Reference
| Type | Format Options |
|------|----------------|
| `String` | None |
| `int` | `compact`, `compactCurrency`, `compactLong`, `compactSimpleCurrency` |
| `double` | `compact`, `compactCurrency`, `currency`, `decimalPattern`, `decimalPercentPattern`, `percentPattern`, `scientificPattern`, `simpleCurrency` |
| `DateTime` | Any `DateFormat` pattern (yMd, yMMMd, jm, etc.) |
| `num` | Same as `int` and `double` |
## Localization Checklist
| Item | Implementation |
|------|----------------|
| Dependencies | `flutter_localizations`, `intl` |
| l10n.yaml | Configure ARB paths and output |
| ARB files | Create for each supported locale |
| App config | Add delegates and supported locales |
| Generate | Run `flutter gen-l10n` |
| Use translations | `AppLocalizations.of(context)` |
| Date/number formatting | Use `intl` formatters with locale |
| RTL support | Use directional widgets |
| Persist preference | Save user's locale choice |
| Testing | Test with different locales |
---
*Flutter is a trademark of Google LLC. intl is an open-source package by the Dart team.*