flutter_slidable has no isOpen field? - flutter

I have a requirement that when my flutter_slidable is open, tapping on it closes it, rather than executing my onTap callback.
I'm not seeing an isOpen field to check whether the slidable is open. I have a workaround where I check to see how long it took to close after an onTap. If it closes fast, I assume it was already closed.
Is there a "real" way? Here's my workaround
const int closingTimeMS = 500;
final stopwatch = Stopwatch();
stopwatch.start();
await slidable.close(duration: const Duration(milliseconds: closingTimeMS));
if (stopwatch.elapsedMilliseconds < closingTimeMS / 2) {
myOnTap();
}

Calling this in an onTap/onPressed solved it for me
final isClosed = Slidable.of(context)!.actionPaneType.value == ActionPaneType.none;
if (isClosed) {
Slidable.of(context)!.openStartActionPane();
} else {
Slidable.of(context)!.close();
}

Related

How to `setState()` in a global function?

Here's a simple code block for detecting transactions from SMS :
FlutterLocalNotificationsPlugin notificationsPlugin =
FlutterLocalNotificationsPlugin();
onBackgroundMessage(SmsMessage message) async {
log("Message recieved in background");
_TransactionState().onMessage(message);
}
class Transaction extends StatefulWidget {
const Transaction({Key? key}) : super(key: key);
#override
State<Transaction> createState() => _TransactionState();
}
class _TransactionState extends State<Transaction> {
String _message = "";
var debit = 0.0;
var credit = 0.0;
var last = 0.0;
var transact = 0.0;
final Telephony telephony = Telephony.instance;
#override
void initState() {
super.initState();
initPlatformState();
}
onMessage(SmsMessage message) async {
setState(() {
bool mesFlag = false;
bool debFlag = false;
if (message.body!.contains('debit')) {
debFlag = true;
} //check whether debit or credit
for (var bank in banks.entries) {
if (message.address!.contains(bank.key)) {
_message = message.body?.split(
' ')[(debFlag ? bank.value.item2 : bank.value.item1)] ??
"Error reading transaction!";
_message = _message.replaceAll(',', ''); //remove comma
if (_message.contains("Rs.")) {
_message = _message.substring(3);
} else if (_message.contains("Rs")) {
_message = _message.substring(2);
} // remove Rs. and Rs
showNotification("Last transaction amount: $_message");
transact = double.parse(_message);
mesFlag = true;
if (debFlag) {
debit += transact;
last = -transact;
} else {
credit += transact;
last = transact;
}
limit += last;
if (limit < 0) {
limit = 0;
showNotification("You are over-spending!");
}
transList.add(TransactionTile(trans: last));
break;
}
}
if (!mesFlag) _message = ''; //if not a transaction
});
}
Here's the exception it throws when I receive a SMS in the background :
setState() called in constructor: _TransactionState#f0921(lifecycle state: created, no widget, not mounted)
This happens when you call setState() on a State object for a widget that hasn't been inserted into the widget tree yet. It is not necessary to call setState() in the constructor, since the state is already assumed to be dirty when it is initially created.
The telephony package requires onBackgroundMessage to be a global function. Is there any work around by means of which I can call the onMessage in the global function or setState() cause I need them to be updated even when the app isn't open & it should function exactly like the onMessage fucntion.
Any help appreciated.
From the Firebase Cloud Messaging Flutter documentation on background messages:
Since the handler runs in its own isolate outside your applications context, it is not possible to update application state or execute any UI impacting logic. You can, however, perform logic such as HTTP requests, perform IO operations (e.g. updating local storage), communicate with other plugins etc.
You can execute a logic that affects the UI only when the user decides to click on a notification that contains a message, and the app comes from background to foreground.
If you want't to silently receive some data and update the UI when the app is activated again, you can for example:
save the data in shared preferences which you can access from onBackgroundMessage,
implement a didChangeAppLifecycleState override that checks the data in shared preferences when the app is activated and updated the UI accordingly.

Automatically set State of Button WITHOUT pressing it

I have got a State Management Problem I couldn't get rid of and I want to reach out to you.
Basically, I activate with the Buttons a game and I am sending a String to the uC. The uC does its stuff and sends a response to Flutter including gameFinished=true (that works).
Now I want to reset the State of the Button to the init state WITHOUT pressing the Button. Following are some things I tried that didn't work.
#override
void initState() {
super.initState();
setState(() {
gameAktivated = false;
gameStarted = false;
});
}
void asyncSetState() async {
setState(() async {
gameAktivated = false;
gameStarted = false;
});
}
I am changing the style from "Start" to "Stop" when the Button is pressed and I send Data to the uC. (Works)
Edit: Ofc I have a second button that triggers gameAktivated=true :)
ElevatedButton(
onPressed: () {
if (gameAktivated) {
setState(() {
gameStarted = !gameStarted;
});
if (gameStarted) {
//Send Data to uC
} else if (!gameStarted) {
//Send Data to uC
}
}
},
child:
!gameStarted ? const Text('Start') : const Text('Stop'),
),
Button Displays Stop now.
Following I am receiving a String from the uC that I jsonEncode and I receive gameFinished=true. (Works)
Container(
child: streamInit
? StreamBuilder<List<int>>(
stream: stream,
builder: (BuildContext context,
AsyncSnapshot<List<int>> snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.connectionState ==ConnectionState.active) {
// getting data from Bluetooth
var currentValue =const BluetoothConnection().dataParser(snapshot.data);
config.jsonDeserializeGameFinished(currentValue);
if(config.gameFinished){
setState(() {
gameAktivated = false;
gameStarted = false;
});
asyncSetState();//Tested both methods seperate!
}
return Column(
children: [
Text(config.time.toString()),
],
);
} else {
return const Text(
'Check the stream',
textAlign: TextAlign.center,
);
}
},
): const Text("NaN",textAlign: TextAlign.center,),
),
When I try to reset the state like in the code above this error occures:
Calling setState Async didnt work for me either.
Where and how can I set the state based on the response from the uC?
Is it possible without using Provider Lib?
Thanks in advance Manuel.
Actually this error is not about the changing the state of button. Its a common mistake to update the widget state when its still building the widget tree.
Inside your StreamBuilder, you are trying to update the state before creating the UI which is raising this issue.
if(config.gameFinished){
setState(() {
gameAktivated = false;
gameStarted = false;
});
This will interrupt the build process of StreamBuilder as it will start updating the whole page. You need to move it out of the StreamBuilder's builder method.
To do that simply convert your stream to a broadcast, which will allow you to listen your stream multiple time.
var controller = StreamController<String>.broadcast();
Then inside the initState of the page you can setup a listener method to listen the changes like this
stream.listen((val) => setState((){
number = val;
}));
Here you can change the state values because from here it will not interrupt the widget tree building cycle.
For more details see this example I created
https://dartpad.dev/?id=a7986c44180ef0cb6555405ec25b482d
If you want to call setState() immediately after the build method was called you should use:
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// this method gets called once directly after the previous setState() finishes.
});
Answer to my own Question:
In initState()
added this:
stream.listen((event) {
String valueJSON = const BluetoothConnection().dataParser(event);
config.jsonDeserializeGameFinished(valueJSON);
if (config.gameFinished) {
setState(() {
gameAktivated = false;
gameStarted = false;
});
}
});
The Code above listens to the stream, UTF-8 Decodes and JSON-Decodes the data. After this you can access the variable to set a state.

