In Flutter: Is it possible to use a Future<bool> be used as a property in another widget? - flutter

I have a Card() widget which contains a ListTile() widget.
One of the ListTile() widget's properties is enabled. I would like to dynamically set the value of this enabled property by using the outcome of a Future<bool> which uses async and await. Is this possible?
Here is the Card() widget with the ListTile() in it
Card myCard = Card(
child: ListTile(
title: Text('This is my list tile in a card'),
enabled: needsToBeEnabled(1),
),
);
Here is my Future
Future<bool> cardNeedsToBeEnabled(int index) async {
bool thisWidgetIsRequired = await getAsynchronousData(index);
if (thisWidgetIsRequired == true) {
return true;
} else {
return false;
}
}
Other attempts
I have tried to use a Future Builder. This works well when I'm building a widget, but in this case I'm trying to set the widget's property; not build a widget itself.

You cannot do that for two reason:
enable does not accept Future<bool> but bool
you need to update the state after result is received (you need a StatefullWidget)
There are 1 million way to do what you want to do and one of this is FutureBuilder but if you want to not rebuild all widget you can use this flow (your main widget need to be Statefull):
create a local variable that contains your bool value, something like bool _enabled
on initState() method override you can launch the call that get asynchronous data and using the then() extension method you can provide the new state to your widget when the call will be completed.
Something like:
#override
void initState() {
super.initState();
getAsynchronousData(index).then((result) {
if (result == true) {
_enabled = true;
} else {
_enabled = false;
}
setState(() {});
});
}
assign the boolean var to the ListTile widget

Related

Flutter - how to correctly create a button widget that has a spinner inside of it to isolate the setState call?

I have a reuable stateful widget that returns a button layout. The button text changes to a loading spinner when the network call is in progress and back to text when network request is completed.
I can pass a parameter showSpinner from outside the widget, but that requires to call setState outside of the widget, what leads to rebuilding of other widgets.
So I need to call setState from inside the button widget.
I am also passing a callback as a parameter into the button widget. Is there any way to isolate the spinner change state setting to inside of such a widget, so that it still is reusable?
The simplest and most concise solution does not require an additional library. Just use a ValueNotifier and a ValueListenableBuilder. This will also allow you to make the reusable button widget stateless and only rebuild the button's child (loading indicator/text).
In the buttons' parent instantiate the isLoading ValueNotifier and pass to your button widget's constructor.
final isLoading = ValueNotifier(false);
Then in your button widget, use a ValueListenableBuilder.
// disable the button while waiting for the network request
onPressed: isLoading.value
? null
: () async {
// updating the state is super easy!!
isLoading.value = true;
// TODO: make network request here
isLoading.value = false;
},
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: isLoading,
builder: (context, value, child) {
if (value) {
return CircularProgressIndicator();
} else {
return Text('Load Data');
}
},
);
}
You can use StreamBuilder to solve this problem.
First, we need to create a stream. Create a new file to store it, we'll name it banana_stream.dart, for example ;).
class BananaStream{
final _streamController = StreamController<bool>();
Stream<bool> get stream => _streamController.stream;
void dispose(){
_streamController.close();
}
void add(bool isLoading){
_streamController.sink.add(isLoading);
}
}
To access this, you should use Provider, so add a Provider as parent of the Widget that contain your reusable button.
Provider<BananaStream>(
create: (context) => BananaStream(),
dispose: (context, bloc) => bloc.dispose(),
child: YourWidget(),
),
Then add the StreamBuilder to your button widget:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: Provider.of<BananaStream>(context, listen:false),
initialData: false,
builder: (context, snapshot){
final isLoading = snapshot.data;
if(isLoading == false){
return YourButtonWithNoSpinner();
} else{
return YourButtonWithSpinner();
}
}
);
}
}
And to change isLoading outside, you can use this code:
final provider = Provider.of<BananaStream>(context, listen:false);
provider.add(true); //here is where you change the isLoading value
That's it!
Alternatively, you can use ValueNotifier or ChangeNotifier but i find it hard to implement.
I found the perfect solution for this and it is using the bloc pattern. With this package https://pub.dev/packages/flutter_bloc
The idea is that you create a BLOC or a CUBIT class. Cubit is just a simplified version of BLOC. (BLOC = business logic component).
Then you use the bloc class with BlocBuilder that streams out a Widget depending on what input you pass into it. And that leads to rebuilding only the needed button widget and not the all tree.
simplified examples in the flutter counter app:
// input is done like this
onPressed: () {
context.read<CounterCubit>().decrement();
}
// the widget that builds a widget depending on input
_counterTextBuilder() {
return BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
if (state.counterValue < 0){
return Text("negative value!",);
} else if (state.counterValue < 5){
return Text("OK: ${state.counterValue}",
);
} else {
return ElevatedButton(onPressed: (){}, child: const Text("RESET NOW!!!"));
}
},
);
}

Flutter: How to share an instance of statefull widget?

I have a "WidgetBackGround" statefullwidget that return an animated background for my app,
I use it like this :
Scaffold( resizeToAvoidBottomInset: false, body: WidgetBackGround( child: Container(),),)
The problem is when I use navigator to change screen and reuse WidgetBackGround an other instance is created and the animation is not a the same state that previous screen.
I want to have the same animated background on all my app, is it possible to instance it one time and then just reuse it ?
WidgetBackGround.dart look like this:
final Widget child;
WidgetBackGround({this.child = const SizedBox.expand()});
#override
_WidgetBackGroundState createState() => _WidgetBackGroundState();
}
class _WidgetBackGroundState extends State<WidgetBackGround> {
double iter = 0.0;
#override
void initState() {
Future.delayed(Duration(seconds: 1)).then((value) async {
for (int i = 0; i < 2000000; i++) {
setState(() {
iter = iter + 0.000001;
});
await Future.delayed(Duration(milliseconds: 50));
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(painter: SpaceBackGround(iter), child: widget.child);
}
}
this is not a solution, but maybe a valid workaround:
try making the iter a static variable,
this of course won't preserve the state of WidgetBackGround but will let the animation continue from its last value in the previous screen
A valid solution (not sure if it's the best out there):
is to use some dependency injection tool (for example get_it) and provide your WidgetBackGround object as a singleton for every scaffold in your app

How to display loading widget until the main widget is loaded in flutter?

I have a widget with a lot of contents like image, text and more, which make it heavy widget in flutter app, But when the app is navigated to the widget having the complex widget the app faces the jank since the widget is too large to load at an instant,
I want to show simple lite loading widget until the original widget is loaded thus removing the jank from the app and enable lazy loading of the widget,
How to achieve this in flutter?
EDIT:-
To make it clear, I am not loading any data from the Internet, and this is not causing the delay. For Loading the data from Internet we have FutureBuilder. Here my widget is itself heavy such that it takes some time to load.
How to display loading Widget while the main widget is being loaded.
First you have to create a variable to keep the state
bool isLoading = true; //this can be declared outside the class
then you can return the loading widget or any other widget according to this variable
return isLoading ?
CircularProgressIndicator() //loading widget goes here
: Scaffold() //otherwidget goes here
you can change between these two states using setState method
Once your data is loaded use the below code
setState(() {
isLoading = false;
});
Sample Code
class SampleClass extends StatefulWidget {
SampleClass({Key key}) : super(key: key);
#override
_SampleClassState createState() => _SampleClassState();
}
bool isLoading = true; // variable to check state
class _SampleClassState extends State<SampleClass> {
loadData() {
//somecode to load data
setState(() {
isLoading = false;//setting state to false after data loaded
});
}
#override
void initState() {
loadData(); //call load data on start
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: isLoading ? //check loadind status
CircularProgressIndicator() //if true
:Container(), //if false
);
}
}
This is a perfect place to use a FutureBuilder.
Widget loadingWidget = ...;
Future<Widget> buildHeavyWidget() async {
// build and return heavy widget
}
FutureBuilder(
future: buildHeavyWidget(),
builder: (context, snapshot) {
if(snapshot.hasData) {
// after the future is completed
// the heavy widget is availabe as snapshot.data
return snapshot.data;
}
return loadingWidget;
},
)
First define a bool value.
bool isLoading = false;
In your function.
yourfunction(){
setState(){
isLoading = true;
}
setState(){
isLoading = false;
}
}
In your widget.
isLoading?CircularProgressIndicator():Widget()

Flutter setState not updating child element

I have an InkWell which uses onTap to perform some actions. When the button is tapped, I like an indicator to be shown (in case the action is long-running). However, the setState in the InkWell does not trigger its children to be re-rendered. The code is as follows:
class PrimaryButtonState extends State<PrimaryButton> {
bool _apiCall;
Widget getWidget() {
if(_apiCall) {
return new CircularProgressIndicator();
} else {
return Text(
widget.label,
);
}
}
#override
Widget build(BuildContext context) {
final List<Color> colors = //omitted
return InkWell(
child: Container(
decoration: // omitted
child: getWidget(), // not updated when _apiCall changes !!!!!
),
onTap: () {
setState(() {
_apiCall = true;
});
widget.onTab(context);
setState(() {
_apiCall = false;
});
}
);
}
}
How can I solve this that getWidget returns the correct widget dependent on _apiCall?
EDIT:
The widget.onTap contains the following:
void performLogin(BuildContext context) {
final String userName = _userName.text.trim();
final String password = _password.text.trim();
UserService.get().loginUser(userName, password).then((val) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => MainLayout()));
}).catchError((e) {
// omitted
});
}
it is passed with the widget:
class PrimaryButton extends StatefulWidget {
final bool isPrimary;
final String label;
final Function(BuildContext context) onTab;
PrimaryButton(this.label, this.isPrimary, this.onTab);
#override
State<StatefulWidget> createState() => PrimaryButtonState();
}
My main concern is, that the given onTap method should not know it is "bound" to a UI widget and therefore should not setState. Also, as this is a general button implementation I like it to be interchangeable (therefore, onTap is not hardcoded)
It looks like your problem is because you are calling setState() twice in your onTap() function. Since onTap() is not an async function it will set _apiCall = true in the first setState, then immediately run widget.onTab(context) and then immediately perform the second setState() to set _apiCall = false so you never see the loading widget.
To fix this you will need to make your onTab function an async function and await for a value in your onTap function for your InkWell:
onTap: () async {
setState(() {
_apiCall = true;
});
await widget.onTab(context);
setState(() {
_apiCall = false;
});
}
This will also let you use the results of your onTab function to show errors or other functionality if needed.
If you are unsure how to use async functions and futures here is a good guide on it that goes over this exact kind of use case.

