0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

パッケージをお借りしてWEB用に上下矢印で選択可能な予測を作成する

Posted at

お借りしたパッケージ

実装イメージ

  • キーボードの上下で予測を選択できるように改修

タイトルなし.gif

import

パッケージ追加

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
    
  dio:
  rxdart:
  async:
  flutter_riverpod:

  # freezed
  freezed_annotation: 

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: 
  # freezed
  freezed:
  build_runner:
  json_serializable:

Client

クライアントクラスを作成

google_places_api.dart
import 'dart:convert';

import 'package:auto_complete_sample/models/place_details/place_details.dart';
import 'package:auto_complete_sample/models/prediction/prediction.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class GooglePlacesApi {
  GooglePlacesApi() : _dio = Dio();

  final Dio _dio;

  final _apiUrl = 'https://places.googleapis.com/v1/places';

  Future<PlacesAutocompleteResponse?> getSuggestionsForInput({
    required String input,
    required String googleAPIKey,
    List<String> countries = const [],
    String? sessionToken,
    String proxyUrl = '',
    String? languageCode,
  }) async {
    final prefix = proxyUrl;

    String url = "$prefix$_apiUrl:autocomplete";

    Map<String, dynamic> requestBody = {
      "input": input,
    };

    if (countries.isNotEmpty) {
      requestBody["includedRegionCodes"] = countries;
      requestBody["languageCode"] = languageCode;
    }
    if (sessionToken != null) {
      requestBody["sessionToken"] = sessionToken;
    }
    Options options = Options(
      headers: {
        "X-Goog-Api-Key": googleAPIKey,
        "X-Goog-FieldMask": "*",
      },
    );

    try {
      final response =
          await _dio.post(url, options: options, data: jsonEncode(requestBody));
      final subscriptionResponse =
          PlacesAutocompleteResponse.fromJson(response.data);
      return subscriptionResponse;
    } on DioException catch (e) {
      if (e.response != null) {
        debugPrint(
            'GooglePlacesApi.getSuggestionsForInput: DioException [${e.type}]: ${e.message}');
        debugPrint('Response data: ${e.response?.data}');
      } else {
        debugPrint(
            'GooglePlacesApi.getSuggestionsForInput: DioException [${e.type}]: ${e.message}');
      }
      return null;
    } catch (e) {
      debugPrint('GooglePlacesApi.getSuggestionsForInput: ${e.toString()}');
      return null;
    }
  }

  Future<Prediction?> fetchCoordinatesForPrediction({
    required Prediction prediction,
    required String googleAPIKey,
    String proxyUrl = '',
    String? sessionToken,
  }) async {
    try {
      final Uri uri =
          Uri.parse('$proxyUrl$_apiUrl/${prediction.placeId}').replace(
        queryParameters: {
          "fields": "*",
          "key": googleAPIKey,
          if (sessionToken != null) "sessionToken": sessionToken,
        },
      );

      final Response response = await _dio.get(uri.toString());

      if (response.data != null && response.statusCode == 200) {
        final placeDetails = PlaceDetails.fromJson(response.data);

        prediction.lat =
            placeDetails.result?.geometry?.location?.lat?.toString();
        prediction.lng =
            placeDetails.result?.geometry?.location?.lng?.toString();

        return prediction;
      } else {
        debugPrint('Unexpected response: ${response.statusCode}');
      }
    } on DioException catch (e) {
      debugPrint(
          'Error in fetchCoordinatesForPrediction: ${e.message}, Type: ${e.type}');
      if (e.response != null) {
        debugPrint('Response data: ${e.response?.data}');
      }
    } catch (e) {
      debugPrint('Unexpected error in fetchCoordinatesForPrediction: $e');
    }

    return null;
  }
}

model

  • freezedで作成に変更
place_details.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'place_details.freezed.dart';
part 'place_details.g.dart';

@freezed
class PlaceDetails with _$PlaceDetails {
  const factory PlaceDetails({
    Result? result,
    String? status,
  }) = _PlaceDetails;

  factory PlaceDetails.fromJson(Map<String, dynamic> json) =>
      _$PlaceDetailsFromJson(json);
}

@freezed
class Result with _$Result {
  const factory Result({
    @JsonKey(name: 'addressComponents')
    List<AddressComponents>? addressComponents,
    @JsonKey(name: 'adrFormatAddress') String? adrAddress,
    @JsonKey(name: 'formattedAddress') String? formattedAddress,
    @JsonKey(name: 'location') Geometry? geometry,
    @JsonKey(name: 'iconMaskBaseUri') String? icon,
    String? name,
    List<Photos>? photos,
    @JsonKey(name: 'placeId') String? placeId,
    String? reference,
    String? scope,
    List<String>? types,
    @JsonKey(name: 'googleMapsUri') String? url,
    @JsonKey(name: 'utcOffsetMinutes') int? utcOffset,
    String? vicinity,
    @JsonKey(name: 'websiteUri') String? website,
  }) = _Result;

  factory Result.fromJson(Map<String, dynamic> json) => _$ResultFromJson(json);
}

@freezed
class AddressComponents with _$AddressComponents {
  const factory AddressComponents({
    @JsonKey(name: 'long_name') String? longName,
    @JsonKey(name: 'short_name') String? shortName,
    List<String>? types,
  }) = _AddressComponents;

  factory AddressComponents.fromJson(Map<String, dynamic> json) =>
      _$AddressComponentsFromJson(json);
}

@freezed
class Photos with _$Photos {
  const factory Photos({
    @JsonKey(name: 'heightPx') int? height,
    @JsonKey(name: 'authorAttributions') List<String>? htmlAttributions,
    @JsonKey(name: 'name') String? photoReference,
    @JsonKey(name: 'widthPx') int? width,
  }) = _Photos;

  factory Photos.fromJson(Map<String, dynamic> json) => _$PhotosFromJson(json);
}

@freezed
class Geometry with _$Geometry {
  const factory Geometry({
    Location? location,
    Viewport? viewport,
  }) = _Geometry;

  factory Geometry.fromJson(Map<String, dynamic> json) =>
      _$GeometryFromJson(json);
}

@freezed
class Location with _$Location {
  const factory Location({
    @JsonKey(name: 'latitude') double? lat,
    @JsonKey(name: 'longitude') double? lng,
  }) = _Location;

  factory Location.fromJson(Map<String, dynamic> json) =>
      _$LocationFromJson(json);
}

@freezed
class Viewport with _$Viewport {
  const factory Viewport({
    Location? northeast,
    Location? southwest,
  }) = _Viewport;

  factory Viewport.fromJson(Map<String, dynamic> json) =>
      _$ViewportFromJson(json);
}
places_autocomplete_response.dart
class PlacesAutocompleteResponse {
  List<Prediction>? predictions;
  String? status;

  PlacesAutocompleteResponse({
    this.predictions,
    this.status,
  });

  PlacesAutocompleteResponse.fromJson(Map<String, dynamic> json) {
    if (json['suggestions'] != null && json['suggestions'].length > 0) {
      predictions = [];
      json['suggestions'].forEach((v) {
        if (v['placePrediction'] != null) {
          predictions!.add(Prediction.fromJson(v['placePrediction']));
        }
      });
    }
  }
}

class Prediction {
  String? description;
  String? id;
  List<MatchedSubstrings>? matchedSubstrings;
  String? placeId;
  String? reference;
  StructuredFormatting? structuredFormatting;
  List<Terms>? terms;
  List<String>? types;
  String? lat;
  String? lng;

  Prediction({
    this.description,
    this.id,
    this.matchedSubstrings,
    this.placeId,
    this.reference,
    this.structuredFormatting,
    this.terms,
    this.types,
    this.lat,
    this.lng,
  });

  Prediction.fromJson(Map<String, dynamic> json) {
    placeId = json['placeId'];
    description = json['text'] != null ? json['text']['text'] : null;
    structuredFormatting = json['structuredFormat'] != null
        ? StructuredFormatting.fromJson(json['structuredFormat'])
        : null;
    types = json['types'].cast<String>();
    lat = json['lat'];
    lng = json['lng'];
  }
}

class MatchedSubstrings {
  int? length;
  int? offset;

  MatchedSubstrings({this.length, this.offset});

