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


102

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

Спасибо.


Смотрите мой ответ здесь. Легко определить интерфейс для связи между классами с помощью слушателей. 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);
    }
}

2
Никогда не делайте статические переменные ни в службе, ни в какой-либо деятельности
— Муртаза Хуршид Хуссейн

@MurtazaKhursheedHussain - не могли бы вы подробнее рассказать об этом?
— SolidSnake

2
Статические члены являются источником утечек памяти в действиях (вокруг много статей), плюс их обслуживание ухудшает ситуацию. Широковещательный приемник гораздо более уместен в ситуации OP, или сохранение в постоянном хранилище также является решением.
— Муртаза Хуршид Хуссейн

@MurtazaKhursheedHussain - так что скажем, если у меня есть класс (а не класс обслуживания, просто какой-то класс модели, который я использую для заполнения данных) со статическим Hashmap, который повторно заполняется некоторыми данными (поступающими из API), каждую минуту считается утечка памяти?
— SolidSnake

В контексте « androidда», если это список, попробуйте создать адаптер или сохранить данные с помощью sqllite / realm db.
— Муртаза Хуршид Хуссейн
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.