You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将JSON映射转换为单键多值的Map结构?

问题:将JSON数组按歌手分组重组为Map结构

我能正常解析JSON数组,但想把多个歌曲数据映射到同一个歌手键下,不知道怎么实现。没办法修改现有JSON输出,示例JSON片段如下:

{
    "pos":"1",
    "element":"37542",
    "title":"Easy On Me",
    "inter":"Adele",
    "isnew":"0"
},
{
    "pos":"66",
    "element":"37183",
    "title":"Send My Love",
    "inter":"Adele",
    "isnew":"0"
}

我期望得到类似Map<String, List<Map<String, String>>>的结构,最终展示效果如下:
Adele

  • Easy On Me, 37542
  • Send My Love, 37183

网上找了很多常规JSON解析示例,但没找到这种遍历重组的方法,附上我的Dart/Flutter代码:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'dart:async';

class SongList {
  final List<Song> songs;

  SongList({
    required this.songs,
  });

  factory SongList.fromJson(List<dynamic> parsedJson) {
    List<Song> songs = <Song>[];
    songs = parsedJson.map((i) => Song.fromJson(i)).toList();

    return SongList(songs: songs);
  }
}

class Song {
  final String? pos;
  final String? element;
  final String? title;
  final String? inter;
  final String? isnew;

  const Song(
      {required this.pos,
      required this.element,
      required this.title,
      required this.inter,
      required this.isnew});

  factory Song.fromJson(Map<String, dynamic> data) {
    return Song(
      pos: data['pos'] as String?,
      element: data['element'] as String?,
      title: data['title'] as String?,
      inter: data['inter'] as String?,
      isnew: data['isnew'] as String?,
    );
  }
}

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

class Songs extends StatefulWidget {
  final String pos;
  final String element;
  final String title;
  final String inter;
  final String isnew;

  const Songs(
      {super.key,
      required this.pos,
      required this.element,
      required this.title,
      required this.inter,
      required this.isnew});

  @override
  State<Songs> createState() => _SongsState();
}

class _SongsState extends State<Songs> {
  late AudioPlayer player;

  @override
  void initState() {
    super.initState();
    player = AudioPlayer();
  }

  @override
  void dispose() {
    player.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var titles = Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Column(children: [
          SizedBox(
            height: 25,
            width: 200,
            child: Text(
              widget.title,
              style: const TextStyle(fontSize: 14),
              textAlign: TextAlign.center,
            ),
          ),
          SizedBox(
            height: 25,
            width: 200,
            child: Text(
              widget.inter,
              style: const TextStyle(fontSize: 10),
              textAlign: TextAlign.center,
            ),
          ),
        ]),
      ],
    );
    return FittedBox(
        fit: BoxFit.contain,
        child: Card(
            child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Column(
              children: [
                Center(
                    child: SizedBox(
                        height: 50,
                        width: 50,
                        child: Align(
                            alignment: Alignment.center,
                            child: widget.isnew != "1"
                                ? Text(
                                    widget.pos.toString(),
                                    textAlign: TextAlign.center,
                                  )
                                : const Text(
                                    "new",
                                    textAlign: TextAlign.center,
                                    style: TextStyle(color: Colors.red),
                                  )))),
              ],
            ),
            Column(
              children: [
                SizedBox(
                  height: 50,
                  width: 50,
                  child: Image.network(
                      '${'http://www.example.com/${widget.element}'}.jpg'),
                ),
              ],
            ),
            Column(
              children: [
                titles,
              ],
            ),
            Column(
              children: [
                SizedBox(
                  height: 50,
                  width: 50,
                  child: TextButton(
                    onPressed: () async {
                      final url =
                          '${'http://www.example.com/${widget.element}'}.mp3';
                      await player.setUrl(url);
                      player.play();
                    },
                    child: Image.asset('assets/images/btn_play.png'),
                  ),
                ),
              ],
            ),
            Column(
              children: [
                SizedBox(
                  height: 50,
                  width: 50,
                  child: TextButton(
                    // ignore: avoid_print
                    onPressed: () =>
                        print('pressed button download!! ${widget.element}'),
                    child: Image.asset('assets/images/btn_download.png'),
                  ),
                ),
              ],
            ),
          ],
        )));
  }
}


