overhaul using a trip struct that notifies its ui dependencies
All checks were successful
Build and push docker image / Build (pull_request) Successful in 1m54s
Build and release APK / Build APK (pull_request) Successful in 5m32s

This commit is contained in:
Remy Moll 2024-08-03 16:52:29 +02:00
parent 5748630b99
commit c87a01b2e8
15 changed files with 324 additions and 164 deletions

View File

@ -37,7 +37,7 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl
logger.info("No end coordinates provided. Using start=end.")
start_landmark = Landmark(name='start', type='start', location=(start[0], start[1]), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
end_landmark = Landmark(name='end', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
end_landmark = Landmark(name='finish', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
# Generate the landmarks from the start location
landmarks, landmarks_short = manager.generate_landmarks_list(

View File

@ -1,3 +1,6 @@
import 'dart:collection';
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
import 'package:anyway/constants.dart';
@ -15,12 +18,12 @@ import 'package:anyway/pages/profile.dart';
// A side drawer is used to switch between pages
class BasePage extends StatefulWidget {
final String mainScreen;
final Future<Trip>? trip;
final Trip? trip;
const BasePage({
super.key,
required this.mainScreen,
this.trip
this.trip,
});
@override
@ -53,13 +56,13 @@ class _BasePageState extends State<BasePage> {
children: [
DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.cyan, theme.primaryColor])
gradient: LinearGradient(colors: [Colors.red, Colors.yellow])
),
child: Center(
child: Text(
APP_NAME,
style: TextStyle(
color: Colors.white,
color: Colors.grey[800],
fontSize: 24,
fontWeight: FontWeight.bold,
),
@ -129,9 +132,54 @@ class _BasePageState extends State<BasePage> {
}
}
Future<Trip> getFirstTrip (Future<List<Trip>> trips) async {
List<Trip> tripsf = await trips;
return tripsf[0];
// This function is used to get the first trip from a list of trips
// TODO: Implement this function
Trip getFirstTrip(Future<List<Trip>> trips) {
Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>());
t1.landmarks.add(
Landmark(
uuid: '1',
name: "Eiffel Tower",
location: [48.859, 2.295],
type: LandmarkType(name: "Tower"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
),
);
t1.landmarks.add(
Landmark(
uuid: "2",
name: "Notre Dame Cathedral",
location: [48.8530, 2.3498],
type: LandmarkType(name: "Monument"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
),
);
t1.landmarks.add(
Landmark(
uuid: "3",
name: "Louvre palace",
location: [48.8606, 2.3376],
type: LandmarkType(name: "Museum"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
),
);
t1.landmarks.add(
Landmark(
uuid: "4",
name: "Pont-des-arts",
location: [48.8585, 2.3376],
type: LandmarkType(name: "Bridge"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"
),
);
t1.landmarks.add(
Landmark(
uuid: "5",
name: "Panthéon",
location: [48.847, 2.347],
type: LandmarkType(name: "Monument"),
imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
),
);
return t1;
}

View File

@ -13,7 +13,7 @@ class App extends StatelessWidget {
return MaterialApp(
title: APP_NAME,
home: BasePage(mainScreen: "map"),
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green),
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]),
);
}
}

View File

@ -3,12 +3,10 @@ import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
class Greeter extends StatefulWidget {
final Future<Trip> trip;
final bool standalone;
final Trip trip;
Greeter({
required this.standalone,
required this.trip
required this.trip,
});
@override
@ -18,55 +16,66 @@ class Greeter extends StatefulWidget {
class _GreeterState extends State<Greeter> {
Widget greeterBuild (BuildContext context, AsyncSnapshot<Trip> snapshot) {
Widget greeterBuilder (BuildContext context, Widget? child) {
ThemeData theme = Theme.of(context);
Widget topGreeter;
if (snapshot.hasData) {
topGreeter = Padding(
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: Text(
'Welcome to ${snapshot.data?.cityName}!',
style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
)
);
} else if (snapshot.hasError) {
topGreeter = const Padding(
padding: EdgeInsets.only(top: 20, bottom: 20),
child: Text('Error while fetching trip')
if (widget.trip.landmarks.length > 1) {
topGreeter = FutureBuilder(
future: widget.trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(
'Welcome to ${snapshot.data}!',
style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
);
} else if (snapshot.hasError) {
return const Text('Welcome to your trip!');
} else {
return const Text('Welcome to ...');
}
}
);
} else {
// still awaiting the cityname
// still awaiting the trip
// We can hopefully infer the city name from the cityName future
// Show a linear loader at the bottom and an info message above
topGreeter = Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: const Text('Generating your trip...', style: TextStyle(fontSize: 20),)
FutureBuilder(
future: widget.trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Text(
'Generating your trip to ${snapshot.data}...',
style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
);
} else if (snapshot.hasError) {
return const Text('Error while fetching city name');
}
return const Text('Generating your trip...');
}
),
const LinearProgressIndicator()
Padding(
padding: EdgeInsets.all(5),
child: const LinearProgressIndicator()
)
]
);
}
if (widget.standalone) {
return Center(
child: topGreeter,
);
} else {
return Center(
child: Column(
children: [
Padding(padding: EdgeInsets.only(top: 24.0)),
topGreeter,
bottomGreeter,
Padding(padding: EdgeInsets.only(bottom: 24.0)),
],
)
);
}
return Center(
child: Column(
children: [
// Padding(padding: EdgeInsets.only(top: 20)),
topGreeter,
Padding(
padding: EdgeInsets.all(20),
child: bottomGreeter
),
],
)
);
}
Widget bottomGreeter = const Text(
@ -79,9 +88,9 @@ class _GreeterState extends State<Greeter> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.trip,
builder: greeterBuild,
return ListenableBuilder(
listenable: widget.trip,
builder: greeterBuilder,
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:anyway/structs/landmark.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
@ -31,9 +32,10 @@ class _LandmarkCardState extends State<LandmarkCard> {
height: double.infinity,
// force a fixed width
width: 160,
child: Image.network(
widget.landmark.imageURL ?? '',
errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
child: CachedNetworkImage(
imageUrl: widget.landmark.imageURL ?? '',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
// TODO: make this a switch statement to load a placeholder if null
// cover the whole container meaning the image will be cropped
fit: BoxFit.cover,

View File

@ -1,17 +1,17 @@
import 'dart:collection';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:anyway/modules/landmark_card.dart';
import 'package:anyway/structs/landmark.dart';
import 'package:anyway/structs/trip.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LandmarksOverview extends StatefulWidget {
final Future<Trip>? trip;
final Trip? trip;
const LandmarksOverview({super.key, this.trip});
@override
@ -23,18 +23,17 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
@override
Widget build(BuildContext context) {
final Future<LinkedList<Landmark>> _landmarks = getLandmarks(widget.trip);
return DefaultTextStyle(
style: Theme.of(context).textTheme.displayMedium!,
textAlign: TextAlign.center,
child: FutureBuilder<LinkedList<Landmark>>(
future: _landmarks,
builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) {
List<Widget> children;
if (snapshot.hasData) {
children = [landmarksWithSteps(snapshot.data!), saveButton()];
} else if (snapshot.hasError) {
children = <Widget>[
return ListenableBuilder(//<LinkedList<Landmark>>
listenable: widget.trip!,
builder: (BuildContext context, Widget? child) {
Trip trip = widget.trip!;
log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
List<Widget> children;
if (trip.uuid == 'pending') {
// the trip is still being fetched from the api
children = [Center(child: CircularProgressIndicator())];
} else if (trip.uuid == 'error') {
children = [
const Icon(
Icons.error_outline,
color: Colors.red,
@ -42,20 +41,25 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)),
child: Text('Error: ${trip.cityName}'),
),
];
} else {
if (trip.landmarks.length <= 1) {
children = [
const Text("No landmarks in this trip"),
];
} else {
children = [Center(child: CircularProgressIndicator())];
children = [
landmarksWithSteps(trip.landmarks),
saveButton(),
];
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
);
},
),
}
return Column(
children: children,
);
},
);
}
Widget saveButton() => ElevatedButton(
@ -100,7 +104,7 @@ Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
);
lkey++;
if (landmark.next != null) {
Widget step = stepBetweenLandmarks(landmark, landmark.next!);
Widget step = stepBetweenLandmarks(landmark);
children.add(step);
}
}
@ -111,7 +115,7 @@ Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
}
Widget stepBetweenLandmarks(Landmark before, Landmark after) {
Widget stepBetweenLandmarks(Landmark landmark) {
// This is a simple widget that draws a line between landmark-cards
// It's a vertical dotted line
// Next to the line is the icon for the mode of transport (walking for now) and the estimated time
@ -134,7 +138,7 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
Column(
children: [
Icon(Icons.directions_walk),
Text("5 min", style: TextStyle(fontSize: 10)),
Text("${landmark.tripTime} min", style: TextStyle(fontSize: 10)),
],
),
Spacer(),
@ -149,8 +153,5 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
);
}
Future<LinkedList<Landmark>> getLandmarks (Future<Trip>? trip) async {
Trip tripf = await trip!;
return tripf.landmarks;
}

View File

@ -8,7 +8,7 @@ import 'package:the_widget_marker/the_widget_marker.dart';
class MapWidget extends StatefulWidget {
final Future<Trip>? trip;
final Trip? trip;
MapWidget({
this.trip
@ -31,8 +31,7 @@ class _MapWidgetState extends State<MapWidget> {
void _onMapCreated(GoogleMapController controller) async {
mapController = controller;
Trip? trip = await widget.trip;
List<double>? newLocation = trip?.landmarks.first.location;
List<double>? newLocation = widget.trip?.landmarks.first.location;
if (newLocation != null) {
CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
controller.moveCamera(update);
@ -48,8 +47,7 @@ class _MapWidgetState extends State<MapWidget> {
void drawLandmarks() async {
// (re)draws landmarks on the map
Trip? trip = await widget.trip;
LinkedList<Landmark>? landmarks = trip?.landmarks;
LinkedList<Landmark>? landmarks = widget.trip?.landmarks;
if (landmarks != null){
for (Landmark landmark in landmarks) {
markers.add(Marker(

View File

@ -30,7 +30,7 @@ class _TripsOverviewState extends State<TripsOverview> {
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "map", trip: Future.value(trip))
builder: (context) => BasePage(mainScreen: "map", trip: trip)
)
);
},

View File

@ -1,8 +1,10 @@
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:anyway/layout.dart';
import 'package:anyway/structs/preferences.dart';
import 'package:anyway/utils/fetch_trip.dart';
import 'package:flutter/material.dart';
import 'package:anyway/structs/preferences.dart';
import "package:anyway/structs/trip.dart";
@ -64,7 +66,16 @@ class _NewTripPageState extends State<NewTripPage> {
double.parse(lonController.text)
];
Future<UserPreferences> preferences = loadUserPreferences();
Future<Trip>? trip = fetchTrip(startPoint, preferences);
Trip trip = Trip();
trip.landmarks.add(
Landmark(
location: startPoint,
name: "start",
type: LandmarkType(name: 'start'),
uuid: "pending"
)
);
fetchTrip(trip, preferences);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BasePage(mainScreen: "map", trip: trip)

View File

@ -10,10 +10,10 @@ import 'package:anyway/modules/greeter.dart';
class NavigationOverview extends StatefulWidget {
final Future<Trip> trip;
final Trip trip;
NavigationOverview({
required this.trip
required this.trip,
});
@override
@ -27,53 +27,56 @@ class _NavigationOverviewState extends State<NavigationOverview> {
@override
Widget build(BuildContext context) {
return SlidingUpPanel(
renderPanelSheet: false,
panel: _floatingPanel(),
collapsed: _floatingCollapsed(),
body: MapWidget(trip: widget.trip)
// collapsed: _floatingCollapsed(),
body: MapWidget(trip: widget.trip),
// renderPanelSheet: false,
// backdropEnabled: true,
maxHeight: MediaQuery.of(context).size.height * 0.8,
padding: EdgeInsets.all(10),
// panelSnapping: false,
borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
boxShadow: [
BoxShadow(
blurRadius: 20.0,
color: Colors.black,
)
],
);
}
Widget _floatingCollapsed(){
final ThemeData theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.canvasColor,
borderRadius: BorderRadius.only(topLeft: Radius.circular(24.0), topRight: Radius.circular(24.0)),
boxShadow: []
),
child: Greeter(standalone: true, trip: widget.trip)
return Greeter(
trip: widget.trip
);
}
Widget _floatingPanel(){
final ThemeData theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(24.0)),
boxShadow: [
BoxShadow(
blurRadius: 20.0,
color: theme.shadowColor,
),
]
),
child: Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Greeter(standalone: false, trip: widget.trip),
LandmarksOverview(trip: widget.trip),
],
),
return Column(
children: [
Padding(
padding: const EdgeInsets.all(15),
child:
Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.all(Radius.circular(12.0)),
),
),
),
),
),
),
Expanded(
child: ListView(
children: [
Greeter(trip: widget.trip),
LandmarksOverview(trip: widget.trip)
]
)
)
],
);
}
}

View File

@ -5,35 +5,56 @@ import 'dart:collection';
import 'dart:convert';
import 'package:anyway/structs/landmark.dart';
import 'package:flutter/foundation.dart';
import 'package:geocoding/geocoding.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Trip {
final String uuid;
final String cityName;
// TODO: cityName should be inferred from coordinates of the Landmarks
final int totalTime;
final LinkedList<Landmark> landmarks;
class Trip with ChangeNotifier {
String uuid;
int totalTime;
LinkedList<Landmark> landmarks;
// could be empty as well
Future<String> get cityName async {
if (GeocodingPlatform.instance == null) {
return '${landmarks.first.location[0]}, ${landmarks.first.location[1]}';
}
List<Placemark> placemarks = await placemarkFromCoordinates(landmarks.first.location[0], landmarks.first.location[1]);
return placemarks.first.locality ?? 'Unknown';
}
Trip({
required this.uuid,
required this.cityName,
required this.landmarks,
this.totalTime = 0
});
this.uuid = 'pending',
this.totalTime = 0,
LinkedList<Landmark>? landmarks
// a trip can be created with no landmarks, but the list should be initialized anyway
}) : landmarks = landmarks ?? LinkedList<Landmark>();
factory Trip.fromJson(Map<String, dynamic> json) {
Trip trip = Trip(
uuid: json['uuid'],
cityName: json['city_name'] ?? 'Not communicated',
landmarks: LinkedList()
totalTime: json['total_time'],
);
return trip;
}
void loadFromJson(Map<String, dynamic> json) {
uuid = json['uuid'];
totalTime = json['total_time'];
notifyListeners();
}
void addLandmark(Landmark landmark) {
landmarks.add(landmark);
notifyListeners();
}
void updateUUID(String newUUID) {
uuid = newUUID;
notifyListeners();
}
factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
String? content = prefs.getString('trip_$uuid');
@ -47,7 +68,7 @@ class Trip {
Map<String, dynamic> toJson() => {
'uuid': uuid,
'city_name': cityName,
'total_time': totalTime,
'first_landmark_uuid': landmarks.first.uuid
};

View File

@ -1,7 +1,7 @@
import "dart:convert";
import "dart:developer";
import 'package:dio/dio.dart';
import 'package:anyway/constants.dart';
import "package:anyway/structs/landmark.dart";
import "package:anyway/structs/trip.dart";
@ -25,14 +25,14 @@ Dio dio = Dio(
),
);
Future<Trip>? fetchTrip(
List<double> startPoint,
fetchTrip(
Trip trip,
Future<UserPreferences> preferences,
) async {
UserPreferences prefs = await preferences;
Map<String, dynamic> data = {
"preferences": prefs.toJson(),
"start": startPoint
"start": trip.landmarks!.first.location,
};
String dataString = jsonEncode(data);
log(dataString);
@ -44,24 +44,25 @@ Future<Trip>? fetchTrip(
// handle errors
if (response.statusCode != 200) {
trip.updateUUID("error");
throw Exception('Failed to load trip');
}
if (response.data["error"] != null) {
trip.updateUUID("error");
throw Exception(response.data["error"]);
}
log(response.data.toString());
Map<String, dynamic> json = response.data;
// only fetch the trip "meta" data for now
Trip trip = Trip.fromJson(json);
// only fill in the trip "meta" data for now
trip.loadFromJson(json);
String? nextUUID = json["first_landmark_uuid"];
while (nextUUID != null) {
var (landmark, newUUID) = await fetchLandmark(nextUUID);
trip.landmarks.add(landmark);
trip.addLandmark(landmark);
nextUUID = newUUID;
}
return trip;
}

View File

@ -17,7 +17,7 @@ Future<List<Trip>> loadTrips() async {
}
if (trips.isEmpty) {
Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList<Landmark>());
Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>());
t1.landmarks.add(
Landmark(
uuid: '1',
@ -66,7 +66,7 @@ Future<List<Trip>> loadTrips() async {
trips.add(t1);
Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList<Landmark>());
Trip t2 = Trip(uuid: '2', landmarks: LinkedList<Landmark>());
t2.landmarks.add(
Landmark(

View File

@ -25,6 +25,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
url: "https://pub.dev"
source: hosted
version: "4.1.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
characters:
dependency: transitive
description:
@ -168,6 +192,38 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
geocoding:
dependency: "direct main"
description:
name: geocoding
sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66
url: "https://pub.dev"
source: hosted
version: "3.0.0"
geocoding_android:
dependency: transitive
description:
name: geocoding_android
sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
geocoding_ios:
dependency: transitive
description:
name: geocoding_ios
sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
geocoding_platform_interface:
dependency: transitive
description:
name: geocoding_platform_interface
sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
google_maps:
dependency: transitive
description:
@ -296,6 +352,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:

View File

@ -41,6 +41,8 @@ dependencies:
dio: ^5.5.0+1
google_maps_flutter: ^2.7.0
the_widget_marker: ^1.0.0
cached_network_image: ^3.4.0
geocoding: ^3.0.0
dev_dependencies:
flutter_test: