↘️

ifをネストさせた方が良いこともあった💡

2024/02/02に公開

疑問だがこれはどうだろうか?

DocumentSnapshotでデータを取得したい。できるけど、分岐処理をつけないとエラーが発生したときにアプリがクラッシュする!

Flutter Fireというサイトにいい感じのエラー処理が書いてる。
https://firebase.flutter.dev/docs/firestore/usage

🔥ドキュメントスナップショット

DocumentSnapshot は、クエリから、またはドキュメントに直接アクセスすることによって返されます。 データベースにドキュメントが存在しない場合でも、スナップショットは常に返されます。

ドキュメントが存在するかどうかを確認するには、exists プロパティを使用します。

FirebaseFirestore.instance
    .collection('users')
    .doc(userId)
    .get()
    .then((DocumentSnapshot documentSnapshot) {
      if (documentSnapshot.exists) {
        print('Document exists on the database');
      }
    });

ドキュメントが存在する場合は、data メソッドを呼び出してそのデータを読み取ることができます。このメソッドは Map<String, Dynamic> を返し、ドキュメントが存在しない場合は null を返します。

FirebaseFirestore.instance
    .collection('users')
    .doc(userId)
    .get()
    .then((DocumentSnapshot documentSnapshot) {
      if (documentSnapshot.exists) {
        print('Document data: ${documentSnapshot.data()}');
      } else {
        print('Document does not exist on the database');
      }
    });

riverpodのコードで書くとこんな感じになる。中途はんぱですいません🙇
今回重要なのは、if(変数.exists)を理解することと、ifをネストしないと、null checkができないので、Text Widgetのところで!マークをつけないといけません!

body: worksState.when(
        data: (works) {
          // ifでerrorの場合の処理を書く
          if (works.exists) {
            // データがあれば、worksのデータを取得
            final work = works.data();
            if (work != null) {
              return // Widgetを書く
            }
          } else {
            // データがなけれが、存在しないことを表示
            return const Text('works data is not exist');
          }
          return null;
        },
        error: (error, stackTrace) => Text('error: $error'),
        loading: () => const IndicatorComponent(),
      ),
    );

上記のコードを使いたいときは、こんな感じでFutureProviderの処理を書きました。まずは、ロジックを書いたクラスを作っておく。

(keepAlive: true)
StopWatchController stopWatchController(StopWatchControllerRef ref) {
  return StopWatchController(ref);
}

class StopWatchController {
  StopWatchController(this.ref);
  final Ref ref;

  Future<DocumentSnapshot<Works>> getWorks() async {
    try {
      final docRef = await ref.read(worksConverterProvider).get();
      return docRef;
    } on Exception catch (e) {
      logger.d('error: $e');
      rethrow;
    }
  }

こんな感じで、Future<DocumentSnapshot<Works>>型のプロバイダーを生成するメソッドを定義します。わかりにくくてすいません💦
これは実験で作ってるコードで、動きますが全部載せると仕事でも使ってるので、どうかなと思ったので、これぐらいしか今回は書いてないです。

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../../../application/controller/stop_watch/stop_watch_controller.dart';
import '../../../../application/helper/logger/logger.dart';
import '../../../entity/task/task_state.dart';
part 'works_future.g.dart';


Future<DocumentSnapshot<Works>> worksFuture(WorksFutureRef ref) async {
  try {
    final docRef = await ref.watch(stopWatchControllerProvider).getWorks();
    return docRef;
  } on Exception catch (e) {
    throw Exception('error: $e');
  } finally {
    logger.d('works Future get🔌');
  }
}

existsってなんなのか?

existsという単語は、存在するという意味で、コードジャンプをして内部実装を見てみると、ゲッターのようです。メソッドってことですね。どんな役割があるかというと....

このスナップショットの参照を返します。
DocumentReference<T> 参照を取得します。

ソースに関するこのドキュメントに関するメタデータ、およびドキュメントにローカルがあるかどうか
SnapshotMetadata メタデータを取得します。

/// Returns true if the document exists.
bool get exists;

ドキュメントが存在する場合は true を返します。
bool get が存在します。

内部実装はこれ:

// Copyright 2020, the Chromium project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of cloud_firestore;

typedef FromFirestore<T> = T Function(
  DocumentSnapshot<Map<String, dynamic>> snapshot,
  SnapshotOptions? options,
);
typedef ToFirestore<T> = Map<String, Object?> Function(
  T value,
  SetOptions? options,
);

/// Options that configure how data is retrieved from a DocumentSnapshot
/// (e.g. the desired behavior for server timestamps that have not yet been set to their final value).
///
/// Currently unsupported by FlutterFire, but exposed to avoid breaking changes
/// in the future once this class is supported.

class SnapshotOptions {}

/// A [DocumentSnapshot] contains data read from a document in your [FirebaseFirestore]
/// database.
///
/// The data can be extracted with the data property or by using subscript
/// syntax to access a specific field.

abstract class DocumentSnapshot<T extends Object?> {
  /// This document's given ID for this snapshot.
  String get id;

  /// Returns the reference of this snapshot.
  DocumentReference<T> get reference;

  /// Metadata about this document concerning its source and if it has local
  /// modifications.
  SnapshotMetadata get metadata;

  /// Returns `true` if the document exists.
  bool get exists;

  /// Contains all the data of this document snapshot.
  T? data();

  /// {@template firestore.documentsnapshot.get}
  /// Gets a nested field by [String] or [FieldPath] from this [DocumentSnapshot].
  ///
  /// Data can be accessed by providing a dot-notated path or [FieldPath]
  /// which recursively finds the specified data. If no data could be found
  /// at the specified path, a [StateError] will be thrown.
  /// {@endtemplate}
  dynamic get(Object field);

  /// {@macro firestore.documentsnapshot.get}
  dynamic operator [](Object field);
}

class _JsonDocumentSnapshot implements DocumentSnapshot<Map<String, dynamic>> {
  _JsonDocumentSnapshot(this._firestore, this._delegate) {
    DocumentSnapshotPlatform.verify(_delegate);
  }

  final FirebaseFirestore _firestore;
  final DocumentSnapshotPlatform _delegate;

  
  String get id => _delegate.id;

  
  late final DocumentReference<Map<String, dynamic>> reference =
      _firestore.doc(_delegate.reference.path);

  
  late final SnapshotMetadata metadata = SnapshotMetadata._(_delegate.metadata);

  
  bool get exists => _delegate.exists;

  
  Map<String, dynamic>? data() {
    // TODO(rrousselGit): can we cache the result, to avoid deserializing it on every read?
    return _CodecUtility.replaceDelegatesWithValueInMap(
      _delegate.data(),
      _firestore,
    );
  }

  
  dynamic get(Object field) {
    return _CodecUtility.valueDecode(_delegate.get(field), _firestore);
  }

  
  dynamic operator [](Object field) => get(field);
}

まとめ

今回やりたいことをまとめると...

  1. if(変数名.exists)で値が取得できているかエラーチェックをする.
  2. !使いたくないので、if (変数名 != null)のネストしたif文を使う.

こんな感じですね。リーダブルコードには、if文はネストさせるのはアンチパターンと書いてありますけど、必要な場面もある気がします。ネストしないとifの分岐処理書けないし、{}の中が変数の有効範囲(スコープ)なので、workって値を渡せません!

最近こんなコードを書くことが多いのですが、riverpodのコードへの理解よりもDartの基本的な文法への理解が必要だなと思いました。
あとは、Firebaseのデータ型とかオブジェクト思考ですね。良いコードを書こうと日々探究してますが中々うまくいかないです💦

Discussion