flutter_colab 0.1.0 copy "flutter_colab: ^0.1.0" to clipboard
flutter_colab: ^0.1.0 copied to clipboard

A Dart/Flutter client for Google Colab by Chinmay Nagar. Execute code, manage files, and control Colab runtimes from desktop apps. No native config required.

example/lib/main.dart

import 'package:flutter_colab/flutter_colab.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const ColabExampleApp());
}

// ---------------------------------------------------------------------------
// Snippet model
// ---------------------------------------------------------------------------

class CodeSnippet {
  const CodeSnippet({
    required this.label,
    required this.icon,
    required this.code,
    this.description = '',
  });
  final String label;
  final IconData icon;
  final String code;
  final String description;
}

const _snippets = [
  CodeSnippet(
    label: 'Hello World',
    icon: Icons.waving_hand,
    description: 'Basic print statement',
    code: "print('Hello from Colab! 🚀')",
  ),
  CodeSnippet(
    label: 'System Info',
    icon: Icons.computer,
    description: 'Python & OS details',
    code: '''import platform, os
print(f"Python : {platform.python_version()}")
print(f"OS     : {platform.system()} {platform.release()}")
print(f"CPU    : {os.cpu_count()} cores")
print(f"Arch   : {platform.machine()}")''',
  ),
  CodeSnippet(
    label: 'Check GPU',
    icon: Icons.memory,
    description: 'NVIDIA GPU info',
    code: '!nvidia-smi',
  ),
  CodeSnippet(
    label: 'Install Package',
    icon: Icons.download,
    description: 'pip install example',
    code: '!pip install requests -q\nprint("requests installed ✓")',
  ),
  CodeSnippet(
    label: 'Uninstall Package',
    icon: Icons.delete_outline,
    description: 'pip uninstall example',
    code: '!pip uninstall requests -y\nprint("requests removed ✓")',
  ),
  CodeSnippet(
    label: 'List Packages',
    icon: Icons.list,
    description: 'Show installed packages',
    code: '!pip list',
  ),
  CodeSnippet(
    label: 'Disk Space',
    icon: Icons.storage,
    description: 'Available disk space',
    code: '!df -h',
  ),
  CodeSnippet(
    label: 'Memory Info',
    icon: Icons.speed,
    description: 'RAM usage',
    code: '''import psutil
mem = psutil.virtual_memory()
print(f"Total    : {mem.total / 1024**3:.1f} GB")
print(f"Available: {mem.available / 1024**3:.1f} GB")
print(f"Used     : {mem.percent}%")''',
  ),
  CodeSnippet(
    label: 'Math Example',
    icon: Icons.calculate,
    description: 'NumPy computation',
    code: '''import numpy as np
a = np.array([1, 2, 3, 4, 5])
print(f"Array  : {a}")
print(f"Mean   : {np.mean(a)}")
print(f"Sum    : {np.sum(a)}")
print(f"Std    : {np.std(a):.4f}")''',
  ),
  CodeSnippet(
    label: 'Custom Code',
    icon: Icons.edit,
    description: 'Write your own code',
    code: '# Write your Python code here\n',
  ),
];

// ---------------------------------------------------------------------------
// App
// ---------------------------------------------------------------------------

class ColabExampleApp extends StatelessWidget {
  const ColabExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Colab Client',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFFF9AB00), // Colab orange-yellow
        brightness: Brightness.dark,
        useMaterial3: true,
        fontFamily: 'monospace',
      ),
      home: const ColabHomePage(),
    );
  }
}

// ---------------------------------------------------------------------------
// Home page
// ---------------------------------------------------------------------------

enum _ConnState { disconnected, connecting, connected }

class ColabHomePage extends StatefulWidget {
  const ColabHomePage({super.key});

  @override
  State<ColabHomePage> createState() => _ColabHomePageState();
}

class _ColabHomePageState extends State<ColabHomePage> {
  final _client = ColabClient();
  final _codeCtrl = TextEditingController(
    text: "print('Hello from Colab! 🚀')",
  );
  final _outputScrollCtrl = ScrollController();

