Android update activity UI from service

Android

Android Problem Overview


I have a service which is checking for new task all the time. If there is new task, I want to refresh the activity UI to show that info. I did find https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ this example. Is that a good approch ? Any other examples?

Thanks.

Android Solutions


Solution 1 - Android

See below for my original answer - that pattern has worked well, but recently I've started using a different approach to Service/Activity communication:

  • Use a bound service which enables the Activity to get a direct reference to the Service, thus allowing direct calls on it, rather than using Intents.

  • Use RxJava to execute asynchronous operations.

  • If the Service needs to continue background operations even when no Activity is running, also start the service from the Application class so that it does not get stopped when unbound.

The advantages I have found in this approach compared to the startService()/LocalBroadcast technique are

  • No need for data objects to implement Parcelable - this is particularly important to me as I am now sharing code between Android and iOS (using RoboVM)
  • RxJava provides canned (and cross-platform) scheduling, and easy composition of sequential asynchronous operations.
  • This should be more efficient than using a LocalBroadcast, though the overhead of using RxJava may outweigh that.

Some example code. First the service:

public class AndroidBmService extends Service implements BmService {

    private static final int PRESSURE_RATE = 500000;   // microseconds between pressure updates
    private SensorManager sensorManager;
    private SensorEventListener pressureListener;
    private ObservableEmitter<Float> pressureObserver;
    private Observable<Float> pressureObservable;

    public class LocalBinder extends Binder {
        public AndroidBmService getService() {
            return AndroidBmService.this;
        }
    }

    private IBinder binder = new LocalBinder();

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        logMsg("Service bound");
        return binder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_NOT_STICKY;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
        if(pressureSensor != null)
            sensorManager.registerListener(pressureListener = new SensorEventListener() {
                @Override
                public void onSensorChanged(SensorEvent event) {
                    if(pressureObserver != null) {
                        float lastPressure = event.values[0];
                        float lastPressureAltitude = (float)((1 - Math.pow(lastPressure / 1013.25, 0.190284)) * 145366.45);
                        pressureObserver.onNext(lastPressureAltitude);
                    }
                }

                @Override
                public void onAccuracyChanged(Sensor sensor, int accuracy) {

                }
            }, pressureSensor, PRESSURE_RATE);
    }

    @Override
    public Observable<Float> observePressure() {
        if(pressureObservable == null) {
            pressureObservable = Observable.create(emitter -> pressureObserver = emitter);
            pressureObservable = pressureObservable.share();
        }
         return pressureObservable;
    }

    @Override
    public void onDestroy() {
        if(pressureListener != null)
            sensorManager.unregisterListener(pressureListener);
    }
} 

And an Activity that binds to the service and receives pressure altitude updates:

public class TestActivity extends AppCompatActivity {

    private ContentTestBinding binding;
    private ServiceConnection serviceConnection;
    private AndroidBmService service;
    private Disposable disposable;

    @Override
    protected void onDestroy() {
        if(disposable != null)
            disposable.dispose();
        unbindService(serviceConnection);
        super.onDestroy();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.content_test);
        serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                logMsg("BlueMAX service bound");
                service = ((AndroidBmService.LocalBinder)iBinder).getService();
                disposable = service.observePressure()
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(altitude ->
                        binding.altitude.setText(
                            String.format(Locale.US,
                                "Pressure Altitude %d feet",
                                altitude.intValue())));
            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) {
                logMsg("Service disconnected");
            }
        };
        bindService(new Intent(
            this, AndroidBmService.class),
            serviceConnection, BIND_AUTO_CREATE);
    }
}

The layout for this Activity is:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.controlj.mfgtest.TestActivity">

        <TextView
            tools:text="Pressure"
            android:id="@+id/altitude"
            android:gravity="center_horizontal"
            android:layout_gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    </LinearLayout>
</layout>

If the service needs to run in the background without a bound Activity it can be started from the Application class as well in OnCreate() using Context#startService().


My Original Answer (from 2013):

In your service: (using COPA as service in example below).

