797 lines
18 KiB
Markdown
797 lines
18 KiB
Markdown
# Flutter App Template
|
|
|
|
## Basic App Structure
|
|
Complete starter template for Flutter applications.
|
|
|
|
```dart
|
|
// main.dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'app/theme/app_theme.dart';
|
|
import 'app/screens/home_screen.dart';
|
|
import 'app/screens/detail_screen.dart';
|
|
|
|
void main() {
|
|
runApp(MyApp());
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Flutter App',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: AppTheme.lightTheme,
|
|
darkTheme: AppTheme.darkTheme,
|
|
themeMode: ThemeMode.system,
|
|
home: HomeScreen(),
|
|
routes: {
|
|
'/detail': (context) => DetailScreen(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Responsive App Template
|
|
Mobile-first responsive design.
|
|
|
|
```dart
|
|
// main.dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'app/theme/responsive_theme.dart';
|
|
import 'app/screens/home_screen.dart';
|
|
import 'app/widgets/responsive_layout.dart';
|
|
|
|
void main() {
|
|
runApp(MyApp());
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Responsive Flutter App',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: ResponsiveTheme.lightTheme,
|
|
darkTheme: ResponsiveTheme.darkTheme,
|
|
themeMode: ThemeMode.system,
|
|
home: ResponsiveLayout(
|
|
mobile: HomeScreen(),
|
|
tablet: HomeScreen(),
|
|
desktop: HomeScreen(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// app/widgets/responsive_layout.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class ResponsiveLayout extends StatelessWidget {
|
|
final Widget mobile;
|
|
final Widget? tablet;
|
|
final Widget? desktop;
|
|
|
|
const ResponsiveLayout({
|
|
required this.mobile,
|
|
this.tablet,
|
|
this.desktop,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
if (constraints.maxWidth >= 1200 && desktop != null) {
|
|
return desktop!;
|
|
} else if (constraints.maxWidth >= 800 && tablet != null) {
|
|
return tablet!;
|
|
} else {
|
|
return mobile;
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Navigation Template
|
|
Structured navigation with routes.
|
|
|
|
```dart
|
|
// app/routes/app_routes.dart
|
|
class AppRoutes {
|
|
static const String home = '/';
|
|
static const String detail = '/detail';
|
|
static const String settings = '/settings';
|
|
static const String profile = '/profile';
|
|
}
|
|
|
|
class RouteGenerator {
|
|
static Route<dynamic> generateRoute(RouteSettings settings) {
|
|
switch (settings.name) {
|
|
case AppRoutes.home:
|
|
return MaterialPageRoute(
|
|
builder: (_) => HomeScreen(),
|
|
settings: settings,
|
|
);
|
|
case AppRoutes.detail:
|
|
final String id = settings.arguments as String;
|
|
return MaterialPageRoute(
|
|
builder: (_) => DetailScreen(id: id),
|
|
settings: settings,
|
|
);
|
|
case AppRoutes.settings:
|
|
return MaterialPageRoute(
|
|
builder: (_) => SettingsScreen(),
|
|
settings: settings,
|
|
);
|
|
default:
|
|
return MaterialPageRoute(
|
|
builder: (_) => NotFoundScreen(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// app/navigation/navigation_service.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class NavigationService {
|
|
static final GlobalKey<NavigatorState> navigatorKey =
|
|
GlobalKey<NavigatorState>();
|
|
|
|
static Future<dynamic> navigateTo(String routeName, {Object? arguments}) {
|
|
return navigatorKey.currentState!.pushNamed(routeName, arguments: arguments);
|
|
}
|
|
|
|
static void goBack() {
|
|
return navigatorKey.currentState!.pop();
|
|
}
|
|
|
|
static Future<dynamic> replaceWith(String routeName, {Object? arguments}) {
|
|
return navigatorKey.currentState!.pushReplacementNamed(
|
|
routeName,
|
|
arguments: arguments,
|
|
);
|
|
}
|
|
|
|
static Future<dynamic> navigateToAndClearStack(String routeName, {Object? arguments}) {
|
|
return navigatorKey.currentState!.pushNamedAndRemoveUntil(
|
|
routeName,
|
|
(route) => false,
|
|
arguments: arguments,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## State Management Template
|
|
Provider-based state management.
|
|
|
|
```dart
|
|
// app/providers/app_state_provider.dart
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
class AppState extends ChangeNotifier {
|
|
bool _isLoading = false;
|
|
String? _errorMessage;
|
|
|
|
bool get isLoading => _isLoading;
|
|
String? get errorMessage => _errorMessage;
|
|
|
|
void setLoading(bool loading) {
|
|
_isLoading = loading;
|
|
notifyListeners();
|
|
}
|
|
|
|
void setError(String? error) {
|
|
_errorMessage = error;
|
|
notifyListeners();
|
|
}
|
|
|
|
void clearError() {
|
|
_errorMessage = null;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
// app/providers/user_provider.dart
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
class UserProvider extends ChangeNotifier {
|
|
User? _user;
|
|
bool _isLoggedIn = false;
|
|
|
|
User? get user => _user;
|
|
bool get isLoggedIn => _isLoggedIn;
|
|
|
|
Future<void> login(String email, String password) async {
|
|
try {
|
|
// Login logic here
|
|
_user = User(id: '1', email: email);
|
|
_isLoggedIn = true;
|
|
notifyListeners();
|
|
} catch (e) {
|
|
// Handle error
|
|
}
|
|
}
|
|
|
|
void logout() {
|
|
_user = null;
|
|
_isLoggedIn = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
class User {
|
|
final String id;
|
|
final String email;
|
|
|
|
User({required this.id, required this.email});
|
|
}
|
|
|
|
// app/widgets/stateful_builder.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class StatefulBuilder<T> extends StatefulWidget {
|
|
final T initialValue;
|
|
final Widget Function(
|
|
T value,
|
|
void Function(T) updateValue,
|
|
) builder;
|
|
|
|
const StatefulBuilder({
|
|
required this.initialValue,
|
|
required this.builder,
|
|
});
|
|
|
|
@override
|
|
_StatefulBuilderState<T> createState() => _StatefulBuilderState<T>();
|
|
}
|
|
|
|
class _StatefulBuilderState<T> extends State<StatefulBuilder<T>> {
|
|
late T _value;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_value = widget.initialValue;
|
|
}
|
|
|
|
void _updateValue(T newValue) {
|
|
setState(() {
|
|
_value = newValue;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return widget.builder(_value, _updateValue);
|
|
}
|
|
}
|
|
```
|
|
|
|
## API Service Template
|
|
Structured API communication.
|
|
|
|
```dart
|
|
// app/services/api_service.dart
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
class ApiService {
|
|
static const String baseUrl = 'https://api.example.com';
|
|
static const Duration timeout = Duration(seconds: 30);
|
|
|
|
static Map<String, String> get headers => {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
};
|
|
|
|
static Future<Map<String, dynamic>> get(String endpoint) async {
|
|
try {
|
|
final response = await http.get(
|
|
Uri.parse('$baseUrl$endpoint'),
|
|
headers: headers,
|
|
).timeout(timeout);
|
|
|
|
return _handleResponse(response);
|
|
} catch (e) {
|
|
throw _handleError(e);
|
|
}
|
|
}
|
|
|
|
static Future<Map<String, dynamic>> post(
|
|
String endpoint,
|
|
Map<String, dynamic> data,
|
|
) async {
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl$endpoint'),
|
|
headers: headers,
|
|
body: json.encode(data),
|
|
).timeout(timeout);
|
|
|
|
return _handleResponse(response);
|
|
} catch (e) {
|
|
throw _handleError(e);
|
|
}
|
|
}
|
|
|
|
static Future<Map<String, dynamic>> put(
|
|
String endpoint,
|
|
Map<String, dynamic> data,
|
|
) async {
|
|
try {
|
|
final response = await http.put(
|
|
Uri.parse('$baseUrl$endpoint'),
|
|
headers: headers,
|
|
body: json.encode(data),
|
|
).timeout(timeout);
|
|
|
|
return _handleResponse(response);
|
|
} catch (e) {
|
|
throw _handleError(e);
|
|
}
|
|
}
|
|
|
|
static Future<void> delete(String endpoint) async {
|
|
try {
|
|
final response = await http.delete(
|
|
Uri.parse('$baseUrl$endpoint'),
|
|
headers: headers,
|
|
).timeout(timeout);
|
|
|
|
_handleResponse(response);
|
|
} catch (e) {
|
|
throw _handleError(e);
|
|
}
|
|
}
|
|
|
|
static Map<String, dynamic> _handleResponse(http.Response response) {
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
return json.decode(response.body);
|
|
} else {
|
|
throw ApiException(
|
|
'API Error: ${response.statusCode}',
|
|
response.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
static Exception _handleError(dynamic error) {
|
|
if (error is http.TimeoutException) {
|
|
return ApiException('Request timeout', 408);
|
|
} else if (error is http.ClientException) {
|
|
return ApiException('Network error', 0);
|
|
} else {
|
|
return ApiException('Unknown error', 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ApiException implements Exception {
|
|
final String message;
|
|
final int statusCode;
|
|
|
|
ApiException(this.message, this.statusCode);
|
|
|
|
@override
|
|
String toString() => 'ApiException: $message ($statusCode)';
|
|
}
|
|
```
|
|
|
|
## Widget Templates
|
|
Common reusable widgets.
|
|
|
|
```dart
|
|
// app/widgets/custom_button.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class CustomButton extends StatelessWidget {
|
|
final String text;
|
|
final VoidCallback onPressed;
|
|
final bool isLoading;
|
|
final Color? color;
|
|
final Color? textColor;
|
|
final double? width;
|
|
final double? height;
|
|
|
|
const CustomButton({
|
|
required this.text,
|
|
required this.onPressed,
|
|
this.isLoading = false,
|
|
this.color,
|
|
this.textColor,
|
|
this.width,
|
|
this.height,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: width,
|
|
height: height ?? 48,
|
|
child: ElevatedButton(
|
|
onPressed: isLoading ? null : onPressed,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: color ?? Theme.of(context).primaryColor,
|
|
foregroundColor: textColor ?? Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: isLoading
|
|
? SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
textColor ?? Colors.white,
|
|
),
|
|
),
|
|
)
|
|
: Text(text),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// app/widgets/custom_text_field.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class CustomTextField extends StatelessWidget {
|
|
final String? label;
|
|
final String? hint;
|
|
final String? errorText;
|
|
final TextEditingController? controller;
|
|
final bool obscureText;
|
|
final TextInputType keyboardType;
|
|
final Function(String)? onChanged;
|
|
final Function(String)? onSubmitted;
|
|
final Widget? prefixIcon;
|
|
final Widget? suffixIcon;
|
|
|
|
const CustomTextField({
|
|
this.label,
|
|
this.hint,
|
|
this.errorText,
|
|
this.controller,
|
|
this.obscureText = false,
|
|
this.keyboardType = TextInputType.text,
|
|
this.onChanged,
|
|
this.onSubmitted,
|
|
this.prefixIcon,
|
|
this.suffixIcon,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TextField(
|
|
controller: controller,
|
|
obscureText: obscureText,
|
|
keyboardType: keyboardType,
|
|
onChanged: onChanged,
|
|
onSubmitted: onSubmitted,
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
hintText: hint,
|
|
errorText: errorText,
|
|
prefixIcon: prefixIcon,
|
|
suffixIcon: suffixIcon,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).primaryColor,
|
|
width: 2,
|
|
),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).errorColor,
|
|
width: 2,
|
|
),
|
|
),
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// app/widgets/loading_widget.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class LoadingWidget extends StatelessWidget {
|
|
final String? message;
|
|
final double? size;
|
|
|
|
const LoadingWidget({
|
|
this.message,
|
|
this.size = 24,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
Theme.of(context).primaryColor,
|
|
),
|
|
),
|
|
),
|
|
if (message != null) ...[
|
|
SizedBox(height: 16),
|
|
Text(
|
|
message!,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// app/widgets/empty_state_widget.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class EmptyStateWidget extends StatelessWidget {
|
|
final String title;
|
|
final String? subtitle;
|
|
final Widget? action;
|
|
final IconData? icon;
|
|
|
|
const EmptyStateWidget({
|
|
required this.title,
|
|
this.subtitle,
|
|
this.action,
|
|
this.icon,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (icon != null) ...[
|
|
Icon(
|
|
icon!,
|
|
size: 64,
|
|
color: Theme.of(context).disabledColor,
|
|
),
|
|
SizedBox(height: 16),
|
|
],
|
|
Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
color: Theme.of(context).disabledColor,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
if (subtitle != null) ...[
|
|
SizedBox(height: 8),
|
|
Text(
|
|
subtitle!,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Theme.of(context).disabledColor,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
if (action != null) ...[
|
|
SizedBox(height: 24),
|
|
action!,
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Screen Templates
|
|
Common screen patterns.
|
|
|
|
```dart
|
|
// app/screens/base_screen.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class BaseScreen extends StatelessWidget {
|
|
final Widget body;
|
|
final String? title;
|
|
final List<Widget>? actions;
|
|
final Widget? floatingActionButton;
|
|
final Widget? bottomNavigationBar;
|
|
final bool showAppBar;
|
|
|
|
const BaseScreen({
|
|
required this.body,
|
|
this.title,
|
|
this.actions,
|
|
this.floatingActionButton,
|
|
this.bottomNavigationBar,
|
|
this.showAppBar = true,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: showAppBar
|
|
? AppBar(
|
|
title: title != null ? Text(title!) : null,
|
|
actions: actions,
|
|
)
|
|
: null,
|
|
body: SafeArea(child: body),
|
|
floatingActionButton: floatingActionButton,
|
|
bottomNavigationBar: bottomNavigationBar,
|
|
);
|
|
}
|
|
}
|
|
|
|
// app/screens/list_screen.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class ListScreen<T> extends StatelessWidget {
|
|
final List<T> items;
|
|
final Widget Function(BuildContext, T) itemBuilder;
|
|
final String? title;
|
|
final Function(T)? onItemTap;
|
|
final Widget? emptyState;
|
|
final bool isLoading;
|
|
|
|
const ListScreen({
|
|
required this.items,
|
|
required this.itemBuilder,
|
|
this.title,
|
|
this.onItemTap,
|
|
this.emptyState,
|
|
this.isLoading = false,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (isLoading) {
|
|
return LoadingWidget();
|
|
}
|
|
|
|
if (items.isEmpty) {
|
|
return emptyState ??
|
|
EmptyStateWidget(
|
|
title: 'No items found',
|
|
icon: Icons.inbox_outlined,
|
|
);
|
|
}
|
|
|
|
return BaseScreen(
|
|
title: title,
|
|
body: ListView.builder(
|
|
itemCount: items.length,
|
|
itemBuilder: (context, index) {
|
|
final item = items[index];
|
|
return GestureDetector(
|
|
onTap: () => onItemTap?.call(item),
|
|
child: itemBuilder(context, item),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Usage Example
|
|
Complete app using all templates.
|
|
|
|
```dart
|
|
// main.dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'app/providers/user_provider.dart';
|
|
import 'app/services/api_service.dart';
|
|
import 'app/widgets/custom_button.dart';
|
|
import 'app/screens/base_screen.dart';
|
|
|
|
void main() {
|
|
runApp(
|
|
ChangeNotifierProvider(
|
|
create: (_) => UserProvider(),
|
|
child: MyApp(),
|
|
),
|
|
);
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Template App',
|
|
theme: AppTheme.lightTheme,
|
|
darkTheme: AppTheme.darkTheme,
|
|
themeMode: ThemeMode.system,
|
|
home: HomeScreen(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomeScreen extends StatelessWidget {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<UserProvider>(
|
|
builder: (context, userProvider, child) {
|
|
return BaseScreen(
|
|
title: 'Home',
|
|
body: Column(
|
|
children: [
|
|
if (userProvider.isLoggedIn) ...[
|
|
Text('Welcome ${userProvider.user?.email}'),
|
|
CustomButton(
|
|
text: 'Logout',
|
|
onPressed: () => userProvider.logout(),
|
|
),
|
|
] else ...[
|
|
Text('Please login'),
|
|
CustomButton(
|
|
text: 'Login',
|
|
onPressed: () => _showLoginDialog(context),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showLoginDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text('Login'),
|
|
content: CustomTextField(
|
|
label: 'Email',
|
|
hint: 'Enter your email',
|
|
),
|
|
actions: [
|
|
CustomButton(
|
|
text: 'Cancel',
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
CustomButton(
|
|
text: 'Login',
|
|
onPressed: () {
|
|
// Login logic
|
|
Navigator.pop(context);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |