IU de actividad de actualización de Android desde el servicio

102

Tengo un servicio que busca nuevas tareas todo el tiempo. Si hay una nueva tarea, quiero actualizar la interfaz de usuario de la actividad para mostrar esa información. Encontré https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ este ejemplo. ¿Es ese un buen enfoque? ¿Algún otro ejemplo?

Gracias.

usuario200658
fuente
Vea mi respuesta aquí. Fácil de definir una interfaz para comunicarse entre clases utilizando oyentes. stackoverflow.com/questions/14660671/…
Simon

Respuestas:

228

Vea a continuación mi respuesta original: ese patrón ha funcionado bien, pero recientemente comencé a usar un enfoque diferente para la comunicación de Servicio / Actividad:

  • Use un servicio vinculado que permita a la Actividad obtener una referencia directa al Servicio, permitiendo así llamadas directas en él, en lugar de usar Intents.
  • Utilice RxJava para ejecutar operaciones asincrónicas.

  • Si el Servicio necesita continuar con las operaciones en segundo plano incluso cuando no se está ejecutando ninguna Actividad, inicie también el servicio desde la clase Aplicación para que no se detenga cuando no esté vinculado.

Las ventajas que he encontrado en este enfoque en comparación con la técnica startService () / LocalBroadcast son

  • No es necesario que los objetos de datos implementen Parcelable; esto es particularmente importante para mí, ya que ahora estoy compartiendo código entre Android e iOS (usando RoboVM)
  • RxJava proporciona programación enlatada (y multiplataforma) y una composición sencilla de operaciones asincrónicas secuenciales.
  • Esto debería ser más eficiente que usar un LocalBroadcast, aunque la sobrecarga de usar RxJava puede superar eso.

Algún código de ejemplo. Primero el servicio:

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

Y una actividad que se une al servicio y recibe actualizaciones de altitud de presión:

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

El diseño de esta actividad es:

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

Si el servicio debe ejecutarse en segundo plano sin una actividad vinculada, también se puede iniciar desde la clase Application en el OnCreate()uso Context#startService().


Mi respuesta original (de 2013):

En su servicio: (usando COPA como servicio en el ejemplo siguiente).

Utilice un LocalBroadCastManager. En onCreate de su servicio, configure la emisora:

broadcaster = LocalBroadcastManager.getInstance(this);

Cuando desee notificar algo a la interfaz de usuario:

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

En tu actividad:

Crea un oyente en 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.
        }
    };
}

y registrarlo en 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();
}
Clyde
fuente
4
@ user200658 Sí, onStart () y onStop () son parte del ciclo de vida de la actividad; consulte Ciclo de vida de la actividad
Clyde
8
Pequeño comentario: te falta la definición de COPA_MESSAGE.
Lior
1
Gracias :) Aquellos que están preguntando sobre COPA_RESULT, no es más que una variable estática generada por el usuario. "COPA" es su nombre de servicio para que pueda reemplazarlo por el suyo por completo. En mi caso, es la cadena pública final estática MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil
1
La interfaz de usuario no se actualizará si el usuario se aleja de la actividad y luego vuelve a ella una vez finalizado el servicio. ¿Cuál es la mejor manera de manejar eso?
SavageKing
1
@SavageKing Debe enviar cualquier solicitud al servicio que sea apropiada en su método onStart()o onResume(). En general, si la Actividad solicita al Servicio que haga algo, pero se cierra antes de recibir el resultado, es razonable suponer que el resultado ya no es necesario. De manera similar, al iniciar una Actividad, debe asumir que el Servicio no está procesando solicitudes pendientes.
Clyde
32

para mí, la solución más simple fue enviar una transmisión, en la actividad oncreate registré y definí la transmisión de esta manera (updateUIReciver se define como una instancia de clase):

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

Y desde el servicio envías la intención de esta manera:

Intent local = new Intent();

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

this.sendBroadcast(local);

no te olvides de anular el registro de la recuperación en la actividad de destruir:

