Flutter JSON数据可视化插件json_explorer的使用

Flutter JSON数据可视化插件json_explorer的使用

JSON Explorer

Pub Version

一个用于渲染、查看和与JSON进行交互的Flutter小部件。它还包括了互动搜索功能。

维护的分支版本,包含修复和改进:

功能

  • 展开和折叠类和数组节点。
  • 带有高亮的动态搜索。
  • 可配置的主题和交互。
  • 可配置的数据显示格式。
  • 缩进指南。

使用

要显示的数据由存储管理,即JsonExplorerStore

为了使用此包的所有功能,你需要在[Provider](https://pub.dev/packages/provider)中注册它。

final JsonExplorerStore store = JsonExplorerStore();

// ...
ChangeNotifierProvider.value(
  value: store,
  child:
// ...

加载JSON对象时,可以使用JsonExplorerStore.buildNodes方法:

store.buildNodes(json.decode(myJson));

要显示数据浏览器,可以使用JsonExplorer小部件。唯一需要的参数是一组节点模型,可以从JsonExplorerStore中获取解码后的JSON。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: SafeArea(
      minimum: const EdgeInsets.all(16),
      child: ChangeNotifierProvider.value(
        value: store,
        child: Consumer<JsonExplorerStore>(
          builder: (context, state, child) => JsonExplorer(
            nodes: state.displayNodes,
          ),
        ),
      ),
    ),
  );
}

这将使用默认主题显示解码后的JSON。

有关如何自定义JsonExplorer小部件的外观和感觉,请参阅/example应用程序。

更改外观和感觉

主题

要更改字体和颜色,可以使用JsonExplorerTheme

JsonExplorer(
  nodes: state.displayNodes,
  theme: JsonExplorerTheme(
    rootKeyTextStyle: GoogleFonts.inconsolata(
      color: Colors.black,
      fontWeight: FontWeight.bold,
      fontSize: 16,
    ),
    propertyKeyTextStyle: GoogleFonts.inconsolata(
      color: Colors.black.withOpacity(0.7),
      fontWeight: FontWeight.bold,
      fontSize: 16,
    ),
    keySearchHighlightTextStyle: GoogleFonts.inconsolata(
      color: Colors.black,
      backgroundColor: const Color(0xFFFFEDAD),
      fontWeight: FontWeight.bold,
      fontSize: 16,
    ),
    focusedKeySearchHighlightTextStyle: GoogleFonts.inconsolata(
      color: Colors.black,
      backgroundColor: const Color(0xFFF29D0B),
      fontWeight: FontWeight.bold,
      fontSize: 16,
    ),
    valueTextStyle: GoogleFonts.inconsolata(
      color: const Color(0xFFCA442C),
      fontSize: 16,
    ),
    valueSearchHighlightTextStyle: GoogleFonts.inconsolata(
      color: const Color(0xFFCA442C),
      backgroundColor: const Color(0xFFFFEDAD),
      fontWeight: FontWeight.bold,
      fontSize: 16,
    ),
    focusedValueSearchHighlightTextStyle: GoogleFonts.inconsolata(
      color: Colors.black,
      backgroundColor: const Color(0xFFF29D0B),
      fontWeight: FontWeight.bold,
      fontSize: 16,
    ),
    indentationLineColor: const Color(0xFFE1E1E1),
    highlightColor: const Color(0xFFF1F1F1),
  ),
)

格式化器

除了更改主题外,还可以通过Formatter方法更改键和值如何被转换为字符串。

默认情况下,显示JSON属性名称的方式为key:,但可以通过格式化器进行更改:

JsonExplorer(
  nodes: state.displayNodes,
  propertyNameFormatter: (name) => '$name -&gt;',
)

现在所有属性键都显示为key -&gt;

动态更改基于值的属性样式

可以使用valueStyleBuilder参数动态更改基于值的属性样式。它期望一个函数,该函数接收属性的dynamic value和当前的style,并返回一个PropertyOverrides

例如,为包含链接的值添加交互:

JsonExplorer(
  nodes: state.displayNodes,
  valueStyleBuilder: (value, style) {
    final isUrl = _valueIsUrl(value);
    return PropertyOverrides(
      style: isUrl
          ? style.copyWith(
              decoration: TextDecoration.underline,
            )
          : style,
      onTap: isUrl ? () => _launchUrl(value) : null,
    );
  },
)

或者遵循相同的原则,根据特定值类型更改其外观:

JsonExplorer(
  nodes: state.displayNodes,
  valueStyleBuilder: (value, style) {
    if (value is num) {
      return PropertyOverrides(
        style: style.copyWith(
          color: Colors.blue,
        ),
      );
    }
    return PropertyOverrides(
      style: style,
    );
  },
)

自定义小部件组件

collapsableToggleBuilder允许更改根节点上显示的展开和折叠按钮。例如,使用简单的隐式动画小部件:

JsonExplorer(
  nodes: state.displayNodes,
  collapsableToggleBuilder: (context, node) =>
      AnimatedRotation(
    turns: node.isCollapsed ? -0.25 : 0,
    duration: const Duration(milliseconds: 300),
    child: const Icon(Icons.arrow_drop_down),
  ),
)

rootInformationBuilder构建一个在类和数组根节点上显示的小部件。例如,这可以用来显示其子节点的一些信息:

JsonExplorer(
  nodes: state.displayNodes,
  rootInformationBuilder: (context, node) => Text(
    node.isClass
        ? '{${(node.childrenCount)}}'
        : '[${node.childrenCount}]',
  ),
)

trailingBuilder构建每个节点的尾部小部件。NodeViewModelState参数允许小部件对某些节点属性做出反应。例如,构建一个仅在节点当前聚焦时出现的小部件:

JsonExplorer(
  nodes: state.displayNodes,
  trailingBuilder: (context, node) => node.isFocused
    ? Text("I'm focused :)")
    : const SizedBox(),
)

搜索

JsonExplorerStore提供了使用search方法的搜索功能。JsonExplorer小部件已经对这些状态变化作出响应,并突出显示搜索结果。请参阅JsonExplorerTheme以更改搜索结果的外观。

可以使用focusPreviousSearchResultfocusNextSearchResult方法更改当前焦点的结果。

以下是一个简单的搜索栏示例,可以在example文件夹中找到完整的示例:

Row(
  children: [
    Expanded(
      child: TextField(
        controller: searchController,
        onChanged: (term) => jsonExplorerStore.search(term),
        maxLines: 1,
        decoration: const InputDecoration(
          hintText: 'Search',
        ),
      ),
    ),
    const SizedBox(
      width: 8,
    ),
    IconButton(
      onPressed: jsonExplorerStore.focusPreviousSearchResult,
      icon: const Icon(Icons.arrow_drop_up),
    ),
    IconButton(
      onPressed: jsonExplorerStore.focusNextSearchResult,
      icon: const Icon(Icons.arrow_drop_down),
    ),
  ],
),

自定义滚动小部件

可以通过使用JsonAttribute小部件来显示每个节点,从而实现自己的滚动。

一个简单的ListView.builder看起来像这样:

ListView.builder(
  itemCount: state.displayNodes.length,
  itemBuilder: (context, index) => JsonAttribute(
    node: state.displayNodes.elementAt(index),
    theme: JsonExplorerTheme.defaultTheme,
  ),
),

