서비스의 Android 업데이트 활동 UI


102

나는 항상 새로운 작업을 확인하는 서비스가 있습니다. 새 작업이있는 경우 해당 정보를 표시하도록 활동 UI를 새로 고치고 싶습니다. 이 예제 는 https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/를 찾았습니다 . 그게 좋은 접근입니까? 다른 예가 있습니까?

감사.


여기 내 대답을 참조하십시오. 리스너를 사용하여 클래스간에 통신 할 인터페이스를 쉽게 정의 할 수 있습니다. stackoverflow.com/questions/14660671/…
Simon

답변:


228

내 원래 대답은 아래를 참조하십시오. 그 패턴은 잘 작동했지만 최근에는 서비스 / 활동 커뮤니케이션에 대해 다른 접근 방식을 사용하기 시작했습니다.

  • 활동이 서비스에 대한 직접 참조를 가져 오도록하여 인 텐트를 사용하는 대신 직접 호출을 허용 하는 바인딩 된 서비스 를 사용하십시오.
  • RxJava를 사용하여 비동기 작업을 실행합니다.

  • 서비스가 실행중인 활동이없는 경우에도 백그라운드 작업을 계속해야하는 경우 바인딩 해제시 중지되지 않도록 Application 클래스에서 서비스를 시작합니다.

startService () / LocalBroadcast 기술과 비교하여이 접근 방식에서 찾은 이점은 다음과 같습니다.

  • Parcelable을 구현하기 위해 데이터 개체가 필요하지 않습니다. 현재 Android와 iOS (RoboVM 사용)간에 코드를 공유하고 있으므로 이것은 특히 중요합니다.
  • RxJava는 미리 준비된 (및 크로스 플랫폼) 스케줄링과 순차 비동기 작업의 손쉬운 구성을 제공합니다.
  • RxJava를 사용하는 오버 헤드가 그보다 클 수 있지만 이것은 LocalBroadcast를 사용하는 것보다 더 효율적이어야합니다.

몇 가지 예제 코드. 먼저 서비스 :

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>

바인딩 된 활동없이 백그라운드에서 서비스를 실행해야하는 경우 .NET Framework를 OnCreate()사용하여 Application 클래스에서도 시작할 수 있습니다 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);
}

활동에서 :

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 ()은 활동 라이프 사이클의 일부입니다. 활동 라이프 사이클
Clyde

8
작은 댓글-COPA_MESSAGE의 정의가 누락되었습니다.
Lior

1
감사합니다 :) COPA_RESULT에 대해 문의하시는 분들은 사용자가 생성 한 정적 변수 일뿐입니다. "COPA"는 그의 서비스 이름이므로 완전히 대체 할 수 있습니다. 제 경우에는 정적 최종 public String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil 2015 년

1
사용자가 활동에서 벗어나 서비스가 완료된 후 다시 돌아 오면 UI가 업데이트되지 않습니다. 그것을 처리하는 가장 좋은 방법은 무엇입니까?
SavageKing

1
@SavageKing 당신은 당신의 onStart()또는 onResume()방법에 적합한 서비스에 어떤 요청을 보내야합니다 . 일반적으로 활동이 서비스에 작업을 요청했지만 결과를 받기 전에 종료하는 경우 결과가 더 이상 필요하지 않다고 가정하는 것이 합리적입니다. 마찬가지로 활동을 시작할 때 서비스에서 처리중인 미해결 요청이 없다고 가정해야합니다.
Clyde

32

나를 위해 가장 간단한 해결책은 방송을 보내는 것이 었습니다. 활동 oncreate에서 다음과 같이 방송을 등록하고 정의했습니다 (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
this.sendBroadcast (local); 이후의 다음 단계 서비스에서?
angel

의도의 엑스트라 그냥 UI를 원하는 업데이트 및 먹으 렴를 추가 내부 @angel에는 다음 단계는 그것이 없다
에 란 카 차브 대통령

12

바인딩 된 서비스 를 사용하여 이를 수행하고 내 활동에 리스너를 구현하여 통신합니다. 따라서 앱이 myServiceListener를 구현하는 경우 바인딩 한 후 서비스에서 리스너로 등록하고 바인딩 된 서비스에서 listener.onUpdateUI를 호출하고 UI를 업데이트 할 수 있습니다!


이것은 내가 찾고있는 것과 정확히 같습니다. 시도해 보겠습니다. 감사.
user200658 2013-02-04

활동에 대한 참조를 유출하지 않도록주의하세요. 활동이 파괴되고 순환하면서 다시 생성 될 수 있기 때문입니다.
에릭

안녕하세요,이 링크를 확인하십시오. 이에 대한 샘플 코드를 공유했습니다. 누군가가 미래에 도움이 될 것이라고 가정하여 여기에 링크를 넣으십시오. myownandroid.blogspot.in/2012/08/…
jrhamza

+1 이것은 실행 가능한 솔루션이지만 제 경우에는 모든 활동이 바인딩 해제 되어도 계속 실행되는 서비스가 필요합니다 (예 : 사용자가 응용 프로그램을 닫고, OS 조기 종료), 브로드 캐스트를 사용할 수밖에 없습니다. 수신기.
Neon Warge

9

특히 Android에 맞춤화 된 EventBus 인 Otto를 확인하는 것이 좋습니다 . 활동 / UI는 서비스에서 버스에 게시 된 이벤트를 수신하고 백엔드에서 분리 할 수 ​​있습니다.


5

Clyde의 솔루션은 작동하지만 브로드 캐스트이므로 메서드를 직접 호출하는 것보다 효율성이 떨어질 것이라고 확신합니다. 착각 할 수 있지만 방송은 애플리케이션 간 커뮤니케이션을위한 것보다 더 많은 의미가 있다고 생각합니다.

서비스를 활동과 바인딩하는 방법을 이미 알고 있다고 가정합니다. 이런 종류의 문제를 처리하기 위해 아래 코드와 같은 작업을 수행합니다.

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 는 응용 프로그램 내에서 통신하도록 설계되었으므로 매우 효율적입니다. 바인딩 된 서비스 접근 방식은 서비스에 대해 제한된 수의 클라이언트가 있고 서비스를 독립적으로 실행할 필요가없는 경우 괜찮습니다. 로컬 브로드 캐스트 접근 방식을 사용하면 서비스를 클라이언트에서보다 효과적으로 분리 할 수 ​​있으며 스레드 안전성이 문제가되지 않습니다.
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

내 솔루션이 가장 깨끗하지는 않지만 문제없이 작동합니다. 논리는 단순히 정적 변수를 만들어에 데이터를 저장 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 후세인

@MurtazaKhursheedHussain-그것에 대해 더 자세히 설명해 주시겠습니까?
SolidSnake

2
정적 멤버는 활동에서 메모리 누수의 원인이며 (많은 기사가 있음) 서비스 상태로 유지하면 상황이 악화됩니다. 방송 수신기는 OP의 상황에서 훨씬 더 적절하거나 영구 저장 장치에 보관하는 것도 해결책입니다.
Murtaza Khursheed Hussain 2011

@MurtazaKhursheedHussain-매분마다 일부 데이터 (API에서 오는)로 다시 채워지는 정적 Hashmap이있는 클래스 (서비스 클래스가 아니라 데이터를 채우는 데 사용하는 일부 모델 클래스가 아님)가 있다고 가정 해 보겠습니다. 메모리 누수?
SolidSnake

androidyes 의 컨텍스트 에서 목록이면 어댑터를 만들거나 sqllite / realm db를 사용하여 데이터를 유지합니다.
Murtaza Khursheed Hussain
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.