← Назад к вопросам
Приведи пример использования принципа DRY
2.2 Middle🔥 111 комментариев
#Архитектура Flutter#ООП и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Пример использования принципа DRY в Flutter
DRY (Don't Repeat Yourself) — один из самых важных принципов чистого кода. Суть: не повторяй код, вынеси в функции, компоненты, константы. Покажу множество примеров нарушения и как это исправить.
Пример 1: Вынос стилей в переменные
❌ Плохо: Дублирование стилей
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
// Кнопка 1
Container(
width: 120,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'Button 1',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
// Кнопка 2 (скопирована, дублируется!)
Container(
width: 120,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'Button 2',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
// Кнопка 3 (скопирована ещё раз!)
Container(
width: 120,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'Button 3',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
}
✅ Хорошо: Вынесение стилей
class MyApp extends StatelessWidget {
// Константы стилей
static const double _buttonWidth = 120;
static const double _buttonHeight = 50;
static const double _borderRadius = 8;
static final _buttonDecoration = BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(_borderRadius),
);
static final _buttonTextStyle = TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
);
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildButton('Button 1'),
_buildButton('Button 2'),
_buildButton('Button 3'),
],
);
}
Widget _buildButton(String label) {
return Container(
width: _buttonWidth,
height: _buttonHeight,
decoration: _buttonDecoration,
child: Center(
child: Text(
label,
style: _buttonTextStyle,
),
),
);
}
}
Пример 2: Вынос компонентов
❌ Плохо: Повторяющийся ListTile
class UserListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: [
// Item 1
Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
backgroundImage: NetworkImage('url1'),
),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('John Doe', style: TextStyle(fontWeight: FontWeight.bold)),
Text('john@example.com', style: TextStyle(color: Colors.grey)),
],
),
],
),
),
// Item 2 (дублировано)
Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
backgroundImage: NetworkImage('url2'),
),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Jane Smith', style: TextStyle(fontWeight: FontWeight.bold)),
Text('jane@example.com', style: TextStyle(color: Colors.grey)),
],
),
],
),
),
],
);
}
}
✅ Хорошо: Компонент UserTile
class UserTile extends StatelessWidget {
final String name;
final String email;
final String avatarUrl;
const UserTile({
required this.name,
required this.email,
required this.avatarUrl,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(backgroundImage: NetworkImage(avatarUrl)),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: TextStyle(fontWeight: FontWeight.bold)),
Text(email, style: TextStyle(color: Colors.grey)),
],
),
],
),
);
}
}
class UserListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: [
UserTile(
name: 'John Doe',
email: 'john@example.com',
avatarUrl: 'url1',
),
UserTile(
name: 'Jane Smith',
email: 'jane@example.com',
avatarUrl: 'url2',
),
],
);
}
}
Пример 3: Валидация (повторяющаяся логика)
❌ Плохо: Повторение валидации
class RegistrationForm extends StatefulWidget {
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
String _email = '';
String _password = '';
String _phone = '';
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) => _email = value,
decoration: InputDecoration(
errorText: _email.isEmpty || !_email.contains('@')
? 'Invalid email'
: null,
),
),
TextField(
onChanged: (value) => _password = value,
decoration: InputDecoration(
errorText: _password.isEmpty || _password.length < 8
? 'Password must be 8+ chars'
: null,
),
),
TextField(
onChanged: (value) => _phone = value,
decoration: InputDecoration(
errorText: _phone.isEmpty || _phone.length < 10
? 'Invalid phone'
: null,
),
),
],
);
}
}
✅ Хорошо: Утилиты валидации
class ValidationUtils {
static String? validateEmail(String? value) {
if (value?.isEmpty ?? true) return 'Email required';
if (!value!.contains('@')) return 'Invalid email';
return null;
}
static String? validatePassword(String? value) {
if (value?.isEmpty ?? true) return 'Password required';
if (value!.length < 8) return 'Min 8 characters';
return null;
}
static String? validatePhone(String? value) {
if (value?.isEmpty ?? true) return 'Phone required';
if (value!.length < 10) return 'Invalid phone';
return null;
}
}
class RegistrationForm extends StatefulWidget {
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
String _email = '';
String _password = '';
String _phone = '';
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) => _email = value,
decoration: InputDecoration(
errorText: ValidationUtils.validateEmail(_email),
),
),
TextField(
onChanged: (value) => _password = value,
decoration: InputDecoration(
errorText: ValidationUtils.validatePassword(_password),
),
),
TextField(
onChanged: (value) => _phone = value,
decoration: InputDecoration(
errorText: ValidationUtils.validatePhone(_phone),
),
),
],
);
}
}
Пример 4: API запросы (повторяющиеся)
❌ Плохо: Каждый раз пишешь http.get
class UserRepository {
Future<List<User>> getUsers() async {
final response = await http.get(
Uri.parse('https://api.example.com/users'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token_123',
},
);
if (response.statusCode != 200) {
throw Exception('Failed to load users');
}
final list = jsonDecode(response.body) as List;
return list.map((item) => User.fromJson(item)).toList();
}
Future<User?> getUserById(int id) async {
final response = await http.get(
Uri.parse('https://api.example.com/users/$id'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token_123',
},
);
if (response.statusCode != 200) {
throw Exception('Failed to load user');
}
return User.fromJson(jsonDecode(response.body));
}
}
✅ Хорошо: ApiClient обёртка
class ApiClient {
static const String _baseUrl = 'https://api.example.com';
static const String _token = 'Bearer token_123';
static final _headers = {
'Content-Type': 'application/json',
'Authorization': _token,
};
Future<dynamic> get(String endpoint) async {
final response = await http.get(
Uri.parse('$_baseUrl$endpoint'),
headers: _headers,
);
_checkResponse(response);
return jsonDecode(response.body);
}
Future<dynamic> post(String endpoint, Map<String, dynamic> body) async {
final response = await http.post(
Uri.parse('$_baseUrl$endpoint'),
headers: _headers,
body: jsonEncode(body),
);
_checkResponse(response);
return jsonDecode(response.body);
}
void _checkResponse(Response response) {
if (response.statusCode != 200) {
throw Exception('API Error: ${response.statusCode}');
}
}
}
class UserRepository {
final ApiClient _api;
UserRepository(this._api);
Future<List<User>> getUsers() async {
final data = await _api.get('/users') as List;
return data.map((item) => User.fromJson(item)).toList();
}
Future<User?> getUserById(int id) async {
final data = await _api.get('/users/$id');
return User.fromJson(data);
}
}
Пример 5: Форматирование (повторяющиеся функции)
❌ Плохо: Везде форматируешь дату
class OrderHistory extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: [
ListTile(
title: Text('Order #1'),
subtitle: Text(
DateTime.parse('2024-03-26T10:30:00').toLocal().toString(),
),
),
ListTile(
title: Text('Order #2'),
subtitle: Text(
DateTime.parse('2024-03-25T15:45:00').toLocal().toString(),
),
),
],
);
}
}
✅ Хорошо: Утилиты форматирования
class DateFormatter {
static String formatDate(DateTime date) {
return '${date.day}/${date.month}/${date.year}';
}
static String formatDateTime(DateTime date) {
return '${formatDate(date)} ${date.hour}:${date.minute}';
}
static String formatRelative(DateTime date) {
final diff = DateTime.now().difference(date);
if (diff.inSeconds < 60) return 'just now';
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
if (diff.inHours < 24) return '${diff.inHours}h ago';
return '${diff.inDays}d ago';
}
}
class OrderHistory extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: [
ListTile(
title: Text('Order #1'),
subtitle: Text(DateFormatter.formatRelative(
DateTime.parse('2024-03-26T10:30:00'),
)),
),
ListTile(
title: Text('Order #2'),
subtitle: Text(DateFormatter.formatRelative(
DateTime.parse('2024-03-25T15:45:00'),
)),
),
],
);
}
}
Пример 6: Цвета (постоянное дублирование)
❌ Плохо: Хардкодные цвета везде
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Color(0xFF2196F3),
scaffoldBackgroundColor: Color(0xFFFAFAFA),
textTheme: TextTheme(
headlineLarge: TextStyle(color: Color(0xFF212121)),
bodyMedium: TextStyle(color: Color(0xFF757575)),
),
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xFF2196F3), # Повтор!
title: Text('Home'),
),
body: Column(
children: [
Text(
'Welcome',
style: TextStyle(color: Color(0xFF212121)), # Повтор!
),
Text(
'Subtitle',
style: TextStyle(color: Color(0xFF757575)), # Повтор!
),
],
),
);
}
}
✅ Хорошо: Констант для цветов
class AppColors {
static const Color primary = Color(0xFF2196F3);
static const Color background = Color(0xFFFAFAFA);
static const Color textPrimary = Color(0xFF212121);
static const Color textSecondary = Color(0xFF757575);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: AppColors.primary,
scaffoldBackgroundColor: AppColors.background,
textTheme: TextTheme(
headlineLarge: TextStyle(color: AppColors.textPrimary),
bodyMedium: TextStyle(color: AppColors.textSecondary),
),
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppColors.primary,
title: Text('Home'),
),
body: Column(
children: [
Text(
'Welcome',
style: TextStyle(color: AppColors.textPrimary),
),
Text(
'Subtitle',
style: TextStyle(color: AppColors.textSecondary),
),
],
),
);
}
}
Резюме DRY
Как применить DRY:
-
Вынеси стили в переменные/константы
const _buttonHeight = 50final _buttonDecoration = BoxDecoration(...)
-
Создай компоненты для повторяющихся UI
UserTileвместо дублирования ListTile кодаCustomButtonвместо повтора Container'ов
-
Создай утилиты для повторяющейся логики
ValidationUtils.validateEmail()DateFormatter.formatDate()
-
Создай wrapper'ы для API/Database
ApiClientдля http запросовRepositoryдля доступа к данным
-
Используй константы для значений
- Цвета в
AppColors - Строки в
AppStrings - Размеры в
AppDimensions
- Цвета в
Преимущества:
- ✅ Меньше кода
- ✅ Легче менять
- ✅ Меньше ошибок
- ✅ Проще читать
- ✅ Легче тестировать