Flutter : trigger a change of screen on a websocket message - android

I'm currently working on a flutter app with a websocket.
I want to change the current screen with a Navigator.pushNamed(context, '/path') in the method that is triggered by the websocket on a new message. The problem is that I don't have any context in this method.
So how to change the current screen from this method ?
I dont know if I'm thinking in the wrong way or if I just don't understand something.
Here is the method that is triggered on a new websocket message, his class is not a widget. This object is contained in every screen I create but his variable _parent is always set to match the active screen.
onMessageReceived(data) {
print("new message: " + data + " !");
data = jsonDecode(data);
data.forEach((key, value) {
switch (key) {
case "state":
_parent.newState(value);
break;
}
});
}
Here is the method of the widget:
newState(state){
if(state == "start"){
Navigator.pushNamed(, "/path");
}
}
Thanks in advance

You can use a stream for that. You can register the websocket response, and when it returns you can add it to the stream, and then on your screen you use a StreamBuilder to subscribe to the stream and navigate when needed. Something like this:
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
// Create a stream to receive the values
var webSocketStream = new BehaviorSubject<String>();
onMessageReceived(data) {
print("new message: " + data + " !");
data = jsonDecode(data);
data.forEach((key, value) {
switch (key) {
case "state":
// Write to the stream
webSocketStream.add(value);
break;
}
});
}
class CheckWebSocket extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<String>(
// Subscribe to the stream
stream: webSocketStream,
builder: (context, snapshot) {
// When the stream changes navigate
if (snapshot.hasData && snapshot.data == "start") {
Navigator.pushNamed(context, "/path");
} else {
return Container();
}
}),
);
}
}
I used rxdart for that, because it makes dealing with streams simpler. I also recommend that you extract the code that reads the websocket to a separate class, I put everything in one file just to make it simpler to understand.

Related

Right way to use useEffect in react native

