0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter × Firebase】他ユーザープロフィール機能でコミュニティを活性化!プライバシー配慮とパフォーマンス最適化のベストプラクティス#6

Posted at

はじめに

コーヒー焙煎ログアプリの開発も6回目になりました!今回は他ユーザープロフィール閲覧機能を実装し、コミュニティ機能をさらに充実させます。

単なるプロフィール表示ではなく、プライバシー配慮Firestoreインデックス最適化エラーハンドリング強化など、実際のプロダクションで必要な要素を盛り込んだ実装をご紹介します。

今回実装する機能

🎯 メイン機能

  • 他ユーザーのプロフィール表示
  • ユーザーの投稿履歴閲覧
  • プライバシー設定による表示制御
  • フォロー/アンフォロー機能

🛠 技術的な特徴

  • Firestoreインデックス最適化
  • リアルタイムデータ更新
  • 堅牢なエラーハンドリング
  • パフォーマンス配慮

アーキテクチャ設計

データモデル

// 公開プロフィール情報
class PublicUserProfile {
  final String userId;
  final String displayName;
  final String? profileImageUrl;
  final String roastingLevel;
  final String roastingMachine;
  final int experienceYears;
  
  // プライバシー設定
  final bool isProfileVisible;      // プロフィール情報公開
  final bool isStatisticsVisible;   // 統計情報公開
  final bool isActivityVisible;     // 投稿履歴公開
  
  PublicUserProfile({
    required this.userId,
    required this.displayName,
    this.profileImageUrl,
    required this.roastingLevel,
    required this.roastingMachine,
    required this.experienceYears,
    required this.isProfileVisible,
    required this.isStatisticsVisible,
    required this.isActivityVisible,
  });
}

サービス層の実装

class UserProfileService {
  static final FirebaseFirestore _firestore = FirebaseFirestore.instance;

  /// 他ユーザーの公開プロフィールを取得
  static Future<PublicUserProfile?> getPublicUserProfile(String userId) async {
    try {
      final doc = await _firestore
          .collection('users')
          .doc(userId)
          .get()
          .timeout(const Duration(seconds: 30));

      if (!doc.exists) return null;

      final data = doc.data()!;
      
      return PublicUserProfile(
        userId: userId,
        displayName: data['displayName'] ?? 'ユーザー',
        profileImageUrl: data['profileImageUrl'],
        roastingLevel: data['roastingLevel'] ?? '初心者',
        roastingMachine: data['roastingMachine'] ?? '未設定',
        experienceYears: data['experienceYears'] ?? 0,
        isProfileVisible: data['isProfileVisible'] ?? false,
        isStatisticsVisible: data['isStatisticsVisible'] ?? false,
        isActivityVisible: data['isActivityVisible'] ?? false,
      );
    } catch (e) {
      debugPrint('ユーザープロフィール取得エラー: $e');
      return null;
    }
  }
}

UI実装のポイント

1. プライバシー配慮のUI

Widget _buildProfileTab() {
  if (!_profile!.isProfileVisible) {
    return const Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.lock, size: 64, color: Colors.grey),
          SizedBox(height: 16),
          Text('このユーザーのプロフィールは非公開です'),
        ],
      ),
    );
  }

  return SingleChildScrollView(
    child: Column(
      children: [
        _buildInfoCard(),
        if (_profile!.isStatisticsVisible) _buildStatisticsCard(),
      ],
    ),
  );
}

2. エラーハンドリングの強化

if (snapshot.hasError) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Icon(Icons.error_outline, size: 64, color: Colors.grey),
        const SizedBox(height: 16),
        Text('投稿の読み込みに失敗しました'),
        const SizedBox(height: 8),
        Text(
          'ネットワーク接続を確認してください',
          style: Theme.of(context).textTheme.bodySmall?.copyWith(
            color: Colors.grey,
          ),
        ),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: () => setState(() {}),
          child: const Text('再試行'),
        ),
      ],
    ),
  );
}

Firestoreインデックス最適化

複合クエリの課題

他ユーザーの投稿を取得する際、以下のクエリが必要になります:

_firestore
  .collection('roastPosts')
  .where('userId', isEqualTo: userId)
  .orderBy('createdAt', descending: true)
  .limit(20)

このクエリには複合インデックスが必要です。

インデックス作成方法

方法1: エラーメッセージのURLから作成

Firestoreエラーメッセージに含まれるURLをクリックすると、必要なインデックスが自動作成されます。

