Multiple Accounts / 多账号
First: which kind do you need? / 先分清你需要哪一种
“Multiple accounts” means two very different things, and they do not need the same solution:
「多账号」其实指两种截然不同的需求,它们的解法不一样:
| Shape | Description | Recommendation |
|---|---|---|
| Switching / 账号切换 | Several accounts are remembered, but only one is used at a time (like Gmail’s account picker) / 记住多个账号,但同时只用一个 | Plain AuthManager: logout() then login(). No new API. / 普通 AuthManager:登出再登录,不需要新 API |
| Concurrent / 账号并存 | Several accounts are signed in at the same time and all can make requests / 多个账号同时登录,且都能发请求 | AuthManagerGroup — one AuthManager per account |
Most apps only need the first. Reach for AuthManagerGroup only when an account
must keep working while another one is in the foreground.
多数应用只需要第一种。只有当「某个账号在后台也要继续工作」时,才需要 AuthManagerGroup。
Shape 1: switching / 形态一:切换
AuthManager models one session. Switching accounts is just logging out and back
in — the state machine stays honest, and there is no second source of truth:
// Keep your own list of saved account identifiers (email, phone, …).
// 自己维护已保存账号标识(邮箱、手机号等)的列表。
final savedAccounts = <String>['alice@example.com', 'bob@example.com'];
Future<void> switchTo(String account, String password) async {
if (auth.current.isAuthenticated) await auth.logout();
await auth.login(Credentials(username: account, password: password));
}Because username is just an opaque identifier, this works for email or phone
logins with no extra plumbing — see
Backend Strategy for what the contract fixes.
Shape 2: concurrent / 形态二:并存
AuthManagerGroup owns one AuthManager per account and tracks which is active.
final group = AuthManagerGroup(
// Both factories receive the account id; sharing one strategy instance is fine.
// 两个工厂都会收到账号 id;共用一个策略实例也没问题。
strategyFactory: (accountId) => MyAuthStrategy(),
// CRITICAL: one store per account, so persisted sessions stay isolated.
// 关键:每个账号一个存储,持久化会话才不会互相覆盖。
storeFactory: (accountId) => SecureTokenStore(key: 'auth_$accountId'),
// Optional: every AuthManager knob is forwarded to the managers it creates.
// 可选:所有 AuthManager 调参都会转发给它创建的管理器。
autoRefreshAhead: const Duration(minutes: 5),
clockSkew: const Duration(seconds: 30),
onStateChanged: (accountId, state) => debugPrint('$accountId → $state'),
// Or build them yourself / 也可自行构建:
// managerFactory: (id, strategy, store) => AuthManager(...),
);
// Sign in (or restore) each account independently.
// 各账号独立登录(或恢复)。
await group.forAccount('alice').login(
Credentials(username: 'alice@example.com', password: pw),
);
await group.forAccount('bob').login(
Credentials(username: 'bob@example.com', password: pw),
);
// Pick the active one — nothing is signed out by this call.
// 选择激活账号 —— 此调用不会登出任何账号。
group.switchTo('bob');Reading the active account / 读取激活账号
The group mirrors the active account and implements AuthTokenSource, so
interceptors keep depending on the narrow interface:
group.current // active account's AuthState / 激活账号的状态
group.currentSession // active account's session / 激活账号的会话
group.accessToken // active account's token / 激活账号的令牌
group.state // stream that follows the active account / 跟随激活账号的流
group.activeIdChanges // stream of the active account id / 激活账号 id 的流
group.accountIds // Iterable<String> the group currently owns / 分组当前持有的账号 id
group.activeId // String? — null when no account is active / 无激活账号时为 null
group.active // AuthManager? — null when no account is active / 激活账号的管理器
// leeway defaults to the manager's clockSkew / leeway 默认取管理器的 clockSkew
await group.validAccessToken(leeway: const Duration(seconds: 10));
dio.interceptors.add(RefreshingAuthInterceptor(group)); // renews then attaches / 先续期再附加Restoring on startup / 启动时恢复
// `knownIds` comes from your own saved-account list.
// `knownIds` 来自你自己保存的账号列表。
await group.restoreAll(knownIds, activeId: lastUsedId);
// Restore only the known ones and release everything else.
// 只恢复已知账号,并释放其余所有管理器。
await group.restoreAll(knownIds, activeId: lastUsedId, dropOthers: true);One account failing to restore does not abandon the rest: every account is attempted and the first error is thrown at the end.
某个账号恢复失败不会连累其余账号:所有账号都会被尝试,最后统一抛出第一个错误。
dropOthers: true disposes the managers of every id that is not in the list
you pass — use it when the saved-account list shrank while the app was running.
dropOthers: true 会释放不在你传入列表中的所有 id 的管理器 —— 当应用运行期间
已保存账号列表变短时使用。
Adding accounts explicitly / 显式新增账号
forAccount creates on first use; addAccount is the same call with a name that
reads better when you mean “register an account here”:
forAccount 首次使用时创建;addAccount 是同一个调用,只是名字在「这里注册账号」的
语境下更好读:
final alice = group.addAccount('alice');Removing an account / 移除账号
await group.remove('alice'); // logs out, disposes, forgets itSigning everything out / 全部登出
await group.logoutAll(); // every account, in turnIf the removed account was active, the group becomes inactive and emits
Unauthenticated.
Teardown / 释放
await group.disposeAll(); // disposes every manager and closes the streamWhy the core stays single-session / 为什么核心仍是单会话
AuthManager answers a singular question: who is logged in? Multi-account asks
a different one: of these signed-in identities, which is active?
Folding the second into the first would mean a Map of sessions inside every
state, a Refreshing that needs an account id, and a breaking change for every
existing user. Keeping AuthManager single-session and adding an opt-in
coordinator gives you both without muddying either.
把第二个问题塞进第一个,会导致每个状态里都带一个会话 Map、Refreshing 需要携带账号
id,并且对所有现有用户构成破坏性变更。让 AuthManager 保持单会话、再提供一个可选的
协调层,两者都能得到,且互不污染。
Gotchas / 注意点
- Never share one
TokenStorebetween accounts — the secondsave()would overwrite the first session. / 绝不要在多个账号间共用一个TokenStore,第二次save()会覆盖第一个会话。 - Switching does not sign anyone out; all managers keep refreshing in the background. / 切换不会登出任何账号,所有管理器仍会在后台续期。
remove()also disposes that manager — do not use it afterwards. /remove()会同时释放该管理器,之后不要再使用它。- Always release accounts through
remove(id)/disposeAll(). A manager you dispose yourself is forgotten by the group — including when it was the active one, which setsactiveIdback tonull. / 始终通过remove(id)/disposeAll()释放账号。你自己 dispose 的管理器会被 分组遗忘 —— 若它正是激活账号,activeId会被置回null。 - After
disposeAll()the group rejects further use withAuthException(code: 'group_disposed'). /disposeAll()之后,分组会以AuthException(code: 'group_disposed')拒绝继续使用。
Next Steps / 下一步
- Auth State Machine — what each manager emits / 各管理器会发出什么
- Token Store — keying persisted sessions per account / 按账号隔离持久化会话
- Backend Strategy — what the contract fixes / 契约固定了什么