Appwrite Realtime subscription Flutter riverpod problem when unsubscribe to subscription - flutter

Appwrite realtime subscription dispose of (unsubscribe) issue with flutter_riverpod. By using this for a single document it is working. But the problem is when I am fetching multiple documents and subscribing to the doc When the page gets closed and the provider gets disposed of the Subscription gets closed appwrite RealtimeSubscription shows this error.
All documents that are subscribe exist in Database Collection
The Current user has permission to read the docs
All Documents in the Same Collection
Appwrite instance in a singleton class so no duplication
Which I already tried:
Appwrite endpoint set With HTTPS and HTTP
Self Signed TRUE and FALSE
flutter: AppwriteRealtime: Allow self-signed certificate
flutter: subscription: wss://10.100.110.13/v1/realtime?project=63b04724be0f82751c50
[ERROR: flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: AppwriteException: null, Missing channels (1008)
Here is the provider:
final testProvider = StateNotifierProvider.family
.autoDispose<TestNotifier, BracketStats, String>((ref, bracketID) {
return TestNotifier(ref, docID: bracketID);
});
class TestNotifier extends StateNotifier<BracketStats> {
final Ref ref;
final String docID;
RealtimeSubscription? _subscription;
TestNotifier(this.ref, {required this.docID}) : super(BracketStats.empty());
Future getData() async {
final k = await ref.read(BracketRepository.getBracketStats(docID).future);
state = k ?? BracketStats.empty();
}
void subscribe() {
final realtime = ref.watch(AppwriteDependencies.realtime);
_subscription = realtime
.subscribe(['databases.bracket_db.collections.stats.documents.$docID']);
if (_subscription != null) {
_subscription?.stream.listen((response) {
final bracketStats = BracketStats.fromJson(response.payload);
state = bracketStats;
});
}
}
#override
void dispose() {
if (_subscription != null) {
_subscription?.close();
}
super.dispose();
}
}

Related

Firestore charging for document snapshot in Riverpod provider

I have setup a riverpod provider in order to listen to a document in firestore as shown below.
My assumption was that the document is only read once after the start of the app and then only read / charged when the content has changed.
When I run the firestore emulator it shows me frequent GET calls against this object.
Is there a way to stream a single object without document reads or do I have to use a query snapshot for that?
final habitProvider =
StateNotifierProvider<ObjectRepository, AsyncValue<Object>>(
(ref) => ObjectRepository(ref.read));
class ObjectRepository extends StateNotifier<AsyncValue<Object>> {
final Reader _reader;
StreamSubscription<Object>? _subscription;
ObjectRepository(this._reader) : super(AsyncData(Object.empty())) {
state = const AsyncLoading();
try {
_subscription?.cancel();
_subscription = getObject().listen((event) {
state = AsyncData(event);
});
} catch (e) {
state = AsyncError(e);
}
}
Stream<HabitCollection> getObject() {
final docReference =
_reader(firebaseFirestoreProvider).collection("x").doc("y");
final snapshot = docReference.snapshots();
return snapshot.map((snapshot) {
if (snapshot.data() == null) {
return Object.empty();
} else {
return Object.fromJson(
snapshot.data() as Map<String, dynamic>);
}
});
}
void dispose() {
_subscription?.cancel();
super.dispose();
}
}

Keep the user logged in flutter (The app has 2 different login and main, one for Client and one for Driver)

I am doing an app in flutter and I am working on the authentication part. I want to know how I can keep my user logged in after I reload the app. Now the thing is that my app has 2 kinds of users (Client and Driver). So each has its own space, like sign in and sign up and main (after logging in).
This is the code that I used for logging.
class Initializer extends StatefulWidget {
// Access to this Screen
static String id = 'initializer';
#override
_InitializerState createState() => _InitializerState();
}
class _InitializerState extends State<Initializer> {
// Firebase Stuff
final _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
User _user;
// To Check if There's a Driver
bool isDriver = true;
void getCurrentUser() async {
try {
final getCurrentUser = _auth.currentUser;
if (getCurrentUser != null) {
getUserKind();
_user = getCurrentUser;
}
} catch (e) {
print(e);
}
}
getUserKind() async {
try {
// To fetch Database for Driver
final QuerySnapshot checkOfDriver =
await _firestore.collection('driver').where('uid', isEqualTo: _user.uid).get().catchError((error) {
print(error);
});
if (checkOfDriver.docs.isEmpty)
setState(() {
isDriver = false;
});
else
setState(() {
isDriver = true;
});
} catch (e) {
print(e);
return null;
}
}
#override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
#override
void initState() {
super.initState();
getCurrentUser();
}
#override
Widget build(BuildContext context) {
getCurrentUser();
SizeConfig().init(context);
return _user == null
? WelcomeScreen()
: isDriver
? DriverMain()
: ClientMain();
}
}
It's actually working but not properly, because when I reload the app while I'm logging in as a Client, the app shows me DriverMain at the beginning for one second then it switches to the right side which is ClientMain and that causes me some errors sometimes, and it's not an efficient work anyway.
So, what I should add to the code or ...
Firebase already persists the users credentials, and restores them automatically when the app restarts.
But this is an asynchronous process, as it requires a call to the server. By the time your getCurrentUser = _auth.currentUser code runs, that asynchronous process hasn't finished yet, so you get null.
To properly respond to the auth state being restored (and other changes), you'll want to use an auth state change listener as shown in the documentation on authentication state:
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
If you want to use this in your UI, you'll typically wrap it in a StreamBuilder instead of calling listen yourself.

Bad state: Stream has already been listened to flutter provider [duplicate]

This question already has answers here:
Flutter : Bad state: Stream has already been listened to
(20 answers)
Closed 1 year ago.
I'm using Provider to loading json data and refresh app state when any operation performed.
This is my Provider ChangeNotifier
class ClassesDetails with ChangeNotifier {
Future<void> getLiveClassList(String token) async {
final url = FitApis.demoBaseURL + "live/class";
try {
final response = await http.get(url, headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': token,
});
//print(response.body);
final responseData = json.decode(response.body);
if (responseData['data'] != null) {
notifyListeners();
Map typesMap = jsonDecode(response.body);
var types = new LiveClassListModel.fromJson(typesMap);
_liveClassController.add(types);
}
} catch (error) {
throw (error);
}
}
final _liveClassController = StreamController<LiveClassListModel>();
Stream<LiveClassListModel> get liveClassStream {
return _liveClassController.stream;
}
void refreshLiveClasses(String token) {
getLiveClassList(token);
}
}
This is getIt used for access provider objects
final getIt = GetIt.instance;
void setup() {
getIt.registerSingleton<ClassesDetails>(ClassesDetails());
}
Method call to get data
Future<void> getLiveClassList() async {
final classProvider = getIt<ClassesDetails>();
classProvider.getLiveClassList("token)";
classProvider.liveClassStream.listen((snapshot) { });
}
Refreshing data
classProvider
.refreshLiveClasses("token");
This is working perfectly. But the issue is that when i logout from app and login again it gives me error Bad state: Stream has already been listened to. I searched alot but not get helpful solution. I tried Flutter : Bad state: Stream has already been listened to but not working. I tried to classProvider.closeStream() but not working. Please help me how I solve this issue.
I just created a same stream and found the issue, just change your StreamController like this:
final _liveClassController = StreamController<LiveClassListModel>.broadcast();

How to close Functions in ChangeNotifier Provider Flutter

How to close a Function without disposing it. I needy this answer because when I log out, I need to close the functions in ChangeNotifier Class.
This Is my ChangeNotifier Class:
class ChatAndRequestProvider extends ChangeNotifier {
bool _areThereNewChatsAndRequests = false;
bool get areThereNewChatsAndRequests => _areThereNewChatsAndRequests;
set areThereNewChatsAndRequests(bool value) {
_areThereNewChatsAndRequests = value;
notifyListeners();
}
List _chatsList = [];
List get chatsList => _chatsList;
set chatsList(List list) {
_chatsList = list;
notifyListeners();
}
getChats() async {
var prefs = await SharedPreferences.getInstance();
print('The getChats Id is ${prefs.getString(kUserId)}');
FirebaseDatabase.instance
.reference()
.child('users')
.child(prefs.getString(kUserId))
.child('friendsArray')
.onValue
.listen((snapshot) {
Map list = snapshot.snapshot.value;
print('map is $list');
var newItems = [];
if (list != null) {
list.forEach((key, value) {
newItems.add(value);
});
chatsList = newItems;
var globalArray = [];
for (var item in newItems) {
if (item[kLastTimestamp] != item[kLastTimestampSeen]) {
areThereNewChatsAndRequests = true;
}
var status;
switch (item['friendsStatus']) {
case 'friends':
status = RequestStatus.alreadyAFriend;
break;
case 'notFriends':
status = RequestStatus.noRequest;
break;
case 'blocked':
status = RequestStatus.userThatBlockedMe;
break;
case 'unblocked':
status = RequestStatus.noRequest;
break;
}
globalArray.add({kUserId: item[kUserId], kTypeOfRequest: status});
}
valuesList = globalArray;
} else {
deleteFromList(null, RequestStatus.alreadyAFriend);
chatsList = [
{kUserId: 'null'}
];
}
});
}
So for example when I log In as user1 and I call this function in the LoadingScreen() I get all of users that are my friends, and I can go to the chats screen List and chat with my friends. Up to this point there is no issue. But when I log out and when I log in with another account lets say user2 and I call this function again, then I get error and two responses because I am calling this function twice. I am not using Auth Packet, I have my own database on MongoDB where I store user Info, but requests and chats are stored on RealTime Database.
So my Question is:
When user1 logs out of my app, I can not call dispose() on provider because if he wants to log in again to another account, he will get an error because Provider was disposed, so how can I stop listening to my database when user logs out and call this function again. Thank You very Much!!
I´m not sure if this works because I don't fully understand the flow of your app but you say that
I can not call dispose() on provider because if he wants to log in
again to another account,
when the users logs out shouldn't the app return to the first screen disposing the provider? (unless you create it in the MaterialApp, I'm not sure about that either). You could save the instance of the Firebase listener and then close it when you log out/ dispose the provider
var _myListener;
getChats() async {
var prefs = await SharedPreferences.getInstance();
print('The getChats Id is ${prefs.getString(kUserId)}');
_myListener = FirebaseDatabase.instance
.reference()
.child('users')
.child(prefs.getString(kUserId))
.child('friendsArray')
.onValue
.listen((snapshot) ...
....
/// The rest of your code
}
void closeListener(){ //call it when the user logs out
_myListener?.close();
}
#override
void dispose(){
closeListener(); // or call it in the dispose if you want
//to dispose and create a new provider when the user logs out/ sign in
super.dispose();
}

Firebase Dynamic Link is not caught by getInitialLink if app is closed and opened by that link

Programmatically generated dynamic links are not properly catched by
FirebaseDynamicLinks.instance.getInitialLink().
if the app is closed. However, if the app is open it is properly detected by the listener for new incoming dynamic links. It is not clear to me if it is a setup problem, how I generate the dynamic link.
To Reproduce
First set up Firebase for Flutter project as documented. Then to set up a dynamic link:
/// See also
/// https://firebase.google.com/docs/dynamic-links/use-cases/rewarded-referral
/// how to implement referral schemes using Firebase.
Future<ShortDynamicLink> buildDynamicLink(String userId) async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
final String packageName = packageInfo.packageName;
var androidParams = AndroidParameters(
packageName: packageInfo.packageName,
minimumVersion: Constants.androidVersion, // app version and not the Android OS version
);
var iosParams = IosParameters(
bundleId: packageInfo.packageName,
minimumVersion: Constants.iosVersion, // app version and not the iOS version
appStoreId: Constants.iosAppStoreId,
);
var socialMetaTagParams = SocialMetaTagParameters(
title: 'Referral Link',
description: 'Referred app signup',
);
var dynamicLinkParams = DynamicLinkParameters(
uriPrefix: 'https://xxxxxx.page.link',
link: Uri.parse('https://www.xxxxxxxxx${Constants.referralLinkPath}?${Constants.referralLinkParam}=$userId'),
androidParameters: androidParams,
iosParameters: iosParams,
socialMetaTagParameters: socialMetaTagParams,
);
return dynamicLinkParams.buildShortLink();
}
This dynamic link then can be shared with other new users.
I listen for initial links at app startup and then for new incoming links.
1) The link properly opens the app if the app is not running but the getInitialLink does not get it.
2) If the app is open the link is properly caught by the listener and all works.
Here is the very simple main.dart that I used to verify 1) that the initial link is not found with FirebaseDynamicLinks.instance.getInitialLink().
void main() async {
WidgetsFlutterBinding.ensureInitialized();
PendingDynamicLinkData linkData = await FirebaseDynamicLinks.instance.getInitialLink();
String link = linkData?.link.toString();
runApp(MyTestApp(link: link));
}
class MyTestApp extends StatelessWidget {
final String link;
MyTestApp({this.link});
#override
Widget build(BuildContext context) {
return MaterialApp(
builder: (BuildContext context, Widget child) {
return Scaffold(
body: Container(
child: Center(
child: Text('Initial dynamic Firebase link: $link')
),
),
);
}
);
}
}
Expected behavior
The link should open the app and trigger FirebaseDynamicLinks.instance.getInitialLink()..
Additional context
I hope properly configured Firebase project with Firebase console. To verify this I created a dynamic link to be used with Firebase Auth 'signup by email link' and these dynamic links are working as expected, also when the app is not open.
The point here is that the referral dynamic link that I generate programmatically is opening the app when it is closed but is then not caught by FirebaseDynamicLinks.instance.getInitialLink(), and to make things more confusing, works as expected if the app is open. In that case it is caught by the listener FirebaseDynamicLinks.instance.onLink.
I also set up the WidgetsBindingObserver in Flutter to handle that callback as required, when the app gets its focus back.
Any help is greatly appreciated. Debugging is very tricky, as you need to do it on a real device and not in the simulator. To make things worse, I did not figure out how to attach a debugger while the dynamic link opens the app. This means I am also stuck in investigating this issue further.
In The FirebaseDynamicLinks Two Methods 1) getInitialLink() 2) onLink().
If When Your App Is Open And You Click On Dynamic Link Then Will Be Call FirebaseDynamicLinks.instance.onLink(), If Your App Is Killed Or Open From PlayStore Then You Get From FirebaseDynamicLinks.instance.getInitialLink();.
First Of You Need To Initialise Instance Of FirebaseDynamicLinks.instance.
static void initDynamicLinks() async {
final PendingDynamicLinkData data =
await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
if (deepLink != null && deepLink.queryParameters != null) {
SharedPrefs.setValue("param", deepLink.queryParameters["param"]);
}
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
final Uri deepLink = dynamicLink?.link;
if (deepLink != null && deepLink.queryParameters != null) {
SharedPrefs.setValue("param", deepLink.queryParameters["param]);
}
}, onError: (OnLinkErrorException e) async {
print(e.message);
});
}
Initialize Link Listener. This works for me.
class _MainAppState extends State<MainApp> {
Future<void> initDynamicLinks() async {
print("Initial DynamicLinks");
FirebaseDynamicLinks dynamicLinks = FirebaseDynamicLinks.instance;
// Incoming Links Listener
dynamicLinks.onLink.listen((dynamicLinkData) {
final Uri uri = dynamicLinkData.link;
final queryParams = uri.queryParameters;
if (queryParams.isNotEmpty) {
print("Incoming Link :" + uri.toString());
// your code here
} else {
print("No Current Links");
// your code here
}
});
// Search for Firebase Dynamic Links
PendingDynamicLinkData? data = await dynamicLinks
.getDynamicLink(Uri.parse("https://yousite.page.link/refcode"));
final Uri uri = data!.link;
if (uri != null) {
print("Found The Searched Link: " + uri.toString());
// your code here
} else {
print("Search Link Not Found");
// your code here
}
}
Future<void> initFirebase() async {
print("Initial Firebase");
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// await Future.delayed(Duration(seconds: 3));
initDynamicLinks();
}
#override
initState() {
print("INITSTATE to INITIALIZE FIREBASE");
super.initState();
initFirebase();
}
I tried Rohit's answer and because several people face the same issue I add here some more details. I created a stateful widget that I place pretty much at the top of the widget tree just under material app:
class DynamicLinkWidget extends StatefulWidget {
final Widget child;
DynamicLinkWidget({this.child});
#override
State<StatefulWidget> createState() => DynamicLinkWidgetState();
}
class DynamicLinkWidgetState extends State<DynamicLinkWidget> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
locator.get<DynamicLinkService>().initDynamicLinks();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(child: widget.child);
}
}
I use the getit package to inject services. The dynamic link service is roughly like this:
class DynamicLinkService {
final UserDataService userDataService;
final ValueNotifier<bool> isLoading = ValueNotifier<bool>(false);
final BehaviorSubject<DynamicLinkError> _errorController = BehaviorSubject<DynamicLinkError>();
Stream<DynamicLinkError> get errorStream => _errorController.stream;
DynamicLinkService({#required this.userDataService});
void initDynamicLinks() async {
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
if (deepLink != null) {
processDynamicLink(deepLink);
}
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
final Uri deepLink = dynamicLink?.link;
if (deepLink != null) {
print('=====> incoming deep link: <${deepLink.toString()}>');
processDynamicLink(deepLink);
}
},
onError: (OnLinkErrorException error) async {
throw PlatformException(
code: error.code,
message: error.message,
details: error.details,
);
}
);
}
Future<void> processDynamicLink(Uri deepLink) async {
if (deepLink.path == Constants.referralLinkPath && deepLink.queryParameters.containsKey(Constants.referrerLinkParam)) {
var referrer = referrerFromDynamicLink(deepLink);
userDataService.processReferrer(referrer);
} else {
await FirebaseEmailSignIn.processDynamicLink(
deepLink: deepLink,
isLoading: isLoading,
onError: this.onError
);
}
}
void onError(DynamicLinkError error) {
_errorController.add(error);
}
}
You see that my app has to process two types of dynamic link, one is for email link signup, the other link is our referral link that is used to link users together and allow us to understand who introduced a new user to us. This setup works now for us. Hope it helps others too.