In my app, I have the following providers.
#override
Widget build(BuildContext context) {
return OverlaySupport.global(
child: MultiProvider(
providers: [userLoggedIn, currentUserData],
child: MaterialApp(...)))
}
var userLoggedIn = StreamProvider<User?>.value(
value: FirebaseAuth.instance.authStateChanges(), initialData: null);
var currentUserData = StreamProvider<FrediUser>.value(
updateShouldNotify: (_, __) => true,
initialData: FrediUser(
loginProvider: '',
email: '',
admin: false,
profileSettings: [],
profileChips: [],
profileStats: [],
id: 'loading',
imageUrl: 'loading',
bio: 'loading',
username: 'loading'),
value: currentUserID != null ? currentUserDataStream() : null,
);
PROBLEM
When the user logs out (or logs in for the first time), the provider is either:
Containing old user data (until a hot restart is done, when the providers are called again and reloaded)
Null or empty, because there was no user before.
What I want to do is to refresh or call the Stream Providers again once I have a new user, or delete all the data once a user logs off.
Thank you!
You can listen to the changes of auth state like this.
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
I've been facing a similar problem as you are, I've come up with a work-around although not sure how "valid" it is according to the Provider architecture
The Problem
I've got a DatabaseService class which has a stream function of type Stream<CustomUser> function, and I used it like this:
//--- main.dart ---//
runApp(MultiProvider(
providers: [
// ..some other providers.. //
// data provider
Provider<DatabaseService?>(
create: (_) => databaseService,
),
// data provider
StreamProvider<CustomUser?>(
create: (context) => databaseService.getCurrUserFromDb(),
lazy: false,
initialData: null,
updateShouldNotify: (_, __) => true,
),
],
child: MyApp(
initPage: initPage,
)
));
Stream Function:
//--- database_service.dart ---//
// gets the user from database and
// assigns it to the variable _user.
Stream<CustomUser?> getCurrUserFromDB() async* {
try {
CustomUser? currUser;
if (_user != null) {
await for (DocumentSnapshot<Object?> event
in users.doc(user.uid).snapshots()) {
final jsonMap = event.data() as Map<String, dynamic>;
currUser = CustomUser.fromJson(jsonMap);
_user = currUser;
CustomPreferences.setCurrUser(_user);
yield currUser;
}
}
} catch (e) {
rethrow;
}
}
databaseService is the DatabaseService class with named constructors.
This was not causing the widgets to rebuild at the start nor when the stream has a new value
Solution:
Created a StreamController in the DatabaseService class, and when the user signs in I add the stream function:getCurrUserFromDB() to the StreamController like this
//--- authentication_screen.dart ---//
...
ElevatedButton(
child: const Text("Sign In"),
onPressed: () async {
final user = await AuthService().googleSignIn();
if (user != null) {
final dbProvider = context.read<DatabaseService?>();
await dbProvider?.setInitUser(user, context);
await dbProvider?.cusUserController
.addStream(dbProvider.getCurrUserFromDB());
}
}),
...
setInitUser(CustomUser? user) is used set the value of the _user variable in DatabaseService and user is used to get this variable.
Reasoning
I am creating a StreamProvider at the start of the app, and its source the StreamController needs to have a stream to listen so I give it when I am trying to sign in.
Or even cleaner solution would be to do it in the constructor of DatabaseService Class like this:
//--- database_service.dart ---//
// method to add the stream to controller //
Future<void> addStream() async {
if (!_cusUserController.isClosed && !_cusUserController.isPaused) {
await _cusUserController.addStream(getCurrUserFromDB());
}
}
// constructor //
DatabaseService._init(CustomUser cusUser) {
addStream();
_user = cusUser;
}
And one last thing to note is that I don't make the declare the Controller as final. When I had it declared as final the streams weren't updating, so it looks like this now:
//--- database_service.dart ---//
StreamController<CustomUser?> _cusUserController = StreamController();
TL;DR
I created a StreamProvider which returns a StreamController in its create property and later down the life cycle of the app I gave the controller a Stream using the addStream method.
Sorry for the wall of text I just wanted to come out as clear as possible.
Related
I have a webview on a Pageview who display a chat from a plugin that I use on my wordpress website (I have no access to data from this plugin). It's not a chat with FB or google account, it's only an open chat room, where users can add and save her nickname (I suppose nickname is stored in cookies ?). As long as the webview is active the nickname remains memorized. Problem, after each time the app is close and reopen, the user lose his nickname.
Here is my code
WebView(
initialUrl: 'https://XXXX',
javascriptMode: JavascriptMode.unrestricted,
gestureRecognizers: [
Factory(() => PlatformViewVerticalGestureRecognizer()),
].toSet(),
),
How can I save session ? Even when after app is close and reopen ?
First, in your website project, add this javascript code which it will be accessible to the HTML pseodo input:
var psuedoInput = document.querySelector('inputSelectorHere');
_selector.addEventListener('change', function(event) {
var message = psuedoInput.value;
if (messageHandler) {
messageHandler.postMessage(message);
}
});
you can add it inside a <script></script> in the .html file or in a .js separate file.
this basically will post a message with the pseudo input value to our app later.
Don't forget to change inputSelectorHere with your psuedo input selector.
now in your flutter code, create a simple Stirng variable like this:
String? cookie;
then in the WebView widget:
WebView(
javascriptChannels: <JavascriptChannel>[
// javascript channel that saves the cookie
JavascriptChannel(
name: 'Cookie',
onMessageReceived: (JavascriptMessage message) {
cookie = message.message;
print("cookie: $cookie");
},
),
].toSet(),
onWebViewCreated: (controller) {
if (cookie == null) {
return;
}
controller.runJavascript("document.cookie = '$cookie';");
// }
},
initialCookies: [],
initialUrl: 'https://XXXX',
javascriptMode: JavascriptMode.unrestricted,
),
here the JavascriptChannel is set so it receives those messages which will be sent from your website from the webview, then it will be saved inside the cookie variable which we created.
when you close the webview and open it again, the onWebViewCreated will be called, and the cookie now is not null, so it will assign the cookie we saved to document.cookie in the webview.
As I can understand. You just need to get cookies (or cache and local storage) and store them in FlutterSecureStorage. when the user closes the app and re-opens just check if cookies are stored in FlutterSecureStorage or not.
If Cookies are present just add the cookies and refresh the page. I have written a pseudo code for the demo purpose (Code might not work as you expected but it will give you a brief idea about my approach).
I have added a code for the cookies. I also added code for the cache and local storage but you have to configure it according to your needs.
Please read the comments.
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:webview_flutter/webview_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const FlutterWebViewDemo());
}
class FlutterWebViewDemo extends StatefulWidget {
const FlutterWebViewDemo({Key? key}) : super(key: key);
#override
State<FlutterWebViewDemo> createState() => _FlutterWebViewDemoState();
}
class _FlutterWebViewDemoState extends State<FlutterWebViewDemo> {
late final WebViewCookieManager cookieManager = WebViewCookieManager();
var controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
// Update loading bar.
},
onPageStarted: (String url) {},
onPageFinished: (String url) {},
onWebResourceError: (WebResourceError error) {},
),
)
..loadRequest(Uri.parse(''));
/// <---- please add the url here
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
Expanded(child: WebViewWidget(controller: controller)),
ElevatedButton(
onPressed: () async {
_onListCache();
},
child: const Text("Save Cache"),
),
ElevatedButton(
onPressed: () async {
_onAddToCache();
},
child: const Text("Set Cache"),
),
ElevatedButton(
onPressed: () async {
_onClearCache();
},
child: const Text("Clear Cache"),
),
ElevatedButton(
onPressed: () async {
_onListCookies();
},
child: const Text("Save Cookies"),
),
ElevatedButton(
onPressed: () async {
_onSetCookie();
},
child: const Text("Set Cookies"),
),
ElevatedButton(
onPressed: () async {
_onClearCookies();
},
child: const Text("Clear Cookies"),
)
],
),
),
);
}
Future<void> _onListCookies() async {
final String cookies = await controller
.runJavaScriptReturningResult('document.cookie') as String;
FlutterSecureStorage secureStorage = const FlutterSecureStorage();
secureStorage.write(key: 'cookies', value: cookies);
}
Future<void> _onSetCookie() async {
FlutterSecureStorage secureStorage = const FlutterSecureStorage();
String? cookies = await secureStorage.read(key: 'cookies');
/// get cookies from flutter secure storage and set them and refresh the page with new cookies.
/// please fill the required fields.
await cookieManager.setCookie(
WebViewCookie(
name: '',
/// required you have to set this
value: cookies!,
domain: '',
/// required
path: '/',
/// required
),
);
/// this will load the new page
await controller.loadRequest(Uri.parse(
'',
/// <---- refresh url
));
}
Future<void> _onClearCookies() async {
final bool hadCookies = await cookieManager.clearCookies();
String message = 'There were cookies. Now, they are gone!';
if (!hadCookies) {
message = 'There are no cookies.';
}
print(">>>>>>>>>> message $message");
}
Future<void> _onAddToCache() async {
/// <--- you have to write the logic to add cache and local storage from flutter secure storage. like this and refresh the page.
await controller.runJavaScript(
'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";',
);
}
Future _onListCache() async {
await controller.runJavaScriptReturningResult('caches.keys()');
/// <--- get cache and local storage and save it in flutter secure storage.
}
Future<void> _onClearCache() async {
await controller.clearCache();
await controller.clearLocalStorage();
}
}
I have a very tricky situation, which I've reproduced in a demo.
I have a Provider of a user, with this method of updating the listeners:
class User extends ChangeNotifier {
...
User({required this.uid}) {
Database().getUser(uid).listen(
(user) async {
displayName = user?.displayName;
email = user?.email;
phoneNumber = user?.phoneNumber;
photoURL = user?.photoURL;
did = user?.did;
interests = user?.interests;
notifyListeners();
},
onError: (e) => print(e),
);
}
...
}
My main.dart starts like this:
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthState>.value(value: _authState),
ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel())
],
child: Consumer<AuthState>(
builder: (context, auth, child) {
var user =
auth.authUser == null ? null : User(uid: auth.authUser!.uid);
return MultiProvider(
providers: [
ChangeNotifierProvider<ZUser?>.value(
value: zuser,
),
],
child: MaterialApp.router(...
This has been sufficient for my use case thus far.
Now, I wish to make an update to the interests field;
I have a DB widget that does:
Future updateUser(String uid, Map<String, Object?> data) async {
return userCollection.doc(uid).update(data);
}
Where the userCollection is my collection in Firestore.
I call this class from my view widget, as:
ZWideButton(
text: "Save",
onPressed: () async {
setState(() {
_localEdit = false;
_loading = true;
});
await user.saveInterests(_interests());
setState(() => _loading = false);
},
),
Where saveInterests is:
Future saveInterests(List<String> interests) async {
return _db.updateUser(uid, {"interests": interests});
}
None of this presents any problem at first -- I can update the interests and it works fine. That is, until I keep updating the interests, and it gets slower and slower each time (the browser says the download time gets longer and longer) and seemingly my computer is eating up more and more memory until the webpage ultimately crashes.
Something of a memory leak appears to be happening, but I'm unsure what about flutter web and firebase could be causing it. I believe it may have to do with the Provider package not disposing appropriately. It does not seem to be the provider as I don't see the Widget being rebuilt over and over. Looking for some thoughts.
For anyone looking; My issue is that my json deserializer was causing an infinite loop with the firebase listener
I am trying to create a streaming app.
I am integrating Firebase Dynamic links, so hosts can share a link to join their session.
But I am kinda lost on how to implement it, and I would like to know your insights about this.
So let me start with the structure, this is the main pages:
MainApp
Authenticate
LoginPage
HomePage
Profile
JoinPage
This is how I setup my app, they are all wrap to Authenticate widget, this widget determines if user should be redirected to login or the home page. Pretty simple eh. :)
Then here comes the Dynamic links.
My dynamic links has a query params on it to determine which session to join. It looks like this:
https://my.app.link/join-session?sessionId=\<some random keys>
And this is what I want to handle it.
If (user is not logged in ) {
// save the sessionId to a singleton (or somewhere that persists along the app lifecycle, I use Provider here)
// redirects user to login page
// user logged in
// upon going to home page, will retrieve the saved sessionId and redirect user to the session using the sessionId
} else {
// retrieve sessionId
// redirect user to the session using the sessionId
}
This is how my code looks:
MainApp.dart logic
....
if (state == PageLoadState.DONE) {
return MultiProvider(
providers: [
Provider<SampleAppAuthProvider>(
create: (_) => SampleAppAuthProvider(FirebaseAuth.instance),
),
Provider<SampleAppConfigProvider>(
create: (_) => SampleAppConfigProvider(sharedPrefs)
),
StreamProvider(
initialData: null,
create: (context) => context.read<SampleAppAuthProvider>().authState,
),
Provider<DynamicLinkService>(create: (_) => DynamicLinkService(instance: FirebaseDynamicLinks.instance)),
ChangeNotifierProvider(create: (_) => UsersApi(repo: Repository('users')), lazy: true),
ChangeNotifierProvider(create: (_) => SessionsApi(repo: Repository('sessions')), lazy: true),
ChangeNotifierProvider(create: (_) => MessagesApi(repo: Repository('messages')), lazy: true),
],
child: MaterialApp(
title: 'SampleApp!',
theme: defaultTheme(),
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
onGenerateRoute: router.generateRoute,
home: Authenticate(),
),
);
}
......
And this is my Authenticate Page:
class Authenticate extends StatelessWidget {
#override
Widget build(BuildContext context) {
context.read<DynamicLinkService>().handleDynamicLinks();
final firebaseUser = context.read<User>();
if (firebaseUser != null) {
return HomePage();
}
return LoginPage();
}
}
I have also create a separate service for handling the dynamic links:
DynamicLinkService.dart
class DynamicLinkService {
DynamicLinkService({#required this.instance});
final FirebaseDynamicLinks instance;
String _sessionIdToJoin = '';
get sessionIdToJoin => _sessionIdToJoin;
void clearSessionId () => _sessionIdToJoin = '';
Future handleDynamicLinks() async {
final PendingDynamicLinkData data =
await instance.getInitialLink();
_handleDeepLink(data);
instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
_handleDeepLink(dynamicLink);
}, onError: (OnLinkErrorException e) async {
print('Link Failed: ${e.message}');
});
}
void _handleDeepLink(PendingDynamicLinkData data) {
final Uri deepLink = data?.link;
if (deepLink != null) {
print('_handleDeepLink | deeplink: $deepLink');
bool isJoiningSession = deepLink.pathSegments.contains('session');
if(isJoiningSession) {
String sessionId = deepLink.queryParameters['sessionId'];
if (sessionId != null) {
_sessionIdToJoin = sessionId;
}
}
}
}
}
I also have another UtilService that generates dynamic links:
Utils.dart
class Utils {
....
static Future<String> generateLink(String sessionId) async {
final DynamicLinkParameters parameters = DynamicLinkParameters(
uriPrefix: AppConfig.dynamicLinkBaseUrl,
link: Uri.parse('${AppConfig.dynamicLinkBaseUrl}/join-session?sessionId=$sessionId'),
dynamicLinkParametersOptions: DynamicLinkParametersOptions(
shortDynamicLinkPathLength: ShortDynamicLinkPathLength.unguessable
),
androidParameters: AndroidParameters(
packageName: 'com.sample.app',
minimumVersion: 0,
),
);
final Uri dynamicUrl = await parameters.buildUrl();
final ShortDynamicLink shortenedLink =
await DynamicLinkParameters.shortenUrl(
dynamicUrl,
DynamicLinkParametersOptions(
shortDynamicLinkPathLength: ShortDynamicLinkPathLength.unguessable),
);
final Uri shortUrl = shortenedLink.shortUrl;
return AppConfig.dynamicLinkBaseUrl + shortUrl.path;
}
...
}
On my home page I have this method that will should be called whenever user lands in the homepage:
HomePage.dart:
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
....
_navigateToDestination() {
DynamicLinkService _service = context.select((DynamicLinkService service) => service);
String sessionToJoin = _service.sessionIdToJoin;
User user = context.read<User>();
if(sessionToJoin != '') {
_service.clearSessionId();
Navigator.pushNamed(context,
AppRoutes.joinStream,
arguments: StreamLiveArguments(
channelName: sessionToJoin,
user: AppUtil.getName(user),
role: ClientRole.Audience
)
);
}
}
#override
initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_navigateToDestination();
});
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(final AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_navigateToDestination();
}
}
....
}
The issue on this implementation is that sometimes it works and sometimes it does not work and I am not sure why. :(
Should I just use a simple class instead of a provider? If yes, how can I persists the sessionId?
Other Scenarios that I want to handle is when user is in different page like the Profile Page.
At this point, user can minimize the app and can click the dynamic link from other sites or from messenger.
When the app resumes, it will open in the ProfilePage where it doesn't have handling for dynamic links (only HomePage and LoginPage has handling on it). How can I remedy this? Should I add handling of dynamic links to all my pages? Or perhaps can I create a dynamic link that can redirect to a certain page in my app? If yes, how can I do that?
I am kinda new in implementing the dynamic links (as well as Providers) and would like your opinion on this issue.Thanks in advance and I hope someone can help me or give me some insights on best practices on how to do this.
New to booth flutter and stackoverflow.
I am making the account verification functionally for my flutter app. My plan is to divided this functionally into two parts, part one shows an alertdialog when the screen is built, and part two checks if the "activated" field in firestore is true or false. I have problem of making part two.
This is what I write for part one
String uid = "fdv89gu3njgnhJGBh";
bool isActivated = false;
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
if (isActivated == false) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: AlertDialog(
title: Text("Activation pending"),
content: Text("Your account is waiting to be activate by admin"),
actions: [
FlatButton(
child: Text("Refresh"),
onPressed: () {
// just bring reassurance to user
},
),
],
),
);
});
}
});
}
For part two I plan to make a Future return type function, what it will do is to subscribe the boolean value that stored in firestore: /user/uid/activated, once the function gets a "true" from firestore, it will return it to part one and part one will close the alertdialog(which I haven't figure out how to do this).
I've already seen some solutions from the internet but most solutions involve StreamBuilder, but it seems that I don't need to build any widget for the stream in part two. Is it better to just make changes to what I write previously* or integrate both parts two one StreamBuilder function?
*What I wrote for get the data from one field among all files (and this works well):
Future<bool> registeredCheck(String email) async {
var userInfo = await _firestore.collection("user").get();
for (var userInf in userInfo.docs) {
if (userInf.data()["email"] == email) {
return true;
}
}
return false;
}
}
Thank you
You don't have to query the entire collection. Since you already know the uid, you can just get the document of the uid directly like this:
Future<bool> registeredCheck(String email) async {
final userDoc = await _firestore.collection("user").doc(uid).get();
return userDoc.data()['activated'] ?? false;
}
The reason why I am adding ?? false is to return false instead of null when the activated value is null;
In my app, I listen to changes from a User Document in Cloud Firestore.
I do this by getting the current user ID, and then getting the document associated with that ID.
class UserService {
...
//GET A USER'S INFORMATION AS A STREAM
// ? IF NO UID IS PASSED, IT GETS THE INFO OF THE CURRENT USER
Stream<User> getUserInfoAsStream({String uid}) async* {
if (uid == null) {
uid = await AuthService().getUID();
}
yield* Firestore.instance
.collection('users')
.document(uid)
.snapshots()
.map((doc) => User.fromFirestore(doc));
}
...
I then use a StreamProvider to listen to the stream in my main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<User>.value(
value: UserService().getUserInfoAsStream(),
),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: SplashScreen(),
),
);
}
}
During the course of the app's lifecycle, it works perfectly, but when the user signs out using FirebaseAuth.instance.signOut(); and then logs in with a different user, the stream remains constant (i.e it listens to the old uid stream), and the StreamProvider doesn't listen to the new stream of data.
| Sign Out Code For Reference |
// ? SIGN OUT CODE: If user signed out, it returns true, else, false
Future<bool> signOut() async {
try {
await _firebaseAuth.signOut();
return true;
} catch (error) {
print(error);
return false;
}
}
| Where it is used |
FlatButton(
onPressed: () {
AuthService().signOut().then((value) =>
Navigator.of(context).pushAndRemoveUntil(
CupertinoPageRoute(
builder: (BuildContext context) {
return Onboarding();
}), (route) => false));
},
child: Text("Yes")),
To solve the problem, I would've passed the current uid to the StreamProvider instead, but I can only get the current uid asynchronously.
How can I listen to an asynchronous stream using the StreamProvider, and update it when the user changes?
EDIT: I managed to fix the problem to some extent by moving the provider up the widget tree to the screen immediately after the login page. But because providers are scoped, I had to create a completely new MaterialApp after my original MaterialApp which is messing up my some components in my app.
Is there any better workaround?
I managed to fix the problem by switching from the provider package to get_it.
get_it allows you to register and unregister singletons, meaning that when a user logs in, I can register the singleton so it can be used across all screens that depend on it. Then, when I logout, I simply unregister it. That way, the User is always updated after signing in and out.
Here's how to do it yourself.
Install the package get_it in your pubspec.yaml.
get_it: ^4.0.2
Create a new file next to your main.dart called locator.dart. Inside it, add this code:
GetIt locator = GetIt.instance;
void setupLocator() {
// Replace this with the object you're trying to listen to.
User user;
Stream<User> userStream = UserService().getUserInfoAsStream();
userStream.listen((event) => user = event);
locator.registerLazySingleton(() => user); // Register your object
}
When you login, just call setupLocator(); and when you log out, use this code:
locator.unregister<User>();
That's all I did to get it up and running!
Edit: I managed to make it even better and lighter by using a UserProvider Singleton that listens to changes in Authentication and then gets the current user when a user logs in.
import 'package:planster/models/core/user.dart';
import 'package:planster/models/services/auth_service.dart';
import 'package:planster/models/services/user_service.dart';
class UserProvider {
// SINGLETON INITIALIZATION
static final UserProvider _singleton = UserProvider._internal();
factory UserProvider.instance() {
return _singleton;
}
UserProvider._internal() {
listenToUserAuthState();
}
// VARIABLES
User user;
void listenToUserAuthState() async {
AuthService().onAuthStateChanged.listen((event) {
uid = event.uid;
if (uid != null)
UserService().getUserInfoAsStream(uid: uid).listen((userEvent) {
user = userEvent;
});
});
}
}