🙆‍♀️

FlutterでSNSアプリのフォロー & フォロワーを作る

2024/03/20に公開

このページではFlutter & firebaseを使用したフォロー&フォロワーの作り方を説明します。

前提

ツール

  • IDE vscode
  • 言語 dart
  • フレームワーク flutter
  • データベース firebase
  • simulator
  • ソースコード管理 Github

SNSアプリに必要な要件

SNSアプリでアカウントに必要なフィールド

  • uid String
  • photo-url String
  • username String
  • email String
  • phone-number String
  • bio String
  • create_time DateTime
  • followers List
  • following List

followers、followingについてはListにする必要がありますので注意してください。

SNSアプリではアカウントの登録が必要となるため、上記のフィールドを使用してアカウントの登録を行います。
アカウント認証、ログイン認証はfirebaseを使用して行います。

アカウントの登録方法

アカウントの登録は以下のようにロジックを組むことで登録が可能です。

RegisterPage.dart
  void signUp() async {
    // ローディング開始
    showDialog(
      context: context,
      builder: (context) => const Center(
        child: CircularProgressIndicator(),
      ),
    );
    // make sure passwords match
    if (passwordTextController.text != confirmPasswordTextController.text) {
      //ローディングを閉じる
      Navigator.pop(context);
      //エラー表示
      setState(() {
        errorTextController.text = "パスワードが間違っています🤔";
      });
      return;
    }
    //アカウントの作成
    try {
      UserCredential userCredential =
          await FirebaseAuth.instance.createUserWithEmailAndPassword(
        email: emailTextController.text,
        password: passwordTextController.text,
      );
      //ここでアカウントの登録を行う
      FirebaseFirestore.instance
          .collection('Users')
          .doc(userCredential.user!.uid)
          .set(
        {
          'email': emailTextController.text,
          'username':
              emailTextController.text.split('@')[0], // 初期ユーザーネーム
          'bio': 'Empty bio...', // 初期の自己紹介
          'uid': userCredential.user!.uid,
          'create_time': DateTime.now(),
          'phone_number': '',
          'Followers': [],
          'Following': []
          // 他フィールドを追加していい
        },
      );

コードからわかるように今回はphoto-urlフィールドは使用していませんが、上記の関数を呼び出すことで登録が可能です。

今回のフォロー & フォロワーの機能を実装する上でアカウントは最低でも3つは作成してください。
1つ目は自分がログインするアカウント
2つ目は他者のアカウントとする
3つ目はログインユーザーが複数のアカウントフォローができるのかを確認するために作成

フォロー & フォロワーの機能を作る

前提

ここで二つのコンポーネントを作成して一つのページを作成します。
よってdartファイルは3つは必要となります。

  1. コンポーネント
    • FollowButton
    • UserFollowComponents
  2. ページ
    • FollowListPage

では実際にコンポーネントの作成に入ります。

FollowButton実装

follow_button.dart
import 'package:flutter/material.dart';

class FollowButton extends StatelessWidget {
  final bool isFollow;
  void Function()? followButtonOnTap;
  void Function()? onTap;
  FollowButton(
      {super.key,
      required this.isFollow,
      required this.onTap,
      this.followButtonOnTap});

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: isFollow
          ? TextButton.icon(
              icon: Icon(
                Icons.person_outline_outlined,
                color: Theme.of(context).colorScheme.onSecondary,
              ),
              label: Text(
                "Follow Now",
                style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 10,
                    color: Theme.of(context).colorScheme.onSecondary),
              ),
              onPressed: followButtonOnTap,
            )
          : Container(
              height: 40,
              decoration: BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(20),
              ),
              child: TextButton.icon(
                icon: Icon(
                  Icons.person_add_alt,
                  color: Theme.of(context).colorScheme.primary,
                ),
                label: Text(
                  "Follow",
                  style:
                      TextStyle(color: Theme.of(context).colorScheme.primary),
                ),
                onPressed: followButtonOnTap,
              ),
            ),
    );
  }
}

