skip to Main Content

I am still learning Flutter and have been having a blast, this has been the fastest I’ve developed a mobile app ever. I am having a bit of trouble with null safety: Null check operator used on a null value the first widget bellow is responsible for rendering a list of values on a ListTile, and the second is the main widget which creates the Tabs, the API response contains null values, I still want to be able to use this data what is the best way to use the data while still having null safety? Or how can I modify the bellow code to help with null safety?

Update: I’ve realised that in the response there are multiple values that are null. I can’t post the whole response as it is way too big but I have left a portion bellow. I’ve updated the post to better suit this issue.

// First widget
import 'package:flutter/material.dart';
import 'package:valoranttools/models/agent_contracts.dart';
import 'package:valoranttools/services/data.dart';

class AgentContractItem extends StatefulWidget {
  const AgentContractItem({super.key});

  @override
  State<AgentContractItem> createState() => _AgentContractItemState();
}

class _AgentContractItemState extends State<AgentContractItem> {
  late Future<Contract> contractData;

  @override
  void initState() {
    super.initState();
    contractData = fetchContracts();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Contract>(
        future: contractData,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            var contracts = snapshot.data!;
            print(contracts);

            return ListView.builder(
              itemCount: contracts.data.length,
              itemBuilder: (context, index) {
                var contract = contracts.data[index];

                return ListTile(
                  title: Text(contract.displayName),
                );
              },
            );
          } else if (snapshot.hasError) {
            return Text('${snapshot.error}');
          }

          // By default, show a loading spinner.
          return const Center(
            child: CircularProgressIndicator(),
          );
        });
  }
}
// Second widget or main tabs widget
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:valoranttools/Agents/agent_contract_item.dart';
import 'package:valoranttools/Agents/agent_item.dart';
import 'package:valoranttools/services/data.dart';

import 'package:valoranttools/models/agent.dart';

class AgentTabScreen extends StatefulWidget {
  const AgentTabScreen({super.key});

  @override
  State<AgentTabScreen> createState() => _AgentTabScreenState();
}

class _AgentTabScreenState extends State<AgentTabScreen> {
  late Future<Agent> agentData;

