Giao diện người dùng hoạt động cập nhật Android từ dịch vụ


102

Tôi có một dịch vụ luôn kiểm tra nhiệm vụ mới. Nếu có nhiệm vụ mới, tôi muốn làm mới giao diện người dùng hoạt động để hiển thị thông tin đó. Tôi đã tìm thấy https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ ví dụ này. Đó có phải là một cách tiếp cận tốt? Bất kỳ ví dụ khác?

Cảm ơn.


Xem câu trả lời của tôi tại đây. Dễ dàng xác định một Giao diện để giao tiếp giữa các lớp bằng cách sử dụng trình nghe. stackoverflow.com/questions/14660671/…
Simon

Câu trả lời:


228

Xem bên dưới để biết câu trả lời ban đầu của tôi - mẫu đó đã hoạt động tốt, nhưng gần đây tôi đã bắt đầu sử dụng một cách tiếp cận khác để giao tiếp Dịch vụ / Hoạt động:

  • Sử dụng một dịch vụ ràng buộc cho phép Hoạt động tham chiếu trực tiếp đến Dịch vụ, do đó cho phép các cuộc gọi trực tiếp trên đó, thay vì sử dụng Ý định.
  • Sử dụng RxJava để thực thi các hoạt động không đồng bộ.

  • Nếu Dịch vụ cần tiếp tục các hoạt động nền ngay cả khi không có Hoạt động nào đang chạy, hãy khởi động dịch vụ từ lớp Ứng dụng để nó không bị dừng khi không được liên kết.

Những lợi thế tôi đã tìm thấy trong cách tiếp cận này so với kỹ thuật startService () / LocalBroadcast là

  • Không cần đối tượng dữ liệu triển khai Parcelable - điều này đặc biệt quan trọng đối với tôi vì tôi hiện đang chia sẻ mã giữa Android và iOS (sử dụng RoboVM)
  • RxJava cung cấp tính năng lập lịch đóng hộp (và đa nền tảng) và dễ dàng cấu thành các hoạt động không đồng bộ tuần tự.
  • Điều này sẽ hiệu quả hơn so với sử dụng LocalBroadcast, mặc dù chi phí sử dụng RxJava có thể lớn hơn điều đó.

Một số mã ví dụ. Đầu tiên là dịch vụ:

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

Và một Hoạt động liên kết với dịch vụ và nhận cập nhật độ cao áp suất:

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

Bố cục cho Hoạt động này là:

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

Nếu dịch vụ cần chạy ở chế độ nền mà không có Hoạt động bị ràng buộc, nó có thể được khởi động từ lớp Ứng dụng cũng như trong quá trình OnCreate()sử dụng Context#startService().


Câu trả lời gốc của tôi (từ năm 2013):

Trong dịch vụ của bạn: (sử dụng COPA làm dịch vụ trong ví dụ bên dưới).

Sử dụng LocalBroadCastManager. Trong onCreate của dịch vụ của bạn, hãy thiết lập chương trình phát sóng:

broadcaster = LocalBroadcastManager.getInstance(this);

Khi bạn muốn thông báo cho UI về điều gì đó:

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

Trong Hoạt động của bạn:

Tạo trình nghe trên 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.
        }
    };
}

và đăng ký nó trong 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 Có, onStart () và onStop () là một phần của vòng đời Hoạt động - xem Vòng đời hoạt động
Clyde

8
Nhận xét nhỏ - bạn đang thiếu định nghĩa của COPA_MESSAGE.
Lior

1
Cảm ơn bạn :) Những người đang hỏi về COPA_RESULT, nó không là gì khác ngoài một biến tĩnh do người dùng tạo. "COPA" là tên dịch vụ của anh ấy nên bạn có thể thay thế hoàn toàn bằng tên dịch vụ của mình. Trong trường hợp của tôi, đó là static final public String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil

1
Giao diện người dùng sẽ không cập nhật nếu người dùng điều hướng khỏi Hoạt động và sau đó quay lại nó sau khi dịch vụ kết thúc. Cách tốt nhất để xử lý điều đó là gì?
SavageKing

1
@SavageKing Bạn cần gửi bất kỳ yêu cầu nào đến dịch vụ phù hợp với phương thức onStart()hoặc onResume()phương thức của bạn . Nói chung, nếu Hoạt động yêu cầu Dịch vụ làm điều gì đó, nhưng thoát ra trước khi nhận được kết quả, thì sẽ hợp lý khi cho rằng kết quả không còn được yêu cầu nữa. Tương tự khi bắt đầu một Hoạt động, nó sẽ giả định rằng không có yêu cầu chưa xử lý nào được Dịch vụ xử lý.
Clyde

32

đối với tôi, giải pháp đơn giản nhất là gửi một chương trình phát sóng, trong hoạt động oncreate, tôi đã đăng ký và xác định chương trình phát sóng như thế này (updateUIReciver được định nghĩa là một cá thể lớp):

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

Và từ dịch vụ bạn gửi ý định như thế này:

Intent local = new Intent();

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

this.sendBroadcast(local);

đừng quên hủy đăng ký khôi phục trong hoạt động trên hủy:

unregisterReceiver(updateUIReciver);

1
Đây là giải pháp tốt hơn nhưng sẽ tốt hơn nếu sử dụng LocalBroadcastManager nếu nó được sử dụng trong ứng dụng sẽ hiệu quả hơn.
Psypher

