Flutter application is not showing remote view while joining the Agora Channel for video calls - flutter

I am developing an application in which user can contact each other via video calls. I have setup my server on railway by following the Agora documentation. If anyone has any suggestions or know what I am doing wrong please do let me know. I have tried leaving the token empty ('') but it still gives invalid token error. The token is getting generated successfully but when users join the call. Remote view is not showing up even though the onUserJoined callback is getting triggered perfectly on both caller and receiver side.
This is the call screen code which will enable users to contact with each other
// ignore_for_file: prefer_typing_uninitialized_variables, use_build_context_synchronously
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../constants/constants.dart';
import '../../global/firebase_ref.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wakelock/wakelock.dart';
import '../../methods/call_methods.dart';
import '../../models/call_model.dart';
import '../../services/app_services.dart';
import '../../services/connectivity_services.dart';
import '../../services/user_services.dart';
import '../../widgets/custom_images.dart';
import '../../widgets/custom_widgets.dart';
class VideoCallScreen extends StatefulWidget {
const VideoCallScreen(this.call, {Key? key}) : super(key: key);
final CallModel call;
#override
State<VideoCallScreen> createState() => _VideoCallScreenState();
}
class _VideoCallScreenState extends State<VideoCallScreen> {
final _users = <int>[];
final _infoStrings = <String>[];
bool muted = false;
RtcEngine? _engine;
bool isspeaker = true;
bool isalreadyendedcall = false;
String current = Get.find<UserServices>().adminid;
CollectionReference? reference;
String token = '';
Stream<DocumentSnapshot>? stream;
#override
void dispose() {
_users.clear();
_engine!.leaveChannel();
_engine!.release();
super.dispose();
}
getToken() async {
final url = Uri.parse(
'https://agora-token-service-production-59a1.up.railway.app/rtc/${widget.call.channelid}/1/uid/0',
);
Get.log('Token URL $url');
final response = await http.get(url);
debugPrint('Response: $response');
if (response.statusCode == 200) {
setState(() {
token = response.body;
token = jsonDecode(token)['rtcToken'];
Get.log('token: $token');
});
} else {
Get.log('Failed to fetch the token');
}
}
#override
void initState() {
initialize();
super.initState();
if (widget.call.by == current) {
reference = userRef.doc(widget.call.receiver!.id).collection('History');
stream = reference!.doc(widget.call.timeepoch.toString()).snapshots();
} else {
reference = adminRef.doc(widget.call.caller!.id).collection('History');
stream = reference!.doc(widget.call.timeepoch.toString()).snapshots();
}
}
Future<void> initialize() async {
try {
await [Permission.microphone, Permission.camera].request();
await getToken();
if (Get.find<AppServices>().appid.isEmpty) {
setState(() {
_infoStrings.add(
'Agora_APP_IDD missing, please provide your Agora_APP_IDD in app_constant.dart',
);
_infoStrings.add('Agora Engine is not starting');
});
return;
}
await _initAgoraRtcEngine();
_addAgoraEventHandlers();
VideoEncoderConfiguration configuration = const VideoEncoderConfiguration(
dimensions: VideoDimensions(height: 1920, width: 1080),
);
await _engine!.setVideoEncoderConfiguration(configuration);
Get.log('Channel id: ${widget.call.channelid}');
await _engine!.joinChannel(
token: token,
channelId: widget.call.channelid!,
uid: 0,
options: const ChannelMediaOptions(),
);
} catch (e) {
Get.log('Catch: $e');
}
}
Future<void> _initAgoraRtcEngine() async {
_engine = createAgoraRtcEngine();
await _engine!.initialize(
RtcEngineContext(
appId: Get.find<AppServices>().appid,
channelProfile: ChannelProfileType.channelProfileCommunication,
),
);
// _engine = await RtcEngine.create(Get.find<AppServices>().agoraid);
await _engine!.enableVideo();
await _engine!.enableAudio();
await _engine!.enableLocalVideo(true);
await _engine!.enableLocalAudio(true);
await _engine!.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
Get.log('---engine----');
}
var remoteid;
void _addAgoraEventHandlers() {
_engine!.registerEventHandler(
RtcEngineEventHandler(
onError: (code, value) {
setState(() {
final info = 'onErrorCode: $code';
_infoStrings.add(info);
Get.log(info);
final infp = 'onError: $value';
_infoStrings.add(infp);
Get.log(infp);
});
},
onJoinChannelSuccess: (channel, elapsed) {
setState(() {
final info = 'onJoinChannel: $channel';
_infoStrings.add(info);
Get.log(info);
});
if (widget.call.caller!.id == current) {
adminRef
.doc(widget.call.caller!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'TYPE': 'OUTGOING',
'ISVIDEOCALL': widget.call.video,
'PEER': widget.call.receiver!.id,
'TARGET': widget.call.receiver!.id,
'TIME': widget.call.timeepoch,
'DP': widget.call.receiver!.picture,
'ISMUTED': false,
'ISJOINEDEVER': false,
'STATUS': 'calling',
'STARTED': null,
'ENDED': null,
'CALLERNAME': widget.call.caller!.name,
'CHANNEL': channel.channelId,
'UID': channel.localUid,
}, SetOptions(merge: true)).then(
(value) => Get.log('added'),
);
userRef
.doc(widget.call.receiver!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'TYPE': 'INCOMING',
'ISVIDEOCALL': widget.call.video,
'PEER': widget.call.caller!.id,
'TARGET': widget.call.receiver!.id,
'TIME': widget.call.timeepoch,
'DP': widget.call.caller!.picture,
'ISMUTED': false,
'ISJOINEDEVER': true,
'STATUS': 'missedcall',
'STARTED': null,
'ENDED': null,
'CALLERNAME': widget.call.caller!.name,
'CHANNEL': channel.channelId,
'UID': channel.localUid,
}, SetOptions(merge: true));
}
Wakelock.enable();
},
onLeaveChannel: (connection, stats) {
// setState(() {
_infoStrings.add('onLeaveChannel');
_users.clear();
// });
if (isalreadyendedcall == false) {
adminRef
.doc(widget.call.caller!.id!)
.collection("History")
.doc(widget.call.timeepoch.toString())
.set({
'STATUS': 'ended',
'ENDED': DateTime.now(),
'ISMUTED': false,
'UID': -1,
}, SetOptions(merge: true));
userRef
.doc(widget.call.receiver!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'STATUS': 'ended',
'ENDED': DateTime.now(),
'ISMUTED': false,
'UID': -1,
}, SetOptions(merge: true));
// //----------
// userRef
// .doc(widget.call.receiver!.id)
// .collection('recent')
// .doc(widget.call.id)
// .set({
// 'id': widget.call.caller!.id,
// 'ENDED': DateTime.now().millisecondsSinceEpoch,
// 'CALLERNAME': widget.call.receiver!.name,
// }, SetOptions(merge: true));
}
Wakelock.disable();
},
onUserJoined: (connection, uid, elapsed) {
setState(() {
final info = 'userJoined: $uid';
_infoStrings.add(info);
_users.add(uid);
Get.log(info);
remoteid = uid;
Get.log(remoteid);
});
startTimerNow();
if (Get.find<UserServices>().adminid == widget.call.caller!.id) {
adminRef
.doc(widget.call.caller!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'STARTED': DateTime.now(),
'STATUS': 'pickedup',
'ISJOINEDEVER': true,
}, SetOptions(merge: true));
userRef
.doc(widget.call.receiver!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'STARTED': DateTime.now(),
'STATUS': 'pickedup',
}, SetOptions(merge: true));
}
Wakelock.enable();
},
onUserOffline: (connection, uid, elapsed) {
setState(() {
final info = 'userOffline: $uid';
_infoStrings.add(info);
_users.remove(uid);
Get.log(info);
remoteid = null;
});
if (isalreadyendedcall == false) {
adminRef
.doc(widget.call.caller!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'STATUS': 'ended',
'ENDED': DateTime.now(),
'ISMUTED': false,
'UID': -1,
}, SetOptions(merge: true));
userRef
.doc(widget.call.receiver!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'STATUS': 'ended',
'ENDED': DateTime.now(),
'ISMUTED': false,
'UID': -1,
}, SetOptions(merge: true));
//----------
// userRef
// .doc(widget.call.receiver!.id)
// .collection('recent')
// .doc(widget.call.id)
// .set({
// 'id': widget.call.caller!.id,
// 'ENDED': DateTime.now().millisecondsSinceEpoch,
// 'CALLERNAME': widget.call.receiver!.name,
// }, SetOptions(merge: true));
}
},
onFirstRemoteVideoFrame: (connection, uid, width, height, elapsed) {
setState(() {
final info = 'firstRemoteVideo: $uid ${width}x $height';
_infoStrings.add(info);
Get.log(info);
});
},
onTokenPrivilegeWillExpire: (connection, string) async {
await getToken();
await _engine!.renewToken(token);
},
),
);
}
void onCallEnd(BuildContext context) async {
await CallMethods.endCall(call: widget.call);
DateTime now = DateTime.now();
if (isalreadyendedcall == false) {
await adminRef
.doc(widget.call.caller!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'STATUS': 'ended',
'ENDED': now,
'ISMUTED': false,
"UID": -1,
}, SetOptions(merge: true));
await userRef
.doc(widget.call.receiver!.id!)
.collection('History')
.doc(widget.call.timeepoch.toString())
.set({
'STATUS': 'ended',
'ENDED': now,
'ISMUTED': false,
'UID': -1,
}, SetOptions(merge: true));
// //----------
// userRef
// .doc(widget.call.receiver!.id)
// .collection('recent')
// .doc(widget.call.id)
// .set({
// 'id': widget.call.caller!.id,
// 'ENDED': DateTime.now().millisecondsSinceEpoch,
// 'CALLERNAME': widget.call.receiver!.name,
// }, SetOptions(merge: true));
}
Wakelock.disable();
Navigator.pop(context);
}
Widget callView({
String status = 'calling',
bool muted = false,
int? remoteuid,
}) {
var w = MediaQuery.of(context).size.width;
var h = MediaQuery.of(context).size.height;
return Container(
alignment: Alignment.center,
decoration: status == 'pickedup'
? null
: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: providerImage(
widget.call.caller!.id == current
? widget.call.receiver!.picture ?? ''
: widget.call.caller!.picture ?? '',
),
),
),
child: Container(
color: status == 'pickedup' ? null : Colors.white.withOpacity(0.3),
child: Stack(
alignment: Alignment.center,
children: [
status != 'pickedup'
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
width: w,
height: h / 5,
alignment: Alignment.center,
margin: EdgeInsets.only(
top: MediaQuery.of(context).padding.top),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.lock_rounded,
size: 17,
color: Colors.white38,
),
SizedBox(width: 6),
Text(
'End-to-end encrypted',
style: TextStyle(
color: Colors.white38,
fontWeight: FontWeight.w400,
fontFamily: AppStrings.opensans,
),
),
],
).marginOnly(top: 50, bottom: 7),
SizedBox(
width: w / 1.1,
child: Text(
widget.call.caller!.id ==
Get.find<UserServices>().adminid
? widget.call.receiver!.name!
: widget.call.caller!.name!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 27,
fontFamily: AppStrings.opensans,
),
),
),
],
),
),
Text(
status == 'calling'
? widget.call.receiver!.id ==
Get.find<UserServices>().adminid
? 'Connecting...'
: 'Calling...'
: status == 'pickedup'
? '$hoursStr : $minutesStr: $secondsStr'
: status == 'ended'
? 'Call Ended'
: status == 'rejected'
? 'Rejected'
: 'Please wait...',
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 18,
fontFamily: AppStrings.opensans,
),
).marginOnly(bottom: 16, top: 10),
Stack(
children: [
widget.call.caller!.id ==
Get.find<UserServices>().adminid
? status == 'ended' || status == 'rejected'
? Container(
height: w + (w / 11),
width: w,
color: Colors.white12,
child: Icon(
status == 'ended'
? Icons.person_off
: status == 'rejected'
? Icons.call_end_rounded
: Icons.person,
size: 140,
),
)
: Container()
: status == 'ended' || status == 'rejected'
? Container(
height: w + (w / 11),
width: w,
color: Colors.white12,
child: Icon(
status == 'ended'
? Icons.person_off
: status == 'rejected'
? Icons.call_end_rounded
: Icons.person,
size: 140,
),
)
: Container(),
Positioned(
bottom: 20,
child: SizedBox(
width: w,
height: 20,
child: Center(
child: status == 'pickedup'
? muted == true
? const Text(
'Muted',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
fontFamily: AppStrings.opensans,
),
)
: const SizedBox(height: 0)
: const SizedBox(height: 0),
),
),
),
],
),
SizedBox(height: h / 6),
],
)
: _engine == null
? SizedBox()
: SizedBox(
child: AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: _engine!,
canvas: VideoCanvas(uid: remoteuid),
connection: RtcConnection(
channelId: widget.call.channelid!,
),
),
),
),
if (status == 'pickedup')
Positioned(
top: 150,
child: Text(
'$hoursStr: $minutesStr: $secondsStr',
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 18,
color: Colors.white,
fontFamily: AppStrings.opensans,
),
),
),
if (status != 'ended' || status != 'rejected')
_engine == null
? SizedBox()
: Align(
alignment: Alignment.bottomRight,
child: SizedBox(
width: 200,
height: 200,
child: AgoraVideoView(
controller: VideoViewController(
rtcEngine: _engine!,
canvas: const VideoCanvas(uid: 0),
),
),
),
),
],
),
),
);
}
onToggleMute() {
setState(() {
muted = !muted;
});
_engine!.muteLocalAudioStream(muted);
reference!
.doc(widget.call.timeepoch.toString())
.set({'ISMUTED': muted}, SetOptions(merge: true));
}
onSwitchCamera() => setState(() => _engine!.switchCamera());
Widget toolbar({String status = 'calling'}) {
return Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.symmetric(vertical: 35),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
status != 'ended' && status != 'rejected'
? SizedBox(
width: 65.67,
child: RawMaterialButton(
onPressed: onToggleMute,
shape: const CircleBorder(),
elevation: 2.0,
fillColor: muted ? Colors.blueAccent : Colors.white,
padding: const EdgeInsets.all(12.0),
child: Icon(
muted ? Icons.mic_off : Icons.mic,
color: muted ? Colors.white : Colors.blueAccent,
size: 22.0,
),
),
)
: const SizedBox(height: 42, width: 65.67),
SizedBox(
width: 65.67,
child: RawMaterialButton(
onPressed: () async {
Get.log('--on call end---');
setState(() {
isalreadyendedcall =
status == 'ended' || status == 'rejected' ? true : false;
onCallEnd(context);
});
},
shape: const CircleBorder(),
elevation: 2.0,
fillColor: status == 'ended' || status == 'rejected'
? Colors.black
: Colors.redAccent,
padding: const EdgeInsets.all(15.0),
child: Icon(
status == 'ended' || status == 'rejected'
? Icons.close
: Icons.call,
color: Colors.white,
size: 35.0,
),
),
),
status == 'ended' || status == 'rejected'
? const SizedBox(width: 65.67)
: SizedBox(
width: 65.67,
child: RawMaterialButton(
onPressed: onSwitchCamera,
shape: const CircleBorder(),
elevation: 2.0,
fillColor: Colors.white,
padding: const EdgeInsets.all(12.0),
child: const Icon(
Icons.switch_camera,
color: Colors.blueAccent,
size: 20.0,
),
),
),
],
),
);
}
Widget panel() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 48),
alignment: Alignment.bottomCenter,
child: FractionallySizedBox(
heightFactor: 0.5,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 48),
child: ListView.builder(
reverse: true,
itemCount: _infoStrings.length,
itemBuilder: (BuildContext context, int index) {
if (_infoStrings.isEmpty) return const SizedBox();
return Padding(
padding:
const EdgeInsets.symmetric(vertical: 3, horizontal: 10),
child: Text(_infoStrings[index]),
);
},
),
),
),
);
}
#override
Widget build(BuildContext context) {
return Obx(
() => Get.find<ConnectivityService>().connectionStatus.value ==
ConnectivityResult.none
? const DisconnectedWidget()
: Scaffold(
body: Stack(
children: [
_engine == null
? Center(
child: Stack(
children: [callView(), panel(), toolbar()],
),
)
: StreamBuilder<DocumentSnapshot>(
stream: stream as Stream<DocumentSnapshot>,
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.data!.data() != null &&
snapshot.data != null) {
var doc = snapshot.data!;
Get.log(doc.toString());
return Center(
child: Stack(
children: [
callView(
status: doc['STATUS'],
muted: doc['ISMUTED'],
remoteuid: doc['UID'],
),
panel(),
toolbar(status: doc['STATUS']),
],
),
);
}
return Center(
child: Stack(
children: [callView(), panel(), toolbar()],
),
);
},
),
],
),
),
);
}
}