示例代码

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:json_explorer/json_explorer.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:url_launcher/url_launcher_string.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Json Explorer',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Test Json Explorer'),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text(
            'Small JSON',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
          const _OpenJsonButton(
            title: 'ISS current location',
            url: 'http://api.open-notify.org/iss-now.json',
            padding: EdgeInsets.symmetric(vertical: 8.0),
          ),
          const _OpenJsonButton(
            title: 'Country List',
            url: 'https://api.foss42.com/country/codes',
            padding: EdgeInsets.symmetric(vertical: 8.0),
          ),
          Text(
            'Medium JSON',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
          const _OpenJsonButton(
            title: 'Nobel prizes country',
            url: 'http://api.nobelprize.org/v1/country.json',
            padding: EdgeInsets.symmetric(vertical: 8.0),
          ),
          const _OpenJsonButton(
            title: 'Australia ABC Local Stations',
            url:
                'https://data.gov.au/geoserver/abc-local-stations/wfs?request=GetFeature&typeName=ckan_d534c0e9_a9bf_487b_ac8f_b7877a09d162&outputFormat=json',
          ),
          Text(
            'Large JSON',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
          const _OpenJsonButton(
            title: 'Pokémon',
            url: 'https://pokeapi.co/api/v2/pokemon/?offset=0&limit=2000',
            padding: EdgeInsets.symmetric(vertical: 8.0),
          ),
          const _OpenJsonButton(
            title: 'Earth Meteorite Landings',
            url: 'https://data.nasa.gov/resource/y77d-th95.json',
          ),
          const _OpenJsonButton(
            title: 'Reddit r/all',
            url: 'https://www.reddit.com/r/all.json',
          ),
          Text(
            'Exploding JSON',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
          const _OpenJsonButton(
            title: '25MB GitHub Json',
            url:
                'https://raw.githubusercontent.com/json-iterator/test-data/master/large-file.json',
            padding: EdgeInsets.only(top: 8.0, bottom: 32.0),
          ),
          Text(
            'More datasets at https://awesomeopensource.com/project/jdorfman/awesome-json-datasets',
            style: Theme.of(context).textTheme.bodySmall,
          ),
        ],
      ),
    );
  }
}

class JsonExplorerPage extends StatefulWidget {
  final String jsonUrl;
  final String title;

  const JsonExplorerPage({
    Key? key,
    required this.jsonUrl,
    required this.title,
  }) : super(key: key);

  [@override](/user/override)
  _JsonExplorerPageState createState() => _JsonExplorerPageState();
}

class _JsonExplorerPageState extends State<JsonExplorerPage> {
  final searchController = TextEditingController();
  final itemScrollController = ItemScrollController();
  final JsonExplorerStore store = JsonExplorerStore();