  MatchedSubstrings.fromJson(Map<String, dynamic> json) {
    length = json['length'];
    offset = json['offset'];
  }
}

class StructuredFormatting {
  String? mainText;

  String? secondaryText;

  StructuredFormatting({this.mainText, this.secondaryText});

  StructuredFormatting.fromJson(Map<String, dynamic> json) {
    mainText = json['mainText'] != null ? json['mainText']['text'] : null;
    secondaryText =
        json['secondaryText'] != null ? json['secondaryText']['text'] : null;
  }
}

class Terms {
  int? offset;
  String? value;

  Terms({this.offset, this.value});

  Terms.fromJson(Map<String, dynamic> json) {
    offset = json['offset'];
    value = json['value'];
  }
}

カスタムWidget

widget.dart
import 'dart:async';
import 'dart:io';

import 'package:async/async.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:rxdart/rxdart.dart';

import 'data/google_places_api.dart';
import 'models/prediction/prediction.dart';

class GooglePlacesAutoCompleteTextFormField extends StatefulWidget {
  const GooglePlacesAutoCompleteTextFormField({
    required this.googleAPIKey,
    this.textEditingController,
    this.debounceTime = 600,
    this.onSuggestionClicked,
    this.fetchCoordinates = true,
    this.countries = const [],
    this.onPlaceDetailsWithCoordinatesReceived,
    this.predictionsStyle,
    this.overlayContainerBuilder,
    this.proxyURL,
    this.minInputLength = 0,
    this.sessionToken,
    this.initialValue,
    this.fetchSuggestionsForInitialValue = false,
    this.focusNode,
    this.decoration,
    this.keyboardType,
    this.textCapitalization = TextCapitalization.none,
    this.textInputAction,
    this.style,
    this.strutStyle,
    this.textDirection,
    this.textAlign = TextAlign.start,
    this.textAlignVertical,
    this.autofocus = false,
    this.readOnly = false,
    this.showCursor,
    this.obscuringCharacter = '•',
    this.obscureText = false,
    this.autocorrect = true,
    this.smartDashesType,
    this.smartQuotesType,
    this.enableSuggestions = true,
    this.maxLengthEnforcement,
    this.maxLines,
    this.minLines,
    this.expands = false,
    this.maxLength,
    this.onChanged,
    this.onTap,
    this.onTapOutside,
    this.onEditingComplete,
    this.onFieldSubmitted,
    this.inputFormatters,
    this.enabled,
    this.cursorWidth = 2.0,
    this.cursorHeight,
    this.cursorRadius,
    this.cursorColor,
    this.keyboardAppearance,
    this.scrollPadding = const EdgeInsets.all(20.0),
    this.enableInteractiveSelection,
    this.selectionControls,
    this.buildCounter,
    this.scrollPhysics,
    this.autofillHints,
    this.autovalidateMode,
    this.scrollController,
    this.enableIMEPersonalizedLearning = true,
    this.mouseCursor,
    this.contextMenuBuilder,
    this.validator,
    this.maxHeight = 200,
    this.languageCode,
    this.onError,
    super.key,
  });