方法2: Firebase Consoleから手動作成

  1. Firebase Console → Firestore Database → インデックス
  2. 複合インデックスを作成
    • コレクション: roastPosts
    • フィールド1: userId (昇順)
    • フィールド2: createdAt (降順)

インデックス最適化のコード

/// 他ユーザーの投稿一覧を取得
static Stream<List<RoastPost>> getUserPosts(String userId, {int limit = 20}) {
  try {
    return _firestore
        .collection('roastPosts')
        .where('userId', isEqualTo: userId)
        .orderBy('createdAt', descending: true)  // インデックス必須
        .limit(limit)
        .snapshots()
        .timeout(const Duration(seconds: 30))
        .map((snapshot) {
      return snapshot.docs.map((doc) {
        return RoastPost.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
      }).toList();
    }).handleError((error) {
      debugPrint('ユーザー投稿取得エラー: $error');
      return <RoastPost>[];
    });
  } catch (e) {
    debugPrint('ユーザー投稿取得エラー: $e');
    return Stream.value([]);
  }
}

ナビゲーション連携の実装

コミュニティからのスマートナビゲーション

GestureDetector(
  onTap: () {
    final currentUser = FirebaseAuth.instance.currentUser;
    if (currentUser?.uid == post.userId) {
      // 自分の投稿 → 自分のプロフィール
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => const UserProfileScreen(),
        ),
      );
    } else {
      // 他ユーザーの投稿 → 他ユーザープロフィール
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => OtherUserProfileScreen(
            userId: post.userId,
            userName: post.userName,
          ),
        ),
      );
    }
  },
  child: Text(post.userName),
)

パフォーマンス最適化のテクニック

1. タイムアウト設定

.timeout(const Duration(seconds: 30))

2. エラー時のフォールバック

.handleError((error) {
  debugPrint('ユーザー投稿取得エラー: $error');
  return <RoastPost>[];
})

3. 効率的なデータ取得

// 必要最小限のフィールドのみ取得
final doc = await _firestore
    .collection('users')
    .doc(userId)
    .get();
    
if (!doc.exists) return null;

陥りやすい落とし穴と対策

1. 無限ループの回避

悪い例

// StreamBuilderで無限ループが発生
static Stream<List<RoastPost>> getUserPosts(String userId) {
  return Stream.fromFuture(_getUserPostsWithRetry(userId));
}

良い例

// snapshotsを使用してリアルタイム更新
static Stream<List<RoastPost>> getUserPosts(String userId) {
  return _firestore
      .collection('roastPosts')
      .where('userId', isEqualTo: userId)
      .snapshots();
}

2. プライバシー設定の適切な処理

// 統計情報が非公開の場合は取得しない
if (!profile.isStatisticsVisible) {
  return null;
}

3. コレクション名の統一

複数のサービスで同じコレクションを扱う場合、命名を統一することが重要です。

セキュリティ考慮事項

1. Firestoreルールの設定

// Firestoreセキュリティルール例
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read: if request.auth != null;
      allow write: if request.auth.uid == userId;
    }
    
    match /roastPosts/{postId} {
      allow read: if request.auth != null;
      allow write: if request.auth.uid == resource.data.userId;
    }
  }
}

2. プライバシー設定の尊重

// プライバシー設定をチェック
if (!user.isProfileVisible) {
  return PrivacyBlockedWidget();
}

まとめ

今回実装した他ユーザープロフィール機能では、以下の重要なポイントを学習しました:

📚 技術的学習事項

  • Firestoreインデックスの重要性と作成方法
  • Stream管理でのパフォーマンス配慮
  • エラーハンドリングのユーザビリティ向上

🔒 プライバシー・セキュリティ

  • プライバシー設定による表示制御
  • 適切なFirestoreルール設計
  • ユーザーデータの保護

🎨 UX設計

  • 直感的なナビゲーション
  • 分かりやすいエラー表示
  • 適切なローディング状態

🚀 スケーラビリティ

  • 効率的なデータ取得
  • インデックス最適化
  • パフォーマンス配慮

次回は、リアルタイム通知機能の実装に挑戦予定です。FCM(Firebase Cloud Messaging)を使用して、フォローやいいねの通知を実装していきます!

参考リンク


この記事が役に立ったら、ぜひいいねフォローをお願いします!
質問やコメントもお気軽にどうぞ 🙌

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?