能否用BottomSheet替代弹窗展示Google账号?求Flutter/原生安卓iOS方案
Flutter 实现方案
要替换默认的Google账号选择弹窗为BottomSheet,核心是自行获取已关联的Google账号列表,再自定义BottomSheet组件完成选择逻辑,步骤如下:
依赖配置
确保项目中引入相关依赖,在pubspec.yaml中添加:dependencies: google_sign_in: ^6.2.1 firebase_auth: ^4.16.0获取已登录账号列表
调用GoogleSignIn的accounts属性获取本地已关联的Google账号:final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: ['email']); List<GoogleSignInAccount>? _accounts; Future<void> _fetchAccounts() async { _accounts = await _googleSignIn.signInSilently().then((_) => _googleSignIn.accounts); }自定义BottomSheet展示账号
用showModalBottomSheet弹出自定义列表,用户选择后触发登录:void _showAccountBottomSheet() async { await _fetchAccounts(); if (_accounts?.isEmpty ?? true) { // 无账号时直接唤起系统登录弹窗 await _googleSignIn.signIn(); return; } showModalBottomSheet( context: context, builder: (context) => Container( padding: EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('选择Google账号', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), SizedBox(height: 16), ..._accounts!.map((account) => ListTile( leading: CircleAvatar(backgroundImage: NetworkImage(account.photoUrl ?? '')), title: Text(account.displayName ?? ''), subtitle: Text(account.email), onTap: () async { Navigator.pop(context); // 使用选中账号登录 final GoogleSignInAuthentication auth = await account.authentication; // 若需Firebase验证,继续以下步骤 // final OAuthCredential credential = GoogleAuthProvider.credential(accessToken: auth.accessToken, idToken: auth.idToken); // await FirebaseAuth.instance.signInWithCredential(credential); }, )), SizedBox(height: 8), TextButton( child: Text('添加账号'), onPressed: () async { Navigator.pop(context); await _googleSignIn.signIn(); }, ) ], ), ), ); }注意事项
- 需要监听账号变化(比如用户新增/移除账号),可以通过
_googleSignIn.onCurrentUserChanged流更新列表 - 首次无账号时,还是需要唤起系统登录弹窗完成账号关联
- 需要监听账号变化(比如用户新增/移除账号),可以通过
原生Android 实现方案
Android中默认的Google SignIn弹窗是系统级的,要自定义BottomSheet需手动获取账号并实现选择逻辑:
配置Google SignIn
在build.gradle中添加依赖:implementation 'com.google.android.gms:play-services-auth:20.7.0'获取已关联账号
通过GoogleSignInClient或AccountManager获取本地Google账号:private val googleSignInClient: GoogleSignInClient by lazy { GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestEmail() .build() .let { GoogleSignIn.getClient(context, it) } } private fun fetchAccounts(): List<GoogleSignInAccount>? { return GoogleSignIn.getLastSignedInAccount(context)?.let { // 获取所有已登录账号(需权限GET_ACCOUNTS) AccountManager.get(context).getAccountsByType("com.google") .mapNotNull { acc -> GoogleSignIn.getAccountForEmail(context, acc.name) } } }自定义BottomSheetDialogFragment
创建底部弹窗展示账号列表:class GoogleAccountBottomSheet : BottomSheetDialogFragment() { private lateinit var accounts: List<GoogleSignInAccount> override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { accounts = arguments?.getParcelableArrayList("ACCOUNTS") ?: emptyList() return inflater.inflate(R.layout.bottom_sheet_accounts, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val recyclerView = view.findViewById<RecyclerView>(R.id.account_recycler) recyclerView.adapter = AccountAdapter(accounts) { account -> dismiss() // 使用选中账号登录 googleSignInClient.signInWithAccount(requireContext(), account) .addOnCompleteListener { task -> if (task.isSuccessful) { // 登录成功,处理用户信息 val user = task.result?.signInAccount } } } } companion object { fun newInstance(accounts: List<GoogleSignInAccount>) = GoogleAccountBottomSheet().apply { arguments = Bundle().apply { putParcelableArrayList("ACCOUNTS", ArrayList(accounts)) } } } } // 适配器示例 class AccountAdapter(private val accounts: List<GoogleSignInAccount>, private val onSelect: (GoogleSignInAccount) -> Unit) : RecyclerView.Adapter<AccountAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_account, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val account = accounts[position] holder.name.text = account.displayName holder.email.text = account.email Glide.with(holder.itemView).load(account.photoUrl).into(holder.avatar) holder.itemView.setOnClickListener { onSelect(account) } } override fun getItemCount() = accounts.size class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val avatar: ImageView = itemView.findViewById(R.id.account_avatar) val name: TextView = itemView.findViewById(R.id.account_name) val email: TextView = itemView.findViewById(R.id.account_email) } }唤起BottomSheet
在登录按钮点击时判断是否有账号,有则弹出BottomSheet,无则唤起系统登录:fun onLoginClick() { val accounts = fetchAccounts() if (accounts.isNullOrEmpty()) { startActivityForResult(googleSignInClient.signInIntent, RC_SIGN_IN) } else { GoogleAccountBottomSheet.newInstance(accounts).show(parentFragmentManager, "AccountBottomSheet") } }
原生iOS 实现方案
iOS的GIDSignIn默认弹窗也是系统级的,自定义BottomSheet需要手动获取账号并设置模态弹窗样式:
配置依赖
在Podfile中添加:pod 'GoogleSignIn', '~> 7.0'获取已登录账号
通过GIDSignIn获取已登录用户,或查询Keychain获取历史账号:import GoogleSignIn func fetchAccounts() -> [GIDGoogleUser] { var accounts = [GIDGoogleUser]() // 获取当前登录用户 if let currentUser = GIDSignIn.sharedInstance.currentUser { accounts.append(currentUser) } // 可通过Keychain查询历史关联账号(需额外处理) return accounts }自定义BottomSheet控制器
创建UIViewController作为底部弹窗,设置UISheetPresentationController:class GoogleAccountSheetViewController: UIViewController { private var accounts: [GIDGoogleUser] private var onAccountSelected: ((GIDGoogleUser) -> Void)? init(accounts: [GIDGoogleUser], onSelected: @escaping (GIDGoogleUser) -> Void) { self.accounts = accounts self.onAccountSelected = onSelected super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground // 创建列表 let tableView = UITableView(frame: view.bounds) tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] tableView.dataSource = self tableView.delegate = self view.addSubview(tableView) // 添加"添加账号"按钮 let addButton = UIButton(type: .system) addButton.setTitle("添加账号", for: .normal) addButton.addTarget(self, action: #selector(addAccountTapped), for: .touchUpInside) addButton.frame = CGRect(x: 0, y: view.bounds.height - 60, width: view.bounds.width, height: 50) view.addSubview(addButton) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // 设置底部弹窗样式 if let sheet = presentationController as? UISheetPresentationController { sheet.detents = [.medium(), .large()] sheet.prefersGrabberVisible = true } } @objc private func addAccountTapped() { dismiss(animated: true) { GIDSignIn.sharedInstance.signIn(withPresenting: self) { result, error in // 处理新账号登录结果 } } } } extension GoogleAccountSheetViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { accounts.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) let user = accounts[indexPath.row] cell.textLabel?.text = user.profile?.name cell.detailTextLabel?.text = user.profile?.email if let url = user.profile?.imageURL(withDimension: 40) { // 加载头像(需SDWebImage等库) cell.imageView?.sd_setImage(with: url) } return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let user = accounts[indexPath.row] dismiss(animated: true) { self.onAccountSelected?(user) } } }唤起BottomSheet
在登录按钮点击时触发:@IBAction func loginButtonTapped(_ sender: UIButton) { let accounts = fetchAccounts() if accounts.isEmpty { GIDSignIn.sharedInstance.signIn(withPresenting: self) { result, error in // 处理登录结果 } } else { let sheetVC = GoogleAccountSheetViewController(accounts: accounts) { user in // 使用选中账号登录(可通过user.refreshToken获取凭证) GIDSignIn.sharedInstance.signIn(withUser: user, presenting: self) { result, error in // 处理登录结果 } } sheetVC.modalPresentationStyle = .pageSheet present(sheetVC, animated: true) } }
内容的提问来源于stack exchange,提问作者zigg-zigg

