Flutter で業務用管理画面:テーブル・フィルタ・エクスポート機能の実装指針
業務システムを開発する際、管理画面は避けて通れない重要な要素です。特に大量のデータを扱うテーブル表示、柔軟なフィルタ機能、そしてデータエクスポート機能は、管理者にとって必須の機能となっています。
Flutter はモバイルアプリ開発のイメージが強いですが、実は Web やデスクトップアプリにも対応しており、業務用管理画面の開発にも適しています。本記事では、Flutter を使って業務用管理画面を構築する際の、テーブル・フィルタ・エクスポート機能の実装指針をご紹介します。
実務で使える具体的なコード例とともに、パフォーマンスや保守性を考慮した設計方法をお伝えしますので、ぜひ最後までご覧ください。
背景
業務用管理画面の重要性
現代のビジネスにおいて、データは最も重要な資産の一つです。顧客情報、売上データ、在庫管理など、あらゆる業務データを効率的に管理・分析するために、管理画面は欠かせません。
管理画面には以下のような要件が求められます。
- 大量のデータを一覧表示できるテーブル機能
- データを素早く探せる検索・フィルタ機能
- レポート作成のためのエクスポート機能
- 直感的で使いやすい UI/UX
- レスポンシブなデザイン
Flutter が業務用管理画面に適している理由
Flutter は以下の特徴により、業務用管理画面の開発に適しています。
まず、クロスプラットフォーム対応により、一つのコードベースで Web・デスクトップ・モバイルに対応できます。これにより開発コストを大幅に削減できるでしょう。
次に、高速な動作とスムーズなアニメーションが実現できます。Flutter は独自のレンダリングエンジンを持つため、ネイティブアプリに匹敵するパフォーマンスを発揮します。
さらに、豊富なパッケージエコシステムがあります。pub.dev には業務アプリ開発に役立つパッケージが数多く公開されています。
下の図は、Flutter で構築する管理画面の基本的なアーキテクチャを示しています。
mermaidflowchart TB
ui["UI Layer<br/>(Widgets)"]
state["State Management<br/>(Provider/Riverpod)"]
repo["Repository Layer<br/>(データ取得・加工)"]
api["API/Backend<br/>(REST/GraphQL)"]
ui -->|ユーザー操作| state
state -->|状態変更| ui
state -->|データ要求| repo
repo -->|API呼び出し| api
api -->|JSON/データ| repo
repo -->|整形済みデータ| state
この図から、UI とビジネスロジックが明確に分離されていることがわかります。このような設計により、保守性と拡張性が向上するのです。
管理画面で扱うデータの特徴
業務用管理画面で扱うデータには、以下のような特徴があります。
データ量が多く、数千から数万件のレコードを扱うことも珍しくありません。そのため、パフォーマンスを考慮した実装が必要になります。
また、データ構造が複雑で、リレーションを持つテーブルや、ネストされた JSON データを扱うこともあります。
さらに、リアルタイム性が求められる場合もあり、在庫管理や注文管理では、データの即時反映が重要になるでしょう。
課題
大量データ表示におけるパフォーマンス問題
業務用管理画面で最も大きな課題は、大量のデータを表示する際のパフォーマンスです。
Flutter の標準的な DataTable ウィジェットは、シンプルな実装には適していますが、数千件のデータを表示しようとするとパフォーマンスが低下します。すべての行を一度にレンダリングするため、メモリ使用量が増大し、スクロールが遅くなってしまうのです。
また、データの読み込み時間も課題となります。API からすべてのデータを一度に取得すると、初期表示に時間がかかり、ユーザー体験が悪化します。
フィルタ機能の複雑性
次に、フィルタ機能の実装には複雑さが伴います。
単純な検索機能だけでなく、複数条件での絞り込み、日付範囲指定、カテゴリー選択など、多様なフィルタ要件に対応する必要があります。これらの状態管理をどのように行うかが重要なポイントとなるでしょう。
また、フィルタを適用した状態でソート機能と組み合わせると、実装の複雑度がさらに増します。
下の図は、フィルタ処理のフローを示しています。
mermaidflowchart LR
raw["元データ"]
filter1["検索フィルタ"]
filter2["カテゴリーフィルタ"]
filter3["日付範囲フィルタ"]
sort["ソート処理"]
result["表示データ"]
raw --> filter1
filter1 --> filter2
filter2 --> filter3
filter3 --> sort
sort --> result
このように、複数のフィルタを順次適用していく必要があり、各ステップでのパフォーマンスを考慮しなければなりません。
エクスポート機能の要件
エクスポート機能にも様々な課題があります。
まず、フォーマット対応です。CSV だけでなく、Excel(XLSX)形式での出力を求められることが多く、それぞれに適したライブラリの選定が必要になります。
次に、大量データのエクスポート時のメモリ管理も重要です。数万件のデータを一度にメモリに展開すると、アプリがクラッシュする可能性があります。
さらに、エクスポート処理中の UI フリーズを防ぐため、非同期処理の実装も必要でしょう。
UI/UX の課題
業務用管理画面では、機能が豊富な分、UI が複雑になりがちです。
限られた画面スペースに、テーブル、フィルタ UI、アクションボタンなどを配置する必要があり、レイアウト設計が難しくなります。特にモバイル対応を考慮すると、レスポンシブなデザインが求められます。
また、操作性の面でも、キーボードショートカット、ページネーション、無限スクロールなど、ユーザーの効率を上げる工夫が必要です。
解決策
適切なパッケージの選定
Flutter で業務用管理画面を構築する際は、適切なパッケージを選定することが成功の鍵となります。
テーブル表示パッケージ
テーブル表示には data_table_2 パッケージの使用を強く推奨します。このパッケージは、Flutter 標準の DataTable を拡張したもので、以下の機能を提供します。
| # | 機能 | 説明 |
|---|---|---|
| 1 | 固定ヘッダー | スクロール時もヘッダーが固定される |
| 2 | 固定カラム | 左端の列を固定できる |
| 3 | ページネーション | 大量データを分割表示できる |
| 4 | ソート機能 | 各列でのソートに対応 |
| 5 | レスポンシブ対応 | 画面サイズに応じた表示調整 |
エクスポート関連パッケージ
エクスポート機能には、以下のパッケージを組み合わせて使用します。
| # | パッケージ | 用途 |
|---|---|---|
| 1 | csv | CSV ファイルの生成 |
| 2 | excel | Excel (XLSX) ファイルの生成 |
| 3 | path_provider | ファイル保存先の取得 |
| 4 | file_picker | ファイル保存ダイアログの表示 |
状態管理パッケージ
状態管理には riverpod または provider の使用をおすすめします。これらは Flutter 公式が推奨する状態管理手法であり、テストも容易です。
アーキテクチャ設計
保守性と拡張性を確保するため、レイヤーを明確に分離した設計を採用しましょう。
レイヤー構成
以下の 3 層構造を基本とします。
**Presentation Layer(UI 層)**では、ウィジェットのみを配置し、ビジネスロジックは含めません。ユーザーの操作を受け取り、State Management Layer に通知する役割を担います。
**State Management Layer(状態管理層)**では、アプリケーションの状態を管理し、データの取得・加工・フィルタリングを行います。Repository Layer からデータを取得し、UI に必要な形式に整形します。
**Repository Layer(データ層)**では、API との通信やローカルストレージへのアクセスを担当し、データソースの詳細を隠蔽します。
下の図は、各層の責務と相互作用を示しています。
mermaidflowchart TB
subgraph presentation["Presentation Layer"]
table["TableWidget"]
filter["FilterWidget"]
export["ExportButton"]
end
subgraph state["State Management"]
provider["TableStateProvider"]
filterState["FilterState"]
end
subgraph repository["Repository Layer"]
dataRepo["DataRepository"]
exportService["ExportService"]
end
table -->|イベント| provider
filter -->|フィルタ変更| filterState
export -->|エクスポート要求| provider
provider -->|データ取得| dataRepo
provider -->|エクスポート実行| exportService
filterState -->|フィルタ条件| provider
このように責務を分離することで、各層を独立してテストでき、仕様変更にも柔軟に対応できます。
パフォーマンス最適化戦略
大量データを扱う際は、以下の戦略を採用しましょう。
ページネーション
サーバー側でページネーションを実装し、必要なデータのみを取得します。一度に表示する件数は、50〜100 件程度が適切でしょう。
仮想スクロール
data_table_2 パッケージは内部的に仮想スクロールを実装しており、画面に表示されている行のみをレンダリングします。これにより、数千件のデータでもスムーズにスクロールできます。
データのキャッシング
一度取得したデータはメモリにキャッシュし、同じデータへのアクセス時は API 呼び出しを省略します。ただし、キャッシュの有効期限を設定し、古いデータが表示され続けないよう注意が必要です。
非同期処理の活用
データ取得やエクスポート処理は、必ず非同期で実行し、UI スレッドをブロックしないようにします。Dart の async/await を活用することで、読みやすいコードを維持できるでしょう。
具体例
ここからは、実際の実装例を段階的に説明します。
プロジェクトのセットアップ
まず、必要なパッケージをインストールしましょう。
pubspec.yaml への依存関係追加
プロジェクトの pubspec.yaml ファイルに、必要なパッケージを追加します。
yamldependencies:
flutter:
sdk: flutter
# 状態管理
flutter_riverpod: ^2.4.0
# テーブル表示
data_table_2: ^2.5.0
# エクスポート機能
csv: ^5.1.0
excel: ^4.0.0
# ファイル操作
path_provider: ^2.1.0
file_picker: ^6.0.0
# HTTP通信
dio: ^5.3.0
パッケージを追加したら、ターミナルで以下のコマンドを実行してインストールします。
bashyarn install
# または
flutter pub get
データモデルの定義
管理画面で扱うデータのモデルを定義します。ここでは、ユーザー管理画面を例に説明しましょう。
User モデルクラス
以下は、ユーザーデータを表すモデルクラスです。JSON からのデシリアライズとシリアライズをサポートしています。
dart// models/user.dart
class User {
final String id;
final String name;
final String email;
final String role;
final DateTime createdAt;
final bool isActive;
User({
required this.id,
required this.name,
required this.email,
required this.role,
required this.createdAt,
required this.isActive,
});
JSON デシリアライズ
API から取得した JSON データを User オブジェクトに変換するファクトリーコンストラクタを定義します。
dart // JSONからUserオブジェクトを生成
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
role: json['role'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
isActive: json['is_active'] as bool,
);
}
JSON シリアライズ
User オブジェクトを JSON に変換するメソッドも定義します。エクスポート機能で使用します。
dart // UserオブジェクトをJSONに変換
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'role': role,
'created_at': createdAt.toIso8601String(),
'is_active': isActive,
};
}
}
これでデータモデルの準備が完了しました。次に、データ取得のための Repository を実装します。
Repository Layer の実装
Repository Layer は、API との通信を担当します。
UserRepository クラスの基本構造
以下は、ユーザーデータを取得する Repository クラスです。
dart// repositories/user_repository.dart
import 'package:dio/dio.dart';
import '../models/user.dart';
class UserRepository {
final Dio _dio;
final String _baseUrl = 'https://api.example.com';
UserRepository({Dio? dio}) : _dio = dio ?? Dio();
ページネーション付きデータ取得
ページネーションをサポートしたデータ取得メソッドを実装します。
dart // ページネーション付きでユーザー一覧を取得
Future<PaginatedUsers> getUsers({
required int page,
required int limit,
String? searchQuery,
String? roleFilter,
}) async {
try {
// クエリパラメータの構築
final queryParams = {
'page': page,
'limit': limit,
if (searchQuery != null && searchQuery.isNotEmpty)
'search': searchQuery,
if (roleFilter != null && roleFilter.isNotEmpty)
'role': roleFilter,
};
レスポンス処理
API からのレスポンスを処理し、User オブジェクトのリストに変換します。
dart // API呼び出し
final response = await _dio.get(
'$_baseUrl/users',
queryParameters: queryParams,
);
// レスポンスデータの変換
final data = response.data as Map<String, dynamic>;
final users = (data['users'] as List)
.map((json) => User.fromJson(json))
.toList();
// ページネーション情報とともに返却
return PaginatedUsers(
users: users,
total: data['total'] as int,
page: page,
limit: limit,
);
} catch (e) {
throw Exception('Failed to fetch users: $e');
}
}
}
ページネーション情報クラス
ページネーション情報を保持するクラスも定義しておきます。
dart// models/paginated_users.dart
class PaginatedUsers {
final List<User> users;
final int total; // 総件数
final int page; // 現在のページ
final int limit; // 1ページあたりの件数
PaginatedUsers({
required this.users,
required this.total,
required this.page,
required this.limit,
});
// 総ページ数を計算
int get totalPages => (total / limit).ceil();
}
State Management の実装
Riverpod を使用して、テーブルの状態を管理します。
フィルタ状態の定義
まず、フィルタの状態を表すクラスを定義します。
dart// providers/filter_state.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FilterState {
final String searchQuery; // 検索キーワード
final String? roleFilter; // ロールフィルタ
final int currentPage; // 現在のページ
final int itemsPerPage; // 1ページの件数
FilterState({
this.searchQuery = '',
this.roleFilter,
this.currentPage = 1,
this.itemsPerPage = 50,
});
フィルタ状態のコピーメソッド
イミュータブルな状態更新のため、copyWith メソッドを実装します。
dart // 状態の一部を更新した新しいインスタンスを生成
FilterState copyWith({
String? searchQuery,
String? roleFilter,
int? currentPage,
int? itemsPerPage,
}) {
return FilterState(
searchQuery: searchQuery ?? this.searchQuery,
roleFilter: roleFilter ?? this.roleFilter,
currentPage: currentPage ?? this.currentPage,
itemsPerPage: itemsPerPage ?? this.itemsPerPage,
);
}
}
StateNotifier の実装
フィルタ状態を管理する StateNotifier を実装します。
dart// フィルタ状態を管理するStateNotifier
class FilterStateNotifier extends StateNotifier<FilterState> {
FilterStateNotifier() : super(FilterState());
// 検索キーワードを更新
void updateSearchQuery(String query) {
state = state.copyWith(searchQuery: query, currentPage: 1);
}
// ロールフィルタを更新
void updateRoleFilter(String? role) {
state = state.copyWith(roleFilter: role, currentPage: 1);
}
// ページを変更
void changePage(int page) {
state = state.copyWith(currentPage: page);
}
}
Provider の定義
フィルタ状態とユーザーデータ取得の Provider を定義します。
dart// providers/user_providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// フィルタ状態のProvider
final filterStateProvider =
StateNotifierProvider<FilterStateNotifier, FilterState>(
(ref) => FilterStateNotifier(),
);
// UserRepositoryのProvider
final userRepositoryProvider = Provider<UserRepository>(
(ref) => UserRepository(),
);
非同期データ取得 Provider
フィルタ状態に基づいてユーザーデータを取得する FutureProvider を定義します。
dart// ユーザーデータ取得のFutureProvider
final usersProvider = FutureProvider<PaginatedUsers>((ref) async {
// フィルタ状態を監視
final filterState = ref.watch(filterStateProvider);
final repository = ref.watch(userRepositoryProvider);
// フィルタ条件に基づいてデータを取得
return repository.getUsers(
page: filterState.currentPage,
limit: filterState.itemsPerPage,
searchQuery: filterState.searchQuery.isNotEmpty
? filterState.searchQuery
: null,
roleFilter: filterState.roleFilter,
);
});
このように Provider を定義することで、フィルタ条件が変更されると自動的にデータが再取得されます。
テーブル UI の実装
data_table_2 パッケージを使用して、テーブル UI を実装します。
UserTableWidget の基本構造
以下は、ユーザー一覧テーブルのウィジェットです。
dart// widgets/user_table_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:data_table_2/data_table_2.dart';
class UserTableWidget extends ConsumerWidget {
const UserTableWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// usersProviderを監視
final usersAsync = ref.watch(usersProvider);
データ読み込み状態の処理
非同期データの読み込み状態に応じて、適切な UI を表示します。
dart return usersAsync.when(
// データ読み込み中
loading: () => const Center(
child: CircularProgressIndicator(),
),
// エラー発生時
error: (error, stack) => Center(
child: Text('Error: $error'),
),
// データ取得成功時
data: (paginatedUsers) => _buildTable(context, ref, paginatedUsers),
);
}
テーブルの構築
DataTable2 ウィジェットを使用してテーブルを構築します。
dart // テーブルを構築するメソッド
Widget _buildTable(
BuildContext context,
WidgetRef ref,
PaginatedUsers paginatedUsers,
) {
return Column(
children: [
// テーブル本体
Expanded(
child: DataTable2(
columnSpacing: 12,
horizontalMargin: 12,
minWidth: 900,
columns: _buildColumns(),
rows: _buildRows(paginatedUsers.users),
),
),
カラム定義
テーブルのカラムを定義します。ソート機能も有効にします。
dart // テーブルのカラムを定義
List<DataColumn2> _buildColumns() {
return [
const DataColumn2(
label: Text('ID'),
size: ColumnSize.S,
),
const DataColumn2(
label: Text('名前'),
size: ColumnSize.L,
),
const DataColumn2(
label: Text('メールアドレス'),
size: ColumnSize.L,
),
const DataColumn2(
label: Text('ロール'),
size: ColumnSize.M,
),
const DataColumn2(
label: Text('登録日'),
size: ColumnSize.M,
),
const DataColumn2(
label: Text('ステータス'),
size: ColumnSize.S,
),
];
}
行データの構築
ユーザーデータから DataRow を生成します。
dart // ユーザーデータからDataRowを生成
List<DataRow2> _buildRows(List<User> users) {
return users.map((user) {
return DataRow2(
cells: [
DataCell(Text(user.id)),
DataCell(Text(user.name)),
DataCell(Text(user.email)),
DataCell(_buildRoleBadge(user.role)),
DataCell(Text(_formatDate(user.createdAt))),
DataCell(_buildStatusBadge(user.isActive)),
],
onTap: () => _onUserTap(user),
);
}).toList();
}
バッジ UI の実装
ロールやステータスを視覚的に表示するバッジを作成します。
dart // ロールバッジの作成
Widget _buildRoleBadge(String role) {
Color color;
switch (role) {
case 'admin':
color = Colors.red;
break;
case 'editor':
color = Colors.blue;
break;
default:
color = Colors.grey;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
role,
style: TextStyle(color: color, fontWeight: FontWeight.bold),
),
);
}
日付フォーマット
日付を読みやすい形式にフォーマットします。
dart // 日付をフォーマット
String _formatDate(DateTime date) {
return '${date.year}/${date.month.toString().padLeft(2, '0')}/'
'${date.day.toString().padLeft(2, '0')}';
}
}
これでテーブルの基本的な表示が完成しました。
フィルタ UI の実装
次に、検索とフィルタリングの UI を実装します。
FilterBarWidget の作成
テーブルの上部に配置するフィルタバーを作成します。
dart// widgets/filter_bar_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class FilterBarWidget extends ConsumerWidget {
const FilterBarWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
検索ボックスの実装
ユーザー名やメールアドレスで検索できる検索ボックスを実装します。
dart child: Row(
children: [
// 検索ボックス
Expanded(
child: TextField(
decoration: const InputDecoration(
labelText: '検索',
hintText: '名前またはメールアドレスで検索',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: (value) {
// 検索キーワードが変更されたらStateを更新
ref.read(filterStateProvider.notifier)
.updateSearchQuery(value);
},
),
),
const SizedBox(width: 16),
ロールフィルタのドロップダウン
ロールで絞り込むためのドロップダウンメニューを実装します。
dart // ロールフィルタ
SizedBox(
width: 200,
child: DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'ロール',
border: OutlineInputBorder(),
),
value: ref.watch(filterStateProvider).roleFilter,
items: const [
DropdownMenuItem(
value: null,
child: Text('すべて'),
),
DropdownMenuItem(
value: 'admin',
child: Text('管理者'),
),
DropdownMenuItem(
value: 'editor',
child: Text('編集者'),
),
DropdownMenuItem(
value: 'viewer',
child: Text('閲覧者'),
),
],
onChanged: (value) {
// ロールフィルタが変更されたらStateを更新
ref.read(filterStateProvider.notifier)
.updateRoleFilter(value);
},
),
),
],
),
);
}
}
これで、検索キーワードやロールフィルタを変更すると、自動的にテーブルのデータが更新されます。
ページネーション UI の実装
大量データを扱う際に必須のページネーション UI を実装します。
PaginationWidget の作成
ページ番号表示と前後ページへの移動ボタンを持つウィジェットを作成します。
dart// widgets/pagination_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PaginationWidget extends ConsumerWidget {
final PaginatedUsers paginatedUsers;
const PaginationWidget({
Key? key,
required this.paginatedUsers,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentPage = paginatedUsers.page;
final totalPages = paginatedUsers.totalPages;
ページネーション情報の表示
現在のページと総ページ数、データ件数を表示します。
dart return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// データ件数の表示
Text(
'全 ${paginatedUsers.total} 件中 '
'${(currentPage - 1) * paginatedUsers.limit + 1} - '
'${(currentPage * paginatedUsers.limit).clamp(0, paginatedUsers.total)} 件を表示',
style: const TextStyle(fontSize: 14),
),
ページ移動ボタンの実装
前のページ・次のページへ移動するボタンを実装します。
dart // ページ移動ボタン
Row(
children: [
// 前のページボタン
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: currentPage > 1
? () {
ref.read(filterStateProvider.notifier)
.changePage(currentPage - 1);
}
: null,
),
// ページ番号表示
Text(
'ページ $currentPage / $totalPages',
style: const TextStyle(fontSize: 14),
),
// 次のページボタン
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: currentPage < totalPages
? () {
ref.read(filterStateProvider.notifier)
.changePage(currentPage + 1);
}
: null,
),
],
),
],
),
);
}
}
これでページネーション機能が完成しました。ボタンをクリックすると、自動的に新しいページのデータが取得されます。
エクスポート機能の実装
最後に、テーブルデータを CSV や Excel 形式でエクスポートする機能を実装します。
ExportService クラスの作成
エクスポート処理を担当するサービスクラスを作成します。
dart// services/export_service.dart
import 'dart:io';
import 'package:csv/csv.dart';
import 'package:excel/excel.dart';
import 'package:path_provider/path_provider.dart';
import 'package:file_picker/file_picker.dart';
class ExportService {
// CSVファイルとしてエクスポート
Future<void> exportToCSV(List<User> users) async {
// CSVデータの生成
final csvData = _generateCSVData(users);
// CSVフォーマットに変換
final csv = const ListToCsvConverter().convert(csvData);
CSV データの生成
ユーザーデータを CSV 形式のリストに変換します。
dart // ファイル保存先の選択
final outputPath = await FilePicker.platform.saveFile(
dialogTitle: 'CSVファイルを保存',
fileName: 'users_${DateTime.now().millisecondsSinceEpoch}.csv',
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (outputPath != null) {
// ファイルに書き込み
final file = File(outputPath);
await file.writeAsString(csv);
}
}
// CSVデータの生成
List<List<dynamic>> _generateCSVData(List<User> users) {
final data = <List<dynamic>>[];
// ヘッダー行
data.add(['ID', '名前', 'メールアドレス', 'ロール', '登録日', 'ステータス']);
// データ行
for (final user in users) {
data.add([
user.id,
user.name,
user.email,
user.role,
_formatDate(user.createdAt),
user.isActive ? '有効' : '無効',
]);
}
return data;
}
Excel エクスポート機能
Excel 形式でのエクスポート機能も実装します。
dart // Excelファイルとしてエクスポート
Future<void> exportToExcel(List<User> users) async {
// Excelファイルの作成
final excel = Excel.createExcel();
final sheet = excel['Users'];
// ヘッダー行の追加(太字でスタイリング)
final headers = ['ID', '名前', 'メールアドレス', 'ロール', '登録日', 'ステータス'];
sheet.appendRow(headers);
Excel へのデータ追加
ユーザーデータを Excel シートに追加します。
dart // データ行の追加
for (final user in users) {
sheet.appendRow([
user.id,
user.name,
user.email,
user.role,
_formatDate(user.createdAt),
user.isActive ? '有効' : '無効',
]);
}
// 列幅の自動調整
for (var i = 0; i < headers.length; i++) {
sheet.setColumnWidth(i, 20);
}
Excel ファイルの保存
生成した Excel ファイルを保存します。
dart // ファイル保存先の選択
final outputPath = await FilePicker.platform.saveFile(
dialogTitle: 'Excelファイルを保存',
fileName: 'users_${DateTime.now().millisecondsSinceEpoch}.xlsx',
type: FileType.custom,
allowedExtensions: ['xlsx'],
);
if (outputPath != null) {
// ファイルに書き込み
final bytes = excel.encode();
if (bytes != null) {
final file = File(outputPath);
await file.writeAsBytes(bytes);
}
}
}
// 日付フォーマット
String _formatDate(DateTime date) {
return '${date.year}/${date.month.toString().padLeft(2, '0')}/'
'${date.day.toString().padLeft(2, '0')}';
}
}
エクスポートボタンの実装
UI にエクスポートボタンを追加します。
dart// widgets/export_button_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ExportButtonWidget extends ConsumerWidget {
const ExportButtonWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return PopupMenuButton<String>(
icon: const Icon(Icons.download),
tooltip: 'エクスポート',
onSelected: (value) async {
// 現在表示中のユーザーデータを取得
final usersAsync = ref.read(usersProvider);
エクスポート処理の実行
選択されたフォーマットに応じて、エクスポート処理を実行します。
dart usersAsync.whenData((paginatedUsers) async {
final exportService = ExportService();
// ローディング表示
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
try {
if (value == 'csv') {
await exportService.exportToCSV(paginatedUsers.users);
} else if (value == 'excel') {
await exportService.exportToExcel(paginatedUsers.users);
}
// ローディング非表示
Navigator.of(context).pop();
// 成功メッセージ表示
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('エクスポートが完了しました')),
);
} catch (e) {
// エラー処理
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エクスポートに失敗しました: $e')),
);
}
});
},
メニューアイテムの定義
CSV と Excel の選択肢を持つメニューを定義します。
dart itemBuilder: (context) => [
const PopupMenuItem(
value: 'csv',
child: Row(
children: [
Icon(Icons.table_chart),
SizedBox(width: 8),
Text('CSV形式でエクスポート'),
],
),
),
const PopupMenuItem(
value: 'excel',
child: Row(
children: [
Icon(Icons.file_present),
SizedBox(width: 8),
Text('Excel形式でエクスポート'),
],
),
),
],
);
}
}
メイン画面の統合
最後に、これまで実装したウィジェットを統合してメイン画面を構築します。
UserManagementPage の作成
すべての要素を配置したメイン画面を作成します。
dart// pages/user_management_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class UserManagementPage extends ConsumerWidget {
const UserManagementPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
return Scaffold(
appBar: AppBar(
title: const Text('ユーザー管理'),
actions: [
// エクスポートボタン
const ExportButtonWidget(),
],
),
レイアウトの構築
フィルタバー、テーブル、ページネーションを縦に配置します。
dart body: Column(
children: [
// フィルタバー
const FilterBarWidget(),
// テーブル(画面の残りスペースを使用)
Expanded(
child: usersAsync.when(
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text('エラーが発生しました: $error'),
],
),
),
data: (paginatedUsers) => const UserTableWidget(),
),
),
// ページネーション
usersAsync.maybeWhen(
data: (paginatedUsers) => PaginationWidget(
paginatedUsers: paginatedUsers,
),
orElse: () => const SizedBox.shrink(),
),
],
),
);
}
}
これで、業務用管理画面の基本的な機能がすべて実装できました。
パフォーマンスチューニング
実装が完了したら、パフォーマンスチューニングを行いましょう。
デバウンス処理の追加
検索ボックスへの入力時、毎回 API を呼び出すとサーバーに負荷がかかります。デバウンス処理を追加して、入力が落ち着いてから検索を実行するようにします。
dart// widgets/filter_bar_widget.dart(改良版)
import 'dart:async';
class FilterBarWidget extends ConsumerStatefulWidget {
const FilterBarWidget({Key? key}) : super(key: key);
@override
ConsumerState<FilterBarWidget> createState() => _FilterBarWidgetState();
}
class _FilterBarWidgetState extends ConsumerState<FilterBarWidget> {
Timer? _debounce;
@override
void dispose() {
_debounce?.cancel();
super.dispose();
}
デバウンス付き検索処理
入力から 500 ミリ秒後に検索を実行する処理を実装します。
dart void _onSearchChanged(String value) {
// 既存のタイマーをキャンセル
if (_debounce?.isActive ?? false) _debounce!.cancel();
// 新しいタイマーを設定(500ms後に実行)
_debounce = Timer(const Duration(milliseconds: 500), () {
ref.read(filterStateProvider.notifier).updateSearchQuery(value);
});
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: TextField(
decoration: const InputDecoration(
labelText: '検索',
hintText: '名前またはメールアドレスで検索',
prefixIcon: Icon(Icons.search),
),
onChanged: _onSearchChanged,
),
);
}
}
これにより、ユーザーが入力を終えるまで API 呼び出しを待つことができ、サーバーへの負荷を大幅に削減できます。
まとめ
本記事では、Flutter を使った業務用管理画面の実装方法を、テーブル表示・フィルタ・エクスポート機能を中心に解説しました。
実装のポイント
まず、適切なパッケージの選定が重要です。data_table_2 を使用することで、大量データでもスムーズに動作するテーブルを実装できました。
次に、レイヤーを分離した設計により、保守性と拡張性が向上します。Presentation Layer、State Management Layer、Repository Layer の 3 層構造を採用することで、各層を独立してテストできるようになります。
また、Riverpod を活用した状態管理により、フィルタ条件の変更が自動的にデータ取得をトリガーする仕組みを実現できました。
さらに、ページネーションとデバウンス処理により、パフォーマンスを最適化することが可能です。
最後に、CSV と Excel の両方のエクスポート機能を実装することで、ユーザーの多様なニーズに対応できます。
今後の拡張可能性
本記事で実装した基本機能を土台に、以下のような機能を追加できるでしょう。
ソート機能を追加すれば、各列のヘッダーをクリックして昇順・降順を切り替えられます。data_table_2 は標準でソート機能をサポートしているため、比較的簡単に実装できます。
一括操作機能として、複数のユーザーを選択して一括削除や一括編集を行える機能も有用です。チェックボックスを追加し、選択状態を管理する必要があります。
リアルタイム更新では、WebSocket や Server-Sent Events を使用して、他のユーザーの変更をリアルタイムに反映できます。
詳細表示・編集機能として、行をクリックすると詳細画面に遷移し、編集できる機能も一般的です。
さらに、権限管理により、ユーザーのロールに応じて表示内容や操作可能な機能を制限することも重要でしょう。
Flutter で業務アプリを開発する利点
Flutter で業務用管理画面を開発することには、多くの利点があります。
クロスプラットフォーム対応により、一つのコードベースで複数のプラットフォームに対応できるため、開発コストを大幅に削減できます。
高いパフォーマンスも魅力的です。適切な実装を行えば、ネイティブアプリに匹敵する速度で動作します。
豊富なウィジェットライブラリにより、美しい UI を短期間で構築できます。Material Design や Cupertino スタイルをサポートしているため、プラットフォームに合わせたデザインも可能です。
ホットリロード機能により、開発効率が向上します。コードを変更すると即座に画面に反映されるため、UI の調整が容易です。
そして、成長中のエコシステムとして、pub.dev には日々新しいパッケージが追加されており、コミュニティも活発です。
最後に
業務用管理画面は、企業の業務効率に直結する重要なシステムです。本記事で紹介した実装パターンを参考に、ぜひ皆さんのプロジェクトでも Flutter を活用してみてください。
適切な設計とパフォーマンス最適化により、使いやすく高速な管理画面を構築できるはずです。Flutter の持つポテンシャルを最大限に引き出し、ユーザーに喜ばれるシステムを作りましょう。
関連リンク
articleFlutter で業務用管理画面:テーブル・フィルタ・エクスポート機能の実装指針
articleFlutter で ToDo アプリを 90 分で作る:状態管理・永続化・ダークモード対応
articleFlutter の状態管理を設計する:Riverpod/Bloc/Provider/Redux の実務指針
articleFlutter ウィジェット早見表:レイアウト・入力・ナビゲーションを 1 枚で把握
articleFlutter 開発環境の最短構築:macOS/Windows/Linux 別インストール完全ガイド
articleFlutter とは?2025 年版:仕組み・強み・向いているプロダクトを徹底解説
articleGPT-5 構造化出力チートシート:JSON/表/YAML/コードブロックの安定生成パターン
articleESLint 変更管理と段階リリース:CI のフェイルセーフ&ロールバック手順
articleDify フィードバック学習運用:人手評価・プロンプト AB テスト・継続改善
articleFlutter で業務用管理画面:テーブル・フィルタ・エクスポート機能の実装指針
articleDeno で Permission Denied が出る理由と解決手順:--allow-\* フラグ総点検
articleEmotion の仕組みを図解で解説:ランタイム生成・ハッシュ化・挿入順序の全貌
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来