🎯 Learning Path:
Welcome to the REL-ID Device Management codelab! This tutorial builds upon your existing MFA implementation to add comprehensive device management capabilities using REL-ID SDK's device management APIs.
In this codelab, you'll enhance your existing MFA application with:
getRegisteredDeviceDetails() APIupdateDeviceDetails() operationType 0updateDeviceDetails() operationType 1By completing this codelab, you'll master:
onGetRegistredDeviceDetails and onUpdateDeviceDetails eventsBefore starting this codelab, ensure you have:
The code to get started can be found 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-device-management folder in the repository you cloned earlier
This codelab extends your MFA application with three core device management components:
Before implementing device management screens, let's understand the key SDK events and APIs that power the device lifecycle management workflow.
The device management process follows this event-driven pattern:
User Logs In → Navigate to Device Management →
getRegisteredDeviceDetails() Called → onGetRegistredDeviceDetails Event →
Device List Displayed with Cooling Period Check →
User Taps Device → Navigate to Detail Screen →
User Renames/Deletes → updateDeviceDetails() Called →
onUpdateDeviceDetails Event → Success/Error Feedback →
Navigate Back → Device List Auto-Refreshes
The REL-ID SDK provides these APIs and events for device management:
API/Event | Type | Description | User Action Required |
API | Fetch all registered devices with cooling period info | System calls automatically | |
Event | Receives device list with metadata | System processes response | |
API | Rename or delete device with JSON payload | User taps action button | |
Event | Update operation result with status codes | System handles response |
The updateDeviceDetails() API supports two operation types via the status field in JSON payload:
Operation Type | Status Value | Description | devName Value |
Rename Device |
| Update device name | New device name string |
Delete Device |
| Remove device from account | NA or Empty string |
The onGetRegistredDeviceDetails event returns this data structure:
class RDNAStatusGetRegisteredDeviceDetails {
RDNAError? error; // API-level error (longErrorCode)
PArgs? pArgs; // Response arguments
}
class PArgs {
Response? response;
}
class Response {
int? statusCode; // 100=success, 146=cooling period
String? statusMsg; // Status message
ResponseData? responseData;
}
class ResponseData {
dynamic response; // Contains RDNAGetRegisteredDeviceDetailsResponse
}
class RDNAGetRegisteredDeviceDetailsResponse {
List<RDNADeviceDetails>? device; // Array of devices
int? deviceManagementCoolingPeriodEndTimestamp; // Cooling period end
}
class RDNADeviceDetails {
String? devUuid; // Device unique identifier
String? devName; // Device display name
String? status; // "ACTIVE" or other status
bool? currentDevice; // true if this is the current device
int? lastAccessedTsEpoch; // Last access timestamp (milliseconds)
int? createdTsEpoch; // Creation timestamp (milliseconds)
String? lastAccessedTs; // Last access timestamp (formatted string)
String? createdTs; // Creation timestamp (formatted string)
String? appUuid; // Application identifier
int? devBind; // Device binding status
}
Cooling periods are server-enforced timeouts between device operations:
Status Code | Meaning | Cooling Period Active | Actions Allowed |
| Success | No | All actions enabled |
| Cooling period active | Yes | All actions disabled |
The currentDevice flag identifies the active device:
currentDevice Value | Delete Button | Rename Button | Reason |
| ❌ Disabled/Hidden | ✅ Enabled | Cannot delete active device |
| ✅ Enabled | ✅ Enabled | Can delete non-current devices |
The updateDeviceDetails() API requires a complete device object in JSON format:
Rename Operation Example:
{
"device": [{
"devUUID": "DEVICE_UUID_HERE",
"devName": "My New Device Name",
"status": "Update",
"lastAccessedTs": "2025-10-09T11:39:49UTC",
"lastAccessedTsEpoch": 1760009989000,
"createdTs": "2025-10-09T11:38:34UTC",
"createdTsEpoch": 1760009914000,
"appUuid": "6b72172f-3e51-4ea9-b217-2f3e51aea9c3",
"currentDevice": true,
"devBind": 0
}]
}
Delete Operation Example:
{
"device": [{
"devUUID": "DEVICE_UUID_HERE",
"devName": "",
"status": "Delete",
"lastAccessedTs": "2025-10-09T11:39:49UTC",
"lastAccessedTsEpoch": 1760009989000,
"createdTs": "2025-10-09T11:38:34UTC",
"createdTsEpoch": 1760009914000,
"appUuid": "6b72172f-3e51-4ea9-b217-2f3e51aea9c3",
"currentDevice": false,
"devBind": 0
}]
}
Device management implements comprehensive error detection:
Layer | Check | Error Source | Example |
Layer 1 |
| API-level errors | Network timeout, invalid userID |
Layer 2 |
| Status codes | 146 (cooling period), validation errors |
Layer 3 |
| SDK/Network failures | Connection refused, SDK errors |
Device management screens use proper event handler cleanup:
// DeviceManagementScreen - dispose cleanup
@override
void dispose() {
print('DeviceManagementScreen - Screen disposed, cleaning up event handlers');
// Reset handler to prevent memory leaks
_rdnaService.getEventManager().setGetRegisteredDeviceDetailsHandler(null);
super.dispose();
}
// DeviceDetailScreen - dispose cleanup
@override
void dispose() {
print('DeviceDetailScreen - Component disposing, cleaning up event handlers');
// Reset handler to prevent memory leaks
_rdnaService.getEventManager().setUpdateDeviceDetailsHandler(null);
super.dispose();
}
Let's implement the device management APIs in your service layer following established REL-ID SDK patterns.
Add this method to
lib/uniken/services/rdna_service.dart
:
/// Gets registered device details for the current user
///
/// This method fetches all devices registered to the user's account. It follows the sync+async pattern:
/// the method returns a sync response, then triggers an onGetRegistredDeviceDetails event with device data.
///
/// ## Parameters
/// - [userId]: The user ID to fetch device details for
///
/// ## Returns
/// RDNASyncResponse containing sync response (error.longErrorCode: 0 = success)
///
/// ## Events Triggered
/// - `onGetRegistredDeviceDetails`: Event with device list data
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. An onGetRegistredDeviceDetails event will be triggered with device list
/// 3. Async events will be handled by event listeners
/// 4. StatusCode 146 indicates cooling period is active for device management actions
///
/// ## Example
/// ```dart
/// final response = await rdnaService.getRegisteredDeviceDetails(userId);
/// if (response.error?.longErrorCode == 0) {
/// print('Device details requested, waiting for onGetRegistredDeviceDetails event');
/// }
/// ```
///
/// ## See Also
/// - [Get Registered Devices Documentation](https://developer.uniken.com/docs/get-registered-devices)
Future<RDNASyncResponse> getRegisteredDeviceDetails(String userId) async {
print('RdnaService - Fetching registered device details for user: $userId');
final response = await _rdnaClient.getRegisteredDeviceDetails(userId);
print('RdnaService - GetRegisteredDeviceDetails sync response received');
print('RdnaService - Sync response:');
print(' Long Error Code: ${response.error?.longErrorCode}');
print(' Short Error Code: ${response.error?.shortErrorCode}');
return response;
}
Add this method to
lib/uniken/services/rdna_service.dart
:
/// Updates device details (rename or delete)
///
/// This method updates device information including renaming or deleting a device.
/// It follows the sync+async pattern: the method returns a sync response,
/// then triggers an onUpdateDeviceDetails event with operation result.
///
/// ## SDK Payload Format
/// ```json
/// {
/// "device": [{
/// "devUUID": "device-uuid",
/// "devName": "new-device-name",
/// "status": "Update" | "Delete"
/// }]
/// }
/// ```
///
/// ## Parameters
/// - [userId]: The user ID who owns the device
/// - [device]: Complete device object with all fields
/// - [newDevName]: The new device name (for rename) or empty string (for delete)
/// - [operationType]: Operation type: 0 = rename (status="Update"), 1 = delete (status="Delete")
///
/// ## Returns
/// RDNASyncResponse containing sync response (error.longErrorCode: 0 = success)
///
/// ## Events Triggered
/// - `onUpdateDeviceDetails`: Event with operation result
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. An onUpdateDeviceDetails event will be triggered with operation result
/// 3. Async events will be handled by event listeners
/// 4. StatusCode 100 indicates successful operation
/// 5. StatusCode 146 indicates cooling period is active (operation not allowed)
///
/// ## Example
/// ```dart
/// // Rename device
/// final response = await rdnaService.updateDeviceDetails(
/// userId,
/// deviceObject,
/// 'My New Phone',
/// 0 // rename
/// );
/// if (response.error?.longErrorCode == 0) {
/// print('Device rename requested, waiting for onUpdateDeviceDetails event');
/// }
///
/// // Delete device
/// final response = await rdnaService.updateDeviceDetails(
/// userId,
/// deviceObject,
/// '',
/// 1 // delete
/// );
/// ```
///
/// ## See Also
/// - [Update Device Details Documentation](https://developer.uniken.com/docs/update-device-details)
Future<RDNASyncResponse> updateDeviceDetails(
String userId,
RDNADeviceDetails device,
String newDevName,
int operationType
) async {
final operation = operationType == 0 ? 'rename' : 'delete';
print('RdnaService - Updating device details ($operation) for user: $userId');
print('RdnaService - Device UUID: ${device.devUuid}');
if (operationType == 0) {
print('RdnaService - New device name: $newDevName');
}
// SDK expects JSON string payload with device array format
// Status field: "Update" for rename, "Delete" for delete
// IMPORTANT: Must include ALL device fields (matching Cordova implementation)
final status = operationType == 0 ? 'Update' : 'Delete';
final payload = jsonEncode({
'device': [{
'devUUID': device.devUuid,
'devName': newDevName,
'status': status,
'lastAccessedTs': device.lastAccessedTs,
'lastAccessedTsEpoch': device.lastAccessedTsEpoch,
'createdTs': device.createdTs,
'createdTsEpoch': device.createdTsEpoch,
'appUuid': device.appUuid,
'currentDevice': device.currentDevice,
'devBind': device.devBind
}]
});
print('RdnaService - JSON payload: $payload');
final response = await _rdnaClient.updateDeviceDetails(userId, payload);
print('RdnaService - UpdateDeviceDetails sync response received');
print('RdnaService - Sync response:');
print(' Long Error Code: ${response.error?.longErrorCode}');
print(' Short Error Code: ${response.error?.shortErrorCode}');
return response;
}
Ensure these imports exist in
lib/uniken/services/rdna_service.dart
:
import 'dart:convert'; // For jsonEncode
import 'package:rdna_client/rdna_client.dart';
import 'package:rdna_client/rdna_struct.dart';
Verify your service class exports all methods:
class RdnaService {
// Existing MFA methods...
// ✅ New device management methods
Future<RDNASyncResponse> getRegisteredDeviceDetails(String userId) async { /* ... */ }
Future<RDNASyncResponse> updateDeviceDetails(String userId, RDNADeviceDetails device, String newDevName, int operationType) async { /* ... */ }
}
Now let's enhance your event manager to handle device management events and callbacks.
The event manager already includes device management handlers in
lib/uniken/services/rdna_event_manager.dart
:
// Device Management Callbacks (already included in event manager)
typedef RDNAGetRegisteredDeviceDetailsCallback = void Function(RDNAStatusGetRegisteredDeviceDetails);
typedef RDNAUpdateDeviceDetailsCallback = void Function(RDNAStatusUpdateDeviceDetails);
class RdnaEventManager {
// Device Management Handlers
RDNAGetRegisteredDeviceDetailsCallback? _getRegisteredDeviceDetailsHandler;
RDNAUpdateDeviceDetailsCallback? _updateDeviceDetailsHandler;
// Event listeners are automatically registered in _registerEventListeners()
void _registerEventListeners() {
// ... existing listeners ...
// Device Management Event Listeners
_listeners.add(
_rdnaClient.on(RdnaClient.onGetRegistredDeviceDetails, _onGetRegistredDeviceDetails),
);
_listeners.add(
_rdnaClient.on(RdnaClient.onUpdateDeviceDetails, _onUpdateDeviceDetails),
);
}
/// Handles registered device details response events
void _onGetRegistredDeviceDetails(dynamic deviceDetailsData) {
print('RdnaEventManager - Get registered device details event received');
// Cast to RDNAStatusGetRegisteredDeviceDetails and pass to handler
final statusData = deviceDetailsData as RDNAStatusGetRegisteredDeviceDetails;
print('RdnaEventManager - Get registered device details data:');
print(' Error Code: ${statusData.errCode}');
final deviceResponse = statusData.pArgs?.response?.responseData?.response is RDNAGetRegisteredDeviceDetailsResponse
? statusData.pArgs?.response?.responseData?.response as RDNAGetRegisteredDeviceDetailsResponse
: null;
print(' Device Count: ${deviceResponse?.device?.length ?? 0}');
print(' Status Code: ${statusData.pArgs?.response?.statusCode}');
print(' Status Msg: ${statusData.pArgs?.response?.statusMsg}');
if (_getRegisteredDeviceDetailsHandler != null) {
_getRegisteredDeviceDetailsHandler!(statusData);
}
}
/// Handles update device details response events (rename/delete)
void _onUpdateDeviceDetails(dynamic updateDeviceData) {
print('RdnaEventManager - Update device details event received');
// Cast to RDNAStatusUpdateDeviceDetails and pass to handler
final statusData = updateDeviceData as RDNAStatusUpdateDeviceDetails;
print('RdnaEventManager - Update device details data:');
print(' Error Code: ${statusData.errCode}');
print(' Status Code: ${statusData.pArgs?.response?.statusCode}');
print(' Status Msg: ${statusData.pArgs?.response?.statusMsg}');
if (_updateDeviceDetailsHandler != null) {
_updateDeviceDetailsHandler!(statusData);
}
}
/// Sets the handler for get registered device details events
void setGetRegisteredDeviceDetailsHandler(RDNAGetRegisteredDeviceDetailsCallback? callback) {
_getRegisteredDeviceDetailsHandler = callback;
}
/// Sets the handler for update device details events
void setUpdateDeviceDetailsHandler(RDNAUpdateDeviceDetailsCallback? callback) {
_updateDeviceDetailsHandler = callback;
}
}
Create the DeviceManagementScreen that displays the device list with pull-to-refresh, cooling period detection, and auto-refresh capabilities.
The file already exists at:
lib/tutorial/screens/device_management/device_management_screen.dart
The complete implementation is in the reference app:
class DeviceManagementScreen extends ConsumerStatefulWidget {
final String? userID;
final RDNAUserLoggedIn? sessionData;
const DeviceManagementScreen({
super.key,
this.userID,
this.sessionData,
});
@override
ConsumerState<DeviceManagementScreen> createState() => _DeviceManagementScreenState();
}
class _DeviceManagementScreenState extends ConsumerState<DeviceManagementScreen> {
final RdnaService _rdnaService = RdnaService.getInstance();
List<RDNADeviceDetails> _devices = [];
bool _isLoading = true;
bool _isRefreshing = false;
int? _coolingPeriodEndTimestamp;
String _coolingPeriodMessage = '';
bool _isCoolingPeriodActive = false;
@override
void initState() {
super.initState();
print('DeviceManagementScreen - Screen initialized');
// Use post-frame callback to load devices after first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadDevices();
});
}
@override
void dispose() {
print('DeviceManagementScreen - Screen disposed, cleaning up event handlers');
// Reset handler to prevent memory leaks
_rdnaService.getEventManager().setGetRegisteredDeviceDetailsHandler(null);
super.dispose();
}
/// Fetches registered device details from the SDK
Future<void> _loadDevices() async {
if (widget.userID == null || widget.userID!.isEmpty) {
print('DeviceManagementScreen - No userID available');
if (mounted) {
_showError('User ID is required to load devices');
setState(() {
_isLoading = false;
_isRefreshing = false;
});
}
return;
}
print('DeviceManagementScreen - Loading devices for user: ${widget.userID}');
if (!_isRefreshing) {
setState(() => _isLoading = true);
}
try {
// Set up event handler for device details response
final eventManager = _rdnaService.getEventManager();
bool handlerCalled = false;
// Set callback for this screen
eventManager.setGetRegisteredDeviceDetailsHandler((RDNAStatusGetRegisteredDeviceDetails data) {
if (handlerCalled) return; // Prevent double handling
handlerCalled = true;
print('DeviceManagementScreen - Received device details event');
// Extract and parse device data
final parsedResponse = data.pArgs?.response?.responseData?.response;
final deviceResponse = parsedResponse is RDNAGetRegisteredDeviceDetailsResponse ? parsedResponse : null;
final deviceList = deviceResponse?.device ?? [];
print('DeviceManagementScreen - Device count: ${deviceList.length}');
print('DeviceManagementScreen - Status code: ${data.pArgs?.response?.statusCode}');
// Check for errors using data.error.longErrorCode (sync error check, no try-catch)
if (data.error?.longErrorCode != 0) {
print('DeviceManagementScreen - API error: ${data.error?.longErrorCode}');
if (mounted) {
_showError(data.error?.errorString ?? 'Failed to load devices. Please try again.');
setState(() {
_isLoading = false;
_isRefreshing = false;
});
}
return;
}
// Extract additional data
final coolingPeriodEnd = deviceResponse?.deviceManagementCoolingPeriodEndTimestamp;
final statusCode = data.pArgs?.response?.statusCode ?? 0;
final statusMsg = data.pArgs?.response?.statusMsg ?? '';
if (mounted) {
setState(() {
_devices = deviceList;
_coolingPeriodEndTimestamp = coolingPeriodEnd;
_coolingPeriodMessage = statusMsg;
_isCoolingPeriodActive = statusCode == 146;
_isLoading = false;
_isRefreshing = false;
});
}
print('DeviceManagementScreen - Devices loaded successfully');
});
// Call the API with userID (check sync error, no try-catch)
final response = await _rdnaService.getRegisteredDeviceDetails(widget.userID!);
// Check sync response error
if (response.error?.longErrorCode != 0) {
print('DeviceManagementScreen - API call failed: ${response.error?.errorString}');
if (mounted) {
_showError(response.error?.errorString ?? 'Failed to load devices');
setState(() {
_isLoading = false;
_isRefreshing = false;
});
}
}
} catch (error) {
print('DeviceManagementScreen - Unexpected error: $error');
if (mounted) {
_showError('Failed to load devices. Please try again.');
setState(() {
_isLoading = false;
_isRefreshing = false;
});
}
}
}
/// Handles device item tap
void _handleDeviceTap(RDNADeviceDetails device) async {
print('DeviceManagementScreen - Device tapped: ${device.devUuid}');
// Navigate to DeviceDetailScreen and wait for result
final result = await context.push('/device-detail', extra: {
'device': device,
'userID': widget.userID,
'isCoolingPeriodActive': _isCoolingPeriodActive,
'coolingPeriodEndTimestamp': _coolingPeriodEndTimestamp,
'coolingPeriodMessage': _coolingPeriodMessage,
});
// Reload devices when returning from detail screen (in case of rename/delete)
if (result == true || result == 'refresh') {
print('DeviceManagementScreen - Reloading devices after detail screen return');
_loadDevices();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
drawer: DrawerContent(
sessionData: widget.sessionData,
currentRoute: 'deviceManagementScreen',
),
appBar: AppBar(/* ... */),
body: Column(
children: [
// Cooling Period Banner
if (_renderCoolingPeriodBanner() != null)
_renderCoolingPeriodBanner()!,
// Main Content with Pull-to-Refresh
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _onRefresh,
child: ListView.builder(
itemCount: _devices.length,
itemBuilder: (context, index) {
return _renderDeviceItem(_devices[index]);
},
),
),
),
],
),
);
}
}
getRegisteredDeviceDetails() when screen loads using addPostFrameCallbackStatusCode == 146 to detect active cooling periodThe following image showcases the screen from the sample application:

