はじめに
普段Next.jsでWeb開発をしている私が、初めてFlutterに挑戦した際の体験談です。特にレスポンシブデザインでタブレットではポップオーバー、スマホではボトムシートを表示しようとした際に遭遇したエラーと、その解決方法について共有します。
Next.jsとFlutterの主な違い
1. アーキテクチャの違い
Next.js(React)
// コンポーネントベース、JSX記法
function MyComponent({ title, children }) {
return (
<div className="container">
<h1>{title}</h1>
{children}
</div>
);
}
Flutter
// Widgetベース、Dart言語
class MyWidget extends StatelessWidget {
final String title;
final Widget child;
const MyWidget({Key? key, required this.title, required this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text(title),
child,
],
),
);
}
}
2. 状態管理の違い
Next.js(React Hook Form)
import { useForm } from 'react-hook-form';
function MyForm() {
const { register, handleSubmit, watch } = useForm();
const watchedValue = watch('inputField');
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('inputField')} />
<p>現在の値: {watchedValue}</p>
</form>
);
}
Flutter(MVVM + Provider + StateNotifier)
FlutterではMVVM(Model-View-ViewModel)パターンを採用しました。これは過去にWPF開発で使用していた設計パターンと同じで、馴染みやすかったです。
// Model - データ構造を定義
class FormModel {
final String inputValue;
final bool isLoading;
final String? errorMessage;
const FormModel({
required this.inputValue,
this.isLoading = false,
this.errorMessage,
});
FormModel copyWith({
String? inputValue,
bool? isLoading,
String? errorMessage,
}) {
return FormModel(
inputValue: inputValue ?? this.inputValue,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
// ViewModel - ビジネスロジックと状態管理
class FormViewModel extends StateNotifier<FormModel> {
FormViewModel() : super(const FormModel(inputValue: ''));
// WPFのINotifyPropertyChangedと同様の役割
void updateInput(String value) {
state = state.copyWith(inputValue: value);
}
Future<void> submitForm() async {
state = state.copyWith(isLoading: true, errorMessage: null);
try {
// ビジネスロジック実行
await _performSubmission(state.inputValue);
state = state.copyWith(isLoading: false);
} catch (e) {
state = state.copyWith(
isLoading: false,
errorMessage: e.toString()
);
}
}
Future<void> _performSubmission(String value) async {
// 実際の処理...
await Future.delayed(Duration(seconds: 1));
}
}
// Provider定義 - WPFのDataContextのような役割
final formViewModelProvider = StateNotifierProvider<FormViewModel, FormModel>(
(ref) => FormViewModel(),
);
// View - UI描画部分
class MyForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final formState = ref.watch(formViewModelProvider);
final formViewModel = ref.read(formViewModelProvider.notifier);
return Column(
children: [
TextField(
onChanged: formViewModel.updateInput,
),
Text('現在の値: ${formState.inputValue}'),
if (formState.isLoading)
CircularProgressIndicator(),
if (formState.errorMessage != null)
Text('エラー: ${formState.errorMessage}',
style: TextStyle(color: Colors.red)),
ElevatedButton(
onPressed: formState.isLoading ? null : formViewModel.submitForm,
child: Text('送信'),
),
],
);
}
}
WPFとFlutterのMVVM比較
要素 | WPF | Flutter |
---|---|---|
Model | データクラス | Dartのimmutableクラス |
View | XAMLファイル | Widget(build メソッド) |
ViewModel | INotifyPropertyChanged | StateNotifier |
DataBinding | {Binding Path=Property} | ref.watch(provider) |
Command | ICommand実装 | ViewModelのメソッド |
DataContext | ViewModel設定 | Provider |
WPF開発経験があったおかげで、Flutter の MVVM パターンはすぐに理解できました。特に状態の変更通知の仕組み(WPFの INotifyPropertyChanged
と Flutter の StateNotifier
)が非常に似ており、スムーズに移行できました。
Flutter独自の重要な概念
Widget Tree
FlutterはすべてがWidgetで構成されており、React のJSXとは異なり、ネストが深くなりがちです。
// 深いネストの例
Scaffold(
appBar: AppBar(title: Text('My App')),
body: Container(
padding: EdgeInsets.all(16),
child: Column(
children: [
Card(
child: Padding(
padding: EdgeInsets.all(8),
child: Text('Hello World'),
),
),
],
),
),
)
MediaQuery によるレスポンシブデザイン
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = screenWidth > 600;
return isTablet ? TabletLayout() : MobileLayout();
}
遭遇したエラーと解決方法
問題:BoxConstraints エラー
レスポンシブデザインを実装中に以下のエラーが発生しました:
BoxConstraints forces an infinite width.
RenderBox was not laid out: RenderConstrainedBox
RenderBox was not laid out: _RenderLayoutBuilder
原因となったコード(問題のあるコード)
// ❌ 問題のあるコード
Widget build(BuildContext context) {
return Column(
children: [
Container(
width: double.infinity, // 無限の幅を要求
child: Row(
children: [
Expanded(
child: Container(
width: double.infinity, // さらに無限の幅を要求
child: Text('レスポンシブテスト'),
),
),
],
),
),
],
);
}
解決したコード
// ✅ 修正されたコード
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = screenWidth > 600;
return Column(
children: [
Container(
width: screenWidth, // 具体的な幅を指定
child: Row(
children: [
Expanded(
child: Container(
// width指定を削除、Expandedが自動調整
child: Text('レスポンシブテスト'),
),
),
],
),
),
// レスポンシブな表示
if (isTablet)
_buildPopover(context)
else
_buildBottomSheet(context),
],
);
}
レスポンシブデザインの実装例
タブレット用ポップオーバー
Widget _buildPopover(BuildContext context) {
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => Dialog(
child: Container(
width: 400,
height: 300,
padding: EdgeInsets.all(16),
child: Column(
children: [
Text('タブレット用ポップオーバー'),
Spacer(),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text('閉じる'),
),
],
),
),
),
);
},
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Text('タップしてポップオーバー', style: TextStyle(color: Colors.white)),
),
);
}
スマホ用ボトムシート
Widget _buildBottomSheet(BuildContext context) {
return GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.7,
padding: EdgeInsets.all(16),
child: Column(
children: [
Text('スマホ用ボトムシート'),
Spacer(),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text('閉じる'),
),
],
),
),
);
},
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Text('タップしてボトムシート', style: TextStyle(color: Colors.white)),
),
);
}
完全なサンプルコード
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'レスポンシブデザイン Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: ResponsiveDemo(),
);
}
}
class ResponsiveDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isTablet = screenWidth > 600;
return Scaffold(
appBar: AppBar(
title: Text('レスポンシブデザイン'),
),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
isTablet ? 'タブレット表示' : 'スマホ表示',
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 32),
if (isTablet)
_buildPopover(context)
else
_buildBottomSheet(context),
],
),
),
);
}
// ... (上記のメソッドをここに含める)
}
学んだポイント
-
BoxConstraints の理解が重要
-
double.infinity
の使用には注意が必要 - 親要素との制約関係を理解する
-
-
MediaQuery の活用
- レスポンシブデザインの基本
- 画面サイズに応じた条件分岐
-
Widget の制約システム
- 「制約は下に、サイズは上に、親が位置を決める」の原則
-
MVVMパターンの活用
- WPF開発経験が活かせる設計パターン
- StateNotifierによる状態管理がINotifyPropertyChangedと類似
- Providerを使った依存性注入でテスタブルなコード構成
まとめ
Next.jsからFlutterへの移行で最も戸惑ったのは、Widget Tree の概念とレイアウト制約システムでした。特に BoxConstraints
エラーは初心者が必ず遭遇する問題だと思います。
しかし、一度理解すれば、Flutterの宣言的UIは非常に直感的で、クロスプラットフォーム開発の強力なツールだと感じました。
Web開発者の皆さんも、ぜひFlutterに挑戦してみてください!