rg_bridge 0.1.1
rg_bridge: ^0.1.1 copied to clipboard
Hardware test bridge: device info + 14 headless test controllers + touchscreen widget. Native Android ships in-package (button interception, screen events, bluetooth/wifi, permissions); iOS is a neutral stub.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rg_bridge/rg_bridge.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'pages/accelerometer_test_page.dart';
import 'pages/biometric_test_page.dart';
import 'pages/bluetooth_test_page.dart';
import 'pages/camera_test_page.dart';
import 'pages/device_info_page.dart';
import 'pages/flashlight_test_page.dart';
import 'pages/form_submission_page.dart';
import 'pages/gps_test_page.dart';
import 'pages/gyroscope_test_page.dart';
import 'pages/microphone_test_page.dart';
import 'pages/navigation_button_test_page.dart';
import 'pages/power_button_test_page.dart';
import 'pages/speaker_test_page.dart';
import 'pages/speech_test_page.dart';
import 'pages/touchscreen_test_page.dart';
import 'pages/vibration_test_page.dart';
import 'pages/volume_button_test_page.dart';
import 'pages/wifi_test_page.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Fullscreen: hide status bar + navigation bar (immersive sticky — swipe to show sementara)
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
),
);
runApp(const ExampleApp());
}
/// Shared SDK instance for example screens that hit the backend (WebView, Form).
final RGBridge exampleSdk = RGBridge(
baseUrl: 'https://rgstudio.usherops.com',
clientId: 'rg_72mg5yD6CKBs5mUZaRAoJY6IJGs91wwc0T1QT2HL',
);
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SDK RG WebView Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const _MenuPage(),
);
}
}
class WebViewExamplePage extends StatefulWidget {
const WebViewExamplePage({super.key, required this.sdk});
final RGBridge sdk;
@override
State<WebViewExamplePage> createState() => _WebViewExamplePageState();
}
class _WebViewExamplePageState extends State<WebViewExamplePage> {
WebViewController? _controller;
bool _started = false;
bool _isLoading = false;
String? _error;
final List<WebViewEvent> _events = [];
WebViewEvent? get _lastEvent => _events.isEmpty ? null : _events.last;
final List<({DateTime time, String level, String message})> _console = [];
bool _showConsole = false;
String? _sessionToken;
final _typeCtrl = TextEditingController(text: 'pengecekan-laptop');
final _conversationIdCtrl = TextEditingController();
RGBridge get _sdk => widget.sdk;
@override
void initState() {
super.initState();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
}
@override
void dispose() {
_typeCtrl.dispose();
_conversationIdCtrl.dispose();
_sdk.webview.offAllEvents();
super.dispose();
}
Future<void> _initWebView() async {
final type = _typeCtrl.text.trim();
if (type.isEmpty) {
setState(() {
_started = true;
_error = 'type wajib diisi';
_isLoading = false;
});
return;
}
final conversationId = _conversationIdCtrl.text.trim();
final meta = conversationId.isEmpty
? null
: <String, dynamic>{'conversation_id': conversationId};
setState(() {
_started = true;
_isLoading = true;
_error = null;
_controller = null;
_events.clear();
_console.clear();
});
try {
final initSession = await _sdk.webview.initSessionWebView(
type: type,
firstName: 'John',
lastName: 'Doe',
memberCode: '123456',
meta: meta,
);
_sessionToken = initSession.sessionToken;
for (final t in WebviewEventType.values) {
_sdk.webview.onEvent(t, _handleEvent);
}
final controller = _sdk.webview.createController(
onConsoleMessage: (msg) {
if (!mounted) return;
setState(
() => _console.add((
time: DateTime.now(),
level: msg.level.name,
message: msg.message,
)),
);
debugPrint('[WebView:${msg.level.name}] ${msg.message}');
},
);
await _sdk.webview.openWebView(
controller: controller,
url: initSession.webViewUrl,
);
if (mounted) {
setState(() {
_controller = controller;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
}
void _backToSetup() {
_sdk.webview.offAllEvents();
setState(() {
_started = false;
_isLoading = false;
_error = null;
_controller = null;
_sessionToken = null;
_events.clear();
_console.clear();
_showConsole = false;
});
}
void _handleEvent(WebViewEvent event) {
debugPrint('[SDKRG] ${event.type.name} ${event.data}');
if (!mounted) return;
setState(() => _events.add(event));
final msg = switch (event.type) {
WebviewEventType.formSubmitted =>
'Form submitted: ${event.data['name'] ?? '-'}',
WebviewEventType.formCancelled =>
'Form cancelled: ${event.data['reason'] ?? '-'}',
WebviewEventType.formError =>
'Form error: ${event.data['message'] ?? event.data['code'] ?? '-'}',
WebviewEventType.formSuccess =>
'Form success: ${event.data['id'] ?? '-'}',
WebviewEventType.imageCaptured =>
'Image captured: ${event.data['imageId'] ?? '-'}',
};
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg), behavior: SnackBarBehavior.floating),
);
}
Future<void> _reload() async {
if (_controller == null || _sessionToken == null) return;
final session = await _sdk.webview.getSessionWebView(
sessionToken: _sessionToken!,
);
await _sdk.webview.openWebView(
controller: _controller!,
url: session.webViewUrl,
);
}
void _toggleConsole() => setState(() => _showConsole = !_showConsole);
@override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
if (!_started) {
return Scaffold(
appBar: AppBar(title: const Text('WebView Demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'Isi type (wajib) dan meta.conversation_id (opsional), '
'lalu buka WebView.',
),
const SizedBox(height: 16),
TextField(
controller: _typeCtrl,
decoration: const InputDecoration(
labelText: 'type',
hintText: 'pengecekan-laptop',
border: OutlineInputBorder(),
isDense: true,
),
style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
),
const SizedBox(height: 12),
TextField(
controller: _conversationIdCtrl,
decoration: const InputDecoration(
labelText: 'meta.conversation_id (opsional)',
border: OutlineInputBorder(),
isDense: true,
),
style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: _initWebView,
icon: const Icon(Icons.web),
label: const Text('Buka WebView'),
),
],
),
);
}
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// WebView / loading / error — full bleed
Positioned.fill(child: _buildBody()),
// Banner last event — overlay top (tanpa AppBar)
if (_lastEvent != null)
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
bottom: false,
child: Container(
color: _colorFor(_lastEvent!.type),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Text(
'${_lastEvent!.type.name}: ${_lastEvent!.data}',
style: const TextStyle(color: Colors.white, fontSize: 11),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
),
// Event log — overlay bottom, bukan AppBar/bottomNavigationBar
if (_events.isNotEmpty)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: SafeArea(
top: false,
child: Container(
height: 140,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest
.withValues(alpha: 0.97),
border: Border(
top: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
child: Row(
children: [
Expanded(
child: Text(
'Event log (${_events.length})',
style: Theme.of(context).textTheme.labelSmall,
),
),
InkWell(
onTap: () => setState(() => _events.clear()),
borderRadius: BorderRadius.circular(6),
child: const Padding(
padding: EdgeInsets.all(4),
child: Icon(Icons.close, size: 16),
),
),
],
),
),
const Divider(height: 1),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
itemCount: _events.length,
reverse: true,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (_, i) {
final e = _events[_events.length - 1 - i];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
_Badge(type: e.type),
const SizedBox(width: 8),
Expanded(
child: Text(
e.data.toString(),
style: const TextStyle(fontSize: 11),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
},
),
),
],
),
),
),
),
// JS Console overlay — developer console untuk WebView
if (_showConsole)
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: _ConsoleOverlay(
entries: _console,
onClose: _toggleConsole,
onClear: () => setState(() => _console.clear()),
onCopy: (text) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Copied to clipboard')),
);
},
),
),
// Floating controls — ganti AppBar actions, tetap tanpa topbar
Positioned(
right: 12,
bottom: (_events.isNotEmpty ? 152 : 0) + (_showConsole ? 0 : 0),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.small(
heroTag: 'setup',
onPressed: _backToSetup,
tooltip: 'Ubah type / conversation_id',
child: const Icon(Icons.edit),
),
const SizedBox(height: 8),
FloatingActionButton.small(
heroTag: 'reload',
onPressed: _controller == null ? null : _reload,
tooltip: 'Reload',
child: const Icon(Icons.refresh),
),
const SizedBox(height: 8),
FloatingActionButton.small(
heroTag: 'console',
onPressed: _toggleConsole,
tooltip: 'Developer console (${_console.length})',
backgroundColor: _showConsole
? const Color(0xFFB3261E)
: null,
child: Badge(
isLabelVisible: _console.isNotEmpty,
label: Text('${_console.length}'),
child: Icon(
_showConsole ? Icons.close : Icons.bug_report,
),
),
),
if (_events.isNotEmpty) const SizedBox(height: 8),
if (_events.isNotEmpty)
FloatingActionButton.small(
heroTag: 'clear',
onPressed: () => setState(() => _events.clear()),
tooltip: 'Clear log',
child: const Icon(Icons.delete_sweep),
),
],
),
),
),
],
),
);
}
Widget _buildBody() {
if (_error != null) {
return Container(
color: Colors.white,
child: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 12),
Text(_error!, textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
_sdk.webview.offAllEvents();
_initWebView();
},
child: const Text('Coba lagi'),
),
const SizedBox(height: 8),
TextButton(
onPressed: _backToSetup,
child: const Text('Ubah input'),
),
],
),
),
),
);
}
if (_isLoading || _controller == null) {
return const ColoredBox(
color: Colors.white,
child: Center(child: CircularProgressIndicator()),
);
}
return WebViewWidget(controller: _controller!);
}
Color _colorFor(WebviewEventType t) => switch (t) {
WebviewEventType.formSubmitted => const Color(0xFF6750A4),
WebviewEventType.formCancelled => const Color(0xFF79747E),
WebviewEventType.formError => const Color(0xFFB3261E),
WebviewEventType.formSuccess => const Color(0xFF2E7D32),
WebviewEventType.imageCaptured => const Color(0xFF00639A),
};
}
class _ConsoleOverlay extends StatelessWidget {
final List<({DateTime time, String level, String message})> entries;
final VoidCallback onClose;
final VoidCallback onClear;
final void Function(String text) onCopy;
const _ConsoleOverlay({
required this.entries,
required this.onClose,
required this.onClear,
required this.onCopy,
});
Color _levelColor(String level) => switch (level) {
'error' => const Color(0xFFB3261E),
'warning' => const Color(0xFFE65100),
_ => const Color(0xFF2E7D32),
};
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFF0F1115).withValues(alpha: 0.96),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
color: const Color(0xFF1E1E1E),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
const Icon(Icons.terminal, size: 16, color: Colors.white70),
const SizedBox(width: 8),
Expanded(
child: Text(
'WebView Console (${entries.length})',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
TextButton(
onPressed: entries.isEmpty ? null : onClear,
child: const Text(
'Clear',
style: TextStyle(color: Colors.white70),
),
),
IconButton(
onPressed: onClose,
icon: const Icon(
Icons.close,
size: 18,
color: Colors.white,
),
tooltip: 'Close console',
),
],
),
),
const Divider(height: 1, color: Color(0xFF333333)),
if (entries.isEmpty)
const Expanded(
child: Center(
child: Text(
'(console kosong — console.log dari JS akan muncul di sini)',
style: TextStyle(color: Color(0xFF888888), fontSize: 12),
),
),
)
else
Expanded(
child: ListView.separated(
padding: const EdgeInsets.all(8),
reverse: true,
itemCount: entries.length,
separatorBuilder: (_, _) =>
const Divider(height: 1, color: Color(0xFF222222)),
itemBuilder: (_, i) {
final e = entries[entries.length - 1 - i];
final time =
'${e.time.hour.toString().padLeft(2, '0')}:${e.time.minute.toString().padLeft(2, '0')}:${e.time.second.toString().padLeft(2, '0')}';
return InkWell(
onTap: () => onCopy(e.message),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 6,
horizontal: 4,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 2,
),
decoration: BoxDecoration(
color: _levelColor(e.level),
borderRadius: BorderRadius.circular(4),
),
child: Text(
e.level,
style: const TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 6),
Text(
time,
style: const TextStyle(
color: Color(0xFF888888),
fontSize: 10,
fontFamily: 'monospace',
),
),
const SizedBox(width: 6),
Expanded(
child: SelectableText(
e.message,
style: const TextStyle(
color: Color(0xFFD4D4D4),
fontSize: 11,
fontFamily: 'monospace',
),
),
),
],
),
),
);
},
),
),
Container(
color: const Color(0xFF1E1E1E),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: const Text(
'Tap baris untuk copy • console.error/warn/log dari JS + [JS] bridge error tampil di sini',
style: TextStyle(color: Color(0xFF888888), fontSize: 10),
),
),
],
),
),
);
}
}
class _Badge extends StatelessWidget {
final WebviewEventType type;
const _Badge({required this.type});
@override
Widget build(BuildContext context) {
final color = switch (type) {
WebviewEventType.formSubmitted => const Color(0xFF6750A4),
WebviewEventType.formCancelled => const Color(0xFF79747E),
WebviewEventType.formError => const Color(0xFFB3261E),
WebviewEventType.formSuccess => const Color(0xFF2E7D32),
WebviewEventType.imageCaptured => const Color(0xFF00639A),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
),
child: Text(
type.name,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w700,
),
),
);
}
}
// ── Hardware test examples (rg_bridge device addon) ────────────────
// Tiap test punya file sendiri di lib/pages/ + scaffold di lib/widgets/.
/// Menu utama example: demo WebView + 16 test hardware + device info.
class _MenuPage extends StatelessWidget {
const _MenuPage();
void _go(BuildContext context, Widget page) {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => page));
}
@override
Widget build(BuildContext context) {
final groups =
<
String,
List<({IconData icon, String title, String subtitle, Widget page})>
>{
'WebView': [
(
icon: Icons.web,
title: 'WebView Demo',
subtitle: 'initSession + event bridge',
page: WebViewExamplePage(sdk: exampleSdk),
),
],
'Form': [
(
icon: Icons.assignment,
title: 'Form Submission',
subtitle: 'initFormSubmission → submitForm',
page: FormSubmissionPage(sdk: exampleSdk),
),
],
'Info Perangkat': [
(
icon: Icons.smartphone,
title: 'Device Info',
subtitle: 'getDeviceInfo + battery/memory/storage',
page: const DeviceInfoPage(),
),
],
'Sensor Gerak': [
(
icon: Icons.touch_app,
title: 'Touchscreen Test',
subtitle: 'sdk.device.checkTouchscreen()',
page: const TouchscreenTestPage(),
),
(
icon: Icons.screen_rotation,
title: 'Accelerometer Test',
subtitle: 'sdk.device.checkAccelerometer()',
page: const AccelerometerTestPage(),
),
(
icon: Icons.rotate_right,
title: 'Gyroscope Test',
subtitle: 'sdk.device.checkGyroscope()',
page: const GyroscopeTestPage(),
),
],
'Biometrik': [
(
icon: Icons.fingerprint,
title: 'Biometric Test',
subtitle: 'sdk.device.checkBiometric()',
page: const BiometricTestPage(),
),
],
'Konektivitas & Lokasi': [
(
icon: Icons.bluetooth,
title: 'Bluetooth Test',
subtitle: 'sdk.device.checkBluetooth()',
page: const BluetoothTestPage(),
),
(
icon: Icons.wifi,
title: 'WiFi Test',
subtitle: 'sdk.device.checkWifi()',
page: const WifiTestPage(),
),
(
icon: Icons.gps_fixed,
title: 'GPS Test',
subtitle: 'sdk.device.checkGps()',
page: const GpsTestPage(),
),
],
'Media': [
(
icon: Icons.camera_alt,
title: 'Camera Test',
subtitle: 'sdk.device.checkCamera()',
page: const CameraTestPage(),
),
(
icon: Icons.volume_up,
title: 'Speaker Test',
subtitle: 'sdk.device.checkSpeaker()',
page: const SpeakerTestPage(),
),
(
icon: Icons.vibration,
title: 'Vibration Test',
subtitle: 'sdk.device.checkVibration()',
page: const VibrationTestPage(),
),
(
icon: Icons.record_voice_over,
title: 'Speech Test',
subtitle: 'sdk.device.checkSpeech()',
page: const SpeechTestPage(),
),
(
icon: Icons.mic,
title: 'Microphone Test',
subtitle: 'sdk.device.checkMicrophone()',
page: const MicrophoneTestPage(),
),
(
icon: Icons.flashlight_on,
title: 'Flashlight Test',
subtitle: 'sdk.device.checkFlashlight()',
page: const FlashlightTestPage(),
),
],
'Tombol': [
(
icon: Icons.smart_button,
title: 'Navigation Button Test',
subtitle: 'sdk.device.checkNavigationButton()',
page: const NavigationButtonTestPage(),
),
(
icon: Icons.power_settings_new,
title: 'Power Button Test',
subtitle: 'sdk.device.checkPower()',
page: const PowerButtonTestPage(),
),
(
icon: Icons.volume_up_outlined,
title: 'Volume Button Test',
subtitle: 'sdk.device.checkVolumeButton()',
page: const VolumeButtonTestPage(),
),
],
};
return Scaffold(
appBar: AppBar(title: const Text('rg_bridge example')),
body: ListView(
padding: const EdgeInsets.all(12),
children: [
for (final entry in groups.entries) ...[
Padding(
padding: const EdgeInsets.fromLTRB(4, 12, 4, 4),
child: Text(
entry.key,
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
),
),
for (final item in entry.value)
Card(
child: ListTile(
leading: Icon(item.icon),
title: Text(item.title),
subtitle: Text(item.subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => _go(context, item.page),
),
),
],
],
),
);
}
}