Infinite loop on using FutureBuilder with API call

I am trying to populate my ListView with the result from an API. The API call must take place after the values have been retrieved from Shared Preference. However on execution my function for API call runs an infinite loop and the UI doesn't render. I tracked this behaviour through debug statements.
The circular indicator that should be shown when Future builder is building UI is also not showing.
How can I resolve this?
My code:
class _MyHomePageState extends State<MyHomePage>{
#override MyHomePage get widget => super.widget;
String userID = "";
String authID = "";
//Retrieving values from Shared Preferences
Future<List<String>> loadData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> l= new List<String>();
if(prefs.getString("ID") == null){
l.add("null");
}
else{
l.add(prefs.getString("ID"));
}
if(prefs.getString("authID") == null){
l.add("null");
}
else{
l.add(prefs.getString("authID"));
}
return l;
}
//Setting values retrieved from Shared Pref
setData() async{
await loadData().then((value) {
setState(() {
userID = value[0];
print('the user ID is' + userID);
authID = value[1];
print('the authID is' + authID);
});
// getAllTasks(userID, authID);
});
print("Set data execution completed ");
}
//FUNCTION to use values from Shared Pref and make API Call
Future<List<Task>> getAllTasks() async{
await setData();
//Waiting for Set Data to complete
print('Ive have retrived the values ' + userID + authID );
List<Task> taskList;
await getTasks(userID, authID, "for_me").then((value){
final json = value;
if(json!="Error"){
Tasks tasks = tasksFromJson(json); //of Class Tasks
taskList = tasks.tasks; //getting the list of tasks from class
}
});
if(taskList != null) return taskList;
else {
print('Tasklist was null ');
throw new Exception('Failed to load data ');
}
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context){
_signedOut(){
widget.onSignedOut();
}
//To CREATE LIST VIEW
Widget createTasksListView(BuildContext context, AsyncSnapshot snapshot) {
var values = snapshot.data;
return ListView.builder(
itemCount: values == null ? 0 : values.length,
itemBuilder: (BuildContext context, int index) {
return values.isNotEmpty ? Ink(....
) : CircularProgressIndicator();
},
);
}
//MY COLUMN VIEW
Column cardsView = Column(
children: <Widget>[
....
Expanded(
child: FutureBuilder(
future: getAllTasks(),
initialData: [],
builder: (context, snapshot) {
return createTasksListView(context, snapshot);
}),
),
],
);
return Scaffold(
body: cardsView,
);
}
}
Instead of being called once... my setData function is being called repeatedly.. How can I resolve this..please help
You're creating Future object on every rebuild of the widget. And since you're calling setState inside your setData method, it triggers a rebuild recursively.
To solve this problem you have to keep a reference to the Future object. And use that reference for the FutureBuilder then it can understand that it is the previously used one.
E.g:
Future<List<Task>> _tasks;
#override
void initState() {
_tasks = getAllTasks();
super.initState();
}
And in your widget tree use it like that:
Expanded(
child: FutureBuilder(
future: _tasks,
initialData: [],
builder: (context, snapshot) {
return createTasksListView(context, snapshot);
}),
),
The FutureBuilder widget that Flutter provides us to create widgets based on the state of some future, keeps re-firing that future every time a rebuild happens!
Every time we call setState, the FutureBuilder goes through its whole life-cycle again!
One option is Memoization:
Memoization is, in simple terms, caching the return value of a function, and reusing it when that function is called again.
Memoization is mostly used in functional languages, where functions are deterministic (they always return the same output for the same inputs), but we can use simple memoization for our problem here, to make sure the FutureBuilder always receives the same future instance.
To do that, we will use Dart’s AsyncMemoizer.
This memoizer does exactly what we want! It takes an asynchronous function, calls it the first time it is called, and caches its result. For all subsequent calls to the function, the memoizer returns the same previously calculated future.
Thus, to solve our problem, we start by creating an instance of AsyncMemoizer in our widget:
final AsyncMemoizer _memoizer = AsyncMemoizer();
Note: you shouldn’t instantiate the memoizer inside a StatelessWidget, because Flutter disposes of StatelessWidgets at every rebuild, which basically beats the purpose. You should instantiate it either in a StatefulWidget, or somewhere where it can persist.
Afterwards, we will modify our _fetchData function to use that memoizer:
_fetchData() {
return this._memoizer.runOnce(() async {
await Future.delayed(Duration(seconds: 2));
return 'REMOTE DATA';
});
}
Note: you must wrap inside runOnce() only the body, not the funciton call
Special thanks to AbdulRahman AlHamali.
You need to save the Future in the State because doing getAllTasks() is triggering the call on every build callback.
In the initState:
this.getAllTasksFuture = getAllTasks();
Then you would use this Future property in the FutureBuilder.