1

I want my Android app to periodically update its UI based on the response from a REST service. I can't do this on the main thread because it's not permitted / bad practice to access the network on the main thread. The general wisdom on SO and the internet is to use a combination a BroadcastReceiver and AlarmManager. For example this is the advice here. I've tried two designs, neither of which I can make to work:

  1. Define a class extending BroadcastReceiver as an inner class of my MainActivity.
  2. Define the same class as an outer class.

With (1) I get this runtime error:

java.lang.RuntimeException: Unable to instantiate receiver com.dbs.alarm.MainActivity$AlarmReceiver: java.lang.InstantiationException: java.lang.Class<com.dbs.alarm.MainActivity$AlarmReceiver> has no zero argument constructor

With (2) the problem is I can't figure out how to access the view I want to modify in MainActivity.

Here is an example implementation of (1):

package com.dbs.alarm;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // I tried making this its own class, but then findViewById isn't accessible.
    public class AlarmReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // I tried wrapping this in runOnUiThread() but it made no difference.
            TextView myTextView = findViewById(R.id.my_text);
            CharSequence myCharSequence = "Set from UpdateReceiver.onReceive()";
            myTextView.setText(myCharSequence);
        }
    }

    private void setRecurringAlarm(Context context) {
        Intent intent = new Intent(context, AlarmReceiver.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        PendingIntent pendingIntent = PendingIntent.getBroadcast(
                context, 0, intent,
                PendingIntent.FLAG_CANCEL_CURRENT);
        AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        alarmManager.setInexactRepeating(
                AlarmManager.ELAPSED_REALTIME_WAKEUP,
                SystemClock.elapsedRealtime() + 1000,
                1000, // Set so short for demo purposes only.
                pendingIntent
        );
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setRecurringAlarm(this);
    }
}

I also added this to my AndroidManifest.xml, and considering that I get an exception it seems to be registered successfully:

<receiver android:name="com.dbs.alarm.MainActivity$AlarmReceiver">
</receiver>
Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
Douglas B. Staple
  • 10,510
  • 8
  • 31
  • 58
  • For the runtime error, I'm pretty sure that if your BroadcastReceiver is an inner class (like yours is), you need to make the class static if you want to register it in the manifest. But then you won't be able to access the non-static properties within it. I'll write up an answer quickly in a second. – pushasha Apr 16 '18 at 19:23
  • Thanks. I'm no Android developer, actually I'm mostly a low-level dev, this is way outside my expertise. – Douglas B. Staple Apr 16 '18 at 19:28
  • AsyncTasks were recommended in 2013 and before. Search for OkHttp and Retrofit, or Volley. They are third-party libs that will allow you to implement a REST service in a much simpler and effective way – Chisko Apr 16 '18 at 19:54

4 Answers4

3

Since you need direct access to your text view, choosing an inner class for your receiver was the right thing to do. However, BroadcastReceivers that are declared as inner classes must be static to be declared in the manifest, which defeats the purpose of making it an inner class in the first place (in your scenario, at least). Because of this, I suggest registering/unregistering your BroadcastReceiver dynamically in the onStart() and onStop() lifecycle methods:

public class MainActivity extends AppCompatActivity {

    private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // the "MainActivity.this" tells it to use the method from the parent class
            MainActivity.this.updateMyTextView();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setRecurringAlarm(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        final IntentFilter filter = new IntentFilter();
        filter.addAction("YOUR_ACTION_NAME"); 
        registerReceiver(alarmReceiver, filter);
    }

    @Override
    protected void onStop() {
        unregisterReceiver(alarmReceiver);
        super.onStop();
    }

    private void updateMyTextView(){
        final TextView myTextView = findViewById(R.id.my_text);
        if (myTextView != null){
            CharSequence myCharSequence = "Set from UpdateReceiver.onReceive()";
            myTextView.post(()-> myTextView.setText(myCharSequence));
        }
    }

    private void setRecurringAlarm(Context context) {
        Intent intent = new Intent("YOUR_ACTION_NAME");
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        PendingIntent pendingIntent = PendingIntent.getBroadcast(
                context, 0, intent,
                PendingIntent.FLAG_CANCEL_CURRENT);
        AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        alarmManager.setInexactRepeating(
                AlarmManager.ELAPSED_REALTIME_WAKEUP,
                SystemClock.elapsedRealtime() + 1000,
                1000, // Set so short for demo purposes only.
                pendingIntent
        );
    }
}

You'll also notice that rather than pass in the receiver class when creating the intent for the alarm, I changed it to use a string ("YOUR_ACTION_NAME") that you can use to define the intent filter your BroadcastReceiver will use to listen for broadcasts.

As for the issue of running the updates on the UI thread, you can always call post() from a view to run something on the UI thread, or use an activity's runOnUiThread like you attempted to do within the BroadcastReceiver. I made the "update" method belong to the activity rather than the broadcast receiver, since it seemed to make more sense that way in my head.


EDIT: When writing this answer, I was more focused on solving the issues you were encountering while implementing your solution rather than actually trying to help solve the larger problem of performing periodic UI updates. @Ashikee AbHi's suggestion of using a Handler for this rather than alarm is definitely something you should consider. An alarm/broadcast receiver is great when you have to notify something in a different process, but if everything is contained in a single activity, it would be much cleaner to use Handler.postDelayed.

pushasha
  • 769
  • 7
  • 15
  • 1
    Thank a lot, this did the trick. Personally I wanted to understand what was going wrong with my previous approach before considering suggestions for other approaches. – Douglas B. Staple Apr 16 '18 at 20:17
1

You can user Handler and call it recursively to perform periodic operations.Check the following Repeat a task with a time delay?

If updating view you need to initialize it with new Handler(Looper.getMainLooper())

Ashikee AbHi
  • 385
  • 2
  • 12
  • My answer more focused on how to deal with the alarm/broadcast receiver issues the asker was having trouble with, but @Ashikee makes a very good point. Using a `Handler` for this type of thing is almost certainly preferable if you're doing everything in one place, and not between processes. +1 – pushasha Apr 16 '18 at 20:03
  • Could this not potentially cause a stack overflow? I don't know what the max recursion depth is in Java but repeating a task indefinitely using recursion seems like a bit much even for a high level language like Java. – Douglas B. Staple Apr 16 '18 at 20:06
  • While the call appears recursive, it actually just adds to Android's built-in `MessageQueue` every time you call `postDelayed`. I don't think that results in the same memory allocation as an actual recursive call. If anyone knows otherwise, please feel free to correct me. – pushasha Apr 16 '18 at 20:25
0

In the simplest way you have to run REST request in the background thread like an AsyncTask doInBackground and send result of the request to UI-thread in onPostExecute. You can do that by means of different ways, but the most convinient for me is a usage of Bus'es, for example Otto.

isabsent
  • 3,683
  • 3
  • 25
  • 46
  • Do you mean I should poll the REST service in an AsyncTask? Or that the BroadcastReceiver should launch an AsyncTask? – Douglas B. Staple Apr 16 '18 at 19:57
  • You have to execute your REST `AsyncTask` each time you want to get some info from server. – isabsent Apr 16 '18 at 20:03
  • Could you please pinpoint a project/tutorial that implements a REST service via AsyncTask? Would be very helpful... – Chisko Apr 16 '18 at 20:16
  • I don't remember a working example project with `AsyncTask` but you can see [this example](https://github.com/bendikv/tinkoff-news-test-task) . More sophisticated Android Priority Job Queue is used there. You can change `Job` with `AsyncTask` but I would prefer to stay with `Job`. – isabsent Apr 16 '18 at 20:22
0

Okay so looking at your requirement I would say that you're fetching some data and you want to let your app know that new data has been fetched so the app can make the necessary UI changes. I would suggest using a Local Broadcast Manager , it allows you to send broadcasts within your app.

The implementation can be found pretty easily, you can check this out. Basically the idea is you fetch data from the REST API, broadcast to your app that data has been fetched and every activity that needs to respond to this event will have a receiver that will get notified.

Jude Fernandes
  • 7,437
  • 11
  • 53
  • 90