I want to create a multiscreen / multipart form wizard, with each screen containing:
a question
several different controls (text, radio-buttons etc.) which allow him to answer the question
I want to save the state of the answers to a remote server.
For the radio buttons that's straight-forward (whenever the user changes it's value).
But what about text-fields?
I don't want to update the server with every character change, but on the other hand I don'w want to lose data if the user exit the screen or the app closes / goes to background...
What would be the best approach to save data from text-fields?
Can Flutter's Form help me with that?
As the comment suggests you can use a debouncer for that. Creating a debouncer is pretty easy:
import 'package:flutter/foundation.dart';
import 'dart:async';
class Debouncer {
final int delay;
late VoidCallback action;
Timer? _timer;
Debouncer({required this.delay});
run(VoidCallback action) {
if (_timer != null) {
_timer?.cancel();
}
_timer = Timer(Duration(milliseconds: delay), action);
}
cancel() {
_timer?.cancel();
}
}
And to use it define it somewhere and then do:
yourDebouncer.run(() async {sendData(...)})
I suggest you send your data in the onchange method of your TextField. This way your user data is saved and your server will certainly not be overwhelmed. Don't forget to cancel the Debouncer in your dispose method.
#override
void dispose() {
_d.cancel();
yourDebouncer.dispose();
}
You might want to have a look at this if you want more details on how to implement a Debouncer.
Related
folks.....how could I use Speech To Text Provider so I can use it multiple times in multiple widgets? I read speech to text could only be started once per app session....
I know I need to use it as a provider class but I don´t know how..
I tried speech to text but It just doesn´t work for me since I need to use it in multiple widgets and I want to be able to use the callbacks "onstatus" and "onerror"
I don´t know what else to say since it is asking me to type a certain number of letters
initialize speech to text in the root/first class.
final SpeechToText speech = SpeechToText();
late SpeechToTextProvider speechProvider;
in the initState, initialize the speechProvider and pass the speech instance to it.
speechProvider = SpeechToTextProvider(speech);
await speechProvider.initialize(); // do it in any other method and call it from initState.
in the root widget, add the ChangeNotifierProvider and pass the SpeechToTextProvider class.
ChangeNotifierProvider<SpeechToTextProvider>.value(
value: speechProvider,
child: ///
now you can use speechToTextProvider anyWhere in your project.
now in order to use it anywhere in your project you have to initilaize the speechToTextProvider in the class where you want to use it.
late SpeechToTextProvider speechToTextProvider;
speechToTextProvider = Provider.of<SpeechToTextProvider>(context); // do it in a didChangeDependency or any other method and call it from initState.
now in order to start listening
speechToTextProvider.listen(
pauseFor: Duration(seconds: 5),
localeId: 'en-us',
listenFor: Duration(seconds: 5));
StreamSubscription<SpeechRecognitionEvent> subscription =
speechToTextProvider.stream.listen(
(event) {
// on every change it update
if (event.eventType ==
SpeechRecognitionEventType.partialRecognitionEvent) {
if (mounted) {
_setState!(
() {
toDisplayText =
speechToTextProvider.lastResult!.recognizedWords; // the textField or Text Widget Will be updated
},
);
}
}
//on error
else if (event.eventType == SpeechRecognitionEventType.errorEvent) {
///on error if some error occurs then close the dilog box here . or stop the listner
}
}
//on done
else if (event.eventType ==
SpeechRecognitionEventType.finalRecognitionEvent) {
//when the user stop speaking.
}
},
);
}
and in that way you can use it in any class or widget. You have to repeat the process of start listening in other class.
For a simple Email login with OTP code I have a structure as follows.
View
await _signUpCntrl.signUp(email, password);
Controller
_showOtpDialog(email);
_showOtpDialog func
return Get.dialog(
AlertDialog(
So the thing is _showOtpDialog function is inside a controller file. ie. /Controllers/controller_file.dart
I want do something like a blocListener, call the _showOtpDialog from a screen(view) file on signup success. (also relocate the _showOtpDialog to a view file)
Using GetX I have to use one of the builders either obs or getbuilder. Which is I think not a good approach to show a dialog box.
On internet it says Workers are the alternative to BlocListener. However Workers function resides on Controller file and with that the dialog is still being called on the controller file.
As OTP dialog will have its own state and a controller I wanted to put it inside a /view/viewfile.dart
How do I obtain this?
I tried using StateMixin but when I call Get.dialog() it throw an error.
visitChildElements() called during build
Unlike BLoC there's no BlocListener or BlocConsumer in GetX.
Instead GetX has RxWorkers. You can store your response object in a Rx variable:
class SomeController extends GetxController{
final response= Rxn<SomeResponse>();
Future<void> someMethod()async{
response.value = await someApiCall();
}
}
And then right before the return of your widget's build method:
class SomeWidget extends StatelessWidget{
final controller = Get.put(SomeController());
#override
Widget build(BuildContext context){
ever(controller.response, (SomeResponse res){
if(res.success){
return Get.dialog(SuccessDialog()); //Or snackbar, or navigate to another page
}
....
});
return UI();
}
First thing, you will need to enhance the quality of your question by making things more clearly. Add the code block and the number list, highlight those and making emphasize texts are bold. Use the code block instead of quote.
Seconds things, Depends on the state management you are using, we will have different approaches:
Bloc (As you already added to the question tag). By using this state management, you controller ( business logic handler) will act like the view model in the MVVM architecture. In terms of that, You will need to emit a state (e.g: Sent success event). Afterward, the UI will listen to the changes and update it value according to the event you have emitted. See this Bloc example
GetX (As your code and question pointed out): GetX will acts a little bit different. you have multiple ways to implement this:
Using callbacks (passed at the start when calling the send otp function)
Declare a general dialog for your application ( this is the most used when it comes to realization) and calling show Dialog from Bloc
Using Rx. You will define a Reactive Variable for e.g final success = RxBool(true). Then the view will listen and update whenever the success changes.
controller.dart
class MyController extends GetxController {
final success = RxBool(false);
void sendOtp() async {
final result = await repository.sendOTP();
success.update((val) => {true});
}
}
view.dart
class MyUI extends GetView<MyController> {
#override
Widget build(BuildContext context) {
ever(controller.success, (bool success) {
// This will update things whenever success is updated
if (success) {
Get.dialog(AlertDialog());
}
});
return Container();
}
}
I have this ViewModel and a Riverpod provider for it:
final signInViewModelProvider = Provider.autoDispose<SignInViewModel>((ref) {
final vm = SignInViewModel();
ref.onDispose(() {
vm.cleanUp();
});
return vm;
});
class SignInViewModel extends VpViewModelNew {
FormGroup get form => _form;
String get emailKey => _emailKey;
String get passwordKey => _passwordKey;
final String _emailKey = UserSignInFieldKeys.email;
final String _passwordKey = UserSignInFieldKeys.password;
final FormGroup _form = FormGroup({
UserSignInFieldKeys.email:
FormControl<String>(validators: [Validators.required]),
UserSignInFieldKeys.password:
FormControl<String>(validators: [Validators.required])
});
void cleanUp() {
print('cleaning up');
}
void onSubmitPressed(BuildContext context) {
// _saveRegistrationLocallyUseCase.invoke(
// form.control(_self.emailKey).value as String ?? '',
// form.control(_self.passwordKey).value as String ?? '');
}
}
abstract class VpViewModelNew {
VpViewModelNew() {
if (onCreate != null) {
onCreate();
print('creating');
}
}
void onCreate() {}
}
When I navigate to the page that has the signInViewModelProvider, it prints to the console:
flutter: signInPage building
flutter: creating
flutter: cleaning up
Then popping the page from the stack with Navigator.pop() prints nothing.
Then navigating to the page again prints the same 3 lines in the same order.
I expected onDispose to be called after Navigator.pop(), and not when navigating to the page that reads the provider. Why is onDispose being called directly after creation, and not when using Navigator.pop() (when I expected the provider to be disposed of since no other views reference it)?
Edit: I access the provider with final viewModel = context.read<SignInViewModel>(signInViewModelProvider);
I don't need to listen since I don't need to rebuild the page on
change. Is consumer less performant for this?
No, the performance is meaningless, even if it's listening it's not really affecting the performance because as a Provider there is no way to notify (which is not the case with a state notifier or change notifier)
Also if you don't care to listen after the value has been read The auto dispose understand no one is watching it and it disposes, it's better to use context.read when using tap or gestures that modify something
(I realize this is late to the party but maybe it'll help somebody)
The Riverpod docs come out pretty strongly against using read for the reason you said, i.e. performance/rebuilding concerns.
Basically you should always use watch except:
If you want your custom callback function called when it updates (use listen)
If the actual reading is happening asynchronously or in response to user action (like in an onPressed): this is the only time to use read.
If you're having issues with your widgets rebuilding too often, Riverpod has some ways to deal with that that don't involve using read.
My scenario is like this:
a BottomNavigationBar where one page contains a list of items ("listPage") and another page is a single item ("itemPage").
both pages can navigate to "itemPage" related to a different items.
the "itemPage" show details of the product and have a "Favourite" toggle button.
in the "listPage" every item shown via ListView.builder, show and can handle the "Favourite" toggle button.
I can't understand how reflect the "Favourite" change in an "itemPage" to others "itemPage" (if opened multiple time for the same item, yes, it's possible) and also to the same item in the "pageList".
I created a NotifierBloc where a BehaviorSubject > Sink is called every time a "Favourite" toggle button change its state (putting the itemId and the boolean value of the Favourite). After a database update an output PublishSubject > Stream is filled with the additional information of the item.
In this way every time a "Favourite" is toggled, all the subscribers receive the info about the flag.
BehaviorSubject<Item> inController = new BehaviorSubject<Item>();
Sink<Item> get putUpdate => inController.sink;
final PublishSubject<Item> outController = PublishSubject<Item>();
Stream<Item> get getUpdates => outController.stream;
NotifierBloc() {
inController.listen(_handleToggle);
}
_handleToggle(Item item) {
...
outController.sink.add(newItemAfterDatabaseCall);
}
In ListPage and ItemPage (both StatefulWidget) I created a StreamSubscription in DidChangeDependencies method which listen to the NotifierBloc Stream.
The StreamSubscription will be cancelled in the Dispose method.
StreamSubscription _subscription;
void didChangeDependencies() {
super.didChangeDependencies();
_subscription = _notifierBloc.getUpdates.listen((item) => {
// Do things with the Item like setState or call bloc methods
...
});
}
void dispose() {
_subscription?.cancel();
super.dispose();
}
Problems are when a new itemPage is opened: the dispose method cancel the subscription in the previous shown page, so new events will not be get listened.
In addition when a page is shown due to a previous page close, the StreamSubscription is renewed and I get updates about the last one Favourite change, but I need a list of Favourite changes, because maybe the user opened several "itemPage"s.
How can I solve?
Maybe the Stream must be passed to the Page(not the PageState)? But how to handle onData function?
You need some sort of state management system. Look into Provider. Here is a simple example of how you could use it. https://github.com/m-Skolnick/provider_example_flutter
The PreferenceActivity seems like a really easy helper class to manage the preferences of an app.
I'd like to also use that same class from the widget.
On the first instance of a widget I want the PreferenceActivity to come up. This was very easy to do from the configuration activity but here is the glitch...
I can't tell when the preference edit is completed!
All examples of widget preferences I see on the net use a manually built preference screen rather than the PreferenceActivity helper class and then listen for the 'save' button being clicked.
How can I do that with the PreferenceActivity since that activity doesn't have a save button. You just use the 'back' button to commit your preferences changes.
Thanks!
I have been trying to do the same thing and I think I've cracked it. I handle the onBackPressed() event in the PreferenceActivity and perform a widget update from there using sendBroadcast().
In PreferenceActivity:
#Override
public void onBackPressed() {
Intent intent=getIntent();
Bundle extras=intent.getExtras();
int widgetId=extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
// this is the intent broadcast/returned to the widget
Intent updateIntent = new Intent(this, WidgetProvider.class);
updateIntent.setAction("PreferencesUpdated");
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
sendBroadcast(updateIntent);
}
In your WidgetProvider:
#Override
public void onReceive(Context context, Intent intent) {
if ("PreferencesUpdated".equals(action)) {
// update your widget here
// my widget supports multiple instances so I needed to uniquely identify them like this
RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.widget);
int appWidgetId = intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
updateWidgetView(context, remoteView, appWidgetId);
}
}
NOTE: In 1.5 onBackPressed isn't support so comment out the #Override for onBackPressed and add this code
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
Log.d(LOG_TAG, "onKeyDown() "+ keyCode);
if (keyCode==KeyEvent.KEYCODE_BACK && Integer.parseInt(Build.VERSION.SDK)<5) {
onBackPressed();
}
}
I should add that I'm new to Android development so I may be doing this entirely wrong. All I can say it is works for me :)
Let me know how you get on.
You should be able to tell when the Preference edits are complete by implementing a SharedPreferences.OnShredPreferenceChangeListener. Using this, you could update only when certain keys are changed, or unconditionally when any are changed. Remember to register it in your PreferenceActivity onCreate and unregister it in the onDestroy.
In this case, you can do something similar to Android: How do I force the update of all widgets of a particular kind within the listener to cause all of your widgets to update based on a SharedPreference change.
It's an old question, so this may not be relevant anymore.