json_view_plus 1.0.0 copy "json_view_plus: ^1.0.0" to clipboard
json_view_plus: ^1.0.0 copied to clipboard

High-performance Flutter JSON tree viewer with lazy row rendering, syntax highlighting, expansion controls, and Chrome DevTools-style large JSON browsing.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:json_view_plus/json_view_plus.dart';

import 'json_data.dart';

void main() => runApp(const MyApp());

class _DemoTheme {
  static ThemeData light() => _theme(Brightness.light);
  static ThemeData dark() => _theme(Brightness.dark);

  static ThemeData _theme(Brightness brightness) {
    final primary = _DemoTokens.primaryColor(brightness);

    return ThemeData(
      useMaterial3: true,
      brightness: brightness,
      fontFamily: _DemoTokens.fontFamilyBase,
      colorScheme: ColorScheme.fromSeed(
        seedColor: primary,
        brightness: brightness,
        primary: primary,
        error: _DemoTokens.errorColor(brightness),
        surface: _DemoTokens.primaryBackground(brightness),
        outline: _DemoTokens.primaryBorder(brightness),
      ),
      scaffoldBackgroundColor: _DemoTokens.secondaryBackground(brightness),
      appBarTheme: AppBarTheme(
        backgroundColor: _DemoTokens.primaryBackground(brightness),
        foregroundColor: _DemoTokens.primaryText(brightness),
        elevation: 0,
        centerTitle: false,
        titleTextStyle: TextStyle(
          fontSize: _DemoTokens.fontSizeMD,
          fontWeight: _DemoTokens.fontWeightSemibold,
          color: _DemoTokens.primaryText(brightness),
        ),
      ),
      cardTheme: CardThemeData(
        color: _DemoTokens.primaryBackground(brightness),
        elevation: 0,
        margin: EdgeInsets.zero,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(_DemoTokens.radiusXLarge),
          side: BorderSide(color: _DemoTokens.primaryBorder(brightness)),
        ),
      ),
      textTheme: _textTheme(brightness),
      inputDecorationTheme: InputDecorationTheme(
        filled: true,
        fillColor: _DemoTokens.primaryBackground(brightness),
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(_DemoTokens.radiusXLarge),
        ),
        contentPadding: const EdgeInsets.symmetric(
          horizontal: 16,
          vertical: 12,
        ),
        hintStyle: TextStyle(
          fontSize: _DemoTokens.fontSizeSM,
          color: _DemoTokens.tertiaryText(brightness),
        ),
      ),
      iconButtonTheme: IconButtonThemeData(
        style: IconButton.styleFrom(
          foregroundColor: _DemoTokens.secondaryText(brightness),
        ),
      ),
      dividerTheme: DividerThemeData(
        color: _DemoTokens.primaryBorder(brightness),
        thickness: 1,
        space: 1,
      ),
      tooltipTheme: TooltipThemeData(
        decoration: BoxDecoration(
          color: brightness == Brightness.light
              ? _DemoTokens.gray800
              : _DemoTokens.gray700,
          borderRadius: BorderRadius.circular(_DemoTokens.radiusLarge),
        ),
        textStyle: const TextStyle(
          fontSize: _DemoTokens.fontSizeXS,
          color: Colors.white,
        ),
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      ),
    );
  }

  static TextTheme _textTheme(Brightness brightness) {
    final primary = _DemoTokens.primaryText(brightness);
    return TextTheme(
      displayLarge: TextStyle(
        fontSize: _DemoTokens.fontSize3XL,
        fontWeight: _DemoTokens.fontWeightSemibold,
        color: primary,
      ),
      displayMedium: TextStyle(
        fontSize: _DemoTokens.fontSize2XL,
        fontWeight: _DemoTokens.fontWeightSemibold,
        color: primary,
      ),
      titleLarge: TextStyle(
        fontSize: _DemoTokens.fontSizeLG,
        fontWeight: _DemoTokens.fontWeightSemibold,
        color: primary,
      ),
      titleMedium: TextStyle(
        fontSize: _DemoTokens.fontSizeMD,
        fontWeight: _DemoTokens.fontWeightSemibold,
        color: primary,
      ),
      bodyLarge: TextStyle(fontSize: _DemoTokens.fontSizeMD, color: primary),
      bodyMedium: TextStyle(fontSize: _DemoTokens.fontSizeSM, color: primary),
      bodySmall: TextStyle(fontSize: _DemoTokens.fontSizeXS, color: primary),
      labelLarge: TextStyle(
        fontSize: _DemoTokens.fontSizeSM,
        fontWeight: _DemoTokens.fontWeightSemibold,
        color: primary,
      ),
      labelMedium: TextStyle(
        fontSize: _DemoTokens.fontSizeXS,
        fontWeight: _DemoTokens.fontWeightSemibold,
        color: primary,
      ),
      labelSmall: TextStyle(
        fontSize: _DemoTokens.fontSizeXS,
        fontWeight: _DemoTokens.fontWeightSemibold,
        color: primary,
      ),
    );
  }
}