  final String? initialValue;
  final bool fetchSuggestionsForInitialValue;
  final FocusNode? focusNode;
  final TextEditingController? textEditingController;
  final void Function(Prediction prediction)? onSuggestionClicked;
  final void Function(Prediction prediction)?
      onPlaceDetailsWithCoordinatesReceived;
  final bool fetchCoordinates;
  final String googleAPIKey;
  final int debounceTime;
  final List<String>? countries;
  final String? languageCode;
  final TextStyle? predictionsStyle;
  final Widget Function(Widget overlayChild)? overlayContainerBuilder;
  final String? proxyURL;
  final int minInputLength;
  final String? sessionToken;
  final double maxHeight;
  final Function? onError;
  final InputDecoration? decoration;
  final TextInputType? keyboardType;
  final TextCapitalization textCapitalization;
  final TextInputAction? textInputAction;
  final TextStyle? style;
  final StrutStyle? strutStyle;
  final TextDirection? textDirection;
  final TextAlign textAlign;
  final TextAlignVertical? textAlignVertical;
  final bool autofocus;
  final bool readOnly;
  final bool? showCursor;
  final String obscuringCharacter;
  final bool obscureText;
  final bool autocorrect;
  final SmartDashesType? smartDashesType;
  final SmartQuotesType? smartQuotesType;
  final bool enableSuggestions;
  final MaxLengthEnforcement? maxLengthEnforcement;
  final int? maxLines;
  final int? minLines;
  final bool expands;
  final int? maxLength;
  final ValueChanged<String>? onChanged;
  final GestureTapCallback? onTap;
  final TapRegionCallback? onTapOutside;
  final VoidCallback? onEditingComplete;
  final ValueChanged<String>? onFieldSubmitted;
  final List<TextInputFormatter>? inputFormatters;
  final bool? enabled;
  final double cursorWidth;
  final double? cursorHeight;
  final Radius? cursorRadius;
  final Color? cursorColor;
  final Brightness? keyboardAppearance;
  final EdgeInsets scrollPadding;
  final bool? enableInteractiveSelection;
  final TextSelectionControls? selectionControls;
  final InputCounterWidgetBuilder? buildCounter;
  final ScrollPhysics? scrollPhysics;
  final Iterable<String>? autofillHints;
  final AutovalidateMode? autovalidateMode;
  final ScrollController? scrollController;
  final bool enableIMEPersonalizedLearning;
  final MouseCursor? mouseCursor;
  final EditableTextContextMenuBuilder? contextMenuBuilder;
  final String? Function(String?)? validator;

  @override
  State<GooglePlacesAutoCompleteTextFormField> createState() =>
      _GooglePlacesAutoCompleteTextFormFieldState();
}

