🦁

【Flutter】Cloud Functionsを使わずに、Firebase AuthとLINE SDKを使ってLINEログインを実装する

2021/11/28に公開

まえがき

https://zenn.dev/yskuue/articles/410e5b787b354a

先日↑こんな記事を書いた。

Flutter×Firebaseのアプリに、LINEログインを追加するという内容だが、Clond Functionsも必要で、個人的にはちょっと面倒というか、アプリ側の実装だけで済ませたいなという感じだった。

そこで今回は、Cloud Functionsは使わずに、LINEログインを実装する方法について書いてみようと思う。

どうやるか?

ざっくりのイメージだが、前回は↓こんな感じ。

ログイン状態をFirebase Authで一元管理するために、LINEログインで得られた情報(userId)を使って、Firebase Authでもログイン(カスタム認証)するという手順を踏んでいた。

前回の記事でも書いたが、この方法だと

Firebase Authでもログイン(カスタム認証)

これがアプリ側ではできないので、Cloud Functionsを使わざるを得なかった。

そのため今回は、Firebase Authでのログイン状態の一元管理をやめ、Firebase AuthとLINE SDKの両方でログイン状態を管理するというやり方に切り替える。

雑な図だが↓こんな感じ(矢印の向きは正直適当・・・)

なので厳密にいうと、本記事タイトルの「Cloud Functionsを使わずに、Firebase AuthとLINE SDKを使ってLINEログインを実装する」というのは間違いで、LINEログイン周りはLINE SDKに一任する形になる。

https://zenn.dev/yskuue/articles/410e5b787b354a

アプリ側の実装

前提を整理したので、実際の実装について書いていく。

ファイル構成

ファイル構成は以下の通り(前回と同じ)。

lib/
 ├ main.dart
 └ app/
    ├ sign_in/
    │  └ sign_in_page.dart (View)
    │
    └ auth_manager/
       └ auth_manager.dart (ロジック)

以下、それぞれのファイルの中身を書いていく。

main.dart

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_line_sdk/flutter_line_sdk.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'app/auth_manager/auth_manager.dart';
import 'app/sign_in/sign_in_page.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  await LineSDK.instance.setup('ここはLINEのチャンネルID');
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final _authManager = ref.watch(authManagerProvider);
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'TITLE',
      home: _authManager.isLoggedIn
          ? const HomePage() // ログイン後の画面
          : const SignInPage(),
    );
  }
}

前回同様、状態管理はriverpodを使用している。

authManager.isLoggedIn でログイン状態を判定して、ログイン後ならHomePage(仮)を、未ログインならSignInPageを表示する。

sign_in_page.dart

ログイン画面は至ってシンプルで、ログインボタンがあって、ボタンを押したらログイン処理が実行されるだけ。

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

import '../auth_manager/auth_manager.dart';

class SignInPage extends ConsumerWidget {
  const SignInPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () async =>
              await ref.read(authManagerProvider).signInWithLine(),
          child: const Text('Sign In with LINE'),
        ),
      ),
    );
  }
}

auth_manager.dart

では一番重要な、auth_manager.dartはというと・・・

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_line_sdk/flutter_line_sdk.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final authManagerProvider = ChangeNotifierProvider<AuthManager>(
  (ref) {
    return AuthManager();
  },
);

class AuthManager with ChangeNotifier {
  AuthManager() {
    // Firebase Authのログイン状態を管理
    _firebaseAuth.authStateChanges().listen((user) {
      isLoggedIn = user != null;
      currentUserId = user?.uid;
      notifyListeners();
    });

    Future<void>(
      () async => await _checkLineAuthState(),
    );
  }
  final _firebaseAuth = FirebaseAuth.instance;
  final _lineSdk = LineSDK.instance;

  String? currentUserId;
  bool isLoggedIn = false;

  // ログイン/ログアウト時に、LINE SDKのログイン状態を更新する処理
  Future<void> _checkLineAuthState() async {
    final _lineAccessToken = await _lineSdk.currentAccessToken;
    isLoggedIn = _lineAccessToken != null;
    if (isLoggedIn) {
      final _lineUser = await _lineSdk.getProfile();
      currentUserId = _lineUser.userId;
    }
    notifyListeners();
  }

  Future<void> signInWithFirebaseAuth() async {
    // Firebase対応のログイン処理
  }

  Future<void> signInWithLine() async {
    // LINEログイン
    await _lineSdk.login();
    await _checkLineAuthState();
  }

  // ログアウト処理
  Future<void> signOut() async {
    // Firebase, LINEどちらも、未ログイン状態でsignOut()しても問題なし
    await _firebaseAuth.signOut();
    await _lineAuthService.signOut();
    await _checkLineAuthState();
  }
}

Firebase Authについては、authStateChanges()を使用すれば、streamでログイン状態を監視できるので、ログイン状態の管理は比較的シンプル。

対してLINE SDKには同じようなメソッドが用意されていないので、ログイン/ログアウトの際に、明示的にログイン状態を更新してやる必要がある。

LINE SDKのログイン状態の判別については、他にもやり方があるかもしれないが、ひとまず今回は、

final _lineAccessToken = await _lineSdk.currentAccessToken;
    isLoggedIn = _lineAccessToken != null;

というように、currentAccessTokenの有無で判別している。

また、ログイン処理とは直接関係ないが、ログイン中のuserのuserIdは使用する機会があるので、↓のように、ログインした場合はcurrentUserIdを保持するようにしている。

if (isLoggedIn) {
    final _lineUser = await _lineSdk.getProfile();
    currentUserId = _lineUser.userId;
 }

これで完成・・・??

ひとまずここまでの内容で、Cloud Functionsを使わずにログイン処理を実装することができた。

auth_manager.dart内はちょっとごちゃっとしているが、外側(View)から見れば、_authManager.isLoggedInでログイン状態を判別できるのでまあ悪くはないと思う。

でもやっぱり、auth_manager.dartのごちゃごちゃが気になる。
Firebase Authで一元管理していた時の方がスッキリしたので、少しでもそれに近づけたい。

ということで、もうちょっとリファクタリングしてみる。

LINE SDKのログイン管理部分を、別ファイルに切り出す

↓こんな感じで、LINE関連の処理をline_auth_service.dartに切り出す

lib/
 ├ main.dart
 └ app/
    ├ auth_manager/
    │  └ auth_manager.dart
    │
    └ line_auth/
       └ line_auth_service.dart     NEW!!

line_auth_service.dart

ただ切り出すだけでなく、Firebase Auth同様に、streamでログイン状態を監視できるようにする。
こんな感じ↓

import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_line_sdk/flutter_line_sdk.dart';

final lineAuthServiceProvider = Provider<LineAuthService>((ref) {
  return LineAuthService();
});

class LineAuthService {
  LineAuthService() {
    Future<void>(() async {
      try {
        final _user = await _lineSdk.getProfile();
        _authStateController.sink.add(_user.userId);
      } catch (e) {
        // 未ログイン時はgetProfile()がエラーになる
        print('Not Logged In');
      }
    });
  }
  final _lineSdk = LineSDK.instance;
  final _authStateController = StreamController<String?>();
  // ログインしていれば、userIdを返す
  Stream<String?> get authStateChange => _authStateController.stream;

  Future<void> signIn() async {
    await _lineSdk.login();
    final _user = await _lineSdk.getProfile();
    _authStateController.sink.add(_user.userId);
  }

  Future<void> signOut() async {
    await _lineSdk.logout();
    _authStateController.sink.add(null);
  }
}

authStateChangeという名前は微妙な気もするが、とりあえずFirebase Authに合わせた。

Firebase Authの場合は、streamでUserを取得できるが、今のところ使用するのはuserIdのみなので、今回はStringにしている。

auth_manager.dart

auth_manager.dartは以下のようになった。

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../line_auth/line_auth_service.dart';

final authManagerProvider = ChangeNotifierProvider<AuthManager>(
  (ref) {
    return AuthManager(
      ref.watch(lineAuthServiceProvider),
    );
  },
);

class AuthManager with ChangeNotifier {
  AuthManager(this._lineAuthService) {
    // Firebase Authのログイン状態を管理
    _firebaseAuth.authStateChanges().listen((user) {
      isLoggedIn = user != null;
      currentUserId = user?.uid;
      notifyListeners();
    });

    // LINE SDKのログイン状態を管理
    _lineAuthService.authStateChange.listen((userId) {
      isLoggedIn = userId != null;
      currentUserId = userId;
      notifyListeners();
    });
  }
  final LineAuthService _lineAuthService;

  final _firebaseAuth = FirebaseAuth.instance;
  String? currentUserId;
  bool isLoggedIn = false;

  Future<void> signInWithFirebaseAuth() async {
    // Firebase対応のログイン処理
  }

  // LINEログイン
  Future<void> signInWithLine() async => await _lineAuthService.signIn();

  // ログアウト処理
  Future<void> signOut() async {
    // Firebase, LINEどちらも、未ログイン状態でsignOut()しても問題なし
    await _firebaseAuth.signOut();
    await _lineAuthService.signOut();
  }
}

Firebase AuthとLINE SDK、それぞれのログイン状態の管理がほぼ同じコードで実現でき、さっきよりシンプルになったと思う。

firebase_auth_service.dartも作って、authStateChanges()で同じくStringのuserIdを返すようにすれば、auth_manager.dart自体はもっとスッキリ書けそうだが、今回はここまでにしておく。

さいごに

以上のやり方で、Cloud Functionsを使うことなく、Firebase AuthとLINE SDKを併用したログイン機能を実装することができた。

どちらの方法を使うかは正直好みの気もするが、Cloud Functionsのメンテナンスやサーバー代を考慮すると、今回の方法の方がいいかもしれない。

Discussion