  [@override](/user/override)
  void initState() {
    _loadJsonDataFrom(widget.jsonUrl);
    super.initState();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: SafeArea(
        minimum: const EdgeInsets.all(16),
        child: ChangeNotifierProvider.value(
          value: store,
          child: Consumer<JsonExplorerStore>(
            builder: (context, state, child) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Expanded(
                      child: TextField(
                        controller: searchController,

                        /// Delegates the search to [JsonExplorerStore] when
                        /// the text field changes.
                        onChanged: (term) => state.search(term),
                        decoration: const InputDecoration(
                          hintText: 'Search',
                        ),
                      ),
                    ),
                    const SizedBox(
                      width: 8,
                    ),
                    if (state.searchResults.isNotEmpty)
                      Text(_searchFocusText()),
                    if (state.searchResults.isNotEmpty)
                      IconButton(
                        onPressed: () {
                          store.focusPreviousSearchResult();
                          _scrollToSearchMatch();
                        },
                        icon: const Icon(Icons.arrow_drop_up),
                      ),
                    if (state.searchResults.isNotEmpty)
                      IconButton(
                        onPressed: () {
                          store.focusNextSearchResult();
                          _scrollToSearchMatch();
                        },
                        icon: const Icon(Icons.arrow_drop_down),
                      ),
                  ],
                ),
                const SizedBox(
                  height: 16.0,
                ),
                Row(
                  children: [
                    TextButton(
                      onPressed: state.areAllExpanded() ? null : state.expandAll,
                      child: const Text('Expand All'),
                    ),
                    const SizedBox(
                      width: 8.0,
                    ),
                    TextButton(
                      onPressed: state.areAllCollapsed() ? null : state.collapseAll,
                      child: const Text('Collapse All'),
                    ),
                  ],
                ),
                const SizedBox(
                  height: 16.0,
                ),
                Expanded(
                  child: JsonExplorer(
                    nodes: state.displayNodes,
                    itemScrollController: itemScrollController,
                    itemSpacing: 4,
                    maxRootNodeWidth: 200,

                    /// Builds a widget after each root node displaying the
                    /// number of children nodes that it has. Displays `{x}`
                    /// if it is a class or `[x]` in case of arrays.
                    rootInformationBuilder: (context, node) => DecoratedBox(
                      decoration: const BoxDecoration(
                        color: Color(0x80E1E1E1),
                        borderRadius: BorderRadius.all(Radius.circular(2)),
                      ),
                      child: Padding(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 4,
                          vertical: 2,
                        ),
                        child: Text(
                          node.isClass
                              ? '{${node.childrenCount}}'
                              : '[${node.childrenCount}]',
                          style: GoogleFonts.inconsolata(
                            fontSize: 12,
                            color: const Color(0xFF6F6F6F),
                          ),
                        ),
                      ),
                    ),

                    /// Build an animated collapse/expand indicator. Implicitly
                    /// animates the indicator when
                    /// [NodeViewModelState.isCollapsed] changes.
                    collapsableToggleBuilder: (context, node) =>
                        AnimatedRotation(
                      turns: node.isCollapsed ? -0.25 : 0,
                      duration: const Duration(milliseconds: 300),
                      child: const Icon(Icons.arrow_drop_down),
                    ),

                    /// Builds a trailing widget that copies the node key: value
                    ///
                    /// Uses [NodeViewModelState.isFocused] to display the
                    /// widget only in focused widgets.
                    trailingBuilder: (context, node) => node.isFocused
                        ? IconButton(
                            padding: EdgeInsets.zero,
                            constraints: const BoxConstraints(maxHeight: 18),
                            icon: const Icon(
                              Icons.copy,
                              size: 18,
                            ),
                            onPressed: () => _printNode(node),
                          )
                        : const SizedBox(),

                    /// Creates a custom format for classes and array names.
                    rootNameFormatter: (dynamic name) => '$name',

                    /// Dynamically changes the property value style and
                    /// interaction when an URL is detected.
                    valueStyleBuilder: (dynamic value, style) {
                      final isUrl = _valueIsUrl(value);
                      return PropertyOverrides(
                        style: isUrl
                            ? style.copyWith(
                                decoration: TextDecoration.underline,
                              )
                            : style,
                        onTap: isUrl ? () => _launchUrl(value as String) : null,
                      );
                    },

                    /// Theme definitions of the json explorer
                    theme: JsonExplorerTheme(
                      rootKeyTextStyle: GoogleFonts.inconsolata(
                        color: Colors.black,
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                      propertyKeyTextStyle: GoogleFonts.inconsolata(
                        color: Colors.black.withOpacity(0.7),
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                      keySearchHighlightTextStyle: GoogleFonts.inconsolata(
                        color: Colors.black,
                        backgroundColor: const Color(0xFFFFEDAD),
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                      focusedKeySearchHighlightTextStyle: GoogleFonts.inconsolata(
                        color: Colors.black,
                        backgroundColor: const Color(0xFFF29D0B),
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                      valueTextStyle: GoogleFonts.inconsolata(
                        color: const Color(0xFFCA442C),
                        fontSize: 16,
                      ),
                      valueSearchHighlightTextStyle: GoogleFonts.inconsolata(
                        color: const Color(0xFFCA442C),
                        backgroundColor: const Color(0xFFFFEDAD),
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                      focusedValueSearchHighlightTextStyle: GoogleFonts.inconsolata(
                        color: Colors.black,
                        backgroundColor: const Color(0xFFF29D0B),
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                      indentationLineColor: const Color(0xFFE1E1E1),
                      highlightColor: const Color(0xFFF1F1F1),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  String _searchFocusText() => 
      '${store.focusedSearchResultIndex + 1} of ${store.searchResults.length}';

  Future _loadJsonDataFrom(String url) async {
    debugPrint('Calling Json API');
    final data = await http.read(Uri.parse(url));
    debugPrint('Done!');
    final dynamic decoded = json.decode(data);
    store.buildNodes(decoded, areAllCollapsed: true);
  }

  void _printNode(NodeViewModelState node) {
    if (node.isRoot) {
      final value = node.isClass ? 'class' : 'array';
      debugPrint('${node.key}: $value');
      return;
    }
    debugPrint('${node.key}: ${node.value}');
  }

  void _scrollToSearchMatch() {
    final index = store.displayNodes.indexOf(store.focusedSearchResult.node);
    if (index != -1) {
      itemScrollController.scrollTo(
        index: index,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOutCubic,
      );
    }
  }

  bool _valueIsUrl(dynamic value) {
    if (value is String) {
      return Uri.tryParse(value)?.hasAbsolutePath ?? false;
    }
    return false;
  }

  Future _launchUrl(String url) {
    return launchUrlString(url);
  }

  [@override](/user/override)
  void dispose() {
    searchController.dispose();
    super.dispose();
  }
}

/// A button that navigates to the data explorer page on pressed.
class _OpenJsonButton extends StatelessWidget {
  final String url;
  final String title;
  final EdgeInsets padding;

  const _OpenJsonButton({
    Key? key,
    required this.url,
    required this.title,
    this.padding = const EdgeInsets.only(bottom: 8.0),
  }) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) => Padding(
        padding: padding,
        child: ElevatedButton(
          child: Text(title),
          onPressed: () => Navigator.of(context).push<MaterialPageRoute>(
            MaterialPageRoute(
              builder: (ctx) => JsonExplorerPage(
                jsonUrl: url,
                title: title,
              ),
            ),
          ),
        ),
      );
}

更多关于Flutter JSON数据可视化插件json_explorer的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter JSON数据可视化插件json_explorer的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,json_explorer 是一个强大的插件,用于可视化和编辑 JSON 数据。以下是一个示例,展示了如何在 Flutter 应用中使用 json_explorer 插件。

首先,确保你的 pubspec.yaml 文件中包含了 json_explorer 依赖项:

dependencies:
  flutter:
    sdk: flutter
  json_explorer: ^最新版本号  # 请替换为实际的最新版本号

然后,运行 flutter pub get 来获取依赖项。

接下来,在你的 Dart 文件中,你可以按照以下方式使用 json_explorer

import 'package:flutter/material.dart';
import 'package:json_explorer/json_explorer.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter JSON Explorer Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // 示例 JSON 数据
  final String jsonData = '''
  {
    "name": "John Doe",
    "age": 30,
    "isEmployed": true,
    "skills": ["Flutter", "Dart", "JSON"],
    "address": {
      "street": "123 Main St",
      "city": "Anytown",
      "country": "USA"
    }
  }
  ''';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('JSON Explorer Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: JsonExplorer(
          json: jsonData,
          readOnly: false,  // 设置为 true 以禁用编辑
          expanded: true,   // 初始时是否展开所有节点
          onEdit: (String newJson) {
            // 用户编辑 JSON 后的回调
            setState(() {
              // 这里你可以处理新的 JSON 数据,比如保存到某个变量或进行其他操作
              print("Updated JSON: $newJson");
            });
          },
        ),
      ),
    );
  }
}

在这个示例中:

  1. 我们定义了一个包含示例 JSON 数据的字符串 jsonData
  2. MyHomePagebuild 方法中,我们使用 JsonExplorer 小部件来显示和编辑这个 JSON 数据。
  3. readOnly 参数设置为 false,允许用户编辑 JSON 数据。如果设置为 true,则用户将无法编辑数据。
  4. expanded 参数设置为 true,表示在初始加载时展开所有节点。
  5. onEdit 回调在 JSON 数据被编辑后触发,你可以在这个回调中处理新的 JSON 数据。

运行这个示例应用,你将看到一个可交互的 JSON 编辑器,你可以在其中浏览和编辑 JSON 数据。

回到顶部