swipeRefreshLayout.setOnRefreshListener(() -> {
swipeRefreshLayout.setRefreshing(true);
retrieveData(mCardAdapter, db);
});
For some reason the following method is blocking my main UI thread, but it should be running in the background. For example, the refresh indicator hangs when I run retrieveData(). If I initialize a progress dialog before running, it also hangs and I can't scroll through my RecyclerView. Am I fundamentally misunderstanding something here?
public void retrieveData(final CardAdapter mCardAdapter, SQLiteHelper db) {
CausticRetrofitService service = ServiceFactory.createRetrofitService(CausticRetrofitService.class, CausticRetrofitService.SERVICE_ENDPOINT);
service.getMedia()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber < MediaResponse > () {
#Override
public final void onCompleted() {
Log.e("CausticRetrofitService", "Caustic Request Completed!");
/* Cancel all progress indicators after data retrieval complete */
setRefreshingFalse();
// TODO: Add media to local data store and then display them one-by-one in real-time
mCardAdapter.addData(db.getAllMediaImages()); // Add all media images to card views
Log.d(getClass().toString(), "Added to local database: " + db.getAllMediaImages());
mCardAdapter.notifyDataSetChanged();
}
#Override
public final void onError(Throwable e) {
/* Cancel all progress indicators on data retrieval error */
setRefreshingFalse();
Toast.makeText(getApplicationContext(), "Cannot retrieve data. Please try again later.", Toast.LENGTH_SHORT).show();
Log.e("CausticRetrofitService", e.getMessage());
}
#Override
public final void onNext(MediaResponse mediaResponse) {
if (mediaResponse != null) {
Log.e("CausticRetrofitService", "Returned objects: " + mediaResponse.getResults());
for (String mediaId: mediaResponse.getResults()) {
Log.e("CausticRetrofitService", mediaId);
}
List < String > mediaIds = mediaResponse.getResults();
Log.d(getClass().toString(), "All Media IDs: " + mediaIds);
if (mediaIds.isEmpty()) {
Toast.makeText(getApplicationContext(), "Cannot retrieve data. Please try again later.", Toast.LENGTH_SHORT).show();
}
mCardAdapter.clear();
mCardAdapter.notifyDataSetChanged();
/* Store objects from remote web service to local database */
for (String mediaId: mediaIds) {
// TODO: Why are these null?
Log.d(getClass().toString(), "Media Id: " + mediaId);
MediaImage newMediaImage = new MediaImage();
newMediaImage.setTitle(mediaId);
db.addMediaImage(newMediaImage); // Add media image to local database
}
} else {
Log.e("CausticRetrofitService", "Object returned is null.");
}
}
});
}
I'm thinking that adding the remote data to the local data store in the onNext() method might be the thing that's blocking, although I'm not certain.
Your network call is done in a new thread as you specified, but the Subscriber methods onNext() and onComplete() runs on the observing Scheduler, which is the main thread.
You seem to be doing some database operations on those, try to offload the caching also to the background thread using a doOnNext() operator.
What doOnNext() will do, is that it is called for each emission in your stream.
It can go something like that
service.getMedia()
.doOnNext(data -> cacheData(data))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
Where cacheData() is a method that does all your DB calls. And the only things left in your onNext() and onComplete() would be updating the UI only.
Related
I am new to doing asynchronous programming in Android Java. I am wondering if there is a way to run another Callback after an initial Callback function has completed. Right now, I think they are running in parallel even though the second relies on the first.
First Callback:
// GETTING USER
private interface FirestoreUserCallback {
void onCallback (User myUser);
}
private void getUser(final FirestoreUserCallback firestoreCallback) {
Task<DocumentSnapshot> task = fStore.collection("users").document(fAuth.getCurrentUser().getUid()).get();
task.addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
user = documentSnapshot.toObject(User.class);
firestoreCallback.onCallback(user);
Log.d(TAG, "user created");
}
});
task.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.d(TAG, "user creation failed");
}
});
}
Second Callback:
// GETTING ALL DOCUMENTS
private interface FirestoreDocumentCallback {
void onCallback (List<TableEntries> myEntries);
}
private void getDocuments (final FirestoreDocumentCallback firestoreDocumentCallback) {
fStore.collection("result")
.document(Integer.toString(user.getCompanyNumber())) // need to use User object returned from the first Callback
.collection("SAM").get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
List<TableEntries> results = new ArrayList<>();
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
// add objects to results ArrayList ...
Log.d(TAG, document.getId() + " => " + document.getData());
}
firestoreDocumentCallback.onCallback(results);
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
}
onCreate:
getUser(new FirestoreUserCallback () {
#Override
public void onCallback(User myUser) {
user = myUser;
}
});
getDocuments(new FirestoreDocumentCallback() {
#Override
public void onCallback(List<TableEntries> myEntries) {
entries = myEntries;
}
});
getDocuments() relies on the user variable being given its value from the first Callback. I'm receiving this error:
java.lang.NullPointerException: Attempt to invoke virtual method 'double java.lang.Double.doubleValue()' on a null object reference
Callbacks are looking fine. You just need to check if your value is null or not before accessing it. Just add a null check
if(doubleValue!=null)
Using RxJava. First, we fetch the user and then fetch the documents. Rx-Java has an operator flatmap. flatmap is used to execute the sequential tasks, where the second task is dependent on the data from the first task.
final CompositeDisposable disposable = new CompositeDisposable();
//function to fetch user data
Single<User> getUser(){
return API.getUserData(...);
}
//function to fetch ducuments
Sinlge<UserDetail> getDocuments(int userId){
return API.getUserDetail(userId, ...);
}
//Subscribe
disposable.add(getUser()
.flatmap(user-> return getDocuments(...))
.subscribeOn(Scheduler.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableSingleObservable(){
#Override
public void onSuccess(UserDetail userDetail){
Log.v("Api result", "Successful";
//Do some work
}
#Override
public void onError(Throwable e)
Log.v("Api result", "Error Returned");
}
}));
If either of the API call fails, onError() is called. If first API fails, second API call is not executed and onError() is called.
The simplest solution for your use-case is to pass both queries to Tasks.whenAllSuccess() method, as explained in my answer from the following post:
Firestore - Merging two queries locally
So once the task is complete, you can use the elements from both queries. Another solution might be to use Android Jetpack with LiveData along with ViewModel, as the Android team recommends.
I have an issue with android studio and the retrofit library and the way in which it processes the data.
I have a simple flow of operation I would like:
Request single item from database on server(fetch request)
Wait for callback to confirm it has been received by the app
Add another request(Loop)
Stop adding requests when all data is sent
The issue I have is my onResponse callback for my fetch result does not run until all my requests are sent. Then all the responses are errors. ( If I call a single item(1 from the database)) the call back runs fine.
How do I force it to send one request and wait until that response before sending another?
Loop code
private void Pull_data_loop(int total_entries){
//int current_data_point = 0;
boolean datum_processing = false;
for (int i = 1; i <= total_entries; i++) {
Add_single_datam(i);//Call until all entries are fetched from the server
}
}
Fetch code- Not running callback need to wait for this callback before sending next request
private void Add_single_datam(int id)
{
HashMap<String, String> map = new HashMap<>();
map.put("Id_request", Integer.toString(id));//The ID value
Call<Fetch_result> call = retrofitInterface.executeGet_data(map);//Run the post
call.enqueue(new Callback<Fetch_result>() {
#Override
public void onResponse(Call<Fetch_result> call, Response<Fetch_result> response) {
if (response.code() == 200)//Successful login
{
D1= response.body().getD1_String();
D2= response.body().getD2_String();
boolean result = BLE_DB.addData_Downloaded(D1, D2);//Add data
if (result == true) {
Log.d(TAG, "data_changes: Added data correctly");
}
if (result == false) {
Log.d(TAG, "data_changes: did not add data correctly");
}//false
} else if (response.code() == 404) {
Utils.toast(getApplicationContext(), "Get data fail");//Pass information to the display
}
}
#Override
public void onFailure(Call<Fetch_result> call, Throwable t) {
Utils.toast(getApplicationContext(), "Get data error");
}
});
}
Note:
I am using a node js server for my requests. I send the Id and it returns that Id in the database.
You could send a callBack instance to your Add_single_datam then in your retrofit response, send to that callback success.
Then in that callBack you would have iteravel i and you could see if you reached the end of total_entries added +1 in i and make request again, or just stop.
use some threading solutions like RxJava or Coroutines or AsyncTask. The reason it's not following the rule is because of there are two threads on which work is getting distributed so in order to get it make it work in sync, we have to use some threading solutions mentioned above and execute this for loop on the background thread and make it like a synchronous call and get all the results and finally switch back to main thread with the results.
If you are familiar with the AsynTask.
private class FetchDataTask extends AsyncTask<Int, Integer, List<Fetch_result>> {
protected Long doInBackground(Int... total_entries) {
List<Fetch_result> allResults = new ArrayList<Fetch_result>();
for (int i = 1; i <= total_entries[0]; i++) {
HashMap<String, String> map = new HashMap<>();
map.put("Id_request", Integer.toString(total_entries[0]));
Fetch_result response = retrofitInterface.executeGet_data(map).execute().body();
allResults.add(response);
}
return allResults;
}
protected void onProgressUpdate(Integer... progress) {
//show progress
}
protected void onPostExecute(List<Fetch_result> result) {
//do something on main thread, in loop on result
D1= result[0].getD1_String();
D2= result[0].getD2_String();
boolean result = BLE_DB.addData_Downloaded(D1, D2);//Add data
if (result == true) {
Log.d(TAG, "data_changes: Added data correctly");
}
if (result == false) {
Log.d(TAG, "data_changes: did not add data correctly");
}//false
}
}
now call like this.
new FetchDataTask().execute(total_entries);
This question already has answers here:
Combining Firebase realtime data listener with RxJava
(5 answers)
Closed 2 years ago.
Does anyone knows how to connect Firebase with RxJava so when I load ALL my data from database then it runs arrayAdapter.notifyDataSetChanged() ??
I was thinking to write it in onComplete() method but it still runs before loading all data
Completable.fromCallable(new Callable<List<cards>>() {
#Override
public List<cards> call() throws Exception {
newUserDb.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
if (dataSnapshot.child(currentUID).child("sex").exists()) {
myInfo.put("sex", dataSnapshot.child(currentUID).child("sex").getValue().toString());
}
if (dataSnapshot.child(currentUID).child("dateOfBirth").exists()) {
int myAge = stringDateToAge(dataSnapshot.child(currentUID).child("dateOfBirth").getValue().toString());
myInfo.put("age", String.valueOf(myAge));
}
if (dataSnapshot.child(currentUID).child("connections").child("yes").exists()) {
for (DataSnapshot ds : dataSnapshot.child(currentUID).child("connections").child("yes").getChildren()) {
if (!dataSnapshot.child(currentUID).child("connections").child("matches").hasChild(ds.getKey())) {
Log.d("rxJava", "onDataChange: " + ds.getKey());
first.add(ds.getKey());
getTagsPreferencesUsers(dataSnapshot.child(ds.getKey()), true);
}
}
}
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
return rowItems;
}
}).subscribeOn(Schedulers.computation())
.subscribe(new CompletableObserver() {
#Override
public void onSubscribe(Disposable d) {
Log.d("rxJava", "Test RxJAVA, onSubscribe");
}
#Override
public void onComplete() {
Log.d("rxJava", "Test RxJAVA, onComplete");
}
#Override
public void onError(Throwable error) {
Log.d("rxJava", "Test RxJAVA, onError");
}
});
and the output is
2020-06-04 23:36:28.797 29515-29515/com.example.tinderapp D/rxJava: Test RxJAVA, onSubscribe
2020-06-04 23:36:28.800 29515-29612/com.example.tinderapp D/rxJava: Test RxJAVA, onComplete
2020-06-04 23:36:29.018 29515-29515/com.example.tinderapp D/rxJava: onDataChange: a4hqGgAJBRTVJOlPp3blNDt5v7q1
2020-06-04 23:36:29.022 29515-29515/com.example.tinderapp D/rxJava: onDataChange: aA9HAOtaB7ao6vzKqqBNp0iaBev2
I would say, this is expected behavior, which is described in the following:
Completable.fromCallable
fromCallable takes a Lambda, which returns a List on subscription. In your case, a database-connection is opened as-well, which is basically fall through, because the callback is registered via callback non-blocking.
subscribeOn
this makes sure, that the subscribeAcutal from fromCallable is called from given scheduler. Therefore the subscribing thread and and emitting thread are decoupled.
You get onComplete first, because the fromCallable will return rowItems immediately and the database connection will stay open, because you did not remove the listener. After a while you get data-base callback logs, because the database connection is still open and the listener is still registered.
You want to actually do something like this:
Single.create<List<Card>> { emitter ->
// register onChange callback to database
// callback will be called, when a value is available
// the Single will stay open, until emitter#onSuccess is called with a collected list.
newUserDb.addListenerForSingleValueEvent {
// do some stuff
emitter.onSuccess(listOf()) // return collected data from database here...
}
emitter.setCancellable {
// unregister addListenerForSingleValueEvent from newUserDb here
}
}.subscribeOn(Schedulers.computation())
.subscribe(
// stuff
)
If you want to have a constant stream of updates, exchange Single with Observable/ Flowable
I am new to Android’s background tasks. I am using Firestore to perform the following tasks:
Read a document.
https://firebase.google.com/docs/firestore/query-data/get-data
DBInstance.collection("restaurants")
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : Objects.requireNonNull(task.getResult())) {
// some other code
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
});
Listen to realtime updates of another document. https://firebase.google.com/docs/firestore/query-data/listen
final DocumentReference docRef = DBInstance.collection("users").document(FirebaseAuth.getInstance().getCurrentUser().getUid());
docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
String source = snapshot != null && snapshot.getMetadata().hasPendingWrites()
? "Local" : "Server";
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, source + " data: " + snapshot.getData());
// some other code is run
} else {
Log.i(TAG,"no snapshot found");
}
}
});
Since these are asynchronous processes, they are performed at the same time (roughly).
I want to trigger an independent method when 1. is completed AND when 2. return a non-null snapshot. Therefore, when some other code comments above have been completed.
So, I essentially want some background process that sits idle/ listens for the above two conditions and perform a task/call a method that updates certain UI features.
I have briefly read about BroadcastReciever. Is this relevant? or maybe can I create a custom listener that runs in a background thread? Any suggestions would be helpful since I am not sure what to search for in order to find what I want.
solutions that seems to work (partly suggested by Nehal)
This is the same code as above with the blanks filled in
DBInstance.collection("restaurants")
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : Objects.requireNonNull(task.getResult())) {
restaurantsLoaded = true;
updateUI();
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
restaurantsLoaded = false;
}
});
final DocumentReference docRef = DBInstance.collection("users").document(FirebaseAuth.getInstance().getCurrentUser().getUid());
docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
String source = snapshot != null && snapshot.getMetadata().hasPendingWrites()
? "Local" : "Server";
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, source + " data: " + snapshot.getData());
usersSnapshotTriggered = true;
udpateUI();
} else {
Log.i(TAG,"no snapshot found");
}
}
});
public void updateUI(){
if(usersSnapshotTriggered && restaurantsLoaded){
// perform the updates
}
}
You can try below solution:
Declare a global int variable, increment that variable in both firebase listener and call someMethod() from both listener.
private int count=0;
DBInstance.collection("restaurants")
.get()
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : Objects.requireNonNull(task.getResult())) {
// some other code
}
count++;
someMethod();
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
});
final DocumentReference docRef = DBInstance.collection("users").document(FirebaseAuth.getInstance().getCurrentUser().getUid());
docRef.addSnapshotListener(new EventListener<DocumentSnapshot>() {
#Override
public void onEvent(#Nullable DocumentSnapshot snapshot,
#Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
String source = snapshot != null && snapshot.getMetadata().hasPendingWrites()
? "Local" : "Server";
if (snapshot != null && snapshot.exists()) {
Log.d(TAG, source + " data: " + snapshot.getData());
// some other code is run
count++;
someMethod();
//Note : this method will call as many times as there is change in this data , so you have to handle according to your requirement
} else {
Log.i(TAG,"no snapshot found");
}
}
});
private void someMethod(){
if(count>=2){
//execute your code
}
}
Hope this will help!!
I want to trigger an independent method when both 1. and 2. have been completed
In your first example, by adding a complete listener, you'll always be able to know when the operation is complete. If the task.isSuccessful() returns true you know for sure that the operation is completed. Besides that, you can also call getResult() to get the elements that are apart of your restaurants collection. Furthermore, the following line of code:
DBInstance.collection("restaurants")
.get()
Returns a Task<QuerySnapshot> object. If you have had two different queries, you could pass both Task objects to Tasks's whenAllSuccess() method, as explained in my answer from the following post:
Firestore - Merging two queries locally
In this way, you'll be able to know when both operations are completed. However, when using the second solution, you cannot know when getting the data from the database is completed because Cloud Firestore is a real-time database and getting data might never complete. That's why is named a real-time database because at any moment the database can be changed, items can be added or deleted.
The only way to partially know if you have all the data in a particular collection is to perform a single value type query on it. Even then, the data may change after that listener is invoked, so all you really have is a snapshot at a particular moment in time.
As a conclusion, the only solution that you have is to use whenAllSuccess() and pass two or even more Task objects.
I have briefly read about BroadcastReciever. Is this relevant?
No, it's not. According to the docs, the BroadcastReceiver class is:
Base class for code that receives and handles broadcast intents sent by Context.sendBroadcast(Intent).
So, it's not the case.
or maybe can I create a custom listener that runs in a background thread?
The Cloud Firestore client already runs all network operations in a background thread. This means that all operations take place without blocking your main thread. Putting it in a background thread does not give any additional benefits.
I have been reading for hours about different multi-threading techniques for JavaFX and cannot seem to find what I'm looking for. The application being worked on is the "Messenger" which part of a bigger application to provide a trading marketplace for a game.
A breakdown of the process I am having trouble with:
A window with a 'Contact Seller' button is displayed
The user clicks 'Contact Seller', and the Messenger window should be displayed
Using the seller's name from the main window, the Messenger should check if a chat already exists with that name
If the chat already exists, get the index of that chat in the Messenger's ListView, and select the chat so the Messenger's text area is populated with the corresponding messages
If the chat doesn't exist, create one
The issue:
Chats are stored server-side
Messages between clients are stored in a message 'cache' on the server when being processed from one user to the other (they are sorted to their respective chats, inserted into the chats, and pushed to the database upon client disconnection)
Messages received on the client are stored locally in the respective chat
When the Messenger is opened, it requests a list of chats for the signed-in user
The server sends an ArrayList of chats, and upon receiving the client builds the Messenger's ListView with these objects
Now when I need to open the Messenger in order to 'Contact Seller', I need to make sure the sync with the server is complete. Without doing so, I won't be able to properly check if a chat already exists with that name since I won't have the most up-to-date list.
The 'RequestWorker' thread that handles incoming server messages is NOT on the JavaFX thread.
The RequestWorker 'gets' the Messenger instance if it is currently open, and populates the ListView with the newly received chat list. (This needs to happen on the JavaFX thread, since I am working in the Messenger GUI)
What I am trying to do is set a static AtomicBoolean syncInProgress to true when the Messenger initiates a sync upon being displayed. When the RequestWorker receives the latest list back from the server and finishes populating the Messengers ListView, it sets syncInProgress to false.
The sync takes longer than opening the Messenger and making it check if a chat exists. Doing it this way it has no items populated yet in the ListView and the method is ineffective.
Calling a while loop to wait until the boolean is changed, blocks the JavaFX thread which means the RequestWorker cannot do what it needs to in the JavaFX thread.
How can I continuously check for this variable to be set false, then continue to 'Contact Seller' once the ListView has been properly populated?
Contact Seller method: The while loop in here causes a block on the JavaFX thread, thus not enabling RequestWorker to properly populate the ListView.
public static void contactSeller(Messenger messenger, String destination, String itemName)
{
while (TarkovTrader.syncInProgress.get())
{
; // Wait until sync is complete to check the latest chat list for an existing chat
}
if (messenger.chatExists(destination))
{
// Chat exists, select the chat for the user
for (Chat openChat : messenger.chatListView.getItems())
{
if (openChat.getName(TarkovTrader.username).equals(destination))
{
messenger.chatListView.getSelectionModel().select(openChat);
messenger.unpackChatMessages(openChat.getMessages());
break;
}
}
}
else
{
messenger.buildNewChat(destination);
}
messenger.chatInput.setText("Hey " + destination + ". Interested in your '" + itemName + "'.");
messenger.chatInput.setOnMouseClicked(e -> messenger.chatInput.clear());
}
RequestWorker process chat block:
switch(receivedFromServer)
case "chatlist":
// Client requested a chat list, results were returned from the server, and now we need to populate the messenger list
ChatListForm chatlistform = (ChatListForm)processedRequest;
if (Messenger.isOpen)
{
FutureTask<Void> updateChatList = new FutureTask(() -> {
Messenger tempMessenger = trader.getMessenger();
int currentIndex = tempMessenger.chatListView.getSelectionModel().getSelectedIndex();
tempMessenger.populate(chatlistform.getChatList());
tempMessenger.chatListView.getSelectionModel().select(currentIndex);
}, null);
Platform.runLater(updateChatList); // RequestWorker needs access to the JavaFX application thread
try {
updateChatList.get(); // Wait until the ListView has been populated before setting 'syncInProgress' to false again
}
catch (InterruptedException e) {
Alert.display(null, "Sync interrupted.");
}
catch (ExecutionException e) {
Alert.display(null, "Sync failed.");
}
TarkovTrader.syncInProgress.compareAndSet(true, false); // The value of syncInProgress should be true, change to false. Sync complete
}
else
{
Platform.runLater(() -> Alert.display(null, "New chat received."));
TarkovTrader.syncInProgress.compareAndSet(true, false);
}
break;
Contact Seller button logic: If a messenger is not open, create it and pass to static contactSeller method to use.
contactButton.setOnAction(e -> {
Messenger messenger;
if (Messenger.isOpen)
{
// Get messenger
messenger = trader.getMessenger();
}
else
{
messenger = new Messenger(worker);
messenger.display();
trader.setMessenger(messenger);
}
Messenger.contactSeller(messenger, item.getUsername(), item.getName());
itemdisplay.close();
});
EDIT:
Partially using Slaw's idea (AtomicBoolean is still being used since I'm not sure how to do this without it), this is what I came up with...
public static void contactSeller(Messenger messenger, String destination, String itemName)
{
Task<Void> waitForSync = new Task<Void>() {
#Override
public Void call()
{
while (TarkovTrader.syncInProgress.get())
{
; // Wait until sync is complete
}
return null;
}
};
waitForSync.setOnSucceeded(e -> {
while (TarkovTrader.syncInProgress.get())
{
; // Wait until sync is complete to check the latest chat list for an existing chat
}
if (messenger.chatExists(destination))
{
// Chat exists, select the chat for the user
for (Chat openChat : messenger.chatListView.getItems())
{
if (openChat.getName(TarkovTrader.username).equals(destination))
{
messenger.chatListView.getSelectionModel().select(openChat);
messenger.unpackChatMessages(openChat.getMessages());
break;
}
}
}
else
{
messenger.buildNewChat(destination);
}
messenger.chatInput.setText("Hey " + destination + ". Interested in your '" + itemName + "'.");
messenger.chatInput.setOnMouseClicked(me -> messenger.chatInput.clear());
});
Thread t = new Thread(waitForSync);
t.setDaemon(true);
t.start();
}
Which does work, but this doesn't seem like a great solution. Is it fine to do something like this or is there a preferred method over this? I feel like the while loop and using a triggered boolean is sloppy..but is this common practice?
Don't try to handle the opened chat in the same method that schedules the chat retrieval. Instead create a queue of handlers that is executed as soon as the chat is ready.
Simplified Example
public class ChatManager {
private final Map<String, Chat> openChats = new HashMap<>();
// only call from application thread
public void openChat(String user, Consumer<Chat> chatReadyHandler) {
Chat chat = openChats.computeIfAbsent(user, this::createNewChat);
chat.addReadyHandler(chatReadyHandler);
}
private Chat createNewChat(String user) {
return new Chat(user);
}
public class Chat {
// list keeping track of handlers any used for synchronisation
private final ArrayList<Consumer<Chat>> readyHandlers = new ArrayList<>(1);
private boolean ready = false;
private final String user;
public String getUser() {
return user;
}
private void addReadyHandler(Consumer<Chat> chatReadyHandler) {
synchronized (readyHandlers) {
// if already ready, immediately execute, otherwise enqueue
if (ready) {
chatReadyHandler.accept(this);
} else {
readyHandlers.add(chatReadyHandler);
}
}
}
private void chatReady() {
synchronized (readyHandlers) {
ready = true;
}
// execute all handlers on the application thread
Platform.runLater(() -> {
synchronized (readyHandlers) {
for (Consumer<Chat> readyHandler : readyHandlers) {
readyHandler.accept(this);
}
readyHandlers.clear();
readyHandlers.trimToSize();
}
});
}
private Chat(String user) {
this.user = user;
new Thread(() -> {
try {
Thread.sleep(10000); // simulate time required to acquire chat
} catch (InterruptedException ex) {
}
chatReady();
}).start();
}
}
}
The following code creates a Chat with a user when Enter is pressed after typing the user name in the TextField and prints a message to the TextArea when the chat is ready.
#Override
public void start(Stage primaryStage) throws Exception {
ChatManager worker = new ChatManager();
TextField userName = new TextField();
TextArea textArea = new TextArea();
textArea.setEditable(false);
userName.setOnAction(evt -> {
String user = userName.getText();
userName.clear();
textArea.appendText("opening chat for " + user + "\n");
worker.openChat(user, chat -> textArea.appendText("chat for " + chat.getUser() + " ready\n"));
});
Scene scene = new Scene(new VBox(10, userName, textArea));
primaryStage.setScene(scene);
primaryStage.show();
}