class _DemoTokens {
  static const lightPrimaryBackground = Color(0xFFFFFFFF);
  static const lightSecondaryBackground = Color(0xFFF9FAFB);
  static const lightPrimaryText = Color(0xFF111827);
  static const lightSecondaryText = Color(0xFF4B5563);
  static const lightTertiaryText = Color(0xFF9CA3AF);
  static const lightPrimaryBorder = Color(0xFFE5E7EB);

  static const darkPrimaryBackground = Color(0xFF141414);
  static const darkSecondaryBackground = Color(0xFF1C1C1C);
  static const darkPrimaryText = Color(0xFFFFFFFF);
  static const darkSecondaryText = Color(0x99FFFFFF);
  static const darkTertiaryText = Color(0x66FFFFFF);
  static const darkPrimaryBorder = Color(0xFF383838);

  static const primary = Color(0xFF2563EB);
  static const primaryDark = Color(0xFF3B82F6);
  static const error = Color(0xFFDC2626);
  static const errorDark = Color(0xFFEF4444);
  static const gray700 = Color(0xFF374151);
  static const gray800 = Color(0xFF1F2937);

  static const radiusLarge = 8.0;
  static const radiusXLarge = 12.0;
  static const String? fontFamilyBase = null;
  static const fontSize3XL = 30.0;
  static const fontSize2XL = 24.0;
  static const fontSizeLG = 18.0;
  static const fontSizeMD = 16.0;
  static const fontSizeSM = 14.0;
  static const fontSizeXS = 12.0;
  static const fontWeightSemibold = FontWeight.w600;

  static Color primaryColor(Brightness brightness) =>
      brightness == Brightness.light ? primary : primaryDark;

  static Color errorColor(Brightness brightness) =>
      brightness == Brightness.light ? error : errorDark;

  static Color primaryBackground(Brightness brightness) =>
      brightness == Brightness.light
          ? lightPrimaryBackground
          : darkPrimaryBackground;

  static Color secondaryBackground(Brightness brightness) =>
      brightness == Brightness.light
          ? lightSecondaryBackground
          : darkSecondaryBackground;

  static Color primaryBorder(Brightness brightness) =>
      brightness == Brightness.light ? lightPrimaryBorder : darkPrimaryBorder;

  static Color primaryText(Brightness brightness) =>
      brightness == Brightness.light ? lightPrimaryText : darkPrimaryText;

  static Color secondaryText(Brightness brightness) =>
      brightness == Brightness.light ? lightSecondaryText : darkSecondaryText;

  static Color tertiaryText(Brightness brightness) =>
      brightness == Brightness.light ? lightTertiaryText : darkTertiaryText;
}

class _DemoTab {
  const _DemoTab({required this.label, required this.subtitle});

  final String label;
  final String subtitle;
}

class _DemoTabs extends StatelessWidget {
  const _DemoTabs({
    required this.tabs,
    required this.selectedIndex,
    required this.onChanged,
  });

  final List<_DemoTab> tabs;
  final int selectedIndex;
  final ValueChanged<int> onChanged;

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

