Interfejs użytkownika aktualizacji Androida z usługi

102

Mam usługę, która cały czas sprawdza nowe zadanie. Jeśli pojawi się nowe zadanie, chcę odświeżyć interfejs użytkownika, aby wyświetlić te informacje. Znalazłem https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ ten przykład. Czy to dobre podejście? Jakieś inne przykłady?

Dzięki.

user200658
źródło
Zobacz moją odpowiedź tutaj. Łatwy do zdefiniowania interfejs do komunikacji między klasami przy użyciu detektorów. stackoverflow.com/questions/14660671/…
Simon,

Odpowiedzi:

228

Zobacz poniżej moją pierwotną odpowiedź - ten wzorzec zadziałał dobrze, ale ostatnio zacząłem stosować inne podejście do komunikacji Usługi / Działania:

  • Użyj powiązanej usługi, która umożliwia Działalności uzyskanie bezpośredniego odniesienia do Usługi, umożliwiając w ten sposób bezpośrednie wywoływanie jej zamiast używania intencji.
  • Użyj RxJava do wykonywania operacji asynchronicznych.

  • Jeśli usługa musi kontynuować operacje w tle, nawet jeśli nie jest uruchomiona żadna aktywność, uruchom ją również z klasy Application, aby nie została zatrzymana po odłączeniu.

Zalety, jakie znalazłem w tym podejściu w porównaniu z techniką startService () / LocalBroadcast, to

  • Nie potrzeba obiektów danych do implementacji Parcelable - jest to dla mnie szczególnie ważne, ponieważ teraz udostępniam kod między Androidem i iOS (używając RoboVM)
  • RxJava zapewnia puszkowe (i wieloplatformowe) planowanie i łatwe komponowanie sekwencyjnych operacji asynchronicznych.
  • Powinno to być bardziej wydajne niż użycie LocalBroadcast, chociaż obciążenie związane z używaniem RxJava może to przeważyć.

Przykładowy kod. Najpierw usługa:

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

Oraz działanie, które wiąże się z usługą i otrzymuje aktualizacje wysokości ciśnieniowej:

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

Układ tego działania to:

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

Jeśli usługa musi działać w tle bez powiązanego działania, można ją uruchomić z klasy Application, a także OnCreate()za pomocą Context#startService().


Moja oryginalna odpowiedź (od 2013):

W twojej usłudze: (używając COPA jako usługi w przykładzie poniżej).

Użyj LocalBroadCastManager. W usłudze onCreate skonfiguruj nadawcę:

broadcaster = LocalBroadcastManager.getInstance(this);

Jeśli chcesz powiadomić interfejs użytkownika o czymś:

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

W Twojej aktywności:

Utwórz odbiornik w 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.
        }
    };
}

i zarejestruj go w 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
źródło
4
@ user200658 Tak, onStart () i onStop () są częścią cyklu życia działania - patrz Cykl życia działania
Clyde,
8
Mały komentarz - brakuje Ci definicji COPA_MESSAGE.
Lior,
1
Dziękuję :) Ci, którzy pytają o COPA_RESULT, to nic innego jak zmienna statyczna wygenerowana przez użytkownika. „COPA” to nazwa jego usługi, więc możesz ją całkowicie zastąpić swoją. W moim przypadku jest to statyczny ostateczny publiczny String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil
1
Interfejs użytkownika nie zostanie zaktualizowany, jeśli użytkownik opuści działanie, a następnie wróci do niego po zakończeniu usługi. Jak najlepiej sobie z tym poradzić?
SavageKing
1
@SavageKing Musisz wysłać dowolną prośbę do usługi, która jest odpowiednia dla Ciebie onStart()lub onResume()metody. Ogólnie rzecz biorąc, jeśli działanie prosi Usługę o zrobienie czegoś, ale kończy pracę przed otrzymaniem wyniku, rozsądne jest założenie, że wynik nie jest już wymagany. Podobnie przy rozpoczynaniu działania należy przyjąć, że usługa nie obsługuje żadnych zaległych żądań.
Clyde,
33

dla mnie najprostszym rozwiązaniem było wysłanie transmisji, w działaniu oncreate zarejestrowałem i zdefiniowałem transmisję w ten sposób (updateUIReciver definiuje się jako instancję klasową):

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

A z usługi wysyłasz taką intencję:

Intent local = new Intent();

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

this.sendBroadcast(local);

nie zapomnij wyrejestrować odzysku z działania przy niszczeniu:

unregisterReceiver(updateUIReciver);
Eran Katsav
źródło
1
Jest to lepsze rozwiązanie, ale lepiej byłoby użyć LocalBroadcastManager, jeśli jest używany w aplikacji, która byłaby bardziej wydajna.
Psypher,
1
następne kroki po this.sendBroadcast (local); w serwisie?
anioł
@angel, nie ma następnego kroku, w dodatkach intencji po prostu dodaj żądane aktualizacje interfejsu użytkownika i to wszystko
Eran Katsav
12

Chciałbym użyć związanego usługi to zrobić i komunikować się z nim poprzez wprowadzenie słuchacza w mojej działalności. Jeśli więc Twoja aplikacja implementuje myServiceListener, możesz zarejestrować ją jako nasłuchiwanie w swojej usłudze po powiązaniu z nią, zadzwoń do listener.onUpdateUI z powiązanej usługi i zaktualizuj interfejs użytkownika!

psykhi
źródło
To wygląda dokładnie na to, czego szukam. Daj mi spróbować. Dzięki.
user200658
Uważaj, aby nie ujawnić odniesienia do swojej działalności. Ponieważ twoja aktywność może zostać zniszczona i odtworzona podczas rotacji.
Eric
Cześć, sprawdź ten link. Udostępniłem przykładowy kod. Umieszczenie linku w tym miejscu przy założeniu, że ktoś może w przyszłości poczuć się pomocny. myownandroid.blogspot.in/2012/08/…
jrhamza
+1 Jest to wykonalne rozwiązanie, ale w moim przypadku naprawdę potrzebuję usługi, aby działała, mimo że cała aktywność się z nią rozpina (np. Zamknięcie aplikacji przez użytkownika, przedwczesne zakończenie systemu operacyjnego), nie mam innego wyboru, jak użyć transmisji odbiorniki.
Neon Warge,
9

Polecam wypróbowanie Otto , EventBusa dostosowanego specjalnie do Androida. Twoja aktywność / interfejs użytkownika może słuchać wydarzeń publikowanych w magistrali z usługi i odłączać się od zaplecza.

SeanPONeil
źródło
5

Rozwiązanie Clyde'a działa, ale jest to transmisja, która, jestem prawie pewien, będzie mniej wydajna niż bezpośrednie wywołanie metody. Mógłbym się mylić, ale myślę, że transmisje są przeznaczone bardziej do komunikacji między aplikacjami.

Zakładam, że wiesz już, jak powiązać usługę z działaniem. Robię coś w rodzaju poniższego kodu, aby poradzić sobie z tego rodzaju problemem:

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

Pominąłem końcówki do zabezpieczania nici, co jest niezbędne. Upewnij się, że używasz blokad lub czegoś podobnego podczas sprawdzania i używania lub zmieniania odniesień do fragmentów w usłudze.

Reynard
źródło
8
LocalBroadcastManager jest przeznaczony do komunikacji w ramach aplikacji, więc jest dość wydajny. Podejście związane z usługą jest prawidłowe, gdy masz ograniczoną liczbę klientów usługi, a usługa nie musi działać niezależnie. Podejście do transmisji lokalnej pozwala na skuteczniejsze oddzielenie usługi od klientów i sprawia, że ​​bezpieczeństwo wątków nie stanowi problemu.
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
źródło
1

Moje rozwiązanie może nie jest najczystsze, ale powinno działać bez problemów. Logika polega po prostu na utworzeniu zmiennej statycznej do przechowywania danych Servicei aktualizowaniu widoku co sekundę w pliku Activity.

Powiedzmy, że masz Stringna swojej Servicekarcie, którą chcesz wysłać do a TextViewna swojej Activity. To powinno wyglądać tak

Twoja usługa:

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

Twoja Aktywność:

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
źródło
2
Nigdy nie rób nic ani zmienną statyczną w służbie, ani w żadnej działalności
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - czy możesz rozwinąć więcej na ten temat?
SolidSnake
2
Statyczne elementy członkowskie są źródłem wycieków pamięci w działaniach (wokół jest wiele artykułów), a utrzymywanie ich w stanie pogarsza sytuację. Odbiornik rozgłoszeniowy jest dużo bardziej odpowiedni w sytuacji OP lub przechowywanie w pamięci trwałej jest również rozwiązaniem.
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - powiedzmy więc, że jeśli mam klasę (nie klasę usługową, tylko pewną klasę modelu, której używam do wypełniania danych) ze statyczną mapą haszy, która jest ponownie wypełniana niektórymi danymi (pochodzącymi z interfejsu API) co minutę, jest uważana za wyciek pamięci?
SolidSnake
W kontekście androidtak, jeśli jest to lista, spróbuj utworzyć adapter lub utrwalić dane za pomocą sqllite / realm db.
Murtaza Khursheed Hussain