Here is my sample React component:
const OwnerView = () => {
const [monthlyCharge, setMonthlyCharge] = useState(0)
useEffect(() => {
getPerMonthCharges(ownerPhoneNumber, vehicles.length)
}, [])
async function getPerMonthCharges(ownerPhoneNumber, noOfCars) {
console.log(`inside getPerMonthCharges`);
try {
const serviceProviderChargesDoc = await firestore().collection(`${serviceProviderId}_charges`).doc(`${ownerPhoneNumber}`).get()
if (serviceProviderChargesDoc?.data()?.chargesPerMonth > 0) {
setMonthlyCharge(serviceProviderChargesDoc?.data()?.chargesPerMonth)
return
}
} catch (error) {
console.log(`Error while fetching monthly charge ${error}`);
}
setMonthlyCharge(noOfCars * perMonthGeneralCharge)
console.log(`done with getPerMonthCharges`);
}
}
There is a possibility that OwnerView gets unmounted even before getPerMonthCharges() completes its execution. Therefore in case OwnerView gets unmounted I receive a warning that am doing state update on an unmounted component and this is a non-op. Can someone please highlight what is your observation and right way to write this piece of code?
There are many ways to address this
You can check if the component is still Mounted, a bit ugly approach I agree, but quite a standard one (I would just use something like useAsync from react-use, which essentially does the same, but hides the ugliness)
Move loading logic outside of UI and make part of the global state (Redux, MobX, Apollo, or any other state management library), it would be in lines of separation of concerns and should make your code more readable.
The worst would be to prevent your user from any actions, while content is loading - making your app seem clunky, but React would not complain anymore.
The closest to the right way would be 2, but this can sparkle religious debates and some witch-burning, which I'm not a fan of.
You can refer to this: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup
You can have a variable to keep track whether your component has unmount, let isMounted = true inside useEffect and set it to false as soon as the component is unmounted.
The code will be:
useEffect(() => {
let isMounted = true;
async function getPerMonthCharges(ownerPhoneNumber, noOfCars) {
console.log(`inside getPerMonthCharges`);
try {
const serviceProviderChargesDoc = await firestore().collection(`${serviceProviderId}_charges`).doc(`${ownerPhoneNumber}`).get()
if (serviceProviderChargesDoc?.data()?.chargesPerMonth > 0 && isMounted) { // add conditional check
setMonthlyCharge(serviceProviderChargesDoc?.data()?.chargesPerMonth)
return
}
} catch (error) {
console.log(`Error while fetching monthly charge ${error}`);
}
if (isMounted) setMonthlyCharge(noOfCars * perMonthGeneralCharge) // add conditional check
console.log(`done with getPerMonthCharges`);
}
getPerMonthCharges(ownerPhoneNumber, vehicles.length)
return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);

How can I update a stream from within a StreamBuilder?

Thank you very much in advance for your help! Please let me know if the question is not clear i would be happy to add more details if needed.
I have a Finite State Machine that handles some audio recognition. This FSM is wrapped by a "manager" whose job is to handle the state transitions (processState, nextState). The FSM manager exposes a stream which is updated every time nextState is called
FSM/Manager layout
class FSM_Manager{
StreamController<RecognitionState> _stateStream =
StreamController<RecognitionState>();
Sink<RecognitionState> get _inState => _stateStream.sink;
Stream<RecognitionState> get outState => _stateStream.stream;
RecognitionState _currentState, _previousState;
void setState(RecognitionState state) {
_previousState = _currentState;
_currentState = state;
_addCurrentStateToStream();
}
void _addCurrentStateToStream() {
_inState.add(_currentState);
}
Future nextState() async {
_currentState.nextState(this);
}
Future processState(itemToRecognize) async {
await _currentState.processState(itemToRecognize);
}
}
abstract class BaseState {
RecognitionStateID get stateID; //enum with each state's ID
Future processState(itemToRecognize);
Future nextState(FSM_Manager manager);
}
class FSM_State1 implements BaseState{
bool isSuccess = false;
void processState(itemToRecognize) async {
isSuccess = await performRecognition(itemToRecognize);
}
void nextState (FSM_Manager fsmManager) {
if(isSuccess){
// go to next State
fsmManager.setState(NEXT_STATE);
} else {
//go to some other state
fsmManager.setState(SOME_OTHER_STATE);
}
}
}
class FSM_State2 implements BaseState{
bool isSuccess = false;
void processState(itemToRecognize) async {
isSuccess = await performRecognition(itemToRecognize);
}
void nextState () {
if(isSuccess){
// go to next State
} else {
// go to another State
}
}
}
I have a screen (Stateful Widget) which uses a StreamBuidler to listen to the "outState" stream in order to rebuild the screen with the information in the new State.
Stateful Widget
class _RecognitionScreenState extends State<RecognitionScreen> {
ItemToRecognize item;
var currStateiD;
FSM_Manager _fsmManager;
RecognitionScreenState(
ItemToRecognize item, FSM_Manager fsmManager) {
this.item = item;
this._fsmManager = fsmManager;
}
#override
Widget build(BuildContext context) {
String outString = '';
return StreamBuilder<RecognitionState>(
stream: _stateContext.outState,
builder: (BuildContext context,
AsyncSnapshot<RecognitionState> snapshot) {
if (snapshot.hasData) {
outString = snapshot.data.stateID.toString();
return Text(outString);
} else {
return Text('');
}
});
}
}
Now, I do not know where/how to call processState and nextState from, I cant do it from the build method so i Tried to use initState() and didUpdateWidget in the StatefulWidget so that the states are processed in the beginning and after every build respectively. This approach didnt work, the nextState method was never called. I feel like im missing something trivial but i just dont see where to call those functions from outside of the Stateful Widget in order to trigger a rebuild only after the state has changed
Thanks again for your help
EDIT
I apologize for the confusion,
I added the BaseState definition (just an abstract class with some method so that i dont forget to implement them)
the nextState method takes an FSM_Manager as a parameter and calls setState on success or failure and sets the next state
the States "implement" the BaseState class, they dont "extend" it
It's difficult for me to tell from your code because I don't know what your BaseState does but inside your builder - presumably from some event or callback - you would do:
inState.add(<--Some RecognitionState-->);
This would trigger the StreamBuilder to rebuild.
If everything else in your code is put together properly.

Upload to Firestore in background with Flutter

I am using christocracy's flutter_background_geolocation package to build a crowdsensing app. This app relies on the geofencing function of the aforementioned package quite heavily. In the main function, I have implemented a callback function that is as follows (partial code):
void _onGeofence(bg.GeofenceEvent event) async {
await showGeofenceNotification(flutterLocalNotificationsPlugin,
title: "Geofence", body: "$event", id: notification_id);
if (action == "ENTER") {
// update certain variables
BarometerService.startBarometerService();
BarometerService.floorChange.listen((floorChanges) {
// update floor
updateDatabase();
});
}
else if (action == "EXIT") {
// update certain variables
BarometerService.stopBarometerService();
}
updateDatabase();
setState(() {
// update UI
});
}
The code works perfectly when the app is open and in focus. However, when in background, the barometer service stops. The updateDatabase() function is also not carried out as my Firestore console doesn't get updated.
Here is the code for updating the database:
Future updateUserState(String matric, bool inLWN, bool inVaughan, String activity, int confidence, int floor) async {
return await userCollection.document(uid).setData({
'matric': matric,
'inLWN': inLWN,
'inVaughan': inVaughan,
'activity': activity,
'confidence': confidence,
'floor': floor,
});
}
And here is the code for BarometerService (which uses Flutter sensors plugin):
import 'package:sensors/sensors.dart';
static startBarometerService() {
Stream<BarometerEvent> barometer10Events = barometerEvents.throttle(Duration(seconds:PERIOD_SECONDS));
subscription = barometer10Events.listen(onBarometer);
streamController = new StreamController();
}
How do I make my services run even when app is closed or terminated? I have implemented the same code in my headless callback functions (except updating UI), but nothing besides updating my (local) variables and showing local flutter notifications is working.
Headless task for reference:
void headlessTask(bg.HeadlessEvent headlessEvent) async {
print('[BackgroundGeolocation HeadlessTask]: $headlessEvent');
switch(headlessEvent.name) {
case bg.Event.GEOFENCE:
bg.GeofenceEvent geofenceEvent = headlessEvent.event;
onHeadlessGeofence(geofenceEvent);
print('- [Headless] GeofenceEvent: $geofenceEvent');
break;
case bg.Event.ACTIVITYCHANGE:
bg.ActivityChangeEvent event = headlessEvent.event;
onHeadlessActivityChange(event);
print('- [Headless] ActivityChangeEvent: $event');
break;
}
}
onHeadlessGeofence is almost identical to the callback _onGeofence, besides the setState().
The full code can be found here

Navigator.push() loop forever

I'm making a LoginScreen, in LoginScreen i check data in database for know user logged or not for each times open app.
If user logged, the app will switched to HomeScreen.
I have a problem, i had logged in LoginScreen and then the app switched to HomeScreen. But my app's not standing in HomeScreen, it's continuing push new HomeScreen and looping this push action.
My code:
goToHomeIfAvailable() async {
// Go to HomeScreen if available
if (await this._databaseProvider.tokenTableIsEmpty() == false) {
print('Logged');
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HomeScreen()),
);
}
}
#override
Widget build(BuildContext context) {
// In first times user open app=> create DB and go to HomeScreen if available
_databaseProvider.openOrCreate().then((_) async {
await goToHomeIfAvailable();
});
/* Return a widget bellow */
}
DatabaseProvider.dart:
class DatabaseProvider {
String _path = 'O2_DB.db';
Database _database;
Map _tableName = {'token': 'token_tbl'};
Future openOrCreate() async {
this._database = await openDatabase(this._path, version: 1,
onCreate: (Database db, version) async {
await db.execute('CREATE TABLE IF NOT EXISTS ' +
this._tableName['token'] +
' (token_id integer primary key autoincrement, token text)');
});
}
}
Build is called many times during the app life cycle - & its better to always put our logic outside build method. Its normal Behavior.
In your case - As build was called each time the method - goToHomeIfAvailable() was called hence multiple push.
Moving goToHomeIfAvailable() out of build to initState() will solve the issue.