Create the DeviceDetailScreen that displays device details and provides rename and delete operations.
The file already exists at:
lib/tutorial/screens/device_management/device_detail_screen.dart
The complete implementation is in the reference app:
class DeviceDetailScreen extends StatefulWidget {
final RDNADeviceDetails device;
final String? userID;
final bool isCoolingPeriodActive;
final int? coolingPeriodEndTimestamp;
final String coolingPeriodMessage;
const DeviceDetailScreen({
super.key,
required this.device,
required this.userID,
required this.isCoolingPeriodActive,
this.coolingPeriodEndTimestamp,
required this.coolingPeriodMessage,
});
@override
State<DeviceDetailScreen> createState() => _DeviceDetailScreenState();
}
class _DeviceDetailScreenState extends State<DeviceDetailScreen> {
final RdnaService _rdnaService = RdnaService.getInstance();
bool _isRenaming = false;
bool _isDeleting = false;
late String _currentDeviceName;
@override
void initState() {
super.initState();
_currentDeviceName = widget.device.devName ?? '';
}
@override
void dispose() {
print('DeviceDetailScreen - Component disposing, cleaning up event handlers');
// Reset handler to prevent memory leaks
_rdnaService.getEventManager().setUpdateDeviceDetailsHandler(null);
super.dispose();
}
/// Unified method to handle device update operations (rename/delete)
Future<void> _updateDevice({
required String newName,
required int operationType, // 0 = rename, 1 = delete
}) async {
final isRename = operationType == 0;
final operation = isRename ? 'rename' : 'delete';
if (isRename) {
setState(() => _isRenaming = true);
} else {
setState(() => _isDeleting = true);
}
try {
print('DeviceDetailScreen - $operation device: ${widget.device.devUuid}');
final eventManager = _rdnaService.getEventManager();
bool handlerCalled = false;
// Set callback for this operation
eventManager.setUpdateDeviceDetailsHandler((RDNAStatusUpdateDeviceDetails data) {
if (handlerCalled) return; // Prevent double handling
handlerCalled = true;
print('DeviceDetailScreen - Received update device details event');
// Check sync error (no try-catch)
if (data.error?.longErrorCode != 0) {
print('DeviceDetailScreen - $operation error: ${data.error?.longErrorCode}');
if (mounted) {
_showError(data.error?.errorString ?? 'Failed to $operation device. Please try again.');
setState(() {
if (isRename) {
_isRenaming = false;
} else {
_isDeleting = false;
}
});
}
return;
}
final statusCode = data.pArgs?.response?.statusCode ?? 0;
final statusMsg = data.pArgs?.response?.statusMsg ?? '';
if (statusCode == 100) {
print('DeviceDetailScreen - $operation successful');
if (mounted) {
if (isRename) {
// Rename success
setState(() {
_currentDeviceName = newName;
_isRenaming = false;
});
_showSuccess('Device renamed successfully');
// Navigate back to device list after success
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
Navigator.of(context).pop(true); // Return true to trigger refresh
}
});
} else {
// Delete success
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Success'),
content: const Text('Device deleted successfully'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close dialog
Navigator.of(context).pop(true); // Go back to device list with refresh signal
},
child: const Text('OK'),
),
],
);
},
);
setState(() => _isDeleting = false);
}
}
} else if (statusCode == 146) {
if (mounted) {
_showError('Device management is currently in cooling period. Please try again later.');
setState(() {
if (isRename) {
_isRenaming = false;
} else {
_isDeleting = false;
}
});
}
} else {
if (mounted) {
_showError(statusMsg.isNotEmpty ? statusMsg : 'Failed to $operation device');
setState(() {
if (isRename) {
_isRenaming = false;
} else {
_isDeleting = false;
}
});
}
}
});
// Call the API (check sync error, no try-catch)
final response = await _rdnaService.updateDeviceDetails(
widget.userID!,
widget.device,
newName,
operationType,
);
// Check sync response error
if (response.error?.longErrorCode != 0) {
print('DeviceDetailScreen - $operation API call failed: ${response.error?.errorString}');
if (mounted) {
_showError(response.error?.errorString ?? 'Failed to $operation device');
setState(() {
if (isRename) {
_isRenaming = false;
} else {
_isDeleting = false;
}
});
}
}
} catch (error) {
print('DeviceDetailScreen - $operation unexpected error: $error');
if (mounted) {
_showError('Failed to $operation device. Please try again.');
setState(() {
if (isRename) {
_isRenaming = false;
} else {
_isDeleting = false;
}
});
}
}
}
/// Handles rename device action
Future<void> _handleRenameDevice(String newName) async {
await _updateDevice(newName: newName, operationType: 0);
}
/// Handles delete device action
void _handleDeleteDevice() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Delete Device'),
content: Text('Are you sure you want to delete "$_currentDeviceName"? This action cannot be undone.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
_performDeleteDevice();
},
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Delete'),
),
],
);
},
);
}
Future<void> _performDeleteDevice() async {
await _updateDevice(newName: '', operationType: 1);
}
@override
Widget build(BuildContext context) {
final isCurrentDevice = widget.device.currentDevice ?? false;
return Scaffold(
appBar: AppBar(/* ... */),
body: SingleChildScrollView(
child: Column(
children: [
// Current Device Banner
if (isCurrentDevice)
Container(/* ... Current device banner ... */),
// Device Details Card
Container(/* ... Device information ... */),
// Cooling Period Warning
if (widget.isCoolingPeriodActive)
Container(/* ... Cooling period warning ... */),
// Actions Card with Rename and Delete buttons
Container(
child: Column(
children: [
// Rename Button
ElevatedButton(
onPressed: (widget.isCoolingPeriodActive || _isRenaming || _isDeleting)
? null
: () async {
final newName = await showDialog<String>(
context: context,
builder: (BuildContext context) {
return RenameDeviceDialog(
currentName: _currentDeviceName,
);
},
);
if (newName != null && newName.isNotEmpty && newName != _currentDeviceName) {
_handleRenameDevice(newName);
}
},
child: _isRenaming
? const CircularProgressIndicator()
: const Text('Rename Device'),
),
// Delete Button (only if not current device)
if (!isCurrentDevice)
ElevatedButton(
onPressed: (widget.isCoolingPeriodActive || _isRenaming || _isDeleting)
? null
: _handleDeleteDevice,
child: _isDeleting
? const CircularProgressIndicator()
: const Text('Delete Device'),
),
],
),
),
],
),
),
);
}
}
data.error?.longErrorCode != 0device.currentDevice == true before allowing deleteThe following images showcase the screen from the sample application:

Create the RenameDeviceDialog modal component for device renaming with validation.
The file already exists at:
lib/tutorial/screens/device_management/rename_device_dialog.dart
The complete implementation is in the reference app:
class RenameDeviceDialog extends StatefulWidget {
final String currentName;
const RenameDeviceDialog({
super.key,
required this.currentName,
});
@override
State<RenameDeviceDialog> createState() => _RenameDeviceDialogState();
}
class _RenameDeviceDialogState extends State<RenameDeviceDialog> {
late TextEditingController _controller;
String _error = '';
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.currentName);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleSubmit() {
final trimmedName = _controller.text.trim();
if (trimmedName.isEmpty) {
setState(() => _error = 'Device name cannot be empty');
return;
}
if (trimmedName == widget.currentName) {
setState(() => _error = 'New name must be different from current name');
return;
}
if (trimmedName.length < 3) {
setState(() => _error = 'Device name must be at least 3 characters');
return;
}
if (trimmedName.length > 50) {
setState(() => _error = 'Device name must be less than 50 characters');
return;
}
Navigator.of(context).pop(trimmedName);
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
const Text(
'Rename Device',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50),
),
),
const SizedBox(height: 20),
// Current Name Display
const Text(
'Current Name:',
style: TextStyle(
fontSize: 14,
color: Color(0xFF7F8C8D),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Text(
widget.currentName,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF2C3E50),
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// New Name Input
const Text(
'New Name:',
style: TextStyle(
fontSize: 14,
color: Color(0xFF7F8C8D),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
TextField(
controller: _controller,
autofocus: true,
maxLength: 50,
decoration: InputDecoration(
hintText: 'Enter new device name',
errorText: _error.isEmpty ? null : _error,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
counterText: '',
),
onChanged: (value) {
if (_error.isNotEmpty) {
setState(() => _error = '');
}
},
onSubmitted: (_) => _handleSubmit(),
),
const SizedBox(height: 24),
// Actions
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _handleSubmit,
child: const Text('Rename'),
),
],
),
],
),
),
);
}
}
The following images showcase the screen from the sample application:

Now let's integrate the Device Management screens into your GoRouter for post-login access.
The routes are already configured in
lib/tutorial/navigation/app_router.dart
:
// Device Management Routes
GoRoute(
path: '/device-management',
name: 'deviceManagementScreen',
builder: (context, state) {
final sessionData = state.extra as RDNAUserLoggedIn?;
return DeviceManagementScreen(
userID: sessionData?.userId,
sessionData: sessionData,
);
},
),
GoRoute(
path: '/device-detail',
name: 'deviceDetailScreen',
builder: (context, state) {
final params = state.extra as Map<String, dynamic>;
return DeviceDetailScreen(
device: params['device'] as RDNADeviceDetails,
userID: params['userID'] as String?,
isCoolingPeriodActive: params['isCoolingPeriodActive'] as bool,
coolingPeriodEndTimestamp: params['coolingPeriodEndTimestamp'] as int?,
coolingPeriodMessage: params['coolingPeriodMessage'] as String,
);
},
),
Modify
lib/tutorial/screens/components/drawer_content.dart
to add menu item:
// Add Device Management menu item to drawer
ListTile(
leading: const Icon(Icons.devices, color: Color(0xFF2C3E50)),
title: const Text(
'Device Management',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF2C3E50),
),
),
selected: currentRoute == 'deviceManagementScreen',
selectedTileColor: const Color(0xFFE3F2FD),
onTap: () {
Navigator.of(context).pop(); // Close drawer
context.push('/device-management', extra: sessionData);
},
),
Navigating to Device Management:
// From drawer or other screens
context.push('/device-management', extra: sessionData);
Navigating to Device Detail:
// From device list, passing all required parameters
final result = await context.push('/device-detail', extra: {
'device': device,
'userID': userID,
'isCoolingPeriodActive': isCoolingPeriodActive,
'coolingPeriodEndTimestamp': coolingPeriodEndTimestamp,
'coolingPeriodMessage': coolingPeriodMessage,
});
// Handle refresh on return
if (result == true || result == 'refresh') {
_loadDevices();
}
Returning with Refresh Signal:
// From DeviceDetailScreen after successful operation
Navigator.of(context).pop(true); // Return true to trigger refresh
Let's verify your device management implementation with comprehensive manual testing scenarios.
Steps:
Expected Console Logs:
DeviceManagementScreen - Loading devices for user: user@example.com
RdnaService - GetRegisteredDeviceDetails sync response success
DeviceManagementScreen - Received device details event
DeviceManagementScreen - Device count: 3
DeviceManagementScreen - Status code: 100
Expected Result: ✅ Device list displays with:
Steps:
Expected Behavior: ✅ Device list refreshes, refresh indicator disappears, updated device data displayed
Steps:
Expected Console Logs:
DeviceDetailScreen - Received update device details event
DeviceDetailScreen - Status code: 100
DeviceDetailScreen - rename successful
Expected Result: ✅ Success snackbar "Device renamed successfully", modal closes, device name updates in UI, navigates back to list
Steps:
currentDevice: false)Expected Console Logs:
DeviceDetailScreen - Received update device details event
DeviceDetailScreen - Status code: 100
DeviceDetailScreen - delete successful
Expected Result: ✅ Success dialog "Device deleted successfully", navigation back to device list, deleted device no longer appears
Steps:
Expected Result: ✅ Delete button hidden, info message displayed explaining current device cannot be deleted
Prerequisites: Perform a device operation (rename or delete) to trigger cooling period
Steps:
Expected Console Logs:
DeviceManagementScreen - Status code: 146
DeviceManagementScreen - Cooling period end: 1760013589000
Expected Result: ✅ Orange/yellow banner displays:
Expected Result: ✅ Nothing happens, buttons remain disabled during cooling period
Test Empty Name:
Expected Result: ✅ Error message: "Device name cannot be empty"
Test Same Name:
Expected Result: ✅ Error message: "New name must be different from current name"
Test Too Short:
Expected Result: ✅ Error message: "Device name must be at least 3 characters"
Test Too Long:
Expected Result: ✅ Error message: "Device name must be less than 50 characters"
Steps:
Expected Behavior: ✅ Device list auto-refreshes when returning from detail screen, showing updated device name without manual refresh
Steps:
Expected Console Logs:
DeviceManagementScreen - Screen disposed, cleaning up event handlers
DeviceManagementScreen - Screen initialized
Expected Result: ✅ Event handlers properly cleaned up and re-registered, no memory leaks or duplicate event handling
Symptoms:
Causes & Solutions:
Cause 1: UserID not passed to DeviceManagementScreen
Solution: Verify GoRouter passes userID via extra parameter
- Check: context.push('/device-management', extra: sessionData)
- Verify widget.userID in DeviceManagementScreen
- Ensure sessionData contains userId field
Cause 2: Event handler not triggered
Solution: Verify event handler registration
- Check: eventManager.setGetRegisteredDeviceDetailsHandler() is called
- Verify handler is set BEFORE API call
- Check console for: "Received device details event"
Cause 3: API call fails silently
Solution: Check sync response error handling
- Verify: rdnaService.getRegisteredDeviceDetails() returns Future
- Check error handling in event callback
- Verify Layer 1 error check: data.error?.longErrorCode != 0
Symptoms:
Causes & Solutions:
Cause 1: StatusCode check incorrect
Solution: Verify exact status code comparison
- Check: _isCoolingPeriodActive = statusCode == 146
- Verify statusCode is int, not String
- Log statusCode value: print('Status code: $statusCode ${statusCode.runtimeType}')
Cause 2: Banner render logic error
Solution: Check conditional rendering
- Verify: if (_renderCoolingPeriodBanner() != null)
- Ensure _isCoolingPeriodActive state is bool
- Check banner widget is not null
Cause 3: Status code extracted from wrong path
Solution: Verify response structure
- Check: data.pArgs?.response?.statusCode
- Log response structure: print('Response: ${data.pArgs?.response}')
- Verify SDK version matches expected response format
Symptoms:
Causes & Solutions:
Cause 1: currentDevice flag check incorrect
Solution: Verify boolean comparison
- Check: if (device.currentDevice ?? false) { ... }
- Ensure device.currentDevice is bool?
- Log device object: print('Current device: ${device.currentDevice}')
Cause 2: Device object not passed correctly
Solution: Verify navigation parameters
- Check: context.push('/device-detail', extra: {params})
- Verify params map contains device object
- Ensure complete device object passed, not just UUID
Cause 3: Cooling period active
Solution: Check cooling period state
- Verify: isCoolingPeriodActive is false
- Check cooling period end timestamp hasn't expired
- Log cooling period state before operations
Symptoms:
Causes & Solutions:
Cause 1: showDialog not awaiting properly
Solution: Verify async/await pattern
- Check: final newName = await showDialog<String>(...)
- Ensure showDialog returns Future<String?>
- Verify RenameDeviceDialog returns value via Navigator.pop()
Cause 2: Dialog widget not rendering
Solution: Verify RenameDeviceDialog component
- Check: showDialog(builder: (context) => RenameDeviceDialog(...))
- Ensure component imported correctly
- Verify Dialog widget structure
Cause 3: Button disabled during cooling period
Solution: Check button disabled logic
- Verify: onPressed: (isCoolingPeriodActive || _isRenaming) ? null : ...
- Ensure cooling period is not active
- Check button enabled state
Symptoms:
Causes & Solutions:
Cause 1: Event handler not set before API call
Solution: Verify handler registration timing
- Set handler BEFORE calling API: eventManager.setGetRegisteredDeviceDetailsHandler()
- Verify timing: handler -> API call -> event received
- Check for race conditions
Cause 2: Event name mismatch
Solution: Verify exact event name
- Check: _rdnaClient.on(RdnaClient.onGetRegistredDeviceDetails, ...)
- Note spelling: 'Registred' not 'Registered' (SDK convention)
- Verify event name matches SDK documentation
Cause 3: Handler not cleaned up properly
Solution: Implement proper lifecycle cleanup
- Set handler when needed: eventManager.setHandler(callback)
- Clean up in dispose: eventManager.setHandler(null)
- Verify cleanup runs: Add print in dispose method
Symptoms:
Causes & Solutions:
Cause 1: Event handlers not cleaned up
Solution: Implement proper cleanup
- Add dispose cleanup: eventManager.setHandler(null)
- Verify cleanup runs on dispose: print('Cleaning up')
- Check handler is null after dispose
Cause 2: Multiple handlers registered
Solution: Remove handlers before setting new ones
- Check existing handler before setting
- Use single handler per screen
- Avoid setting handlers in build methods
Cause 3: Listeners not removed
Solution: Remove native event listeners
- Store listener: final listener = _rdnaClient.on(...)
- Remove on cleanup: _rdnaClient.off(listener)
- Verify listeners array cleared in event manager
Device Data Handling:
uuid.substring(0, 10) + '...')Cooling Period Enforcement:
Error Handling:
Loading States:
Visual Feedback:
Navigation:
Validation:
File Structure:
lib/
├── uniken/
│ ├── services/
│ │ ├── rdna_service.dart (✅ Add getRegisteredDeviceDetails, updateDeviceDetails)
│ │ └── rdna_event_manager.dart (✅ Add device management event handlers)
│ └── utils/
│ └── connection_profile_parser.dart
└── tutorial/
├── navigation/
│ └── app_router.dart (✅ Add device management routes)
└── screens/
├── device_management/
│ ├── device_management_screen.dart (✅ NEW)
│ ├── device_detail_screen.dart (✅ NEW)
│ └── rename_device_dialog.dart (✅ NEW)
└── components/
└── drawer_content.dart (✅ Add device management menu)
Component Responsibilities:
Render Optimization:
WidgetsBinding.instance.addPostFrameCallback for initializationMemory Management:
Network Optimization:
getRegisteredDeviceDetails() only when screen initializesBefore deploying to production, verify:
Congratulations! You've successfully implemented comprehensive device management functionality with REL-ID SDK in Flutter!
In this codelab, you learned how to:
getRegisteredDeviceDetails()onGetRegistredDeviceDetails and onUpdateDeviceDetails