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以更改搜索结果的外观。

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

以下是一个简单的搜索栏示例,可以在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. 在 MyHomePage 的 build 方法中,我们使用 JsonExplorer 小部件来显示和编辑这个 JSON 数据。
  3. readOnly 参数设置为 false,允许用户编辑 JSON 数据。如果设置为 true,则用户将无法编辑数据。
  4. expanded 参数设置为 true,表示在初始加载时展开所有节点。
  5. onEdit 回调在 JSON 数据被编辑后触发,你可以在这个回调中处理新的 JSON 数据。

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

回到顶部