๐Ÿ“ mn_loc โ€” Dependent Dropdowns from JSON

pub package License: MIT Flutter Platform

A modern, highly customizable Flutter package for 3-level dependent dropdown selections (District โž” Subdivision โž” Circle) loaded directly from JSON files (district.json, subdivision.json, circle.json).


๐ŸŽฏ Key Design Philosophy: Individual Standalone Widgets

Important

No Monolithic Single-Widget Wrappers! Unlike traditional packages that force all dropdowns into one fixed container, mn_loc provides unwrapped, standalone individual widgets:

  • <DistrictDropdown />
  • <SubdivisionDropdown />
  • <CircleDropdown />

You can place each widget anywhere in your UI tree โ€” across different cards, grid columns, form tabs, or stepper screens. They reactively sync state through LocationDropdownProvider.


โœจ Features

  • ๐Ÿงฉ 100% Modular & Unwrapped: Place District, Subdivision, and Circle dropdowns anywhere in your layout.
  • โšก Reactive Auto-Dependencies: Selecting a District automatically updates Subdivisions and resets Circle selections.
  • ๐Ÿ“ JSON Asset & Raw String Support: Built-in JSON loader (LocationJsonService) for asset paths, raw JSON strings, or direct model lists.
  • ๐ŸŽจ Material 3 Design: Supports theme customization, custom input decorations, custom item builders, icons, and disabled states.
  • ๐Ÿงน Clean State Management: Built with provider (ChangeNotifier).

๐Ÿš€ Getting Started

Add mn_loc to your pubspec.yaml:

dependencies:
  mn_loc: ^1.0.3
  provider: ^6.1.2
  go_router: ^16.2.5

Ensure your pubspec.yaml includes the location JSON assets:

flutter:
  assets:
    - assets/data/district.json
    - assets/data/subdivision.json
    - assets/data/circle.json

๐Ÿ’ป Usage Guide

1. Register LocationDropdownProvider

Wrap your root widget or screen with MultiProvider or ChangeNotifierProvider:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:mn_loc/mn_loc.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) => LocationDropdownProvider()..initialize(),
        ),
      ],
      child: const MyApp(),
    ),
  );
}

2. Place Dropdown Widgets Independently

Each widget connects automatically to LocationDropdownProvider:

import 'package:flutter/material.dart';
import 'package:mn_loc/mn_loc.dart';

class LocationFormPage extends StatelessWidget {
  const LocationFormPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Location Form')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // 1. District Dropdown Widget
            const DistrictDropdown(
              label: 'District',
              hint: 'Choose a district...',
            ),
            const SizedBox(height: 16),

            // 2. Subdivision Dropdown Widget (Auto-disabled until District is chosen)
            const SubdivisionDropdown(
              label: 'Subdivision',
              hint: 'Choose a subdivision...',
              disabledHint: 'Select District first',
            ),
            const SizedBox(height: 16),

            // 3. Circle Dropdown Widget (Auto-disabled until Subdivision is chosen)
            const CircleDropdown(
              label: 'Circle',
              hint: 'Choose a circle...',
              disabledHint: 'Select Subdivision first',
            ),
            const SizedBox(height: 24),

            // Display current selection summary card
            const LocationSummaryCard(),
          ],
        ),
      ),
    );
  }
}

๐Ÿ“‹ JSON Schema Specification

district.json

[
  { "id": "d_01", "name": "Imphal West", "code": "IW" },
  { "id": "d_02", "name": "Imphal East", "code": "IE" }
]

subdivision.json

[
  { "id": "s_01", "district_id": "d_01", "name": "Lamphelpat" },
  { "id": "s_02", "district_id": "d_01", "name": "Patsoi" }
]

circle.json

[
  { "id": "c_01", "subdivision_id": "s_01", "name": "Lamphel Circle I" },
  { "id": "c_02", "subdivision_id": "s_01", "name": "Lamphel Circle II" }
]

๐Ÿ” Accessing Selected Data

Access selections reactively anywhere in your code using Provider:

final provider = context.watch<LocationDropdownProvider>();

final DistrictModel? district = provider.selectedDistrict;
final SubdivisionModel? subdivision = provider.selectedSubdivision;
final CircleModel? circle = provider.selectedCircle;

print('District ID: ${district?.id}, Name: ${district?.name}');
print('Subdivision ID: ${subdivision?.id}, Name: ${subdivision?.name}');
print('Circle ID: ${circle?.id}, Name: ${circle?.name}');

To pre-select initial location values (e.g. for pre-filled or edit forms):

// Pre-select District, Subdivision, and Circle by IDs
context.read<LocationDropdownProvider>().setInitialSelection(
  districtId: '1',
  subdivisionId: '1',
  circleId: '1',
);

Or pass initial IDs during provider initialization:

LocationDropdownProvider()..initialize(
  initialDistrictId: '1',
  initialSubdivisionId: '1',
  initialCircleId: '1',
);

To reset selections programmatically:

context.read<LocationDropdownProvider>().resetSelections();

๐Ÿ› ๏ธ API Reference

DistrictDropdown

Parameter Type Default Description
label String? 'District' Input decoration field label.
hint String 'Select District' Placeholder text.
decoration InputDecoration? null Custom input decoration.
onChanged ValueChanged<DistrictModel?>? null Optional selection change listener callback.
itemBuilder Widget Function(...) null Custom dropdown item builder widget.

SubdivisionDropdown

Parameter Type Default Description
label String? 'Subdivision' Input decoration field label.
hint String 'Select Subdivision' Placeholder text when enabled.
disabledHint String 'Select District First' Placeholder text when parent district is not selected.
onChanged ValueChanged<SubdivisionModel?>? null Selection change listener.

CircleDropdown

Parameter Type Default Description
label String? 'Circle' Input decoration field label.
hint String 'Select Circle' Placeholder text when enabled.
disabledHint String 'Select Subdivision First' Placeholder text when parent subdivision is not selected.
onChanged ValueChanged<CircleModel?>? null Selection change listener.

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.