😇

Dart Result<S, E extends Exception>

2024/07/18に公開

📕Overview

DartにもSwiftのResultのような、データ型があるらしい?
https://developer.apple.com/documentation/swift/result

成功または失敗を表す値で、それぞれの場合に関連する値を含む。

Result.success(_:)
A success, storing a Success value.

Result.success(_:)
成功、Success値を格納する。

あると言っても自作してるようだ。
https://codewithandrea.com/articles/flutter-exception-handling-try-catch-result-type/

dioというHTTP通信ができるパッケージを使って、郵便番号検索で使ってみました!
https://pub.dev/packages/dio
https://zipcloud.ibsnet.co.jp/doc/api

🧷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