I know the difference between RepositoryProvider and RepositoryProvider.value: the first one creates the repository for you and the second one receives a repository that is already created.
Please see the difference between the 2 code blocks - the first one is okay, the second one gives the following error.
RepositoryProvider.of() called with a context that does not contain a repository of type AuthRepository.
No ancestor could be found starting from the context that was passed to RepositoryProvider.of<AuthRepository>().
This can happen if the context you used comes from a widget above the RepositoryProvider.
The context used was: HomeScreen(dirty)
I don't understand why code 2 gives an error.
Code 1: success
class MyApp {
void main() {
// 1) Let the RepositoryProvider create the AuthRepository
runApp(RepositoryProvider(
crate: (context) => AuthRepository(),
child: BlocProvider(
create: (context) => AuthCubit(authRepository: RepositoryProvider.of<AuthRepository>(context)),
child: const MaterialApp(
// 2) Show HomeScreen that will access the AuthRepository instance
home: HomeScreen()
)
)
)
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final authCubit = BlocProvider.of<AuthCubit>(context);
// 3) This will succeed
final authRepo = RepositoryProvider.of<AuthRepository>(context);
return Scaffold(
body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
return const Text(state.toString());
},
),
);
}
}
Code 2: error
class MyApp {
void main() {
// 1) Create a repository instance of AuthRepository
final authRepo = AuthRepository();
// 2) Add this AuthRepository instance to the RepositoryProvider.value
runApp(RepositoryProvider.value(
value: (context) => authRepo,
child: BlocProvider(
create: (context) => AuthCubit(authRepository: authRepo),
child: const MaterialApp(
// 3) Show HomeScreen that will access the AuthRepository instance
home: HomeScreen()
)
)
)
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final authCubit = BlocProvider.of<AuthCubit>(context);
// 4) This will fail
final authRepo = RepositoryProvider.of<AuthRepository>(context);
return Scaffold(
body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
return const Text(state.authenticationStatus.toString());
},
),
);
}
}
The value should be a repository not a function.
Instead of
runApp(RepositoryProvider.value(
value: (context) => authRepo,
try
runApp(RepositoryProvider.value(
value: authRepo,
Related
I am studying, provider in flutter. I try to make login process by using beamer and provider.
If user's auth state that is dectected by Provider context.watch<AuthenticationNotifier>().isAuthenticated; is false, BeamGuard force user to go auth screen.
final _routerDelegate = BeamerDelegate(
guards: [
BeamGuard(
pathPatterns: ['/'],
check: (context, location) {
return context.watch<AuthenticationNotifier>().isAuthenticated;
},
beamToNamed: (origin, target) => '/auth',
)
],
locationBuilder: BeamerLocationBuilder(
beamLocations: [PostListLocations(), AuthLocations()]),
);
User click login button in auth screen, auth state change true. I checked user'auth state is changed in AuthScreen.
void attemptVerify(BuildContext context) {
var authNotifier = context.read<AuthenticationNotifier>();
authNotifier.setUserAuth(true);
logger.d(authNotifier.userState);
}
}
but provider in BeamGuard is not watch state change. user do not go to main page, stay in auth page. if i set user's auth state True, user go to directly main page.So I think beamer is not problem. I think Provider doesn't work. I cannot find my mistake. could you help me?
this is full code.
main.dart
final _routerDelegate = BeamerDelegate(
guards: [
BeamGuard(
pathPatterns: ['/'],
check: (context, location) {
return context.watch<AuthenticationNotifier>().isAuthenticated;
},
beamToNamed: (origin, target) => '/auth',
)
],
locationBuilder: BeamerLocationBuilder(
beamLocations: [PostListLocations(), AuthLocations()]),
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: (BuildContext context, Widget? child) {
return ChangeNotifierProvider<AuthenticationNotifier>( //provider
create: (context) => AuthenticationNotifier(),
child: MaterialApp.router(
routeInformationParser: BeamerParser(),
routerDelegate: _routerDelegate,
),
);
},
);
}
}
auth_notifier.dart
import 'package:flutter/widgets.dart';
class AuthenticationNotifier extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
void setUserAuth(bool authState) {
_isAuthenticated = authState;
notifyListeners();
}
}
auth_scree.dart
class AuthScreen extends StatefulWidget {
const AuthScreen({Key? key}) : super(key: key);
#override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: Scaffold(
body: SingleChildScrollView(
child: ElevatedButton(
onPressed: () {
attemptVerify(context);
},
child: Text("button"),
),
)),
),
);
}
void attemptVerify(BuildContext context) {
var authNotifier = context.read<AuthenticationNotifier>();
authNotifier.setUserAuth(true);
}
}
The problem is that I would like to show a loading indicator when the user tries to fetch some data from an api. But when the user presses the button, loading indicator shows once. But I would like to show the loading indicator every time when the user tries to fetch. It works but as I say It works once. Could anyone have any idea what can cause this problem? Here's the minimal code to reproduce the issue:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => HomeCubit()),
],
child: const MaterialApp(
title: 'Flutter Bloc Demo',
home: HomeView(),
),
);
}
}
class HomeView extends BaseView<HomeCubit, HomeState> {
const HomeView({Key? key}) : super(key: key);
#override
Widget builder(HomeCubit cubit, HomeState state) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.count.toString()),
ElevatedButton(
onPressed: cubit.increment,
child: const Text('Increase'),
),
],
),
);
}
}
class HomeState extends BaseState {
final int count;
HomeState({required this.count});
HomeState copyWith({
int? count,
}) {
return HomeState(
count: count ?? this.count,
);
}
}
class HomeCubit extends BaseCubit<HomeState> {
HomeCubit() : super(HomeState(count: 0));
void increment() {
flow(() async {
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(count: state.count + 1));
});
}
}
#immutable
abstract class BaseView<C extends StateStreamable<S>, S extends BaseState>
extends StatelessWidget {
const BaseView({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return BaseCubit(context.read<S>());
},
child: Scaffold(
body: BlocBuilder<C, S>(
builder: (context, state) {
final cubit = context.read<C>();
if (state.loadingState == LoadingState.loading) {
return loadingWidget;
}
return builder.call(cubit, state);
},
),
),
);
}
Widget builder(C cubit, S state);
Widget get loadingWidget => const Center(
child: CircularProgressIndicator(),
);
}
enum LoadingState { initial, loading, loaded }
class BaseState {
LoadingState loadingState;
BaseState({
this.loadingState = LoadingState.initial,
});
}
class BaseCubit<S extends BaseState> extends Cubit<S> {
BaseCubit(S state) : super(state);
Future<void> flow(Future<void> Function() function) async {
state.loadingState = LoadingState.loading;
emit(state);
await function();
state.loadingState = LoadingState.loaded;
emit(state);
}
}
Is it overengineering? I don't think you are duplicating much code if you just use BlocBuilder instead of some base class.
If bloc already exist you should provide it by BlocProvider.value instead of BlocProvider(create: read())
You should use context.watch instead of context.read to get a new value every time the state changes. context.read receives state only once.
It's overengineering, please take a look at https://bloclibrary.dev/#/coreconcepts. There are enough tutorials to catch the basic idea.
Then try to use bloc + freezed. Here is an example https://dev.to/ptrbrynt/why-bloc-freezed-is-a-match-made-in-heaven-29ai
I have a provider with an int variable currentPage that defines the initial page of a PageView. I have this because I want to change the currentPage with widgets that far under the tree, or descendent widgets. I've set up everything correctly, but when changeNotifier is called, the page doesn't change.
Here's the provider class-
class CurrentPageProvider with ChangeNotifier{
int? currentPage;
CurrentPageProvider({this.currentPage});
changeCurrentPage(int page) {
currentPage = page;
notifyListeners();
}
}
To use it, I've wrapped my MaterialWidget with a MultiProvider as such-
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => CurrentPageProvider(currentPage: 0))
],
child: MaterialApp(
title: "Test",
debugShowCheckedModeBanner: false,
theme: ThemeData.light().copyWith(
primaryColor: yellowColor,
),
home: const ResponsiveRoot(),
),
);
}
}
And here's the widget where the child should rebuild, but isn't-
class ResponsiveRoot extends StatelessWidget {
const ResponsiveRoot({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
int currentPage = Provider.of<CurrentPageProvider>(context).currentPage!;
print("CurrentPageUpdated");
return LayoutBuilder(
builder: ((context, constraints) {
if (constraints.maxWidth > kWebScreenWidth) {
return const WebscreenLayout();
} else { //The page view is here
return MobileScreenLayout(
currentPage: currentPage,
);
}
}),
);
}
}
Upon debugging, I've found out that "CurrentPageUdated" gets printed when I'm calling the changeCurrentPage. However, the initState of the MobileScreenLayout doesn't get called (This widget has the pageView)
How do I fix this? Thanks!
in order to update the state of the the app you need to use Consumer widget.
Consumer<Your_provider_class>(
builder: (BuildContext context, provider_instance, widget?){
},
child: any_widget, but not neccessary,
)
The problem seems to be that even though your Provider.of mechanism needs to listen to changes, it does not.
What you can do is, do the recommended way on the documentation and you can either use the watch extension function or use Consumer or Selector widgets.
Here is an example on how to do it with your example with a Selector.
For more information read about Selector here
class ResponsiveRoot extends StatelessWidget {
const ResponsiveRoot({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Selector<CurrentPageProvider, int>(
selector: (context, provider) => provider.currentPage!,
builder: (context, currentPage, child) {
print("CurrentPageUpdated");
return LayoutBuilder(
builder: ((context, constraints) {
if (constraints.maxWidth > kWebScreenWidth) {
return const WebscreenLayout();
} else {
//The page view is here
return MobileScreenLayout(
currentPage: currentPage,
);
}
}),
);
},
);
}
}
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 have two streams:
Stream<FirebaseUser> FirebaseAuth.instance.onAuthStateChanged
Stream<User> userService.streamUser(String uid)
My userService requires the uid of the authenticated FirebaseUser as a parameter.
Since I will probably need to access the streamUser() stream in multiple parts of my app, I would like it to be a provider at the root of my project.
This is what my main.dart looks like:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
var auth = FirebaseAuth.instance;
var userService = new UserService();
return MultiProvider(
providers: [
Provider<UserService>.value(
value: userService,
),
],
child: MaterialApp(
home: StreamBuilder<FirebaseUser>(
stream: auth.onAuthStateChanged,
builder: (context, snapshot) {
if (!snapshot.hasData) return LoginPage();
return StreamProvider<User>.value(
value: userService.streamUser(snapshot.data.uid),
child: HomePage(),
);
}),
),
);
}
}
The issue is that when I navigate to a different page, everything below the MaterialApp is changed out and I lose the context with the StreamProvider.
Is there a way to add the StreamProvider to the MultiProvider providers-list?
Because when I try, I also have to create another onAuthStateChanged stream for the FirebaseUser and I don't know how to combine them into one Provider.
So this seems to work fine:
StreamProvider<User>.value(
value: auth.onAuthStateChanged.transform(
FlatMapStreamTransformer<FirebaseUser, User>(
(firebaseUser) => userService.streamUser(firebaseUser.uid),
),
),
),
If anybody has doubts about this in certain edge cases, please let me know.
Thanks to pskink for the hint about flatMap.
Maybe you can try this approach:
main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<FirebaseUser>(
builder: (_) => FirebaseUser(),
),
],
child: AuthWidgetBuilder(builder: (context, userSnapshot) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.indigo),
home: AuthWidget(userSnapshot: userSnapshot),
);
}),
);
}
}
AuthWidgetBuilder.dart
Used to create user-dependant objects that need to be accessible by
all widgets. This widget should live above the [MaterialApp]. See
[AuthWidget], a descendant widget that consumes the snapshot generated
by this builder.
class AuthWidgetBuilder extends StatelessWidget {
const AuthWidgetBuilder({Key key, #required this.builder}) : super(key: key);
final Widget Function(BuildContext, AsyncSnapshot<User>) builder;
#override
Widget build(BuildContext context) {
final authService =
Provider.of<FirebaseUser>(context, listen: false);
return StreamBuilder<User>(
stream: authService.onAuthStateChanged,
builder: (context, snapshot) {
final User user = snapshot.data;
if (user != null) {
return MultiProvider(
providers: [
Provider<User>.value(value: user),
Provider<UserService>(
builder: (_) => UserService(uid: user.uid),
),
],
child: builder(context, snapshot),
);
}
return builder(context, snapshot);
},
);
}
}
AuthWidget.dart
Builds the signed-in or non signed-in UI, depending on the user
snapshot. This widget should be below the [MaterialApp]. An
[AuthWidgetBuilder] ancestor is required for this widget to work.
class AuthWidget extends StatelessWidget {
const AuthWidget({Key key, #required this.userSnapshot}) : super(key: key);
final AsyncSnapshot<User> userSnapshot;
#override
Widget build(BuildContext context) {
if (userSnapshot.connectionState == ConnectionState.active) {
return userSnapshot.hasData ? HomePage() : SignInPage();
}
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
This is originally from the tutorial of advance provider from Andrea Bizotto.
But I tailored some the code according to your your code above.
Hope this works, good luck!
Reference:
https://www.youtube.com/watch?v=B0QX2woHxaU&list=PLNnAcB93JKV-IarNvMKJv85nmr5nyZis8&index=5