You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter集成Appwrite Google OAuth登录session为空无法跳转求助

问题描述

我正在为采用Riverpod做状态管理的Flutter应用集成Appwrite的Google OAuth登录功能。邮箱密码登录/注册功能正常,成功后能跳转至home_view.dart;但Google登录后,Appwrite后台能正常显示用户数据,却返回null的session变量,导致无法跳转至home_view.dart。不过按下热重载后,已登录的用户就能进入home_view.dart。求帮忙解决,感谢!


相关代码

auth_api.dart

final authAPIProvider = Provider((ref) {
  final account = ref.watch(appwriteAccountProvider);
  return AuthAPI(account: account);
});

abstract class IAuthAPI {
  FutureEither<model.User> signUp({
    required String email,
    required String password,
  });
  FutureEither<model.Session> login({
    required String email,
    required String password,
  });
    FutureEither<model.Session> loginWithGoogle();
  Future<model.User?> currentUserAccount();
  FutureEitherVoid logout();
}

class AuthAPI implements IAuthAPI {
  final Account _account;
  AuthAPI({required Account account}) : _account = account;

  @override
  Future<model.User?> currentUserAccount() async {
    try {
      return await _account.get();
    } on AppwriteException {
      return null;
    } catch (e) {
      return null;
    }
  }

  @override
  FutureEither<model.User> signUp({
    required String email,
    required String password,
  }) async {
    try {
      final account = await _account.create(
        userId: ID.unique(),
        email: email,
        password: password,
      );
      return right(account);
    } on AppwriteException catch (e, stackTrace) {
      return left(
        Failure(e.message ?? 'Some unexpected error occurred', stackTrace),
      );
    } catch (e, stackTrace) {
      return left(
        Failure(e.toString(), stackTrace),
      );
    }
  }

  @override
  FutureEither<model.Session> login({
    required String email,
    required String password,
  }) async {
    try {
      final session = await _account.createEmailSession(
        email: email,
        password: password,
      );
      return right(session);
    } on AppwriteException catch (e, stackTrace) {
      return left(
        Failure(e.message ?? 'Some unexpected error occurred', stackTrace),
      );
    } catch (e, stackTrace) {
      return left(
        Failure(e.toString(), stackTrace),
      );
    }
  }

    @override
  FutureEither<model.Session> loginWithGoogle(
  ) async {
    try {
      final session = await _account.createOAuth2Session(
        provider: 'google',
        success: "",
        failure: "",
      );
      debugPrint(session);
      return right(session);
    } on AppwriteException catch (e, stackTrace) {
      return left(
        Failure(e.message ?? 'Some unexpected error occurred', stackTrace),
      );
    } catch (e, stackTrace) {
      return left(
        Failure(e.toString(), stackTrace),
      );
    }
  }

  @override
  FutureEitherVoid logout() async {
    try {
      await _account.deleteSession(
        sessionId: 'current',
      );
      return right(null);
    } on AppwriteException catch (e, stackTrace) {
      return left(
        Failure(e.message ?? 'Some unexpected error occurred', stackTrace),
      );
    } catch (e, stackTrace) {
      return left(
        Failure(e.toString(), stackTrace),
      );
    }
  }
}

auth_controller.dart

final authControllerProvider =
    StateNotifierProvider<AuthController, bool>((ref) {
  return AuthController(
    authAPI: ref.watch(authAPIProvider),
    userAPI: ref.watch(userAPIProvider),
  );
});

final currentUserDetailsProvider = FutureProvider((ref) {
  final currentUserId = ref.watch(currentUserAccountProvider).value!.$id;
  debugPrint("Utente corrente: $currentUserId");
  final userDetails = ref.watch(userDetailsProvider(currentUserId));
  return userDetails.value;
});

final userDetailsProvider = FutureProvider.family((ref, String uid) {
  final authController = ref.watch(authControllerProvider.notifier);
  return authController.getUserData(uid);
});

final currentUserAccountProvider = FutureProvider((ref) {
  final authController = ref.watch(authControllerProvider.notifier);
  return authController.currentUser();
});

class AuthController extends StateNotifier<bool> {
  final AuthAPI _authAPI;
  final UserAPI _userAPI;
  AuthController({
    required AuthAPI authAPI,
    required UserAPI userAPI,
  })  : _authAPI = authAPI,
        _userAPI = userAPI,
        super(false);
  // state = isLoading

  Future<model.User?> currentUser() => _authAPI.currentUserAccount();

  void signUp({
    required String email,
    required String password,
    required BuildContext context,
  }) async {
    state = true;
    final res = await _authAPI.signUp(
      email: email,
      password: password,
    );
    state = false;
    res.fold(
      (l) => showSnackBar(context, l.message),
      (r) async {
        UserModel userModel = UserModel(
          email: email,
          name: getNameFromEmail(email),
          followers: const [],
          following: const [],
          profilePic: '',
          bannerPic: '',
          uid: r.$id,
          bio: '',
          isTwitterBlue: false,
        );
        final res2 = await _userAPI.saveUserData(userModel);
        res2.fold((l) => showSnackBar(context, l.message), (r) {
          showSnackBar(context, 'Accounted created! Please login.');
          Navigator.push(context, LoginView.route());
        });
      },
    );
  }

