dart - How to listen for Text-Change in Quill text Editor (Flutter) - flutter

I am only familiar with HTML/CSS/JS, and basics of dart/flutter
Developer Level: Beginner
Project type & language: I am developing a notes app for myself, using flutter.
My aim is to save my note, as soon as I update the text... for which I need to use a dart Function to run on every 'text-change' event..
How do I use the Text-Changes event of Quill Editor to detect changes in the Content
THE EQUIVALENT OF THIS IN JAVASCRIPT IS GIVEN BELOW , BUT I DON'T KNOW HOW TO DO IT DART & FLUTTER.
quill.on('text-change', function(delta, oldDelta, source) {
if (source == 'api') {
console.log("An API call triggered this change.");
} else if (source == 'user') {
console.log("A user action triggered this change.");
}
});

You can listen to quill document changes stream and handle it accordingly.
_controller.document.changes.listen((event) {
print(event.item1); //Delta
print(event.item2); //Delta
print(event.item3); //ChangeSource
});

I am also run into this issue. After some hours of research, I found a solution. You habe to add a listener to your QuillController, which will be called on each editor event, like pressed keys or toolbar actions.
Use the initState() method in your State class for adding a listener.
class _TextEditorState extends State<TextEditor> {
final QuillController _controller = QuillController.basic();
#override
void initState() {
_controller.addListener(() {
print('Here I am, rock me like a hurricane!!!');
});
super.initState();
}
#override
Widget build(BuildContext context) {
// Build your Widget with QuillToolbar and QuillEditor here...
}
});

Here's how it's done:
await for (final change in _quillEditorController.changes) {
final oldDelta = change.toList()[0];
final changeDelta = change.toList()[1];
final changeSource = change.toList()[2];
if (changeSource == ChangeSource.REMOTE) {
console.log("An API call triggered this change.");
} else if (changeSource == ChangeSource.LOCAL) {
console.log("A user action triggered this change.");
}
}
Note that flutter-quill's api doesn't exactly match quill.js. And since the documentation is lacking, your best hope in understanding what's available is by digging into the code-base using your editor (eg. using "go to definition").

Related

GetX Unbind Stream

