1. 이제 실제로 내가 위치한/선택한 위치에 대해 detail 화면에 띄워보도록 하자.
- place.dart
import 'dart:io';
import 'package:uuid/uuid.dart';
const uuid = Uuid();
class PlaceLocation {
const PlaceLocation({
required this.latitude,
required this.longitude,
required this.address,
});
final double latitude;
final double longitude;
final String address;
}
class Place {
Place({
required this.title,
required this.image,
required this.location,
}) : id = uuid.v4();
final String id;
final String title;
final File image;
final PlaceLocation location;
}
1) Place 원상복구
- user_places.dart
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:favorite_places/models/place.dart';
class UserPlacesNotifier extends StateNotifier<List<Place>> {
UserPlacesNotifier() : super(const []);
void addPlace(String title, File image, PlaceLocation location) {
final newPlace = Place(title: title, image: image, location: location);
state = [newPlace, ...state];
}
}
final userPlacesProvider =
StateNotifierProvider<UserPlacesNotifier, List<Place>>(
(ref) => UserPlacesNotifier(),
);
1) 프로바이더 addPlace에서 location 추가
- add_place.dart
import 'dart:io';
import 'package:favorite_places/models/place.dart';
import 'package:favorite_places/widgets/location_input.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:favorite_places/widgets/image_input.dart';
import 'package:favorite_places/providers/user_places.dart';
class AddPlaceScreen extends ConsumerStatefulWidget {
const AddPlaceScreen({super.key});
@override
ConsumerState<AddPlaceScreen> createState() {
return _AddPlaceScreenState();
}
}
class _AddPlaceScreenState extends ConsumerState<AddPlaceScreen> {
final _titleController = TextEditingController();
File? _selectedImage;
PlaceLocation? _selectedLocation;
void _savePlace() {
final enteredTitle = _titleController.text;
if (enteredTitle.isEmpty ||
_selectedImage == null ||
_selectedLocation == null) {
return;
}
ref
.read(userPlacesProvider.notifier)
.addPlace(enteredTitle, _selectedImage!, _selectedLocation!);
Navigator.of(context).pop();
}
@override
void dispose() {
_titleController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Add new Place'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
children: [
TextField(
decoration: const InputDecoration(labelText: 'Title'),
controller: _titleController,
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground,
),
),
const SizedBox(height: 10),
ImageInput(
onPickImage: (image) {
_selectedImage = image;
},
),
const SizedBox(height: 10),
LocationInput(
onSelectLocation: (location) {
_selectedLocation = location;
},
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _savePlace,
icon: const Icon(Icons.add),
label: const Text('Add Place'),
),
],
),
),
);
}
}
1) 프로바이더 addPlace에 값을 전달해줄 _selectedLocation 생성. 그리고 해당 _selectedLocation값은 LocationInput 위젯에서 받아온다.
- location_input.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:location/location.dart';
import 'package:http/http.dart' as http;
import 'package:favorite_places/models/place.dart';
class LocationInput extends StatefulWidget {
const LocationInput({super.key, required this.onSelectLocation});
final void Function(PlaceLocation location) onSelectLocation;
@override
State<LocationInput> createState() {
return _LocationInputState();
}
}
class _LocationInputState extends State<LocationInput> {
PlaceLocation? _pickedLocation;
var _isGettingLocation = false;
String get locationImage {
if (_pickedLocation == null) {
return '';
}
final lat = _pickedLocation!.latitude;
final lng = _pickedLocation!.longitude;
return 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng=&zoom=16&size=600x300&maptype=roadmap&markers=color:red%7Clabel:A%7C$lat,$lng&key=AIzaSyDLcwxUggpPZo8lcbH0TB4Crq5SJjtj4ag';
}
void _getCurrentLocation() async {
Location location = Location();
bool serviceEnabled;
PermissionStatus permissionGranted;
LocationData locationData;
serviceEnabled = await location.serviceEnabled();
if (!serviceEnabled) {
serviceEnabled = await location.requestService();
if (!serviceEnabled) {
return;
}
}
permissionGranted = await location.hasPermission();
if (permissionGranted == PermissionStatus.denied) {
permissionGranted = await location.requestPermission();
if (permissionGranted != PermissionStatus.granted) {
return;
}
}
setState(() {
_isGettingLocation = true;
});
locationData = await location.getLocation();
final lat = locationData.latitude;
final lng = locationData.longitude;
if (lat == null || lng == null) {
return;
}
final url = Uri.parse(
'https://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lng&key=AIzaSyDLcwxUggpPZo8lcbH0TB4Crq5SJjtj4ag');
final response = await http.get(url);
final resData = json.decode(response.body);
final address = resData['results'][0]['formatted_address'];
setState(() {
_pickedLocation = PlaceLocation(
latitude: lat,
longitude: lng,
address: address,
);
_isGettingLocation = false;
});
widget.onSelectLocation(_pickedLocation!);
}
@override
Widget build(BuildContext context) {
Widget previewContent = Text(
'No location chosen',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
);
if (_pickedLocation != null) {
previewContent = Image.network(
locationImage,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
);
}
if (_isGettingLocation) {
previewContent = const CircularProgressIndicator();
}
return Column(
children: [
Container(
height: 170,
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
),
),
child: previewContent,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton.icon(
icon: const Icon(Icons.location_on),
label: const Text('Get Current Location'),
onPressed: _getCurrentLocation,
),
TextButton.icon(
icon: const Icon(Icons.map),
label: const Text('Select on Map'),
onPressed: () {},
),
],
),
],
);
}
}
1) _pickedLocation 값에 현재 추출된 값들 기반으로 만들어진 PlaceLocation 클래스 값이 전달되고, 해당 값은 onSelectLocation 함수에 의해 부모 위젯인 add_place.dart로 전달이 된다. (add_place의 프로바이더 상태 값으로 전달됨)
- place_detail.dart
import 'package:flutter/material.dart';
import 'package:favorite_places/models/place.dart';
class PlaceDetailScreen extends StatelessWidget {
const PlaceDetailScreen({super.key, required this.place});
final Place place;
String get locationImage {
final lat = place.location.latitude;
final lng = place.location.longitude;
return 'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng=&zoom=16&size=600x300&maptype=roadmap&markers=color:red%7Clabel:A%7C$lat,$lng&key=AIzaSyDLcwxUggpPZo8lcbH0TB4Crq5SJjtj4ag';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(place.title),
),
body: Stack(
children: [
Image.file(
place.image,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Column(
children: [
CircleAvatar(
radius: 70,
backgroundImage: NetworkImage(locationImage),
),
Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 16,
),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
Colors.black54,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Text(
place.location.address,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
),
),
],
),
)
],
),
);
}
}
1) 최종적으로, place_detail 화면에는 해당 클래스의 이미지와 주소를 볼 수 있음
2) 디자인적인 측면에서 stack을 통해 위젯끼리 곂치게 할 수 있어 아래와 같이 표현하게 할 수 있다. 그리고, 컨테이너 데코레이션과 텍스트의 합을 통해 글씨 백그라운드에 멋있게 그라디언트도 줄 수 있다.
- 결과 화면
'코딩강의 > favorite_places(플러터-유데미)' 카테고리의 다른 글
~257. Using the Map Screen in the "Add Place" Form (0) | 2023.11.21 |
---|---|
~255. Displaying the Picked Place on a Dynamic Map (0) | 2023.11.21 |
~250. Displaying a Location Preview Map Snapshot via Google (0) | 2023.11.20 |
~244. Previewing the Picked Image (0) | 2023.11.19 |
~242. Using the Device Camera For Taking Pictures (0) | 2023.11.19 |