How to solve: Flutter cannot call nested json API

Question

Asked by Quentinio on December 06, 2021 (source).

I am still new to flutter development and recently I've been trying to call API. I've managed to do so without placing data into the model and it worked fine. The issue is when I try to serialize the data. I've created a model using app.quicktype.io. and I am not sure how to call it in my views file.

The project is using Getx.

API manager

import 'package:api_test_2/models/event.dart';
import 'package:http/http.dart' as http;


class ApiManager {
  static var client = http.Client();


  // Future as <List<Event>> maybe ?

  static Future<Event?> fetchEvents() async {
    var response = await client.get(Uri.parse(
        'https://xposure.ae/wp-json/wp/auditorium/v1/events'));
    if (response.statusCode == 200) {
      var jsonString = response.body;
      print(jsonString);
      return eventFromJson(jsonString);
    } else {
      //show error message
      return null;
    }
  }
}

Event model

// To parse this JSON data, do
//
//     final event = eventFromJson(jsonString);

import 'package:meta/meta.dart';
import 'dart:convert';

Event eventFromJson(String str) => Event.fromJson(json.decode(str));

String eventToJson(Event data) => json.encode(data.toJson());

class Event {
  Event({
    required this.data,
  });

  List<Datum>? data;

  factory Event.fromJson(Map<String, dynamic> json) => Event(
    data: json["data"] == null ? null : List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
  );


  //null check added after data

  Map<String, dynamic> toJson() => {
    "data": data == null ? null : List<dynamic>.from(data!.map((x) => x.toJson())),
  };
}

class Datum {
  Datum({
    required this.eventtitle,
    required this.description,
    required this.eventImage,
    required this.speaker,
    required this.datetime,
    required this.location,
  });

  String? eventtitle;
  String description;
  String? eventImage;
  Speaker? speaker;
  String? datetime;
  Location? location;

  factory Datum.fromJson(Map<String, dynamic> json) => Datum(
    eventtitle: json["Eventtitle"] == null ? null : json["Eventtitle"],
    description: json["Description"] == null ? null : json["Description"],
    eventImage: json["event_image"] == null ? null : json["event_image"],
    speaker: json["Speaker"] == null ? null : Speaker.fromJson(json["Speaker"]),
    datetime: json["datetime"] == null ? null : json["datetime"],
    location: json["Location"] == null ? null : Location.fromJson(json["Location"]),
  );

  Map<String, dynamic> toJson() => {
    "Eventtitle": eventtitle == null ? null : eventtitle,
    "Description": description == null ? null : description,
    "event_image": eventImage == null ? null : eventImage,
    "Speaker": speaker == null ? null : speaker!.toJson(),
    "datetime": datetime == null ? null : datetime,
    "Location": location == null ? null : location!.toJson(),
  };
}

class Location {
  Location({
    required this.venue,
    required this.address,
  });

  Venue? venue;
  Address? address;

  factory Location.fromJson(Map<String, dynamic> json) => Location(
    venue: json["venue"] == null ? null : venueValues.map[json["venue"]],
    address: json["address"] == null ? null : addressValues.map[json["address"]],
  );

  //added null checks after reverse

  Map<String, dynamic> toJson() => {
    "venue": venue == null ? null : venueValues.reverse![venue],
    "address": address == null ? null : addressValues.reverse![address],
  };
}

enum Address { SHARJAH_BR_SHARJAH_BR_61110_BR_UNITED_ARAB_EMIRATES }

final addressValues = EnumValues({
  "Sharjah</br>Sharjah,</br>61110,</br>United Arab Emirates": Address.SHARJAH_BR_SHARJAH_BR_61110_BR_UNITED_ARAB_EMIRATES
});

enum Venue { XPOSURE_INTERNATIONAL_PHOTOGRAPHY_FESTIVAL }

final venueValues = EnumValues({
  "Xposure International Photography Festival": Venue.XPOSURE_INTERNATIONAL_PHOTOGRAPHY_FESTIVAL
});

class Speaker {
  Speaker({
    required this.speakername,
    required this.link,
  });

  String speakername;
  String link;

  factory Speaker.fromJson(Map<String, dynamic> json) => Speaker(
    speakername: json["speakername"] == null ? null : json["speakername"],
    link: json["link"] == null ? null : json["link"],
  );

