Search code examples
jsonflutterdartflutter-futurebuilderflutter-listview

Show nested JSON from API in single Page with multiple lists


Im new in Flutter and i'm struggeling with a nested JSON from API which data i want to show in one single page.

I get this JSON from a URL and decode it in a class, which is working fine:

{
  "service1": [
    {
      "firstname": "Peter",
      "lastname": "Smith"
    },
    {
      "firstname": "Paul",
      "lastname": "Johnson"
    }
  ],
  "service2": [
    {
      "firstname": "Mary",
      "lastname": "Williams"
    },
    {
      "firstname": "Guy",
      "lastname": "Brown"
    }
  ]
}

Classes:

/*------------------------------
staff.dart
------------------------------*/
import 'dart:convert';

class Staff {
  String? service;
  String? firstname;
  String? lastname;

  Staff({this.service, this.firstname, this.lastname});

  factory Staff.fromMap(Map<String, dynamic> data) => Staff(
        service: data['service'] as String?,
        firstname: data['firstname'] as String?,
        lastname: data['lastname'] as String?,
      );

  Map<String, dynamic> toMap() => {
        'service': '',
        'firstname': firstname,
        'lastname': lastname,
      };

  /// Parses the string and returns the resulting Json object.
  factory Staff.fromJson(String data) {
    return Staff.fromMap(json.decode(data) as Map<String, dynamic>);
  }

  /// Converts [Staff] to a JSON string.
  String toJson() => json.encode(toMap());
}


/*------------------------------
servicedesk.dart
------------------------------*/
import 'dart:convert';
import 'staff.dart';

class ServiceDesk {
  List<Staff>? service1;
  List<Staff>? service2;

  ServiceDesk({
    this.service1,
    this.service2,
  });

  factory ServiceDesk.fromMap(Map<String, dynamic> data) => ServiceDesk(
        service1: (data['service1'] as List<dynamic>?)
            ?.map((e) => Staff.fromMap(e as Map<String, dynamic>))
            .toList(),
        service2: (data['service2'] as List<dynamic>?)
            ?.map((e) => Staff.fromMap(e as Map<String, dynamic>))
            .toList(),
      );

  Map<String, dynamic> toMap() => {
        'service1': service1?.map((e) => e.toMap()).toList(),
        'service2': service2?.map((e) => e.toMap()).toList(),
      };

  /// Parses the string and returns the resulting Json object as [ServiceDesk].
  factory ServiceDesk.fromJson(String data) {
    var object = ServiceDesk.fromMap(json.decode(data) as Map<String, dynamic>);
    object.b1!.insert(0, Staff(service: 'Title for Service1'));
    object.b2!.insert(0, Staff(service: 'Title for Service2'));
    return object;
  }

  /// Converts to a JSON string.
  String toJson() => json.encode(toMap());
}

That's the code i have (Pseudocode between):

  // PSEUDOCODE!!
  Widget ListWithService(List<Staff>? entry) {
    return ListView.builder(
        itemCount: entry!.length,
        padding: const EdgeInsets.all(2.0),
        itemBuilder: (context, position) {
          final item = entry[position];
          if (item.service != null) {
            return ListTile(
              title: Text(
                '${item.service}',
                style: Theme.of(context).textTheme.headline5,
              ),
            );
          } else {
            return ListTile(
              title: Text(
                '${item.firstname} ${item.lastname}',
                style: Theme.of(context).textTheme.bodyText1,
              ),
            );
          }
        });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Service Desk'),
      ),
      body: FutureBuilder<sdclass.ServiceDesk>(
        future: getData(),
        builder: (context, snapshot) {
          if (snapshot.hasData == true) {
            return [
              ListWithService(snapshot.data.service1), 
              ListWithSercice(snapshot.data.service1);
            ] // PSEUDOCODE!!
          } else if (snapshot.hasError) {
            return const Icon(
              Icons.error_outline,
              color: Colors.red,
              size: 60,
            );
          } else {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
        }
      ),
    );
  }

What i would have at the end should look like this on the full page:

Title for Service1 (Headline)
Peter Smith
Paul Johnson

Title for Service2 (Headline)
Mary Williams
Guy Brown

Could someone help me with the code to get it work?

Update Code

Thanks for your updated example. I tested it in my code. First, everything looks fine. But wenn i switch to the screen with the json, i get a error:

