I use the health package in my App to sync data between my app and Google Fit / Apple Health. So far I have only testet Apple Health and it works perfectly fine if I grant all the necessary permissions. But I found one weird thing and I can't figure out how to fix it. So far I have only implemented and tested the integration with Apple Health.
So, I check if the app has the permission to read or write a certain data type, and then I read it or write it to Apple Health. If the permission was granted and the read / write operation was successful, I want to present that to the user. If the permission was not granted, I also want to present that to the user, so he knows what went wrong. In the HealthFactory class of the package there is the requestAuthorization method that I call to check whether the user has permission or not. The behavior I expected was that, if the permission is not granted, the return value of that method is false if the permission is not granted. But instead, it is always true. Also, the methods getHealthDataFromTypes and writeHealthData don't really indicate whether the permission is granted or not.
I implemented a few methods to showcase that:
class TestScreen extends StatelessWidget {
TestScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
child: Text("Health Permission"),
onPressed: () async {
final HealthFactory _health = HealthFactory();
await Permission.activityRecognition.request();
bool permission = await _health.requestAuthorization(
[HealthDataType.WEIGHT],
permissions: [HealthDataAccess.READ_WRITE],
);
print(permission);
},
),
ElevatedButton(
child: Text("Read Data"),
onPressed: () async {
final HealthFactory _health = HealthFactory();
DateTime untilDate = DateTime.now();
DateTime fromDate = DateTime(1970);
try {
List<HealthDataPoint> healthData =
await _health.getHealthDataFromTypes(
fromDate,
untilDate,
[HealthDataType.WEIGHT],
);
return print(healthData.toString());
} on Exception catch (e) {
print(e.toString());
rethrow;
}
},
),
ElevatedButton(
child: Text("Write Data"),
onPressed: () async {
final HealthFactory _health = HealthFactory();
try {
bool success = await _health.writeHealthData(
80,
HealthDataType.WEIGHT,
DateTime.now(),
DateTime.now(),
);
print(success);
} on Exception catch (e) {
print(e.toString());
}
},
),
],
),
),
);
}
}
This is my test screen.
The first button requests READ_WRITE permission for the datatype WEIGHT. At the first call, this window pops up:
When I click on "Don't Allow" in the top left corner, the permissions are NOT granted, but the method still returns true. If I go to the settings, no matter if I activate the permission or not, it always returns true.
The second button is supposed to read WEIGHT data from Apple Health. If the permission is granted, everything works fine. If the permission is NOT granted, it still "works", but jsut returns an empty list. This is pretty bad, because I can't indicate whether it failed or Apple Health just doesn't have any WEIGHT data.
The third button is supposed to write WEIGHT data into Apple Health. If the permission is granted, again, everthing works fine. If ther permission is NOT granted, the mehod returns false and doesn't write anything. This is already a better indicator than from the READ operation, but still, I can't indicate whether the permission wasn't granted or it was some other error.
So my question is: Is there any way to indicate at code level whether the permissions have been granted or not? I really need this to present to the user if the sync worked, and if not, why it didn't work.
Please take a look at the documentation https://pub.dev/documentation/health/latest/health/HealthFactory/hasPermissions.html please also verify but on IOS it looks like it returns null. Apple protects this fields and the package returns null as Apple HealthKit will not disclose if READ access has been granted for a data type due to privacy concern, this method can only return null to represent an undertermined status, if it is called on iOS with a READ or READ_WRITE access.:
You are right, it seems like HealthKit do not allow us to access the permission status. I personally consider that the user did not toggled the permission if I can't get its steps over the past month:
Future<bool> hasPermissionsOniOS() async {
final now = DateTime.now();
final lastMonth = DateTime(now.year, now.month - 1, now.day);
final stepsOfLastMonth =
await health.getTotalStepsInInterval(lastMonth, now);
return stepsOfLastMonth != null;
}
Related
I have an app where when I logout with Firebase Auth, user's datas are still there, and I need to Hot restart the app to totally remove them.
I didn’t find a way to improve the signout method, so I want to try to rebuild app when user logout, or maybe delete the cache so user’s datas will no longer exist in app.
I have done some research but I can’t find any solution that can help me.
EDIT : Here's how I logout
Future<void> signOut() async {
await FirebaseAuth.instance
.signOut()
.then((value) => print('Sign Out'))
.onError((error, stackTrace) => print('Error in signed out'));
}
IconButton(
onPressed: () async {
await signOut();
Navigator.of(context, rootNavigator: true)
.pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return const OnBoardingPage();
},
),
(_) => false,
);
},
icon: const Icon(Icons.logout))
I know that user's data are still there because I can display user.email in my onBoardingPage (where technically no data can be there because I logout previously).
Here a preview of my onBoardingPage :
After that, if I want to connect with another account, I will be connected to the previous user's session. The same if I want to create a new account, all new user's data will be into the previous connected user. To fix this problem, in development mode, I need to hot restart the app.
It seems like the user is not totally logout.
I have this code showing the Retry button:
TextButton(
onPressed: () async {
await _checkPermission().then(
(hasGranted) {
if (hasGranted == PermissionStatus.granted) {
refreshContacts();
}
},
);
},
...
Future<PermissionStatus> _checkPermission() async {
final Permission permission = Permission.contacts;
final status = await permission.request();
return status;
}
This always returns:
PermissionStatus.permanentlyDenied
but it doesn't open the permission question again.
Info.plist:
<key>NSContactsUsageDescription</key>
<string>This app requires access to display the contacts list. This will be used to import contacts automatically if necessary to the app contacts list.</string>
If a permission is permanently denied, it can only be enabled from the application settings (bar reinstalling the app). You can use the openAppSettings function to redirect the user to the settings.
Fixed by this issue on its repo: https://github.com/Baseflow/flutter-permission-handler/issues/718#event-5560838657
as per the current version of permission handler you need to add contact permission in Podfile and plist both files make sure you have added this.
I am attempted to update an avatar on my app and then load and display it once done. However, I am seeing the following errors which seem to indicate a false positive or race condition when the image has actually finished uploading.
I'm using a CicleAvatar widget, but also attempted with NetworkImage and am experiencing the same issues. I have also attempted .then/onComplete and various others outside of a delayed or wrapping it in a completer.
What is the best way to handle Firebase storage upload and immediate download without error§
Example Error n attempting to retrieve the image from the DownloadURLL:
════════ Exception caught by image resource service
════════════════════════════ HTTP request failed, statusCode: 503,
!isImageProcessing
? GestureDetector(
onTap: () => _uploadAvatarImage(),
child: CircleAvatar(
minRadius: 40,
backgroundColor: Colors.grey,
backgroundImage: NetworkImage(user.imageURL),
),
)
: Center(
child: CircularProgressIndicator(),
),
The actual upload of the file is being managed in this function/class
class StorageController {
static Future<String> storeAvatarImage(File file) async {
// Get user UUID to reference avatar;
String uuid = await identityBloc.retrieveActiveUUID();
String downloadURL;
TaskSnapshot ts;
ts = await firebase_storage.FirebaseStorage.instance
.ref('avatars/$uuid-avatar.png')
.putFile(file);
downloadURL = await ts.ref.getDownloadURL();
User user = await ProfileDataController.retrieveUserProfile();
user.imageURL = downloadURL;
await ProfileDataController.createUserProfile(user);
downloadURL = downloadURL;
return downloadURL;
}
}
I think you are not properly awaiting for the file upload. Can you change this line to read:
ts = await firebase_storage.FirebaseStorage.instance
.ref('avatars/$uuid-avatar.png')
.putFile(file);
// removed the below part
// .snapshot;
The image would update if there are listeners to listen to changes in the changed user avatar.
What I would advise as a workaround is store the avatarUrl to firestore or rtdb, there you can set a listener that updates the UI on the frontend when a change is written there.
Initially, the avatarUrl field would be null then when a user uploads a new picture the field is then a string and you can supply it to your UI
I am trying to get a bool value from Firestore when the app is being initialized: it returns True if it is "like" and False if it is not "like". Every time a user likes/unlikes a post, a database (called userFavorites) is being created or update on Firestore. The userFavorite database is composed of: document (user's ID), collection ('posts'), document (post's ID), collection (isLiked: true OR isLiked: false). So when initializing the app, I'm trying to get access to this True/False for each of the posts that are being displayed on the UI (if the user has never liked or unliked the post, the value for this bool will automatically be False).
I would really appreciate if you can give me feedback/corrections on the code I use to get the True/False bool value from Firestore, because even though I am not getting any errors, the bool value on my IU is Null, and I don't know whether I made an error in this part of my code or in another part.
Here is the code I used:
class HomeFeed extends StatelessWidget {
final user = FirebaseAuth.instance.currentUser;
ValueKey valueKey;
Future<DocumentSnapshot> getDocumentSnapshotForCurrentUserLikes(String userId, String documentId) async {
final String userId = user.uid;
final String documentId = valueKey.value;
return await FirebaseFirestore.instance.collection('userFavorites').doc(userId).collection('posts').doc(documentId).get();
}
bool getCurrentUserLikesValue(DocumentSnapshot documentSnapshotForCurrentUserLikes) {
return documentSnapshotForCurrentUserLikes.data()['isLiked'];
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('post').doc('Post in Feed').collection('posts').orderBy('createdAt', descending: true,).snapshots(),
builder: (ctx, AsyncSnapshot<QuerySnapshot> postSnapshot) {
if (postSnapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
final postDocs = postSnapshot.data.docs;
return ListView.builder(
reverse: false,
itemCount: postDocs.length,
itemBuilder: (ctx, index) {
ValueKey valueKey = ValueKey(postDocs[index].id);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: PostContainer(
user.uid,
postDocs[index].data()['likes'],
getCurrentUserLikesValue(postDocs[index]) == null ? false : getCurrentUserLikesValue(postDocs[index]),
key: valueKey,
),
),
);
},
);
},
);
}
}
Thank you for the input! Do you mean using something like this:
IconButton(
icon: Icon(
isLiked == true ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
size: 25.0,
),
onPressed: () async{
DocumentReference docRef = FirebaseFirestore.instance.collection('userFavorites').doc(this.widget.userId);
DocumentSnapshot doc = await docRef.get();
List userLikedPosts = doc.data['userLikedPosts'];
if(userLikedPosts.contains(documentId)==true) {
docRef.update({'userLikedPosts' : FieldValue.arrayRemove([documentId])});
} else {
docRef.update({'userLikedPosts' : FieldValue.arrayUnion([documentId])});
}
If this is kind of code you are referring to, how can I use it with "set", instead of "get" (because if I use "get", the user reference would have to be created in Firebase beforehand for every user, which would be inefficient)? Also for doc.data['userLikedPosts'], I get an error for ['userLikedPosts'], which says "The operator '[]' isn't defined for the type 'Map<String, dynamic> Function()'. Try defining the operator '[]'." How would you solve this? Thanks a lot for the help!
Hello! I have been researching and working on it for a while, and the problem that I am having is that I am not able to get and display the T/F bool value from the Firestore database into the UI each time a post is initialized (or seen on the screen). What code could I use to do that? I would really appreciate your help here. Thanks!
You're making your life much harder than it should be:
Consider a db structure like this:
userFavorite (collection)
|---{userId} (document)
|--- liked_posts (Array) [array of post Ids the posts the user liked]
|--- ['postA', 'postB', 'post3131',...]
By doing it this way, for each postID, you can just check if it exists in that liked_posts array. This a cleaner way to do things. You don't need extra document and collection levels.
When the user clicks a "like" on a post, you can use ArrayUnion to add that postId to the liked_posts field of that user.
Update:
Sorry, I can't help you with Flutter code. All I can say is this:
? did the user like the post? If so, you can update the userLikedPosts (Array) field WITHOUT reading it first. With ArrayUnion, if the postId is within that array, it won't be added, if it's not there it will be added, the other elements in the Array will not be changed.
? did the user dislike the post? If so, you can update userLikedPosts (Array) field WITHOUT reading it first. With ArrayRemove, if the postId is within that array, it will be removed, if it's not there then nothing happens, the other elements in the Array will not be changed.
In your place, I would not use update():
docRef.update({'userLikedPosts' : FieldValue.arrayRemove([documentId])});
Instead, I would use set() with {merge:true}:
docRef.set( {'userLikedPosts' : FieldValue.arrayRemove([documentId])}, {merge:true} );
ArrayUnion/ArrayRemove works flawlessly with set() and won't rewrite the array. Also, if the document doesn't exist, then it will be created automatically.
Sorry I can't help you with actual Flutter code, but my main point is that you do not need to read the document containing the userLikedPosts Array when responding to LIKE/DISLIKE user actions. You only need to read it when displaying whether or not the post is liked by the user, and only on subsequent post page visits. When the user presses like, you can respond in the UI immediately and the logic above to update the db with set/merge:true and ArrayUnion.
I'm currently attempting to connect my Firebase backend to my Flutter app. Upon creating a simple get() request to Firestore I keep coming across the same error every single time:
[cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation.
The security rules that I've attempted:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2020, 11, 30);
}
}
}
allow read, write;
allow read, write: if true;
allow read, write: if request.auth.uid != null;
allow read, write: if request.auth != null;
allow read, write: if auth != null ;
I've literally tried everything under the sun with no success at reading or writing to my Database which leads me to believe that the fault lies somewhere in my code. I've checked to make sure that Firebase is correctly initialized, Firebase Core and Firebase Firestore have both been correctly initialized as well.
Is this a Firebase issue or is there a specific way that calls to Firestore need to be performed?
Get request:
fetchChatMessages() async {
var messageSnapShots =
await FirebaseFirestore.instance.collection('topicofday').get();
print('Messages: $messageSnapShots');
}
}
Initialize Firebase:
class MyApp extends StatelessWidget {
final Future<FirebaseApp> _initialization = Firebase.initializeApp();
#override
FutureBuilder build(BuildContext context) {
return FutureBuilder(
future: _initialization,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return buildMaterialApp(context);
}
return Center(
child: Container(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor,
),
),
);
},
);
}
Additional info:
Not sure if this would affect the request at all but I'm using Provider for State Management.
#DougStevenson thank you man! Turns out I was attempting to pull data from the wrong project within my Firebase console. Strange scenario but your comment encouraged me to think outside of the box.
The cause was placing the wrong GoogleService-Info.plist file from a different project inside of my Runner file.