それぞれフィールドの説明をします。

  1. isFollow
    • これはフォローしている場合にはtrue、そうでない場合にはfalseとなるようにしています。
    • フォローをするとFollow Now が表示され、そうでない場合には Followが表示されます。
  2. followButtonOnTap
    • こちらは今後何かで使用することがあると考えて加えたフィールドですが、現時点では必要がありません。
  3. onTap
    • こちらはFollowButtonを押下することでisFollowのtrue,falseの切り替えを行うようにしています。

FollowButtonの実装については特に難しさはなかったと思います。
次へ進みます。

UserFollowComponentsの実装

user_follow_components.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_timelines/view/components/follow_button.dart';

class UserFollowComponents extends StatefulWidget {
  final String followUserName;
  final String followUserEmail;
  final String followUid;
  List<String> following;

  UserFollowComponents({
    Key? key,
    required this.followUserName,
    required this.following,
    required this.followUid,
    required this.followUserEmail,
  }) : super(key: key);

  
  State<UserFollowComponents> createState() => _UserFollowComponentsState();
}

class _UserFollowComponentsState extends State<UserFollowComponents> {
  late bool isFollow;
  final currentUser = FirebaseAuth.instance.currentUser!;

  
  void initState() {
    super.initState();
    isFollow = widget.following.contains(widget.followUserEmail);
  }