I am using the bindStream() function with the GetX package inside a controller.
class FrediUserController extends GetxController {
#override
void onReady() {
super.onReady();
final userController = Get.find<FrediUserController>();
var groupIds = userController.user.groups;
groupList.bindStream(DatabaseManager().groupsStream(groupIds));
ever(groupList, everCallback);
}
}
But, when the groupIds update in the FrediUserController (with an ever function that gets triggered, I want to RE-bind the streams. Meaning, delete the existing ones and bind again with new ids, or replace the ones that have changed.
Temporary Solution: Inside ever() function
Get.delete<FrediGroupController>();
Get.put(FrediGroupController());
This code gets run everytime my groupIds change from the database. But I do not want to initiate my controllers every time a small thing changes, it is bad UX.
This seems difficult, could someone guide me to the right direction? Maybe there is a completely different approach to connecting two GetX controllers?
Note: the first one include editing the source code of the Getx package.
first:
looking in the source code of the package :
void bindStream(Stream<T> stream) {
final listSubscriptions =
_subscriptions[subject] ??= <StreamSubscription>[];
listSubscriptions.add(stream.listen((va) => value = va));
}
here is what the bind stream actually do, so if we want to access the listSubscriptions list, I would do:
final listSubscriptions;
void bindStream(Stream<T> stream) {
listSubscriptions =
_subscriptions[subject] ??= <StreamSubscription>[];
listSubscriptions.add(stream.listen((va) => value = va));
}
now from your controller you will be able to cancel the streamSubscription stored in that list with the cancel method like this :
listSubscriptions[hereIndexOfThatSubscription].cancel();
then you can re-register it again with another bindStream call
second :
I believe also I've seen a method called close() for the Rx<T> that close the subscriptions put on it, but I don't know if it will help or not
Rx<String> text = ''.obs;
text.close();
I've also run into this issue, and there appears to be no exposed close function. There is a different way to do it though, using rxdart:
import 'package:get/get.dart';
import 'package:rxdart/rxdart.dart' hide Rx;
class YourController extends GetxController {
final value = 0.obs;
final _closed = false.obs;
void bindValue(Stream<int> valueStream) {
_closed
..value = true
..value = false;
final hasClosed = _closed.stream.where((c) => c).take(1);
value.bindStream(
valueStream.takeUntil(hasClosed)
);
}
}
Whenever you want to unbind, just set _closed.value = true.

Future Builder doesn't update view always

I have a future builder, and Im using two future variables:
Future<List<Notifications>>? notificationsOfTheDay; //get saved notifications from db
Future<List<NotificationCategory>>? notificationsByCat; // sort/modify "notificationsOfTheDay"
I'm sending notificationsByCat to Future Builder.
The issues:
While the app starts, the future builder is able to receive all the notifications and manipulate the data with some asynchronous operations.
But sometimes the Future Builder displays blank.
I'm also appending received notifications to the existing Future Variable notificationsOfTheDay, when sometimes the view does not update.
Code snippets are listed below:
Here is my initState
void initState() {
super.initState();
initPlatformState(); // Notification Listener Service
notificationsOfTheDay = initializeData(isToday);
}
initilizeData Method
Future<List<Notifications>> initializeData(bool istoday) async {
notificationsOfTheDay = initializeNotifications(istoday);
if (notifications!.length > 0) {
notificationsByCat = notificationsByCategory(notificationsOfTheDay); //sorting/manipulation of existing future
}
return notifications!;
}
notificationsByCategory
Future<List<NotificationCategory>> notificationsByCategory(
List<Notifications> notificationsFuture) async {
return await NotificationsHelper.getCategoryListFuture(
isToday ? 0 : 1, notificationsFuture);
}
When any new notifications are received, it is inserted into the db and the exising future is appended with the new notification;
setState(() {
notificationsOfTheDay =
appendElements(notificationsOfTheDay!, _currentNotification!);
notificationsByCat = notificationsByCategory(notifications!);
});
Future<List<Notifications>> appendElements(
Future<List<Notifications>> listFuture,
Notifications elementToAdd) async {
final list = await listFuture;
list.add(elementToAdd);
return list;
}
Can anyone please guide me to a solution? Tried many combinations. If I'm directly showing the data without modifying it according to category, it works fine.
Where am I going wrong?

Flutter Signalr Listener is not connected in 2nd screen after migrated to null safety stable version

Chatting was working perfectly before migrating to null safety using signalr. But after migrating It is not working in chatting part.
Scenario is like there are 2 screens where I am using signalr.
1)Chatlist.
2)Chatting with person.
listener in Chatlist is perfect but in 2nd screen it is not working(Just worked when I installed and run for the 1st time). Weird issue.
All was working in old. I am using bloc for statemanagement and also migrated to yield to emit.
Piece of code is like:
void listenOnMessageReceived(
HubConnection hubConnection,
Function(Message? chatMessageReceive) onMessageReceived,
) {
final SocketResponseCallBack chatMessageReceived =
(response) => onMessageReceived(Message.fromJson(response));
final hubMethod = HubMethod(
CHAT_RECEIVED_MESSAGE_METHOD_NAME,
SignalRHelper.toSocketFunction(
CHAT_RECEIVED_MESSAGE_METHOD_NAME, chatMessageReceived));
bool exists = listenOnHubMethod.any((method) => method.methodName == CHAT_RECEIVED_MESSAGE_METHOD_NAME);
if(exists) {
listenOnHubMethod.removeWhere((element) =>
element.methodName == CHAT_RECEIVED_MESSAGE_METHOD_NAME);
SignalRHelper(hubConnection: hubConnection).on(
hubMethod.methodName,
hubMethod.methodFunction,
);
listenOnHubMethod.add(hubMethod);
}else{
SignalRHelper(hubConnection: hubConnection).on(
hubMethod.methodName,
hubMethod.methodFunction,
);
listenOnHubMethod.add(hubMethod);
}
}
I am having 2 types of above code in different screens. but it is working in only 1 screen and not listening in 2nd screen.
here is a piece of signalr listener code:
static MethodInvocationFunc toSocketFunction(
String methodName, SocketResponseCallBack responseCallBack) {
return (arguments) {
try {
if (arguments!.isEmpty) {
throw SocketEmptyResponseException(methodName);
}
final response = arguments.first;
responseCallBack(response);
} on FormatException {
throw SocketResponseException(methodName);
}
};
}
Is there any limitations in migration of stable version or anything else. Every help is appreciable.
Thank you.
Do not use signalR, on IOS it will be impossible to run listener on background or when app is closed and you will miss messages. Use FCM.

Flutter Bloc Rx dart combineLatest2 combine function not running

