vnc_viewer

vnc_viewer is a Flutter plugin for embedding an Android VNC client inside your app.

It connects to a VNC/RFB server, renders the remote desktop through a Flutter Texture, and exposes APIs for sending keyboard and pointer events from Dart.

Features

  • Connect to a VNC server with hostName, port, and password
  • Configure a connection timeout from Dart, with a built-in default timeout
  • Render the remote framebuffer inside Flutter with VncViewerWidget
  • Receive connection lifecycle callbacks such as onStart, onClose, and onError
  • React to framebuffer size changes with onImageResize
  • Send keyboard and pointer events with VncViewerHandel
  • Includes an example app for quick local verification

Platform Support

Platform Status
Android Supported
iOS Not supported
macOS Not supported
Windows Not supported
Linux Not supported
Web Not supported

Requirements

  • Flutter >=3.3.0
  • Dart SDK >=3.3.4 <4.0.0
  • Android minSdkVersion 19
  • Android app must have network access permission

The plugin currently builds native libraries for:

  • armeabi-v7a
  • arm64-v8a

Installation

Add the dependency to your pubspec.yaml:

dependencies:
  vnc_viewer: ^0.0.6

Then run:

flutter pub get

Android Configuration

Your host app must declare Internet permission in android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:label="your_app_name">
        ...
    </application>
</manifest>

If you plan to test a release build, add the permission to the main manifest, not only the debug manifest.

Quick Start

Use VncViewerWidget when you want a ready-to-embed viewer widget:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:vnc_viewer/vnc_viewer_handel.dart';
import 'package:vnc_viewer/vnc_viewer_widget.dart';

class VncPage extends StatefulWidget {
  const VncPage({
    super.key,
    required this.hostName,
    required this.port,
    required this.password,
  });

  final String hostName;
  final int port;
  final String password;

  @override
  State<VncPage> createState() => _VncPageState();
}

class _VncPageState extends State<VncPage> {
  final VncViewerHandel _handle = VncViewerHandel();
  int? _clientId;
  String _status = 'Connecting...';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('${widget.hostName}:${widget.port}'),
        actions: [
          IconButton(
            onPressed: _clientId == null ? null : _sendEnter,
            icon: const Icon(Icons.keyboard_return),
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: VncViewerWidget(
              hostName: widget.hostName,
              port: widget.port,
              password: widget.password,
              onStart: (clientId) {
                setState(() {
                  _clientId = clientId;
                  _status = 'Connected';
                });
              },
              onClose: (_) {
                setState(() {
                  _clientId = null;
                  _status = 'Closed';
                });
              },
              onImageResize: () {
                setState(() {
                  _status = 'Rendering';
                });
              },
              onError: (message) {
                setState(() {
                  _status = 'Error: $message';
                });
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(message)),
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(child: Text(_status)),
                FilledButton(
                  onPressed: _clientId == null ? null : _sendEnter,
                  child: const Text('Send Enter'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _sendEnter() async {
    final clientId = _clientId;
    if (clientId == null) {
      return;
    }

    _handle.sendKey(clientId, 0xff0d, true);
    await Future.delayed(const Duration(milliseconds: 80));
    _handle.sendKey(clientId, 0xff0d, false);
  }
}

API Overview

VncViewerWidget

The widget manages VNC client initialization, event subscription, rendering, and cleanup for you.

Constructor:

const VncViewerWidget({
  Key? key,
  required String hostName,
  required String password,
  int port = 5900,
  Duration connectTimeout = VncViewerWidget.defaultConnectTimeout,
  void Function(int clientId)? onStart,
  void Function(int clientId)? onClose,
  VoidCallback? onImageResize,
  void Function(String msg)? onError,
})

Parameters:

  • hostName: Hostname or IP address of the VNC server
  • port: TCP port of the VNC server, default is 5900
  • password: VNC password, pass an empty string if the server does not require one
  • connectTimeout: Timeout used while establishing the native VNC connection. Defaults to Duration(seconds: 10)
  • onStart: Called after the native VNC connection is established successfully
  • onClose: Called when the VNC session is closed
  • onImageResize: Called when the remote framebuffer size changes
  • onError: Called when initialization or runtime errors occur

Behavior:

  • Shows a loading state while connecting
  • Shows an error or closed state instead of staying on the loading indicator when the session fails or ends
  • Scales the remote frame to fit the available viewport
  • Wraps the frame with InteractiveViewer for zooming and panning
  • Closes the native client automatically on widget disposal

Logging

Android-side logs use the tag libvncviewer_flutter.

The plugin now logs key lifecycle steps including:

  • Init request and validation
  • Event stream registration
  • Native start, connect success, framebuffer resize, and close
  • Error and cleanup paths

VncViewerHandel

VncViewerHandel exposes low-level methods for manual control:

final handle = VncViewerHandel();

Available methods:

  • getPlatformVersion()
  • initVncClient(String hostName, int port, String password, {Duration? connectTimeout})
  • startVncClient(int clientId)
  • closeVncClient(int clientId)
  • sendPointer(int clientId, int x, int y, int mask)
  • sendKey(int clientId, int key, bool down)

In most cases, you should let VncViewerWidget manage initialization and lifecycle, and only use VncViewerHandel for sending input events after onStart gives you a clientId.

Timeout example:

final clientId = await handle.initVncClient(
  '192.168.1.20',
  5900,
  'secret',
  connectTimeout: const Duration(seconds: 5),
);

Sending Input Events

Keyboard events

sendKey expects VNC/X11 keysym values.

Example: send Enter

handle.sendKey(clientId, 0xff0d, true);
handle.sendKey(clientId, 0xff0d, false);

Pointer events

sendPointer expects remote coordinates and a VNC button mask.

Common button mask values:

  • 0: release all buttons
  • 1: left button
  • 2: middle button
  • 4: right button

Example:

handle.sendPointer(clientId, 120, 80, 1);
handle.sendPointer(clientId, 120, 80, 0);

Note that VncViewerWidget currently focuses on rendering. If you want full remote mouse or touch control, map your own Flutter gestures to sendPointer.

Example App

The bundled example supports --dart-define so you can launch a server directly:

cd example
flutter run \
  --dart-define=VNC_HOST=192.168.1.100 \
  --dart-define=VNC_PORT=5900 \
  --dart-define=VNC_PASSWORD=secret \
  --dart-define=VNC_AUTO_CONNECT=true

If VNC_AUTO_CONNECT is omitted or set to false, the example app shows a form where you can enter the connection details manually.

Error Handling

Recommended practice:

  • Always provide onError
  • Validate host and port before opening the viewer
  • Treat clientId == null or 0 as initialization failure
  • Clean up any custom input controllers when the page is disposed

Typical failure reasons:

  • Wrong host, port, or password
  • The VNC server is unreachable
  • The server requires an authentication flow not covered by the current API
  • Missing INTERNET permission in the Android app

Limitations

  • Android only at the moment
  • Current Dart API only accepts hostName, port, and password
  • No built-in username field for auth flows that require a non-empty username
  • No clipboard, file transfer, or audio API exposed in Dart
  • No built-in desktop control overlay; custom gesture mapping is up to the app

Testing

The package includes Dart-side unit tests for:

  • VncViewerHandel delegation
  • MethodChannelVncViewer method channel calls

Run tests with:

flutter test

License

This package is distributed under the GNU General Public License, version 2 or later (GPL-2.0-or-later). See LICENSE.