I didn't set it up with a rails server but it should work the same. On the flutter side im calling a function at the very first point:
final token = await createToken(channelName, userId);
channel name to identify the channel for the users and a userId of my user who should be able to join the channel.
Future<dynamic> createToken(String channelName, int uid) async {
try {
//404
// final response = await dio.get('${url}/api/video/create-token?agChannelName=$channelName&agRole=$role&agUid=$uid&agExpireTime=$expireTime');
final response = await dio.get(
'${url}/api/video/create-token?agChannelName=$channelName&agUid=$uid');
print('res is ${response.data["token"]}');
return response.data["token"];
} on DioError catch (e) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx and is also not 304.
if (e.response != null) {
//print(HttpException(e.response!.data["message"]));
return e.response!.data;
//print(e.response!.headers);
//print(e.response!.requestOptions);
} else {
// Something happened in setting up or sending the request that triggered an Error
//print(e.requestOptions);
print('get events: ${e.message}');
}
}
}
On my server side where im using a javascript framework, im doing the following:
...
const token = RtcTokenBuilder.buildTokenWithUid(
process.env.AGORA_APP_ID,
process.env.AGORA_APP_CERTIFICATE,
channelName,
uid,
RtcRole.PUBLISHER,
privilegeExpireTime
);
console.log(token)
return res.status(201).json({ token: token });
For that im using the agora-access-token library on npm https://www.npmjs.com/package/agora-access-token

