# Public Transit Stop Schedule

Last updated:

<!-- -->

October 18, 2026

<!-- -->

\| 6 minutes read

<br />

<br />

This example showcases how to build a Flutter app featuring an interactive map. Users can select public transport points of interest (POIs) to access detailed information, including departure times, route names, and the status of transport vehicles, such as whether they have departed.

## How it works[​](#how-it-works "Direct link to How it works")

The example app includes the following features:

* Display a map.
* Select public transport stations from the map.
* Display public transport trip information.
* Show departure times for each public transport trip.

![Public Transport POI](/docs/flutter/assets/images/example_flutter_public_transit_stop_schedule1-14a97cc52afdf79f54f4fa9f92d4cfcb.png)

**Public Transport POI**

![Public Transport Routes](/docs/flutter/assets/images/example_flutter_public_transit_stop_schedule2-b330f96ed6f9a184ee81de932627952c.png)

**Public Transport Routes**

![Departure Times](/docs/flutter/assets/images/example_flutter_public_transit_stop_schedule3-c23a744851049a797e7260eb5aa92002.png)

**Departure Times**

### UI and Map Integration[​](#ui-and-map-integration "Direct link to UI and Map Integration")

The following code creates a user interface with an interactive `GemMap` and an app bar.

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/public_transit_stop_schedule/lib/main.dart?ref_type=heads#L12)

```dart
const projectApiToken = String.fromEnvironment('YOUR_API_TOKEN_HERE');

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Public Transit Stops',
      home: MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late GemMapController _mapController;
  PTStopInfo? _selectedPTStop;
  Coordinates? _selectedPTStopCoords;
  @override
  void dispose() {
    GemKit.release();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.deepPurple[900],
        title: const Text(
          'Public Transit Stops',
          style: TextStyle(color: Colors.white),
        ),
      ),
      body: Stack(
        children: [
          GemMap(
            key: ValueKey("GemMap"),
            appAuthorization: projectApiToken,
            onMapCreated: (controller) => _onMapCreated(controller),
          ),
          if (_selectedPTStop != null)
            Positioned(
              bottom: 0,
              left: 0,
              right: 0,
              child: SizedBox(
                height: MediaQuery.of(context).size.height * 0.6,
                child: FutureBuilder(
                  future: getLocalTime(_selectedPTStopCoords!),
                  builder: (context, snapshot) {
                    if (snapshot.connectionState == ConnectionState.waiting) {
                      return Container();
                    }
                    return PublicTransitStopPanel(
                      ptStopInfo: _selectedPTStop!,
                      localTime: snapshot.data!,
                      onCloseTap: () => setState(() {
                        _selectedPTStop = null;
                        _selectedPTStopCoords = null;
                      }),
                    );
                  },
                ),
              ),
            ),
        ],
      ),
    );
  }

  void _onMapCreated(GemMapController controller) async {
    // Save controller for further usage.
    _mapController = controller;

    _mapController.registerOnLongPress((pos) async {
      // Update the cursor screen position
      await _mapController.setCursorScreenPosition(pos);

      // Get the public transit overlay items at that position
      final items = _mapController.cursorSelectionOverlayItemsByType(
        CommonOverlayId.publicTransport,
      );
      final coords = _mapController.transformScreenToWgs(pos);

      for (final OverlayItem item in items) {
        // Get the stop information
        final ptStopInfo = await item.getPTStopInfo();
        if (ptStopInfo != null) {
          setState(() {
            _selectedPTStop = ptStopInfo;
            _selectedPTStopCoords = coords;
          });
        }
      }
    });

    _mapController.centerOnCoordinates(
      Coordinates(latitude: 41.40377426147461, longitude: 2.173604965209961),
      zoomLevel: 95,
    );
  }
}

```

### Public Transport Stop Panel[​](#public-transport-stop-panel "Direct link to Public Transport Stop Panel")

public\_transit\_stop\_panel.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/public_transit_stop_schedule/lib/public_transit_stop_panel.dart?ref_type=heads#L13)

