Flutterアプリケーション開発において、適切なウィジェット命名は、コードの可読性、保守性、そして全体的な品質を大きく左右します。本記事では、効果的なウィジェットサフィックスの使用方法について、実践的な例を交えながら詳しく解説します。
ウィジェットサフィックスは以下の理由で重要です:
それでは、各カテゴリーと具体的なサフィックスについて、詳細に見ていきましょう。
このグループは、アプリケーションの主要な画面や大きなビューを表すコンポーネントを含みます。
Screen
やView
を含む可能性があり、アプリケーションの最上位レベルのナビゲーション単位となることが多いです。HomePage
, ProfilePage
, SettingsPage
class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ホーム')),
body: Column(
children: [
WelcomeSection(),
RecentActivityList(),
QuickActionButtons(),
],
),
);
}
}
LoginScreen
, ProductDetailScreen
, NotificationScreen
class LoginScreen extends StatefulWidget {
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
LoginForm(),
ForgotPasswordButton(),
SignUpPrompt(),
],
),
),
);
}
}
Screen
よりも小さい単位で、特定の情報や機能を表示するために使用されます。UserProfileView
, ProductListView
, OrderSummaryView
class UserProfileView extends StatelessWidget {
final User user;
UserProfileView({required this.user});
Widget build(BuildContext context) {
return Column(
children: [
UserAvatar(user: user),
UserInfoSection(user: user),
UserStatsDisplay(user: user),
],
);
}
}
このグループは、他のウィジェットを包含し、レイアウトや視覚的なグループ化を提供するコンポーネントを含みます。
ContentContainer
, ImageContainer
, ButtonContainer
class ContentContainer extends StatelessWidget {
final Widget child;
ContentContainer({required this.child});
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
margin: EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 2,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: child,
);
}
}
AuthWrapper
, ThemeWrapper
, ErrorBoundaryWrapper
class AuthWrapper extends StatelessWidget {
final Widget child;
AuthWrapper({required this.child});
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
User? user = snapshot.data;
if (user == null) {
return LoginScreen();
}
return child;
}
return LoadingScreen();
},
);
}
}
Container
よりも具体的な用途を持ち、特定の視覚的スタイルや寸法制約を適用します。InfoBox
, AlertBox
, ImageBox
class AlertBox extends StatelessWidget {
final String message;
final Color color;
AlertBox({required this.message, this.color = Colors.red});
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(12),
margin: EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
border: Border.all(color: color),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
Icon(Icons.warning, color: color),
SizedBox(width: 8),
Expanded(child: Text(message, style: TextStyle(color: color))),
],
),
);
}
}
ProductCard
, UserCard
, EventCard
class ProductCard extends StatelessWidget {
final Product product;
ProductCard({required this.product});
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(product.imageUrl),
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.name, style: Theme.of(context).textTheme.headline6),
SizedBox(height: 4),
Text('\$${product.price}', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text(product.description, maxLines: 2, overflow: TextOverflow.ellipsis),
],
),
),
],
),
);
}
}
このグループは、リストやグリッド内で繰り返し使用される個別の項目を表すコンポーネントを含みます。
ContactTile
, SettingsTile
, NotificationTile
class ContactTile extends StatelessWidget {
final Contact contact;
final VoidCallback onTap;
ContactTile({required this.contact, required this.onTap});
Widget build(BuildContext context) {
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(contact.avatarUrl),
),
title: Text(contact.name),
subtitle: Text(contact.email),
trailing: Icon(Icons.chevron_right),
onTap: onTap,
);
}
}
Tile
よりも一般的で、様々なタイプのリスト項目に使用できます。TodoItem
, ShoppingCartItem
, CommentItem
class TodoItem extends StatelessWidget {
final Todo todo;
final ValueChanged<bool> onComplete;
TodoItem({required this.todo, required this.onComplete});
Widget build(BuildContext context) {
return Row(
children: [
Checkbox(
value: todo.isCompleted,
onChanged: (value) => onComplete(value ?? false),
),
Expanded(
child: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
),
),
),
],
);
}
}
CalendarCell
, DataCell
, GridCell
class CalendarCell extends StatelessWidget {
final DateTime date;
final bool isSelected;
final VoidCallback onTap;
CalendarCell({required this.date, this.isSelected = false, required this.onTap});
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: isSelected ? Theme.of(context).primaryColor : null,
border: Border.all(color: Colors.grey[300]!),
),
child: Center(
child: Text(
'${date.day}',
style: TextStyle(
color: isSelected ? Colors.white : null,
fontWeight: isSelected ? FontWeight.bold : null,
),
),
),
),
);
}
}
このグループは、他のウィジェットを特定の方法で配置するためのコンポーネントを含みます。全体的なページ構造を定義するのに役立ちます。
MenuRow
, ButtonRow
, IconRow
class MenuRow extends StatelessWidget {
final List<MenuItem> items;
MenuRow({required this.items});
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: items.map((item) =>
TextButton(
onPressed: item.onTap,
child: Text(item.title),
)
).toList(),
);
}
}
ProfileColumn
, SettingsColumn
, StatsColumn
class ProfileColumn extends StatelessWidget {
final User user;
ProfileColumn({required this.user});
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(backgroundImage: NetworkImage(user.avatarUrl)),
SizedBox(height: 8),
Text(user.name, style: Theme.of(context).textTheme.headline6),
SizedBox(height: 4),
Text(user.email, style: Theme.of(context).textTheme.subtitle1),
SizedBox(height: 8),
Text(user.bio, style: Theme.of(context).textTheme.bodyText2),
],
);
}
}
PhotoGrid
, CategoryGrid
, DashboardGrid
class PhotoGrid extends StatelessWidget {
final List<String> photoUrls;
PhotoGrid({required this.photoUrls});
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: photoUrls.length,
itemBuilder: (context, index) {
return Image.network(
photoUrls[index],
fit: BoxFit.cover,
);
},
);
}
}
NotificationList
, CommentList
, ProductList
class NotificationList extends StatelessWidget {
final List<Notification> notifications;
NotificationList({required this.notifications});
Widget build(BuildContext context) {
return ListView.builder(
itemCount: notifications.length,
itemBuilder: (context, index) {
final notification = notifications[index];
return ListTile(
leading: Icon(notification.icon),
title: Text(notification.title),
subtitle: Text(notification.message),
trailing: Text(notification.timeAgo),
onTap: () => _handleNotificationTap(notification),
);
},
);
}
void _handleNotificationTap(Notification notification) {
// Handle notification tap
}
}
このグループは、アプリケーションの構造を形成し、コンテンツを論理的に分割するためのコンポーネントを含みます。
ControlPanel
, InfoPanel
, SettingsPanel
class ControlPanel extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Control Panel', style: Theme.of(context).textTheme.headline6),
SizedBox(height: 16),
ToggleButtons(
children: [Icon(Icons.format_bold), Icon(Icons.format_italic), Icon(Icons.format_underline)],
isSelected: [true, false, false],
onPressed: (int index) {
// Handle toggle
},
),
SizedBox(height: 16),
Slider(
value: 0.5,
onChanged: (double value) {
// Handle slider change
},
),
],
),
);
}
}
ProfileSection
, DetailsSection
, SummarySection
class ProfileSection extends StatelessWidget {
final User user;
ProfileSection({required this.user});
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Profile', style: Theme.of(context).textTheme.headline6),
SizedBox(height: 8),
ListTile(
leading: Icon(Icons.person),
title: Text(user.name),
subtitle: Text('Name'),
),
ListTile(
leading: Icon(Icons.email),
title: Text(user.email),
subtitle: Text('Email'),
),
ListTile(
leading: Icon(Icons.phone),
title: Text(user.phone),
subtitle: Text('Phone'),
),
],
);
}
}
AdBlock
, WeatherBlock
, StatisticsBlock
class WeatherBlock extends StatelessWidget {
final WeatherData weather;
WeatherBlock({required this.weather});
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Icon(Icons.wb_sunny, size: 48, color: Colors.orange),
SizedBox(height: 8),
Text('${weather.temperature}°C', style: Theme.of(context).textTheme.headline4),
Text(weather.condition, style: Theme.of(context).textTheme.subtitle1),
SizedBox(height: 8),
Text('Humidity: ${weather.humidity}%'),
Text('Wind: ${weather.windSpeed} km/h'),
],
),
);
}
}
HeaderFragment
, FooterFragment
, SidebarFragment
class HeaderFragment extends StatelessWidget {
final String title;
final VoidCallback? onMenuPressed;
HeaderFragment({required this.title, this.onMenuPressed});
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Theme.of(context).primaryColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
if (onMenuPressed != null)
IconButton(
icon: Icon(Icons.menu, color: Colors.white),
onPressed: onMenuPressed,
),
],
),
);
}
}
このグループは、アプリケーション全体で再利用可能な小さなUI要素を含みます。
RatingComponent
, SearchComponent
, PaginationComponent
class RatingComponent extends StatelessWidget {
final double rating;
final int maxRating;
final double size;
RatingComponent({
required this.rating,
this.maxRating = 5,
this.size = 24,
});
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(maxRating, (index) {
return Icon(
index < rating.floor() ? Icons.star : Icons.star_border,
color: Colors.amber,
size: size,
);
}),
);
}
}
IconElement
, AvatarElement
, BadgeElement
class BadgeElement extends StatelessWidget {
final String text;
final Color color;
BadgeElement({required this.text, this.color = Colors.red});
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(12),
),
child: Text(
text,
style: TextStyle(color: Colors.white, fontSize: 12),
),
);
}
}
このグループは、ユーザーからの入力を受け付けるためのコンポーネントを含みます。
EmailField
, PasswordField
, DateField
class EmailField extends StatelessWidget {
final TextEditingController controller;
final String? Function(String?)? validator;
EmailField({required this.controller, this.validator});
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
validator: validator ?? (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
},
);
}
}
ColorInput
, SliderInput
, ToggleInput
class SliderInput extends StatefulWidget {
final double initialValue;
final double min;
final double max;
final ValueChanged<double> onChanged;
SliderInput({
required this.initialValue,
required this.min,
required this.max,
required this.onChanged,
});
_SliderInputState createState() => _SliderInputState();
}
class _SliderInputState extends State<SliderInput> {
late double _value;
void initState() {
super.initState();
_value = widget.initialValue;
}
Widget build(BuildContext context) {
return Column(
children: [
Slider(
value: _value,
min: widget.min,
max: widget.max,
onChanged: (newValue) {
setState(() {
_value = newValue;
});
widget.onChanged(newValue);
},
),
Text('Value: ${_value.toStringAsFixed(2)}'),
],
);
}
}
LoginForm
, RegistrationForm
, ContactForm
class LoginForm extends StatefulWidget {
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
EmailField(controller: _emailController),
SizedBox(height: 16),
PasswordField(controller: _passwordController),
SizedBox(height: 24),
ElevatedButton(
onPressed: _submitForm,
child: Text('Login'),
),
],
),
);
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
// Perform login logic
print('Login with: ${_emailController.text}');
}
}
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
}
このグループは、ユーザーのアクションをトリガーするためのコンポーネントを含みます。
SubmitButton
, CancelButton
, IconButton
class SubmitButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final bool isLoading;
SubmitButton({
required this.text,
required this.onPressed,
this.isLoading = false,
});
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(text),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
);
}
}
SwipeAction
, LongPressAction
, DoubleTapAction
class SwipeAction extends StatelessWidget {
final Widget child;
final VoidCallback onSwipe;
SwipeAction({required this.child, required this.onSwipe});
Widget build(BuildContext context) {
return GestureDetector(
onHorizontalDragEnd: (details) {
if (details.primaryVelocity! < 0) {
onSwipe();
}
},
child: child,
);
}
}
このグループは、情報を視覚的に表示するためのコンポーネントを含みます。
ChartDisplay
, GraphDisplay
, StatisticsDisplay
class ChartDisplay extends StatelessWidget {
final List<double> data;
ChartDisplay({required this.data});
Widget build(BuildContext context) {
return Container(
height: 200,
padding: EdgeInsets.all(16),
child: CustomPaint(
painter: ChartPainter(data),
size: Size.infinite,
),
);
}
}
class ChartPainter extends CustomPainter {
final List<double> data;
ChartPainter(this.data);
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 2
..style = PaintingStyle.stroke;
final path = Path();
final width = size.width / (data.length - 1);
final maxData = data.reduce(math.max);
for (int i = 0; i < data.length; i++) {
final x = i * width;
final y = size.height - (data[i] / maxData * size.height);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, paint);
}
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
HeadlineText
, DescriptionText
, ErrorText
class HeadlineText extends StatelessWidget {
final String text;
final TextAlign textAlign;
HeadlineText({required this.text, this.textAlign = TextAlign.left});
Widget build(BuildContext context) {
return Text(
text,
style: Theme.of(context).textTheme.headline4,
textAlign: textAlign,
);
}
}
FieldLabel
, StatusLabel
, PriceLabel
class StatusLabel extends StatelessWidget {
final String status;
StatusLabel({required this.status});
Widget build(BuildContext context) {
Color color;
IconData icon;
switch (status.toLowerCase()) {
case 'active':
color = Colors.green;
icon = Icons.check_circle;
break;
case 'pending':
color = Colors.orange;
icon = Icons.access_time;
break;
case 'inactive':
color = Colors.red;
icon = Icons.cancel;
break;
default:
color = Colors.grey;
icon = Icons.help;
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 16),
SizedBox(width: 4),
Text(
status,
style: TextStyle(color: color, fontWeight: FontWeight.bold),
),
],
);
}
}
このグループは、アプリケーション内のナビゲーションを管理するためのコンポーネントを含みます。
TopBar
, BottomBar
, SideBar
class BottomBar extends StatelessWidget {
final int currentIndex;
final ValueChanged<int> onTap;
BottomBar({required this.currentIndex, required this.onTap});
Widget build(BuildContext context) {
return BottomNavigationBar(
currentIndex: currentIndex,
onTap: onTap,
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
);
}
}
DropdownMenu
, SideMenu
, ContextMenu
class SideMenu extends StatelessWidget {
final List<MenuItem> items;
final ValueChanged<MenuItem> onItemSelected;
SideMenu({required this.items, required this.onItemSelected});
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Text(
'Menu',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
...items.map((item) => ListTile(
leading: Icon(item.icon),
title: Text(item.title),
onTap: () {
onItemSelected(item);
Navigator.pop(context);
},
)),
],
),
);
}
}
class MenuItem {
final String title;
final IconData icon;
MenuItem({required this.title, required this.icon});
}
AppNavigator
, TabNavigator
, StackNavigator
class AppNavigator extends StatelessWidget {
Widget build(BuildContext context) {
return Navigator(
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => HomeScreen());
case '/profile':
return MaterialPageRoute(builder: (_) => ProfileScreen());
case '/settings':
return MaterialPageRoute(builder: (_) => SettingsScreen());
default:
return MaterialPageRoute(builder: (_) => NotFoundScreen());
}
},
);
}
}
ソフトウェアエンジニア。趣味は競馬、写真、ゲーム。
お問い合わせはTwitterのDMでお願いします。