Related

Delete profile in firebase flutter

I need to delete the users profile from firebase, thing is I have 3 collections petfiles, petsdetails and users which need to be deleted. As in when a user signs up, they can create a pet and create records for their pet. But when having to delete the user, it should delete all the pets and records the pet has (it could be up to any amount)
I havent tried anything as I dont really know how to solve this but this is my code
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:image_picker/image_picker.dart';
import 'package:vet_bookr/oScreens/petFiles.dart';
import '../constant.dart';
class UserDetails extends StatefulWidget {
UserDetails({Key? key, required this.details}) : super(key: key);
Map<String, dynamic> details;
#override
State<UserDetails> createState() => _UserDetailsState();
}
class _UserDetailsState extends State<UserDetails> {
List<String> labels = ["Email", "Phone Number"];
bool editableText = false;
String imageUrl = "";
List<String> petIds = [];
#override
void initState() {
// TODO: implement initState
nameController.text = widget.details["name"];
ageController.text = widget.details["age"];
super.initState();
}
Future<void> uploadImages({required String path}) async {
try {
final imageRef = storageRef.child(
"Users/${FirebaseAuth.instance.currentUser?.uid}/${widget.details["id"]}");
await imageRef.putFile(File(path));
imageUrl = await imageRef.getDownloadURL();
setState(() {});
//print(imageRef.getDownloadURL());
} on FirebaseException catch (e) {
print("Function does work");
SnackBar snackBar = SnackBar(content: Text(e.message!));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
bool isLoading = false;
final nameController = TextEditingController();
final ageController = TextEditingController();
XFile? profilePic;
ImagePicker imagePicker = ImagePicker();
final storageRef = FirebaseStorage.instance.ref();
TextEditingController controllerChanger(index) {
if (index == 0) {
return nameController;
}
if (index == 1) {
return ageController;
}
return TextEditingController();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
},
child: Scaffold(
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle.dark,
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back,
color: Colors.black,
),
onPressed: () {
Navigator.pop(context);
},
),
actions: [
Padding(
padding: EdgeInsets.only(right: 5.sp),
child: PopupMenuButton(
icon: Icon(
Icons.more_vert,
color: Colors.black,
),
itemBuilder: (context) {
return [
PopupMenuItem<int>(
value: 0,
child: Text("Edit Profile"),
),
PopupMenuItem<int>(
value: 1,
child: Text("Delete Profile"),
),
];
},
onSelected: (value) async {
if (value == 0) {
setState(() {
editableText = true;
});
print(editableText);
} else if (value == 1) {
setState(() {
isLoading = true;
});
final ref = storageRef.child(
"Users/${FirebaseAuth.instance.currentUser?.uid}/${widget.details["id"]}");
await ref.delete();
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser?.uid)
.delete();
DocumentSnapshot<Map<dynamic, dynamic>> snap =
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser?.uid)
.get();
await FirebaseFirestore.instance
.collection("petDetails")
.doc(FirebaseAuth.instance.currentUser?.uid)
.update({
'pets': FieldValue.arrayRemove(snap.data()!["pets"]!),
});
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser?.uid)
.update({
'pets': FieldValue.arrayUnion(snap.data()!["pets"]!),
});
setState(() {
isLoading = false;
});
Navigator.pop(context);
}
},
))
],
),
extendBodyBehindAppBar: true,
backgroundColor: kBackgroundColor,
body: isLoading
? Container(
width: 1.sw,
height: 0.4.sh,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 15.sp,
width: 15.sp,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xffFF8B6A),
),
),
],
),
)
: SafeArea(
child: Container(
//alignment: Alignment.center,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(15.sp),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// sBox(h: 10),
Text(
'Pet Information',
style: TextStyle(
color: Color(0xffF08519), fontSize: 0.05.sw),
),
// myPetTile()
SizedBox(
height: 0.05.sh,
),
Stack(
children: [
CircleAvatar(
radius: 0.095.sh,
backgroundColor: Color(0xffFF8B6A),
backgroundImage: profilePic == null
? NetworkImage(
widget.details["profilePicture"],
)
: null,
child: profilePic == null
? Container(
width: 0,
height: 0,
)
: ClipRRect(
borderRadius:
BorderRadius.circular(100),
child: Image.file(
File(
"${profilePic?.path}",
),
),
),
),
editableText
? Positioned(
right: 0.025.sw,
bottom: 0.005.sh,
child: GestureDetector(
onTap: () async {
profilePic = await ImagePicker()
.pickImage(
source: ImageSource.gallery);
setState(() {});
},
child: CircleAvatar(
backgroundColor: Color(0xffFF8B6A),
radius: 0.02.sh,
child: Icon(
Icons.camera_alt_outlined,
size: 0.05.sw,
color: Colors.white,
),
),
),
)
: Container(
width: 0,
height: 0,
)
],
),
SizedBox(
height: 0.02.sh,
),
...List.generate(
2,
(index) => Padding(
padding: EdgeInsets.only(
top: 0.02.sh, left: 0.05.sw, right: 0.05.sw),
child: TextField(
enabled: editableText,
controller: controllerChanger(index),
style: TextStyle(fontSize: 0.017.sh),
cursorColor: Colors.black,
decoration: InputDecoration(
label: Text(labels[index],
style: TextStyle(fontSize: 0.02.sh)),
labelStyle: TextStyle(color: Colors.black54),
focusedBorder: OutlineInputBorder(
borderRadius:
BorderRadius.circular(10.sp),
borderSide:
BorderSide(color: Color(0xffFF8B6A))),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.sp),
),
enabledBorder: OutlineInputBorder(
borderRadius:
BorderRadius.circular(10.sp),
borderSide:
BorderSide(color: Color(0xffFF8B6A))),
contentPadding:
EdgeInsets.symmetric(horizontal: 10.sp),
hintStyle: TextStyle(color: Colors.grey),
),
),
),
),
buttonWidget()
],
),
),
),
),
),
),
);
}
bool isLoadingEdit = false;
Widget buttonWidget() {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
width: 0.4.sw,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
backgroundColor: Color(0xffFF8B6A),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.sp),
),
),
onPressed: () async {
if (editableText) {
setState(() {
isLoadingEdit = true;
});
if (profilePic != null) {
final ref = storageRef.child(
"Users/${FirebaseAuth.instance.currentUser?.uid}/${widget.details["id"]}");
await ref.delete();
await uploadImages(path: profilePic!.path);
}
await FirebaseFirestore.instance
.collection("petsDetails")
.doc(widget.details["id"])
.update({
'name': nameController.text,
'age': ageController.text,
'breed': breedController.text,
'weight': weightController.text,
'profilePicture': profilePic != null
? imageUrl
: widget.details["profilePicture"],
'lastVaccinationDate':
widget.details["lastVaccinationDate"]
});
DocumentSnapshot<Map<String, dynamic>> snap =
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser?.uid)
.get();
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser?.uid)
.update({
'pets': FieldValue.arrayRemove(snap.data()!["pets"]!)
});
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser?.uid)
.update({
'pets': FieldValue.arrayRemove(snap.data()!["pets"]!),
});
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser?.uid)
.update({
'pets': FieldValue.arrayUnion(snap.data()!["pets"]!),
});
Navigator.pop(context);
setState(() {
isLoadingEdit = false;
editableText = false;
});
const snackBar = SnackBar(
content: Text("Your pet's details have been changed"),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PetFiles(
petId: widget.details["id"],
),
),
);
}
},
child: isLoadingEdit
? Container(
height: 15.sp,
width: 15.sp,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.sp,
),
)
: Text(
editableText ? "Save Changes" : "Pet Health Records",
style:
TextStyle(color: Colors.white, fontSize: 0.03.sw),
),
),
),
],
),
SizedBox(
height: 0.01.sh,
),
],
);
}
}
You can check out their official docs for deleting collections here.
Or you can upgrade your project to blaze plan and use this extension which takes care of it for you.
Alternatively you can use NodeJS and FirebaseFunctions to do this task for you. Use this code snippet
async function deleteCollection(db, collectionPath, batchSize) {
const collectionRef = db.collection(collectionPath);
const query = collectionRef.orderBy('__name__').limit(batchSize);
return new Promise((resolve, reject) => {
deleteQueryBatch(db, query, resolve).catch(reject);
});
}
async function deleteQueryBatch(db, query, resolve) {
const snapshot = await query.get();
const batchSize = snapshot.size;
if (batchSize === 0) {
// When there are no documents left, we are done
resolve();
return;
}
// Delete documents in a batch
const batch = db.batch();
snapshot.docs.forEach((doc) => {
batch.delete(doc.ref);
});
await batch.commit();
// Recurse on the next process tick, to avoid
// exploding the stack.
process.nextTick(() => {
deleteQueryBatch(db, query, resolve);
});
}
index.js

FLUTTER WEB: How To Use Google Places Autocomplete In Flutter Web?

I used almost every package for places auto complete, I am getting cross error. Please let me know if there is any way to use places autocomplete In Android, iOS and web together.
var p = await PlacesAutocomplete.show(
radius: 500000,
strictbounds: false,
region: "in",
language: "en",
context: context,
mode: Mode.overlay,
apiKey: "key",
components: [new Component(Component.country, "in")],
types: ["(cities)"],
hint: "Search City",
);
currently I am using this function
You need to have separate api for this google placeAutocomple
var response = await _client.get("https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$input"
"&types=establishment&language=en&components=country:in&key=$kGoogleApiKey&sessiontoken=19");
and then show result in autocomplete textfield or listview.
void getPlaces(input, lng,lat) {
SearchLocation()
.getPlaces(input: input, lang: lng,lat: lat)
.then((value) => _placesResponse(value))
.catchError((e) => _placesError(e));
}
_placesResponse(List<Place> v) {
setState(() {
addressList = v;
});
}
_placesError(e) {
errorSnackbar(context,e.toString());
}
Listview Widget
ListView.builder(
itemCount: addressList.length,
shrinkWrap: true,
itemBuilder: (ctx, index) {
var address = addressList[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: InkWell(
onTap: () {
if(_isLoading != true) {
getSelectedPlace(address.placeId);
}
},
child: Column(
children: [
Row(
children: [
Container(
width: 25,
height: 25,
decoration: BoxDecoration(color: Colors.grey, borderRadius: borderRadiusAll_30),
child: Center(
child: Icon(
CupertinoIcons.location_solid,
size: 14,
color: Colors.white,
))),
Flexible(
fit: FlexFit.tight,
child: Padding(
padding: const EdgeInsets.only(left: 12.0),
child: Text(
address.description ?? '',
style: GoogleFonts.sourceSans3(textStyle: textTheme.bodyText2),
textAlign: TextAlign.left,
),
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Divider(
color: MyColor.kGrey,
),
)
],
),
),
);
}),
For this you need to have kGoogleApiKey
To get lat long u need add geolocator
Future<void> _getCurrentPosition() async {
setState((){
_isLoading = true;
});
final hasPermission = await _handleLocationPermission();
if (!hasPermission) {
Geolocator.requestPermission();
return;
}
await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best).then((Position position) {
setState(() {
_position = position;
lang = _position?.longitude.toString() ?? '';
lat = _position?.latitude.toString() ?? '';
});
Log.d("Latitude ${lat} Longitude ${lang}");
setState((){
_isLoading = false;
});
}).catchError((e) {
setState((){
_isLoading = false;
});
warning(context,e.toString());
Geolocator.requestPermission();
Log.d('Catch error ${e.toString()}');
});
}

Two orders being generated instead of one in Flutter app

I am trying to make an e-commerce app that allows a user to place an order by either making an online payment using Razorpay or opting for cash on delivery (CoD).
Things work fine for the CoD part, it is in the razorpay powered order that duplicate orders are being created.
My approach: In the checkout screen, I am having two buttons, one each for CoD and online payment using Razorpay. I have a bool isCod initially set to true when the user first navigates to the page. Clicking on either of these buttons sets the isCod variable's values accordingly.
Now, when the user goes ahead with Razorpay payments, once the payment completes successfully two orders are being created at Firestore cloud database (in fact they get created at the same timestamp).
My Code:
class CheckoutScreen extends StatefulWidget {
const CheckoutScreen({super.key, required this.product});
final Map<String, dynamic> product;
#override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
bool isCod = true;
final String? currUserId = FirebaseAuth.instance.currentUser?.uid;
var _razorpay = Razorpay();
int? amount;
final _random = Random();
#override
void initState() {
super.initState();
_razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
_razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
_razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
generateDropDownValues();
}
void _handlePaymentSuccess(PaymentSuccessResponse response) {
// Do something when payment succeeds
print(response);
//Add current product to Current Orders array
currentOrders.add({
'orderId': Random().nextInt(1000),
'order': [{
'product': widget.product,
'qty': dropDownValue,
}],
'date': DateTime.now(),
'amount': buyNowValue
});
FirebaseFirestore.instance.collection('customers').doc(currUserId).update({
'currentOrders': currentOrders,
});
buyNowValue = 0;
Navigator.pushNamed(context, orderSuccess);
_razorpay.clear();
}
void _handlePaymentError(PaymentFailureResponse response) {
// Do something when payment fails
print("Payment Failed");
}
void _handleExternalWallet(ExternalWalletResponse response) {
// Do something when an external wallet is selected
}
Future<String?> generateRzpOrderId() async {
try {
String basicAuth = 'Basic ${base64Encode(utf8.encode('${razorpayId}:${razorpaySecret}'))}';
Map<String, dynamic> orderData = {
"amount": (buyNowValue*100).toInt(),
"currency": "INR",
'receipt': 'CG_${1000 + _random.nextInt(9999 - 1000)}'
};
var res = await http.post(
Uri.https("api.razorpay.com", "v1/orders"),
headers: <String, String>{
'Authorization': basicAuth,
'Content-Type': 'application/json'
},
body: json.encode(orderData),
);
print(res.body);
if ((json.decode(res.body)).containsKey('error')) {
return null;
} else {
return (json.decode(res.body))['id'];
}
} catch (e) {
print(e);
throw e;
}
}
Future openGateWay() async {
generateRzpOrderId().then((value){
var options = {
'key': 'rzp_live_hUk8LTeESrQ6lL',
'amount': (buyNowValue*100).toInt(), //in the smallest currency sub-unit.
'currency': "INR",
'name': 'Cattle GURU',
'description': 'Online purchase of cattle food',
'order_id': value, // Generate order_id using Orders API
'timeout': 300, // in seconds
'prefill': {
'contact': phoneNumber
}
};
try {
_razorpay.open(options);
} catch (e) {
print(e);
}
});
_razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
_razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
_razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
}
#override
void dispose() {
_razorpay.clear();
super.dispose(); // Removes all listeners
}
int dropDownValue = 1;
List dropDownValues = [];
generateDropDownValues(){
for(int i = 0; i < buyNowProduct['units']; i++){
dropDownValues.add(i+1);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 0.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 2.h,),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: ProductListTile(
onTap: (){
Navigator.push(context, MaterialPageRoute(builder: (context) =>
ProductScreen(
product: widget.product,
isCarted: false,
id: widget.product['prodId'],
prodQty: dropDownValue,
)));
},
imgUrl: widget.product['imgUrls'][0],
productDeliveryDays: widget.product['deliveryDays'],
date: DateTime.now(),
productName: widget.product['name'],
productWeight: widget.product['weight'].toDouble(),
price: widget.product['price'].toDouble(),
mrp: widget.product['mrp'].toDouble(),
protein: widget.product['protein'].toDouble(),
fibre: widget.product['fibre'].toDouble(),
fat: widget.product['fat'].toDouble(),
isCarted: false,
onSubtract: (){},
onAdd: (){},
qty: qty,
width: 0,
height: 0,
onAddToCart: (){},
onBuyNow: (){},
),
),
// SizedBox(height: 1.h,),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: Row(
children: [
Text(isEnglish ? "Select quantity" : "संख्या चुनें", style: globalTextStyle.copyWith(color: black, fontSize: 4.w, fontWeight: FontWeight.bold),),
SizedBox(width: 2.5.w,),
DropdownButton(
value: dropDownValue,
style: globalTextStyle.copyWith(color: grey, fontWeight: FontWeight.bold),
iconEnabledColor: grey,
items: dropDownValues.map((items) => DropdownMenuItem(value: items, child: Text(items.toString()))).toList(),
onChanged: (newValue){
setState(() {
dropDownValue = newValue as int;
buyNowValue = dropDownValue*auxBuyNowValue;
});
},
)
// CustomDropDown(items: dropDownValues, dropdownvalue: dropDownValue)
],
),
),
SizedBox(height: 1.h,),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: Text(isEnglish ? "Deliver to" : "इस पते पर पहुंचाएं", style: globalTextStyle.copyWith(color: black, fontSize: 4.w, fontWeight: FontWeight.bold),),
),
SizedBox(height: 1.h,),
AddressCard(
onTap: (){},
isDefault: false,
name: firestoreCurrentAddress['name'],
address: "${firestoreCurrentAddress['houseNum']}, ${firestoreCurrentAddress['village']}, ${firestoreCurrentAddress['district']}, ${firestoreCurrentAddress['state']}, ${firestoreCurrentAddress['pinCode']}",
onEditTap: (){
// Navigator.push(context, MaterialPageRoute(builder: (context) => EditAddressScreen(address: addressTiles[1], addressIndex: 1, isDefault: false,)));
Navigator.pushNamed(context, myAddresses);
},
onDefaultTap: (){
},
onRemoveTap: (){},
),
SizedBox(height: 1.h,),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: Row(
children: [
InkWell(
onTap: (){
setState(() {
isCod= true;
});
},
child: Container(
width: 44.w,
height: 15.w,
decoration: BoxDecoration(
border: Border.all(color: primary),
borderRadius: BorderRadius.all(Radius.circular(2.w)),
color: isCod ? primary : primaryLight,
),
child: Center(
child: Text(isEnglish ? "Cash on Delivery (COD)" : "डिलवरी पर नकदी", style: globalTextStyle.copyWith(color: isCod ? white : black, fontSize: 3.5.w, fontWeight: FontWeight.bold),),
),
),
),
SizedBox(width: 2.w,),
InkWell(
onTap: (){
setState(() {
isCod = false;
});
},
child: Container(
width: 44.w,
height: 15.w,
decoration: BoxDecoration(
border: Border.all(color: primary),
borderRadius: BorderRadius.all(Radius.circular(2.w)),
color: isCod ? primaryLight : primary,
),
child: Center(
child: Text(isEnglish ? "Online Payment" : "ऑनलाइन भुगतान", style: globalTextStyle.copyWith(color: isCod ? black : white, fontSize: 3.5.w, fontWeight: FontWeight.bold),),
),
),
),
],
),
),
SizedBox(height: 1.h,),
Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: CustomButton(width: 90.w, height: 15.w, color: primary,
onTap: () async {
if(isCod == false) {
await openGateWay();
} else {
//Add current product to Current Orders array
currentOrders.add({
'orderId': Random().nextInt(1000),
'order': [{
'product': widget.product,
'qty': dropDownValue,
}],
'date': DateTime.now(),
'amount': buyNowValue
});
FirebaseFirestore.instance.collection('customers').doc(currUserId).update({
'currentOrders': currentOrders,
});
buyNowValue = 0;
Navigator.pushNamed(context, orderSuccess);
}
},
text: isEnglish ? "Pay ${buyNowValue.toCurrencyString(leadingSymbol: "₹", useSymbolPadding: true)}" : "${buyNowValue.toCurrencyString(leadingSymbol: "₹", useSymbolPadding: true)} भुगतान करें ", fontColor: white, borderColor: primary),
)
],
),
),
),
);
}
}
I would like to know what is wrong in my code that results in the duplicate order being created and how this issue can be solved.