```dart
class PublicTransitStopPanel extends StatefulWidget {
  final PTStopInfo ptStopInfo;
  final DateTime localTime;
  final VoidCallback onCloseTap;

  const PublicTransitStopPanel({
    super.key,
    required this.ptStopInfo,
    required this.localTime,
    required this.onCloseTap,
  });

  @override
  State<PublicTransitStopPanel> createState() => _PublicTransitStopPanelState();
}

class _PublicTransitStopPanelState extends State<PublicTransitStopPanel> {
  PTTrip? _selectedTrip;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          width: double.infinity,
          color: Colors.white,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              if (_selectedTrip != null)
                IconButton(
                  icon: const Icon(Icons.arrow_back),
                  onPressed: () => setState(() => _selectedTrip = null),
                )
              else
                const SizedBox(width: 48),
              Text(
                _selectedTrip == null
                    ? 'Select a Trip'
                    : 'Stops for ${_selectedTrip!.route.routeShortName}',
              ),
              IconButton(
                icon: const Icon(Icons.close),
                onPressed: widget.onCloseTap,
              ),
            ],
          ),
        ),

        // Body: either list of trips or list of stops
        Expanded(
          child: Container(
            color: Colors.white,
            child: _selectedTrip == null
                ? ListView.separated(
                    itemCount: widget.ptStopInfo.trips.length,
                    itemBuilder: (context, index) {
                      final trip = widget.ptStopInfo.trips[index];
                      return Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: PTLineListItem(
                          localCurrentTime: widget.localTime,
                          ptTrip: trip,
                          onTap: () {
                            setState(() {
                              _selectedTrip = trip;
                            });
                          },
                        ),
                      );
                    },
                    separatorBuilder: (_, __) => const Divider(
                      height: 1,
                      thickness: 1,
                      indent: 16,
                      endIndent: 16,
                    ),
                  )
                : DepartureTimesPanel(
                    stopTimes: _selectedTrip!.stopTimes,
                    localTime: widget.localTime,
                    onCloseTap: widget.onCloseTap,
                  ),
          ),
        ),
      ],
    );
  }
}

class PTLineListItem extends StatelessWidget {
  final PTTrip ptTrip;
  final DateTime localCurrentTime;
  final VoidCallback onTap;

  const PTLineListItem({
    super.key,
    required this.ptTrip,
    required this.localCurrentTime,
    required this.onTap,
  });
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onTap,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Row(
            children: [
              Icon(getTransportIcon(ptTrip.route.routeType)),
              SizedBox(width: 7.0),
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Container(
                    padding: EdgeInsets.symmetric(
                      vertical: 4.0,
                      horizontal: 15.0,
                    ),
                    decoration: BoxDecoration(
                      color: ptTrip.route.routeColor,
                      borderRadius: BorderRadius.circular(
                        14.0,
                      ), // adjust radius as you like
                    ),
                    child: Text(
                      ptTrip.route.routeShortName ?? "None",
                      style: TextStyle(
                        color: ptTrip.route.routeTextColor,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                  Text(
                    ptTrip.route.heading ?? ptTrip.route.routeLongName ?? "Nan",
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  Text(
                    ptTrip.departureTime != null
                        ? 'Scheduled • ${DateFormat('H:mm').format(ptTrip.departureTime!)}'
                        : 'Scheduled • ',
                  ),
                ],
              ),
            ],
          ),
          Text(calculateTimeDifference(localCurrentTime, ptTrip)),
        ],
      ),
    );
  }
}

```

### Departure Times Panel[​](#departure-times-panel "Direct link to Departure Times Panel")

departure\_times\_panel.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/public_transit_stop_schedule/lib/departure_times_panel.dart?ref_type=heads#L11)

```dart
class DepartureTimesPanel extends StatelessWidget {
  final List<PTStopTime> stopTimes;
  final DateTime localTime;
  final VoidCallback onCloseTap;
  const DepartureTimesPanel({
    super.key,
    required this.stopTimes,
    required this.localTime,
    required this.onCloseTap,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // the scrollable list with dividers
        Expanded(
          child: Container(
            color: Colors.white,
            child: ListView.separated(
              itemCount: stopTimes.length,
              itemBuilder: (context, index) => Padding(
                padding: const EdgeInsets.all(8.0),
                child: DepartureTimesListItem(
                  stop: stopTimes[index],
                  localCurrentTime: localTime,
                ),
              ),
              separatorBuilder: (context, index) => const Divider(
                height: 1,
                thickness: 1,
                indent: 16, // optional: inset the divider from the left
                endIndent: 16, // optional: inset the divider from the right
              ),
            ),
          ),
        ),
      ],
    );
  }
}

class DepartureTimesListItem extends StatelessWidget {
  final PTStopTime stop;
  final DateTime localCurrentTime;
  const DepartureTimesListItem({
    super.key,
    required this.stop,
    required this.localCurrentTime,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(stop.stopName, maxLines: 1, overflow: TextOverflow.ellipsis),
              if (stop.departureTime != null)
                Text(
                  stop.departureTime!.isAfter(localCurrentTime)
                      ? "Scheduled"
                      : "Departed",
                ),
            ],
          ),
        ),
        SizedBox(width: 8),
        Text(
          stop.departureTime != null
              ? DateFormat('H:mm').format(stop.departureTime!)
              : '-',
        ),
      ],
    );
  }
}

```

### Utility Functions[​](#utility-functions "Direct link to Utility Functions")

utils.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/public_transit_stop_schedule/lib/utils.dart?ref_type=heads#L12)

```dart
// Retrieves the local time for the region corresponding to the given coordinates.
Future<DateTime> getLocalTime(Coordinates referenceCoords) async {
  final completer = Completer<TimezoneResult>();

  TimezoneService.getTimezoneInfoFromCoordinates(
    coords: referenceCoords,
    time: DateTime.now(),
    onComplete: (error, result) {
      if (error == GemError.success) completer.complete(result);
    },
  );

  final timezoneResult = await completer.future;

  return timezoneResult.localTime;
}

IconData getTransportIcon(PTRouteType type) {
  switch (type) {
    case PTRouteType.bus:
      return Icons.directions_bus;
    case PTRouteType.underground:
      return Icons.directions_subway;
    case PTRouteType.railway:
      return Icons.directions_railway;
    case PTRouteType.tram:
      return Icons.directions_bus_filled;
    case PTRouteType.waterTransport:
      return Icons.directions_boat;
    case PTRouteType.misc:
      return Icons.miscellaneous_services;
  }
}

// Computes how many minutes remain until the PTTrip’s scheduled departure.
String calculateTimeDifference(DateTime localCurrentTime, PTTrip ptTrip) {
  return ptTrip.departureTime != null
      ? '${ptTrip.departureTime!.difference(localCurrentTime).inMinutes} min'

```
