Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Placeholder в Flutter: Концепция и применение
Определение
Placeholder — это временный виджет, который используется как заполнитель на месте элемента UI, который ещё не реализован, не загружен или находится в процессе загрузки. Это инструмент разработки для быстрого прототипирования и визуализации макета приложения.
Встроенный Placeholder виджет
В Flutter есть встроенный виджет Placeholder, который отображает квадратик с сеткой и помогает разработчикам видеть структуру приложения:
class PlaceholderExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Placeholder Demo')),
body: Column(
children: [
// Реальный контент
Text('Header'),
// Заполнитель для контента, который ещё не готов
Container(
height: 200,
child: Placeholder(
fallbackHeight: 200,
fallbackWidth: double.infinity,
color: Colors.blue[100]!,
strokeWidth: 2,
),
),
Text('Footer'),
],
),
);
}
}
Практические применения
1. Loading State с Placeholder
class ImageWithPlaceholder extends StatefulWidget {
final String imageUrl;
final double width;
final double height;
const ImageWithPlaceholder({
required this.imageUrl,
required this.width,
required this.height,
});
@override
State<ImageWithPlaceholder> createState() => _ImageWithPlaceholderState();
}
class _ImageWithPlaceholderState extends State<ImageWithPlaceholder> {
bool _isLoading = true;
bool _hasError = false;
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Container(
width: widget.width,
height: widget.height,
color: Colors.grey[300],
child: Placeholder(
fallbackHeight: widget.height,
fallbackWidth: widget.width,
),
);
}
if (_hasError) {
return Container(
width: widget.width,
height: widget.height,
color: Colors.red[50],
child: Icon(Icons.broken_image, color: Colors.red),
);
}
return Image.network(
widget.imageUrl,
width: widget.width,
height: widget.height,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
width: widget.width,
height: widget.height,
color: Colors.grey[300],
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
_hasError = true;
return Container(
width: widget.width,
height: widget.height,
color: Colors.red[50],
child: Icon(Icons.image_not_supported),
);
},
);
}
}
2. Skeleton Loading (Shimmer эффект)
Это более изощрённая версия placeholder для создания скелета загружаемого контента:
import 'package:shimmer/shimmer.dart';
class SkeletonLoader extends StatelessWidget {
final double width;
final double height;
final BorderRadius borderRadius;
const SkeletonLoader({
this.width = double.infinity,
this.height = 100,
this.borderRadius = const BorderRadius.all(Radius.circular(8)),
});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: borderRadius,
),
),
);
}
}
// Использование
class UserListWithSkeleton extends StatefulWidget {
@override
State<UserListWithSkeleton> createState() => _UserListWithSkeletonState();
}
class _UserListWithSkeletonState extends State<UserListWithSkeleton> {
late Future<List<User>> _usersFuture;
@override
void initState() {
super.initState();
_usersFuture = fetchUsers();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<User>>(
future: _usersFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Показываем skeleton loading
return ListView.builder(
itemCount: 5,
itemBuilder: (context, index) => Padding(
padding: EdgeInsets.all(8),
child: SkeletonLoader(height: 80),
),
);
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final users = snapshot.data ?? [];
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) => UserTile(user: users[index]),
);
},
);
}
Future<List<User>> fetchUsers() async {
await Future.delayed(Duration(seconds: 2));
return [
User(name: 'John', email: 'john@example.com'),
User(name: 'Jane', email: 'jane@example.com'),
];
}
}
3. Placeholder для асинхронных данных
class AsyncDataPlaceholder<T> extends StatelessWidget {
final Future<T> future;
final Widget Function(BuildContext, T) builder;
final Widget? placeholder;
final Widget Function(BuildContext, Object)? errorBuilder;
const AsyncDataPlaceholder({
required this.future,
required this.builder,
this.placeholder,
this.errorBuilder,
});
@override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return placeholder ?? Placeholder();
}
if (snapshot.hasError) {
return errorBuilder?.call(context, snapshot.error!) ??
Center(child: Text('Error: ${snapshot.error}'));
}
return builder(context, snapshot.data as T);
},
);
}
}
// Использование
AsyncDataPlaceholder<String>(
future: fetchTitle(),
placeholder: SkeletonLoader(height: 40),
builder: (context, title) => Text(title, style: Theme.of(context).textTheme.headlineSmall),
errorBuilder: (context, error) => Text('Failed to load', style: TextStyle(color: Colors.red)),
)
Лучшие практики
1. Соответствие размерам
// ✅ Placeholder имеет размер как настоящий контент
Container(
width: 300,
height: 200,
child: Placeholder(),
)
// ❌ Mismatched size
Placeholder() // без явного размера
2. Использование в тестах
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({required this.product});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
// Временно заменить на Placeholder для быстрой разработки
if (product.imageUrl.isNotEmpty)
Image.network(product.imageUrl)
else
Placeholder(fallbackHeight: 200), // TODO: добавить реальное изображение
Text(product.name),
],
),
);
}
}
3. Условный рендеринг с BlocBuilder
BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductLoading) {
return SkeletonLoader();
}
if (state is ProductLoaded) {
return ProductList(products: state.products);
}
return SizedBox.shrink();
},
)
Вывод
Placeholder — это не просто визуальный инструмент, а важная часть разработки приложений Flutter. Он позволяет:
- Быстро прототипировать макеты
- Видеть структуру приложения до реализации всех компонентов
- Улучшить UX, показывая скелет контента вместо пустого пространства
- Разрабатывать UI параллельно с бизнес-логикой
Правильное использование placeholder'ов делает разработку более гибкой и пользовательский опыт более приятным.