art_adk 1.0.1
art_adk: ^1.0.1 copied to clipboard
Flutter SDK for ART - A Realtime Tech communication — a real-time messaging platform offering WebSocket-based channels, presence tracking, encrypted communication, shared object channels.
example/lib/main.dart
import 'dart:convert';
import 'package:art_adk/art_adk.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Adk? adk;
BaseSubscription? activeSubscription;
final List<UIMessage> messages = <UIMessage>[];
LiveObjSubscription? get liveObjSubscription {
final subscription = activeSubscription;
if (subscription is LiveObjSubscription) {
return subscription;
}
return null;
}
List<String> interceptors = <String>[];
String interceptorInput = '';
Map<String, Offset> userPositions = <String, Offset>{};
@override
void initState() {
super.initState();
}
Future<void> connect() async {
try {
final clientCreds = await AppConfigLoader.loadCredentials();
final passcode = await AuthService.connectWithPasscode(
username: "auth_user_name",
firstName: "auth_user_first_name",
lastName: "auth_user_last_name",
credentials: clientCreds,
);
var updatedCreds = clientCreds.copyWith(accessToken: passcode);
final config = AdkConfig(
uri: 'your_server_websocket_uri',
authToken: passcode,
getCredentials: () => updatedCreds,
);
final instance = Adk(adkConfig: config);
adk = instance;
instance.on('connection', (dynamic data) {
});
instance.on('close', (dynamic _) {
});
await instance.connect();
} catch (error) {
debugPrint("connection Error $error");
}
}
Future<void> generateKeyPair() async {
try {
await adk?.generateKeyPair();
} catch (error) {
debugPrint("generateKeyPair Error $error");
}
}
Future<void> subscribe() async {
final currentAdk = adk;
if (currentAdk == null) {
return;
}
try {
final sub = await currentAdk.subscribe(channel: 'channel_name');
activeSubscription = sub;
await fetchPresence(sub);
if (sub is LiveObjSubscription) {
await listenCRDT();
} else {
}
sub.emitter.on('message', (dynamic data) {
if (data is Map<String, dynamic> && data['message'] is String) {
messages.add(
UIMessage(
id: DateTime.now().microsecondsSinceEpoch.toString(),
sender: "to_user-name",
text: data['message'] as String,
isMe: false,
timestamp: DateTime.now(),
),
);
}
});
} catch (error) {
debugPrint("subscribe error $error");
}
}
Future<void> fetchPresence(BaseSubscription sub) async {
try {
await sub.fetchPresence(
callback: (List<String> users) {
debugPrint('👥 Online users: $users');
},
);
} catch (error) {
debugPrint('❌ Presence error: $error');
}
}
Future<void> sendMessage() async {
await activeSubscription?.push(
event: 'message',
data: <String, dynamic>{'message': "your message"},
options: PushConfig(to: <String>["to_user-name"]),
);
messages.add(
UIMessage(
id: DateTime.now().microsecondsSinceEpoch.toString(),
sender: "auth_user_name",
text: "your message",
isMe: true,
timestamp: DateTime.now(),
),
);
}
Future<void> listenCRDT() async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
debugPrint('⚠️ CRDT not available - using secure/normal channel');
return;
}
final query = liveSub.query(path: 'user');
final initial = await query.execute();
debugPrint(' Initial CRDT: $initial');
await query.listen((dynamic data) {
debugPrint(' CRDT update: $data');
if (data == null) {
userPositions = <String, Offset>{};
return;
}
if (data is! Map) {
debugPrint('❌ Invalid data format: ${data.runtimeType}');
return;
}
double? extractCoordinate(dynamic value) {
if (value is num) {
return value.toDouble();
}
if (value is Map) {
final directValue = value['value'];
if (directValue is num) {
return directValue.toDouble();
}
final nestedValue = value['index'];
if (nestedValue is Map) {
final inner = nestedValue['value'];
if (inner is num) {
return inner.toDouble();
}
}
}
return null;
}
final result = <String, Offset>{};
for (final MapEntry<dynamic, dynamic> entry in data.entries) {
final username = '${entry.key}';
final userData = entry.value;
if (userData is! Map) {
continue;
}
final index = userData['index'];
if (index is! Map) {
debugPrint('⚠️ Missing index for $username');
continue;
}
final x = extractCoordinate(index['x']);
final y = extractCoordinate(index['y']);
if (x != null && y != null) {
result[username] = Offset(x, y);
debugPrint(' Parsed $username: ($x, $y)');
} else {
debugPrint('⚠️ Missing x or y for $username. index=$index');
}
}
userPositions = result;
debugPrint('📍 Updated ${result.length} user positions');
});
}
Future<void> updateMyPosition({required double x, required double y}) async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
debugPrint('⚠️ CRDT not available for this channel type');
return;
}
userPositions = <String, Offset>{
...userPositions,
"auth_user_name": Offset(x, y),
};
liveSub.state()['user']["auth_user_name"].set(<String, dynamic>{
'index': <String, dynamic>{'x': x, 'y': y},
});
await liveSub.flush();
}
Future<void> addInterceptor() async {
final name = interceptorInput.trim();
if (name.isEmpty) {
return;
}
final currentAdk = adk;
if (currentAdk == null) {
debugPrint('⚠️ Please authenticate first');
return;
}
try {
await currentAdk.intercept(
interceptor: name,
fn:
(
Map<String, dynamic> data,
void Function(dynamic data) resolve,
void Function(String error) reject,
) {
debugPrint("🔀 Intercepted on '$name': $data");
resolve(data);
},
);
interceptors = <String>[...interceptors, name];
interceptorInput = '';
debugPrint('✅ Interceptor added: $name');
} catch (error) {
debugPrint('❌ Failed to add interceptor: $error');
} finally {
}
}
void removeInterceptor({required int index}) {
if (index < 0 || index >= interceptors.length) {
return;
}
final removed = interceptors[index];
interceptors = <String>[
...interceptors.take(index),
...interceptors.skip(index + 1),
];
debugPrint('🗑 Removed interceptor: $removed');
}
Future<void> crdtArrayPush(String item) async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
return;
}
final count = liveSub.state()['items'].push(item);
await liveSub.flush();
debugPrint("➕ pushed '$item', count=$count");
}
Future<void> crdtArrayPop() async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
return;
}
final removed = liveSub.state()['items'].pop();
await liveSub.flush();
debugPrint(' popped: $removed');
}
Future<void> setCRDTTitle(String title) async {
final liveSub = liveObjSubscription;
if (liveSub == null) {
debugPrint('⚠️ Not a CRDT channel');
return;
}
liveSub.state()['document']['title'].set(title);
await liveSub.flush();
debugPrint('✏️ title set → $title');
}
Future<void> disconnect() async {
await activeSubscription?.unsubscribe();
await adk?.disconnect();
activeSubscription = null;
messages.clear();
userPositions = <String, Offset>{};
debugPrint('Disconnected');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("ADK Example")),
body: Center(
child: Column(crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
SizedBox(height: 5,),
ElevatedButton(onPressed: connect, child: Text("Connect to ADK")),
SizedBox(height: 5,),
ElevatedButton(onPressed: sendMessage, child: Text("Send Message")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: () => updateMyPosition(x:100,y: 200),
child: Text("Update My Position")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: () => setCRDTTitle('My Doc'), child: Text("Set Title")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: () =>crdtArrayPush('Hello ${DateTime.now()}'), child: Text("Array Push")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: crdtArrayPop, child: Text("Array Pop")),
SizedBox(height: 5,),
ElevatedButton(
onPressed: disconnect, child: Text("Disconnect")),
],
),
),
),
);
}
}
class ADKServicesFile {
ADKServicesFile({
required this.environment,
required this.projectKey,
required this.orgTitle,
required this.clientID,
required this.clientSecret,
});
factory ADKServicesFile.fromJson(Map<String, dynamic> json) {
return ADKServicesFile(
environment: json['Environment'] as String? ?? '',
projectKey: json['ProjectKey'] as String? ?? '',
orgTitle: json['Org-Title'] as String? ?? '',
clientID: json['Client-ID'] as String? ?? '',
clientSecret: json['Client-Secret'] as String? ?? '',
);
}
final String environment;
final String projectKey;
final String orgTitle;
final String clientID;
final String clientSecret;
}
class AppConfigLoader {
static Future<CredentialStore> loadCredentials() async {
final data = await rootBundle.loadString('assets/adk-services.json');
final file = ADKServicesFile.fromJson(
jsonDecode(data) as Map<String, dynamic>,
);
return CredentialStore(
environment: file.environment,
projectKey: file.projectKey,
orgTitle: file.orgTitle,
clientID: file.clientID,
clientSecret: file.clientSecret,
);
}
}
class PasscodeRequest {
PasscodeRequest({
required this.username,
required this.firstName,
required this.lastName,
});
final String username;
final String firstName;
final String lastName;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'username': username,
'first_name': firstName,
'last_name': lastName,
};
}
}
class AuthService {
static Future<String> connectWithPasscode({
required String username,
required String firstName,
required String lastName,
required CredentialStore credentials,
}) async {
final uri = Uri.parse(
'https://dev.arealtimetech.com/ws/v1/connect/passcode',
);
final response = await post(
uri,
headers: <String, String>{
'Client-Id': credentials.clientID,
'Client-Secret': credentials.clientSecret,
'ProjectKey': credentials.projectKey,
'Environment': credentials.environment,
'X-Org': credentials.orgTitle,
'Content-Type': 'application/json',
},
body: jsonEncode(
PasscodeRequest(
username: username,
firstName: firstName,
lastName: lastName,
).toJson(),
),
);
final responseBody = response.body;
debugPrint('Passcode Response: $responseBody');
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception(
'Passcode request failed (${response.statusCode}): $responseBody',
);
}
final decoded = jsonDecode(responseBody) as Map<String, dynamic>;
final nestedData = decoded['data'];
final passcode =
(nestedData is Map<String, dynamic> ? nestedData['passcode'] : null) ??
decoded['passcode'];
if (passcode is! String || passcode.isEmpty) {
throw Exception('Unable to parse passcode from response');
}
return passcode;
}
}
class UIMessage {
UIMessage({
required this.id,
required this.sender,
required this.text,
required this.isMe,
required this.timestamp,
});
final String id;
final String sender;
final String text;
final bool isMe;
final DateTime timestamp;
}