Streambuilder only fire once - flutter

I am having a problem where my stream builder is only firing once.
I am trying to configure my bottomNavigationBar to be of a different colour based on the theme selected by the user.
To do this, I have a page whereby the user can decide whether to use the light theme or dark theme. This is saved into the device while shared preferences and then using async, i will stream the current value into my bottomNavigationBar.
The problem occurs when i use a stream builder to create two if statement. Stating that if the value returned from the stream is 0, i will show a "light mode" bottom navigation bar. Else if its 1, i will show a dark theme.
All is well when i run the program for the first time. However upon navigation into the settings page and changing the user preference, the stream builder will not load again. Here are some snapshots of my code
I have tried removing the dispose method whereby the stream will close. However that didn't solve the problem.
The Stream Builder
class mainPagev2 extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _mainPageV2();
}
}
class _mainPageV2 extends State<mainPagev2>
with SingleTickerProviderStateMixin {
// TabController _tabController;
StreamController<int> streamController = new StreamController.broadcast();
#override
void initState() {
super.initState();
// _tabController = TabController(vsync: this, length: _pageList.length);
Stream<int> stream = new Stream.fromFuture(readCurrentTheme());
streamController.addStream(stream);
}
#override
void dispose() {
// _tabController.dispose();
super.dispose();
}
String currentColor = "#ab3334";
#override
Widget build(BuildContext context) {
// TODO: implement build
return StreamBuilder(
stream: streamController.stream,
builder: (context, asyncSnapshot) {
print(asyncSnapshot.data.toString() + "WHssssAT IS THIS");
if (asyncSnapshot.hasData) {
print(asyncSnapshot.error);
if (asyncSnapshot.data == 0) {
//Return light themed Container
currentColor = "#ffffff";
return ThemeContainer(color: currentColor );
} else {
currentColor = "#101424";
//Return dark themed Container
return ThemeContainer(color: currentColor );
}
} else {
//return dark themed
return ThemeContainer(color:currentColor);
}
},
);
//
}
}
Async Code to retrieve the value stored
Future<int> readCurrentTheme() async {
final prefs = await SharedPreferences.getInstance();
final key = 'themeMode';
final value = prefs.getInt(key) ?? 0;
print('read: $value LOOK AT THISSS');
return value;
}
It is expected that the stream builder will fire whenever the value stored is changed!

I don't see in your code a way to read data from SharedPreferences when the value stored is changed. You are effectively reading it once, so the StreamBuilder is only firering once. That makes sense.
To be able to do what you want, you have to use something to tell you widget that a state has changed elsewhere in the application. There a multiple ways to achieve this and I won't make the choice for you as it would be opinion based, so you can check thing like BloC, Provider, ScopedModel, InheritedWidget

Related

Flutter Getx builder not updating UI

I'm trying to use a GetX Builder in combination with a bool in the controller to display a loading spinner while fetching data and then the data afterwards.
Printing the update to the bool shows it finishes loading and changes the variable but the UI is never updated to reflect this.
Controller
class AuthController extends GetxController {
//final RxBool isLoading = true.obs;
//var isLoading = true.obs;
final Rx<bool> isLoading = Rx<bool>(true);
setLoading(bool status) {
isLoading.value = status;
print(isLoading.value);
update();
}
fetchUserData() async {
setLoading(true);
_firebaseUser.bindStream(_auth.authStateChanges());
if (_auth.currentUser != null) {
bool profileRetrieved =
await FirestoreDB().getUserProfile(_auth.currentUser!.uid).then((_) {
return true;
});
setLoading(!profileRetrieved);
}
}
}
Profile Page
class ProfileCard extends StatelessWidget {
const ProfileCard({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetBuilder<AuthController>(
init: AuthController(),
initState: (_) => AuthController().fetchUserData(),
builder: (controller) {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return Container(...);
},
);
}
}
That widget is called within another as part of the wider UI. Let me know if you'd need to see that and/or anything else.
As you can see I've tried different ways of setting up the bool and none of them seem to trigger a change of UI in the builder.
Probably doing something stupid but I've tried a few different approaches and looked around for people having similar problems but not been able to crack it.
Thanks in advance.
You are using isLoading as a Rx<bool>. In that case you need to change GetBuilder into GetX. And no need to call update().
GetBuilder is for non-reactive, non-Rx, simple state management
GetX is for reactive, Rx state management

Flutter control tab programmatically via Provider

I am unable to figure out how I can listen to changes in a class that uses the ChangeNotifier class. More specifically, outside the widget build method. So far I've tried simply attaching listeners to the class like so:
final providerObj = SomeProvider();
void initState() {
providerObj.addListener(() {
print("Something happens");
});
}
But this does nothing. I don't know why. I got it working once, but afterwards it just died on me. Nothing gets triggered, when there is a change in the provider class' variable values.
Here's a snippet of the SomeProvider
class SomeProvider with ChangeNotifier {
int _tabIndex = 0;
List<Tab> _tabs = [];
Tab _searchTab = new Tab(id: 0, name: 'Search');
void searchItems(String searchWord) {
_searchTab.items.addAll(_items
.where((item) => product.item.contains(searchWord))
.toList());
setTabIndex(0);
}
void setTabIndex(int index) {
_tabIndex = index;
notifyListeners();
}
/// Get all products
List<Category> get tabs {
return [..._tabs];
}
int get tabIndex {
return _tabIndex;
}
What I'm trying to accomplish is simple: Control the selected tab, when the selected index is in the SomeProvider class. Every time a tab is pressed, the index value gets saved in the provider. When the value changes, it should trigger an animateTo() call for a TabController instance.
The reason for this is because I have a separate search widget, which needs to change the tab to a Search tab. I just have no idea how I can listen to changes in the provider class outside the build() method and change the TabController index there.
So, I have a basic TabBarView to which I give a TabController instance.
class _ProductsState extends State<Products> with TickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(
vsync: this,
length: 0,
initialIndex: 0,
);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _fetchTabs(context) async {
await Provider.of<SomeProvider>(context, listen: false)
.fetchTabs();
_tabController = TabController(
vsync: this,
length: Provider.of<SomeProvider>(context, listen: false)
.tabs
.length,
);
}
Widget build(BuildContext context) {
// There's a FutureBuilder here etc.
...
child: TabBarView(
controller: _tabController,
...
What I'm trying to do is something like:
void triggeredWhenStateChanges() {
_tabController.animateTo(Provider.of<SomeProvider>(context, listen: false).tabIndex);
}
Little bit off-topic: I've used React before (not React-Native) and with it you can use something called a hook which can perform side effects when a value change is detected. With the useEffect hook I can normally execute some actions when a value in the state changes. I don't know how I can do this with Flutter, if it can be done at all.

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

Get a stream of permissions from a future

I'm trying to stream two types of location package based data:
final _location = Location();
runApp(
MultiProvider(
providers: [
StreamProvider<PermissionStatus>(create: (_) => _location.hasPermission().asStream()),
StreamProvider<bool>(create: (_) => _location.serviceEnabled().asStream()),
],
child: MaterialApp();
)
)
When I 'stream' the data, it loads the initial value and streams that. It is not continuously listening to changes which is what I want to do. I have tried abstracting both futures into their own class and creating an async* stream that yields the current status, both of which give the same problem.
My use case involves continuously listening to the permission status and location on/off and shut down certain parts of the UI when these are modified in between tasks.
Simplified usage:
class LocationWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer2<PermissionStatus, bool>(
builder: (_, permission, isLocationEnabled, __) => _LocationWidget(
permission: permission, isLocationEnabled: isLocationEnabled));
}
}
class _LocationWidget extends StatelessWidget {
const _LocationWidget({this.permission, this.isLocationEnabled})
: assert(permission != null);
final PermissionStatus permission;
final bool isLocationEnabled;
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: (() {
if (!isLocationEnabled) // Check Bool and show different text
return Text(
"Off",
);
else
return Text("On");
}())));
}
}
Add updateShouldNotify: (_, __) => true to your StreamProvider
By default, StreamProvider considers that the Stream listened uses immutable data. As such, it will not rebuild dependents if the previous and the new value are ==. To change this behavior, pass a custom updateShouldNotify.
ikerfah is right, you're creating a Stream from a Future, meaning the Stream will only contain a single event when the Future is completed (basically, it's not a real "stream" in the true sense of the word).
FutureBuilder won't work either, since the Future only gets completed once, so it will only trigger a single state change too.
If this is the plugin you're using, it seems the author hasn't implemented anything to expose a "real" Stream for permission change events. I wouldn't hold my breath for that either, because as far as I know neither iOS nor Android broadcast an event if/when permissions are changed.
If you need to disable/enable something based on whether permissions have changed, you'll just need to set a periodic Timer in a StatefulWidget to poll for changes.
class _LocationWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _LocationWidgetState();
}
}
class _LocationWidgetState extends State<_LocationWidget> {
PermissionStatus permission;
bool isLocationEnabled = false;
Timer _timer;
#override
void initState() {
_timer = Timer.periodic(Duration(seconds:5), (_) {
var permission = await _location.hasPermission();
var isLocationEnabled = await _location.serviceEnabled();
if(permission != this.permission || isLocationEnabled != this.isLocationEnabled)
setState(() {});
});
}
#override
void dispose() {
_timer.cancel();
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: (() {
if (!isLocationEnabled) // Check Bool and show different text
return Text(
"Off",
);
else
return Text("On");
}())));
}
It's up to you whether 5 seconds is an appropriate interval. initState() should probably also set the initial isLocationEnabled/permission values when the state is initialized, too, rather than waiting 5 seconds for the timer to kick in.
I believe checking your permissions in didChangeApplifecycle would do the job, As the user most certainly had to put the app in the background to change the permissions
Example
#override
void didChangeAppLifecycleState(AppLifecycleState state) async {
final loc = Location();
final isPermitted = await loc.hasPermission();
final isServiceEnabled = await loc.serviceEnabled()
// request permissions
}