How to properly change video player source at runtime?

I'm building an app that basically is a YouTube clone. I use the official video_player plugin for playback and chewie for controls. I'd like to implement a quality switcher, so the user can decide what quality they want the video to be streamed at
I've built a bottom sheet with switches and I run changeQuality() when the user selects the desired quality. What it should do is simply giving a new source file to the old player and keep playing from where the video left.
This is the video player and chewie player that run on initState():
videoPlayer = VideoPlayerController.network(data == null
? dataAll[indexNo]["video"]["480"]
: data[indexNo]["video"]["480"]);
chewieController = ChewieController(
videoPlayerController: videoPlayer,
aspectRatio: 16 / 9,
autoPlay: true,
allowedScreenSleep: false,
placeholder: data == null
? Image(
image: NetworkImage(dataAll[indexNo]["thumbnail"]),
)
: Image(
image: NetworkImage(data[indexNo]["thumbnail"]),
)
);
And the changeQuality() function:
changeQuality(String newQuality) {
setState(() {
position = videoPlayer.value.position;
chewieController.pause();
videoPlayer = new VideoPlayerController.network(data == null
? dataAll[indexNo]["video"]["$newQuality"]
: data[indexNo]["video"]["$newQuality"]);
chewieController = ChewieController(
videoPlayerController: videoPlayer,
aspectRatio: 16 / 9,
autoPlay: true,
allowedScreenSleep: false,
startAt: position,
);
});
Navigator.of(context).pop();
}
I've also tried disposing the old video player and then setting the new value, but I get an error that variables cannot be used after being disposed.
The switcher works a bit, because it changes the quality around 4 to 5 times and then it runs into an error and won't play anything.
I expand upon this solution for video_player and extend it to also cover chewie.
Key parts of this solution
You need two widgets. MyVideoPlayer that encapsulates video_player and chewie and an outer widget where you react to user input or state changes and swap out MyVideoPlayer with a new one.
This solution roundabouts the whole question in one way. I doesn't solve how to change video of video_player or chewie. Instead it follows the documented principal on how to use chewie for the whole life cycle of a host widget (MyVideoPlayer) and swap that one out to change video url.
You can stuff in more things in the outer widget as you see fit if you don't want to dedicate it just to containing MyVideoPlayer. Ie. if you want a description text adjacent to it based on app state.
Outer Widget
I write with this. but it can be omitted in Dart code.
class QuizVideoPlayer extends StatefulWidget {
#override
_QuizVideoPlayerState createState() => _QuizVideoPlayerState();
}
class _QuizVideoPlayerState extends State<QuizVideoPlayer> {
Word _url;
UniqueKey _urlKey;
// Call this method from button or in reaction to model change etc.
// I call it from Provider.of in didChangeDependencies, but I don't think it is
// a necessary detail of the answer as it depends on how you do state management.
// The key in this solution is that state management occur in the outer widget and
// due to some trigger call _changeUrl() which changes _url and _urlKey which then
// swaps out MyVideoPlayer.
#override
void _changeUrl(String newUrl) async {
this.setState(() {
// Rebuild MyVideoPlayer with a new instance => eventually dispose old controllers
this._url = newUrl;
this._urlKey = UniqueKey();
});
}
#override
Widget build(BuildContext context) {
return
/* ... */
this._url != null
? MyVideoPlayer(
this._url,
this._urlKey,
)
: AspectRatio(
aspectRatio: 3 / 2,
child: Container(color: Colors.black),
)
/* ... */
);
}
}
MyVideoPlayer
I write with this. but it can be omitted in Dart code.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
class MyVideoPlayer extends StatefulWidget {
final String videoUrl;
final UniqueKey newKey;
MyVideoPlayer(this.videoUrl, this.newKey): super(key: newKey); // passing Unique key to dispose old class instance and create new with new data
#override
_MyVideoPlayerState createState() => _MyVideoPlayerState();
}
class _MyVideoPlayerState extends State<MyVideoPlayer> {
VideoPlayerController _controller;
ChewieController _chewie;
#override
void initState() {
this._initControllers(this.widget.videoUrl);
super.initState();
}
void _initControllers(String url) {
this._controller = VideoPlayerController.network(url);
this._chewie = ChewieController(
videoPlayerController: this._controller,
autoPlay: true,
);
}
#override
void dispose() {
this._controller?.dispose();
this._chewie?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Chewie(controller: this._chewie);
}
}

Categories

Resources