0

I'm using the ScheduledExecutorService to get data from a website every 15 seconds:

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(this::refresh, 0, 15, TimeUnit.SECONDS);

This is the method which is being called every 15 seconds:

public void refresh() {
        Site.refreshData();
        System.out.println("Refreshed Data");
        LinearLayout listView = findViewById(R.id.linearLayout);
        System.out.println("Size: " + List.getList().size());
        for (int i = 0; i < List.getList().size(); i++) {
            Entry entry = List.getList().get(i);

            CardView cardView = (CardView) LayoutInflater.from(
                    getApplicationContext()).inflate(R.layout.card, listView, false);

            ImageView checkedView = (ImageView) cardView.getChildAt(0);
            checkedView.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(),
                    entry.isChecked() ? R.drawable.check : R.drawable.cross));

            TextView name = (TextView) cardView.getChildAt(1);
            name.setText(entry.getName());

            TextView comment = (TextView) cardView.getChildAt(2);
            comment.setText(entry.getComment());

            listView.addView(cardView, i);
        }
        System.out.println("Added Cardviews");
        listView.invalidate();
        System.out.println("Refreshed LinearLayout");
    }

I've added multiple prints, as a poor-mans debugger, I'm only getting to the point where the size of the ArrayList is being printed. From there on nothing is being printed, it's like the execution stops. I'm sure the error is happening inside the for loop. I've checked by adding a print, which show me the current i and it just stopped at 4, even though the list size is 57.

What's the problem?

moeux
  • 191
  • 15
  • 1
    Your problem is that you add GUI elements in a worker thread. Quote from the Android doc: **So, you must not manipulate your UI from a worker thread—you must do all manipulation to your user interface from the UI thread.** – Robert Nov 20 '20 at 23:28
  • @Robert how would I be able to do that? I have to refresh my data using the ScheduledExecutorService, so how would I "go back" to the main thread to manipulate the UI? Since the method being called by the SES and everything inside will operate in the SES-Thread not the main thread. Could use some detailed answer regarding that, please. – moeux Nov 21 '20 at 13:54
  • The common way is to update a data structure on the worker thread and then let the main thread do the update work: https://stackoverflow.com/questions/16483462/update-the-ui-outside-the-main-thread – Robert Nov 21 '20 at 15:07

2 Answers2

2

Before building the GUI, get your scheduled page retrievals working on the console.

Here is an example app doing just that. Every five seconds for a half-minute we download a page and dump its various parts to console.

This demo uses the HttpClient classes added to Java 11 per JEP 321: HTTP Client. The web page access code shown here was copied from this article.

Tip: Always gracefully shutdown your executor service, as its thread pool may continue running indefinitely even after your app ends.

package work.basil.example;

import java.io.IOException;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class PageFetch
{
    public static void main ( String[] args )
    {
        PageFetch app = new PageFetch();
        app.demo();
    }

    private void demo ( )
    {
        Runnable pageFetchRunnable = ( ) -> { this.fetchPage(); };

        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        scheduledExecutorService.scheduleAtFixedRate( pageFetchRunnable , 1 , 5 , TimeUnit.SECONDS );  // Wait one second, then every five seconds.

        try
        {
            Thread.sleep( Duration.ofSeconds( 30 ).toMillis() );  // Let the executor service do its thing for a half-minute, then shut it down.
        }
        catch ( InterruptedException e )
        {
            e.printStackTrace();
        }
        finally
        {
            scheduledExecutorService.shutdown();
        }
    }

    private void fetchPage ( )
    {
        // Example code for using `HttpClient` framework of Java 11 taken from this article:
        // https://mkyong.com/java/java-11-httpclient-examples/

        HttpClient httpClient = HttpClient.newBuilder()
                .version( HttpClient.Version.HTTP_2 )
                .followRedirects( HttpClient.Redirect.NORMAL )
                .connectTimeout( Duration.ofSeconds( 20 ) )
//                .proxy( ProxySelector.of(new InetSocketAddress("proxy.yourcompany.com", 80)))
//                .authenticator( Authenticator.getDefault())
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .GET()
                .uri( URI.create( "https://httpbin.org/get" ) )
                .setHeader( "User-Agent" , "Java 11 HttpClient Bot" ) // add request header
                .build();

        HttpResponse < String > response = null;
        try
        {
            System.out.println( "\n-----|  Demo  |-------------------------------------------" );
            System.out.println( "INFO - Access attempt at " + Instant.now() );
            response = httpClient.send( request , HttpResponse.BodyHandlers.ofString() );
            // print response headers
            HttpHeaders headers = response.headers();
            headers.map().forEach( ( k , v ) -> System.out.println( k + ":" + v ) );

            // print status code
            System.out.println( response.statusCode() );

            // print response body
            System.out.println( response.body() );
        }
        catch ( IOException e )
        {
            e.printStackTrace();
        }
        catch ( InterruptedException e )
        {
            e.printStackTrace();
        }
    }
}

When run:

-----|  Demo  |-------------------------------------------
INFO - Access attempt at 2020-11-20T21:54:37.905896Z
:status:[200]
access-control-allow-credentials:[true]
access-control-allow-origin:[*]
content-length:[242]
content-type:[application/json]
date:[Fri, 20 Nov 2020 21:54:38 GMT]
server:[gunicorn/19.9.0]
200
{
  "args": {}, 
  "headers": {
    "Host": "httpbin.org", 
    "User-Agent": "Java 11 HttpClient Bot", 
    "X-Amzn-Trace-Id": "Root=1-5fb83b1e-7a6acb893aec6fb310984adb"
  }, 
  "origin": "76.22.40.96", 
  "url": "https://httpbin.org/get"
}



-----|  Demo  |-------------------------------------------
INFO - Access attempt at 2020-11-20T21:54:42.907678Z
:status:[200]
access-control-allow-credentials:[true]
access-control-allow-origin:[*]
content-length:[242]
content-type:[application/json]
date:[Fri, 20 Nov 2020 21:54:43 GMT]
server:[gunicorn/19.9.0]
200
{
  "args": {}, 
  "headers": {
    "Host": "httpbin.org", 
    "User-Agent": "Java 11 HttpClient Bot", 
    "X-Amzn-Trace-Id": "Root=1-5fb83b23-3dbb566f5d58a8a367e0e528"
  }, 
  "origin": "76.22.40.96", 
  "url": "https://httpbin.org/get"
}

… and so on for a half-minute.
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Thank you for your answer. Could you maybe provide a more Android-related answer? Because using your method causes a "app is not responding" error. I've also overwritten the `onStop()` method in which I shut down the ScheduledExecutorService, thanks for the tip! – moeux Nov 21 '20 at 13:51
  • @M0e (a) No, sorry, I don’t know Android programming. (b) And your Android app would not be responding because my code example sleeps the main thread. I do so because in a plain-vanilla Java app, we need to allow time for the background threads to do their work before the app execution ends and exits. But in an Android/Swing/JavaFX app, you should never sleep the main UI thread because the app becomes non-responsive to the user, appearing to have crashed. – Basil Bourque Nov 21 '20 at 16:59
2

After even more searching I've found a method which does exactly what I want:

runOnUiThread(Runnable runnable)

//scheduler.scheduleAtFixedRate(this::refresh, 0, 15, TimeUnit.SECONDS); ->

public void refresh() {
        Site.refreshData();
        runOnUiThread(() -> {
            ((TextView) findViewById(R.id.queueView)).setText("Total Entries: " + List.getList().size());
            LinearLayout listView = findViewById(R.id.linearLayout);
            listView.removeAllViews();
            for (Entry entry : List.getList()) {

                CardView cardView = (CardView) LayoutInflater.from(
                        getApplicationContext()).inflate(R.layout.card, listView, false);

                ImageView checkedView = (ImageView) cardView.getChildAt(0);
                checkedView.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(),
                        entry.isChecked() ? R.drawable.check : R.drawable.cross));

                TextView name = (TextView) cardView.getChildAt(1);
                name.setText(entry.getName());

                TextView comment = (TextView) cardView.getChildAt(2);
                comment.setText(entry.getComment());

                listView.addView(cardView);
            }
            Toast.makeText(getApplicationContext(), "Refreshed data and UI.", Toast.LENGTH_SHORT).show();
        });
    }
moeux
  • 191
  • 15