如何用Dart调用GraphQL API获取挪威特隆海姆公交站点数据
Hey there! Let's break down how to solve your problem—first addressing the query filter issue, then walking through the Dart/GraphQL implementation step by step.
First: Fixing the Trondheim Filter Issue
Your original query uses countyReference: "Trondheim" which returns no data, and that's likely because the API expects an official NSR administrative zone identifier instead of the county name. Here's how to find the right value:
- Open the API IDE you linked earlier
- Run this broad query to grab sample stop data and their administrative zone references:
{ stopPlaces(stopPlaceType: onstreetBus, first: 20) { name { value } administrativeZoneRef } }
- Look for entries that correspond to Trondheim stops, copy their
administrativeZoneRefvalue, and use that in your query instead of the county name. If this still doesn't work, reaching out to the Entur team is a good next step—their docs are indeed sparse on this detail.
Step 1: Set Up the GraphQL Client with Required Headers
First, you need to configure the graphql package to include the mandatory ET-Client-Name header (critical to avoid being blocked/rate-limited). Here's how to set that up:
import 'package:graphql/graphql.dart'; // Create the base HTTP link to the API final HttpLink httpLink = HttpLink( 'https://api.entur.io/stop-places/v1/graphql', ); // Create a custom link to inject the ET-Client-Name header final Link headerLink = Link.function((request, [forward]) { final updatedRequest = request.updateContextEntry<HttpLinkHeaders>( (headers) => HttpLinkHeaders( headers: { ...headers?.headers ?? {}, 'ET-Client-Name': 'your-university - your-flutter-app-name', // Replace with your own identifier (e.g., "ntnu - trondheim-bus-tracker") }, ), ); return forward!(updatedRequest); }); // Combine the header link with the HTTP link final Link combinedLink = headerLink.concat(httpLink); // Initialize the GraphQL client final GraphQLClient graphQLClient = GraphQLClient( cache: GraphQLCache(), link: combinedLink, );
Step 2: Define Your GraphQL Query
Use the plural stopPlaces (instead of singular stopPlace) to fetch all matching stops, and use a variable for the zone reference to make it flexible:
query GetTrondheimBusStops($zoneRef: String!) { stopPlaces(stopPlaceType: onstreetBus, administrativeZoneRef: $zoneRef) { id name { value } quays { geometry { coordinates } } } }
Step 3: Execute the Query and Parse Results into BusStop Objects
Now, write a function to run the query, handle the response, and map the data to your BusStop model. Note that GraphQL returns coordinates as [longitude, latitude], so we need to reverse them for LatLng:
import 'package:google_maps_flutter/google_maps_flutter.dart'; // Your BusStop model (I removed ChangeNotifier since it's not needed for data parsing—add it back if you need it for state management) class BusStop { final String id; final String name; final LatLng location; BusStop({ required this.id, required this.name, required this.location, }); } Future<List<BusStop>> fetchTrondheimBusStops(String zoneRef) async { const String query = ''' query GetTrondheimBusStops(\$zoneRef: String!) { stopPlaces(stopPlaceType: onstreetBus, administrativeZoneRef: \$zoneRef) { id name { value } quays { geometry { coordinates } } } } '''; final QueryOptions options = QueryOptions( document: gql(query), variables: {'zoneRef': zoneRef}, ); final QueryResult result = await graphQLClient.query(options); if (result.hasException) { throw Exception('Failed to fetch stops: ${result.exception.toString()}'); } final List<dynamic> stopData = result.data?['stopPlaces'] ?? []; final List<BusStop> busStops = []; for (final stop in stopData) { final String id = stop['id']; final String name = stop['name']['value']; // Grab the first quay's coordinates (most stops have at least one) final List<dynamic>? coordinates = stop['quays']?[0]?['geometry']?['coordinates']; if (coordinates != null && coordinates.length == 2) { // Reverse coordinates: GraphQL returns [lon, lat], LatLng expects [lat, lon] final LatLng location = LatLng(coordinates[1], coordinates[0]); busStops.add(BusStop(id: id, name: name, location: location)); } } return busStops; }
Quick Notes
- If you do need
BusStopto be aChangeNotifier, add backwith ChangeNotifierand callnotifyListeners()whenever properties change (though for initial data parsing, this isn't necessary). - Double-check your
ET-Client-Nameformat—follow the "organization - app name" pattern to avoid rate limits or blocks.
内容的提问来源于stack exchange,提问作者Nikolai Henriksen