Future<SongList> fetchSong() async {
  final response = await http
      .get(Uri.parse('http://example.com'));
  if (response.statusCode == 200) {
    return SongList.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('Failed to Load Songs');
  }
}

//snippet from class that instatiates
late Future<SongList> futureSong;

@override
void initState() {
  super.initState();
  futureSong = fetchSong();
}

//snippet from class that builds the list
FutureBuilder<SongList>(
          future: futureSong,
          builder: (context, AsyncSnapshot<SongList> snapshot) {
            if (snapshot.hasData) {
              return ListView.builder(
                  itemCount: snapshot.data!.songs.length,
                  itemBuilder: (context, index) {
                    if (snapshot.data!.songs[index].element.toString() !=
                        "null") {
                      return Songs(
                          element:
                              snapshot.data!.songs[index].element.toString(),
                          inter: snapshot.data!.songs[index].inter.toString(),
                          isnew: snapshot.data!.songs[index].isnew.toString(),
                          pos: snapshot.data!.songs[index].pos.toString(),
                          title: snapshot.data!.songs[index].title.toString());
                    } else {
                      return const SizedBox(height: 0);
                    }
                  });
            } else if (snapshot.hasError) {
              return Text('${snapshot.error}');
            }

            // By default, show a loading spinner.
            return const CircularProgressIndicator();
          }

解决方案

你可以在获取到SongList后,遍历歌曲列表通过Map完成分组,键为歌手名(inter字段),值为对应歌曲的信息集合。

1. 添加分组工具方法

在代码中新增一个分组函数,可放在SongList类中或单独的工具类里:

Map<String, List<Map<String, String>>> groupSongsByArtist(List<Song> songs) {
  final artistMap = <String, List<Map<String, String>>>{};
  
  for (final song in songs) {
    final artist = song.inter ?? '未知歌手';
    // 按需保存需要的字段,这里保留title和element
    final songInfo = {
      'title': song.title ?? '',
      'element': song.element ?? '',
      'pos': song.pos ?? '',
      'isnew': song.isnew ?? ''
    };
    
    // 分组逻辑:存在则追加,不存在则创建新列表
    if (artistMap.containsKey(artist)) {
      artistMap[artist]!.add(songInfo);
    } else {
      artistMap[artist] = [songInfo];
    }
  }
  
  return artistMap;
}

2. 修改FutureBuilder使用分组结果

替换原来的列表构建逻辑,基于分组后的Map生成UI:

FutureBuilder<SongList>(
  future: futureSong,
  builder: (context, AsyncSnapshot<SongList> snapshot) {
    if (snapshot.hasData) {
      final groupedSongs = groupSongsByArtist(snapshot.data!.songs);
      
      return ListView.builder(
        itemCount: groupedSongs.keys.length,
        itemBuilder: (context, index) {
          final artist = groupedSongs.keys.elementAt(index);
          final songs = groupedSongs[artist]!;
          
          // 用ExpansionTile实现折叠展开的歌手-歌曲结构
          return ExpansionTile(
            title: Text(artist, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            children: songs.map((songInfo) {
              // 直接复用你已有的Songs组件
              return Songs(
                element: songInfo['element']!,
                inter: artist,
                isnew: songInfo['isnew']!,
                pos: songInfo['pos']!,
                title: songInfo['title']!
              );
            }).toList(),
          );
        },
      );
    } else if (snapshot.hasError) {
      return Text('加载失败:${snapshot.error}');
    }

    return const CircularProgressIndicator();
  },
)

说明

  • 分组函数可根据需求调整保存的字段,比如需要pos、isnew就一并加入songInfo
  • ExpansionTile是Flutter自带组件,完美匹配你期望的"歌手+展开显示歌曲"的交互效果
  • 如果不需要折叠展开,也可以直接遍历分组后的Map,用Column嵌套列表实现静态展示

内容的提问来源于stack exchange,提问作者hirnspiel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 12:50:27