Open18

Flutter関係のメモ書き

8rine238rine23

別Formの値をバリデーションで取得するサンプル

8rine238rine23
import 'package:flutter/material.dart';

class OriginalForm extends StatefulWidget {
  const OriginalForm({Key? key}) : super(key: key);

  
  _OriginalFormState createState() => _OriginalFormState();
}

class _OriginalFormState extends State<OriginalForm> {
  final _formKey = GlobalKey<FormState>();

  final _emailFormKey = GlobalKey<FormFieldState>();
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('test'),
      ),
      body: Form(
        key: _formKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            PartialForm(
              formKey: _emailFormKey,
            ),
            PartialConfirmationForm(
              emailFormKey: _emailFormKey,
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState != null &&
                      _formKey.currentState!.validate()) {
                    _formKey.currentState!.save();
                  }
                },
                child: const Text('Submit'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class PartialForm extends StatelessWidget {
  const PartialForm({
    Key? key,
    required this.formKey,
  }) : super(key: key);

  final Key formKey;

  
  Widget build(BuildContext context) {
    return TextFormField(
      key: formKey,
      decoration: const InputDecoration(
        labelText: "Email address form",
        hintText: 'Enter your email',
      ),
      autovalidateMode: AutovalidateMode.disabled,
      validator: (value) {
        if (value == null || value.isEmpty) {
          return 'Please enter some text';
        }
        return null;
      },
      onSaved: (value) => () {
        print('$value');
      },
    );
  }
}

class PartialConfirmationForm extends StatelessWidget {
  const PartialConfirmationForm({
    Key? key,
    required this.emailFormKey,
  }) : super(key: key);
  final GlobalKey<FormFieldState> emailFormKey;
  
  Widget build(BuildContext context) {
    return TextFormField(
      decoration: const InputDecoration(
        labelText: "Confirmation",
        hintText: 'Enter your email',
      ),
      autovalidateMode: AutovalidateMode.disabled,
      validator: (value) {
        return TestValidator().isValid(value, emailFormKey);
      },
    );
  }
}

class TestValidator {
  String? isValid(String? value, GlobalKey<FormFieldState> emailFormKey) {
    if (value == null || value.isEmpty) {
      return 'Please enter some text';
    }

    final String email = emailFormKey.currentState!.value ?? '';
    if (value != email) {
      return 'not equall';
    }
    return null;
  }
}

8rine238rine23

FormFieldを使って独自なフォームを作成したときにバリデーション文を出す方法

8rine238rine23
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class SampleForm extends StatelessWidget {
  const SampleForm({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Form(
      child: OriginalForm(),
    );
  }
}

class OriginalForm extends FormField<int> {
  OriginalForm({Key? key})
      : super(
          validator: (value) {
            if (value! < 0) return 'Negative values not supported';
            return null;
          },
          initialValue: 0,
          autovalidateMode: AutovalidateMode.always,
          builder: (FormFieldState<int> state) {
            return Column(
              children: <Widget>[
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    IconButton(
                      icon: Icon(Icons.remove),
                      onPressed: () {
                        // Fieldの値を変化させる
                        state.didChange(state.value! - 1);
                      },
                    ),
                    Text(state.value.toString()),
                    IconButton(
                      icon: Icon(Icons.add),
                      onPressed: () {
                        state.didChange(state.value! + 1);
                      },
                    ),
                  ],
                ),
                state.hasError // Validatorの結果がエラー時の表示をここで対応
                    ? Text(
                        state.errorText!,
                        style: const TextStyle(color: Colors.red),
                      )
                    : Container(),
              ],
            );
          },
        );
}

8rine238rine23

state.hasErrorでバリデーションエラーが発生したときだけエラーを出すようにする

state.hasError // Validatorの結果がエラー時の表示をここで対応
                    ? Text(
                        state.errorText!,
                        style: const TextStyle(color: Colors.red),
                      )
                    : Container(),
8rine238rine23

index相互変換

8rine238rine23
var lists = [
  {
    'cd': '01',
    'name': 'sample1',
  },
  {
    'cd': '02',
    'name': 'sample2',
  },
  {
    'cd': '03',
    'name': 'sample3',
  },
];

// cdをnameに変換する
String toName(String cd) {
  int? index = toIndex(cd);
  if (index == null) {
    return '';
  }
  return lists[index]['name'] ?? '';
}

// cdをindexに変換する
int? toIndex(String cd) {
  int index = lists.indexWhere((list) => list['cd'] == cd);
  return index != -1 ? index : null;
}

// indexをcdに変換する
String? toCd(int index) {
  return lists.asMap().containsKey(index) ? lists[index]['cd'] : null;
}

