app_pigeon 0.1.4
app_pigeon: ^0.1.4 copied to clipboard
A simple auth-aware networking layer for Flutter apps. Built on top of Dio, socket_io_client and flutter_secure_storage.
app_pigeon #
app_pigeon is a Flutter networking and socket layer with two operation modes:
AuthorizedPigeon: token-based HTTP + auth persistence + refresh flow.GhostPigeon: lightweight HTTP + socket client for anonymous/guest/ghost use cases.
The package exposes a shared AppPigeon interface so app code can depend on one contract while switching implementations at runtime. It acts as your reusable API client foundation, instead of rebuilding the same networking layer from scratch for every new REST API project.
Capabilities #
- Typed HTTP wrappers over
dio:get,post,put,patch,delete
- Realtime socket API:
socketInit,listen,emit
- Authorized mode auth persistence:
- secure local auth storage via
flutter_secure_storage - multiple account records
- current-account switching
- auth stream updates
- secure local auth storage via
- Authorized mode auto-refresh:
- interceptor-driven refresh flow on unauthorized responses
- refresh request queueing + replay of pending requests
- Ghost mode optional bearer support:
- ghost interceptor can attach bearer if you call
setAuthToken(...)
- ghost interceptor can attach bearer if you call
Install #
Add to pubspec.yaml:
dependencies:
app_pigeon: ^<latest>
Exports #
package:app_pigeon/app_pigeon.dart exports:
AppPigeon(interface)AuthorizedPigeonGhostPigeonSocketConnetParamXRefreshTokenManagerInterface,RefreshTokenResponsediotypesflutter_secure_storagetypes
Core Types #
AppPigeon interface #
The shared contract:
Future<Response> get/post/put/patch/delete(...)Future<void> socketInit(SocketConnetParamX param)Stream<dynamic> listen(String channelName)void emit(String eventName, [dynamic data])void dispose()
SocketConnetParamX #
class SocketConnetParamX {
final String? token;
final String socketUrl;
final String joinId;
}
token is optional. In authorized mode, if token is null, current stored auth token is used.
Authorized Mode #
Use AuthorizedPigeon when your API requires authentication and token refresh.
Setup #
import 'package:app_pigeon/app_pigeon.dart';
class MyRefreshTokenManager implements RefreshTokenManagerInterface {
@override
final String url = '/auth/refresh';
@override
Future<bool> shouldRefresh(
DioException err,
ErrorInterceptorHandler handler,
) async {
return err.response?.statusCode == 401;
}
@override
Future<RefreshTokenResponse> refreshToken({
required String refreshToken,
required Dio dio,
}) async {
final res = await dio.post(
url,
data: {'refreshToken': refreshToken},
);
final data = res.data['data'] as Map<String, dynamic>;
return RefreshTokenResponse(
accessToken: data['accessToken'] as String,
refreshToken: data['refreshToken'] as String,
data: data,
);
}
}
final authorized = AuthorizedPigeon(
MyRefreshTokenManager(),
baseUrl: 'https://api.example.com',
);
Save login auth #
await authorized.saveNewAuth(
saveAuthParams: SaveNewAuthParams(
uid: userId,
accessToken: accessToken,
refreshToken: refreshToken,
data: userData,
),
);
Listen auth state #
authorized.authStream.listen((status) {
if (status is Authenticated) {
// signed in
} else if (status is UnAuthenticated) {
// signed out
}
});
Account operations #
final current = await authorized.getCurrentAuthRecord();
final all = await authorized.getAllAuthRecords();
await authorized.switchAccount(uid: 'user_2');
await authorized.logOut();
Socket in authorized mode #
await authorized.socketInit(
SocketConnetParamX(
token: null, // use stored token
socketUrl: 'https://socket.example.com',
joinId: 'global_chat',
),
);
authorized.listen('message').listen((event) {
// handle incoming message
});
authorized.emit('message', {'text': 'hello'});
Lifecycle notes #
dispose()closes auth storage stream and socket.- If you only want to stop socket and keep auth storage alive, use:
authorized.disconnectSocket()
Ghost Mode #
Use GhostPigeon for anonymous/ghost flows where full auth persistence is not needed.
Setup #
final ghost = GhostPigeon(
baseUrl: 'https://api.example.com',
);
Optional token #
ghost.setAuthToken('optional_token');
If set, ghost interceptor attaches Authorization: Bearer <token> on requests.
HTTP and socket usage #
final res = await ghost.post(
'/chat/ghost/register',
data: {'userName': 'ghostfox'},
);
await ghost.socketInit(
SocketConnetParamX(
token: null,
socketUrl: 'https://socket.example.com',
joinId: 'ghost_room',
),
);
ghost.listen('ghost_message').listen((event) {
// handle ghost messages
});
ghost.emit('ghost_message', {
'ghostId': 'ghost_ghostfox',
'text': 'hello from ghost',
});
Lifecycle notes #
dispose()closes ghost socket resources.- If you only want to explicitly stop socket listeners/connection, use:
ghost.disconnectSocket()
Dynamic Mode Switching #
Common architecture in apps:
- Keep one
AuthorizedPigeon. - Keep one
GhostPigeon. - Expose an app-level active resolver.
- Repositories call the currently active client at runtime.
This pattern lets you switch seamlessly between:
- authenticated identity
- ghost identity
without rebuilding repository objects.
Error Handling Behavior #
Authorized interceptor #
- Appends access token from current stored auth.
- On configured unauthorized errors:
- starts one refresh request
- queues pending failed requests
- updates auth storage with new tokens
- retries queued requests
- If refresh fails:
- clears current auth
- rejects queued requests
Ghost interceptor #
- Attaches optional token if set.
- Does not run refresh flow.
Recommended Backend Contract #
For best compatibility, backend should return refreshed tokens as:
{
"data": {
"accessToken": "new_access",
"refreshToken": "new_refresh"
}
}
For ghost identity flows, typical endpoints:
POST /chat/ghost/registerPOST /chat/ghost/loginPOST /chat/ghost/messagesGET /chat/ghost/messages
Minimal End-to-End Example #
final authorized = AuthorizedPigeon(
MyRefreshTokenManager(),
baseUrl: 'https://api.example.com',
);
final ghost = GhostPigeon(baseUrl: 'https://api.example.com');
// Use authorized client
await authorized.post('/auth/login', data: {...});
await authorized.saveNewAuth(
saveAuthParams: SaveNewAuthParams(
uid: 'u1',
accessToken: 'a1',
refreshToken: 'r1',
data: {'uid': 'u1'},
),
);
// Later switch to ghost flow
authorized.disconnectSocket();
final ghostSession = await ghost.post(
'/chat/ghost/register',
data: {'userName': 'ghostfox'},
);
await ghost.socketInit(
SocketConnetParamX(
token: null,
socketUrl: 'https://socket.example.com',
joinId: 'ghost_room',
),
);
Example App #
This repository includes an example/ app and example backend showing:
- authorized login/signup/refresh
- multi-account switching
- ghost identity flow
- realtime universal chat