class _GooglePlacesAutoCompleteTextFormFieldState
    extends State<GooglePlacesAutoCompleteTextFormField> {
  final subject = PublishSubject<String>();
  late GooglePlacesApi _api;
  OverlayEntry? _overlayEntry;
  List<Prediction> allPredictions = [];

  final LayerLink _layerLink = LayerLink();

  late FocusNode _focus;
  late StreamSubscription<String> subscription;
  CancelableOperation? cancelableOperation;

  int _selectedIndex = -1;

  @override
  void dispose() {
    subscription.cancel();
    subject.close();
    _focus.dispose();
    if (cancelableOperation != null) {
      cancelableOperation?.cancel();
    }

    super.dispose();
  }

  @override
  void initState() {
    _api = GooglePlacesApi();
    subscription = subject.stream
        .distinct()
        .debounceTime(Duration(milliseconds: widget.debounceTime))
        .listen(textChanged);

    _focus = widget.focusNode ?? FocusNode();
    _focus.addListener(() {
      if (!_focus.hasFocus && allPredictions.isNotEmpty) {
        // インデックスが範囲外にならないようにチェック
        if (_selectedIndex >= 0 && _selectedIndex < allPredictions.length) {
          final selectedPrediction = allPredictions[_selectedIndex];
          widget.textEditingController?.text = selectedPrediction.description!;
          widget.onChanged?.call(selectedPrediction.description!);
        }
        removeOverlay();
        _selectedIndex = -1;
      } else if (!_focus.hasFocus) {
        removeOverlay();
        _selectedIndex = -1;
      }
    });
    if (!kIsWeb && !Platform.isMacOS) {
      _focus.addListener(() {
        if (!_focus.hasFocus) {
          removeOverlay();
        }
      });
    }

    if (widget.initialValue != null && widget.fetchSuggestionsForInitialValue) {
      subject.add(widget.initialValue!);
    }

    if (widget.initialValue != null && widget.textEditingController != null) {
      widget.textEditingController!.text = widget.initialValue!;
    }

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CompositedTransformTarget(
      link: _layerLink,
      child: KeyboardListener(
        focusNode: FocusNode(),
        onKeyEvent: (KeyEvent event) {
          if (event is KeyDownEvent) {
            if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
              // 予測リストが空でない場合のみインデックスを更新
              if (allPredictions.isNotEmpty) {
                setState(() {
                  _selectedIndex = (_selectedIndex + 1) % allPredictions.length;
                });
                _overlayEntry?.markNeedsBuild();
              }
            } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
              if (allPredictions.isNotEmpty) {
                setState(() {
                  _selectedIndex =
                      (_selectedIndex - 1 + allPredictions.length) %
                          allPredictions.length;
                });
                _overlayEntry?.markNeedsBuild();
              }
            } else if (event.logicalKey == LogicalKeyboardKey.enter) {
              if (_selectedIndex != -1 &&
                  _selectedIndex < allPredictions.length &&
                  _overlayEntry != null) {
                final selectedPrediction = allPredictions[_selectedIndex];
                widget.onSuggestionClicked?.call(selectedPrediction);
                if (widget.fetchCoordinates) {
                  getPlaceDetailsFromPlaceId(selectedPrediction);
                }
                removeOverlay();
              } else if (_overlayEntry == null || allPredictions.isEmpty) {
                widget.onFieldSubmitted
                    ?.call(widget.textEditingController?.text ?? '');
              }
            }
          }
        },
        child: TextFormField(
          onFieldSubmitted: (value) {
            if (_overlayEntry == null || allPredictions.isEmpty) {
              widget.onFieldSubmitted?.call(value);
            }
          },
          controller: widget.textEditingController,
          initialValue:
              widget.textEditingController != null ? null : widget.initialValue,
          focusNode: _focus,
          decoration: widget.decoration,
          keyboardType: widget.keyboardType,
          textCapitalization: widget.textCapitalization,
          textInputAction: widget.textInputAction,
          style: widget.style,
          strutStyle: widget.strutStyle,
          textDirection: widget.textDirection,
          textAlign: widget.textAlign,
          textAlignVertical: widget.textAlignVertical,
          autofocus: widget.autofocus,
          readOnly: widget.readOnly,
          showCursor: widget.showCursor,
          obscuringCharacter: widget.obscuringCharacter,
          obscureText: widget.obscureText,
          autocorrect: widget.autocorrect,
          smartDashesType: widget.smartDashesType,
          smartQuotesType: widget.smartQuotesType,
          enableSuggestions: widget.enableSuggestions,
          maxLengthEnforcement: widget.maxLengthEnforcement,
          maxLines: widget.maxLines,
          minLines: widget.minLines,
          expands: widget.expands,
          maxLength: widget.maxLength,
          onChanged: (string) {
            widget.onChanged?.call(string);
            subject.add(string);
          },
          onTap: widget.onTap,
          onTapOutside: widget.onTapOutside,
          onEditingComplete: widget.onEditingComplete,
          inputFormatters: widget.inputFormatters,
          enabled: widget.enabled,
          cursorWidth: widget.cursorWidth,
          cursorHeight: widget.cursorHeight,
          cursorRadius: widget.cursorRadius,
          cursorColor: widget.cursorColor,
          keyboardAppearance: widget.keyboardAppearance,
          scrollPadding: widget.scrollPadding,
          enableInteractiveSelection: widget.enableInteractiveSelection,
          selectionControls: widget.selectionControls,
          buildCounter: widget.buildCounter,
          scrollPhysics: widget.scrollPhysics,
          autofillHints: widget.autofillHints,
          autovalidateMode: widget.autovalidateMode,
          scrollController: widget.scrollController,
          enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
          mouseCursor: widget.mouseCursor,
          contextMenuBuilder: widget.contextMenuBuilder,
          validator: widget.validator,
        ),
      ),
    );
  }

  Future<void> getLocation(String text) async {
    if (text.isEmpty || text.length < widget.minInputLength) {
      allPredictions.clear();
      _overlayEntry?.remove();
      _overlayEntry?.dispose();
      return;
    }

    try {
      final result = await _api.getSuggestionsForInput(
        input: text,
        googleAPIKey: widget.googleAPIKey,
        countries: widget.countries ?? [],
        sessionToken: widget.sessionToken,
        proxyUrl: widget.proxyURL ?? "",
        languageCode: widget.languageCode,
      );
      if (result == null) return;
      final predictions = result.predictions;
      if (predictions == null || predictions.isEmpty) return;

      allPredictions.clear();
      allPredictions.addAll(predictions);
    } catch (e) {
      debugPrint('getLocation: ${e.toString()}');
      if (e is DioException) {
        widget.onError?.call(e.response);
      } else {
        widget.onError?.call(e);
      }
    }
  }

  Future<void> getPlaceDetailsFromPlaceId(Prediction prediction) async {
    final predictionWithCoordinates = await _api.fetchCoordinatesForPrediction(
      prediction: prediction,
      googleAPIKey: widget.googleAPIKey,
      proxyUrl: widget.proxyURL ?? "",
      sessionToken: widget.sessionToken,
    );
    if (predictionWithCoordinates == null) return;
    widget.onPlaceDetailsWithCoordinatesReceived
        ?.call(predictionWithCoordinates);
  }

  Future<dynamic> fromCancelable(Future<dynamic> future) async {
    cancelableOperation =
        CancelableOperation.fromFuture(future, onCancel: () {});
    return cancelableOperation?.value;
  }

  Future<void> textChanged(String text) async {
    final overlay = Overlay.of(context);
    fromCancelable(getLocation(text)).then(
      (_) {
        try {
          _overlayEntry?.remove();
        } catch (_) {}

        _overlayEntry = null;
        _overlayEntry = _createOverlayEntry();
        overlay.insert(_overlayEntry!);
      },
    );
  }

  OverlayEntry? _createOverlayEntry() {
    if (context.findRenderObject() != null) {
      final renderBox = context.findRenderObject() as RenderBox;
      var size = renderBox.size;
      var offset = renderBox.localToGlobal(Offset.zero);

      return OverlayEntry(
        builder: (context) => Positioned(
          left: offset.dx,
          top: size.height + offset.dy,
          width: size.width,
          child: CompositedTransformFollower(
            showWhenUnlinked: false,
            link: _layerLink,
            offset: Offset(0.0, size.height + 5.0),
            child: widget.overlayContainerBuilder?.call(_overlayChild) ??
                Material(
                  elevation: 1.0,
                  child: Container(
                    constraints: BoxConstraints(
                      maxHeight: widget.maxHeight,
                    ),
                    child: _overlayChild,
                  ),
                ),
          ),
        ),
      );
    }
    return null;
  }

  Widget get _overlayChild {
    return ListView.builder(
      padding: EdgeInsets.zero,
      shrinkWrap: true,
      itemCount: allPredictions.length,
      itemBuilder: (BuildContext context, int index) => InkWell(
        onTap: () {
          if (index < allPredictions.length) {
            widget.onSuggestionClicked?.call(allPredictions[index]);

            if (widget.fetchCoordinates) {
              getPlaceDetailsFromPlaceId(allPredictions[index]);
            }
            removeOverlay();
          }
        },
        child: Builder(
          builder: (BuildContext context) {
            final bool highlight = index == _selectedIndex;

            if (highlight) {
              SchedulerBinding.instance.addPostFrameCallback((_) {
                Scrollable.ensureVisible(context, alignment: 0.5);
              });
            }

            return Container(
              color: highlight ? Theme.of(context).focusColor : null,
              padding: const EdgeInsets.all(10),
              child: Text(
                allPredictions[index].description!,
                style: widget.predictionsStyle ?? widget.style,
              ),
            );
          },
        ),
      ),
    );
  }

  void removeOverlay() {
    allPredictions.clear();
    try {
      _overlayEntry?.remove();
    } catch (_) {}

    _overlayEntry?.dispose();
    _overlayEntry = _createOverlayEntry();
    Overlay.of(context).insert(_overlayEntry!);
    _overlayEntry!.markNeedsBuild();
  }
}

View

import 'package:auto_complete_sample/google_places_auto_complete_text_form_field.dart';
import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: GooglePlacesAutoCompleteTextFormField(
                textInputAction: TextInputAction.done,
                textEditingController: _controller,
                googleAPIKey: 'YOUR-API-KEY',
                debounceTime: 400,
                countries: const ["jp"],
                fetchCoordinates: true, 
                onSuggestionClicked: (prediction) {
                  _controller.text = prediction.description!;
                  _controller.selection = TextSelection.fromPosition(
                      TextPosition(offset: prediction.description!.length));
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

感想

privateな変数がなければ、継承してやろうと思ったが出来なさそうなのでコピペベースで作成になってしまった。本当はForkしてPR出したい

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?