Expected a value of type 'FutureOr<Map<String, List<Map<String, String>>>>', but got one of type '_JsonMap'

    import 'dart:convert';
    import 'dart:core';
    
    import 'package:flutter/material.dart';
    import 'package:http/http.dart' as http;
    
    class ServiceDesk extends StatelessWidget {
      const ServiceDesk({Key? key}) : super(key: key);
    
      Future<Map<String, List<Map<String, String>>>> getData() async {
        String link = "https://url-to-json-file.json";
        final res = await http
            .get(Uri.parse(link), headers: {"Accept": "application/json"});
        if (res.statusCode == 200) {
          var utf8decoded = utf8.decode(res.body.toString().codeUnits);
          var decoded = json.decode(utf8decoded);
          return decoded;
        } else {
          throw Exception('Failed to load JSON');
        }
      }
    
      Widget listViewWidget(
          Iterable<MapEntry<String, List<Map<String, String>>>> entries) {
        return ListView.builder(
            itemCount: entries.length,
            padding: const EdgeInsets.all(2.0),
            itemBuilder: (context, index) {
              final entry = entries.elementAt(index);
              final key = entry.key;
              final values = entry.value;
    
              return Column(
                children: [
                  ListTile(
                    title: Text(
                      'Title for $key',
                      style: Theme.of(context).textTheme.headline5,
                    ),
                  ),
                  for (var person in values)
                    ListTile(
                      title: Text(
                        '${person["firstname"]} ${person["lastname"]}',
                        style: Theme.of(context).textTheme.bodyText1,
                      ),
                    ),
                ],
              );
            });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Service Desk'),
          ),
          body: FutureBuilder(
              future: getData(),
              builder: (_,
                  AsyncSnapshot<Map<String, List<Map<String, String>>>> snapshot) {
                if (snapshot.hasData == true) {
                  final entries = snapshot.data?.entries ?? {};
    
                  return listViewWidget(entries);
                } else if (snapshot.hasError) {
                  return Center(
                      child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      const Icon(
                        Icons.error_outline,
                        color: Colors.red,
                        size: 60,
                      ),
                      Text("Fehler: ${snapshot.error}"),
                    ],
                  ));
                } else {
                  return const Center(
                    child: CircularProgressIndicator(),
                  );
                }
              }),
        );
      }
    }

Solution

  • In the Dart language, you can use for loop in the list, it makes it easier to work with Flutter UI.

    import 'package:flutter/material.dart';
    import 'package:flutter/cupertino.dart';
    import 'dart:convert';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const RootView(),
        );
      }
    }
    
    class RootView extends StatelessWidget {
      const RootView({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: TextButton(
              child: const Text('GO TO SAFF SERVICE'),
              onPressed: () {
                Navigator.push(context, Home.route());
              },
            ),
          ),
        );
      }
    }
    
    class Home extends StatefulWidget {
      const Home({Key? key}) : super(key: key);
    
      static Route route() {
        return CupertinoPageRoute(builder: (_) => const Home());
      }
    
      @override
      _HomeState createState() => _HomeState();
    }
    
    class _HomeState extends State<Home> {
      late Future<StaffService> staffService;
    
      @override
      void initState() {
        staffService = getStaffService();
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Services')),
          body: Center(
            child: FutureBuilder<StaffService>(
              future: staffService,
              builder: (_, snapshot) {
                if (snapshot.hasData) {
                  final saff = snapshot.data!;
    
                  return ListView(
                    children: [
                      if (saff.service1.isNotEmpty)
                        StaffServiceTile(
                          title: Text(
                            'Title for Service 1',
                            style: Theme.of(context).textTheme.headline5,
                          ),
                          services: saff.service1,
                        ),
                      if (saff.service2.isNotEmpty)
                        StaffServiceTile(
                          title: Text(
                            'Title for Service 2',
                            style: Theme.of(context).textTheme.headline5,
                          ),
                          services: saff.service2,
                        ),
                    ],
                  );
                } else if (snapshot.hasError) {
                  return const Text('Error on loaad data. Try again later.');
                } else {
                  return const CircularProgressIndicator();
                }
              },
            ),
          ),
        );
      }
    }
    
    class StaffServiceTile extends StatelessWidget {
      const StaffServiceTile({
        Key? key,
        required this.title,
        required this.services,
      }) : super(key: key);
    
      final Widget title;
      final List<Service> services;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            ListTile(title: title),
            for (var person in services)
              ListTile(
                title: Text(
                  '${person.firstname} ${person.lastname}',
                ),
              ),
          ],
        );
      }
    }
    
    class StaffService {
      StaffService({this.service1 = const [], this.service2 = const []});
    
      List<Service> service1, service2;
    
      factory StaffService.fromJson(String str) {
        return StaffService.fromMap(json.decode(str));
      }
    
      String toJson() => json.encode(toMap());
    
      factory StaffService.fromMap(Map<String, dynamic> json) => StaffService(
            service1: List<Service>.from(json["service1"].map((x) => Service.fromMap(x))),
            service2: List<Service>.from(json["service2"].map((x) => Service.fromMap(x))),
          );
    
      Map<String, dynamic> toMap() => {
            "service1": List<dynamic>.from(service1.map((x) => x.toMap())),
            "service2": List<dynamic>.from(service2.map((x) => x.toMap())),
          };
    }
    
    class Service {
      Service({this.firstname, this.lastname});
    
      String? firstname, lastname;
    
      factory Service.fromJson(String str) => Service.fromMap(json.decode(str));
    
      String toJson() => json.encode(toMap());
    
      factory Service.fromMap(Map<String, dynamic> json) => Service(
            firstname: json["firstname"],
            lastname: json["lastname"],
          );
    
      Map<String, dynamic> toMap() => {
            "firstname": firstname,
            "lastname": lastname,
          };
    }
    
    Future<StaffService> getStaffService() async {
      await Future.delayed(const Duration(seconds: 2));
      return StaffService.fromMap(data); // <- use fromJson if you load data from the JSON.
    }
    
    final data = <String, List<Map<String, String>>>{
      "service1": [
        {"firstname": "Peter", "lastname": "Smith"},
        {"firstname": "Paul", "lastname": "Johnson"}
      ],
      "service2": [
        {"firstname": "Mary", "lastname": "Williams"},
        {"firstname": "Guy", "lastname": "Brown"}
      ]
    };
    

    Copy and paste in the DartPad to test it.