  void login({
    required String email,
    required String password,
    required BuildContext context,
  }) async {
    state = true;
    final res = await _authAPI.login(
      email: email,
      password: password,
    );
    state = false;
    res.fold(
      (l) => showSnackBar(context, l.message),
      (r) {
        Navigator.push(context, HomeView.route());
      },
    );
  }

  void loginWithGoogle({
    required BuildContext context,
  }) async {
    state = true;
    final res = await _authAPI.loginWithGoogle(
    );
    state = false;
    res.fold(
      (l) => showSnackBar(context, l.message),
      (r) {
        Navigator.push(context, HomeView.route());
      },
    );
  }

  Future<UserModel> getUserData(String uid) async {
    final document = await _userAPI.getUserData(uid);
    final updatedUser = UserModel.fromMap(document.data);
    return updatedUser;
  }

  void logout(BuildContext context) async {
    final res = await _authAPI.logout();
    res.fold((l) => null, (r) {
      Navigator.pushAndRemoveUntil(
        context,
        SignUpView.route(),
        (route) => false,
      );
    });
  }
}

解决方案

问题根源

Appwrite的OAuth2登录是跳转式授权流程,createOAuth2Session方法不会像邮箱登录那样同步返回session——用户会被带到浏览器完成授权,授权完成后才会回到App,此时原方法的返回值是空的,所以你拿到的session为null,无法触发跳转。而热重载后,currentUserAccount()能读取到本地已保存的session,所以能进入首页。


具体修复步骤

1. 修改AuthAPI的loginWithGoogle方法

OAuth2流程不会同步返回session,所以修改方法返回类型为FutureEitherVoid,只负责触发授权跳转:

@override
FutureEitherVoid loginWithGoogle() async {
  try {
    // 填写你的App回调URL,比如flutterapp://login-success
    await _account.createOAuth2Session(
      provider: 'google',
      success: "flutterapp://login-success",
      failure: "flutterapp://login-failure",
    );
    return right(null);
  } on AppwriteException catch (e, stackTrace) {
    return left(
      Failure(e.message ?? 'Some unexpected error occurred', stackTrace),
    );
  } catch (e, stackTrace) {
    return left(
      Failure(e.toString(), stackTrace),
    );
  }
}

同时更新IAuthAPI中的对应方法签名。

2. 配置Deep Link并处理回调

  • 在AndroidManifest.xml、Info.plist中配置Deep Link(具体配置参考Appwrite官方文档的Flutter OAuth部分),确保授权完成后能跳转回App。
  • 在main.dart中监听Deep Link,触发状态刷新:
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initAppwrite();
  
  // 处理初始Deep Link
  final initialLink = await getInitialLink();
  if (initialLink != null) {
    handleDeepLink(initialLink);
  }
  // 监听后续Deep Link
  linkStream.listen((link) {
    handleDeepLink(link);
  });
  
  runApp(const ProviderScope(child: MyApp()));
}

void handleDeepLink(Uri link) {
  if (link.path == '/login-success') {
    // 授权成功,强制刷新用户状态
    final container = ProviderScope.containerOf(navigatorKey.currentContext!);
    container.refresh(currentUserAccountProvider);
  } else if (link.path == '/login-failure') {
    showSnackBar(navigatorKey.currentContext!, 'Google登录失败');
  }
}

3. 修改AuthController的loginWithGoogle方法

不再依赖返回的session跳转,等待Deep Link触发后自动刷新状态:

void loginWithGoogle({
  required BuildContext context,
}) async {
  state = true;
  final res = await _authAPI.loginWithGoogle();
  state = false;
  res.fold(
    (l) => showSnackBar(context, l.message),
    (r) => null, // 不再直接跳转,等待状态刷新
  );
}

4. 优化路由守卫

让根路由监听用户状态变化,自动跳转页面:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      home: Consumer(
        builder: (context, ref, child) {
          final userAsync = ref.watch(currentUserAccountProvider);
          return userAsync.when(
            data: (user) => user != null ? const HomeView() : const LoginView(),
            loading: () => const CircularProgressIndicator(),
            error: (_, __) => const LoginView(),
          );
        },
      ),
    );
  }
}

5. 补充用户数据创建逻辑

首次通过Google登录时,Appwrite会自动创建用户,但你的应用需要手动创建对应的用户数据。可以在currentUserDetailsProvider中添加判断:

final currentUserDetailsProvider = FutureProvider((ref) async {
  final user = ref.watch(currentUserAccountProvider).value;
  if (user == null) return null;
  
  final userDetailsAsync = ref.watch(userDetailsProvider(user.$id));
  final userDetails = userDetailsAsync.value;
  
  // 如果用户数据不存在,创建默认数据
  if (userDetails == null) {
    final authController = ref.watch(authControllerProvider.notifier);
    final defaultUser = UserModel(
      email: user.email ?? '',
      name: user.name ?? '',
      followers: const [],
      following: const [],
      profilePic: user.prefs.getString('profilePic') ?? '',
      bannerPic: '',
      uid: user.$id,
      bio: '',
      isTwitterBlue: false,
    );
    await ref.watch(userAPIProvider).saveUserData(defaultUser);
    return defaultUser;
  }
  return userDetails;
});

内容的提问来源于stack exchange,提问作者Leon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 11:25:05