1
các bước tiếp theo sau this.sendBroadcast (cục bộ); trong dịch vụ?
angel

@angel không có bước tiếp theo, bên trong của tính năng bổ sung ý định chỉ cần thêm các bản cập nhật ui bạn muốn và thats it
Eran Katsav

12

Tôi sẽ sử dụng một dịch vụ ràng buộc để làm điều đó và giao tiếp với nó bằng cách triển khai một người nghe trong hoạt động của tôi. Vì vậy, nếu ứng dụng của bạn triển khai myServiceListener, bạn có thể đăng ký ứng dụng đó như một bộ lắng nghe trong dịch vụ của mình sau khi bạn đã ràng buộc với nó, hãy gọi listener.onUpdateUI từ dịch vụ liên kết của bạn và cập nhật giao diện người dùng của bạn trong đó!


Điều này giống chính xác những gì tôi đang tìm kiếm. Hãy để tôi thử. Cảm ơn.
user200658

Hãy cẩn thận để không rò rỉ tham chiếu đến hoạt động của bạn. Vì hoạt động của bạn có thể bị phá hủy và được tạo lại khi quay.
Eric

Xin chào, vui lòng kiểm tra liên kết này. Tôi đã chia sẻ một mã mẫu cho việc này. Đặt liên kết ở đây với giả định rằng ai đó có thể cảm thấy hữu ích trong tương lai. myownandroid.blogspot.in/2012/08/…
jrhamza

+1 Đây là một giải pháp khả thi, nhưng trong trường hợp của tôi, tôi thực sự cần dịch vụ để tiếp tục chạy mặc dù tất cả hoạt động không liên kết với nó (ví dụ: Người dùng đóng ứng dụng, chấm dứt Hệ điều hành sớm), tôi không còn lựa chọn nào khác ngoài việc sử dụng chương trình phát sóng máy thu.
Neon Warge

9

Tôi khuyên bạn nên xem Otto , một EventBus được thiết kế riêng cho Android. Hoạt động / Giao diện người dùng của bạn có thể lắng nghe các sự kiện được đăng trên Xe buýt từ Dịch vụ và tự tách khỏi chương trình phụ trợ.


5

Giải pháp của Clyde hoạt động, nhưng nó là một chương trình phát sóng, mà tôi khá chắc chắn sẽ kém hiệu quả hơn so với việc gọi một phương thức trực tiếp. Tôi có thể nhầm, nhưng tôi nghĩ rằng các chương trình phát sóng có ý nghĩa hơn đối với giao tiếp giữa các ứng dụng.

Tôi cho rằng bạn đã biết cách liên kết dịch vụ với Activity. Tôi làm một cái gì đó giống như mã bên dưới để xử lý loại sự cố này:

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

Tôi đã bỏ đi các bit để đảm bảo an toàn cho luồng, điều này là cần thiết. Đảm bảo sử dụng khóa hoặc thứ gì đó tương tự khi kiểm tra và sử dụng hoặc thay đổi các tham chiếu phân mảnh trên dịch vụ.


8
LocalBroadcastManager được thiết kế để giao tiếp trong một ứng dụng, vì vậy nó khá hiệu quả. Cách tiếp cận dịch vụ ràng buộc là phù hợp khi bạn có một số lượng khách hàng hạn chế cho dịch vụ và dịch vụ không cần phải chạy độc lập. Cách tiếp cận quảng bá cục bộ cho phép dịch vụ được tách rời hiệu quả hơn khỏi các máy khách của nó và làm cho an toàn luồng không phải là vấn đề.
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);

1

Giải pháp của tôi có thể không phải là sạch nhất nhưng nó sẽ hoạt động mà không có vấn đề gì. Logic đơn giản là tạo một biến tĩnh để lưu trữ dữ liệu của bạn Servicevà cập nhật chế độ xem của bạn mỗi giây trên của bạn Activity.

Giả sử rằng bạn có một Stringtrên của bạn Servicemà bạn muốn gửi nó cho một TextViewcủa bạn Activity. Nó sẽ giống như thế này

Dịch vụ của bạn:

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

Hoạt động của bạn:

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
Không bao giờ làm biến điều không tĩnh trong dịch vụ cũng không phải trong bất kỳ hoạt động
Murtaza Khursheed Hussain

@MurtazaKhursheedHussain - bạn có thể giải thích thêm về điều đó không?
SolidSnake

2
Các thành viên tĩnh là nguồn gây rò rỉ bộ nhớ trong các hoạt động (có rất nhiều bài báo xung quanh) cộng với việc giữ chúng trong dịch vụ làm cho nó trở nên tồi tệ hơn. Một máy thu phát sóng phù hợp hơn nhiều trong tình hình của OP hoặc giữ trong bộ lưu trữ liên tục cũng là một giải pháp.
Murtaza Khursheed Hussain

@MurtazaKhursheedHussain - vì vậy, giả sử nếu tôi có một lớp (không phải lớp dịch vụ chỉ là một số lớp mô hình mà tôi sử dụng để điền dữ liệu) với Hashmap tĩnh được điền lại với một số dữ liệu (đến từ API) mỗi phút được coi là bộ nhớ bị rò rỉ?
SolidSnake

Trong trường hợp androidcó, nếu nó là một danh sách, hãy thử tạo một bộ điều hợp hoặc duy trì dữ liệu bằng cách sử dụng sqllite / domains db.
Murtaza Khursheed Hussain
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.