void main() {
  var cd = '03';
  print(toName(cd));
  print(toIndex(cd));

  var key = 2;
  print(toCd(key));
}
8rine238rine23
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class TestPage extends HookWidget {
  const TestPage({required this.testKey}) : super(key: testKey);

  final Key testKey;

  
  Widget build(
    BuildContext context,
  ) {
    final _height = MediaQuery.of(context).viewInsets.bottom;
    final _scrollController = ScrollController();

    useEffect(() {
      WidgetsBinding.instance?.addPostFrameCallback((_) {
        _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
      });
      return () {
        _scrollController.dispose();
      };
    }, []);

    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: const Text('遷移先の画面'),
      ),
      body: SingleChildScrollView(
        controller: _scrollController,
        reverse: true,
        child: Padding(
          padding: EdgeInsets.only(
            bottom: _height,
          ),
          child: Column(
            children: const [
              OriginalForm(num: 1),
              OriginalForm(num: 2),
              OriginalForm(num: 3),
              OriginalForm(num: 4),
              OriginalForm(num: 5),
              OriginalForm(num: 6),
              OriginalForm(num: 7),
              OriginalForm(num: 8),
              OriginalForm(num: 9),
              OriginalForm(num: 10),
              OriginalForm(num: 11),
              OriginalForm(num: 12),
            ],
          ),
        ),
      ),
    );
  }
}

class OriginalForm extends StatelessWidget {
  const OriginalForm({Key? key, required this.num}) : super(key: key);
  final int num;
  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('form$num'),
        TextFormField(),
      ],
    );
  }
}

8rine238rine23
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Dummy(),
    );
  }
}

class Dummy extends StatelessWidget {
  const Dummy({
    Key? key,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    final List<GlobalKey<FormFieldState>> keys = List.generate(
      30,
      (index) => GlobalKey<FormFieldState>(),
    );
    final items = List.generate(
      30,
      (int index) => TextFormField(
        key: keys[index],
        initialValue: index.toString(),
      ),
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('dummyApp'),
      ),
      // ListViewをSingleChildScrollViewでラップしている理由
      // 特定のGlobalKeyを持つWidgetが表示されるようにスクロールする処理は、そのWidgetのcontextが必要
      // contextはGlobakKey.currentContextで取得できる
      // buildされないとWidgetツリーに登録されない関係でListView単体で使うと画面外のWidgetがbuildされないためのWidgetツリーに登録されない
      // Widgetツリーに登録されないということはcontextを取得できない
      // したがって、ListViewのchildren内の要素を一度すべてbuildさせるためにSingleChildScrollViewでラップしている
      // https://github.com/flutter/flutter/issues/69190#issuecomment-718710989
      // 追記: そもそもListView使わずにSingleScrollViewだけ使えばいい希ガス
      body: SingleChildScrollView(
        child: ListView(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          reverse: true,
          children: items,
        ),
      ),
      floatingActionButton: IconButton(
        onPressed: () {
          WidgetsBinding.instance?.addPostFrameCallback((_) {
            final BuildContext? contx = keys.last.currentContext;
            Scrollable.ensureVisible(
              contx!,
              curve: Curves.easeInOut,
              alignment: 0.5,
              alignmentPolicy: ScrollPositionAlignmentPolicy.explicit,
            );
          });
        },
        icon: const Icon(
          Icons.arrow_circle_up_sharp,
          size: 48,
          color: Colors.blue,
        ),
      ),
    );
  }
}

8rine238rine23
8rine238rine23
class _Camera extends HookWidget {
  const _Camera({
    Key? key,
  }) : super(key: key);

  static show({
    required BuildContext context,
  }) {
    Navigator.push(
      context,
      MaterialPageRoute(
        fullscreenDialog: true,
        builder: (_) => _Camera(),
      ),
    );
  }

  Future<CameraController?> init() async {
    final List<CameraDescription> _cameras = await availableCameras();
    if (_cameras.isEmpty) {
      return null;
    }

    final CameraController _controller = CameraController(
      _cameras.first,
      ResolutionPreset.high,
    );
    await _controller.initialize();
    return _controller;
  }

  
  Widget build(BuildContext context) {
    final snapshot = useFuture(useMemoized(() => init(), const []));
    if (!snapshot.hasData) {
      return const Center(
        child: CircularProgressIndicator.adaptive(),
      );
    }

    final CameraController cameraController = snapshot.data!;
    return Scaffold(
      appBar: AppBar(),
      body: SizedBox(
        width: double.maxFinite,
        height: double.maxFinite,
        child: CameraPreview(cameraController),
      ),
      floatingActionButton: IconButton(
        onPressed: () async {
          final XFile pict = await cameraController.takePicture();
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(
                pict.path,
              ),
            ),
          );
          Navigator.pop(context);
        },
        icon: const Icon(
          Icons.camera_alt_rounded,
        ),
      ),
    );
  }
}