unregisterReceiver(updateUIReciver);
Eran Katsav
fuente
1
Esta es una mejor solución, pero sería mejor usar LocalBroadcastManager si se usa dentro de la aplicación, que sería más eficiente.
Psypher
1
siguientes pasos después de this.sendBroadcast (local); ¿en el servicio?
ángel
@angel no hay un siguiente paso, dentro de los extras de la intención simplemente agregue las actualizaciones de interfaz de usuario que desee y eso es todo
Eran Katsav
12

Usaría un servicio vinculado para hacer eso y me comunicaría con él implementando un oyente en mi actividad. Entonces, si su aplicación implementa myServiceListener, puede registrarla como oyente en su servicio después de haberse vinculado con ella, llamar a listener.onUpdateUI desde su servicio vinculado y actualizar su interfaz de usuario allí.

psykhi
fuente
Esto se parece exactamente a lo que estoy buscando. Déjame intentarlo. Gracias.
user200658
Tenga cuidado de no filtrar una referencia a su actividad. Porque su actividad puede ser destruida y recreada en rotación.
Eric
Hola, mira este enlace. Había compartido un código de muestra para esto. Poner el enlace aquí asumiendo que alguien puede sentirse útil en el futuro. myownandroid.blogspot.in/2012/08/…
jrhamza
+1 Esta es una solución viable, pero en mi caso, realmente necesito que el servicio siga funcionando a pesar de que toda la actividad se desvincula de él (por ejemplo, el usuario cierra la aplicación, la terminación prematura del sistema operativo), no tengo más remedio que usar la transmisión receptores.
Neon Warge
9

Recomendaría revisar Otto , un EventBus diseñado específicamente para Android. Su Actividad / UI puede escuchar los eventos publicados en el Bus desde el Servicio y desacoplarse del backend.

SeanPONeil
fuente
5

La solución de Clyde funciona, pero es una transmisión, que estoy bastante seguro de que será menos eficiente que llamar a un método directamente. Podría estar equivocado, pero creo que las transmisiones están destinadas más a la comunicación entre aplicaciones.

Supongo que ya sabe cómo vincular un servicio con una actividad. Hago algo parecido al código de abajo para manejar este tipo de problema:

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

Dejé algunos bits para la seguridad de los hilos, que es esencial. Asegúrese de usar bloqueos o algo así al verificar y usar o cambiar las referencias de fragmentos en el servicio.

Reynard
fuente
8
LocalBroadcastManager está diseñado para comunicarse dentro de una aplicación, por lo que es bastante eficiente. El enfoque de servicio vinculado está bien cuando tiene un número limitado de clientes para el servicio y el servicio no necesita ejecutarse de forma independiente. El enfoque de transmisión local permite que el servicio se desacople de manera más efectiva de sus clientes y hace que la seguridad de los subprocesos no sea un problema.
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);
Mohammed Shoeb
fuente
1

Puede que mi solución no sea la más limpia, pero debería funcionar sin problemas. La lógica es simplemente crear una variable estática para almacenar sus datos en el Servicey actualizar su vista cada segundo en su Activity.

Digamos que tiene un Stringen su Serviceque desea enviarlo a un TextViewen su Activity. Debe tener un aspecto como este

Tu servicio:

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

Tu actividad:

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
fuente
2
Nunca hagas cosas de variables estáticas ni en el Servicio ni en ninguna actividad
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain, ¿puedes dar más detalles sobre eso?
SolidSnake
2
Los miembros estáticos son la fuente de pérdidas de memoria en las actividades (hay muchos artículos alrededor) y mantenerlos en servicio empeora las cosas. Un receptor de transmisión es mucho más apropiado en la situación de OP o mantener un almacenamiento persistente también es una solución.
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain: digamos que si tengo una clase (no una clase de servicio, solo una clase de modelo que uso para completar datos) con un Hashmap estático que se vuelve a completar con algunos datos (provenientes de una API), cada minuto se considera como un ¿pérdida de memoria?
SolidSnake
En el contexto de androidsí, si es una lista, intente crear un adaptador o persistir los datos usando sqllite / realm db.
Murtaza Khursheed Hussain