  Map<String, dynamic> toJson() => {
    "speakername": speakername == null ? null : speakername,
    "link": link == null ? null : link,
  };
}

class EnumValues<T> {
  Map<String, T> map;
  Map<T, String>? reverseMap;

  EnumValues(this.map);

  Map<T, String>? get reverse {
    if (reverseMap == null) {
      reverseMap = map.map((k, v) => new MapEntry(v, k));
    }
    return reverseMap;
  }
}

Controller

import 'package:api_test_2/models/event.dart';
import 'package:api_test_2/services/api_manager.dart';
import 'package:get/state_manager.dart';


class EventController extends GetxController {
  var isLoading = true.obs;
  // var eventList = List<Event>().obs;
  var eventList = <Event>[].obs;

  @override
  void onInit() {
    fetchEvents();
    super.onInit();
  }

  void fetchEvents() async {
    try {
      isLoading(true);
      var events = await ApiManager.fetchEvents();
      if (events != null) {

        // added cast as List<Event>
        eventList.value = events as List<Event>;
      }
    } finally {
      isLoading(false);
    }
  }
}

Main view

import 'package:api_test_2/controllers/event_controller.dart';
import 'package:api_test_2/views/event_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:get/get.dart';
import 'package:get/instance_manager.dart';

class HomePage extends StatelessWidget {
  final EventController eventController = Get.put(EventController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: 0,
        leading: const Icon(
          Icons.arrow_back_ios,
        ),
        actions: [
          IconButton(
            icon: const Icon(
              Icons.shopping_cart,
            ),
            onPressed: () {},
          )
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                const Expanded(
                  child: Text(
                    'ShopX',
                    style: TextStyle(
                        fontFamily: 'avenir',
                        fontSize: 32,
                        fontWeight: FontWeight.w900),
                  ),
                ),
                IconButton(
                    icon: const Icon(Icons.view_list_rounded), onPressed: () {}),
                IconButton(icon: const Icon(Icons.grid_view), onPressed: () {}),
              ],
            ),
          ),
          Expanded(
            child: Obx(() {
              if (eventController.isLoading.value)
                return Center(child: CircularProgressIndicator());
              else {
                return StaggeredGridView.countBuilder(
                  crossAxisCount: 1,
                  itemCount: eventController.eventList.length,
                  crossAxisSpacing: 16,
                  mainAxisSpacing: 16,
                  itemBuilder: (context, index) {
                    return EventTile(eventController.eventList[index]);
                  },
                  staggeredTileBuilder: (index) => StaggeredTile.fit(1),
                );
              }
            }),
          )
        ],
      ),
    );
  }
}

Event tile

import 'package:api_test_2/models/event.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';


class EventTile extends StatelessWidget {
  final Event event;
  const EventTile(this.event);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Text(

            // ),
          ],
        ),
      ),
    );
  }
}

I think I'm doing something wrong with the conversion to List or Map but I am not sure if it's in my API manager class or the Event model itself. If anyone could help I would be so gratefull.

Answer

Question answered by S. M (source).

Your actual event list is inside the data property of your model. Therefore, your controller should be:

class EventController extends GetxController {
  final isLoading = true.obs;
  final event = Rxn<Event>();

 @override
 void onInit() async{
   await fetchEvents();
   super.onInit();
 }

 void fetchEvents() async {
  try {
    isLoading(true);
    var response = await ApiManager.fetchEvents();
    if (response != null) {
    event.value = response;
  }
 } finally {
    isLoading(false);
  }
 }
}

And your StaggeredGridView:

 return StaggeredGridView.countBuilder(
              crossAxisCount: 1,
              itemCount: eventController.event.value.data.length,
              crossAxisSpacing: 16,
              mainAxisSpacing: 16,
              itemBuilder: (context, index) {
                return EventTile(eventController.event.value.data[index]);
              },
              staggeredTileBuilder: (index) => StaggeredTile.fit(1),
            );

And finally, your EventTile:

class EventTile extends StatelessWidget {
    final Datum datum;
    const EventTile(this.datum);

    @override
    Widget build(BuildContext context) {
      return Card(
      elevation: 2,
      child: Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
        // Text(

        // ),
         ],
       ),
     ),
   );
 }
}
API FLUTTER FLUTTER-GETX JSON
SHARE: