I have a small flutter application that uses Firebase Auth to login and then uses bindStream to query a list of documents from Firestore. It works from a fresh start/hot restart, but as soon as I logout I get a firebase/firestore permission error and subsequent login's don't refresh the stream. I thought that a GetxController disposes streams created via bindStream when the view that uses the controller is disposed. In this case, when I logout I pop off all routes via Get.offAll, but it appears the stream is still active and that's when the permissions error happens. But I'm not actually sure what is happening.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
Get.put(LoginController());
Get.put(AuthController(), permanent: true);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'GetX Firebase Firestore',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SplashScreen(),
);
}
}
auth_controller.dart
class AuthController extends GetxController {
final AuthService _authService = AuthService();
AuthService get authService => _authService;
final LoginController _loginController = Get.find<LoginController>();
LoginController get loginController => _loginController;
Rxn<User> _user = Rxn<User>();
User? get user => _user.value;
#override
void onReady() async {
// bind auth state to _firebaesUser, but also give an initial value
_user = Rxn<User>(_authService.currentUser);
_user.bindStream(_authService.authState);
//run every time auth state changes
ever<User?>(_user, handleAuthChanged);
super.onReady();
}
handleAuthChanged(User? user) {
print("handleAuthChanged - ${user?.uid}");
if (user == null) {
Get.offAll(() => LoginScreen());
} else {
Get.offAll(() => HomeScreen(), binding: HomeBinding());
}
}
}
user_controller.dart
class UserController extends GetxController {
final UserRepository _userRepository = UserRepository();
final repository = UserRepository();
final users = Rx<List<FirestoreUser>>([]);
late Rx<FirestoreUser> _firestoreUser;
FirestoreUser get firestoreUser => _firestoreUser.value;
#override
void onInit() {
super.onInit();
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
_firestoreUser = Rx<FirestoreUser>(FirestoreUser.fromAuth(user));
// get user data from firestore
_firestoreUser.bindStream(_userRepository.getUserById(user.uid));
// query user collection
getAllUsers();
}
void getAllUsers() {
users.bindStream(repository.getAllUsers());
}
}
home_screen.dart
class HomeScreen extends GetView<UserController> {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("All Sample Users"),
actions: [
IconButton(
onPressed: () => Get.to(ProfileScreen()),
icon: Icon(Icons.person),
),
],
),
body: Obx(
() => ListView.builder(
itemCount: controller.users.value.length,
itemBuilder: (context, index) {
final user = controller.users.value[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(user.photoURL),
),
title: Text(user.displayName),
);
},
),
),
);
}
}
home_binding.dart
class HomeBinding extends Bindings {
#override
void dependencies() {
Get.lazyPut<UserController>(() => UserController(), fenix: true);
}
}
Related
First I created the GetxController class
final languageController = GetStorage();
var myLocal = [];
void saveLocale(List list) {
languageController.write('savedLocale', list);
}
#override
void onInit() {
List<dynamic>? savedLocale = languageController.read('savedLocale');
if (savedLocale != null) {
myLocal = savedLocale;
}
super.onInit();
}
}
Then I initialized GetStorage in main.dart
final myLocal = LanguageController().myLocal;
void main() async {
print(myLocal);
await GetStorage.init();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
translations: LocaleString(),
locale: myLocal.isNotEmpty
? Locale(myLocal[0], myLocal[1])
: Locale('en', 'US'),
debugShowCheckedModeBanner: false,
home: HomeScreen(),
);
}
}
And then in the dialog after setting the locale I writes it in storage
Future<dynamic> myMaterialDialog(BuildContext context) {
final LanguageController languageController = Get.find();
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(chooseLanguage.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
Get.back();
Get.updateLocale(Locale('en', 'US'));
languageController.saveLocale(['en', 'US']);
},
child: Text(englishLanguage.tr),
),
TextButton(
onPressed: () {
Get.back();
Get.updateLocale(Locale('ru', 'RU'));
languageController.saveLocale(['ru', 'RU']);
},
child: Text(russianLanguage.tr),
),
],
),
);
});
}
And it's not working, every time I restarted my app it's shows 1 what myLocale is empty
To check if saveLocale() method is working, I created printSavedLocale() method
void printSavedLocale() {
print(languageController.read('savedLocale'));
}
and put it to dialoge button after saveLocale() and it's printing my saved locale, but after restarting saved locale is null
use this static methods. put them anywhere in your project:
void setData(String key, dynamic value) => GetStorage().write(key, value);
int? getInt(String key) => GetStorage().read(key);
String? getString(String key) => GetStorage().read(key);
bool? getBool(String key) => GetStorage().read(key);
double? getDouble(String key) => GetStorage().read(key);
dynamic getData(String key) => GetStorage().read(key);
void clearData() async => GetStorage().erase();
I can solove this by reading from the storage directly from main.dart
final LanguageController languageController = Get.put(LanguageController());
final myLocal = LanguageController().readSavedLocale();
void main() async {
await GetStorage.init();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
translations: LocaleString(),
locale: myLocal.isNotEmpty
? Locale(myLocal[0], myLocal[1])
: Locale('en', 'US'),
debugShowCheckedModeBanner: false,
home: HomeScreen(),
);
}
}
And readSavedLocale() method is
List readSavedLocale() {
var savedLocale = languageController.read('savedLocale');
return savedLocale;
}
if you still needs this , I use my app differently but I just made it work he it my main file (minus what you don' need)
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await GetStorage.init();
await firebaseInitialization.then((value) {
Get.put(HomeController());
});
runApp(Start());
}
class Start extends StatelessWidget {
Start({
Key? key,
}) : super(key: key);
final storage = GetStorage();
#override
Widget build(BuildContext context) {
Get.put(HomeController());
print(storage.read('langCode'));
print(storage.read('countryCode'));
return GetMaterialApp(
translations: LocaleString(),
fallbackLocale: const Locale('en', 'US'),
locale: storage.read('langCode') != null
? Locale(storage.read('langCode'), storage.read('countryCode'))
: const Locale('ar', 'MA'),
title: 'title'.tr,
}));
}
}
i have a button on my drawer that switches between arabic and english, you can put it wherever you want, you just need to have the widget
class Page extends GetView<HomeController>
which gives you the value 'controller' to represent the controller responsible for the language.
and this is the button responsible for the switch:
SizedBox(
height: 70,
child: OutlinedButton(
child: ListTile(
title: Text(
'language'.tr,
style: Theme.of(context).textTheme.headline6,
textDirection: TextDirection.rtl,
),
leading: const Icon(Icons.language),
),
onPressed: () {
controller.switchLang();
},
)),
and here is my homeController which is responsible for the locale:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
class HomeController extends GetxController {
static HomeController instance = Get.find();
final storage = GetStorage();
var ar = const Locale('ar', 'MA');
var us = const Locale('en', 'US');
switchLang() {
if (Get.locale == us) {
Get.updateLocale(ar);
storage.write('langCode', 'ar');
storage.write('countryCode', 'MA');
} else {
Get.updateLocale(us);
storage.write('langCode', 'en');
storage.write('countryCode', 'US');
}
update();
}
}
in your case if you have multiple locales , just change my switchlang function to handle multiple locales, you can do that easily with a switch case
I have a MultiProvider in the main with the following code:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => ReadPreferences(),
),
ChangeNotifierProvider(
create: (context) => ItemsCrud(),
),
],
child: MaterialApp(...
I am using shared preferences to save and updated the last opened list, so the following in my ReadPreferences file:
import 'package:flutter/foundation.dart'; //To use the "ChangeNotifier"
import 'package:shared_preferences/shared_preferences.dart'; //local store
class ReadPreferences extends ChangeNotifier {
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
String openedList = '';
//Constructor method
ReadPreferences() {
getPreferences();
}
void getPreferences() async {
final SharedPreferences prefs = await _prefs;
openedList = prefs.getString('openedList');
}
Future<bool> updateOpenedList({String listTitle}) async {
final SharedPreferences prefs = await _prefs;
bool result = await prefs.setString('openedList', listTitle);
if (result == true) {
openedList = listTitle;
}
notifyListeners();
return result;
}
}
When I'm trying to update the opened list it updates in the shared Preferences file normally but it never listen to the new "openedList" value in my homepage screen.
The code I use in the homepage screen like the following:
child: Text(Provider.of<ReadPreferences>(context).openedList),
I checked many times by printing the new value inside the "ReadPreferences" files, but outside it, it keeps give me the old value not the updated one at all.
I tested with a modified Flutter Counter (default app), everything seams to be working fine. Note that I'm not calling setState() anywhere, so the only refresh is coming from the ReadPreferences class.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ReadPreferences extends ChangeNotifier {
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
String openedList = '';
//Constructor method
ReadPreferences() {
getPreferences();
}
void getPreferences() async {
final SharedPreferences prefs = await _prefs;
openedList = prefs.getString('openedList');
}
Future<bool> updateOpenedList({String listTitle}) async {
final SharedPreferences prefs = await _prefs;
bool result = await prefs.setString('openedList', listTitle);
if (result == true) {
openedList = listTitle;
}
notifyListeners();
return true;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => ReadPreferences(),
)
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
));
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(Provider.of<ReadPreferences>(context).openedList)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_counter++;
Provider.of<ReadPreferences>(context, listen: false).updateOpenedList(listTitle: (_counter).toString());
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
I finally found the answer, many thanks for #Andrija explanation. What I was doing wrong is to create a new instance from ReadPreferences() then using it for the update method, but the correct approach is to use Provider.of<ReadPreferences>(context, listen: false).updateOpenedList(listTitle: list.title); to use the update method.
For more explanation I'll add #Andrija comment hereafter:-
You are right, you should be using Provider.of. When you add Provider using ChangeNotifierProvider(create: (context) => ReadPreferences(), ) - new instance of ReadPreferences() is created, and it is kept in WidgetTree. This is the instance you want, and you get it by using Provider.of. In your code above, you created a new instance of ReadPreferences - and this is where you added a new value. This new instance has nothing to do with the one that Provider manages, and this new instance has nothing to do with your Widget.
I'm trying to make a flutter mobile app with my own back end using Django rest framework, and JWT
My flow is, after signing in i will store access token in local storage, and create a stream to update the widget (like FirebaseAuth.instance.authStateChanges()). It's working fine but after i switching screen, my stream has NO DATA, and it show login screen
Below is my creation of that stream (token_services.dart), while localStorage is from localstorage package
StreamController<String> accessTokenStreamController =
StreamController<String>.broadcast();
Stream<String> get accessTokenStream => accessTokenStreamController.stream;
void setAccessToken(String accessToken) {
localStorage.setItem('access_token', accessToken);
accessTokenStreamController.add(accessToken);
}
String getAccessToken() {
return localStorage.getItem('access_token');
}
Here is my screen (account_screen.dart):
class AccountScreen extends StatefulWidget {
#override
_AccountScreenState createState() => _AccountScreenState();
}
class _AccountScreenState extends State<AccountScreen> {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: accessTokenStream,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null)
return AuthorizedScreen();
else
return UnauthorizedScreen();
});
}
}
And the root screen:
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
int status = 0;
List<Map> screens = [
{'name': 'Home', 'widget': HomeScreen(), 'icon': Icon(Icons.home)},
{
'name': 'Account',
'widget': AccountScreen(),
'icon': Icon(Icons.account_circle)
},
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: screens[status]['widget']),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: status,
items: screens
.map((scr) =>
BottomNavigationBarItem(label: scr['name'], icon: scr['icon']))
.toList(),
onTap: (value) {
setState(() {
status = value;
});
},
),
);
}
}
Note that if i provide initialData to StreamBuilder, it's completely fine
I have a simple Provider class:
import 'package:flutter/foundation.dart';
class AppState with ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
set isLoggedIn(bool newValue) {
_isLoggedIn = newValue;
notifyListeners();
}
}
And in the login class I just set isLoggedIn to true if login is successful:
void _signInWithEmailAndPassword(appState) async {
try {
final FirebaseUser user = await _auth.signInWithEmailAndPassword(
...
);
if (user != null) {
appState.isLoggedIn = true;
appState.userData = user.providerData;
...
}
} catch (e) {
setState(() {
_errorMessage = e.message;
});
}
}
Pressing the back button on Android lets users go back to this page even after successfully logging in. So I wanted to know if Provider.of can be accessed before Widget build and redirect a user if isLoggedIn is true.
Now I have something like:
#override
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);
...
This is only one use case for the login view, but I'm sure this functionality can be used in other cases.
If you are going to use the FirebaseUser or Logged in user throughout your app, i would suggest that you add the Provider on the highest level of your app. Example
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<FirebaseUser>.value(
stream: FirebaseAuth.instance.onAuthStateChanged, // Provider here
),
],
child: MaterialApp(
title: 'My App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.green,
primarySwatch: Colors.green,
accentColor: Colors.yellow,
),
home: MainPage(),
),
);
}
}
class MainPage extends StatefulWidget {
MainPage({Key key, this.storage}) : super(key: key);
final FirebaseStorage storage;
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context); // gets the firebase user
bool loggedIn = user != null;
return loggedIn ? HomePage() : LoginPage(); // Will check if the user is logged in. And will change anywhere in the app if the user logs in
}
}
References
Fireship 185 Provider
Great Youtube video explaining the code
I have a raised button that kicks off my fingerprint authentication, when the Future returns I want to be able to change the Raised Button to new text and new onPressed method to complete the required authentication. I have given the Raised Button a key but can not find how to act upon that button to change it. Is it possible? Anyone have examples of it?
I tried to create new Raised Button with same key based on if the user is authenticated, but it did not change anything.
Any help would be great.
I would recommend reviewing the Flutter Interactivity Tutorial.
Once the Future completes you can call setState to tell Flutter to rebuild your StatefulWidget. And in your build() method, you can use the authenticated status of the user to construct a different RaisedButton.
Here's some example code that does this:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Local Auth Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _authenticated = false;
Future<Null> _authenticate() async {
final LocalAuthentication auth = new LocalAuthentication();
bool authenticated = false;
try {
authenticated = await auth.authenticateWithBiometrics(
localizedReason: 'Scan your fingerprint to authenticate',
useErrorDialogs: true);
} on PlatformException catch (e) {
print(e);
}
if (!mounted) return;
setState(() {
_authenticated = authenticated;
});
}
Widget _buildAuthButton() {
assert(!_authenticated);
return new RaisedButton(
child: new Text('Authenticate'),
onPressed: _authenticate,
);
}
Widget _buildContinueButton() {
assert(_authenticated);
return new RaisedButton(
child: new Text('Continue'),
onPressed: () {
// Do something now that the user is authenticated
},
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Interactivity Tutoral'),
),
body: new Center(
child: _authenticated ? _buildContinueButton() : _buildAuthButton(),
),
);
}
}
I would use FutureBuilder, just return one widget or the other based on whether the Future is complete
new FutureBuilder<String>(
future: your_future,
builder: (_, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return const CircularProgressIndicator();
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Text(
'Your data: ${snapshot.data}',
);
}
})