    return DecoratedBox(
      decoration: BoxDecoration(
        color: _DemoTokens.primaryBackground(brightness),
        border: Border(
          bottom: BorderSide(color: _DemoTokens.primaryBorder(brightness)),
        ),
      ),
      child: SingleChildScrollView(
        padding: const EdgeInsets.fromLTRB(12, 4, 12, 6),
        scrollDirection: Axis.horizontal,
        child: Row(
          children: [
            for (var index = 0; index < tabs.length; index++) ...[
              if (index > 0) const SizedBox(width: 4),
              _DemoTabButton(
                tab: tabs[index],
                selected: index == selectedIndex,
                onTap: () => onChanged(index),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

class _DemoTabButton extends StatefulWidget {
  const _DemoTabButton({
    required this.tab,
    required this.selected,
    required this.onTap,
  });

  final _DemoTab tab;
  final bool selected;
  final VoidCallback onTap;

  @override
  State<_DemoTabButton> createState() => _DemoTabButtonState();
}

class _DemoTabButtonState extends State<_DemoTabButton> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    final brightness = Theme.of(context).brightness;
    final hoverBg = brightness == Brightness.dark
        ? const Color(0x14FFFFFF)
        : const Color(0x0A000000);
    final color = widget.selected
        ? _DemoTokens.primaryText(brightness)
        : _DemoTokens.secondaryText(brightness);

    return MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: widget.onTap,
        child: Tooltip(
          message: widget.tab.subtitle,
          waitDuration: const Duration(milliseconds: 400),
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 140),
            curve: Curves.easeOut,
            padding: const EdgeInsets.fromLTRB(12, 3, 12, 5),
            decoration: BoxDecoration(
              color: widget.selected
                  ? _DemoTokens.primaryBackground(brightness)
                  : (_hovered ? hoverBg : Colors.transparent),
              borderRadius: BorderRadius.circular(7),
              border: widget.selected
                  ? Border.all(color: _DemoTokens.primaryBorder(brightness))
                  : null,
              boxShadow: widget.selected
                  ? const [
                      BoxShadow(
                        color: Color(0x0F000000),
                        offset: Offset(0, 2),
                        blurRadius: 4,
                        spreadRadius: -1,
                      ),
                      BoxShadow(
                        color: Color(0x0A000000),
                        offset: Offset(0, 1),
                        blurRadius: 2,
                        spreadRadius: -1,
                      ),
                    ]
                  : null,
            ),
            child: Text(
              widget.tab.label,
              style: TextStyle(
                color: color,
                fontSize: 12,
                fontWeight: widget.selected ? FontWeight.w600 : FontWeight.w500,
                letterSpacing: -0.1,
                height: 1.0,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _DemoPanel extends StatelessWidget {
  const _DemoPanel({
    required this.title,
    required this.subtitle,
    required this.icon,
    required this.child,
    this.trailing,
  });

  final String title;
  final String subtitle;
  final IconData icon;
  final Widget child;
  final Widget? trailing;

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

    return DecoratedBox(
      decoration: BoxDecoration(
        color: _DemoTokens.primaryBackground(brightness),
        borderRadius: BorderRadius.circular(10),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.08),
            blurRadius: 3,
            offset: const Offset(0, 1),
          ),
        ],
      ),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(10),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Container(
              height: 37,
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              decoration: BoxDecoration(
                color: _DemoTokens.primaryBackground(brightness),
                border: Border(
                  bottom: BorderSide(
                    color: _DemoTokens.primaryBorder(brightness),
                  ),
                ),
              ),
              child: Row(
                children: [
                  Tooltip(
                    message: subtitle,
                    child: Icon(
                      icon,
                      size: 14,
                      color: _DemoTokens.tertiaryText(brightness),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      title,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: TextStyle(
                        color: _DemoTokens.primaryText(brightness),
                        fontSize: 14,
                        fontWeight: FontWeight.w600,
                        letterSpacing: -0.1,
                        height: 1.0,
                      ),
                    ),
                  ),
                  if (trailing != null) ...[
                    const SizedBox(width: 8),
                    trailing!,
                  ],
                ],
              ),
            ),
            Expanded(child: child),
          ],
        ),
      ),
    );
  }
}

class _DemoIconButton extends StatefulWidget {
  const _DemoIconButton({
    required this.icon,
    required this.onPressed,
    this.tooltip,
    this.iconColor,
    this.hoverIconColor,
    this.size = 28,
    this.iconSize = 14,
  });

  final IconData icon;
  final VoidCallback onPressed;
  final String? tooltip;
  final Color? iconColor;
  final Color? hoverIconColor;
  final double size;
  final double iconSize;

  @override
  State<_DemoIconButton> createState() => _DemoIconButtonState();
}

class _DemoIconButtonState extends State<_DemoIconButton> {
  bool _hovered = false;
  bool _pressed = false;

  @override
  Widget build(BuildContext context) {
    final brightness = Theme.of(context).brightness;
    final hoverBg = brightness == Brightness.dark
        ? Colors.white.withValues(alpha: 0.08)
        : Colors.black.withValues(alpha: 0.04);
    final iconColor = _hovered && widget.hoverIconColor != null
        ? widget.hoverIconColor!
        : widget.iconColor ?? _DemoTokens.secondaryText(brightness);

    Widget button = MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTapDown: (_) => setState(() => _pressed = true),
        onTapUp: (_) => setState(() => _pressed = false),
        onTapCancel: () => setState(() => _pressed = false),
        onTap: widget.onPressed,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 140),
          curve: Curves.easeOut,
          width: widget.size,
          height: widget.size,
          decoration: BoxDecoration(
            color: _hovered && !_pressed ? hoverBg : Colors.transparent,
            borderRadius: BorderRadius.circular(8),
          ),
          child: Icon(widget.icon, size: widget.iconSize, color: iconColor),
        ),
      ),
    );

    if (widget.tooltip != null) {
      button = Tooltip(message: widget.tooltip!, child: button);
    }

    return button;
  }
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ThemeMode _themeMode = ThemeMode.light;

  void _toggleTheme() {
    setState(() {
      _themeMode =
          _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'JsonView Plus',
      debugShowCheckedModeBanner: false,
      theme: _DemoTheme.light(),
      darkTheme: _DemoTheme.dark(),
      themeMode: _themeMode,
      home: HomePage(
        themeMode: _themeMode,
        onToggleTheme: _toggleTheme,
      ),
    );
  }
}

class _Sample {
  const _Sample({
    required this.label,
    required this.subtitle,
    required this.build,
  });

  final String label;
  final String subtitle;
  final Widget Function(BuildContext, Brightness, JsonViewController) build;
}

class HomePage extends StatefulWidget {
  const HomePage({
    super.key,
    required this.themeMode,
    required this.onToggleTheme,
  });

  final ThemeMode themeMode;
  final VoidCallback onToggleTheme;

  @override
  State<HomePage> createState() => _HomePageState();
}

const double _mobilePaneBreakpoint = 600;
const double _normalPaneWidth = 420;
const double _widePaneWidth = _normalPaneWidth * 1.5;
const double _paneGap = 16;
const EdgeInsets _paneCanvasPadding = EdgeInsets.all(16);
const EdgeInsets _panelBodyPadding = EdgeInsets.all(12);
const int _sampleCount = 5;

class _HomePageState extends State<HomePage> {
  int _selected = 0;
  final ScrollController _paneScrollController = ScrollController();
  final PageController _mobilePageController = PageController();
  final List<JsonViewController> _jsonControllers = List.generate(
    _sampleCount,
    (_) => JsonViewController(),
  );
  final List<bool> _expandedStates = List<bool>.filled(_sampleCount, true);

  List<_Sample> get _samples => [
        _Sample(
          label: 'Map',
          subtitle: 'Nested object, first layer open',
          build: (_, __, c) => JsonView(
            json: getJsonData(),
            padding: _panelBodyPadding,
            jsonViewController: c,
            styleScheme: const JsonStyleScheme(
              openAtStart: true,
              arrow: Icon(LucideIcons.chevron_right, size: 12),
            ),
          ),
        ),
        _Sample(
          label: 'Large list',
          subtitle: 'Chunked expansion, 200+ items',
          build: (_, __, c) => JsonView(
            json: largeJsonData(),
            padding: _panelBodyPadding,
            jsonViewController: c,
            styleScheme: const JsonStyleScheme(
              openAtStart: true,
              arrow: Icon(LucideIcons.chevron_right, size: 12),
            ),
          ),
        ),
        _Sample(
          label: 'List',
          subtitle: 'Plain array root',
          build: (_, __, c) => JsonView(
            json: listJsonData(),
            padding: _panelBodyPadding,
            jsonViewController: c,
            styleScheme: const JsonStyleScheme(
              arrow: Icon(LucideIcons.chevron_right, size: 12),
            ),
          ),
        ),
        _Sample(
          label: 'Raw string',
          subtitle: 'Decoded via isolate when large',
          build: (_, __, c) => JsonView.fromJsonString(
            jsonString: jsonEncode(largeJsonData()),
            decodeStrategy: JsonDecodeStrategy.auto,
            padding: _panelBodyPadding,
            jsonViewController: c,
            styleScheme: const JsonStyleScheme(
              arrow: Icon(LucideIcons.chevron_right, size: 12),
              charactersBeforeCutoff: 100,
            ),
          ),
        ),
        _Sample(
          label: 'Compact',
          subtitle: 'Nested JSON-in-string, summaries preferred',
          build: (_, __, c) => JsonView(
            json: {
              'metadata': '{"id":42,"tags":["json","viewer"],"active":true}',
              'payload': largeJsonData(),
              'notes': 'Compact mode keeps big branches shallow by default.',
            },
            padding: _panelBodyPadding,
            jsonViewController: c,
            styleScheme: const JsonStyleScheme(
              arrow: Icon(LucideIcons.chevron_right, size: 12),
              compactMode: true,
              compactDepth: 0,
              parseNestedJsonStrings: true,
              charactersBeforeCutoff: 80,
            ),
          ),
        ),
      ];

