I am writing a Flutter app that uses a Global Function to handle Pushy.me notifications. This function needs to update a stateful widget's state.
I have tried a Global Key to access the widgets current state but it did nothing. I have tried an Eventify emitter, the emit and the listener didnt seem to line up.
import 'package:eventify/eventify.dart';
EventEmitter emitter = new EventEmitter();
GlobalKey<_WrapperScreenState> _key = GlobalKey<_WrapperScreenState>();
void backgroundNotificationListener(Map<String, dynamic> data) {
// Print notification payload data
print('Received notification: $data');
// Notification title
String notificationTitle = 'MyApp';
// Attempt to extract the "message" property from the payload: {"message":"Hello World!"}
String notificationText = data['message'] ?? 'Hello World!';
Pushy.notify(notificationTitle, notificationText, data);
emitter.emit('updateList',null,"");
try{
print(_key.currentState.test);
}
catch(e){
print(e);
}
// Clear iOS app badge number
Pushy.clearBadge();
}
class WrapperScreen extends StatefulWidget {
#override
_WrapperScreenState createState() => _WrapperScreenState();
}
You can try using events for this, using a StreamController or the event bus package. Your stateful widget would listen on a global event bus, and you can fire an event with the necessary information for the widget itself to use to update the state.
Something like this (using the event bus package):
// main.dart
EventBus eventBus = EventBus();
class MyEvent {}
void somewhereGlobal() {
// trigger widget state change from global location
eventBus.fire(MyEvent());
}
void main() {
...
}
// my_stateful_widget.dart
...
class _MyWidgetState extends State<MyWidget> {
#override
void initState() {
super.initState();
eventBus.on<MyEvent>().listen((event) {
// update widget state
print("update widget state");
});
}
...
Related
This stateful widget is attached to a tab bar.
The first time it appears, it successfully listens to the stream.
But when I tap on another tab and tap again on the radar tab, I get the Bad state: Stream has already been listened to. error message.
Here are the logs:
flutter: init state radar: stream subscription is null
flutter: init state radar: stream subscription
flutter: init state radar: stream subscription is null
flutter: init state radar: stream subscription
Bad state: Stream has already been listened to.
I wonder
why dispose is never called,
why the subscription is no permitted in the state either.
How should I handle this use case?
/// The gas station screen
class GasStationRadarScreen extends StatefulWidget {
GasStationRadarScreen({required Key key, required this.title})
: super(key: key);
final String title;
#override
_GasStationRadarScreenState createState() => _GasStationRadarScreenState();
}
class _GasStationRadarScreenState extends State<GasStationRadarScreen> {
/// The gas stations list to be shown
GasStationsList _gasStations = [];
/// The sbscription to the stream
StreamSubscription? _gasStationsListSubscription;
#override
void initState() {
super.initState();
print(
'init state radar: stream subscription is $_gasStationsListSubscription');
/// The stream controller that brings the gas stations list whenever it changes
if (_gasStationsListSubscription == null) {
print('init state radar: stream subscription');
_gasStationsListSubscription =
GasStationRadar.shared.streamController.stream.listen((gasStations) {
setState(() {
_gasStations = gasStations;
});
});
}
}
#override
void dispose() {
print('disposing radar: cancelling subscription');
// == Stop listening the stream
_gasStationsListSubscription?.cancel();
_gasStationsListSubscription = null;
super.dispose();
}
}
I fixed the issue by createing:
/// The stream controller that pushes gas stations list whenever the vehicle moves
StreamController<GasStationsList> streamController =
StreamController<GasStationsList>.broadcast();
My code then becomes:
/// The gas station radar singleton that holds the broadcast stream controller
class GasStationRadar {
StreamController<GasStationsList> streamController =
StreamController<GasStationsList>.broadcast();
}
/// The stateful widget that subscribes to the stream
class _GasStationRadarScreenState extends State<GasStationRadarScreen> {
/// The gas stations list to be shown
GasStationsList _gasStations = [];
/// The subscription to the stream
StreamSubscription? _gasStationsListSubscription;
#override
void initState() {
super.initState();
print(
'init state radar: stream subscription is $_gasStationsListSubscription');
/// The stream controller that brings the gas stations list whenever it changes
if (_gasStationsListSubscription == null) {
print('init state radar: stream subscribe');
_gasStationsListSubscription =
GasStationRadar.shared.streamController.stream.listen((gasStations) {
setState(() {
_gasStations = gasStations;
});
});
}
}
#override
void dispose() {
print('disposing radar: cancelling subscription');
// == Stop listening the stream
_gasStationsListSubscription?.cancel();
_gasStationsListSubscription = null;
super.dispose();
}
In our system we used Pusher on both of our Admin Panel and mobile application, in flutter i want to wrap whole application screens with Pusher and Riverpod and using providers with Hook widget in screens, that means i want to implement Pusher in main or MyApp and broadcast received data from Pusher on all of screens which we have them, basically we had to use Pusher in each screen which we need to receive data and its difficult to maintain application, for example:
this sample code is not correct and i tried to implement that, but i can't
here as you can see Pusher events and listeners work fine without any problem:
D/PusherClientPlugin( 7447): Event stream cancelled.
D/PusherClientPlugin( 7447): Event stream listening...
D/PusherClientPlugin( 7447): Event stream cancelled.
D/PusherClientPlugin( 7447): Event stream listening...
D/PusherClientPlugin( 7447): [BIND] new-login
D/PusherClientPlugin( 7447): Event stream cancelled.
D/PusherClientPlugin( 7447): Event stream listening...
D/PusherClientPlugin( 7447): Event stream cancelled.
D/PusherClientPlugin( 7447): Event stream listening...
D/PusherClientPlugin( 7447): [BIND] new-login
D/PusherClientPlugin( 7447): [ON_EVENT] Channel: backers-list, EventName: new-login,
D/PusherClientPlugin( 7447): Data: Sending Test Data , User Id: null
I/flutter ( 7447): received
SpashScreen class as initializing Pusher
class SplashScreen extends HookWidget{
#override
Widget build(BuildContext context) {
_routeNavigation();
final PusherClient pusher = PusherClient(
"xxxx",
PusherOptions(
cluster: 'us2',
));
final Channel channel;
pusher.connect();
channel = pusher.subscribe("backers-list");
channel.bind("new-login", (event) {
print('received');
context
.read(alarmNotificationStateProvider.notifier)
.increment('${event?.data.toString()}');
});
return Scaffold(
/* ... */
Profile screen should be receive data from Pusher which i implemented that into SplashScreen
class Profile extends HookWidget {
#override
Widget build(BuildContext context) {
final alarmNotification = useProvider(alarmNotificationStateProvider);
print('PUSH RECEIVED');
/* ... */
}
alarmNotificationStateProvider :
final alarmNotificationStateProvider = StateNotifierProvider<AlarmNotification, AlarmNotificationData>(
(ref) => AlarmNotification(),
);
AlarmNotification class:
class AlarmNotification extends StateNotifier<AlarmNotificationData> {
AlarmNotification() : super(_initialValue);
static const _initialValue = AlarmNotificationData('');
void increment(String data) {
state = AlarmNotificationData(data);
}
}
AlarmNotificationData class:
class AlarmNotificationData {
final String data;
const AlarmNotificationData(this.data);
}
To listen changes in riverpod you require useProvider(provider);
The useProvider method will listen for state changes in provider, which is the current instance of your model class
This also gives you a reference to the notifier state, which you’re storing in model class
use this:
final alarmNotificationNotifier = useProvider(alarmNotificationProvider);
instead of final alarmNotificationNotifier = context.read(alarmNotificationProvider);
the read method gets you a reference to your state management class (CounterNotifier) without listening for changes in the state
I haven't seen your code of AlarmNotification, so I use a simple version below.
class AlarmNotification extends ChangeNotifier{
var _data;
String get data => _data;
void receivedData(String data){
_data = data;
notifyListeners();
}
}
I think you misunderstand a little about riverpod. The MyApp seems only provide the event data but not consume it. So the part you can just use StatelessWidget.(It is also suggested to use "context.read(someProvider)" if you only read not watch)
final alarmNotificationProvider = ChangeNotifierProvider((ref)=>AlarmNotification());
class MyApp extends StatelessWidget{
const MyApp({Key? key}):super(key: key);
#override
Widget build(BuildContext context) {
...
channel.bind("new-login", (event) {
context
.read(alarmNotificationProvider)
.receivedData('${event?.data.toString()}');
});
...
}
}
And the other widgets that need the data just listen to the alarmNotificationProvider (you may need to import the file to see it). It will rebuild the widget once it gets the notification.
class OtherWidget extends HookWidget {
const OtherWidget({Key? key}):super(key: key);
#override
Widget build(BuildContext context) {
final alarmNotification = useProvider(alarmNotificationProvider);
return Text(alarmNotification.data);
}
}
We are also using pusher. We are using flutter_pusher package and notifications work as expected. you can initialize it by
e.g.
Pusher.init('API_KEY', PusherOptions(encrypted:true));
Try it this way,
/// Code
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<SettingsProvider>(
create: (context) => SettingsProvider(),
),
],
child: MyApp(),
),
);
///
class SettingsProvider extends ChangeNotifier {
Locale _currentLocale = Locale("en", "");
String _languageCode = "en";
Locale get getCurrentLocale => _currentLocale;
String get getLanguageCode => _languageCode;
void updateLocale(Locale locale) {
Log.loga(title, "updateLocale: >>>> $locale");
Log.loga(title, "updateLocale: toLanguageTag >>>> ${locale.toLanguageTag()}");
this._currentLocale = locale;
AppTranslations.load(locale);
this._languageCode = locale.toLanguageTag();
//notifyListeners();
Future.delayed(Duration(seconds: 0)).then((value) {
super.notifyListeners();
});
}
}
/// update data
Provider.of<SettingsProvider>(context, listen: false).updateLocale(locale);
/// get data
Provider.of<SettingsProvider>(context).getCurrentLocale
make sure both screeens are under the changenotifier in widget tree,
other option is pass your provider to login screen as argument and use changenotifier.value() in login screen,
everyone. I am new in Flutter and BLoC pattern.
I needed to implement contact page so I created event GetContacts and passed it into context.read().add() after that I called this event into initState() of contacts screen.
Here my event:
abstract class ContactEvent extends Equatable {
const ContactEvent([List props = const []]) : super();
}
class GetContacts extends ContactEvent {
const GetContacts() : super();
#override
List<Object> get props => [];
}
Here is my bloc:
class ContactsBloc extends Bloc<ContactEvent, ContactsState> {
final ContactsRepo contactsRepo;
ContactsBloc({required this.contactsRepo}) : super(ContactInitial());
#override
Stream<ContactsState> mapEventToState(ContactEvent event,) async* {
yield ContactsLoading();
//
// if (event is UpdatePhoto) {
// yield PhotoLoading();
//
// print("LOADING STARTED");
//
// final photo = await contactsRepo.updatePhoto(event.identifier, event.photo);
// print("LOADING FINISHED");
//
// yield PhotoLoaded(photo: photo);
// }
if (event is GetContacts) {
print("get contacts photoBloc");
try {
final contacts = await contactsRepo.getContacts();
yield ContactsLoaded(contacts);
} on AccessException {
yield ContactsError();
}
}
}
}
That works right and contacts page renders contacts as it is supposed.
[contacts screen][1]
[1]: https://i.stack.imgur.com/Gx3JA.png
But then I decided to implement new feature: when user clicks on any contact he is offered to change its photo.
If I understand BLoC pattern correctly then if I want to change my state I need to create new event. Then I created new action UpdatePhoto and passed it into the same Bloc as it shown at 2nd part of code (in comments). Exactly there I encounter a misunderstanding of architecture expansion. This action is not supposed to return ContactsLoaded state so when I tried to catch this into my another bloc builder it broke my previous bloc builder that caught GetContact event.
ContactState:
abstract class ContactsState extends Equatable {
const ContactsState([List props = const []]) : super();
}
// class PhotoLoading extends PhotoState {
// #override
// List<Object?> get props => [];
// }
//
// class PhotoLoaded extends PhotoState {
// final Uint8List photo;
// const PhotoLoaded({required this.photo});
// #override
// List<Object?> get props => [photo];
// }
class ContactInitial extends ContactsState {
#override
List<Object> get props => [];
}
class ContactsLoading extends ContactsState {
#override
List<Object> get props => [];
}
class ContactsLoaded extends ContactsState {
final List<MyContact> contacts;
ContactsLoaded(this.contacts) : super([contacts]);
#override
List<Object> get props => [contacts];
}
class ContactsError extends ContactsState {
#override
List<Object?> get props => [];
}
Question: If I want to create new event (for example UpdatePhoto) which is not supposed to return the state that I caught before at the same bloc then I need to create new bloc for that purpose and cover my screen by multiProvider?
You should also post your ContactState code.
However you do not necessarely need a new Bloc. It all depends on what you are trying to achieve.
I suppose than when you yield PhotoLoading() you want to show a loader.
But when you update the photos, if I understand what you are trying to achieve you should yield an updated list of contacts using again yield ContactsLoaded(contacts) or add(GetContacts())instead of yield PhotoLoaded(photo: photo).
If you want to show a confirmation message, you can keep your PhotoLoaded state, but you need to build your UI taking into account the different state the bloc may emit.
Remember in BloC architecture event can yield to multiple states in successions and the UI decide if and how to react to each state.
I guess use optional parameter buildWhen in BlocBuilder is the best way to avoid creating new bloc for each event.
I am new to flutter and I am woking on sockets which keeps streaming data which I need to update on a appbar. So I have to stateful widgets
class _HomePageState extends State<HomePage> {
TickerAppBar appBar = TickerAppBar();
//Some declarations
#override
void initState() {
// TODO: implement initState
super.initState();
connectionSetup();
}
connectionSetup() async {
this.socket.connect(connectionString);
this.socket.sendSubscription(subscriptionString);
await processData();
}
Future<Null> processData() async{
subscription = socket.streamController.stream.listen(
(data) {
appBar.setStreamData(data); //Passing data into appbar widget
//rest of the code of current widget
}
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBar,
//rest of the code
)
}
}
class TickerAppBar extends StatefulWidget implements PreferredSizeWidget {
String streamData;
_TickerAppBarState child = _TickerAppBarState();
setStreamData(data){
this.streamData = data;
child.processData(); //To invoke the function every time the data is passed
}
}
class _TickerAppBarState extends State<TickerAppBar>{
Future<String> processData() async{
//Cannot set state here since its a constructor and the widget is not mounted.
}
}
So how I can set the data every time the data is passed into tickerappbar? so that my custom appbar displays new data which comes in every second.
I would suggest you follow the BLoC pattern, in which you can define your streams at one place and subscribe to the said stream from any widget, once you have the data ready, you can write to the stream and it will be shared on all the subscribed widgets.
let's say I want to check for internet connection every time I call Api, if there's no internet the call with throw exception like NoInternetException and then show a state screen to the user tells him to check their connection.
How can I achieve that without creating a new state for every bloc in flutter_bloc library?
You can do this in the bloc that manages your root pages like authentication_page and homepage.
Create a state for noConnectivity.
NoConnectivity extends AuthenticationState{
final String message;
const NoConnectivity({ this.message });
}
Now create an event for noConnectivity.
NoConnectivityEvent extends AuthenticationEvent{}
Finally, create a StreamSubscription in your AuthenticationBloc to continuously listen to connecitvityState change and if the state is connectivity.none we'll trigger the NoConnecitivity state.
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
StreamSubscription subscription;
#override
AuthenticationState get initialState => initialState();
#override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event,
) async* {
// ... all other state map
else if(event is NoConnectivityEvent) {
yield* _mapNoConnectivityEventToState();
}
Stream<AuthenticationState> _mapNoConnectivityEventToState() async * {
subscription?.cancel();
//Edit to handle android internet connectivity.
subscription = Connectivity()
.onConnectivityChanged
.listen((ConnectivityResult result) {
if(Platform.isAndroid) {
try {
final lookupResult = InternetAddress.lookup('google.com');
if (lookupResult.isNotEmpty && lookupResult[0].rawAddress.isNotEmpty) {
print('connected');
}
} on SocketException catch (error) {
return add(NoConnectivityState(message: error.message ));
}
} else if(result == ConnectivityResult.none ) {
return add(NoConnectivityState(message: "Noconnection")) ;
}
print("Connected");
});
}
#override
Future<void> close() {
subscription?.cancel();
return super.close();
}
}
This subscription Stream will forever listen to a no connection and push the appropriate page you like to the state.
Required packages
rxdart
connectivity
Hope it helps!
you need base class bloc let's say his name "BaseBloc" and he shall inherit from the "Bloc" class, and implement "mapToStateEvent" method to process "noInternet" exception, and after that call method let's say his name "internalMapToStateEvent" you created, this method it's override method, and inherited all your bloc class from "BaseBloc" and you need same that for pages to draw one widget "noInternetWidget"