如何将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
相关产品推荐
相关产品推荐

