UI กิจกรรมอัปเดต Android จากบริการ


102

ฉันมีบริการที่กำลังตรวจสอบงานใหม่ตลอดเวลา หากมีงานใหม่ฉันต้องการรีเฟรช UI กิจกรรมเพื่อแสดงข้อมูลนั้น ฉันพบhttps://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ตัวอย่างนี้ เป็นแนวทางที่ดีหรือไม่? ตัวอย่างอื่น ๆ ?

ขอบคุณ.


ดูคำตอบของฉันที่นี่ ง่ายต่อการกำหนดอินเทอร์เฟซเพื่อสื่อสารระหว่างชั้นเรียนโดยใช้ผู้ฟัง stackoverflow.com/questions/14660671/…
Simon

คำตอบ:


228

ดูคำตอบเดิมของฉันด้านล่าง - รูปแบบนั้นใช้ได้ดี แต่เมื่อเร็ว ๆ นี้ฉันได้เริ่มใช้วิธีการอื่นในการสื่อสารบริการ / กิจกรรม:

  • ใช้บริการที่ถูกผูกไว้ซึ่งทำให้กิจกรรมได้รับการอ้างอิงโดยตรงไปยังบริการซึ่งทำให้สามารถเรียกใช้บริการได้โดยตรงแทนที่จะใช้ Intents
  • ใช้ 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);
    }
} 

และกิจกรรมที่เชื่อมโยงกับบริการและรับการอัปเดตระดับความดัน:

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>

หากความต้องการบริการที่จะทำงานในพื้นหลังโดยไม่ต้องมีกิจกรรมที่ถูกผูกไว้ก็สามารถเริ่มต้นจากระดับแอพลิเคชันได้เป็นอย่างดีในการใช้OnCreate()Context#startService()


คำตอบเดิมของฉัน (จากปี 2013):

ในบริการของคุณ: (ใช้ COPA เป็นบริการตามตัวอย่างด้านล่าง)

ใช้ LocalBroadCastManager ใน onCreate ของบริการของคุณให้ตั้งค่าผู้ออกอากาศ:

broadcaster = LocalBroadcastManager.getInstance(this);

เมื่อคุณต้องการแจ้ง UI ของบางสิ่ง:

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

ในกิจกรรมของคุณ:

สร้าง Listener บน 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 () เป็นส่วนหนึ่งของวงจรชีวิตของกิจกรรม - ดูวงจรชีวิตของกิจกรรม
ไคลด์

8
ความคิดเห็นเล็ก ๆ - คุณไม่มีคำจำกัดความของ COPA_MESSAGE
Lior

1
ขอบคุณ :) ผู้ที่ถามเกี่ยวกับ COPA_RESULT ไม่มีอะไรนอกจากผู้ใช้สร้างตัวแปรคงที่ "COPA" คือชื่อบริการของเขาเพื่อให้คุณสามารถแทนที่ด้วยชื่อของคุณได้อย่างสมบูรณ์ ในกรณีของฉันมันเป็นสตริงสาธารณะสุดท้ายแบบคงที่ MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil

1
UI จะไม่อัปเดตหากผู้ใช้ออกจากกิจกรรมและกลับมาที่กิจกรรมนั้นหลังจากบริการเสร็จสิ้น วิธีที่ดีที่สุดในการจัดการคืออะไร?
SavageKing

1
@SavageKing คุณต้องส่งคำขอใด ๆ ไปยังบริการที่เหมาะสมในวิธีการของคุณonStart()หรือ onResume()โดยทั่วไปหากกิจกรรมขอให้บริการทำบางสิ่ง แต่หยุดก่อนที่จะได้รับผลลัพธ์ก็สมเหตุสมผลที่จะถือว่าผลลัพธ์นั้นไม่จำเป็นอีกต่อไป ในทำนองเดียวกันในการเริ่มต้นกิจกรรมควรถือว่าไม่มีคำขอใด ๆ ที่ค้างอยู่ในการดำเนินการโดยบริการ
Clyde

32

สำหรับฉันวิธีแก้ปัญหาที่ง่ายที่สุดคือการส่งการออกอากาศในกิจกรรมที่สร้างฉันลงทะเบียนและกำหนดการออกอากาศเช่นนี้ (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);

อย่าลืมยกเลิกการลงทะเบียนการกู้คืนในกิจกรรมบน destroy:

unregisterReceiver(updateUIReciver);

1
นี่เป็นทางออกที่ดีกว่า แต่จะดีกว่าถ้าใช้ LocalBroadcastManager หากใช้ภายในแอปพลิเคชันซึ่งจะมีประสิทธิภาพมากกว่า
Psypher

1
ขั้นตอนต่อไปหลังจากนี้sendBroadcast (ในเครื่อง); ในการบริการ?
นางฟ้า

