This codelab demonstrates how to implement the RELID Initialization flow using the rdna_client Flutter plugin. The RELID SDK provides secure identity verification and session management for mobile applications.
rdna_client plugin added to your projectThe code to get started is stored in a GitHub repository.
You can clone the repository using the following command:
git clone https://github.com/uniken-public/codelab-flutter.git
Navigate to the relid-initialize folder in the repository you cloned earlier
Before implementing your own RELID initialization, let's examine the sample app structure to understand the recommended architecture:
Component | Purpose | Sample App Reference |
Connection Profile | Configuration data |
|
Profile Parser | Utility to parse connection data |
|
Event Manager | Handles SDK callbacks |
|
RELID Service | Main SDK interface |
|
Event Provider | Global event handling |
|
UI Components | User interface screens |
|
Create the following directory structure in your Flutter project:
lib/
├── main.dart
├── uniken/
│ ├── cp/
│ │ └── agent_info.json
│ ├── services/
│ │ ├── rdna_event_manager.dart
│ │ └── rdna_service.dart
│ ├── utils/
│ │ ├── connection_profile_parser.dart
│ │ └── progress_helper.dart
│ └── providers/
│ └── sdk_event_provider.dart
├── tutorial/
│ ├── navigation/
│ │ └── app_router.dart
│ └── screens/
│ └── tutorial/
│ ├── tutorial_home_screen.dart
│ ├── tutorial_success_screen.dart
│ └── tutorial_error_screen.dart
assets/
Ensure you have Flutter installed:
flutter doctor
Add the SDK plugin to your pubspec.yaml:
For local package:
dependencies:
flutter:
sdk: flutter
rdna_client:
path: rdna_client
For remote package:
dependencies:
flutter:
sdk: flutter
rdna_client: ^1.0.0
flutter pub get
Follow the Flutter platform setup guide for platform-specific configuration.
Flutter plugins provide platform-specific functionality through Dart APIs.
Plugin Structure:
import 'package:rdna_client/rdna_client.dart';
// Create instance
final rdnaClient = RdnaClient();
// Initialize
await rdnaClient.initialize(config);
// Use methods
final result = await rdnaClient.someMethod();
Organize your Flutter project:
lib/
├── main.dart # App entry point
├── uniken/
│ ├── services/ # SDK service wrappers
│ │ ├── rdna_service.dart
│ │ └── rdna_event_manager.dart
│ ├── screens/ # UI screens
│ │ └── tutorial_home_screen.dart
│ ├── widgets/ # Reusable widgets
│ └── utils/ # Utilities
│ └── connection_profile_parser.dart
assets/
└── agent_info.json # Asset files
Create your connection profile JSON file in lib/uniken/cp/agent_info.json:
{
"RelIds": [
{
"Name": "YourRELIDAgentName",
"RelId": "your-rel-id-string-here"
}
],
"Profiles": [
{
"Name": "YourRELIDAgentName",
"Host": "your-gateway-host.com",
"Port": "443"
}
]
}
Add the asset to your pubspec.yaml:
flutter:
assets:
- lib/uniken/cp/agent_info.json
Define the Dart classes for type safety:
// lib/uniken/utils/connection_profile_parser.dart
import 'dart:convert';
import 'package:flutter/services.dart';
/// Represents a single REL-ID entry from the connection profile
class RelId {
final String name;
final String relId;
RelId({required this.name, required this.relId});
factory RelId.fromJson(Map<String, dynamic> json) {
return RelId(
name: json['Name'] as String,
relId: json['RelId'] as String,
);
}
}
/// Represents a single Profile entry with connection details
class Profile {
final String name;
final String host;
final dynamic port; // Can be String or int from JSON
Profile({required this.name, required this.host, required this.port});
factory Profile.fromJson(Map<String, dynamic> json) {
return Profile(
name: json['Name'] as String,
host: json['Host'] as String,
port: json['Port'], // Accept both String and int
);
}
}
/// Complete agent info structure from JSON file
class AgentInfo {
final List<RelId> relIds;
final List<Profile> profiles;
AgentInfo({required this.relIds, required this.profiles});
factory AgentInfo.fromJson(Map<String, dynamic> json) {
return AgentInfo(
relIds: (json['RelIds'] as List)
.map((e) => RelId.fromJson(e as Map<String, dynamic>))
.toList(),
profiles: (json['Profiles'] as List)
.map((e) => Profile.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
}
/// Parsed connection profile with extracted values
class ParsedAgentInfo {
final String relId;
final String host;
final int port;
ParsedAgentInfo({
required this.relId,
required this.host,
required this.port,
});
}
/// Parses agent info structure and extracts connection details
ParsedAgentInfo parseAgentInfo(AgentInfo profileData) {
if (profileData.relIds.isEmpty) {
throw Exception('No RelIds found in agent info');
}
if (profileData.profiles.isEmpty) {
throw Exception('No Profiles found in agent info');
}
// Always pick the first array objects
final firstRelId = profileData.relIds[0];
if (firstRelId.name.isEmpty || firstRelId.relId.isEmpty) {
throw Exception('Invalid RelId object - missing Name or RelId');
}
// Find matching profile by Name (1-1 mapping)
final matchingProfile = profileData.profiles.firstWhere(
(profile) => profile.name == firstRelId.name,
orElse: () => throw Exception(
'No matching profile found for RelId name: ${firstRelId.name}'),
);
if (matchingProfile.host.isEmpty || matchingProfile.port == null) {
throw Exception('Invalid Profile object - missing Host or Port');
}
// Convert port to int if it's a String
int port;
if (matchingProfile.port is String) {
port = int.tryParse(matchingProfile.port as String) ?? -1;
if (port == -1) {
throw Exception('Invalid port value: ${matchingProfile.port}');
}
} else {
port = matchingProfile.port as int;
}
return ParsedAgentInfo(
relId: firstRelId.relId,
host: matchingProfile.host,
port: port,
);
}
/// Loads and parses the agent_info.json file from assets
Future<ParsedAgentInfo> loadAgentInfo() async {
try {
final jsonString =
await rootBundle.loadString('lib/uniken/cp/agent_info.json');
final jsonData = json.decode(jsonString) as Map<String, dynamic>;
final profileData = AgentInfo.fromJson(jsonData);
return parseAgentInfo(profileData);
} catch (error) {
throw Exception('Failed to load agent info: $error');
}
}
The parser performs several critical functions:
rootBundle for asset loadingThe event manager handles all RELID SDK callbacks using a singleton pattern:
// lib/uniken/services/rdna_event_manager.dart
import 'package:eventify/eventify.dart';
import 'package:rdna_client/rdna_client.dart';
import 'package:rdna_client/rdna_struct.dart';
/// Type definitions for event callbacks
typedef RDNAInitializeProgressCallback = void Function(RDNAInitProgressStatus);
typedef RDNAInitializeErrorCallback = void Function(RDNAInitializeError);
typedef RDNAInitializeSuccessCallback = void Function(RDNAInitialized);
/// REL-ID SDK Event Manager
class RdnaEventManager {
static RdnaEventManager? _instance;
final RdnaClient _rdnaClient;
final List<Listener?> _listeners = [];
// Composite event handlers (can handle multiple concerns)
RDNAInitializeProgressCallback? _initializeProgressHandler;
RDNAInitializeErrorCallback? _initializeErrorHandler;
RDNAInitializeSuccessCallback? _initializedHandler;
RdnaEventManager._(this._rdnaClient) {
_registerEventListeners();
}
/// Gets the singleton instance of RdnaEventManager
static RdnaEventManager getInstance(RdnaClient rdnaClient) {
_instance ??= RdnaEventManager._(rdnaClient);
return _instance!;
}
/// Registers native event listeners for all SDK events
void _registerEventListeners() {
print('RdnaEventManager - Registering native event listeners');
_listeners.add(
_rdnaClient.on(RdnaClient.onInitializeProgress, _onInitializeProgress),
);
_listeners.add(
_rdnaClient.on(RdnaClient.onInitializeError, _onInitializeError),
);
_listeners.add(
_rdnaClient.on(RdnaClient.onInitialized, _onInitialized),
);
print('RdnaEventManager - Native event listeners registered');
}
/// Handles SDK initialization progress events
void _onInitializeProgress(dynamic progressData) {
print('RdnaEventManager - Initialize progress event received');
final data = progressData as RDNAInitProgressStatus;
print('RdnaEventManager - Progress: ${data.initializeStatus}');
if (_initializeProgressHandler != null) {
_initializeProgressHandler!(data);
}
}
/// Handles SDK initialization error events
void _onInitializeError(dynamic errorData) {
print('RdnaEventManager - Initialize error event received');
final data = errorData as RDNAInitializeError;
print('RdnaEventManager - Initialize error: ${data.errorString}');
if (_initializeErrorHandler != null) {
_initializeErrorHandler!(data);
}
}
/// Handles SDK initialization success events
void _onInitialized(dynamic initializedData) {
print('RdnaEventManager - Initialize success event received');
final data = initializedData as RDNAInitialized;
print('RdnaEventManager - Successfully initialized, Session ID: ${data.session?.sessionId}');
if (_initializedHandler != null) {
_initializedHandler!(data);
}
}
/// Sets event handlers for SDK events. Only one handler per event type.
/// Sets the handler for initialization progress events
void setInitializeProgressHandler(RDNAInitializeProgressCallback? callback) {
_initializeProgressHandler = callback;
}
/// Sets the handler for initialization error events
void setInitializeErrorHandler(RDNAInitializeErrorCallback? callback) {
_initializeErrorHandler = callback;
}
/// Sets the handler for initialization success events
void setInitializedHandler(RDNAInitializeSuccessCallback? callback) {
_initializedHandler = callback;
}
/// Cleans up all event listeners and handlers
void cleanup() {
print('RdnaEventManager - Cleaning up event listeners and handlers');
// Remove native event listeners
for (final listener in _listeners) {
if (listener != null) {
_rdnaClient.off(listener);
}
}
_listeners.clear();
// Clear all event handlers
_initializeProgressHandler = null;
_initializeErrorHandler = null;
_initializedHandler = null;
print('RdnaEventManager - Cleanup completed');
}
}
Key features of the event manager:
setInitializeProgressHandler(), setInitializeErrorHandler(), etc. to register callbacksThe RELID service provides the main interface for SDK operations:
// lib/uniken/services/rdna_service.dart
import 'package:rdna_client/rdna_client.dart';
import 'package:rdna_client/rdna_struct.dart';
import '../utils/connection_profile_parser.dart';
import 'rdna_event_manager.dart';
/// REL-ID SDK Service
class RdnaService {
static RdnaService? _instance;
final RdnaClient _rdnaClient;
final RdnaEventManager _eventManager;
RdnaService._(this._rdnaClient, this._eventManager);
/// Gets the singleton instance of RdnaService
static RdnaService getInstance() {
if (_instance == null) {
final rdnaClient = RdnaClient();
final eventManager = RdnaEventManager.getInstance(rdnaClient);
_instance = RdnaService._(rdnaClient, eventManager);
}
return _instance!;
}
/// Cleans up the service and event manager
void cleanup() {
print('RdnaService - Cleaning up service');
_eventManager.cleanup();
}
/// Gets the event manager instance for external callback setup
RdnaEventManager getEventManager() {
return _eventManager;
}
/// Gets the version of the REL-ID SDK
Future<String> getSDKVersion() async {
print('RdnaService - Requesting SDK version');
final response = await _rdnaClient.getSDKVersion();
final version = response.response ?? 'Unknown';
print('RdnaService - SDK Version: $version');
return version;
}
/// Initializes the REL-ID SDK
Future<RDNASyncResponse> initialize() async {
final profile = await loadAgentInfo();
print('RdnaService - Loaded connection profile:');
print(' Host: ${profile.host}');
print(' Port: ${profile.port}');
print(' RelId: ${profile.relId.substring(0, 10)}...');
print('RdnaService - Starting initialization');
final response = await _rdnaClient.initialize(
profile.relId, // agentInfo: RelId from connection profile
profile.host, // gatewayHost: Server hostname from connection profile
profile.port, // gatewayPort: Server port from connection profile
'', // cipherSpec: Empty for default
'', // cipherSalt: Empty for default
null, // proxySettings: null for no proxy
null, // sslCertificate: null for default
RDNALoggingLevel.RDNA_NO_LOGS, // logLevel: No logs for production
);
print('RdnaService - Initialize sync response received');
print('RdnaService - Sync response:');
print(' Long Error Code: ${response.error?.longErrorCode}');
print(' Short Error Code: ${response.error?.shortErrorCode}');
return response;
}
}
The rdnaClient.initialize() call requires specific parameter ordering:
Parameter | Purpose | Example |
| RelId | From connection profile Ex. {"Name": "YourRELIDAgentName","RelId": "your-rel-id-string-here"} |
| Server hostname | From connection profile Ex. {"Host": "your-gateway-host.com","Port": "443"} |
| Server port | From connection profile Ex. {"Host": "your-gateway-host.com","Port": "443"} |
| Logging setting |
|
Create a screen widget that handles user interaction and displays progress:
// lib/tutorial/screens/tutorial/tutorial_home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:rdna_client/rdna_struct.dart';
import '../../../uniken/services/rdna_service.dart';
import '../../../uniken/utils/progress_helper.dart';
class TutorialHomeScreen extends ConsumerStatefulWidget {
const TutorialHomeScreen({super.key});
@override
ConsumerState<TutorialHomeScreen> createState() => _TutorialHomeScreenState();
}
class _TutorialHomeScreenState extends ConsumerState<TutorialHomeScreen> {
String _sdkVersion = 'Loading...';
bool _isInitializing = false;
String _progressMessage = '';
@override
void initState() {
super.initState();
_loadSDKVersion();
_setupEventHandlers();
}
@override
void dispose() {
// Cleanup - reset handlers
final rdnaService = RdnaService.getInstance();
final eventManager = rdnaService.getEventManager();
eventManager.setInitializeProgressHandler(null);
eventManager.setInitializeErrorHandler(null);
super.dispose();
}
/// Load SDK version on screen init
Future<void> _loadSDKVersion() async {
try {
final rdnaService = RdnaService.getInstance();
final version = await rdnaService.getSDKVersion();
if (mounted) {
setState(() {
_sdkVersion = version;
});
}
} catch (error) {
print('TutorialHomeScreen - Failed to load SDK version: $error');
if (mounted) {
setState(() {
_sdkVersion = 'Unknown';
});
}
}
}
void _setupEventHandlers() {
final rdnaService = RdnaService.getInstance();
final eventManager = rdnaService.getEventManager();
// Register error handler directly in TutorialHomeScreen
eventManager.setInitializeErrorHandler((RDNAInitializeError errorData) {
print('TutorialHomeScreen - Received initialize error: ${errorData.errorString}');
// Update local state
setState(() {
_isInitializing = false;
_progressMessage = '';
});
// Navigate to error screen with the error details
if (mounted) {
context.goNamed('tutorialErrorScreen', extra: errorData);
}
});
}
Future<void> _handleInitializePress() async {
if (_isInitializing) return;
setState(() {
_isInitializing = true;
_progressMessage = 'Starting RDNA initialization...';
});
print('TutorialHomeScreen - User clicked Initialize - Starting RDNA...');
// Register progress handler directly with the event manager
final rdnaService = RdnaService.getInstance();
final eventManager = rdnaService.getEventManager();
eventManager.setInitializeProgressHandler((RDNAInitProgressStatus data) {
print('TutorialHomeScreen - Progress update: ${data.initializeStatus}');
final message = getProgressMessage(data);
if (mounted) {
setState(() {
_progressMessage = message;
});
}
});
// Call rdnaService.initialize()
final syncResponse = await rdnaService.initialize();
print('TutorialHomeScreen - RDNA initialization sync response received');
print('TutorialHomeScreen - Sync response:');
print(' Long Error Code: ${syncResponse.error!.longErrorCode}');
print(' Short Error Code: ${syncResponse.error!.shortErrorCode}');
// Check sync response - error is always present, check longErrorCode
if (syncResponse.error!.longErrorCode != 0) {
// Sync error - show dialog immediately
print('TutorialHomeScreen - Sync error: ${syncResponse.error!.errorString}');
if (mounted) {
setState(() {
_isInitializing = false;
_progressMessage = '';
});
_showErrorDialog(syncResponse.error!);
}
} else {
// Sync success (longErrorCode == 0) - async events will handle next steps
print('TutorialHomeScreen - Sync success, waiting for async events...');
}
}
/// Show sync error dialog
void _showErrorDialog(RDNAError error) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Initialization Failed'),
content: Text(
'${error.errorString}\n\n'
'Error Codes:\n'
'Long: ${error.longErrorCode}\n'
'Short: ${error.shortErrorCode}',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
// Build method includes SDK version display, initialize button, and progress display
return Scaffold(
body: SafeArea(
child: Column(
children: [
// Display SDK version, initialize button, progress message
Text('SDK Version: $_sdkVersion'),
ElevatedButton(
onPressed: _isInitializing ? null : _handleInitializePress,
child: Text(_isInitializing ? 'Initializing...' : 'Initialize'),
),
if (_isInitializing)
Text(_progressMessage),
],
),
),
);
}
}
The sample app uses a centralized approach for handling the onInitialized success event through SDKEventProvider:
// lib/uniken/providers/sdk_event_provider.dart
class SDKEventProviderWidget extends ConsumerStatefulWidget {
final Widget child;
const SDKEventProviderWidget({super.key, required this.child});
@override
ConsumerState<SDKEventProviderWidget> createState() => _SDKEventProviderWidgetState();
}
class _SDKEventProviderWidgetState extends ConsumerState<SDKEventProviderWidget> {
@override
void initState() {
super.initState();
_setupSDKEventHandlers();
}
void _setupSDKEventHandlers() {
final rdnaService = RdnaService.getInstance();
final eventManager = rdnaService.getEventManager();
// Set up event handlers once on mount
eventManager.setInitializedHandler(_handleInitialized);
}
/// Event handler for successful initialization
void _handleInitialized(RDNAInitialized data) {
print('SDKEventProvider - Successfully initialized, Session ID: ${data.session?.sessionId}');
appRouter.goNamed('tutorialSuccessScreen', extra: data);
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
This provider should wrap your app's root to ensure the onInitialized event is handled globally.
The sample app demonstrates several important patterns:
Async/Await Pattern:
Error Handling:
longErrorCode to determine sync successProgress Tracking:
Event-Driven Architecture:
The following images showcase screens from the sample application:
|
|
|
# Run on connected device
flutter run
# Run on iOS simulator
flutter run -d ios
# Run on Android emulator
flutter run -d android
# List available devices
flutter devices
Flutter supports hot reload for rapid development:
r in terminal for hot reloadR for hot restartVS Code: Use the built-in debugger with breakpoints Android Studio: Use the Flutter Inspector DevTools: Run flutter pub global activate devtools then devtools
Test your implementation with below APIs:
// Test SDK version retrieval
final version = await rdnaService.getSDKVersion();
print('SDK Version: $version');
// Test initialization
final syncResponse = await rdnaService.initialize();
if (syncResponse.error?.longErrorCode == 0) {
print('Initialization successful');
} else {
print('Initialization failed: ${syncResponse.error?.errorString}');
}
The sample app includes comprehensive testing patterns in the tutorial screens:
Error: "No RelIds found in agent info" Solution: Verify your JSON structure matches the sample in lib/uniken/cp/agent_info.json
Error: "No matching profile found for RelId name" Solution: Ensure the Name field in RelIds matches the Name field in Profiles
"MissingPluginException"
flutter clean && flutter pub get, then rebuild"Asset not found"
flutter.assets in pubspec.yaml"Null check operator used on null value"
Hot reload not working
flutter runiOS build fails
cd ios && pod install --repo-updateAndroid build fails
flutter clean and rebuildPlugin version conflicts
pubspec.lock, run flutter pub upgradeError: Network connection failures Solution: Check host/port values and network accessibility
Error: REL-ID connection issues Solution: Verify the REL-ID server setup and running
Error Code 88: SDK already initialized Solution: Terminate the sdk
Error Code 288: SDK detected dynamic attack performed on the app Solution: Terminate the app
Error Code 179: Initialization in progress Solution: Wait for current initialization to complete before retrying
RDNALoggingLevel.RDNA_NO_LOGS in productionrelId.substring(0, 10) + '...')flutter build --obfuscateconst constructors where possibledispose() if neededCongratulations! You've successfully learned how to implement RELID SDK initialization with: