cover image
Nitish Kumar Singh
Nitish Kumar Singh
Clock Image 13 minutes
Posted on
December 27, 2020

Multi-Level dropdown in a flutter - Part 2


For this, I don’t have any idea what to use but I am thinking of using something which can be used by most of you.

  • Heroku: This requires an easy setup
  • Firebase: This requires some setup and this might be difficult
  • Custom Server: This will be not easy for the beginner as this will include some other stack

I will go with Heroku, and I have a GitHub repo that is one-click deploy. Just deploy that on your Heroku and get try it yourself. This is something that anyone of you can do because you don’t need to write any code.

Skip the Backend

Backend #

As this will require some backend and for that, I can’t keep any server as long as this article is available. So, I came up with a solution which is on click deploy. You can deploy this API using a single click, no setup is required.

Deploy

You just need one free Heroku account and you have a running API. No Credit Card is required for a free Heroku account.

Testing Backend #

To Test the backend API you first need to find the URL of your deployed Heroku app. You can find an Open App button at the right top corner of your Heroku app dashboard.

Let’s consider that your app URL is https://pin-code-api.herokuapp.com/ then you will API endpoint like this

Open your app https://pin-code-api.herokuapp.com/ and if you see output it means you have successfully deployed the application.

To test the Backend API you will need the API docs. From API docs you will get the endpoint and the request method details.

API Docs #

You app url is going to be the baseurl of your API - https://pin-code-api.herokuapp.com/

To make the request to the endpoint you need to concatenate the base_url and path. Concatenating will give you the endpoint and when you will GET request to the endpoint you will get the desired output.

pathSummary
/api/pin/Query one post office from each state
/api/pin/:stateQuery on post office from the selected District and selected State

I am proving the minimum docs that we require for the flutter app. If you want to know more about the complete API you can head over to the Github repository.

If you would like to have a look a the API Code then you can find it on my github.

sample_api_response.json
{ 
    "message":"List of Post Office",
    "data":[
        {
            "postOfficeName":"string",
            "pincode":"string",
            "districtsName":"string",
            "city":"string",
            "state":"string",
        },
        {
            "postOfficeName":"string",
            "pincode":"string",
            "districtsName":"string",
            "city":"string",
            "state":"string",
        }
    ]
}

Get Started #

First, let’s create a model because this is something which we will be requiring at very first.

After seeing the above sample response you probably got the idea about the shape of the data.

particularType
pincodeString
postOfficeNameString
cityString
districtsNameString
stateString
post_office.dart
class PostOffice {
  final String postOfficeName;
  final String pinCode;
  final String districtsName;
  final String city;
  final String state;

  PostOffice({
    this.pinCode,
    this.postOfficeName,
    this.city,
    this.districtsName,
    this.state,
  });

  factory PostOffice.fromJson(Map<String, dynamic> json) {
    return PostOffice(
      state: json['State'],
      districtsName: json['DistrictsName'],
      city: json['City'],
      postOfficeName: json['PostOfficeName'],
      pinCode: json['Pincode'],
    );
  }
}

There might be some people who are not aware of factory constructor. You can just copy and paste the above code, it will work.

If you are interested in understanding then let me make you understand this in simple language.

PostOffice.fromJson factory constructor initializes all final variable from a JSON object and returns an object of PostOffice.

factory PostOffice.fromJson(Map<String, dynamic> json) {
    return PostOffice(
        state: json['State'],
        districtsName: json['DistrictsName'],
        city: json['City'],
        postOfficeName: json['PostOfficeName'],
        pinCode: json['Pincode'],
    );
}

Looking for Flutter Developer

Hire Now

Flutter UI #

Screenshot of final UI

Now we will create the UI of the app, the UI will be very much similar to the previous one. In Part 1 of this article, we saw two dropdowns, country, and province(state). In this part, we will see the province(state) and District dropdown.

network_dropdown.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:multilevel_dropdown_flutter/model/post_office.dart';

class NetworkDropDown extends StatefulWidget {
  @override
  _NetworkDropDownState createState() => _NetworkDropDownState();
}

class _NetworkDropDownState extends State<NetworkDropDown> {
  final String baseURL = "https://pin-code-api.herokuapp.com";

  List<PostOffice> states = [];
  List<PostOffice> districts = [];
  
  PostOffice selectedState;
  PostOffice selectedDistrict;

  @override
  void initState() {
    super.initState();
    // On Page Load Get all the states from the server
    listState().then((List<PostOffice> value) {
      print('List of State/Province is loaded')
      setState(() {
        states = value;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Network Dropdown'),
      ),
    );
  }

  Future<List<PostOffice>> apiCall(String endpoint) async {
    http.Response response = await http.get(endpoint);
    String body = response.body;
    Map<String, dynamic> jsonResponse = json.decode(body);
    List data = jsonResponse['data'];
    return data.map((e) => PostOffice.fromJson(e)).toList();
  }
  
}

Variable #

There are various things in the app and I will cover all of them one by one. As I told earlier we will have two dropdown province(state) and District. So we need two arrays for them and also two variables to store their current value. I store the base_url in one variable so that we can easily change and maintain the code.

final String baseURL = "https://pin-code-api.herokuapp.com";

List<PostOffice> states = [];
List<PostOffice> districts = [];

PostOffice selectedState;
PostOffice selectedDistrict;

Method - apiCall #

This method is sending a request to the given endpoint and return a List of PostOffices.

Future<List<PostOffice>> listState(String endpoint) async {
  http.Response response = await http.get(endpoint);
  String body = response.body;
  Map<String, dynamic> jsonResponse = json.decode(body);
  List data = jsonResponse['data'];
  return data.map((e) => PostOffice.fromJson(e)).toList();
}

initState #

when the page will open we need to fill the dropdown of states with data. We have to lead the data from the network and set it into the dropdown. This is needed to be done the first time the page gets shown, so for that initState is the best of doing that.

As initState can’t be async it means we can’t use await. But there is two way to use an async method in the non-async method.

  • use then with an async method
  • call another async method
  @override
  void initState() {
    super.initState();
    // On Page Load Get all the states from the server
    String endpoint = "$baseURL/api/pin";
    listState(endpoint).then((List<PostOffice> value) {
      print('List of State/Province is loaded')
      setState(() {
        states = value;
      });
    });
  }

Now we can focus on building the UI of the body, I mean adding two dropdowns. We have loaded the state data in initState and so now we will show the value in the dropdown.

building body UI #

network_dropdown.dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Network Dropdown'),
      ),
      body: ListView(
        padding: EdgeInsets.all(20.0),
        children: [
          // State Dropdown
          DropdownButton<PostOffice>(
            hint: Text('State'),
            value: selectedState,
            isExpanded: true,
            items: states.map((PostOffice postOffice) {
              return DropdownMenuItem<PostOffice>(
                value: postOffice,
                child: Text(postOffice.state),
              );
            }).toList(),
            onChanged: onStateChange,
          ),
          // State Dropdown Ends here
        ],
      ),
    );
  }

  void onStateChange(PostOffice state) {
    setState(() {
      selectedState = state;
    });
    // Sending Request to get Districts of selected State
    String endpoint = "$baseURL/api/pin/${selectedState.state}";
    listState(endpoint).then((List<PostOffice> value) {
      setState(() {
        districts = value;
      });
    });
  }

Second Dropdown #

Now we will add the second dropdown in the body. In the previous method onStateChange we load the district.

network_dropdown.dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Network Dropdown'),
      ),
      body: ListView(
        padding: EdgeInsets.all(20.0),
        children: [
          // State Dropdown
          DropdownButton<PostOffice>(
            hint: Text('State'),
            value: selectedState,
            isExpanded: true,
            items: states.map((PostOffice postOffice) {
              return DropdownMenuItem<PostOffice>(
                value: postOffice,
                child: Text(postOffice.state),
              );
            }).toList(),
            onChanged: onStateChange,
          ),
          // State Dropdown Ends here

          // Districts Dropdown
          DropdownButton<PostOffice>(
            hint: Text('District'),
            value: selectedDistrict,
            isExpanded: true,
            items: districts.map((PostOffice postOffice) {
              return DropdownMenuItem<PostOffice>(
                value: postOffice,
                child: Text(postOffice.districtsName),
              );
            }).toList(),
            onChanged: onDistrictChange,
          ),
          // Districts Dropdown Ends here
        ],
      ),
    );
  }

  void onDistrictChange(PostOffice district) {
    setState(() {
      selectedDistrict = district;
    });
  }

Issue #

The above code will work fine for one time but when you will try this for the second time it will not work fine. The reason is when you change the state then the corresponding district is also being changed.

We need to reset the value of district to null when we change the state. selectedDistrict of State A will not be available in State A.

void onStateChange(PostOffice state) {
  setState(() {
    selectedState = state;
    districts = [];
    selectedDistrict = null;
  });
  // Sending Request to get Districts of selected State
  String endpoint = "$baseURL/api/pin/${selectedState.state}";
  listState(endpoint).then((List<PostOffice> value) {
    setState(() {
      districts = value;
    });
  });
}

Final Code #

GitHub Link

Here is the final code of network dropdown, the complete code is also available on GitHub.

network_dropdown.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:multilevel_dropdown_flutter/model/post_office.dart';

class NetworkDropDown extends StatefulWidget {
  @override
  _NetworkDropDownState createState() => _NetworkDropDownState();
}

class _NetworkDropDownState extends State<NetworkDropDown> {
  final String baseURL = "https://pin-code-api.herokuapp.com";

  PostOffice selectedState;
  PostOffice selectedDistrict;

  List<PostOffice> states = [];
  List<PostOffice> districts = [];

  @override
  void initState() {
    super.initState();
    // On Page Load Get all the states from the server
    String endpoint = "$baseURL/api/pin";
    listState(endpoint).then((List<PostOffice> value) {
      setState(() {
        states = value;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Network Dropdown'),
      ),
      body: ListView(
        padding: EdgeInsets.all(20.0),
        children: [
          // State Dropdown
          DropdownButton<PostOffice>(
            hint: Text('State'),
            value: selectedState,
            isExpanded: true,
            items: states.map((PostOffice postOffice) {
              return DropdownMenuItem<PostOffice>(
                value: postOffice,
                child: Text(postOffice.state),
              );
            }).toList(),
            onChanged: onStateChange,
          ),
          // State Dropdown Ends here
          // Districts Dropdown
          DropdownButton<PostOffice>(
            hint: Text('District'),
            value: selectedDistrict,
            isExpanded: true,
            items: districts.map((PostOffice postOffice) {
              return DropdownMenuItem<PostOffice>(
                value: postOffice,
                child: Text(postOffice.districtsName),
              );
            }).toList(),
            onChanged: onDistrictChange,
          ),
          // Districts Dropdown Ends here
        ],
      ),
    );
  }

  void onStateChange(state) {
    setState(() {
      selectedState = state;
      districts = [];
      selectedDistrict = null;
    });
    String endpoint = "$baseURL/api/pin/${selectedState.state}";
    listState(endpoint).then((List<PostOffice> value) {
      setState(() {
        districts = value;
      });
    });
  }

  void onDistrictChange(district) {
    setState(() {
      selectedDistrict = district;
    });
  }

  Future<List<PostOffice>> listState(String endpoint) async {
    http.Response response = await http.get(endpoint);
    String body = response.body;
    Map<String, dynamic> jsonResponse = json.decode(body);
    List data = jsonResponse['data'];
    return data.map((e) => PostOffice.fromJson(e)).toList();
  }
}

Conclusion #

I hope you got an idea about the multi-level dropdown using data from the network. I tried to make it as simple, if you like it then let me know. Probably, I put the maximum effort into this article compared to others.

If you have any suggestions or questions you can ask in a comment. I would love to answer them

Last updated at Sunday, Jan 24, 2021 by Nitish Kumar Singh
comments powered by Disqus

Subscribe to our Newsletter


Tweet this article