class CustomImageRectangle extends StatefulWidget {
final String url;
const CustomImageRectangle({Key key, this.url}): super(key: key);
#override
_CustomImageRectangleState createState() => _CustomImageRectangleState();
}
class _CustomImageRectangleState extends State<CustomImageRectangle> {
#override
void initState() {
super.initState();
print(widget.url != null?'not null':'null');
}
Above class, I called 3 times in my home widget, but only one-time run initState. why is that?
only 1 print in the console.
logo != ''
? FutureBuilder<String>(
future: storage.getVendorLogo(logo),
builder: (context, snapshot) {
if (snapshot.hasData) {
return CustomImageRectangle(url: snapshot.data);
} else {
return CustomImageRectangle();
}
})
: CustomImageRectangle(),
CustomImageRectangle isn't being created 3 times at once. It is being conditionally rendered once. Hence only once print is observed.
In the code snippet :
logo != ''
? FutureBuilder<String>(
future: storage.getVendorLogo(logo),
builder: (context, snapshot) {
if (snapshot.hasData) {
return CustomImageRectangle(url: snapshot.data);
} else {
return CustomImageRectangle();
}
})
: CustomImageRectangle(),
The FutureBuilder will only be returned if logo!='' is true.
In the builder of FutureBuilder, either if (snapshot.hasData) will be true or false.
Depending on that only one of the CustomImageRectangle(url: snapshot.data) or CustomImageRectangle() will be returned.
Otherwise if logo!='' is false the CustomImageRectangle() in the ternary operator will be returned.
Here CustomImageRectangle is created only once in all cases. Hence only once the initState is called.
Consider the below example where it is rendered 3 times at once:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
children: [
CustomImageRectangle(),
CustomImageRectangle(),
CustomImageRectangle(),
],
),
),
),
);
}
}
Console Output:
null
null
null
Related
I've changed from Statefulwidget using initState to fetch the data and Futurebuilder to load it to Futureprovider. But it seems like Futureprovider is execute build method twice, while my previous approach executed it once. Is this behaviour normal?
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg()
);
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
if (reportList == null) {
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Det finns inga rapporter."));
}
print(reportList.length);
return Container();
}
}
Im relativly new to flutter but I think its because StatelessWidget is #immutable, which means whenever something changes it needs to rebuild itself.
At first build there is async calling made and ReportWidg() is rendered.
Then this line final reportList = Provider.of<List<ReportModel>>(context); get new fetched data as result of async function therefore immutable widget needs to rebuild itself because it cannot be "changed".
In object-oriented and functional programming, an immutable object
(unchangeable object) is an object whose state cannot be modified
after it is created. ... This is in contrast to a mutable object
(changeable object), which can be modified after it is created
or am I wrong ?
I suspect your FutureProvider should be hoisted to a single instantiation, like placed into a global variable outside any build() methods. This will of course cache the result, so you can set it up for rebuild by having the value depend on other Providers being watch()ed or via FutureProvider.family.
You can copy paste run full code below
Yes. it's normal
First time Execute Build reportList is null and show CircularProgressIndicator()
Second time Execute Build reportList has data and show data
If you set listen: false , final reportList = Provider.of<List<ReportModel>>(context, listen: false);
You get only one Execute Build and the screen will always show CircularProgressIndicator()
In working demo simulate 5 seconds network delay so you can see CircularProgressIndicator() then show ListView
You can reference https://codetober.com/flutter-provider-examples/
code snippet
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
working demo
full code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ReportModel {
String title;
ReportModel({this.title});
}
class ReportsProvider with ChangeNotifier {
Future<List<ReportModel>> loadReportData(int no) async {
await Future.delayed(Duration(seconds: 5), () {});
return Future.value([
ReportModel(title: "1"),
ReportModel(title: "2"),
ReportModel(title: "3")
]);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ReportsPage(),
);
}
}
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg());
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
itemBuilder: (context, index) {
return Card(
elevation: 6.0,
child: Padding(
padding: const EdgeInsets.only(
top: 6.0, bottom: 6.0, left: 8.0, right: 8.0),
child: Text(reportList[index].title.toString()),
));
}),
);
}
}
In your case you should use Consumer, i.e.
FutureProvider<List<ReportModel>(
create: (_) => ...,
child: Consumer<List<ReportModel>(
builder: (_, reportList, __) {
return reportList == null ?
CircularProgressIndicator() :
ReportWidg(reportList);
}
),
),
But in this case you must to refactor your ReportWidg.
I am trying to learn Bloc with creating dynamic simple theme manager. I create a class called theme_bloc :
class DefaultApi {
final String name;
final ThemeData theme;
DefaultApi(this.name, this.theme);
}
class ThemeBloc {
DefaultApi _defualt;
ThemeBloc() {}
final _themeManager = StreamController<DefaultApi>.broadcast();
Stream<DefaultApi> get themeManager => _themeManager.stream;
Function(DefaultApi) get changeTheme => _themeManager.sink.add;
DefaultApi initialTheme() {
_defualt = DefaultApi("light", ThemeManager.instance.lightTheme);
return _defualt;
}
void dispose() {
_themeManager.close();
}
}
to inject bloc class i use provider like this:
class ThemeProvider with ChangeNotifier{
ThemeBloc _bloc;
ThemeProvider(){
_bloc = ThemeBloc();
}
ThemeBloc get bloc => _bloc;
}
I use StringBuilder in main class to set theme like this:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: ThemeProvider(),
child: Consumer<ThemeProvider>(
builder: (crx, provider, child) {
return StreamBuilder(
initialData: provider.bloc.initialTheme(),
stream: provider.bloc.themeManager,
builder: (context, AsyncSnapshot<DefaultApi>snapshot) {
return snapshot.hasData? MaterialApp(
title: 'Flutter Demo',
theme: snapshot.data.theme,
home: HomePage(),
):Container();
});
},
),
);
}
In HomePage page i have switch to change theme between light and dark theme.
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = Provider.of<ThemeProvider>(context).bloc;
return Scaffold(
appBar: AppBar(
title: Text("Theme manager"),
),
body: StreamBuilder<DefaultApi>(
stream: bloc.themeManager,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Switch(
value: true,
onChanged: (bool value) {
},
);
} else if (!snapshot.hasData) {
return Text("loading");
}
return Text("!!!!");
}),
);
}
But after running just loading is printed into screen.
Someone know what is my problem?
Your problem will solve if the StreamBuilder widget inside HomePage have initialData. like this:
...
body: StreamBuilder<DefaultApi>(
initialData: bloc.initialTheme(), // <<< new line
stream: bloc.themeManager,
builder: (context, snapshot) {
...
This input is not required. Yet I don't have any clue why it's absence cause problem here.
There are few deeper considerations:
1. Using ChangeNotifierProvider
As the documentation recommends, use ChangeNotifierProvider instead of ChangeNotifierProvider.value. Obviously because you're creating a new instance
...
return ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: Consumer<ThemeProvider>(
...
2. Prevent useless listening
Based on this guide, if you are using provider to only call actions, you have to use listen: false to prevent useless rebuilds.
I know how to build a Widget from a FutureBuilder, so this is my future/async function, for example:
Future<Null> myFuture() async
{
// etc.
}
If I want to build an AppBar title, this works fine:
class MyStuff extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
title: FutureBuilder(
future: myFuture(),
builder: (context, snapshot) {
// etc...
return Text("blahblah");
}
));
}
}
Now, I want to build the AppBar's bottom which expects a PreferredSizeWidget, so this works, but is not async:
class MyStuff2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: PreferredSize(),
);
}
}
But how can I use this in a future/async way? this doesn't even compile:
class MyStuff3 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: FutureBuilder(, // is there a FutureBuilder for PreferredSizeWidgets?
future: myFuture(),
builder: (context, snapshot) {
// etc...
// I want to compute preferred size from async call result
return PreferredSize(); // pseudo-code ???
}
);
}
}
It's not gonna work since the FutureBuilder returns a Widget , so, what you can do is convert your stateless builder to a stateful one and create your own "PreferredSizeFutureBuilder"
import 'package:flutter/material.dart';
class PreferredSizeFutureBuilder extends StatefulWidget {
#override
_PreferredSizeFutureBuilderState createState() =>
_PreferredSizeFutureBuilderState();
}
class _PreferredSizeFutureBuilderState
extends State<PreferredSizeFutureBuilder> {
bool isCompleted = false;
#override
void initState() {
super.initState();
myFuture();
}
Future<void> myFuture() async {
//do something
setState(() {
isCompleted = true;
});
}
#override
Widget build(BuildContext context) {
return AppBar(
bottom: isCompleted //we check if the future has ended
? PreferredSize(
child: YourComponent(), //your component
preferredSize: Size.fromHeight(12.0), //the size you want
)
: null); //if the future is loading we don't return anything, you can add your loading widget here
}
}
Of course this means that the snapshot object is not gonna work , instead you use the isCompleted bool to check if your future is done, you can even use the ConnectionState instead of a boolean to have the "snapshot" behaviour
You can wrap FutureBuilder into a PreferredSize-Widget.
No need to create an own FutureBuilder-Widget that implements PreferredSize.
class MyStuff2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return AppBar(
bottom: PreferredSize(
child: FutureBuilder(future: _getData(), builder: (BuildContext context, AsyncSnapshot snapshot) {
if(snapshot.data != null) {
return Text(snapshot.data);
} else {
return Text('Loading...');
}
),
preferredSize: Size.fromHeight(50.0),
);
}
}
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
i need to fetch data from localstorge and depending upoun that data value isLogin is true or false if isLogin value is true then return different MaterialApp and if it's false then different MaterialApp.
Widget build(BuildContext context) {
return FutureBuilder(
future: storage.ready,
builder: (BuildContext context, snapshots) {
if (snapshots.hasData) {
var isLogin = storage.getItem('isLogin');
if (snapshots.data == true) {
return MaterialApp(
initialRoute: '/sample',
onGenerateRoute: RouteGenerator.generateRoute,
);
} else {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
);
}
}
},
);
}
So here is how I did that I'm not sure if it's the best solution or not but it works.
main() async {
String isToNavigate = await MainAppService().getPrefValue('isToRemember');
String typeOfUser = await MainAppService().getPrefValue('typeOfUser');
if (isToNavigate != null) {
if (typeOfUser == 'admin') {
WidgetsFlutterBinding.ensureInitialized();
runApp(AdminMyApp());
}
if (typeOfUser == 'client') {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyAppClient());
}
if (typeOfUser == 'professional') {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyProfessionalsApp());
}
} else {
print('null is found /');
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
}
Found a solution that worked for me, maybe it helps someone in the future.
Before it was throwing me errors that weren't really making any sense:
`Null check operator used on a null value. The relevant error-causing widget was MaterialApp.
However, my solution was converting the app to a StatefulWidget, adding the future to the initState method (like you usually do it on screens as well) and then adding keys for each app.
class DEUSApp extends StatefulWidget {
const DEUSApp({Key key}) : super(key: key);
#override
_DEUSAppState createState() => _DEUSAppState();
}
class _DEUSAppState extends State<DEUSApp> {
Future<bool> initializeData;
final GlobalKey _appKey = GlobalKey();
final GlobalKey _loadingKey = GlobalKey();
#override
void initState() {
super.initState();
initializeData = context.read<SplashCubit>().initializeData();
}
#override
Widget build(BuildContext ctx) {
return BlocBuilder<SplashCubit, SplashState>(builder: (context, state) {
// at the beginning, show a splash screen, when the data hasn't been loaded yet.
return FutureBuilder(
future: initializeData,
builder: (context, snapshot) {
if (!snapshot.hasData || !(state is SplashSuccess))
return MaterialApp(key: _loadingKey, theme: MyStyles.theme, home: SplashScreen());
return MaterialApp(
key: _appKey,
title: 'Deus Finance',
debugShowCheckedModeBanner: false,
theme: MyStyles.theme,
routes: generateRoutes(context),
initialRoute: kInitialRoute,
);
},
);
});
}
}