Dart Result<S, E extends Exception>
📕Overview
DartにもSwiftのResultのような、データ型があるらしい?
成功または失敗を表す値で、それぞれの場合に関連する値を含む。
Result.success(_:)
A success, storing a Success value.
Result.success(_:)
成功、Success値を格納する。
あると言っても自作してるようだ。
dio
というHTTP通信ができるパッケージを使って、郵便番号検索で使ってみました!
🧷summary
Dart3.0から追加されたsealed class
を使って、Result型のような列挙型のように使いエラーハンドリングをやってみました。
CODE ANDREのコードをお借りしております。
/// Base Result class
/// [S] represents the type of the success value
/// [E] should be [Exception] or a subclass of it
sealed class Result<S, E extends Exception> {
const Result();
}
final class Success<S, E extends Exception> extends Result<S, E> {
const Success(this.value);
final S value;
}
final class Failure<S, E extends Exception> extends Result<S, E> {
const Failure(this.exception);
final E exception;
}
郵便番号APIを検索するメソッドを持っているクラスに、Resutl型をつけてみました。参考にしたコードは、String型しかなかったですが、Result型をつけることによって、sealed class
を使用したエラー処理を行うことができるようになります。
// zip_code_repository.dart
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:resutl_type_example/api/result.dart';
class ZipApi {
final _dio = Dio();
Future<Result<String, Exception>> zipCodeToAddress(String zipCode) async {
if (zipCode.length != 7) {
return Failure(Exception('Invalid zip code'));
}
try {
final response = await _dio.get(
'https://zipcloud.ibsnet.co.jp/api/search?zipcode=$zipCode',
);
if (response.statusCode != 200) {
return Failure(Exception('Failed to fetch address'));
}
final result = jsonDecode(response.data);
if (result['results'] == null) {
return Failure(Exception('Failed to fetch address'));
}
final addressMap = (result['results'] as List).first;
final address =
'${addressMap['address1']} ${addressMap['address2']} ${addressMap['address3']}';
return Success(address);
} catch (e) {
return Failure(Exception('Failed to fetch address'));
}
}
}
UIには、郵便番号を入力してボタンを押すと、郵便番号が存在していれば、Text Widgetに表示、存在しなければ、住所を取得できませんでしたと表示されます。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:resutl_type_example/api/result.dart';
import 'package:resutl_type_example/api/zip_api.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: DemoPage(),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
_DemoPageState createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
final TextEditingController _zipCodeController = TextEditingController();
String _address = '';
void _fetchAddress() async {
final zipCode = _zipCodeController.text;
final result = await ZipApi().zipCodeToAddress(zipCode);
if (result is Success<String, Exception>) {
setState(() {
_address = result.value;
});
} else if (result is Failure<String, Exception>) {
setState(() {
_address = '住所を取得できませんでした。';
});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('住所検索'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _zipCodeController,
decoration: const InputDecoration(
hintText: '郵便番号を入力してください',
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
maxLength: 7,
onSubmitted: (value) => _fetchAddress(),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _fetchAddress,
child: const Text('検索'),
),
const SizedBox(height: 20),
Text(_address),
],
),
),
);
}
}
flutter_hooks
StatefulWidgetを個人的には、使いたくないと考えている人なので、flutter_hooks
でページないの状態管理をやってみました。buildメソッドの中に処理を書くと、何度も呼ばれるので、それを防止する方法として、useMemoized
を使って、メソッドをキャッシュして、何度もビルドされたときに、呼ばれないように対策してます。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:resutl_type_example/api/result.dart';
import 'package:resutl_type_example/api/zip_api.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: DemoPage(),
);
}
}
class DemoPage extends HookWidget {
const DemoPage({super.key});
Widget build(BuildContext context) {
final zipCodeController = useTextEditingController();
final address = useState('');
// useMemoizedを使って、zipCodeToAddressメソッドをキャッシュする
// キャッシュすることで、zipCodeToAddressメソッドが再生成されることを防ぐ
final zipCodeToAddress = useMemoized(() => ZipApi().zipCodeToAddress);
// zipCodeToAddressメソッドを呼び出すfetchAddressメソッドを定義
Future<void> fetchAddress() async {
final zipCode = zipCodeController.text;
final result = await zipCodeToAddress(zipCode);
if (result is Success<String, Exception>) {
address.value = result.value;
} else if (result is Failure<String, Exception>) {
address.value = '住所を取得できませんでした。';
}
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepOrange,
title: const Text('住所検索'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: zipCodeController,
decoration: const InputDecoration(
hintText: '郵便番号を入力してください',
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
maxLength: 7,
onSubmitted: (value) => fetchAddress(),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: fetchAddress,
child: const Text('検索'),
),
const SizedBox(height: 20),
Text(address.value),
],
),
),
);
}
}
[実行結果]
🧑🎓thoughts
今回は、API通信のエラーハンドリングで使えるらしいResult型を使ってみました。正しくは、sealed class で自作したResultクラスですね。
プロジェクトで、sealed class
を使う機会がなかなかないので、導入してみたいなと思いつつできておりません😅
Discussion