Flutter TextFormField call value change method after finish text input

In my screen there are search features as per logic that's working fine but issue is I want to run my search function after stope typing?? So in the TextFormField there are onChanged method, how can we achieve it? I tried lots of ways like comparing DateTime but not able to achieved it.
~ PS : onEditingComplete method works perfect in my case but issues is, To call this method I have to click on return button, without click on return button is it possible?
I would suggest to create a debouncer, something like this.
class Debouncer {
final int milliseconds;
VoidCallback action;
Timer _timer;
Debouncer({this.milliseconds});
run(VoidCallback action) {
if (null != _timer) {
_timer.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
}
Then initiate the object with your desired time of "after stop typing".
final _debouncer = Debouncer(milliseconds: 1000);
And use it in your onChanged method.
onChanged: (string) {
_debouncer.run(() {
//Perform your search
}
);

How to make a stopwatch that takes a widget off the air?

I need to know how to make this stopwatch, which after 48 hours disables a widget, which in my case is a button. Can someone explain to me how to do it? What classes to use?
I tried to use this, but don't works:
var timer = Timer(Duration(seconds: 1), () => print('done'));
it seems to me that you want this button to be disabled after 2 days of the app that was installed, you need persist the date on the device so that after app-restarts the date will be itself, you need to use a package the persists the data on the device. i recommend shared_preference which is easy to use.
for your case, in the screen where you use the button you need to do this
import 'package:shared_preferences/shared_preferences.dart';
class MyFirstStatefullScreen extends StatefullWidget {
MyFirstStatefullScreenState createState() => MyFirstStatefullScreenState();
}
class MyFirstStatefullScreenState extends State<MyFirstStatefullScreen>{
// some other code that u wrote
bool shouldButtonBeActive = true;
#override
void initState() {
Future.delayed(Duration(0))
.then((_) {
SharedPreferences sharedPrefs= await SharedPreferences.getInstance();
final deadLine = sharedPrefs.getString('deadLine');
if(deadLine == null) {
// this is the first time the app has been installed on the device
// so we need to set the deadLine to N number of days after the installation
final deadLineDate = DateTime.now().add(Duration(days: 2)); // 2 days from the date that the app was installed;
sharedPrefs.setString(deadLineDate.toIso8601String()); // set it so we can check on the successfull state
return;
}
final deadLineDate = DateTime.parse(deadLine); // since we stored it as a string;
/// the deadline is set and is not null
if(DateTime.now().compareTo(deadLineDate) == 0) {
we are successfull, N hours have passed since the intial instalation, now we can disable the button
shouldButtonBeActive = false; // the button should be disabled
}
})
}
Widget build(context) {
// in your UI where you use the button
MaterialButton(
child: Text("Button"),
onPressed: shouldButtonBeActive ? func() : null
)
}
}
PS: we are using the Future inside the initState, because initState dose not allow async ( api calls, storage access) in it.

Flutter optimize API number of calls in TextField autocomplete implementation

I am trying to implement autocomplete for selecting places in my app. For that I use Here Maps API. At the moment I have this for a TextField:
onChanged: (query){
print("Current value is: ${query}");
if(query) { getPlacesFromHereMaps(query); }
},
Here, each time user enters some letter Here autocomplete API is being called.
So, if user types "New York" that means app will call API for about 8 times which I find too much. Is there a way to optimize this?
You can call the API whenever the user finshes typing a word or after every 3 (or 2) characters.
But don't forget to call the API when the user submits the query(using onSubmitted).
Solution Code:
onChanged: (query){
print("Current value is: ${query}");
if((query.length%3==0)||(query[query.length-1]==' ')) { getPlacesFromHereMaps(query); }
onSubmitted: (query){
getPlacesFromHereMaps(query);
}
},
=========
Alternate Solution:
As per #Karim Elghamry 's advice and #CopsOnRoad 's concern you can even use debounce to improve your UX.
In your widget state declare a controller and timer:
final _searchQuery = new TextEditingController();
Timer _debounce;
Add a listener method:
_onSearchChanged() {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
getPlacesFromHereMaps(query);
});
}
Hook and un-hook the method to the controller:
#override
void initState() {
super.initState();
_searchQuery.addListener(_onSearchChanged);
}
#override
void dispose() {
_searchQuery.removeListener(_onSearchChanged);
_searchQuery.dispose();
super.dispose();
}
In your build tree bind the controller to the TextField:
child: TextField(
controller: _searchQuery,
[...]
)
Source: How to debounce Textfield onChange in Dart?
onChanged is doing its job. According to docs:
The text field calls the onChanged callback whenever the user changes
the text in the field. If the user indicates that they are done typing
in the field (e.g., by pressing a button on the soft keyboard), the
text field calls the onSubmitted callback.
If you want to optimize, you can do something like:
onChanged(query) {
if (query.length < 2) return;
// if the length of the word is less than 2, stop executing your API call.
// rest of your code
getPlacesFromHereMaps(query);
}
Considering the popular node package 'debounce', below is the simple implementation
/// Implementation of well known 'Debounce' node package in Dart
class Debounce {
final Function _function;
final Duration _duration;
Timer _timer;
int _lastCompletionTime;
Debounce(this._duration, this._function)
: assert(_duration != null, "Duration can not be null"),
assert(function != null, "Function can not be null");
void schedule() {
var now = DateTime.now().millisecondsSinceEpoch;
if (_timer == null || (_timer != null && !_timer.isActive)) {
_lastCompletionTime = now + _duration.inMilliseconds;
_timer = Timer(_duration, _function);
} else {
_timer?.cancel(); // doesn't throw exception if _timer is not active
int wait = _lastCompletionTime - now; // this uses last wait time, so we need to wait only for calculated wait time
_lastCompletionTime = now + wait;
_timer = Timer(Duration(milliseconds: wait), _function);
}
}
}
Usage:
1. Define
var debounce = Debounce(Duration(seconds: 1), () {
getPlacesFromHereMaps(query);
});
2. Call every time when value changes
onChanged: (query){
print("Current value is: ${query}");
if(query.trim().length > 3) { d.schedule(); }
}