UI aktivitas pembaruan Android dari layanan

102

Saya memiliki layanan yang selalu memeriksa tugas baru. Jika ada tugas baru, saya ingin menyegarkan UI aktivitas untuk menampilkan info itu. Saya menemukan https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ contoh ini. Apakah itu pendekatan yang bagus? Ada contoh lain?

Terima kasih.

pengguna200658
sumber
Lihat jawaban saya di sini. Mudah untuk mendefinisikan Antarmuka untuk berkomunikasi antar kelas menggunakan pendengar. stackoverflow.com/questions/14660671/…
Simon

Jawaban:

228

Lihat di bawah untuk jawaban asli saya - pola itu telah bekerja dengan baik, tetapi baru-baru ini saya mulai menggunakan pendekatan yang berbeda untuk komunikasi Layanan / Aktivitas:

  • Gunakan layanan terikat yang memungkinkan Aktivitas mendapatkan referensi langsung ke Layanan, sehingga memungkinkan panggilan langsung padanya, daripada menggunakan Intent.
  • Gunakan RxJava untuk menjalankan operasi asinkron.

  • Jika Layanan perlu melanjutkan operasi latar belakang meskipun tidak ada Aktivitas yang berjalan, mulai juga layanan dari kelas Aplikasi sehingga tidak dihentikan saat tidak terikat.

Keuntungan yang saya temukan dalam pendekatan ini dibandingkan dengan teknik startService () / LocalBroadcast adalah

  • Tidak perlu objek data untuk mengimplementasikan Parcelable - ini sangat penting bagi saya karena saya sekarang berbagi kode antara Android dan iOS (menggunakan RoboVM)
  • RxJava menyediakan penjadwalan terekam (dan lintas platform), dan komposisi mudah dari operasi asinkron berurutan.
  • Ini seharusnya lebih efisien daripada menggunakan LocalBroadcast, meskipun biaya penggunaan RxJava mungkin lebih besar daripada itu.

Beberapa kode contoh. Pertama layanannya:

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

Dan Aktivitas yang mengikat ke layanan dan menerima pembaruan ketinggian tekanan:

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

Tata letak untuk Aktivitas ini adalah:

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

Jika layanan perlu dijalankan di latar belakang tanpa Aktivitas terikat, layanan dapat dimulai dari kelas Aplikasi juga dalam OnCreate()penggunaan Context#startService().


Jawaban Asli Saya (dari 2013):

Dalam layanan Anda: (menggunakan COPA sebagai layanan pada contoh di bawah).

Gunakan LocalBroadCastManager. Di layanan onCreate Anda, siapkan penyiar:

broadcaster = LocalBroadcastManager.getInstance(this);

Saat Anda ingin memberi tahu UI tentang sesuatu:

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

Dalam Aktivitas Anda:

Buat pendengar di 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.
        }
    };
}

dan daftarkan di 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
sumber
4
@ user200658 Ya, onStart () dan onStop () adalah bagian dari siklus hidup Aktivitas - lihat Daur Hidup Aktivitas
Clyde
8
Komentar kecil - Anda kehilangan definisi COPA_MESSAGE.
Lior
1
Terima kasih :) Mereka yang bertanya tentang COPA_RESULT, tidak lain adalah variabel statis yang dihasilkan pengguna. "COPA" adalah nama layanannya sehingga Anda dapat menggantinya dengan milik Anda sepenuhnya. Dalam kasus saya, itu adalah public final statis String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil
1
UI tidak akan diperbarui jika pengguna keluar dari Aktivitas lalu kembali lagi setelah layanan selesai. Apa cara terbaik untuk mengatasinya?
SavageKing
1
@Savageing Anda perlu mengirim permintaan apa pun ke layanan yang sesuai dengan metode onStart()atau Anda onResume(). Secara umum, jika Aktivitas meminta Layanan untuk melakukan sesuatu, tetapi berhenti sebelum menerima hasilnya, masuk akal untuk menganggap hasilnya tidak lagi diperlukan. Demikian pula saat memulai Aktivitas, harus diasumsikan bahwa tidak ada permintaan yang belum diproses oleh Layanan.
Clyde
32

bagi saya solusi paling sederhana adalah mengirim siaran, dalam aktivitas oncreate saya mendaftar dan mendefinisikan siaran seperti ini (updateUIReciver didefinisikan sebagai instance kelas):

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

Dan dari layanan Anda mengirimkan maksud seperti ini:

Intent local = new Intent();

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

this.sendBroadcast(local);

jangan lupa untuk membatalkan registrasi pemulihan dalam aktivitas di penghancuran:

unregisterReceiver(updateUIReciver);
Eran Katsav
sumber
1
Ini adalah solusi yang lebih baik tetapi akan lebih baik menggunakan LocalBroadcastManager jika digunakan dalam aplikasi yang akan lebih efisien.
Psypher
1
langkah selanjutnya setelah this.sendBroadcast (local); dalam pelayanan?
malaikat
@angel tidak ada langkah berikutnya, di dalam tambahan maksud tersebut cukup tambahkan pembaruan UI yang Anda inginkan dan hanya itu
Eran Katsav
12

Saya akan menggunakan layanan terikat untuk melakukan itu dan berkomunikasi dengannya dengan menerapkan pendengar dalam aktivitas saya. Jadi, jika aplikasi Anda mengimplementasikan myServiceListener, Anda bisa mendaftarkannya sebagai listener di layanan Anda setelah Anda terikat dengannya, panggil listener.onUpdateUI dari layanan terikat Anda dan perbarui UI Anda di sana!

psykhi
sumber
Ini persis seperti yang saya cari. Biarkan saya mencobanya. Terima kasih.
pengguna200658
Berhati-hatilah untuk tidak membocorkan referensi ke aktivitas Anda. Karena aktivitas Anda mungkin dihancurkan dan dibuat ulang secara bergilir.
Eric
Hai, silakan lihat tautan ini. Saya telah membagikan kode sampel untuk ini. Menempatkan tautan di sini dengan asumsi bahwa seseorang mungkin merasa berguna di masa depan. myownandroid.blogspot.in/2012/08/…
jrhamza
+1 Ini adalah solusi yang layak, tetapi dalam kasus saya, saya sangat membutuhkan layanan untuk tetap berjalan meskipun semua aktivitas tidak mengikatnya (mis. Pengguna menutup aplikasi, penghentian OS Prematur), saya tidak punya pilihan selain menggunakan siaran penerima.
Neon Warge
9

Saya akan merekomendasikan untuk memeriksa Otto , EventBus yang dirancang khusus untuk Android. Aktivitas / UI Anda dapat memantau peristiwa yang diposting di Bus dari Layanan, dan memisahkan dirinya dari backend.

SeanPONeil
sumber
5

Solusi Clyde berfungsi, tetapi ini adalah siaran, yang saya cukup yakin akan kurang efisien daripada memanggil metode secara langsung. Saya bisa saja salah, tapi menurut saya siarannya lebih ditujukan untuk komunikasi antar aplikasi.

Saya berasumsi Anda sudah tahu cara mengikat layanan dengan Aktivitas. Saya melakukan sesuatu seperti kode di bawah ini untuk menangani masalah semacam ini:

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

Saya meninggalkan sedikit untuk keamanan benang, yang penting. Pastikan untuk menggunakan kunci atau semacamnya saat memeriksa dan menggunakan atau mengubah referensi fragmen pada layanan.

Reynard
sumber
8
LocalBroadcastManager dirancang untuk komunikasi dalam aplikasi, jadi cukup efisien. Pendekatan layanan terikat tidak masalah jika Anda memiliki jumlah klien terbatas untuk layanan dan layanan tidak perlu dijalankan secara independen. Pendekatan siaran lokal memungkinkan layanan dipisahkan secara lebih efektif dari kliennya, dan menjadikan keamanan thread bukan masalah.
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
sumber
1

Solusi saya mungkin bukan yang terbersih tetapi seharusnya bekerja tanpa masalah. Logikanya sederhana untuk membuat variabel statis untuk menyimpan data Anda di Servicedan memperbarui tampilan Anda setiap detik di Activity.

Katakanlah Anda memiliki Stringpada Anda Serviceyang ingin Anda kirimkan ke TextViewpada Anda Activity. Seharusnya terlihat seperti ini

Layanan Anda:

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

Aktivitas Anda:

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
sumber
2
Jangan pernah melakukan hal variabel statis baik dalam Layanan maupun dalam aktivitas apa pun
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - bisakah Anda menjelaskan lebih lanjut tentang itu?
SolidSnake
2
Anggota statis adalah sumber kebocoran memori dalam aktivitas (ada banyak artikel di sekitar) ditambah mempertahankan mereka dalam layanan membuatnya lebih buruk. Penerima siaran jauh lebih tepat dalam situasi OP atau menyimpan dalam penyimpanan persisten juga merupakan solusi.
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - jadi katakanlah jika saya memiliki kelas (bukan kelas layanan hanya beberapa kelas model yang saya gunakan untuk mengisi data) dengan Hashmap statis yang diisi ulang dengan beberapa data (berasal dari API) setiap menit dianggap sebagai kebocoran memori?
SolidSnake
Dalam konteks androidya, jika itu adalah daftar, coba buat adaptor atau pertahankan data menggunakan sqllite / realm db.
Murtaza Khursheed Hussain