I am developing a simple app that makes the user sign in anonymously using Firebase Auth. But when I refresh the browser (Chrome), I can see that the Home screen is rendering twice in a few milliseconds.
I have seen other questions for older Firebase versions, but I did not find the solution.
class AppRoot extends StatelessWidget {
const AppRoot({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return const Text('Error!');
} else if (snapshot.hasData) {
print("HOME"); // calling twice -> calling HomeScreen initState twice -> fetching resources twice...
return const HomeScreen();
} else {
return Container();
}
}),
);
}
}
Main:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
if (FirebaseAuth.instance.currentUser == null) {
try {
await FirebaseAuth.instance.signInAnonymously();
} catch (e) {
print(e);
}
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Auth Test',
theme: ThemeData(),
home: const AppRoot(),
);
}
}
Why is authStateChanges rendering HomeScreen twice? It should only once after the user logs in anonymously.
Related
I'm just started with flutter bloc. I want to make a movie listing app, create your own lists and share your friends etc.
The problem is, when i tapped to logout button, UI does not update.
Here's where i try to change the UI. If the state is Authenticated(), I'm returning WatchlistNavBar(), if the state is Unauthenticated() I'm returning WelcomeView() for login or register.
If the user has submitted email and password correctly, WatchlistNavBar() is building. Everything works fine. But when the user tries the logout, WelcomeView() does not build.
By the way BlocNavigate() class is called in MaterialApp()'s home property.
class BlocNavigate extends StatelessWidget {
const BlocNavigate({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Loading) {
return const LoadingWidget();
} else if (state is Authenticated) {
return const WatchlistNavBar();
} else if (state is Unauthenticated) {
return const WelcomeView();
} else {
return const SignInView();
}
},
);
}
}
AuthBloc:
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthRepository authRepository = AuthRepository();
AuthBloc(this.authRepository) : super(AuthInitial()) {
on<AuthenticationStarted>(_onAuthStarted);
on<AuthenticationSignedOut>(_onSignOut);
}
_onAuthStarted(AuthenticationStarted event, Emitter<AuthState> emit) async {
UserModel user = await authRepository.getCurrentUser().first;
if (user.uid != "uid") {
emit(Authenticated());
} else {
emit(Unauthenticated());
}
}
_onSignOut(AuthenticationSignedOut event, Emitter<AuthState> emit) async {
authRepository.signOut();
emit(Unauthenticated());
}
}
AuthState:
abstract class AuthState extends Equatable {
const AuthState();
#override
List<Object> get props => [];
}
class AuthInitial extends AuthState {}
class Authenticated extends AuthState {}
class Unauthenticated extends AuthState {}
class Loading extends AuthState {}
And this is the logout button, where i add AuthenticationSignedOut() to AuthBloc():
class LogoutButton extends StatelessWidget {
const LogoutButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.exit_to_app, color: Colors.black),
onPressed: () {
context.read<AuthBloc>().add(AuthenticationSignedOut());
});
}
}
My main function and MaterialApp():
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
Bloc.observer = AppBlocObserver();
runApp(
MultiBlocProvider(providers: [
BlocProvider(create: (context) => FormBloc()),
BlocProvider(create: (context) => DatabaseBloc(DatabaseRepositoryImpl())),
BlocProvider(
create: (context) =>
AuthBloc(AuthRepository())..add(const AuthenticationStarted())),
BlocProvider(
create: (context) => FavoritesBloc()..add(const FavoritesLoad()))
], child: const WatchlistApp()),
);
}
class WatchlistApp extends StatelessWidget {
const WatchlistApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Watchlist',
theme: WatchlistTheme.mainTheme,
home: const BlocNavigate(),
);
}
}
As i said, i'm new to flutter bloc and don't know exactly what I'm doing wrong. If you need more information please let me know.
I am using Bloc to check internet connection. If there is no connection, I show the SnackBar. But I also need to be able to reuse the connection method to re-check the connection by clicking on the button, but I don’t understand how to call this method. Tell me how to call the connection method when the button is clicked?
bloc
class ConnectedBloc extends Bloc<ConnectedEvent, ConnectedState> {
StreamSubscription? subscription;
ConnectedBloc() : super(ConnectedInitial()) {
on<OnConnectedEvent>((event, emit) => emit(ConnectedSucess()));
on<OnNotConnectedEvent>((event, emit) => emit(ConnectedFailure()));
void connection() => Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult result) {
if (result == ConnectivityResult.wifi ||
result == ConnectivityResult.mobile) {
add(OnConnectedEvent());
} else {
add(OnNotConnectedEvent());
}
});
home
home: BlocConsumer<ConnectedBloc, ConnectedState>(
listener: ((context, state) {
if (state is ConnectedSucess) {
const SizedBox();
} else if (state is ConnectedFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 3),
backgroundColor: Colors.transparent,
elevation: 0,
content: SystemMessagesSnackBar(
message: 'No internet access. Check your connection',
textButton: 'Refresh',
onPressed: () =>
ScaffoldMessenger.of(context).hideCurrentSnackBar(),
icon: SvgPicture.asset(constants.Assets.no_connection),
),
),
);
}
}),
To call a method inside your bloc, you need to get a reference to your bloc first by using context.read<T>(). In your case:
context.read<ConnectedBloc>()
You can then call the method as follows:
onPressed:(){
ScaffoldMessenger.of(context).hideCurrentSnackBar();
context.read<ConnectedBloc>().connection();
}
But this will create an additional stream. Maybe you should use checkConnectivity if you click on the button.
wrap the code with a streambuilder and then associate the bloc streams with the streambuilder
in your button click event interact with the bloc class
check the streambuilder state and process the returning data.
yaml
dependencies:
flutter:
sdk: flutter
equatable: ^2.0.3
rxdart: ^0.27.4
import 'package:flutter/material.dart';
import 'package:equatable/equatable.dart';
import 'package:rxdart/rxdart.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Button Stream',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Test_StreambuilderButton(),
);
}
}
class Test_StreambuilderButton extends StatefulWidget {
Test_StreambuilderButton({Key? key}) : super(key: key);
BlocCode bloc= new BlocCode();
#override
State<Test_StreambuilderButton> createState() => _Test_StreambuilderButtonState();
}
class _Test_StreambuilderButtonState extends State<Test_StreambuilderButton> {
#override
Widget build(BuildContext context) {
return
Scaffold(appBar: AppBar(title:Text("Button Stream Event")),
body:
Column(children: [
StreamBuilder<BlocState>(
stream: widget.bloc.blocStream,
builder:(context,snapshot)
{
if (snapshot.hasData)
{
String data=snapshot.data!._message;
if (data == null)
{
return (Container(child:Text("No data")));
}
return (Container(child:Text(data)));
}
else
{
return (Container(child:Text("No activity")));
}
}
),
ElevatedButton(onPressed:(){
widget.bloc.setMessage(BlocState("Hello World"));
}, child: Text("Press Me"))
]));
}
}
class BlocState extends Equatable
{
BlocState(this._message);
final String _message;
#override
List<Object> get props=>[_message];
String get getMessage {return _message;}
}
class BlocCode
{
BlocCode();
Stream<BlocState> get blocStream => _loadController.stream;
final _loadController=BehaviorSubject<BlocState>();
void dispose()
{
_loadController.close();
}
setMessage(BlocState state)
{
_loadController.sink.add(state);
}
}
My problem is that, before showing the screen. It should load the necessary data while displaying a splashscreen.
It works fine, until it goes to the create provider, the data which has been loaded into the list is getting cleared due to the list getting recreated. I wonder how can i tackle this? How should i load the data (json) file into the list instead.
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<void> loadJson;
#override
void initState() {
loadJson = QuestionProvider().loadJsonFiles();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: loadJson,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const MaterialApp(home: Splash());
} else {
return MultiProvider(
providers: [
ChangeNotifierProvider<QuestionProvider>(create: (_) => QuestionProvider()),
],
child: MaterialApp(
title: "MyApp",
theme: ThemeData(
primarySwatch: Colors.amber,
),
home: const Home(),
)
);
}
},
);
}
}
class QuestionProvider with ChangeNotifier {
final List<QuestionModel> questionList = <QuestionModel>[];
Future<void> loadJsonFiles() async {
final String response = await rootBundle.loadString("assets/questions.json");
final Map<String, dynamic> data = await jsonDecode(response);
for (int i = 0; i < data.length; i++) {
questionList.add(QuestionModel.fromJson(data[i]));
}
}
}
Why not invert the future builder and the providers?
Widget build(BuildContext context) {
return MultiProvider(
[...],
child: Builder(
builder: (context) =>
FutureBuilder(
future: Provider.of<QuestionProvider>().loadJsonFiles,
child: [...]
),
),
);
}
There may or may not be some disadvantages to this method, specifically, the value of the future is no longer cached, if this worries you, I recommend you cache the value within the QuestionProvider class itself.
I am trying to set the home page of the Flutter app asynchronously, but that is not working because the build method cannot have async properties.
class _MyAppState extends State<MyApp> {
// Widget homeWidget;
// #override
// void initState() async {
// super.initState();
// homeWidget = (await AuthUser.getCurrentUser() != null)
// ? NavBarPage()
// : OnBoardingWidget();
// }
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WizTkt',
theme: Theme.of(context).copyWith(
appBarTheme: Theme.of(context)
.appBarTheme
.copyWith(brightness: Brightness.dark),
primaryColor: Colors.blue),
home: (await AuthUser.getCurrentUser() != null)
? NavBarPage()
: OnBoardingWidget(),
);
}
}
As you can see in the code, I also tried to use initState to set the homepage widget but I cannot make initState an asynchronous function. I feel like there is a better way to choose your homepage in Flutter. What am I missing?
Do note that AuthUser.getCurrentUser() has to be an async function because I use the SharedPreferences library to obtain the login token stored in memory.
You can use FutureBuilder which allows you to build an Widget in a future time.
Here an example:
class OnBoardingWidget extends StatefulWidget {
const OnBoardingWidget({Key key}) : super(key: key);
#override
State<OnBoardingWidget> createState() => _OnBoardingWidgetState();
}
class _OnBoardingWidgetState extends State<OnBoardingWidget> {
final Future<String> _waiter = Future<String>.delayed(
const Duration(seconds: 2), () => 'Data Loaded',
);
#override
Widget build(BuildContext context) {
return Container(
child: FutureBuilder<String>(
future: _waiter,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
Widget wdgt;
if (snapshot.hasData) {
wdgt = Text('Result: ${snapshot.data}');
} else if (snapshot.hasError) {
wdgt = Text('Ops ops ops');
} else {
wdgt = Text('Not ready yet');
}
return Center(child: wdgt);
},
),
);
}
}
I'm using flutter to make a mobile app.
In app.dart, the code controls the main routing.
class App extends StatelessWidget {
const App({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primaryColor: hexToColor("#6a1717")),
home: BlocBuilder<AuthenticationBloc, AuthenticationDataState>(
builder: (context, state) {
if (state.state == AuthenticationState.uninitialized) {
return SplashPage();
} else if (state.state == AuthenticationState.authenticated) {
print("now home page");
return HomePage();
} else if (state.state == AuthenticationState.unauthenticated) {
return LoginPage();
} else if (state.isLoading) {
return LoadingIndicator();
} else {
return null;
}
},
),
);
}
}
And in login bloc handler, yielded logged_in event after log in.
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthenticationBloc authenticationBloc;
LoginBloc({
#required this.authenticationBloc,
}) : assert(authenticationBloc != null),
super(LoginInitial());
#override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginButtonPressed){
yield LoginLoading();
try {
final user = await userController.authenticate(
email: event.email,
password: event.password
);
if(user != null){
if(user.accessToken != null){
authenticationBloc.add(LoggedIn(user: user));
yield LoginInitial();
}else{
yield NotValid();
}
}else{
yield NotRegistered();
}
}catch(error){
yield LoginFailure(error: AppException.unknown(message: error.toString()));
}
}
}
}
When debugging, I checked out that the command "print("now home page");" is executed, and the processor went in home page actually but the login screen does not disapper and the home page is not rendered.
I don't know what's going on in flutter. Please help me.
Login page:
class LoginPage extends StatelessWidget {
const LoginPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
// resizeToAvoidBottomInset: false,
body: BlocProvider(
create: (context) {
return LoginBloc(
authenticationBloc: BlocProvider.of<AuthenticationBloc>(context),
);
},
child: LoginForm(),
),
);
}
}
Home Page:
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
...
}
}