Android update activity UI from service
Asked Answered
C

8

121

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.

Claudetteclaudia answered 4/2, 2013 at 21:0 Comment(1)
See my answer here. Easy to define an Interface to communicate between classes using listeners. #14661171Replay
O
247

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();
}
Opportune answered 4/2, 2013 at 21:25 Comment(23)
Today I tested it and it's working partially. Where onStart() and onStop() go ? In activity ?Claudetteclaudia
@Claudetteclaudia Yes, onStart() and onStop() are part of the Activity lifecycle - see Activity LifecycleOpportune
Tiny comment - you're missing the definition of COPA_MESSAGE.Suffrage
can u please tell me what is COPA_RESULT??Domini
@SweetWisherツ It's just a string static final public String COPA_RESULT = "com.controlj.copame.backend.COPAService.REQUEST_PROCESSED"; Opportune
are there definitions in the manifest that must be written for thsi solution? like actions? intent-filters?Cantrell
@Cantrell No, apart from declaring the service. The intent filter is provided when registering the receiver.Opportune
Do I need my package name in these static strings? I still don't understand :/Ranking
@kamil no, the strings are arbitrary for local broadcasts - they are local to your app only.Opportune
Thank you :) Those who are asking about COPA_RESULT, it's nothing but a user generated static variable. "COPA" is his service name so you can replace it with yours completely. In my case, it's static final public String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";Minnaminnaminnie
One question, how to update UI when the activity is recreated?Minnaminnaminnie
in my case unfortunately just one received occurs and after that app crashes.Plenty
UI won't update if the user navigates away from the Activity and then comes back to it after the service has finished. What is the best way to handle that?Hachmann
@Hachmann You need to send whatever request to the service is appropriate in your onStart() or onResume() method. In general if the Activity asks the Service to do something, but quits before receiving the result, it's reasonable to assume the result is no longer required. Similarly on starting an Activity it should assume that there are no outstanding requests being processed by the Service.Opportune
is the reciever object public static? or how did you pass this object to the service?Kaifeng
@Kaifeng The receiver is not static, nor is it "passed to the service", it is registered to receive broadcasts from the service with the registerReceiver() call.Opportune
You could also safely bind to the service and pass it instances of the views you 'd like to update. Not the cleanest way around this it works.Bocanegra
any chance for some imports on those Activities?Doomsday
What happens if the activity is in the background? then the update to the activity won't happenGaggle
@redM depends on what you mean by "in the background". If the activity is stopped or destroyed then you won't get the broadcasts, since onStop unregisters the receiver. This is intentional since a stopped activity is not visible. If the activity is merely paused, then it can and will update the UI views.Opportune
Sure. My current issue is that the activity is not being updated when it’s in the background and the broadcast receiver is broadcasting the data passed from the background service, however, when activity in foreground, everything works wellGaggle
What do you mean by "in the background"?Opportune
The broadcast has already unregistered in the Activity because onPause has been called. I opened a question here: #53401634Gaggle
M
38

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);
Mudstone answered 15/8, 2014 at 13:23 Comment(3)
This is better solution but it would be better to use LocalBroadcastManager if its being used within the application which would be more efficient.Wundt
next steps after this.sendBroadcast(local); in the service?Kaifeng
@Kaifeng there is no next step, inside the intent's extras just add the ui updates you want and thats itMudstone
T
13

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!

Toul answered 4/2, 2013 at 21:5 Comment(3)
Be careful to not leak a reference to your activity. Because your activity might be destroyed and recreated on rotation.Territorial
Hi, please check this out link. I had shared a sample code for this. Putting the link here assuming that someone may feel helpful in future. myownandroid.blogspot.in/2012/08/…Fifteenth
+1 This is a viable solution, but on my case, I really need the service to keep running even though all of the activity unbinds to it (e.g. User close the application, Premature OS termination), I have no choice but to use broadcast receivers.Chev
C
9

I would recommend checking out Otto, 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.

Circumjacent answered 4/2, 2013 at 21:3 Comment(0)
M
5

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.

Manipulator answered 6/11, 2013 at 23:37 Comment(1)
LocalBroadcastManager is designed for communication within an application, so is quite efficient. The bound service approach is ok when you have a limited number of clients for the service and the service doesn't need to run independently. The local broadcast approach allows the service to be more effectively decoupled from its clients, and makes thread-safety a non-issue.Opportune
L
5
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);
Lookout answered 5/5, 2016 at 7:17 Comment(0)
S
1

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);
    }
}
Sourdough answered 5/7, 2016 at 23:18 Comment(6)
Never do static variable thing neither in Service nor in any activitySeedtime
@MurtazaKhursheedHussain - can you elaborate more on that?Safir
Static members are the source of memory leaks in activities ( there are a lot of articles around) plus keeping them in service makes it worse. A broadcast receiver is much more appropriate in OP's situation or keeping in persistent storage is also a solution.Seedtime
@MurtazaKhursheedHussain - so let's say if I have a class (not a service class just some model class I use for populating data) with a static Hashmap that gets re-populated with some data (coming from an API) every minute is considered as a memory leak?Safir
In the context of android yes, if its a list, try to create an adapter or persist data using sqllite/realm db.Seedtime
See mobile devices have limited memory assigned to apps. if those classes lets say have bitmaps which are not recycled bitmaps then those consume memory and if your class is a hard reference to something which has to be garbage collected it will not because you have static reference and it's not being cleared.Seedtime
A
1

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.

Atrioventricular answered 20/8, 2021 at 19:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.