@angel ไม่มีขั้นตอนต่อไปภายในความตั้งใจพิเศษเพียงแค่เพิ่มการอัปเดต UI ที่คุณต้องการและนั่นก็คือ
Eran Katsav

12

ฉันจะใช้บริการที่ผูกมัดเพื่อทำสิ่งนั้นและสื่อสารกับมันโดยใช้ผู้ฟังในกิจกรรมของฉัน ดังนั้นหากแอปของคุณใช้ myServiceListener คุณสามารถลงทะเบียนเป็นผู้ฟังในบริการของคุณได้หลังจากที่คุณเชื่อมโยงกับแอปแล้วโทรหา Listener.onUpdateUI จากบริการที่ผูกไว้และอัปเดต UI ของคุณที่นั่น!


ดูเหมือนว่าสิ่งที่ฉันกำลังมองหา ให้ฉันลองดู ขอบคุณ.
user200658

ระวังอย่าให้การอ้างอิงถึงกิจกรรมของคุณรั่วไหล เนื่องจากกิจกรรมของคุณอาจถูกทำลายและสร้างขึ้นใหม่เมื่อหมุนเวียน
Eric

สวัสดีโปรดตรวจสอบลิงก์นี้ ฉันได้แชร์โค้ดตัวอย่างสำหรับสิ่งนี้ วางลิงก์ไว้ที่นี่โดยสมมติว่าอาจมีคนรู้สึกว่ามีประโยชน์ในอนาคต myownandroid.blogspot.in/2012/08/…
jrhamza

+1 นี่เป็นวิธีแก้ปัญหาที่ใช้งานได้ แต่ในกรณีของฉันฉันต้องการบริการเพื่อให้ทำงานต่อไปแม้ว่ากิจกรรมทั้งหมดจะไม่ถูกผูกมัดก็ตาม (เช่นผู้ใช้ปิดแอปพลิเคชันการยุติระบบปฏิบัติการก่อนกำหนด) ฉันไม่มีทางเลือกอื่นนอกจากใช้การออกอากาศ ผู้รับ
Neon Warge

9

ฉันอยากจะแนะนำให้ลองใช้Ottoซึ่งเป็น EventBus ที่ออกแบบมาเพื่อ Android โดยเฉพาะ กิจกรรม / UI ของคุณสามารถฟังเหตุการณ์ที่โพสต์บนรถบัสจากบริการและแยกตัวเองออกจากแบ็กเอนด์


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ได้รับการออกแบบมาเพื่อการสื่อสารภายในแอปพลิเคชันดังนั้นจึงค่อนข้างมีประสิทธิภาพ แนวทางการบริการที่ถูกผูกไว้นั้นใช้ได้เมื่อคุณมีลูกค้าสำหรับบริการจำนวน จำกัด และบริการไม่จำเป็นต้องดำเนินการโดยอิสระ วิธีการออกอากาศในพื้นที่ช่วยให้บริการสามารถแยกออกจากไคลเอนต์ได้อย่างมีประสิทธิภาพมากขึ้นและทำให้ความปลอดภัยของเธรดไม่ใช่ปัญหา
ไคลด์

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
อย่าทำสิ่งที่แปรผันคงที่ไม่ว่าจะในบริการหรือในกิจกรรมใด ๆ
Murtaza Khursheed Hussain

@MurtazaKhursheedHussain - คุณสามารถอธิบายเพิ่มเติมเกี่ยวกับเรื่องนี้ได้หรือไม่?
SolidSnake

2
สมาชิกแบบคงที่เป็นแหล่งที่มาของการรั่วไหลของหน่วยความจำในกิจกรรม (มีบทความมากมายอยู่รอบ ๆ ) และการให้บริการเหล่านี้ทำให้แย่ลง เครื่องรับกระจายสัญญาณมีความเหมาะสมมากกว่าในสถานการณ์ของ OP หรือการเก็บไว้ในที่จัดเก็บถาวรก็เป็นวิธีแก้ปัญหาเช่นกัน
Murtaza Khursheed Hussain

@MurtazaKhursheedHussain - สมมติว่าฉันมีคลาส (ไม่ใช่คลาสบริการเป็นเพียงคลาสโมเดลบางคลาสที่ฉันใช้สำหรับการเติมข้อมูล) พร้อมกับ Hashmap แบบคงที่ซึ่งได้รับการเติมข้อมูลใหม่ (มาจาก API) ทุกนาทีจะถือเป็น หน่วยความจำรั่ว?
SolidSnake

ในบริบทของandroidใช่ถ้าเป็นรายการให้ลองสร้างอะแด็ปเตอร์หรือคงอยู่ข้อมูลโดยใช้ sqllite / realm db
Murtaza Khursheed Hussain
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.