  @override
  void initState() {
    super.initState();
    agentData = fetchAgents();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Agent>(
      future: agentData,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          var agents = snapshot.data!;

          return DefaultTabController(
            length: 2,
            child: Scaffold(
              appBar: AppBar(
                title: Text('Agents & Contracts'),
                bottom: const TabBar(
                  tabs: [
                    Tab(
                      icon: Icon(FontAwesomeIcons.userNinja),
                    ),
                    Tab(
                      icon: Icon(FontAwesomeIcons.bookMedical),
                    ),
                  ],
                ),
              ),
              body: TabBarView(children: [
                GridView.count(
                  primary: false,
                  padding: const EdgeInsets.all(20.0),
                  crossAxisSpacing: 10.0,
                  crossAxisCount: 2,
                  children: agents.data
                      .map((agent) => AgentItem(agent: agent))
                      .toList(),
                ),
                const AgentContractItem()
              ]),
            ),
          );
        } else if (snapshot.hasError) {
          return Text('${snapshot.error}');
        }

        // By default, show a loading spinner.
        return const Center(
          child: CircularProgressIndicator(),
        );
      },
    );
  }
}
// Portion of API reponse
"data": [
        {
            "uuid": "cae6ab4a-4b4a-69a0-3c7a-48b17e313f52",
            "displayName": "Gekko Contract",
            "displayIcon": null,
            "shipIt": false,
            "freeRewardScheduleUuid": "32cb6884-4f18-65e6-f84d-8ea09821c74b",
            "content": {
                "relationType": "Agent",
                "relationUuid": "e370fa57-4757-3604-3648-499e1f642d3f",
                "chapters": [
                    {
                        "isEpilogue": false,
                        "levels": [
                            {
                                "reward": {
                                    "type": "Spray",
                                    "uuid": "cd56a893-44f1-2b56-a477-08a9652c66f8",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 20000,
                                "vpCost": 200,
                                "isPurchasableWithVP": true,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            },
                            {
                                "reward": {
                                    "type": "PlayerCard",
                                    "uuid": "0d62fdfb-482e-2ddf-87eb-7ca7c88f222f",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 30000,
                                "vpCost": 200,
                                "isPurchasableWithVP": true,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            },
                            {
                                "reward": {
                                    "type": "Title",
                                    "uuid": "e64d8d84-46a5-6a9b-ee09-08af910b3235",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 40000,
                                "vpCost": 200,
                                "isPurchasableWithVP": true,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            },
                            {
                                "reward": {
                                    "type": "Spray",
                                    "uuid": "93a69ca4-4422-1e2f-d7e0-3baa04b745fd",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 50000,
                                "vpCost": 200,
                                "isPurchasableWithVP": true,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            },
                            {
                                "reward": {
                                    "type": "Character",
                                    "uuid": "e370fa57-4757-3604-3648-499e1f642d3f",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 60000,
                                "vpCost": 200,
                                "isPurchasableWithVP": true,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            }
                        ],
                        "freeRewards": null
                    },
                    {
                        "isEpilogue": false,
                        "levels": [
                            {
                                "reward": {
                                    "type": "EquippableCharmLevel",
                                    "uuid": "cdfc181f-4e27-3bb0-633c-4f8e52348cd0",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 75000,
                                "vpCost": 0,
                                "isPurchasableWithVP": false,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            },
                            {
                                "reward": {
                                    "type": "Spray",
                                    "uuid": "eb91c1e4-4553-a885-e9cc-1a9db6a8b344",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 100000,
                                "vpCost": 0,
                                "isPurchasableWithVP": false,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            },
                            {
                                "reward": {
                                    "type": "Title",
                                    "uuid": "e79d9585-4ea7-f8f1-3d4e-31a1e2c71577",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 150000,
                                "vpCost": 0,
                                "isPurchasableWithVP": false,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            },
                            {
                                "reward": {
                                    "type": "PlayerCard",
                                    "uuid": "35e11e04-46d3-9e5f-c2a5-e5beb9a59e97",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 200000,
                                "vpCost": 0,
                                "isPurchasableWithVP": false,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            },
                            {
                                "reward": {
                                    "type": "EquippableSkinLevel",
                                    "uuid": "acab9281-445b-e301-19c6-fe91e3ef27fa",
                                    "amount": 1,
                                    "isHighlighted": false
                                },
                                "xp": 250000,
                                "vpCost": 0,
                                "isPurchasableWithVP": false,
                                "doughCost": 0,
                                "isPurchasableWithDough": false
                            }
                        ],
                        "freeRewards": null
                    }
                ],
                "premiumRewardScheduleUuid": null,
                "premiumVPCost": -1
            },
            "assetPath": "ShooterGame/Content/Contracts/Characters/Aggrobot/Contract_Aggrobot_DataAssetV2"
        },
    ]
}
// Model
import 'dart:convert';

Contract contractFromJson(String str) => Contract.fromJson(json.decode(str));

String contractToJson(Contract data) => json.encode(data.toJson());

class Contract {
    int status;
    List<Datum> data;

    Contract({
        required this.status,
        required this.data,
    });

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

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

class Datum {
    String uuid;
    String displayName;
    String? displayIcon;
    bool shipIt;
    String freeRewardScheduleUuid;
    Content content;
    String assetPath;

    Datum({
        required this.uuid,
        required this.displayName,
        this.displayIcon,
        required this.shipIt,
        required this.freeRewardScheduleUuid,
        required this.content,
        required this.assetPath,
    });

    factory Datum.fromJson(Map<String, dynamic> json) => Datum(
        uuid: json["uuid"],
        displayName: json["displayName"],
        displayIcon: json["displayIcon"],
        shipIt: json["shipIt"],
        freeRewardScheduleUuid: json["freeRewardScheduleUuid"],
        content: Content.fromJson(json["content"]),
        assetPath: json["assetPath"],
    );

    Map<String, dynamic> toJson() => {
        "uuid": uuid,
        "displayName": displayName,
        "displayIcon": displayIcon,
        "shipIt": shipIt,
        "freeRewardScheduleUuid": freeRewardScheduleUuid,
        "content": content.toJson(),
        "assetPath": assetPath,
    };
}

class Content {
    RelationType? relationType;
    String? relationUuid;
    List<Chapter> chapters;
    String? premiumRewardScheduleUuid;
    int premiumVpCost;

    Content({
        this.relationType,
        this.relationUuid,
        required this.chapters,
        this.premiumRewardScheduleUuid,
        required this.premiumVpCost,
    });

    factory Content.fromJson(Map<String, dynamic> json) => Content(
        relationType: relationTypeValues.map[json["relationType"]]!,
        relationUuid: json["relationUuid"],
        chapters: List<Chapter>.from(json["chapters"].map((x) => Chapter.fromJson(x))),
        premiumRewardScheduleUuid: json["premiumRewardScheduleUuid"],
        premiumVpCost: json["premiumVPCost"],
    );

    Map<String, dynamic> toJson() => {
        "relationType": relationTypeValues.reverse[relationType],
        "relationUuid": relationUuid,
        "chapters": List<dynamic>.from(chapters.map((x) => x.toJson())),
        "premiumRewardScheduleUuid": premiumRewardScheduleUuid,
        "premiumVPCost": premiumVpCost,
    };
}

class Chapter {
    bool isEpilogue;
    List<Level> levels;
    List<Reward>? freeRewards;

    Chapter({
        required this.isEpilogue,
        required this.levels,
        this.freeRewards,
    });

    factory Chapter.fromJson(Map<String, dynamic> json) => Chapter(
        isEpilogue: json["isEpilogue"],
        levels: List<Level>.from(json["levels"].map((x) => Level.fromJson(x))),
        freeRewards: json["freeRewards"] == null ? [] : List<Reward>.from(json["freeRewards"]!.map((x) => Reward.fromJson(x))),
    );

    Map<String, dynamic> toJson() => {
        "isEpilogue": isEpilogue,
        "levels": List<dynamic>.from(levels.map((x) => x.toJson())),
        "freeRewards": freeRewards == null ? [] : List<dynamic>.from(freeRewards!.map((x) => x.toJson())),
    };
}

class Reward {
    Type type;
    String uuid;
    int amount;
    bool isHighlighted;

    Reward({
        required this.type,
        required this.uuid,
        required this.amount,
        required this.isHighlighted,
    });

    factory Reward.fromJson(Map<String, dynamic> json) => Reward(
        type: typeValues.map[json["type"]]!,
        uuid: json["uuid"],
        amount: json["amount"],
        isHighlighted: json["isHighlighted"],
    );

    Map<String, dynamic> toJson() => {
        "type": typeValues.reverse[type],
        "uuid": uuid,
        "amount": amount,
        "isHighlighted": isHighlighted,
    };
}

enum Type { PLAYER_CARD, TITLE, EQUIPPABLE_CHARM_LEVEL, CURRENCY, SPRAY, EQUIPPABLE_SKIN_LEVEL, CHARACTER }

final typeValues = EnumValues({
    "Character": Type.CHARACTER,
    "Currency": Type.CURRENCY,
    "EquippableCharmLevel": Type.EQUIPPABLE_CHARM_LEVEL,
    "EquippableSkinLevel": Type.EQUIPPABLE_SKIN_LEVEL,
    "PlayerCard": Type.PLAYER_CARD,
    "Spray": Type.SPRAY,
    "Title": Type.TITLE
});

class Level {
    Reward reward;
    int xp;
    int vpCost;
    bool isPurchasableWithVp;
    int doughCost;
    bool isPurchasableWithDough;

    Level({
        required this.reward,
        required this.xp,
        required this.vpCost,
        required this.isPurchasableWithVp,
        required this.doughCost,
        required this.isPurchasableWithDough,
    });

    factory Level.fromJson(Map<String, dynamic> json) => Level(
        reward: Reward.fromJson(json["reward"]),
        xp: json["xp"],
        vpCost: json["vpCost"],
        isPurchasableWithVp: json["isPurchasableWithVP"],
        doughCost: json["doughCost"],
        isPurchasableWithDough: json["isPurchasableWithDough"],
    );

    Map<String, dynamic> toJson() => {
        "reward": reward.toJson(),
        "xp": xp,
        "vpCost": vpCost,
        "isPurchasableWithVP": isPurchasableWithVp,
        "doughCost": doughCost,
        "isPurchasableWithDough": isPurchasableWithDough,
    };
}

enum RelationType { AGENT, EVENT, SEASON }

final relationTypeValues = EnumValues({
    "Agent": RelationType.AGENT,
    "Event": RelationType.EVENT,
    "Season": RelationType.SEASON
});

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

    EnumValues(this.map);

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

Console output of exception

2

Answers


  1. Chosen as BEST ANSWER

    After a lot more research of my own, I solved this by changing the models file to make sure any values that could possibly be null had a fallback value, I chose something simple "NA".

    I understand that this may have seemed simple, but I am completely new to Flutter's way of Null safety. I have left the updated model file bellow for anyone who has the same trouble as myself.

    import 'dart:convert';
    
    Contract contractFromJson(String str) => Contract.fromJson(json.decode(str));
    
    String contractToJson(Contract data) => json.encode(data.toJson());
    
    class Contract {
      int status;
      List<Datum> data;
    
      Contract({
        required this.status,
        required this.data,
      });
    
      factory Contract.fromJson(Map<String, dynamic> json) => Contract(
            status: json["status"],
            data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
          );
    
      Map<String, dynamic> toJson() => {
            "status": status,
            "data": List<dynamic>.from(data.map((x) => x.toJson())),
          };
    }
    
    class Datum {
      String uuid;
      String displayName;
      String displayIcon;
      bool shipIt;
      String freeRewardScheduleUuid;
      Content content;
      String assetPath;
    
      Datum({
        required this.uuid,
        required this.displayName,
        required this.displayIcon,
        required this.shipIt,
        required this.freeRewardScheduleUuid,
        required this.content,
        required this.assetPath,
      });
    
      factory Datum.fromJson(Map<String, dynamic> json) => Datum(
            uuid: json["uuid"],
            displayName: json["displayName"],
            displayIcon: json["displayIcon"] ?? "NA",
            shipIt: json["shipIt"],
            freeRewardScheduleUuid: json["freeRewardScheduleUuid"],
            content: Content.fromJson(json["content"]),
            assetPath: json["assetPath"],
          );
    
      Map<String, dynamic> toJson() => {
            "uuid": uuid,
            "displayName": displayName,
            "displayIcon": displayIcon,
            "shipIt": shipIt,
            "freeRewardScheduleUuid": freeRewardScheduleUuid,
            "content": content.toJson(),
            "assetPath": assetPath,
          };
    }
    
    class Content {
      RelationType? relationType;
      String? relationUuid;
      List<Chapter> chapters;
      String? premiumRewardScheduleUuid;
      int premiumVpCost;
    
      Content({
        this.relationType,
        this.relationUuid,
        required this.chapters,
        this.premiumRewardScheduleUuid,
        required this.premiumVpCost,
      });
    
      factory Content.fromJson(Map<String, dynamic> json) => Content(
            relationType: relationTypeValues.map[json["relationType"]],
            relationUuid: json["relationUuid"] ?? "NA",
            chapters: List<Chapter>.from(json["chapters"].map((x) => Chapter.fromJson(x))),
            premiumRewardScheduleUuid: json["premiumRewardScheduleUuid"] ?? "NA",
            premiumVpCost: json["premiumVPCost"],
          );
    
      Map<String, dynamic> toJson() => {
            "relationType": relationTypeValues.reverse[relationType],
            "relationUuid": relationUuid,
            "chapters": List<dynamic>.from(chapters.map((x) => x.toJson())),
            "premiumRewardScheduleUuid": premiumRewardScheduleUuid,
            "premiumVPCost": premiumVpCost,
          };
    }
    
    class Chapter {
      bool isEpilogue;
      List<Level> levels;
      List<Reward>? freeRewards;
    
      Chapter({
        required this.isEpilogue,
        required this.levels,
        this.freeRewards,
      });
    
      factory Chapter.fromJson(Map<String, dynamic> json) => Chapter(
            isEpilogue: json["isEpilogue"],
            levels: List<Level>.from(json["levels"].map((x) => Level.fromJson(x))),
            freeRewards: json["freeRewards"] == null ? [] : List<Reward>.from(json["freeRewards"].map((x) => Reward.fromJson(x))),
          );
    
      Map<String, dynamic> toJson() => {
            "isEpilogue": isEpilogue,
            "levels": List<dynamic>.from(levels.map((x) => x.toJson())),
            "freeRewards": freeRewards == null ? [] : List<dynamic>.from(freeRewards!.map((x) => x.toJson())),
          };
    }
    
    class Reward {
      Type type;
      String uuid;
      int amount;
      bool isHighlighted;
    
      Reward({
        required this.type,
        required this.uuid,
        required this.amount,
        required this.isHighlighted,
      });
    
      factory Reward.fromJson(Map<String, dynamic> json) => Reward(
            type: typeValues.map[json["type"]] ?? Type.CHARACTER,
            uuid: json["uuid"],
            amount: json["amount"],
            isHighlighted: json["isHighlighted"],
          );
    
      Map<String, dynamic> toJson() => {
            "type": typeValues.reverse[type],
            "uuid": uuid,
            "amount": amount,
            "isHighlighted": isHighlighted,
          };
    }
    
    enum Type {
      PLAYER_CARD,
      TITLE,
      EQUIPPABLE_CHARM_LEVEL,
      CURRENCY,
      SPRAY,
      EQUIPPABLE_SKIN_LEVEL,
      CHARACTER
    }
    
    final typeValues = EnumValues({
      "Character": Type.CHARACTER,
      "Currency": Type.CURRENCY,
      "EquippableCharmLevel": Type.EQUIPPABLE_CHARM_LEVEL,
      "EquippableSkinLevel": Type.EQUIPPABLE_SKIN_LEVEL,
      "PlayerCard": Type.PLAYER_CARD,
      "Spray": Type.SPRAY,
      "Title": Type.TITLE
    });
    
    class Level {
      Reward reward;
      int xp;
      int vpCost;
      bool isPurchasableWithVp;
      int doughCost;
      bool isPurchasableWithDough;
    
      Level({
        required this.reward,
        required this.xp,
        required this.vpCost,
        required this.isPurchasableWithVp,
        required this.doughCost,
        required this.isPurchasableWithDough,
      });
    
      factory Level.fromJson(Map<String, dynamic> json) => Level(
            reward: Reward.fromJson(json["reward"]),
            xp: json["xp"],
            vpCost: json["vpCost"],
            isPurchasableWithVp: json["isPurchasableWithVP"],
            doughCost: json["doughCost"],
            isPurchasableWithDough: json["isPurchasableWithDough"],
          );
    
      Map<String, dynamic> toJson() => {
            "reward": reward.toJson(),
            "xp": xp,
            "vpCost": vpCost,
            "isPurchasableWithVP": isPurchasableWithVp,
            "doughCost": doughCost,
            "isPurchasableWithDough": isPurchasableWithDough,
          };
    }
    
    enum RelationType { AGENT, EVENT, SEASON }
    
    final relationTypeValues = EnumValues({
      "Agent": RelationType.AGENT,
      "Event": RelationType.EVENT,
      "Season": RelationType.SEASON
    });
    
    class EnumValues<T> {
      Map<String, T> map;
      late Map<T, String> reverseMap;
    
      EnumValues(this.map);
    
      Map<T, String> get reverse {
        reverseMap = map.map((k, v) => MapEntry(v, k));
        return reverseMap;
      }
    }
    

  2. Check the data in fetchContracts and fetchAgents. Also Flutter 2.0 does enable null safety so could be an idea to migrate. But yes add print statments or breakpoints in methods to check for null values.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search