# モバイル開発者向けFlutter面接質問トップ20 > Flutter面接で頻出する20問を徹底解説します。ウィジェット、状態管理、Dart言語、アーキテクチャ、ベストプラクティスをコード例とともに紹介します。 - Published: 2026-01-24 - Updated: 2026-04-12 - Author: SharpSkill - Tags: flutter, dart, interview, mobile development, technical interview - Reading time: 15 min --- Flutterの面接では、フレームワークの理解度、Dart言語の習熟度、モバイルアーキテクチャパターンへの知見が問われます。本ガイドでは、基礎から応用まで最も頻出する20問を、詳細な解説とコード例とともに取り上げます。 > **面接のヒント** > > 面接官は「どのように」だけでなく「なぜ」を説明できる候補者を高く評価します。各概念について、ユースケースと技術的なトレードオフを理解しておくことが合否を分けます。 ## Flutterとdartの基本的な質問 ### 1. StatelessWidgetとStatefulWidgetの違いは何ですか? **StatelessWidget**は、初期設定にのみ依存する不変のウィジェットです。一度構築されると変更されることはありません。一方、**StatefulWidget**は時間とともに変化しうるミュータブルな状態を保持し、ウィジェットの再構築をトリガーします。 ```dart // stateless_example.dart // StatelessWidget: static display, no change after construction class WelcomeMessage extends StatelessWidget { // Final parameter - never changes final String username; const WelcomeMessage({super.key, required this.username}); @override Widget build(BuildContext context) { // Build called once (unless parent rebuilds) return Text('Welcome, $username'); } } ``` ```dart // stateful_example.dart // StatefulWidget: mutable state, can rebuild itself class LikeButton extends StatefulWidget { const LikeButton({super.key}); @override State createState() => _LikeButtonState(); } class _LikeButtonState extends State { // Mutable local state int _likeCount = 0; void _incrementLike() { // setState triggers rebuild with new state setState(() { _likeCount++; }); } @override Widget build(BuildContext context) { return ElevatedButton( onPressed: _incrementLike, child: Text('Likes: $_likeCount'), ); } } ``` 原則として、デフォルトではStatelessWidgetを使用し、ウィジェットローカルな状態が必要な場合にのみStatefulWidgetを選択します。 ### 2. Flutterのウィジェットツリーはどのように機能しますか? Flutterはインターフェースを3つの相互接続されたツリーで構成します。**Widget Tree**(不変の宣言)、**Element Tree**(ライフサイクルとバインディング)、**Render Tree**(レイアウトと描画)です。 ```dart // widget_tree_example.dart // Declarative structure - widgets describe the UI class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { // Each widget creates an Element in the Element Tree return MaterialApp( home: Scaffold( // Scaffold creates multiple RenderObjects body: Center( // Center modifies its child's layout child: Column( children: [ // Each Text has its own RenderParagraph Text('First'), Text('Second'), ], ), ), ), ); } } ``` `setState`が呼ばれると、Flutterは新旧のウィジェットツリーを比較し、変更されたエレメントのみを再構築します。この差分検出はKeyとウィジェットの型に依存しています。 ### 3. constコンストラクタとは何ですか?なぜ使うのですか? **constコンストラクタ**は、ランタイムではなくコンパイル時にウィジェットを生成します。Flutterはこれらのインスタンスを再利用できるため、不要な再構築を回避しパフォーマンスが向上します。 ```dart // const_example.dart class OptimizedScreen extends StatelessWidget { const OptimizedScreen({super.key}); @override Widget build(BuildContext context) { return Column( children: [ // ✅ const: same instance reused on each build const Icon(Icons.star, size: 48), const SizedBox(height: 16), const Text('Static title'), // ❌ Non-const: new instance on each build Icon(Icons.star, color: Theme.of(context).primaryColor), ], ); } } // Custom widget with const constructor class StaticCard extends StatelessWidget { final String title; // All fields must be final for const const StaticCard({super.key, required this.title}); @override Widget build(BuildContext context) { return Card(child: Text(title)); } } ``` Flutterアナライザーは`prefer_const_constructors`リントで、const化の見落としを検出します。 ### 4. FlutterにおけるKeyの種類を説明してください **Key**は、ウィジェットの並び替え時に状態を保持する役割を果たします。Keyがない場合、Flutterはツリー内の位置に基づいて判断するため、リストの並び替え時にバグが発生する可能性があります。 ```dart // keys_example.dart class TodoList extends StatelessWidget { final List todos; const TodoList({super.key, required this.todos}); @override Widget build(BuildContext context) { return ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; // ✅ ValueKey: preserves state if order changes return TodoTile( key: ValueKey(todo.id), todo: todo, ); }, ); } } // Different key types for different contexts class KeyExamples extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ // ValueKey: based on a unique value Container(key: ValueKey('unique-id')), // ObjectKey: based on object identity Container(key: ObjectKey(myObject)), // UniqueKey: new key on each build Container(key: UniqueKey()), // GlobalKey: access state from outside Form(key: _formKey), ], ); } } ``` GlobalKeyを使えば任意の場所からウィジェットの状態にアクセスできますが、使用は最小限に抑えるべきです。 ## Dartの質問 ### 5. Dartにおけるfinalとconstの違いは何ですか? **final**は一度だけ代入可能な変数を定義し、ランタイム時に評価されます。**const**はコンパイル時定数を生成し、実行前に評価される不変の値です。 ```dart // final_const_example.dart class DateExample { // final: value assigned at runtime final DateTime createdAt = DateTime.now(); // const: value known at compile time static const int maxItems = 100; static const String appName = 'MyApp'; // ❌ Error: DateTime.now() not const (runtime value) // static const DateTime timestamp = DateTime.now(); } void demonstrateDifference() { // final: each call can have different value final timestamp1 = DateTime.now(); final timestamp2 = DateTime.now(); print(timestamp1 == timestamp2); // false (different) // const: same instance reused const list1 = [1, 2, 3]; const list2 = [1, 2, 3]; print(identical(list1, list2)); // true (same instance) } ``` Flutterでは、静的なウィジェットにはconstを、計算された値にはfinalを使用するのが適切です。 ### 6. FutureとAsync/Awaitはどのように動作しますか? **Future**は将来利用可能になる値を表現します。**async/await**はこれらの非同期操作を、ネストされたコールバックなしで可読性の高い構文で扱う仕組みです。 ```dart // async_example.dart class UserRepository { final ApiClient _client; UserRepository(this._client); // Future: promise of a future value Future fetchUser(String id) async { try { // await suspends execution until resolution final response = await _client.get('/users/$id'); return User.fromJson(response); } catch (e) { // Errors propagate normally with async/await throw UserNotFoundException(id); } } // Parallel execution with Future.wait Future> fetchUsers(List ids) async { // All requests start simultaneously final futures = ids.map((id) => fetchUser(id)); // Wait for all to complete return Future.wait(futures); } // Sequential processing Future processSequentially(List ids) async { for (final id in ids) { // Each request waits for the previous one await fetchUser(id); } } } ``` 並列処理には`Future.wait`を、逐次処理にはasyncループを使用します。 > **FutureBuilder vs Riverpod** > > FutureBuilderは単純なケースに適していますが、Riverpod(AsyncValue)は複雑なアプリケーションにおいてキャッシュ管理、エラーハンドリング、リフレッシュ機能の面で優れた選択肢です。 ### 7. Streamとその使い方を説明してください **Stream**は非同期の値のシーケンスを表し、WebSocket、センサーデータ、ユーザーインタラクションなどのリアルタイムイベントに最適です。 ```dart // stream_example.dart class MessageService { // StreamController manages creation and broadcasting final _messageController = StreamController.broadcast(); // Expose only the Stream (not the Sink) Stream get messages => _messageController.stream; void addMessage(Message message) { _messageController.sink.add(message); } void dispose() { _messageController.close(); } } // Usage with StreamBuilder class MessageList extends StatelessWidget { final MessageService service; const MessageList({super.key, required this.service}); @override Widget build(BuildContext context) { return StreamBuilder( stream: service.messages, builder: (context, snapshot) { // Handle all possible states if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } if (!snapshot.hasData) { return const Text('No messages'); } return MessageCard(message: snapshot.data!); }, ); } } ``` `.broadcast()`のStreamは複数のリスナーを許可しますが、単一サブスクリプションStreamは1つのリスナーのみに対応します。 ## アーキテクチャと状態管理の質問 ### 8. 状態管理ソリューションの比較:Provider、Riverpod、Bloc 各ソリューションは異なるニーズに対応します。**Provider**はシンプルさとネイティブ統合を提供します。**Riverpod**は型安全性とテスト容易性をもたらします。**Bloc**はイベント駆動型アーキテクチャを厳格に強制します。 ```dart // riverpod_example.dart // Riverpod: declarative and type-safe approach final userProvider = FutureProvider.autoDispose((ref) async { final repository = ref.watch(userRepositoryProvider); return repository.fetchCurrentUser(); }); class UserProfile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // AsyncValue handles loading/error/data final userAsync = ref.watch(userProvider); return userAsync.when( loading: () => const CircularProgressIndicator(), error: (error, stack) => Text('Error: $error'), data: (user) => Text(user.displayName), ); } } ``` ```dart // bloc_example.dart // Bloc: explicit events/states separation abstract class AuthEvent {} class LoginRequested extends AuthEvent { final String email; final String password; LoginRequested(this.email, this.password); } abstract class AuthState {} class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} class AuthSuccess extends AuthState { final User user; AuthSuccess(this.user); } class AuthFailure extends AuthState { final String error; AuthFailure(this.error); } class AuthBloc extends Bloc { AuthBloc() : super(AuthInitial()) { on(_onLoginRequested); } Future _onLoginRequested( LoginRequested event, Emitter emit, ) async { emit(AuthLoading()); try { final user = await _repository.login(event.email, event.password); emit(AuthSuccess(user)); } catch (e) { emit(AuthFailure(e.toString())); } } } ``` RiverpodはモダンなAPIとテストの容易さから、新規プロジェクトに推奨されます。 ### 9. FlutterにおけるClean Architectureとは何ですか? **Clean Architecture**はコードを独立したレイヤーに分離します。Domain(ビジネスロジック)、Data(データソース)、Presentation(UI)の3層です。この分離によりテストとメンテナンスが容易になります。 ```dart // domain/entities/user.dart // Entity: pure business object, no framework dependency class User { final String id; final String email; final String name; const User({ required this.id, required this.email, required this.name, }); } // domain/repositories/user_repository.dart // Interface: abstract contract, implementation in Data abstract class UserRepository { Future getUser(String id); Future updateUser(User user); } // domain/usecases/get_user_usecase.dart // Use case: isolated business logic class GetUserUseCase { final UserRepository _repository; GetUserUseCase(this._repository); Future call(String userId) { return _repository.getUser(userId); } } ``` ```dart // data/repositories/user_repository_impl.dart // Concrete implementation with data sources class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource _remoteDataSource; final UserLocalDataSource _localDataSource; UserRepositoryImpl(this._remoteDataSource, this._localDataSource); @override Future getUser(String id) async { try { // Try local cache first final cachedUser = await _localDataSource.getUser(id); if (cachedUser != null) return cachedUser; } catch (_) {} // Fallback to API final user = await _remoteDataSource.fetchUser(id); await _localDataSource.cacheUser(user); return user; } } ``` Domainレイヤーは何にも依存せず、DataはDomainに依存し、Presentationは両方に依存します。 ### 10. 依存性注入はどのように実装しますか? **依存性注入**は、依存関係を外部から提供することでコンポーネント間の結合を解消します。RiverpodのProviderはこの目的に優れています。 ```dart // di_example.dart // Provider definitions (dependencies) final apiClientProvider = Provider((ref) { return ApiClient(baseUrl: Environment.apiUrl); }); final userRepositoryProvider = Provider((ref) { // Automatically injects ApiClient final client = ref.watch(apiClientProvider); return UserRepositoryImpl(client); }); final getUserUseCaseProvider = Provider((ref) { final repository = ref.watch(userRepositoryProvider); return GetUserUseCase(repository); }); // Usage in a widget class UserScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final getUserUseCase = ref.watch(getUserUseCaseProvider); // Use the use case... } } // Tests: easy dependency override void main() { testWidgets('displays user', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ // Replace with mock for tests userRepositoryProvider.overrideWithValue(MockUserRepository()), ], child: const MyApp(), ), ); }); } ``` 最大の利点は、テスト時に任意の依存関係をモックに置き換えられることです。 ## パフォーマンスの質問 ### 11. 再構築のパフォーマンスを最適化するには? 再構築を最小限に抑えることがパフォーマンス向上の鍵です。主な手法は、constウィジェット、粒度の細かい分割、Riverpodセレクターの活用です。 ```dart // rebuild_optimization.dart // ❌ Bad: everything rebuilds on each change class BadExample extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userProvider); return Column( children: [ Text(user.name), Text(user.email), const ExpensiveWidget(), // Rebuilds unnecessarily ], ); } } // ✅ Good: selector to rebuild only if name changes class GoodExample extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // select: rebuilds only if name changes final name = ref.watch(userProvider.select((u) => u.name)); return Text(name); } } // ✅ Good: splitting into smaller widgets class OptimizedScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ const Header(), // Static, never rebuilds const UserNameWidget(), // Rebuilds if name changes const UserEmailWidget(), // Rebuilds if email changes const Footer(), // Static ], ); } } ``` DevToolsのPerformanceタブで、過剰な再構築を特定できます。 ### 12. 長いリストを最適化するには? 長いリストにはListView.builderによる**遅延読み込み**が必要です。20以上のアイテムがある場合、直接childrenを持つListViewの使用は避けるべきです。 ```dart // list_optimization.dart class OptimizedList extends StatelessWidget { final List items; const OptimizedList({super.key, required this.items}); @override Widget build(BuildContext context) { // ✅ ListView.builder: builds on demand return ListView.builder( // Fixed height improves scrolling itemExtent: 72, itemCount: items.length, itemBuilder: (context, index) { return ItemTile( // Key to preserve state during scroll key: ValueKey(items[index].id), item: items[index], ); }, ); } } // List widget with infinite loading class InfiniteList extends ConsumerStatefulWidget { @override ConsumerState createState() => _InfiniteListState(); } class _InfiniteListState extends ConsumerState { final _scrollController = ScrollController(); @override void initState() { super.initState(); _scrollController.addListener(_onScroll); } void _onScroll() { // Load more data near the end if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { ref.read(itemsProvider.notifier).loadMore(); } } @override Widget build(BuildContext context) { final items = ref.watch(itemsProvider); return ListView.builder( controller: _scrollController, itemCount: items.length, itemBuilder: (context, index) => ItemTile(item: items[index]), ); } } ``` 1000件以上の超長大リストには、`ListView.separated`や`scrollable_positioned_list`などの専用パッケージの検討が推奨されます。 > **画像キャッシュ** > > 画像を含むリストでは、再読み込みを防ぐために`cached_network_image`を使用してください。キャッシュされていない画像はスクロール時のカクつきの原因になります。 ### 13. Impellerレンダリングエンジンの仕組みを説明してください **Impeller**はFlutter 3.16以降でSkiaに代わるデフォルトのレンダリングエンジンです。シェーダーを事前コンパイルすることで、初回表示時の「ジャンク」を排除します。 ```dart // impeller_benefits.dart // Complex animations benefit from Impeller class SmoothAnimation extends StatefulWidget { @override State createState() => _SmoothAnimationState(); } class _SmoothAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { // Impeller: no runtime shader compilation return Transform.rotate( angle: _controller.value * 2 * 3.14159, child: child, ); }, child: const FlutterLogo(size: 100), ); } } ``` ImpellerはiOSではデフォルトで有効です。Androidでは`flutter run --enable-impeller`で互換性を確認できます。 ## ナビゲーションとフォームの質問 ### 14. ディープリンクを使ったナビゲーションはどのように実装しますか? **ディープリンク**はURLを通じてアプリの特定の画面を直接開く仕組みです。GoRouterはこの機能をネイティブにサポートしています。 ```dart // deep_linking.dart final router = GoRouter( routes: [ GoRoute( path: '/products/:productId', builder: (context, state) { // Extract URL parameter final productId = state.pathParameters['productId']!; return ProductScreen(productId: productId); }, ), GoRoute( path: '/search', builder: (context, state) { // Query parameters (?query=flutter) final query = state.uri.queryParameters['query'] ?? ''; return SearchScreen(initialQuery: query); }, ), ], ); // Programmatic navigation void navigateToProduct(BuildContext context, String id) { // go: replaces navigation stack context.go('/products/$id'); // push: adds to stack (allows back) context.push('/products/$id'); // pushNamed with extra for complex data context.pushNamed( 'productDetail', pathParameters: {'productId': id}, extra: ProductData(id: id), ); } ``` システムレベルのディープリンクを有効にするには、ネイティブファイル(AndroidManifest.xml、Info.plist)の設定が必要です。 ### 15. フォームバリデーションを効果的に実装するには? バリデーションはForm、TextFormField、カスタムバリデーターを組み合わせて実現します。`formz`のようなZodライクなパッケージで構造化することも可能です。 ```dart // form_validation.dart class RegistrationForm extends StatefulWidget { @override State createState() => _RegistrationFormState(); } class _RegistrationFormState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmController = TextEditingController(); @override Widget build(BuildContext context) { return Form( key: _formKey, // Real-time validation autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, validator: _validateEmail, ), TextFormField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Password'), obscureText: true, validator: _validatePassword, ), TextFormField( controller: _confirmController, decoration: const InputDecoration(labelText: 'Confirm'), obscureText: true, validator: _validateConfirmPassword, ), ElevatedButton( onPressed: _submit, child: const Text('Create account'), ), ], ), ); } String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email required'; } final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(value)) { return 'Invalid email format'; } return null; } String? _validatePassword(String? value) { if (value == null || value.length < 8) { return 'Minimum 8 characters'; } if (!value.contains(RegExp(r'[A-Z]'))) { return 'At least one uppercase letter required'; } if (!value.contains(RegExp(r'[0-9]'))) { return 'At least one number required'; } return null; } String? _validateConfirmPassword(String? value) { if (value != _passwordController.text) { return 'Passwords do not match'; } return null; } void _submit() { if (_formKey.currentState!.validate()) { // Form is valid, proceed } } } ``` `AutovalidateMode.onUserInteraction`モードは、ユーザーが操作した後にエラーを表示するため、最も優れたUXを提供します。 ## テストの質問 ### 16. Flutterウィジェットのテスト方法は? ウィジェットテストはデバイスに依存せずにUIを検証します。`flutter_test`パッケージが必要なユーティリティを提供します。 ```dart // widget_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() { testWidgets('displays welcome message', (tester) async { // Arrange: build the widget await tester.pumpWidget( const ProviderScope( child: MaterialApp( home: WelcomeScreen(username: 'Alice'), ), ), ); // Assert: verify content expect(find.text('Welcome, Alice'), findsOneWidget); }); testWidgets('button increments counter', (tester) async { await tester.pumpWidget( const MaterialApp(home: CounterScreen()), ); // Initial state expect(find.text('0'), findsOneWidget); // Act: tap the button await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Rebuild after setState // Assert: counter incremented expect(find.text('1'), findsOneWidget); }); testWidgets('form validates email', (tester) async { await tester.pumpWidget( const MaterialApp(home: LoginForm()), ); // Enter invalid email await tester.enterText( find.byKey(const Key('email-field')), 'invalid-email', ); await tester.tap(find.byType(ElevatedButton)); await tester.pump(); // Verify error message expect(find.text('Invalid email format'), findsOneWidget); }); } ``` `pump()`は1フレーム進め、`pumpAndSettle()`はすべてのアニメーションの完了を待ちます。 ### 17. Riverpodプロバイダーのテスト方法は? Riverpodは`ProviderContainer`とオーバーライドによりテストを容易にします。 ```dart // provider_test.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; // Repository mock class MockUserRepository extends Mock implements UserRepository {} void main() { late MockUserRepository mockRepository; late ProviderContainer container; setUp(() { mockRepository = MockUserRepository(); container = ProviderContainer( overrides: [ userRepositoryProvider.overrideWithValue(mockRepository), ], ); }); tearDown(() { container.dispose(); }); test('loads user from repository', () async { // Arrange final expectedUser = User(id: '1', name: 'Test'); when(() => mockRepository.getUser('1')) .thenAnswer((_) async => expectedUser); // Act final user = await container.read(userProvider('1').future); // Assert expect(user, expectedUser); verify(() => mockRepository.getUser('1')).called(1); }); test('handles repository error', () async { when(() => mockRepository.getUser(any())) .thenThrow(Exception('Network error')); expect( () => container.read(userProvider('1').future), throwsException, ); }); } ``` プロバイダーテストはUI描画を必要としないため、高速に実行されます。 ## デプロイの質問 ### 18. 開発・ステージング・本番環境の管理方法は? 環境設定は`.env`ファイルまたは`--dart-define`によるコンパイル時定数で管理します。 ```dart // environment.dart enum Environment { dev, staging, prod } class AppConfig { final Environment environment; final String apiUrl; final bool enableAnalytics; const AppConfig._({ required this.environment, required this.apiUrl, required this.enableAnalytics, }); // Predefined configurations static const dev = AppConfig._( environment: Environment.dev, apiUrl: 'https://api-dev.example.com', enableAnalytics: false, ); static const staging = AppConfig._( environment: Environment.staging, apiUrl: 'https://api-staging.example.com', enableAnalytics: true, ); static const prod = AppConfig._( environment: Environment.prod, apiUrl: 'https://api.example.com', enableAnalytics: true, ); // Read from --dart-define static AppConfig fromEnvironment() { const env = String.fromEnvironment('ENV', defaultValue: 'dev'); switch (env) { case 'prod': return prod; case 'staging': return staging; default: return dev; } } } ``` ```bash # terminal # Launch with specific environment flutter run --dart-define=ENV=staging # Production build flutter build apk --dart-define=ENV=prod --release ``` ### 19. 複数のアプリバリアントを作るフレーバリングとは? **フレーバリング**は、異なる設定(アイコン、名前、API)を持つ複数のバリアント(client1、client2)を作成する仕組みです。 ```dart // main_client1.dart import 'package:flutter/material.dart'; import 'config/flavor_config.dart'; import 'app.dart'; void main() { FlavorConfig( flavor: Flavor.client1, name: 'App Client 1', apiUrl: 'https://api.client1.com', primaryColor: Colors.blue, ); runApp(const App()); } // config/flavor_config.dart enum Flavor { client1, client2, internal } class FlavorConfig { final Flavor flavor; final String name; final String apiUrl; final Color primaryColor; // Singleton for global access static FlavorConfig? _instance; static FlavorConfig get instance => _instance!; FlavorConfig({ required this.flavor, required this.name, required this.apiUrl, required this.primaryColor, }) { _instance = this; } bool get isProduction => flavor != Flavor.internal; } ``` 各フレーバーに対応するネイティブファイル(Androidのbuild.gradle、iOSのxcconfig)の設定も必要です。 ### 20. Flutterのセキュリティベストプラクティスは? セキュリティは、機密データの保存、入力バリデーション、リバースエンジニアリング対策の3つの領域をカバーします。 ```dart // security_example.dart import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class SecureStorage { // Encrypted storage for sensitive data final _storage = const FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), ); Future saveToken(String token) async { await _storage.write(key: 'auth_token', value: token); } Future getToken() async { return _storage.read(key: 'auth_token'); } Future deleteToken() async { await _storage.delete(key: 'auth_token'); } } // Input validation class InputValidator { // Injection prevention static String sanitize(String input) { return input .replaceAll(RegExp(r'[<>"\']'), '') .trim(); } // Length validation static bool isValidLength(String input, int min, int max) { return input.length >= min && input.length <= max; } } // SSL pinning protection class SecureApiClient { Dio createSecureClient() { final dio = Dio(); // Add certificate pinning (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { final client = HttpClient(); client.badCertificateCallback = (cert, host, port) { // Verify certificate fingerprint return _isValidCertificate(cert); }; return client; }; return dio; } } ``` シークレットをコードに直接埋め込んではいけません。トークンには`flutter_secure_storage`を使用し、本番ビルドには`--obfuscate`オプションを適用します。 ## まとめ これらの20問はFlutter面接の重要な領域を網羅しています。フレームワークの基礎、Dart言語の習熟、アーキテクチャパターン、プロダクション向けベストプラクティスの各分野です。合格の鍵は、構文の暗記ではなく、根底にあるメカニズムの深い理解にあります。 ### 準備チェックリスト - StatelessWidgetとStatefulWidgetの違いとユースケースを習得する - ウィジェットツリーと再構築の最適化を理解する - async/await、Future、Streamを演習で実践する - Riverpodを使った状態管理プロジェクトを実装する - GoRouterとディープリンクを把握する - ユニットテストとウィジェットテストを書く - マルチ環境構成を理解する 個人プロジェクトでの継続的な実践が、これらの知識を定着させる最善の方法です。ここで取り上げた各質問は、Flutterフレームワークの細部を習得するために、実際のコードでより深く掘り下げる価値があります。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/flutter/flutter-interview-questions