  _ConnState _connState = _ConnState.disconnected;
  bool _executing = false;
  String _statusMsg = 'Not connected';

  final _outputItems = <_OutputItem>[];

  // ---------------------------------------------------------------------------
  // Actions
  // ---------------------------------------------------------------------------

  Future<void> _loginAndConnect() async {
    setState(() {
      _connState = _ConnState.connecting;
      _statusMsg = 'Logging in…';
    });

    try {
      await _client.login();

      setState(() => _statusMsg = 'Assigning runtime…');
      await _client.connect();

      setState(() => _statusMsg = 'Starting kernel…');
      await _client.openSession();

      _client.startKeepAlive();

      setState(() {
        _connState = _ConnState.connected;
        _statusMsg = 'Connected · ${_client.assignment?.endpoint ?? ''}';
      });
    } catch (e) {
      setState(() {
        _connState = _ConnState.disconnected;
        _statusMsg = 'Error: $e';
      });
    }
  }

  Future<void> _disconnect() async {
    _client.stopKeepAlive();
    await _client.disconnect();
    setState(() {
      _connState = _ConnState.disconnected;
      _statusMsg = 'Disconnected';
    });
  }

  Future<void> _execute() async {
    if (_connState != _ConnState.connected) {
      _addOutput(_OutputItem.stderr('Not connected. Click "Connect" first.'));
      return;
    }
    final code = _codeCtrl.text.trim();
    if (code.isEmpty) return;

    setState(() {
      _executing = true;
      _outputItems.clear();
    });

    try {
      await for (final out in _client.executeStream(code)) {
        _OutputItem? item;
        switch (out) {
          case StdoutOutput(:final text):
            item = _OutputItem.stdout(text);
          case StderrOutput(:final text):
            item = _OutputItem.stderr(text);
          case ErrorOutput(:final error):
            item = _OutputItem.error('${error.name}: ${error.value}');
          case ResultOutput(:final data):
            item = _OutputItem.result(data['text/plain']?.toString() ?? '');
          case DisplayDataOutput():
            item = _OutputItem.result('[display data]');
        }
        setState(() => _outputItems.add(item!));
        _scrollToBottom();
      }
    } catch (e) {
      setState(() => _outputItems.add(_OutputItem.error(e.toString())));
    } finally {
      setState(() => _executing = false);
    }
  }

  void _loadSnippet(CodeSnippet s) {
    _codeCtrl.text = s.code;
    _outputItems.clear();
    setState(() {});
  }

