So I'm having an issue with a bottom sheet that I'm trying to display.
the idea is that i want to display a bottom sheet and remove my bottom navigation bar when the bottom sheet shows. anyway, I've made a boolean called sheetOpen which is set to false initially and the idea is to set it to true in order to close the bottom navigation bar when the sheet pops up .
doing so without using setstate does not reflect any changes to the UI . But if I use set state in the show Bottom sheet function the app crashes and i get this message : 'Looking up deactivated widget's ancestor is unsafe. at this point the state of the widget's element tree is no longer stable . to safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.'
I have tried multiple solutions(Stateful Builder, calling _controller.setstate ..) but nothing works.
Been stuck at this for 3 days..
anyway i will show the code that i have written and i would really appreciate anyone who can help.
class FeedScreen extends StatefulWidget {
static bool sheetOpen = false;
static int selectedIndex = 0;
const FeedScreen();
#override
_FeedScreenState createState() => _FeedScreenState();
}
class _FeedScreenState extends State<FeedScreen> {
late final GlobalKey<ScaffoldState> _key;
late PersistentBottomSheetController _controller;
void _showPreview(
final BuildContext context,
) {
//this is what's causing the issue
setState(() {
FeedScreen.sheetOpen = true;
});
_controller = _key.currentState!.showBottomSheet(
(ctx) {
//etc....
}
#override
void initState() {
super.initState();
_key = GlobalKey<ScaffoldState>();
}
Widget build(BuildContext context) {
return Scaffold(
//....
bottomNavigationBar: FeedScreen.sheetOpen
? null
: BottomNavBar(
FeedScreen.selectedIndex,
_changeTab,
),
),
https://api.flutter.dev/flutter/widgets/StatefulBuilder-class.html?
you can wrap Stateful builder with Bottomsheet to use setState.
wrap it with statefulbuilder
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return BottomNavBar(
FeedScreen.selectedIndex,
_changeTab,
);}
Related
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!!!"));
}
},
);
}
TL;DR: I want to spawn a temporary overlay whenever there's a state change, and I don't really know how to approach it. (I'm using riverpod)
I have an OverlayEntry widget that I want to spawn whenever there's a state change in a StateProvider (I'm not married to anything, it doesn't have to be a StateProvider and this doesn't have to be a widget)
I tried messing around with ChangeNotifier and StateNotifier but to no avail. I'm having a tough time understanding them and I couldn't find any example of stuff that is similar to what I'm looking.
My current approach is to create a widget which is a builder for the main page, the widget for the main page should have a listener for the StateProvider and will build the main page widget. Whenever the listener is called, I want to spawn that overlay (by calling a method?)
But I can't seem to figure it out. This is my current code refs and ideas. this does not work.
CustomOverlay widget:
class CustomOverlay extends HookWidget {
final bool currentState;
PlayerOverlay({this.currentState});
#override
Widget build(BuildContext context) {
final controller = useAnimationController(
duration: Duration(milliseconds: 500);
Animation<double> _fadeInFadeOut = createCurvedAnimation(controller);
controller.addStatusListener((status) => loopAnimation(status, controller));
return createFadeTransition(context, _fadeInFadeOut);
}
...
}
MainPageBuilder
class PlayerPageBuilder extends HookWidget {
final CustomOverlay playerOverlay;
final MainPage playerPage;
MainPageBuilder(this.customOverlay, this.mainPage);
#override
Widget build(BuildContext context) {
final currentState = useProvider
currentStateProvider.addListener(() { // obviously this doesnt exist, and I'm having a tough time figuring it out (how to use StateNotifierProvider or anything that fits)
spawnOverlayAnimation(context, customOverlay);
});
return mainPage;
}
spawnPlayerOverlayAnimation(BuildContext context, Widget child) async {
OverlayState overlayState = Overlay.of(context);
OverlayEntry overlayEntry = OverlayEntry(
builder: (context) => Positioned(
child: child,
),
);
overlayState.insert(overlayEntry);
await Future.delayed(Duration(milliseconds: 500));
overlayEntry.remove();
}
}
So, i have a widget with infinite width (Row) . To fill the widget with items i'm using a Lisview builder with Axis horizontal. I also can use a SingleChildScrollview with axis horizontal and a Row as the child.
If there is a few items, the width of the screen is not filled, so that's great. However, when there is a lot of items, the Listview becomes "scrollable" , and i can scroll to the right to reveal the items.
I would like to know if the list is scrollable (if it overflows). The purpose is to show a little text saying "Scroll to reveal more>>".
I know i can use maths and calculate the items width with the screen width. However... The Listview already knows this, so i was wondering if there was a way of getting access to that information
I had a similar problem. I found this solution here: Determine Scroll Widget height
Here's my question in case it's helpful: How do you tell if scrolling is not needed in a SingleChildScrollView when the screen is first built?
Solution:
Import the scheduler Flutter library:
import 'package:flutter/scheduler.dart';
Create a boolean flag inside the state object but outside of the build method to track whether build has been called yet:
bool buildCalledYet = false;
Create a boolean isScrollable variable inside the state object but outside of the build method:
bool isScrollable;
Add the following in the beginning of the build method:
if (!firstBuild) {
firstBuild = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
isScrollable = !(_scrollController.position.maxScrollExtent > 0);
});
});
}
(The purpose of buildCalledYet is to prevent this code from causing build to be called over and over again.)
If isScrollable is true, then show your text.
I created a widget called ScrollCheckerWidget with a controller property to pass the controller of a scrollable widget and check if it is scrollable in the view. The ScrollCheckerWidget widget has a builder callback function where you can return a widget and use the isScrollable boolean from the builder to check if the widget related to the controller is scrollable or not. Here you have the code:
import 'package:flutter/material.dart';
class ScrollCheckerWidget extends StatefulWidget {
const ScrollCheckerWidget({
Key? key,
required this.controller,
required this.builder,
}) : super(key: key);
final ScrollController controller;
final Widget Function(bool isScrollable) builder;
#override
State<EGScrollCheckerWidget> createState() => _EGScrollCheckerWidgetState();
}
class _EGScrollCheckerWidgetState extends State<EGScrollCheckerWidget> {
late final ScrollController _scrollController;
late bool _isScrollable;
#override
void initState() {
_scrollController = widget.controller;
_isScrollable = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_isScrollable = _scrollController.position.maxScrollExtent > 0;
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return widget.builder.call(_isScrollable);
}
}
In builder:
return NotificationListener(
onNotification: (scrollNotification) {
print("ScrollStartNotification");
// add your logic here
},
child: SingleChildScrollView(...
Am trying to detect exact same thing myself, wrapping SingleChildScrollView with NotificationListener at least gave me a point of reference to start working on this.
Is there any callbacks available in flutter for every time the page is visible on screen? in ios there are some delegate methods like viewWillAppear, viewDidAppear, viewDidload.
I would like to call a API call whenever the particular page is on-screen.
Note: I am not asking the app states like foreground, backround, pause, resume.
Thank You!
Specifically to your question:
Use initState but note that you cannot use async call in initState because it calls before initializing the widget as the name means. If you want to do something after UI is created didChangeDependencies is great. But never use build() without using FutureBuilder or StreamBuilder
Simple example to demostrate:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MaterialApp(home: ExampleScreen()));
}
class ExampleScreen extends StatefulWidget {
ExampleScreen({Key key}) : super(key: key);
#override
_ExampleScreenState createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
List data = [];
bool isLoading = true;
void fetchData() async {
final res = await http.get("https://jsonplaceholder.typicode.com/users");
data = json.decode(res.body);
setState(() => isLoading = false);
}
// this method invokes only when new route push to navigator
#override
void initState() {
super.initState();
fetchData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: isLoading
? CircularProgressIndicator()
: Text(data?.toString() ?? ""),
),
);
}
}
Some lifecycle method of StatefulWidget's State class:
initState():
Describes the part of the user interface represented by this widget.
The framework calls this method in a number of different situations:
After calling initState.
After calling didUpdateWidget.
After receiving a call to setState.
After a dependency of this State object changes (e.g., an InheritedWidget referenced by the previous build changes).
After calling deactivate and then reinserting the State object into the tree at another location.
The framework replaces the subtree below this widget with the widget
returned by this method, either by updating the existing subtree or by
removing the subtree and inflating a new subtree, depending on whether
the widget returned by this method can update the root of the existing
subtree, as determined by calling Widget.canUpdate.
Read more
didChangeDependencies():
Called when a dependency of this State object changes.
For example, if the previous call to build referenced an
InheritedWidget that later changed, the framework would call this
method to notify this object about the change.
This method is also called immediately after initState. It is safe to
call BuildContext.dependOnInheritedWidgetOfExactType from this method.
Read more
build() (Stateless Widget)
Describes the part of the user interface represented by this widget.
The framework calls this method when this widget is inserted into the
tree in a given BuildContext and when the dependencies of this widget
change (e.g., an InheritedWidget referenced by this widget changes).
Read more
didUpdateWidget(Widget oldWidget):
Called whenever the widget configuration changes.
If the parent widget rebuilds and request that this location in the
tree update to display a new widget with the same runtimeType and
Widget.key, the framework will update the widget property of this
State object to refer to the new widget and then call this method with
the previous widget as an argument.
Read more
Some widgets are stateless and some are stateful. If it's a stateless widget, then only values can change but UI changes won't render.
Same way for the stateful widget, it will change for both as value as well as UI.
Now, will look into methods.
initState(): This is the first method called when the widget is created but after constructor call.
#override
void initState() {
// TODO: implement initState
super.initState();
}
didChangeDependecies() - Called when a dependency of this State object changes.Gets called immediately after initState method.
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
didUpdateWidget() - It gets called whenever widget configurations gets changed. Framework always calls build after didUpdateWidget
#override
void didUpdateWidget (
covariant Scaffold oldWidget
)
setState() - Whenever internal state of State object wants to change, need to call it inside setState method.
setState(() {});
dispose() - Called when this object is removed from the tree permanently.
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
You don't need StatefulWidget for calling the api everytime the screen is shown.
In the following example code, press the floating action button to navigate to api calling screen, go back using back arrow, press the floating action button again to navigate to api page.
Everytime you visit this page api will be called automatically.
import 'dart:async';
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => ApiCaller())),
),
);
}
}
class ApiCaller extends StatelessWidget {
static int counter = 0;
Future<String> apiCallLogic() async {
print("Api Called ${++counter} time(s)");
await Future.delayed(Duration(seconds: 2));
return Future.value("Hello World");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Api Call Count: $counter'),
),
body: FutureBuilder(
future: apiCallLogic(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const CircularProgressIndicator();
if (snapshot.hasData)
return Text('${snapshot.data}');
else
return const Text('Some error happened');
},
),
);
}
}
This is the simple code with zero boiler-plate.
The simplest way is to use need_resume
1.Add this to your package's pubspec.yaml file:
dependencies:
need_resume: ^1.0.4
2.create your state class for the stateful widget using type ResumableState instead of State
class HomeScreen extends StatefulWidget {
#override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends ResumableState<HomeScreen> {
#override
void onReady() {
// Implement your code inside here
print('HomeScreen is ready!');
}
#override
void onResume() {
// Implement your code inside here
print('HomeScreen is resumed!');
}
#override
void onPause() {
// Implement your code inside here
print('HomeScreen is paused!');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Go to Another Screen'),
onPressed: () {
print("hi");
},
),
),
);
}
}
If you want to make an API call, then you must be (or really should be) using a StatefulWidget.
Walk through it, let's say your stateful widget receives some id that it needs to make an API call.
Every time your widget receives a new id (including the first time) then you need to make a new API call with that id.
So use didUpdateWidget to check to see if the id changed and, if it did (like it does when the widget appears because the old id will be null) then make a new API call (set the appropriate loading and error states, too!)
class MyWidget extends StatefulWidget {
Suggestions({Key key, this.someId}) : super(key: key);
String someId
#override
State<StatefulWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
dynamic data;
Error err;
bool loading;
#override
Widget build(BuildContext context) {
if(loading) return Loader();
if(err) return SomeErrorMessage(err);
return SomeOtherStateLessWidget(data);
}
#override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// id changed in the widget, I need to make a new API call
if(oldWidget.id != widget.id) update();
}
update() async {
// set loading and reset error
setState(() => {
loading = true,
err = null
});
try {
// make the call
someData = await apiCall(widget.id);
// set the state
setState(() => data = someData)
} catch(e) {
// oops an error happened
setState(() => err = e)
}
// now we're not loading anymore
setState(() => loading = false);
}
}
I'm brand new to Flutter (literally, just started playing with it this weekend), but it essentially duplicates React paradigms, if that helps you at all.
Personal preference, I vastly prefer this method rather than use FutureBuilder (right now, like I said, I'm brand new). The logic is just easier to reason about (for me).
I came from Android Development, so, I'll try to explain my situation in Android terms in some cases. So, I have the MainScreen, which uses Scaffold and has FAB. The body of this screen is another widget (as Fragment in Android). When I click on the FAB (which is on MainScreen), bottom modal is opened. By this model, I can add data to the database. But I also want to add new data to my widget (fragment) at the same time. And I don't know in which method of lifecycle I should do call to DB. In android such method is onResume, but I didn't find it's analogue in Flutter.
I'll appreciate any help, thanks in advance!
UPD
There's my screen:
Bottom navigation and FAB is on the MainScreen. ListView is on the widget, which is body of MainScreen's Scaffold. Another my step is clicking on FAB. This click opens bottom modal
When I click save on the modal, data is saved to DB and modal close. And after this closing I want to see new database entry, which I just added from the modal. Now I can see new notes only after closing and then opening screen again. Here's my code:
class NotesScreen extends StatefulWidget {
#override
_NotesScreenState createState() => _NotesScreenState();
}
class _NotesScreenState extends State<NotesScreen> with WidgetsBindingObserver {
List<Note> _notes = new List<Note>();
#override
void initState() {
super.initState();
Fimber.i("Init");
WidgetsBinding.instance.addObserver(this);
NotesDataManager().getNotes().then((value) {
_notes = value;
setState(() {
});
});
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state){
case AppLifecycleState.resumed:
NotesDataManager().getNotes().then((value){
_notes = value;
setState(() {
});
});
Fimber.i("Resumed");
break;
case AppLifecycleState.inactive:
Fimber.i("Inactive");
break;
case AppLifecycleState.paused:
Fimber.i("Paused");
break;
case AppLifecycleState.suspending:
Fimber.i("Suspending");
break;
}
super.didChangeAppLifecycleState(state);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(AppColors.layoutBackgroundColor),
body: Container(
child: ListView.builder(
itemCount: _notes.length,
itemBuilder: (BuildContext context, int index) {
return NotesListItemWidget(note: _notes[index]);
},
),
),
);
}
}
As you can see, I added WidgetsBindingObserver, but it didn't help
You might want to use state management in your app. The easiest one is Provider. Your model should extend ChangeNotifier and whenever you add a new instance of model the app will be notified: notifyListener. In build you will implement ChangeNotifierProvider. Here is some tutorials:
Provider example 1
Provider example 2
Or you can just search for Provider