IU de atividade de atualização do Android a partir do serviço

102

Tenho um serviço que verifica constantemente se há novas tarefas. Se houver uma nova tarefa, quero atualizar a IU de atividade para mostrar essa informação. Encontrei https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ este exemplo. Essa é uma boa abordagem? Algum outro exemplo?

Obrigado.

user200658
fonte
Veja minha resposta aqui. Fácil de definir uma interface para comunicação entre classes usando ouvintes. stackoverflow.com/questions/14660671/…
Simon

Respostas:

228

Veja abaixo minha resposta original - esse padrão funcionou bem, mas recentemente comecei a usar uma abordagem diferente para comunicação de serviço / atividade:

  • Use um serviço vinculado que permite que a Activity obtenha uma referência direta ao Service, permitindo assim chamadas diretas para ele, em vez de usar Intents.
  • Use RxJava para executar operações assíncronas.

  • Se o serviço precisar continuar as operações em segundo plano mesmo quando nenhuma atividade estiver em execução, inicie também o serviço da classe de aplicativo para que ele não seja interrompido quando não ligado.

As vantagens que encontrei nesta abordagem em comparação com a técnica startService () / LocalBroadcast são

  • Não há necessidade de objetos de dados para implementar Parcelable - isso é particularmente importante para mim, pois agora estou compartilhando código entre Android e iOS (usando RoboVM)
  • RxJava fornece agendamento pronto (e multiplataforma) e fácil composição de operações assíncronas sequenciais.
  • Isso deve ser mais eficiente do que usar um LocalBroadcast, embora a sobrecarga de usar RxJava possa superar isso.

Algum código de exemplo. Primeiro o serviço:

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

E uma atividade que se liga ao serviço e recebe atualizações de altitude de pressão:

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

O layout para esta atividade é:

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

Se as necessidades de serviço para ser executado em segundo plano, sem uma atividade ligada pode ser iniciado a partir da classe de aplicações, bem como no OnCreate()uso Context#startService().


Minha resposta original (de 2013):

No seu serviço: (usando COPA como serviço no exemplo abaixo).

Use um LocalBroadCastManager. No onCreate do seu serviço, configure a emissora:

broadcaster = LocalBroadcastManager.getInstance(this);

Quando você deseja notificar a IU sobre algo:

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

Em sua atividade:

Crie um listener em 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.
        }
    };
}

e registre-o no 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
fonte
4
@ user200658 Sim, onStart () e onStop () fazem parte do ciclo de vida da atividade - consulte Ciclo de vida da atividade
Clyde
8
Pequeno comentário - está faltando a definição de COPA_MESSAGE.
Lior,
1
Obrigado :) Aqueles que estão perguntando sobre COPA_RESULT, nada mais é que uma variável estática gerada pelo usuário. "COPA" é o nome do serviço para que você possa substituí-lo pelo seu completamente. No meu caso, é público final estático String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil
1
A IU não será atualizada se o usuário sair da Activity e voltar a ela após a conclusão do serviço. Qual é a melhor maneira de lidar com isso?
SavageKing
1
@SavageKing Você precisa enviar qualquer solicitação ao serviço que seja apropriada em seu método onStart()ou onResume(). Em geral, se a Activity pede ao serviço para fazer algo, mas sai antes de receber o resultado, é razoável supor que o resultado não é mais necessário. Da mesma forma, ao iniciar uma Atividade, ele deve assumir que não há solicitações pendentes sendo processadas pelo Serviço.
Clyde
32

para mim a solução mais simples era enviar um broadcast, na atividade oncreate eu registrei e defini o broadcast assim (updateUIReciver é definido como uma instância de classe):

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

E a partir do serviço, você envia a intenção desta forma:

Intent local = new Intent();

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

this.sendBroadcast(local);

não se esqueça de cancelar o registro da recuperação na atividade de destruição:

unregisterReceiver(updateUIReciver);
Eran Katsav
fonte
1
Esta é a melhor solução, mas seria melhor usar LocalBroadcastManager se estiver sendo usado dentro do aplicativo, o que seria mais eficiente.
Psypher
1
próximas etapas após this.sendBroadcast (local); no serviço?
anjo de
@angel não há próxima etapa, dentro dos extras da intenção, basta adicionar as atualizações de interface do usuário que você deseja e pronto
Eran Katsav
12

Eu usaria um serviço vinculado para fazer isso e me comunicar com ele implementando um ouvinte em minha atividade. Portanto, se seu aplicativo implementa myServiceListener, você pode registrá-lo como um ouvinte em seu serviço depois de vinculá-lo, chame listener.onUpdateUI de seu serviço vinculado e atualize sua IU lá!

psykhi
fonte
Parece exatamente o que estou procurando. Deixe-me tentar isso. Obrigado.
user200658
Tenha cuidado para não vazar uma referência à sua atividade. Porque sua atividade pode ser destruída e recriada na rotação.
Eric
Olá, por favor, verifique este link. Eu havia compartilhado um código de amostra para isso. Colocar o link aqui presumindo que alguém pode se sentir útil no futuro. myownandroid.blogspot.in/2012/08/…
jrhamza
+1 Esta é uma solução viável, mas no meu caso, eu realmente preciso que o serviço continue em execução, mesmo que todas as atividades sejam desvinculadas dele (por exemplo, o usuário fecha o aplicativo, o encerramento prematuro do SO), não tenho escolha a não ser usar a transmissão receptores.
Neon Warge
9

Eu recomendaria dar uma olhada no Otto , um EventBus feito especificamente para Android. Sua atividade / IU pode ouvir eventos postados no ônibus do serviço e se desacoplar do back-end.

SeanPONeil
fonte
5

A solução de Clyde funciona, mas é uma transmissão, que tenho certeza de que será menos eficiente do que chamar um método diretamente. Posso estar enganado, mas acho que as transmissões se destinam mais à comunicação entre aplicativos.

Estou assumindo que você já sabe como vincular um serviço a uma atividade. Eu faço algo parecido com o código abaixo para lidar com esse 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... }
}

Eu deixei de fora algumas partes para a segurança do thread, que é essencial. Certifique-se de usar bloqueios ou algo parecido ao verificar e usar ou alterar as referências de fragmento no serviço.

Reynard
fonte
8
LocalBroadcastManager é projetado para comunicação dentro de um aplicativo, portanto, é bastante eficiente. A abordagem de serviço vinculado está ok quando você tem um número limitado de clientes para o serviço e o serviço não precisa ser executado de forma independente. A abordagem de transmissão local permite que o serviço seja desacoplado de forma mais eficaz de seus clientes e torna a segurança de thread um 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
fonte
1

Minha solução pode não ser a mais limpa, mas deve funcionar sem problemas. A lógica é simplesmente criar uma variável estática para armazenar seus dados no Servicee atualizar sua visualização a cada segundo no seu Activity.

Digamos que você tenha um Stringem seu e Servicedeseja enviá-lo para um TextViewem seu Activity. Deve ser assim

Seu serviço:

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

Sua atividade:

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
fonte
2
Nunca faça coisas variáveis ​​estáticas nem em serviço nem em qualquer atividade
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - você pode elaborar mais sobre isso?
SolidSnake de
2
Membros estáticos são a fonte de vazamentos de memória nas atividades (há muitos artigos por aí), além de mantê-los em serviço torna tudo pior. Um receptor de broadcast é muito mais apropriado na situação do OP ou manter um armazenamento persistente também é uma solução.
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - digamos que se eu tiver uma classe (não uma classe de serviço, apenas uma classe de modelo que uso para preencher dados) com um Hashmap estático que é populado novamente com alguns dados (provenientes de uma API) a cada minuto é considerado um vazamento de memória?
SolidSnake de
No contexto de androidsim, se for uma lista, tente criar um adaptador ou persistir os dados usando sqllite / realm db.
Murtaza Khursheed Hussain