Use a LocalBroadCastManager. In your service's onCreate, set up the broadcaster:

broadcaster = LocalBroadcastManager.getInstance(this);

When you want to notify the UI of something:

static final public String COPA_RESULT = "com.controlj.copame.backend.COPAService.REQUEST_PROCESSED";

static final public String COPA_MESSAGE = "com.controlj.copame.backend.COPAService.COPA_MSG";

public void sendResult(String message) {
	Intent intent = new Intent(COPA_RESULT);
	if(message != null)
		intent.putExtra(COPA_MESSAGE, message);
	broadcaster.sendBroadcast(intent);
}

In your Activity:

Create a listener on onCreate:

public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	super.setContentView(R.layout.copa);
	receiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			String s = intent.getStringExtra(COPAService.COPA_MESSAGE);
            // do something here.
		}
	};
}

and register it in onStart:

@Override
protected void onStart() {
	super.onStart();
	LocalBroadcastManager.getInstance(this).registerReceiver((receiver), 
        new IntentFilter(COPAService.COPA_RESULT)
    );
}

@Override
protected void onStop() {
	LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
	super.onStop();
}

Solution 2 - Android

for me the simplest solution was to send a broadcast, in the activity oncreate i registered and defined the broadcast like this (updateUIReciver is defined as a class instance) :

 IntentFilter filter = new IntentFilter();

 filter.addAction("com.hello.action"); 
 
 updateUIReciver = new BroadcastReceiver() {

			@Override
			public void onReceive(Context context, Intent intent) {
				//UI update here

			}
		};
 registerReceiver(updateUIReciver,filter);

And from the service you send the intent like this:

Intent local = new Intent();
			
local.setAction("com.hello.action");
			
this.sendBroadcast(local);

don't forget to unregister the recover in the activity on destroy :

unregisterReceiver(updateUIReciver);

Solution 3 - Android

I would use a bound service to do that and communicate with it by implementing a listener in my activity. So if your app implements myServiceListener, you can register it as a listener in your service after you have bound with it, call listener.onUpdateUI from your bound service and update your UI in there!

Solution 4 - Android

I would recommend checking out [Otto][1], an EventBus tailored specifically to Android. Your Activity/UI can listen to events posted on the Bus from the Service, and decouple itself from the backend.

[1]: https://github.com/square/otto "Otto"

Solution 5 - Android

Clyde's solution works, but it is a broadcast, which I am pretty sure will be less efficient than calling a method directly. I could be mistaken, but I think the broadcasts are meant more for inter-application communication.

I'm assuming you already know how to bind a service with an Activity. I do something sort of like the code below to handle this kind of problem:

class MyService extends Service {
    MyFragment mMyFragment = null;
    MyFragment mMyOtherFragment = null;

    private void networkLoop() {
        ...

        //received new data for list.
        if(myFragment != null)
            myFragment.updateList();
        }

        ...

        //received new data for textView
        if(myFragment !=null)
            myFragment.updateText();

        ...

        //received new data for textView
        if(myOtherFragment !=null)
            myOtherFragment.updateSomething();

        ...
    }
}


class MyFragment extends Fragment {

    public void onResume() {
        super.onResume()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyFragment=this;
    }

    public void onPause() {
        super.onPause()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyFragment=null;
    }

    public void updateList() {
        runOnUiThread(new Runnable() {
            public void run() {
                //Update the list.
            }
        });
    }

    public void updateText() {
       //as above
    }
}

class MyOtherFragment extends Fragment {
             public void onResume() {
        super.onResume()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyOtherFragment=this;
    }

    public void onPause() {
        super.onPause()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyOtherFragment=null;
    }

    public void updateSomething() {//etc... }
}

I left out bits for thread safety, which is essential. Make sure to use locks or something like that when checking and using or changing the fragment references on the service.

Solution 6 - Android

Callback from service to activity to update UI.
ResultReceiver receiver = new ResultReceiver(new Handler()) {
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        //process results or update UI
    }
}

