Пользовательский интерфейс обновления Android из службы

102

У меня есть служба, которая постоянно проверяет наличие новых задач. Если есть новая задача, я хочу обновить пользовательский интерфейс активности, чтобы отобразить эту информацию. Я нашел https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ этот пример. Это хороший подход? Есть еще примеры?

Спасибо.

user200658
источник
Смотрите мой ответ здесь. Легко определить интерфейс для связи между классами с помощью слушателей. stackoverflow.com/questions/14660671/…
Саймон

Ответы:

228

См. Ниже мой исходный ответ - этот шаблон хорошо сработал, но недавно я начал использовать другой подход к взаимодействию между сервисами и действиями:

  • Используйте связанную службу, которая позволяет Activity получать прямую ссылку на службу, что позволяет выполнять прямые вызовы на нее, а не с использованием намерений.
  • Используйте RxJava для выполнения асинхронных операций.

  • Если службе необходимо продолжить фоновые операции, даже если активность не запущена, также запустите службу из класса приложения, чтобы она не останавливалась при отключении.

Преимущества, которые я обнаружил в этом подходе по сравнению с методом startService () / LocalBroadcast:

  • Нет необходимости в объектах данных для реализации Parcelable - это особенно важно для меня, поскольку теперь я делюсь кодом между Android и iOS (используя RoboVM).
  • RxJava обеспечивает постоянное (и кросс-платформенное) планирование и простую композицию последовательных асинхронных операций.
  • Это должно быть более эффективным, чем использование LocalBroadcast, хотя накладные расходы на использование RxJava могут перевесить это.

Пример кода. Сначала сервис:

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);
    }
} 

И Activity, которая привязывается к сервису и получает обновления барометрической высоты:

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);
    }
}

Макет этого действия:

<?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>

Если службе необходимо работать в фоновом режиме без привязки Activity, ее также можно запустить из класса Application при OnCreate()использовании Context#startService().


Мой оригинальный ответ (от 2013 г.):

В вашем сервисе: (используя COPA в качестве сервиса в примере ниже).

Используйте LocalBroadCastManager. В onCreate вашего сервиса настройте вещатель:

broadcaster = LocalBroadcastManager.getInstance(this);

Если вы хотите уведомить пользовательский интерфейс о чем-либо:

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);
}

В вашей деятельности:

Создайте слушателя 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.
        }
    };
}

и зарегистрируйте его в 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();
}
Клайд
источник
4
@ user200658 Да, onStart () и onStop () являются частью жизненного цикла Activity - см. Жизненный цикл Activity
Клайд
8
Небольшой комментарий - вам не хватает определения COPA_MESSAGE.
Лиор
1
Спасибо :) Тем, кто спрашивает про COPA_RESULT, это не что иное, как статическая переменная, созданная пользователем. «COPA» - это его служебное название, поэтому вы можете полностью заменить его своим. В моем случае это статическая финальная публичная строка MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil
1
Пользовательский интерфейс не обновляется, если пользователь уходит от Activity, а затем возвращается к нему после завершения службы. Как лучше всего с этим справиться?
SavageKing
1
@SavageKing Вам нужно отправить любой запрос в службу, который подходит для вашего метода onStart()or onResume(). В общем, если Activity просит сервис сделать что-то, но завершает работу до получения результата, разумно предположить, что результат больше не требуется. Точно так же при запуске Activity следует предполагать, что Служба не обрабатывает невыполненных запросов.
Clyde
33

для меня самым простым решением было отправить трансляцию, в действии oncreate я зарегистрировал и определил трансляцию следующим образом (updateUIReciver определяется как экземпляр класса):

 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);

И из службы вы отправляете намерение следующим образом:

Intent local = new Intent();

local.setAction("com.hello.action");

this.sendBroadcast(local);

не забудьте отменить регистрацию восстановления в действии при уничтожении:

unregisterReceiver(updateUIReciver);
Эран Кацав
источник
1
Это лучшее решение, но было бы лучше использовать LocalBroadcastManager, если он используется в приложении, что было бы более эффективно.
Psypher
1
следующие шаги после this.sendBroadcast (local); в службе?
ангел
@angel, следующего шага нет, внутри дополнений намерения просто добавьте обновления пользовательского интерфейса, которые вы хотите, и все
Эран Кацав
12

Я бы использовал связанный сервис для этого и общался с ним, внедряя слушателя в свою деятельность. Итак, если ваше приложение реализует myServiceListener, вы можете зарегистрировать его в качестве слушателя в своей службе после того, как вы связались с ним, вызовите listener.onUpdateUI из вашей связанной службы и обновите свой пользовательский интерфейс там!

психи
источник
Это похоже на то, что я ищу. Дай мне попробовать. Спасибо.
user200658 04
Будьте осторожны, чтобы не пропустить ссылку на вашу деятельность. Потому что ваша деятельность может быть уничтожена и воссоздана при ротации.
Эрик
Привет, пожалуйста, проверьте эту ссылку. Я поделился для этого образцом кода. Поместите ссылку здесь, предполагая, что кто-то может почувствовать себя полезным в будущем. myownandroid.blogspot.in/2012/08/…
jrhamza
+1 Это жизнеспособное решение, но в моем случае мне действительно нужно, чтобы служба продолжала работать, даже если все действия отвязаны от нее (например, пользователь закрывает приложение, преждевременное завершение работы ОС), у меня нет выбора, кроме как использовать широковещательную рассылку приемники.
Neon Warge
9

Я бы порекомендовал попробовать Otto , EventBus, специально созданный для Android. Ваше действие / пользовательский интерфейс может прослушивать события, отправленные в шину из службы, и отделяться от серверной части.

ШонПОНейл
источник
5

Решение Клайда работает, но это широковещательная передача, которая, я уверен, будет менее эффективной, чем прямой вызов метода. Я могу ошибаться, но я думаю, что трансляции предназначены больше для общения между приложениями.

Я предполагаю, что вы уже знаете, как связать службу с действием. Я делаю что-то вроде кода ниже, чтобы решить эту проблему:

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... }
}

Я не упомянул биты для обеспечения безопасности потоков, что очень важно. Обязательно используйте блокировки или что-то подобное при проверке и использовании или изменении ссылок на фрагменты в службе.

Рейнард
источник
8
LocalBroadcastManager разработан для связи внутри приложения, поэтому он довольно эффективен. Подход с привязкой к службе подходит, когда у вас ограниченное количество клиентов для службы, и служба не должна работать независимо. Подход с локальной широковещательной рассылкой позволяет более эффективно отделить службу от клиентов и делает безопасность потоков не проблемой.
Clyde
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);
Мохаммед Шуб
источник
1

Мое решение может быть не самым чистым, но оно должно работать без проблем. Логика заключается в том, чтобы просто создать статическую переменную для хранения ваших данных в Serviceи обновлять ваше представление каждую секунду в вашем Activity.

Допустим, у вас есть Stringфайл, Serviceкоторый вы хотите отправить TextViewна свой Activity. Это должно выглядеть так

Ваша служба:

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

Ваша активность:

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);
    }
}
Naheel
источник
2
Никогда не делайте статические переменные ни в службе, ни в какой-либо деятельности
Муртаза Хуршид Хуссейн
@MurtazaKhursheedHussain - не могли бы вы подробнее рассказать об этом?
SolidSnake
2
Статические члены являются источником утечек памяти в действиях (вокруг много статей), плюс их обслуживание ухудшает ситуацию. Широковещательный приемник гораздо более уместен в ситуации OP, или сохранение в постоянном хранилище также является решением.
Муртаза Хуршид Хуссейн
@MurtazaKhursheedHussain - так что скажем, если у меня есть класс (а не класс обслуживания, просто какой-то класс модели, который я использую для заполнения данных) со статическим Hashmap, который повторно заполняется некоторыми данными (поступающими из API), каждую минуту считается утечка памяти?
SolidSnake
В контексте « androidда», если это список, попробуйте создать адаптер или сохранить данные с помощью sqllite / realm db.
Муртаза Хуршид Хуссейн