  void _addOutput(_OutputItem item) => setState(() => _outputItems.add(item));

  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_outputScrollCtrl.hasClients) {
        _outputScrollCtrl.animateTo(
          _outputScrollCtrl.position.maxScrollExtent,
          duration: const Duration(milliseconds: 150),
          curve: Curves.easeOut,
        );
      }
    });
  }

  // ---------------------------------------------------------------------------
  // Build
  // ---------------------------------------------------------------------------

  @override
  void dispose() {
    _client.close();
    _codeCtrl.dispose();
    _outputScrollCtrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;

    return Scaffold(
      backgroundColor: const Color(0xFF1E1E1E),
      appBar: _buildAppBar(cs),
      body: Row(
        children: [
          // Left: snippet sidebar
          _SnippetSidebar(snippets: _snippets, onSelect: _loadSnippet),
          // Right: main area
          Expanded(
            child: Column(
              children: [
                // Status bar
                _StatusBar(state: _connState, message: _statusMsg),
                // Code block
                Expanded(flex: 4, child: _CodeBlock(controller: _codeCtrl)),
                // Divider
                const Divider(
                  height: 1,
                  thickness: 1,
                  color: Color(0xFF3C3C3C),
                ),
                // Output block
                Expanded(
                  flex: 3,
                  child: _OutputBlock(
                    items: _outputItems,
                    scrollController: _outputScrollCtrl,
                    executing: _executing,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
      floatingActionButton: _buildFab(cs),
    );
  }

  AppBar _buildAppBar(ColorScheme cs) {
    return AppBar(
      backgroundColor: const Color(0xFF2D2D2D),
      elevation: 0,
      title: Row(
        children: [
          Container(
            width: 28,
            height: 28,
            decoration: BoxDecoration(
              color: const Color(0xFFF9AB00),
              borderRadius: BorderRadius.circular(6),
            ),
            child: const Icon(Icons.science, size: 16, color: Colors.black),
          ),
          const SizedBox(width: 10),
          const Text(
            'Colab Client',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.w600,
              color: Colors.white,
              letterSpacing: 0.3,
            ),
          ),
        ],
      ),
      actions: [
        // Connect / Disconnect button
        if (_connState == _ConnState.disconnected)
          Padding(
            padding: const EdgeInsets.only(right: 8),
            child: FilledButton.icon(
              style: FilledButton.styleFrom(
                backgroundColor: const Color(0xFFF9AB00),
                foregroundColor: Colors.black,
              ),
              onPressed: _loginAndConnect,
              icon: const Icon(Icons.cloud_upload_outlined, size: 16),
              label: const Text('Connect'),
            ),
          )
        else if (_connState == _ConnState.connecting)
          const Padding(
            padding: EdgeInsets.only(right: 16),
            child: SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(strokeWidth: 2),
            ),
          )
        else
          Padding(
            padding: const EdgeInsets.only(right: 8),
            child: OutlinedButton.icon(
              style: OutlinedButton.styleFrom(
                foregroundColor: Colors.redAccent,
                side: const BorderSide(color: Colors.redAccent),
              ),
              onPressed: _disconnect,
              icon: const Icon(Icons.cloud_off_outlined, size: 16),
              label: const Text('Disconnect'),
            ),
          ),
      ],
    );
  }

  Widget _buildFab(ColorScheme cs) {
    return FloatingActionButton.extended(
      backgroundColor: _connState == _ConnState.connected
          ? const Color(0xFFF9AB00)
          : Colors.grey.shade700,
      foregroundColor: Colors.black,
      onPressed: _executing ? null : _execute,
      icon: _executing
          ? const SizedBox(
              width: 18,
              height: 18,
              child: CircularProgressIndicator(
                strokeWidth: 2,
                color: Colors.black,
              ),
            )
          : const Icon(Icons.play_arrow_rounded),
      label: Text(_executing ? 'Running…' : 'Run Cell'),
    );
  }
}

// ---------------------------------------------------------------------------
// Snippet Sidebar
// ---------------------------------------------------------------------------

class _SnippetSidebar extends StatefulWidget {
  const _SnippetSidebar({required this.snippets, required this.onSelect});
  final List<CodeSnippet> snippets;
  final void Function(CodeSnippet) onSelect;

  @override
  State<_SnippetSidebar> createState() => _SnippetSidebarState();
}

class _SnippetSidebarState extends State<_SnippetSidebar> {
  int _selected = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 200,
      color: const Color(0xFF252526),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Padding(
            padding: EdgeInsets.fromLTRB(12, 14, 12, 6),
            child: Text(
              'SNIPPETS',
              style: TextStyle(
                fontSize: 10,
                fontWeight: FontWeight.w700,
                color: Color(0xFF858585),
                letterSpacing: 1.2,
              ),
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: widget.snippets.length,
              itemBuilder: (_, i) {
                final s = widget.snippets[i];
                final active = _selected == i;
                return InkWell(
                  onTap: () {
                    setState(() => _selected = i);
                    widget.onSelect(s);
                  },
                  child: Container(
                    color: active
                        ? const Color(0xFF37373D)
                        : Colors.transparent,
                    padding: const EdgeInsets.symmetric(
                      horizontal: 12,
                      vertical: 8,
                    ),
                    child: Row(
                      children: [
                        Icon(
                          s.icon,
                          size: 15,
                          color: active
                              ? const Color(0xFFF9AB00)
                              : const Color(0xFF858585),
                        ),
                        const SizedBox(width: 8),
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                s.label,
                                style: TextStyle(
                                  fontSize: 12,
                                  color: active
                                      ? Colors.white
                                      : const Color(0xFFCCCCCC),
                                  fontWeight: active
                                      ? FontWeight.w600
                                      : FontWeight.normal,
                                ),
                              ),
                              Text(
                                s.description,
                                style: const TextStyle(
                                  fontSize: 10,
                                  color: Color(0xFF6A6A6A),
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Status Bar
// ---------------------------------------------------------------------------

class _StatusBar extends StatelessWidget {
  const _StatusBar({required this.state, required this.message});
  final _ConnState state;
  final String message;

  @override
  Widget build(BuildContext context) {
    final color = switch (state) {
      _ConnState.connected => const Color(0xFF4EC994),
      _ConnState.connecting => const Color(0xFFF9AB00),
      _ConnState.disconnected => const Color(0xFF858585),
    };
    final dot = switch (state) {
      _ConnState.connected => '●',
      _ConnState.connecting => '◌',
      _ConnState.disconnected => '○',
    };

    return Container(
      width: double.infinity,
      color: const Color(
        0xFF007ACC,
      ).withValues(alpha: state == _ConnState.connected ? 1 : 0.4),
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      child: Row(
        children: [
          Text(dot, style: TextStyle(color: color, fontSize: 12)),
          const SizedBox(width: 6),
          Expanded(
            child: Text(
              message,
              style: const TextStyle(
                color: Colors.white70,
                fontSize: 11,
                fontFamily: 'monospace',
              ),
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Code Block
// ---------------------------------------------------------------------------

class _CodeBlock extends StatelessWidget {
  const _CodeBlock({required this.controller});
  final TextEditingController controller;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: const Color(0xFF1E1E1E),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Header
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
            color: const Color(0xFF2D2D2D),
            child: Row(
              children: [
                const Icon(Icons.code, size: 14, color: Color(0xFFF9AB00)),
                const SizedBox(width: 6),
                const Text(
                  'Code Cell',
                  style: TextStyle(
                    fontSize: 11,
                    color: Color(0xFFCCCCCC),
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const Spacer(),
                // Copy button
                _IconAction(
                  icon: Icons.copy_outlined,
                  tooltip: 'Copy code',
                  onTap: () =>
                      Clipboard.setData(ClipboardData(text: controller.text)),
                ),
                const SizedBox(width: 4),
                // Clear button
                _IconAction(
                  icon: Icons.clear,
                  tooltip: 'Clear',
                  onTap: () => controller.clear(),
                ),
              ],
            ),
          ),
          // Editor
          Expanded(
            child: TextField(
              controller: controller,
              maxLines: null,
              expands: true,
              style: const TextStyle(
                fontFamily: 'monospace',
                fontSize: 13,
                color: Color(0xFFD4D4D4),
                height: 1.5,
              ),
              decoration: const InputDecoration(
                contentPadding: EdgeInsets.all(14),
                border: InputBorder.none,
                hintText: '# Write Python code here…',
                hintStyle: TextStyle(color: Color(0xFF4A4A4A)),
              ),
              keyboardType: TextInputType.multiline,
            ),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Output Block
// ---------------------------------------------------------------------------

class _OutputBlock extends StatelessWidget {
  const _OutputBlock({
    required this.items,
    required this.scrollController,
    required this.executing,
  });
  final List<_OutputItem> items;
  final ScrollController scrollController;
  final bool executing;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: const Color(0xFF1A1A1A),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Header
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
            color: const Color(0xFF252526),
            child: Row(
              children: [
                Icon(
                  executing ? Icons.hourglass_top_rounded : Icons.terminal,
                  size: 14,
                  color: executing
                      ? const Color(0xFFF9AB00)
                      : const Color(0xFF4EC994),
                ),
                const SizedBox(width: 6),
                Text(
                  executing ? 'Executing…' : 'Output',
                  style: const TextStyle(
                    fontSize: 11,
                    color: Color(0xFFCCCCCC),
                    fontWeight: FontWeight.w600,
                  ),
                ),
                const Spacer(),
                if (items.isNotEmpty)
                  _IconAction(
                    icon: Icons.delete_sweep_outlined,
                    tooltip: 'Clear output',
                    onTap: () {},
                  ),
              ],
            ),
          ),
          // Output content
          Expanded(
            child: items.isEmpty && !executing
                ? const Center(
                    child: Text(
                      'Run a cell to see output here',
                      style: TextStyle(color: Color(0xFF4A4A4A), fontSize: 12),
                    ),
                  )
                : ListView.builder(
                    controller: scrollController,
                    padding: const EdgeInsets.all(12),
                    itemCount: items.length + (executing ? 1 : 0),
                    itemBuilder: (_, i) {
                      if (i == items.length) {
                        return const Padding(
                          padding: EdgeInsets.only(top: 4),
                          child: Row(
                            children: [
                              SizedBox(
                                width: 12,
                                height: 12,
                                child: CircularProgressIndicator(
                                  strokeWidth: 1.5,
                                  color: Color(0xFFF9AB00),
                                ),
                              ),
                              SizedBox(width: 8),
                              Text(
                                'running…',
                                style: TextStyle(
                                  color: Color(0xFF858585),
                                  fontSize: 11,
                                ),
                              ),
                            ],
                          ),
                        );
                      }
                      return _OutputLine(item: items[i]);
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

class _OutputLine extends StatelessWidget {
  const _OutputLine({required this.item});
  final _OutputItem item;

  @override
  Widget build(BuildContext context) {
    final color = switch (item.type) {
      _OutputType.stdout => const Color(0xFFD4D4D4),
      _OutputType.stderr => const Color(0xFFCE9178),
      _OutputType.error => const Color(0xFFF44747),
      _OutputType.result => const Color(0xFF4EC994),
    };

    final prefix = switch (item.type) {
      _OutputType.stdout => '',
      _OutputType.stderr => '⚠ ',
      _OutputType.error => '✗ ',
      _OutputType.result => '→ ',
    };

    return Padding(
      padding: const EdgeInsets.only(bottom: 2),
      child: Text(
        '$prefix${item.text}',
        style: TextStyle(
          color: color,
          fontFamily: 'monospace',
          fontSize: 12,
          height: 1.5,
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Small icon action button
// ---------------------------------------------------------------------------

class _IconAction extends StatelessWidget {
  const _IconAction({
    required this.icon,
    required this.tooltip,
    required this.onTap,
  });
  final IconData icon;
  final String tooltip;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Tooltip(
      message: tooltip,
      child: InkWell(
        borderRadius: BorderRadius.circular(4),
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.all(4),
          child: Icon(icon, size: 14, color: const Color(0xFF858585)),
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Output item model
// ---------------------------------------------------------------------------

enum _OutputType { stdout, stderr, error, result }

class _OutputItem {
  const _OutputItem._(this.type, this.text);
  factory _OutputItem.stdout(String t) => _OutputItem._(_OutputType.stdout, t);
  factory _OutputItem.stderr(String t) => _OutputItem._(_OutputType.stderr, t);
  factory _OutputItem.error(String t) => _OutputItem._(_OutputType.error, t);
  factory _OutputItem.result(String t) => _OutputItem._(_OutputType.result, t);

  final _OutputType type;
  final String text;
}
0
likes
150
points
28
downloads

Documentation

API reference

Publisher

verified publisherchipnexa.in

Weekly Downloads

A Dart/Flutter client for Google Colab by Chinmay Nagar. Execute code, manage files, and control Colab runtimes from desktop apps. No native config required.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, http, shared_preferences, shelf, shelf_router, uuid, web_socket_channel

More

Packages that depend on flutter_colab