I/flutter (32524): type 'String' is not a subtype of type 'int' of 'index'

I have this error ("type 'String' is not a sub type of type 'int' of 'index'")
but i don't know where the problem is or which line, the compiler don't tell me which line the error is,
here is the code:
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Auth>(
create: (_) =>
Auth(), //lezem nekten l auth abel l prodect li2an l prodect ya3tamed 3le
),
ChangeNotifierProxyProvider<Auth, Products>(
// 2 provider product ya3tamed 3ala auth
update: (ctx, value, previousProduct) => Products(value.token,
previousProduct==null? []: previousProduct.productsList),
),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<Auth>(
builder: (ctx, value, _) => MaterialApp(
theme: ThemeData(
primaryColor: Colors.orange,
canvasColor: Color.fromRGBO(255, 238, 219, 1)),
debugShowCheckedModeBanner: false,
home: value.isAuth? MyHomePage(): AuthScreen(),
),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isLoading = true;
#override
void initState() {
Provider.of<Products>(context, listen: false)
.fetchData()
.then((_) => _isLoading = false)
.catchError((onError) => print(onError));
super.initState();
}
Widget detailCard(id, tile, desc, price, imageUrl) {
return Builder(
builder: (innerContext) => FlatButton(
onPressed: () {
print(id);
Navigator.push(
innerContext,
MaterialPageRoute(builder: (_) => ProductDetails(id)),
).then(
(id) => Provider.of<Products>(context, listen: false).delete(id));
},
child: Column(
children: [
SizedBox(height: 5),
Card(
elevation: 10,
color: Color.fromRGBO(115, 138, 119, 1),
child: Row(
children: <Widget>[
Expanded(
flex: 3,
child: Container(
padding: EdgeInsets.only(right: 10),
width: 130,
child: Hero(
tag: id,
child: Image.network(imageUrl, fit: BoxFit.fill),
),
),
),
Expanded(
flex: 3,
child: Column(
//mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 10),
Text(
tile,
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold),
),
Divider(color: Colors.white),
Container(
width: 200,
child: Text(
desc,
style: TextStyle(color: Colors.white, fontSize: 14),
softWrap: true,
overflow: TextOverflow.fade,
textAlign: TextAlign.justify,
maxLines: 3,
),
),
Divider(color: Colors.white),
Text(
"\$$price",
style: TextStyle(color: Colors.black, fontSize: 18),
),
SizedBox(height: 13),
],
),
),
Expanded(flex: 1, child: Icon(Icons.arrow_forward_ios)),
],
),
),
],
),
),
);
}
#override
Widget build(BuildContext context) {
List<Product> prodList =
Provider.of<Products>(context, listen: true).productsList;
return Scaffold(
appBar: AppBar(title: Text('My Products')),
body: _isLoading
? Center(child: CircularProgressIndicator())
: (prodList.isEmpty
? Center(
child: Text('No Products Added.',
style: TextStyle(fontSize: 22)))
: RefreshIndicator(
onRefresh: () async =>
await Provider.of<Products>(context, listen: false)
.fetchData(),
child: ListView(
children: prodList
.map(
(item) => detailCard(item.id, item.title,
item.description, item.price, item.imageUrl),
)
.toList(),
),
)),
floatingActionButton: Container(
width: 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
color: Theme.of(context).primaryColor,
),
child: FlatButton.icon(
label: Text("Add Product",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 19)),
icon: Icon(Icons.add),
onPressed: () => Navigator.push(
context, MaterialPageRoute(builder: (_) => AddProduct())),
),
),
);
}
}
product.dart
class Product {
final String id;
final String title;
final String description;
final double price;
final String imageUrl;
Product({
#required this.id,
#required this.title,
#required this.description,
#required this.price,
#required this.imageUrl,
});
}
class Products with ChangeNotifier {
List<Product> productsList = [];
final String authToken;
Products(this.authToken, this.productsList);
Future<void> fetchData() async {
final url = "https://flutter-app-568d3.firebaseio.com/product.json?auth=$authToken";
try {
final http.Response res = await http.get(url);
final extractedData = json.decode(res.body) as Map<String, dynamic>;
extractedData.forEach((prodId, prodData) {
final prodIndex =
productsList.indexWhere((element) => element.id == prodId);
if (prodIndex >= 0) {
productsList[prodIndex] = Product(
id: prodId,
title: prodData['title'],
description: prodData['description'],
price: prodData['price'],
imageUrl: prodData['imageUrl'],
);
} else {
productsList.add(Product(
id: prodId,
title: prodData['title'],
description: prodData['description'],
price: prodData['price'],
imageUrl: prodData['imageUrl'],
));
}
});
notifyListeners();
} catch (error) {
throw error;
}
}
Future<void> updateData(String id) async {
final url = "https://flutter-app-568d3.firebaseio.com/product/$id.json?auth=$authToken";
final prodIndex = productsList.indexWhere((element) => element.id == id);
if (prodIndex >= 0) {
await http.patch(url,
body: json.encode({
"title": "new title 4",
"description": "new description 2",
"price": 199.8,
"imageUrl":
"https://cdn.pixabay.com/photo/2015/06/19/21/24/the-road-815297__340.jpg",
}));
productsList[prodIndex] = Product(
id: id,
title: "new title 4",
description: "new description 2",
price: 199.8,
imageUrl:
"https://cdn.pixabay.com/photo/2015/06/19/21/24/the-road-815297__340.jpg",
);
notifyListeners();
} else {
print("...");
}
}
Future<void> add(
{String id,
String title,
String description,
double price,
String imageUrl}) async {
final url = "https://flutter-app-568d3.firebaseio.com/product.json?auth=$authToken";
try {
http.Response res = await http.post(url,
body: json.encode({
"title": title,
"description": description,
"price": price,
"imageUrl": imageUrl,
}));
print(json.decode(res.body));
productsList.add(Product(
id: json.decode(res.body)['name'],
title: title,
description: description,
price: price,
imageUrl: imageUrl,
));
notifyListeners();
} catch (error) {
throw error;
}
}
Future<void> delete(String id) async {
final url = "https://flutter-app-568d3.firebaseio.com/product/$id.json?auth=$authToken";
final prodIndex = productsList.indexWhere((element) => element.id == id);
var prodItem = productsList[prodIndex];
productsList.removeAt(prodIndex);
notifyListeners();
var res = await http.delete(url);
if (res.statusCode >= 400) {
productsList.insert(prodIndex, prodItem);
notifyListeners();
print("Could not deleted item");
} else {
prodItem = null;
print("Item deleted");
}
}
}
auth.dart
class Auth with ChangeNotifier {
String _token;
DateTime _expireDate;
String _userId;
bool get isAuth{
return token != null;
}
String get token{
if(_expireDate != null && _expireDate.isAfter(DateTime.now()) && _token != null){
return _token;
}return null;
}
Future<void> _authenticate(
String email, String password, String urlSegment) async {
final url =
"https://identitytoolkit.googleapis.com/v1/accounts:$urlSegment?key=AIzaSyAN1Z9ibJ9-HqBen65x5ALKiqsRTzFfhh8";
try {
final res = await http.post(url,
body: json.encode({
'email': email,
'password': password,
'returnSecureToken': true,
}));
print(json.decode(res.body));// byetba3 lma3lomat l rej3a ya3ni resalet l error
final resData = json.decode(res.body);
// to print the error
if(resData['error'] != null){
throw "${resData['error']['message']}";
}
_token = resData['idToken'];
_userId = resData['localId'];
_expireDate = DateTime.now().add(Duration(seconds: int.parse(resData['expiresIn'])));
notifyListeners();
} catch (e) {
throw e;
}
}
Future<void> signUp(String email, String password) async {
return _authenticate(email, password, "signUp");
}
Future<void> login(String email, String password) async {
return _authenticate(email, password, "signInWithPassword");
}
}
auth_screen.dart
enum AuthMode { SignUp, Login }
class AuthScreen extends StatelessWidget {
static const routeName = '/auth';
#override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
return Scaffold(
// resizeToAvoidBottomInset: false,
body: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color.fromRGBO(215, 117, 255, 1).withOpacity(0.5),
Color.fromRGBO(255, 188, 117, 1).withOpacity(0.9),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0, 1],
),
),
),
SingleChildScrollView(
child: Container(
height: deviceSize.height,
width: deviceSize.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
child: Container(
margin: EdgeInsets.only(bottom: 20.0),
padding:
EdgeInsets.symmetric(vertical: 8.0, horizontal: 94.0),
transform: Matrix4.rotationZ(-8 * pi / 180)
..translate(-10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.deepOrange.shade900,
boxShadow: [
BoxShadow(
blurRadius: 8,
color: Colors.black26,
offset: Offset(0, 2),
)
],
),
child: Text(
'MyShop',
style: TextStyle(
color: Colors.white,
fontSize: 40,
),
),
),
),
Flexible(
flex: deviceSize.width > 600 ? 2 : 1,
child: AuthCard(),
),
],
),
),
),
],
),
);
}
}
class AuthCard extends StatefulWidget {
const AuthCard({
Key key,
}) : super(key: key);
#override
_AuthCardState createState() => _AuthCardState();
}
class _AuthCardState extends State<AuthCard> {
final GlobalKey<FormState> _formKey = GlobalKey();
AuthMode _authMode = AuthMode.Login;
Map<String, String> _authData = {
'email': '',
'password': '',
};
var _isLoading = false;
final _passwordController = TextEditingController();
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text("An Error Occurred"),
content: Text(message),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.of(ctx).pop();
},
child: Text("OK")),
],
));
}
Future<void> _submit() async{
if (!_formKey.currentState.validate()) {
// Invalid!
return;
}
_formKey.currentState.save();
setState(() {
_isLoading = true;
});
try {
if (_authMode == AuthMode.Login) {
// Log user in
await Provider.of<Auth>(context, listen: false)
.login(
_authData['email'],
_authData['password'],
);
} else {
// Sign user up
await Provider.of<Auth>(context, listen: false)
.signUp(
_authData['email'],
_authData['password'],
);
}
} catch (error) {
var errorMessage = "Authentication failed";
if(error.toString().contains('EMAIL_EXISTS')){
errorMessage = 'This email address is already taken';
}else if(error.toString().contains('INVALID_EMAIL')){
errorMessage = 'This is not a valid email address';
}else if(error.toString().contains('WEAK_PASSWORD')){
errorMessage = 'This password is too weak';
} else if(error.toString().contains('EMAIL_NOT_FOUND')){
errorMessage = 'Could not find a user with that email';
} else if(error.toString().contains('INVALID_PASSWORD')){
errorMessage = 'Invalid password';
}
_showErrorDialog(errorMessage);
}
setState(() {
_isLoading = false;
});
}
void _switchAuthMode() {
if (_authMode == AuthMode.Login) {
setState(() {
_authMode = AuthMode.SignUp;
});
} else {
setState(() {
_authMode = AuthMode.Login;
});
}
}
#override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
elevation: 8.0,
child: Container(
height: _authMode == AuthMode.SignUp ? 320 : 260,
constraints:
BoxConstraints(minHeight: _authMode == AuthMode.SignUp ? 320 : 260),
width: deviceSize.width * 0.75,
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: 'E-Mail'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value.isEmpty || !value.contains('#')) {
return 'Invalid email!';
}
return null;
},
onSaved: (value) {
_authData['email'] = value;
},
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
controller: _passwordController,
validator: (value) {
if (value.isEmpty || value.length < 5) {
return 'Password is too short!';
}
return null;
},
onSaved: (value) {
_authData['password'] = value;
},
),
if (_authMode == AuthMode.SignUp)
TextFormField(
enabled: _authMode == AuthMode.SignUp,
decoration: InputDecoration(labelText: 'Confirm Password'),
obscureText: true,
validator: _authMode == AuthMode.SignUp
? (value) {
if (value != _passwordController.text) {
return 'Passwords do not match!';
}
return null;
}
: null,
),
SizedBox(
height: 20,
),
if (_isLoading)
CircularProgressIndicator()
else
RaisedButton(
child:
Text(_authMode == AuthMode.Login ? 'LOGIN' : 'SIGN UP'),
onPressed: _submit,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
padding:
EdgeInsets.symmetric(horizontal: 30.0, vertical: 8.0),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.button.color,
),
FlatButton(
child: Text(
'${_authMode == AuthMode.Login ? 'SIGN UP' : 'LOGIN'} INSTEAD'),
onPressed: _switchAuthMode,
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 4),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
textColor: Theme.of(context).primaryColor,
),
],
),
),
),
),
);
}
}
Compiler
Launching lib\main.dart on SM G610F in debug mode...
Running Gradle task 'assembleDebug'...
√ Built build\app\outputs\flutter-apk\app-debug.apk.
Installing build\app\outputs\flutter-apk\app.apk...
Waiting for SM G610F to report its views...
Debug service listening on ws://127.0.0.1:60889/J5CQX-fKfbs=/ws
Syncing files to device SM G610F...
I/zygote (32524): Do partial code cache collection, code=30KB, data=25KB
I/zygote (32524): After code cache collection, code=30KB, data=25KB
I/zygote (32524): Increasing code cache capacity to 128KB
D/libGLESv2(32524): STS_GLApi : DTS, ODTC are not allowed for Package : com.example.animation
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 0
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 1
D/InputMethodManager(32524): SSI - flag : 0 Pid : 32524 view : com.example.animation
V/InputMethodManager(32524): Starting input: tba=android.view.inputmethod.EditorInfo#9dca947 nm : com.example.animation ic=io.flutter.plugin.editing.InputConnectionAdaptor#271e574
D/InputMethodManager(32524): startInputInner - Id : 0
I/InputMethodManager(32524): startInputInner - mService.startInputOrWindowGainedFocus
D/InputTransport(32524): Input channel constructed: fd=100
D/InputTransport(32524): Input channel destroyed: fd=91
D/ViewRootImpl#886418b[MainActivity](32524): MSG_RESIZED: frame=Rect(0, 0 - 1080, 1920) ci=Rect(0, 72 - 0, 873) vi=Rect(0, 72 - 0, 873) or=1
D/ViewRootImpl#886418b[MainActivity](32524): Relayout returned: old=[0,0][1080,1920] new=[0,0][1080,1920] result=0x1 surface={valid=true 3430410240} changed=false
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 0
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 1
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 0
I/zygote (32524): Do partial code cache collection, code=61KB, data=61KB
I/zygote (32524): After code cache collection, code=61KB, data=61KB
I/zygote (32524): Increasing code cache capacity to 256KB
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 1
V/InputMethodManager(32524): Starting input: tba=android.view.inputmethod.EditorInfo#fb092e3 nm : com.example.animation ic=io.flutter.plugin.editing.InputConnectionAdaptor#981e4e0
D/InputMethodManager(32524): startInputInner - Id : 0
I/InputMethodManager(32524): startInputInner - mService.startInputOrWindowGainedFocus
D/InputTransport(32524): Input channel constructed: fd=94
D/InputTransport(32524): Input channel destroyed: fd=100
W/IInputConnectionWrapper(32524): getExtractedText on inactive InputConnection
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 0
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 1
D/InputMethodManager(32524): SSI - flag : 0 Pid : 32524 view : com.example.animation
V/InputMethodManager(32524): Starting input: tba=android.view.inputmethod.EditorInfo#391b65e nm : com.example.animation ic=io.flutter.plugin.editing.InputConnectionAdaptor#54f7a3f
D/InputMethodManager(32524): startInputInner - Id : 0
I/InputMethodManager(32524): startInputInner - mService.startInputOrWindowGainedFocus
D/InputTransport(32524): Input channel constructed: fd=97
D/InputTransport(32524): Input channel destroyed: fd=94
D/InputMethodManager(32524): HSIFW - flag : 0 Pid : 32524
D/ViewRootImpl#886418b[MainActivity](32524): MSG_RESIZED: frame=Rect(0, 0 - 1080, 1920) ci=Rect(0, 72 - 0, 0) vi=Rect(0, 72 - 0, 0) or=1
D/ViewRootImpl#886418b[MainActivity](32524): Relayout returned: old=[0,0][1080,1920] new=[0,0][1080,1920] result=0x1 surface={valid=true 3430410240} changed=false
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 0
D/ViewRootImpl#886418b[MainActivity](32524): ViewPostIme pointer 1
I/flutter (32524): {kind: identitytoolkit#VerifyPasswordResponse, localId: urJPn2l2EBeHNGJ7MmcMMkCIJtf1, email: ali#gmail.com, displayName: , idToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImM0ZWFhZjkxM2VlNWY0MDY0YmE2NjUzN2M0Njk3YzY5OGE3NGYwODIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vZmx1dHRlci1hcHAtYjM0ZTAiLCJhdWQiOiJmbHV0dGVyLWFwcC1iMzRlMCIsImF1dGhfdGltZSI6MTYxNDg4MzQwMywidXNlcl9pZCI6InVySlBuMmwyRUJlSE5HSjdNbWNNTWtDSUp0ZjEiLCJzdWIiOiJ1ckpQbjJsMkVCZUhOR0o3TW1jTU1rQ0lKdGYxIiwiaWF0IjoxNjE0ODgzNDAzLCJleHAiOjE2MTQ4ODcwMDMsImVtYWlsIjoiYWxpQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJhbGlAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.XfeiI4-UqDcaugW1mQxaoQbrk-h_NclPANgKKt0w36H6Jj32mGdUV-pZ_WdXdfMRxnF9lJEt9nfe0FgQSVKyYk8oLxkEaqlNJME1BnF5wUvHEJsqpHsG89mFz77cYWw-jYcjvquvPlJYSiwIc-snEyinRPx5DZwpbIkWtGe-oqWzJyE23C5inh0yu0O52tLrEDtBHy5m9qBvCK-3HLHKJ1Phrz0Q06qdH8cxw_3-AdRPqZQ8w8F4FhVOyrB04NEqWIBs-uvjAkbjo6y3LlM5PqLebrhDSdKlUlbVYOVPotMp0UMkcDRqkuZDlFZoMRoU
D/ViewRootImpl#886418b[MainActivity](32524): Relayout returned: old=[0,0][1080,1920] new=[0,0][1080,1920] result=0x1 surface={valid=true 3430410240} changed=false
I/flutter (32524): type 'String' is not a subtype of type 'int' of 'index'
D/ViewRootImpl#886418b[MainActivity](32524): MSG_WINDOW_FOCUS_CHANGED 0
D/ViewRootImpl#886418b[MainActivity](32524): MSG_WINDOW_FOCUS_CHANGED 1
I need a help please:
what i need to know is where is the error and how to solve it.
thank you in advance

Unable to identify source of exception error: Flutter

Image of code that occurs with error
I am going through a udemy Flutter course and I do not understand why the try catch cannot handle the error with the if statement inside inside of the try{}. I have had this problem for this a couple of times and I have just avoided it by running error-free trials.
The code is as follows:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../models/http_exception.dart';
class Auth with ChangeNotifier {
String _token;
//tokens only last about an hour
DateTime _expiryDate;
String _userId;
Future<void> _authenticate(
String email, String password, String urlSegment) async {
final url = 'UrlIsCorrectInMyCode'; //changed on purpose
try {
final response = await http.post(
url,
body: json.encode(
{
'email': email,
'password': password,
'returnSecureToken': true,
},
),
);
final responseData = json.decode(response.body);
if(responseData['error'] != null){
throw HttpException(responseData['error']['message']);
}
} catch (error) {
throw error;
}
}
Future<void> signUp(String email, String password) async {
return _authenticate(email, password, 'signUp');
//in order for the progress indicator to be shown
//it must be returned so that it takes the future of
//_authenticate and not just any future
}
Future<void> login(String email, String password) async {
return _authenticate(email, password, 'signInWithPassword');
}
}
And the class that registers UI and uses Auth:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth.dart';
import '../models/http_exception.dart';
enum AuthMode { Signup, Login }
class AuthScreen extends StatelessWidget {
static const routeName = '/auth';
#override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
// final transformConfig = Matrix4.rotationZ(-8 * pi / 180);
// transformConfig.translate(-10.0);
return Scaffold(
// resizeToAvoidBottomInset: false,
body: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color.fromRGBO(215, 117, 255, 1).withOpacity(0.5),
Color.fromRGBO(255, 188, 117, 1).withOpacity(0.9),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0, 1],
),
),
),
SingleChildScrollView(
child: Container(
height: deviceSize.height,
width: deviceSize.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
child: Container(
margin: EdgeInsets.only(bottom: 20.0),
padding:
EdgeInsets.symmetric(vertical: 8.0, horizontal: 94.0),
transform: Matrix4.rotationZ(-8 * pi / 180)
..translate(-10.0),
//transform allows to rotate or scale a box
//the '..' operator allows you to return the matrix4
//object that rotationZ provides instead of the void
//response that .translate provides
// ..translate(-10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.deepOrange.shade900,
boxShadow: [
BoxShadow(
blurRadius: 8,
color: Colors.black26,
offset: Offset(0, 2),
)
],
),
child: Text(
'MyShop',
style: TextStyle(
color:
Theme.of(context).accentTextTheme.headline6.color,
fontSize: 50,
fontFamily: 'Anton',
fontWeight: FontWeight.normal,
),
),
),
),
Flexible(
flex: deviceSize.width > 600 ? 2 : 1,
child: AuthCard(),
),
],
),
),
),
],
),
);
}
}
class AuthCard extends StatefulWidget {
const AuthCard({
Key key,
}) : super(key: key);
#override
_AuthCardState createState() => _AuthCardState();
}
class _AuthCardState extends State<AuthCard> {
final GlobalKey<FormState> _formKey = GlobalKey();
AuthMode _authMode = AuthMode.Login;
Map<String, String> _authData = {
'email': '',
'password': '',
};
var _isLoading = false;
final _passwordController = TextEditingController();
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text("An error occured"),
content: Text(message),
actions: [
FlatButton(
child: Text("Okay"),
onPressed: () {
Navigator.of(ctx).pop();
},
),
],
),
);
}
Future<void> _submit() async {
if (!_formKey.currentState.validate()) {
// Invalid!
return;
}
_formKey.currentState.save();
setState(() {
_isLoading = true;
});
try {
if (_authMode == AuthMode.Login) {
await Provider.of<Auth>(context, listen: false).login(
_authData['email'],
_authData['password'],
);
} else {
await Provider.of<Auth>(context, listen: false).signUp(
_authData['email'],
_authData['password'],
);
}
} on HttpException catch (error) {
//this means a special type of error is thrown
//this acts as a filter for the type of error
var errorMessage = 'Authentication failed.';
if (error.toString().contains("EMAIL_EXISTS")) {
errorMessage = 'This email address is already in use';
} else if (error.toString().contains('INVALID_EMAIL')) {
errorMessage = 'This is not a valid email address.';
} else if (error.toString().contains('WEAK_PASSWORD')) {
errorMessage = 'This password is too weak.';
} else if (error.toString().contains('EMAIL_NOT_FOUND')) {
errorMessage = 'Could not find a user with that email.';
} else if (error.toString().contains('INVALID_PASSWORD')) {
errorMessage = 'Invalid password.';
}
_showErrorDialog(errorMessage);
} catch (error) {
const errorMessage = 'Could not authenticate. Please try again.';
_showErrorDialog(errorMessage);
}
setState(() {
_isLoading = false;
});
}
void _switchAuthMode() {
if (_authMode == AuthMode.Login) {
setState(() {
_authMode = AuthMode.Signup;
});
} else {
setState(() {
_authMode = AuthMode.Login;
});
}
}
#override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
elevation: 8.0,
child: Container(
height: _authMode == AuthMode.Signup ? 320 : 260,
constraints:
BoxConstraints(minHeight: _authMode == AuthMode.Signup ? 320 : 260),
width: deviceSize.width * 0.75,
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: 'E-Mail'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value.isEmpty || !value.contains('#')) {
return 'Invalid email!';
} else {
return null;
}
},
onSaved: (value) {
_authData['email'] = value;
},
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
controller: _passwordController,
validator: (value) {
if (value.isEmpty || value.length < 5) {
return 'Password is too short!';
} else {
return null;
}
},
onSaved: (value) {
_authData['password'] = value;
},
),
if (_authMode == AuthMode.Signup)
TextFormField(
enabled: _authMode == AuthMode.Signup,
decoration: InputDecoration(labelText: 'Confirm Password'),
obscureText: true,
//this allows stars to mask the input
validator: _authMode == AuthMode.Signup
? (value) {
if (value != _passwordController.text) {
return 'Passwords do not match!';
} else {
return null;
}
}
: null,
),
SizedBox(
height: 20,
),
if (_isLoading)
CircularProgressIndicator()
else
RaisedButton(
child:
Text(_authMode == AuthMode.Login ? 'LOGIN' : 'SIGN UP'),
onPressed: _submit,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
padding:
EdgeInsets.symmetric(horizontal: 30.0, vertical: 8.0),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.button.color,
),
FlatButton(
child: Text(
'${_authMode == AuthMode.Login ? 'SIGNUP' : 'LOGIN'} INSTEAD'),
onPressed: _switchAuthMode,
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 4),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
//reduces the amount of surface area that is tappable
textColor: Theme.of(context).primaryColor,
),
],
),
),
),
),
);
}
}