Intent instructionServiceIntent = new Intent(context, InstructionService.class);
instructionServiceIntent.putExtra("receiver", receiver);
context.startService(instructionServiceIntent);

Solution 7 - Android

My solution might not be the cleanest but it should work with no problems. The logic is simply to create a static variable to store your data on the Service and update your view each second on your Activity.

Let's say that you have a String on your Service that you want to send it to a TextView on your Activity. It should look like this

Your Service:

public class TestService extends Service {
    public static String myString = "";
    // Do some stuff with myString

Your Activty:

public class TestActivity extends Activity {
    TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        tv = new TextView(this);
        setContentView(tv);
        update();
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    while (!isInterrupted()) {
                        Thread.sleep(1000);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                update();
                            }
                        });
                    }
                } catch (InterruptedException ignored) {}
            }
        };
        t.start();
        startService(new Intent(this, TestService.class));
    }
    private void update() {
        // update your interface here
        tv.setText(TestService.myString);
    }
}

Solution 8 - Android

You could use android Jetpack's LiveData

As per the documentation:

> You can extend a LiveData object using the singleton pattern to wrap > system services so that they can be shared in your app. The LiveData > object connects to the system service once, and then any observer that > needs the resource can just watch the LiveData object. For more > information, see Extend > LiveData.

Below is what I did for communication between Service and Activity and also Service and Fragment.

In this example, I have:

  • a class SyncLogLiveData extending LiveData that contains a SpannableStringBuilder
  • a service SyncService
  • a fragment SyncFragment

The Fragment "observes" the LiveData (ie SyncLogLiveData) and performs an action when the LiveData changes.
The LiveData is updated by the Service.
I could also update the LiveData from the Fragment in the same way but don't show it here.

class SyncLogLiveData

public class SyncLogLiveData extends LiveData<SpannableStringBuilder> {
    private static SyncLogLiveData sInstance;
    private final static SpannableStringBuilder log = new SpannableStringBuilder("");

    @MainThread
    public static SyncLogLiveData get() {
        if (sInstance == null) {
            sInstance = new SyncLogLiveData();
        }
        return sInstance;
    }

    private SyncLogLiveData() {
    }

    public void appendLog(String text) {
        log.append(text);
        postValue(log);
    }

    public void appendLog(Spanned text) {
        log.append(text);
        postValue(log);
    }
}

in class SyncService

This line of code will update the content of the LiveData

SyncLogLiveData.get().appendLog(message);

You could also make direct use of setValue(...) or postValue(...) methods of LiveData

SyncLogLiveData.get().setValue(message);

class SyncFragment

public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {

    //...

    // Create the observer which updates the UI.
    final Observer<SpannableStringBuilder> ETAObserver = new Observer<SpannableStringBuilder>() {
        @Override
        public void onChanged(@Nullable final SpannableStringBuilder spannableLog) {
            // Update the UI, in this case, a TextView.
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    textViewLog.setText(spannableLog);
                }
            });
        }
    };
    // Observe the LiveData, passing in this activity/fragment as the LifecycleOwner and the observer.
    SyncLogLiveData.get().observe(getViewLifecycleOwner(), ETAObserver);

    //...
}

From within an activity it works the same way, but for .observe(...), you may use this instead

SyncLogLiveData.get().observe(this, ETAObserver);

You could also fetch the current value of the LiveData this way at anytime in your code.

SyncLogLiveData.get().getValue();

Hopefully this will help someone. There wasn't any mention of LiveData in this answer yet.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser200658View Question on Stackoverflow
Solution 1 - AndroidClydeView Answer on Stackoverflow
Solution 2 - AndroidEran KatsavView Answer on Stackoverflow
Solution 3 - AndroidpsykhiView Answer on Stackoverflow
Solution 4 - AndroidSeanPONeilView Answer on Stackoverflow
Solution 5 - AndroidReynardView Answer on Stackoverflow
Solution 6 - AndroidMohammed ShoebView Answer on Stackoverflow
Solution 7 - AndroidNaheelView Answer on Stackoverflow
Solution 8 - AndroidusiloView Answer on Stackoverflow