1

New to flutter and dart, struggling to populate this dropdown from an api that returns an object and within the object there is a data array of objects. Looks like:

{
    "data": [
         {
        "Country_str_code": "AL",
        "Region_str_code": "Eur",
        "Country_str_name": "Albania"
    },
    {
        "Country_str_code": "DZ",
        "Region_str_code": "Afr",
        "Country_str_name": "Algeria"
    },
  ]
}

Below is the code for my widget. I will like just show the country name but not sure if I will need the country code in the dropdown so save it on submission. I cannot even get this to show. I'm getting this error:

import 'dart:async';
import 'dart:convert';
   
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'register.dart';
    
Future<List<Country>> fetchCountries() async {
  final response = await http.get(Uri.https('localhost/msd', '/api/countries'));
  final responseJson = json.decode(response.body);

  print(responseJson['data']);

  if (response.statusCode == 200) {
 
  return (responseJson['data'] as List<Country>)
    .map((country) => country);
  } else {
    throw Exception('Failed to load countries');
  }
}

class Country {
  final String countryCode;
  final String countryName;

  Country({@required this.countryCode, @required this.countryName});

  factory Country.fromJson(Map<String, dynamic> json) {
    var countrNameFromJson = json['Country_str_name'];
    var countrCodeFromJson = json['Country_str_code'];
    String castedCountryName = countrNameFromJson.cast<String>();
    String castedCountryCode = countrCodeFromJson.cast<String>();
    return Country(
      countryCode: castedCountryCode,
      countryName: castedCountryName
    );
  }
}

class Location extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return LocationState();
  }

}

class LocationState extends State<Location> {
  Future<List<Country>> futureCountry;
  Map<String, dynamic> _selected = {'countryCode': '', 'countryName':'Select Country'};

  @override
  void initState() {
    super.initState();
    futureCountry = fetchCountries();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Registrate'),
        backgroundColor: Colors.blue[600],
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Center(
            child:  FutureBuilder<List<Country>>(
              future: futureCountry,
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  print (snapshot.data);
                  return DropdownButton(
                    value: _selected,
                    icon: Icon(Icons.arrow_drop_down),
                    iconSize: 30, 
                    elevation: 16,
                    style: TextStyle(color: Colors.black),
                    onChanged: (newValue) {
                      setState(() {
                        _selected = newValue;
                      });
                    },
                    items: snapshot.data
                      .map<DropdownMenuItem<String>>((Country value) {
                        return DropdownMenuItem<String>(
                          value: value.countryCode,
                          child: Text(value.countryName),
                        );
                    }).toList(),
                  );
                } else if (snapshot.hasError) {
                  return Text("${snapshot.error}");
                }
                return CircularProgressIndicator();
              },
            ),
          ),
          SizedBox(
            width: double.maxFinite,
            child: ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => Register())
                );
              },
              child: Text('Continue'),
              style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all<Color>(Colors.red[900]),
                foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
              )
            ),
          ),
        ],
      ),
    );
  }
}

Currently getting this error:

type 'List<dynamic> is not a subtype of type 'List<Country>' in type cast

Can someone please guide me, as the error says clearly its an issue with the types but I'm not sure how solve this.

sayayin
  • 971
  • 5
  • 23
  • 36

2 Answers2

0

The problem with the type is that until you map the content of responseJson['data'] it is not a List of Country objects is just dynamic, and you call map after trying to cast as Country thus the error.

  return (responseJson['data'] as List<Country>)
    .map((country) => country);

You can solve it just by casting to List instead of List<Country>

  return (responseJson['data'] as List)
    .map((country) => Country.fromJson(country)).toList();

You might also wonder why I call the toList() method after mapping, that is because the map function returns an Iterable and not a List.

I have corrected the snippet you shared and you can just copy/paste it and see how it works (I've deleted the http code for the sake of not having your localhost to test ).

import 'dart:async';
import 'dart:convert';
   
import 'package:flutter/material.dart';


void main(){
runApp(MaterialApp(home: Location(),));

}

Future<List<Country>> fetchCountries() async {
  final responseJson = json.decode("""
  {
    "data": [
         {
        "Country_str_code": "AL",
        "Region_str_code": "Eur",
        "Country_str_name": "Albania"
    },
    {
        "Country_str_code": "DZ",
        "Region_str_code": "Afr",
        "Country_str_name": "Algeria"
    }
  ]
}
  """);

  print(responseJson['data']);

  return (responseJson['data'] as List)
    .map((country) => Country.fromJson(country)).toList();
}

class Country {
  final String countryCode;
  final String countryName;

  Country({@required this.countryCode, @required this.countryName});

  factory Country.fromJson(Map<String, dynamic> json) {

    return Country(
      countryCode: 
      json['Country_str_code'],
      countryName: 
      json['Country_str_name'],
    
    );
  }
}

class Location extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => LocationState();
  

}

class LocationState extends State<Location> {
  Future<List<Country>> futureCountry;
  Country _selected ;

  @override
  void initState() {
    super.initState();
    futureCountry = fetchCountries();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Registrate'),
        backgroundColor: Colors.blue[600],
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Center(
            child:  FutureBuilder<List<Country>>(
              future: futureCountry,
              builder: (context, snapshot) {
                if (snapshot.hasData) {

                  return DropdownButton(
                    value: _selected,
                    icon: Icon(Icons.arrow_drop_down),
                    iconSize: 30, 
                    elevation: 16,
                    style: TextStyle(color: Colors.black),
                    onChanged: (newValue) {
                      setState(() {
                        _selected = newValue;
                      });
                    },
                    items: snapshot.data
                      .map<DropdownMenuItem<Country>>((Country value) {
                        return DropdownMenuItem<Country>(
                          value: value,
                          child: Text(value.countryName),
                        );
                    }).toList(),
                  );
                } else if (snapshot.hasError) {
                  return Text("${snapshot.error}");
                }
                return CircularProgressIndicator();
              },
            ),
          ),
          SizedBox(
            width: double.maxFinite,
            child: ElevatedButton(
              onPressed: () {
              },
              child: Text('Continue'),
              style: ButtonStyle(
                backgroundColor: MaterialStateProperty.all<Color>(Colors.red[900]),
                foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
              )
            ),
          ),
        ],
      ),
    );
  }
}

croxx5f
  • 5,163
  • 2
  • 15
  • 36
  • Thank you for your answer. This helped get rid of the casting issue but now I'm stuck on this other error, something with the dropdown selected value. There should be exactly one item with [DropdownButtons]'s value: Select. Either zero or 2 or more [DropdownMenuItems]s were detected with the same value. Guess it has something to do with that selected value, not sure how to solve it. No matter I do it leads to a different error – sayayin Apr 24 '21 at 16:08
  • I have been able to do this in the past with just string arrays and had to make sure the initial selected value is also part of the main array but in this case here not sure what to do. Also each item is an object, not sure if I still have to pass in the object to the dropdown or sting, not matter what I do it keeps complaining :-) – sayayin Apr 24 '21 at 16:31
  • Check out the code in my answer there I change the selected value to Country instead of Map with an initial value of null(unselected). That way the compiler does not complain. Run the whole code and see if it works the way you wanted it to. – croxx5f Apr 24 '21 at 18:40
  • Thank you! yes, that worked but what if I wanted it to say something like "Select Country", any ideas how would I achieve that? – sayayin Apr 24 '21 at 19:25
  • Just set `hint: Text('Select a country')` in the DropdownButton widget. – croxx5f Apr 24 '21 at 21:31
  • You saved my day. Thank you so much! – sayayin Apr 24 '21 at 21:36
  • No problem, whenever you're stuck without knowing how to use a widget remember that autocomplete and the widgets comments are your friends – croxx5f Apr 24 '21 at 21:38
0
(responseJson['data'] as List<Country>)

Dart doesn't know how to convert/cast the items that are in responseJson['data'] (a List it seems) into Country objects.

You'll have to take the list items from responseJson['data'], one by one, and instantiate Country objects. Using .map like you're trying, is a common way to go.

It would be something like this: (do not copy/paste this stream-of-consciousness fake-code):

List<Country> countries = responseJson['data'].map((data) => Country(data.countryName, data.countryCode)).toList();

This is very error prone to do by hand and it's not recommended. It's advised to use a code-generator to create serialization/deserialization classes/methods for you as per Flutter.dev.

Here's also some tips on using the recommended Google json_serialization / build_runner code-generation tools.

Baker
  • 24,730
  • 11
  • 100
  • 106