I'm writing a flutter app and and using the bloc library. I have a bloc and a cubit, within the state of each is a list of ids of some other documents I need to fetch from firestore. There can be some overlap and some docs are already fetched so I want to get the list of ids from both states, compare them, and then only go to firestore for ones that exist in one but no the other.
I set a new cubit for this:
class CircleRecipesCubit extends Cubit<CircleRecipesState> {
CircleRecipesCubit({
#required RecipesBloc recipesBloc,
#required CirclesCubit circlesCubit,
}) : assert(
recipesBloc != null,
circlesCubit != null,
),
_recipesBloc = recipesBloc,
_circlesCubit = circlesCubit,
super(CircleRecipesInitial());
final RecipesBloc _recipesBloc;
final CirclesCubit _circlesCubit;
StreamSubscription _recipesSubscription;
StreamSubscription _circlesSubscription;
Future<void> getCircleRecipes() async {
// get a list of recipes the user already has loaded
List<String> userRecipesIds;
_recipesSubscription = _recipesBloc.stream.listen((RecipesState event) {
if (event is RecipesLoaded) {
userRecipesIds = event.recipes.map((e) => e.id).toList();
print('*');
print(userRecipesIds);
print('*');
}
});
// get a list of recipes in the circles
List<String> circleRecipeIds;
_circlesSubscription = _circlesCubit.stream.listen((CirclesState event) {
if (event is CirclesLoaded) {
circleRecipeIds = event.circles.fold([],
(previousValue, element) => [...previousValue, ...element.recipes]);
print('|');
print(circleRecipeIds);
print('|');
// List<String> circleOnlyRecipeIds = circleRecipeIds;
// circleRecipeIds.removeWhere((e) => userRecipesIds.contains(e));
// print(circleOnlyRecipeIds);
}
});
// reduce the list of recipes to a set of only circle recipes
//TODO
//------- Try with RX dart
Stream<RecipesState> recipesStream = _recipesBloc.stream;
Stream<CirclesState> circlesStream = _circlesCubit.stream;
Rx.combineLatest2(recipesStream, circlesStream, (
RecipesState recipesState,
CirclesState circlesState,
) {
print("This doesn't print!");
print(recipesState);
print(circlesState);
if (recipesState is RecipesLoaded) {
userRecipesIds = recipesState.recipes.map((e) => e.id).toList();
print('*');
print(userRecipesIds);
print('*');
}
if (circlesState is CirclesLoaded) {
circleRecipeIds = circlesState.circles.fold([],
(previousValue, element) => [...previousValue, ...element.recipes]);
print('|');
print(circleRecipeIds);
print('|');
// List<String> circleOnlyRecipeIds = circleRecipeIds;
// circleRecipeIds.removeWhere((e) => userRecipesIds.contains(e));
// print(circleOnlyRecipeIds);
}
// fetch the set of recipes
});
}
#override
Future<void> close() {
_recipesSubscription.cancel();
_circlesSubscription.cancel();
return super.close();
}
}
So above is my cubit - it listens to the recipesBloc and the circlesCubit. The first two expressions in the getCiricleRecipes() function are only there to prove that its hooked up correctly - when it runs those print statement print the ids I want it to from both the other bloc and the other cubit.
I need the latest values from both though at the same time to compare them - so I thought rx.combinelatest2 would be good. I give it the stream from the bloc and the cubit. But the combiner function doesn't even run even though things seem 'wired up' correctly.
Any help greatly appreciated.
Make sure both streams have already emitted at least one item.
combineLatest documentation states:
The Stream will not emit until all streams have emitted at least one item.
Since the first block (where you subscribe to _circlesCubit) prints, then most likely _recipesBloc is the culprit here.

Flutter Riverpod design pattern (inhibit garbage collection)

I've written a Swift/IOS package to externalize and standardize all of my Social/Federated/Firebase authentication boilerplate (both SDK's and UI). I've taken it upon myself to port this to Flutter as a learning exercise ... but to also allow custom UI to be passed-in via config.
Since I'm new to Flutter & Riverpod, I'm afraid I'm making some serious mistake & want to get feedback from you experts before I go too deep.
The package is called "Social Login Helper" or SLH, and this is the public API I desire:
runApp(
slh.authStateBuilder(
builder: (authStatus) {
switch (authStatus.stage) {
case SlhResultStage.initializing:
return SplashScreen();
case SlhResultStage.unauthenticated:
// using Riverpod and Nav 2.0
return slh.authFlowUi;
case SlhResultStage.authenticated:
return ExampleApp(appKey, authStatus, slh.logoutCallback);
case SlhResultStage.wantsAnnonOnlyFeatures:
return ExampleApp(appKey, null, slh.startAuthCallback);
case SlhResultStage.excessiveFailures: // restart the app
return TotalFailure();
}
},
),
);
As you can see from the above, the State/Stream builder at root must never be garbage collected or purged. I'm unclear if and when Riverpod will dispose my provider, or if Dart itself will collect objects that must remain immortal. I'm also unsure whether to use a StreamProvider or a State provider??
As you can see below, I've created an intentional memory-leak (deadlock) to guard me. I'm sure it's an anti-pattern, but being novice, I'm not sure how else to guarantee immortality.
All guidance and explicit feedback would be most welcome.
class LivingAuthState extends StateNotifier<SlhResultStage> {
// create deadly embrace to prevent this from ever being collected
_Unpurgeable _up;
LivingAuthState() : super(SlhResultStage.initializing) {
//
final StreamProvider<SlhResultStage> rssp =
StreamProvider<SlhResultStage>((ref) {
return this.stream.asBroadcastStream();
});
_up = _Unpurgeable(this, rssp);
// how do I keep rssp from ever being collected??
}
StreamProvider<SlhResultStage> get authStatusStream => _up.rssp;
void logout() {
this.state = SlhResultStage.unauthenticated;
}
void restartLogin() {
this.state = SlhResultStage.unauthenticated;
}
}
class _Unpurgeable {
final LivingAuthState _aliveState;
final StreamProvider<SlhResultStage> rssp;
_Unpurgeable(this._aliveState, this.rssp);
}
One improvement I'd like to see in the Riverpod documentation is clarity on HOW LONG a provider will live, WITHOUT an active listener, before it will self-dispose / garbage-collect.
Ah, it looks like I can subclass AlwaysAliveProviderBase() to achieve the same goal ... I'll experiment with this.
Move your provider final to the top level. Riverpod providers are top-level final variables.
Also remember to wrap your app in the riverpod provider.