  void toggleFollow() {
    setState(() {
      isFollow = !isFollow;
    });
    DocumentReference followRef =
        FirebaseFirestore.instance.collection('Users').doc(currentUser.uid);
    if (isFollow) {
      followRef.update({
        'Following': FieldValue.arrayUnion([widget.followUserEmail])
      });
    } else {
      followRef.update({
        'Following': FieldValue.arrayRemove([widget.followUserEmail])
      });
    }
    DocumentReference followerRef =
        FirebaseFirestore.instance.collection('Users').doc(widget.followUid);
    if (isFollow) {
      followerRef.update({
        'Followers': FieldValue.arrayUnion([currentUser.email])
      });
    } else {
      followerRef.update({
        'Followers': FieldValue.arrayRemove([currentUser.email])
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10, top: 10),
      child: Container(
        width: 435,
        height: 75,
        decoration: BoxDecoration(
          color: Theme.of(context).colorScheme.primary,
          borderRadius: BorderRadius.circular(10),
        ),
        child: Padding(
          padding: const EdgeInsets.all(10.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Text(
                  widget.followUserName,
                ),
              ),
              StreamBuilder(
                stream: FirebaseFirestore.instance
                    .collection('Users')
                    .doc(currentUser.uid)
                    .snapshots(),
                builder: (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return CircularProgressIndicator();
                  } else if (snapshot.hasError) {
                    return Text('Error: ${snapshot.error}');
                  } else {
                    // Firestoreから現在のユーザーのデータを取得する
                    var userData = snapshot.data;

                    // 現在のユーザーがフォローしているかどうかを判定する
                    isFollow =
                        userData!['Following'].contains(widget.followUserEmail);

                    if (currentUser.uid != widget.followUid) {
                      return FollowButton(
                        isFollow: isFollow,
                        onTap: toggleFollow,
                      );
                    } else if (currentUser.uid == widget.followUid) {
                      return SizedBox();
                    } else {
                      return CircularProgressIndicator();
                    }
                  }
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

ではフィールドの説明を行います。

  1. followUserName
    • フォロー対象のユーザーネーム
  2. followUserEmail
    • フォロー対象のメールアドレス
  3. followUid
    • フォロー対象のuid
  4. following
    • フォロー中のリスト

アカウント登録時のパスは以下
collection('Users').doc(currentUser.uid)
docには現在ログインしているユーザのuidを設定しています。
そしてそのdocにはfollowingとfollowersフィールドがあります。
toggleFollow関数ではcurrentUserがフォローを行うと対象followUidのfollowersフィールドにcurrentUserのメールアドレスが登録され、currentUserのfollowingフィールドにも対象followUserEmailが追加されるようになっています。

こうすることでAというアカウントでログインしてBとCをフォローしていたとするとBとCのアカウントのフォロワーに追加されるようなロジックを組むことが可能です。

initStateに以下のようなコードがあるかと思います。

isFollow = widget.following.contains(widget.followUserEmail);

このコードは対象ユーザのメールアドレスをフォローしているのかそうでないかの確認を行います。

FollowListPageの実装

follow_list_page.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_timelines/view/components/user_follow_components.dart';

class FollowListPage extends StatefulWidget {
  const FollowListPage({Key? key});

  
  State<FollowListPage> createState() => _FollowListPageState();
}

class _FollowListPageState extends State<FollowListPage> {
  final currentUser = FirebaseAuth.instance.currentUser;
  List<UserFollowComponents> users = [];
  Future<void> getLoading() async {
    // 新しい情報を取得する処理をここに追加する
    // 例: データベースから最新の投稿内容を取得する

    // データベースから最新の投稿内容を取得する場合の例
    QuerySnapshot snapshot = await FirebaseFirestore.instance
        .collection('Users')
        .orderBy("username", descending: true)
        .get();

    // 新しい情報を反映させるためにStateを更新する
    setState(() {
      // snapshotのデータを使ってUIを更新する
      // ここでは新しい投稿内容をStateにセットしてUIを再構築する
      // snapshotから投稿データを取得し、Stateにセットする
      users = snapshot.docs
          .map((user) => UserFollowComponents(
                key: Key(user.id),
                following: List<String>.from(user['Following'] ?? []),
                followUserName: user["username"],
                followUid: user["uid"],
                followUserEmail: user["email"],
              ))
          .toList();
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Follow & Follower"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(10),
        child: Column(
          children: [
            // 投稿
            Expanded(
              child: StreamBuilder(
                stream: FirebaseFirestore.instance
                    .collection('Users')
                    .orderBy("username", descending: true)
                    .snapshots(),
                builder: (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const CircularProgressIndicator();
                  } else if (snapshot.hasError) {
                    return Text('Error: ${snapshot.error}');
                  } else {
                    users.clear();
                    for (var user in snapshot.data!.docs) {
                      final usr = UserFollowComponents(
                        key: Key(user.id),
                        following: List<String>.from(user['Following'] ?? []),
                        followUserName: user["username"],
                        followUid: user["uid"],
                        followUserEmail: user["email"],
                      );
                      users.add(usr);
                    }
                    return ListView.builder(
                      itemCount: users.length,
                      itemBuilder: (context, index) {
                        //users[index] --- usersの中にあるuidが入っている
                        return users[index];
                      },
                    );
                  }
                },
              ),
            ),
            const SizedBox(
              height: 50,
            ),
          ],
        ),
      ),
    );
  }
}


/**
 Column(
          children: [
            Padding(
              padding: const EdgeInsets.only(bottom: 10, top: 10),
              child: Container(
                width: 435,
                height: 75,
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.primary,
                  borderRadius: BorderRadius.circular(10),
                ),
                child: Padding(
                  padding: const EdgeInsets.all(10.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Container(
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(20),
                        ),
                        child: Text(
                          "test@gmail.com",
                        ),
                      ),
                      FollowButton(
                        isFollow: isFollow,
                        followUserName: '',
                      )
                    ],
                  ),
                ),
              ),
            ),
            Padding(
              padding: EdgeInsets.only(bottom: 10, top: 10),
              child: Container(
                width: 435,
                height: 75,
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.primary,
                  borderRadius: BorderRadius.circular(10),
                  border: Border.all(
                    color: Theme.of(context).colorScheme.primary,
                  ),
                ),
                child: Padding(
                  padding: const EdgeInsets.all(10.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Container(
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(20),
                        ),
                        child: Text(
                          "oootoco@gmail.com",
                        ),
                      ),
                      FollowButton(
                        isFollow: isFollow,
                        followUserName: '',
                      )
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
 */

上記のコードはユーザをリスト化してフォローできるか、そうでないかの判断ができるページとなっています。

ログインしているユーザ自身はフォローができないようにしています。
test@gmail.comでログインを行なっているのでFollowButtonは表示されていません。

StreamBuilderを使用することでリアルタイムでフォローを行う、フォローを外すことが可能です。

isFollow = widget.following.contains(widget.followUserEmail);

これを行なっているのでリロードしてもフォローが外れると言った問題は解決できます

以上となります。

Discussion