  @override
  void dispose() {
    _paneScrollController.dispose();
    _mobilePageController.dispose();
    for (final controller in _jsonControllers) {
      controller.dispose();
    }
    super.dispose();
  }

  double _paneOffsetFor(int index) {
    var offset = 0.0;
    for (var i = 0; i < index; i++) {
      offset += _paneWidthFor(i) + _paneGap;
    }
    return offset;
  }

  double _paneWidthFor(int index) =>
      index == 3 ? _widePaneWidth : _normalPaneWidth;

  void _selectSample(int index) {
    setState(() => _selected = index);

    if (_mobilePageController.hasClients) {
      _mobilePageController.animateToPage(
        index,
        duration: const Duration(milliseconds: 320),
        curve: Curves.easeOutCubic,
      );
    }

    if (!_paneScrollController.hasClients) return;
    final maxExtent = _paneScrollController.position.maxScrollExtent;
    final target = _paneOffsetFor(index).clamp(0.0, maxExtent);
    _paneScrollController.animateTo(
      target,
      duration: const Duration(milliseconds: 320),
      curve: Curves.easeOutCubic,
    );
  }

  void _handleMobilePageChanged(int index) {
    if (_selected == index) return;
    setState(() => _selected = index);
  }

  void _expandPane(int index) {
    setState(() => _expandedStates[index] = true);
    _jsonControllers[index].expandAll();
  }

  void _collapsePane(int index) {
    setState(() => _expandedStates[index] = false);
    _jsonControllers[index].collapseAll();
  }