Change state in one Widget from another widget

I'm programming a flutter application in which a user is presented with a PageView widget that allows him/her to navigate between 3 "pages" by swiping.
I'm following the setup used in https://medium.com/flutter-community/flutter-app-architecture-101-vanilla-scoped-model-bloc-7eff7b2baf7e, where I use a single class to load data into my model, which should reflect the corresponding state change (IsLoadingData/HasData).
I have a main page that holds all ViewPage widgets. The pages are constructed in the MainPageState object like this:
#override
void initState() {
_setBloc = SetBloc(widget._repository);
_notificationBloc = NotificationBloc(widget._repository);
leftWidget = NotificationPage(_notificationBloc);
middleWidget = SetPage(_setBloc);
currentPage = middleWidget;
super.initState();
}
If we go into the NotificationPage, then the first thing it does is attempt to load data:
NotificationPage(this._notificationBloc) {
_notificationBloc.loadNotificationData();
}
which should be reflected in the build function when a user directs the application to it:
//TODO: Consider if state management is correct
#override
Widget build(BuildContext context) {
return StreamBuilder<NotificationState>(
stream: _notificationBloc.notification.asBroadcastStream(),
//initialData might be problematic
initialData: NotificationLoadingState(),
builder: (context, snapshot) {
if (snapshot.data is NotificationLoadingState) {
return _buildLoading();
}
if (snapshot.data is NotificationDataState) {
NotificationDataState state = snapshot.data;
return buildBody(context, state.notification);
} else {
return Container();
}
},
);
}
What happens is that the screen will always hit "NotificationLoadingState" even when data has been loaded, which happens in the repository:
void loadNotificationData() {
_setStreamController.sink.add(NotificationState._notificationLoading());
_repository.getNotificationTime().then((notification) {
_setStreamController.sink
.add(NotificationState._notificationData(notification));
print(notification);
});
}
The notification is printed whilst on another page that is not the notification page.
What am i doing wrong?
//....
class _SomeState extends State<SomeWidget> {
//....
Stream<int> notificationStream;
//....
#override
void initState() {
//....
notificationStream = _notificationBloc.notification.asBroadcastStream()
super.initState();
}
//....
Widget build(BuildContext context) {
return StreamBuilder<NotificationState>(
stream: notificationStream,
//....
Save your Stream somewhere and stop initialising it every time.
I suspect that the build method is called multiple times and therefore you create a new stream (initState is called once).
Please try let me know if this helped.