  Widget _buildPane(int index, Brightness brightness, bool isDark) {
    final sample = _samples[index];

    return _DemoPanel(
      title: sample.label,
      subtitle: sample.subtitle,
      icon: LucideIcons.braces,
      trailing: _DemoIconButton(
        icon: _expandedStates[index]
            ? LucideIcons.chevrons_down_up
            : LucideIcons.chevrons_up_down,
        tooltip: _expandedStates[index] ? 'Collapse all' : 'Expand all',
        size: 24,
        iconSize: 13,
        iconColor: _DemoTokens.secondaryText(brightness),
        hoverIconColor: _DemoTokens.primaryColor(brightness),
        onPressed: () =>
            _expandedStates[index] ? _collapsePane(index) : _expandPane(index),
      ),
      child: JsonConfig(
        data: JsonConfigData(
          gap: 100,
          style: JsonStyleScheme(
            openAtStart: false,
            arrow: const Icon(LucideIcons.chevron_right, size: 12),
            depth: 2,
            quotation: JsonQuotation.doubleQuote,
          ),
          color: isDark ? defaultDarkColorScheme : defaultLightColorScheme,
        ),
        child: sample.build(context, brightness, _jsonControllers[index]),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final brightness = Theme.of(context).brightness;
    final isDark = brightness == Brightness.dark;

    return Scaffold(
      backgroundColor: _DemoTokens.secondaryBackground(brightness),
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _AppHeader(
              themeMode: widget.themeMode,
              onToggleTheme: widget.onToggleTheme,
            ),
            _DemoTabs(
              tabs: [
                for (final s in _samples)
                  _DemoTab(label: s.label, subtitle: s.subtitle),
              ],
              selectedIndex: _selected,
              onChanged: _selectSample,
            ),
            Expanded(
              child: LayoutBuilder(
                builder: (context, constraints) {
                  if (constraints.maxWidth < _mobilePaneBreakpoint) {
                    return PageView.builder(
                      controller: _mobilePageController,
                      onPageChanged: _handleMobilePageChanged,
                      itemCount: _samples.length,
                      itemBuilder: (context, index) {
                        return Padding(
                          padding: _paneCanvasPadding,
                          child: _buildPane(index, brightness, isDark),
                        );
                      },
                    );
                  }

                  return Scrollbar(
                    controller: _paneScrollController,
                    thumbVisibility: true,
                    child: SingleChildScrollView(
                      controller: _paneScrollController,
                      scrollDirection: Axis.horizontal,
                      primary: false,
                      padding: _paneCanvasPadding,
                      child: Row(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          for (var i = 0; i < _samples.length; i++) ...[
                            SizedBox(
                              width: _paneWidthFor(i),
                              height: double.infinity,
                              child: _buildPane(i, brightness, isDark),
                            ),
                            if (i != _samples.length - 1)
                              const SizedBox(width: _paneGap),
                          ],
                        ],
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// Flat 46px app-header — bottom border only, brand chip + title + theme toggle.
class _AppHeader extends StatelessWidget {
  const _AppHeader({
    required this.themeMode,
    required this.onToggleTheme,
  });

  final ThemeMode themeMode;
  final VoidCallback onToggleTheme;

  @override
  Widget build(BuildContext context) {
    final brightness = Theme.of(context).brightness;
    final primary = _DemoTokens.primaryColor(brightness);

    return Container(
      height: 46,
      decoration: BoxDecoration(
        color: _DemoTokens.primaryBackground(brightness),
        border: Border(
          bottom: BorderSide(
            color: _DemoTokens.primaryBorder(brightness),
            width: 1,
          ),
        ),
      ),
      padding: const EdgeInsets.symmetric(horizontal: 14),
      child: Row(
        children: [
          Container(
            width: 24,
            height: 24,
            decoration: BoxDecoration(
              color: primary.withValues(alpha: 0.12),
              borderRadius: BorderRadius.circular(6),
            ),
            alignment: Alignment.center,
            child: Icon(LucideIcons.braces, size: 14, color: primary),
          ),
          const SizedBox(width: 10),
          Text(
            'JsonView Plus',
            style: TextStyle(
              fontSize: 13,
              fontWeight: FontWeight.w600,
              letterSpacing: -0.2,
              height: 1.0,
              color: _DemoTokens.primaryText(brightness),
            ),
          ),
          const Spacer(),
          _DemoIconButton(
            icon: themeMode == ThemeMode.dark
                ? LucideIcons.sun
                : LucideIcons.moon,
            tooltip: themeMode == ThemeMode.dark
                ? 'Switch to light'
                : 'Switch to dark',
            iconColor: _DemoTokens.primaryText(brightness),
            onPressed: onToggleTheme,
          ),
        ],
      ),
    );
  }
}
2
likes
160
points
23
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

High-performance Flutter JSON tree viewer with lazy row rendering, syntax highlighting, expansion controls, and Chrome DevTools-style large JSON browsing.

Homepage
Repository (GitHub)
View/report issues

Topics

#json #tree-view #flutter #devtools #viewer

License

MIT (license)

Dependencies

flutter

More

Packages that depend on json_view_plus