Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Wear 앱을 GoogleApi로 마이그레이션

Google Play 서비스 버전 11.8.0부터 Wear OS 앱은 GoogleApiClient 클래스를 사용하는 대신 GoogleApi 클래스에 기반한 클라이언트 개체를 사용합니다.

GoogleApi를 사용하면 비동기 작업을 보다 쉽게 설정할 수 있습니다. 예를 들어 Tasks API 소개에 설명된 것처럼 PendingResult 개체 대신 Task 개체를 얻을 수 있습니다.

이 페이지에는 다음 항목이 포함되어 있습니다.

  • 교체용 구성요소 표
  • Tasks API 사용을 위해 기존 앱을 업데이트하는 방법의 예

참고: 이 업데이트는 Google Play 서비스 버전 10.2.0을 사용하는 중국용 Wear OS 앱에는 적용되지 않습니다.

지원 중단된 구성요소 교체

GoogleApi 클래스를 확장하는 클래스 DataClient MessageClient 같은 클래스를 사용하면 Google Play 서비스 SDK가 Google Play 서비스 연결을 자동으로 관리합니다.

아래의 교체용 클래스를 사용하는 앱은 GoogleApiClient 개체를 만들고 관리할 필요가 없습니다. Google API 액세스 Wearable 클래스의 참조 페이지도 참조하세요.

아래 표에는 지원 중단된 구성요소와 교체용 구성요소가 나와 있습니다.

지원 중단된 구성요소 교체용 구성요소
CapabilityApi CapabilityClient
Channel ChannelClient.Channel
ChannelApi ChannelClient
DataApi DataClient
MessageApi MessageClient
NodeApi NodeClient

또한 다음을 참고하세요.

Wear 앱 이전의 예제

이전의 예로 제공되는 아래의 코드 스니펫에는 데이터 영역 API를 사용하는 Wear 데이터 영역 샘플이 Google Play 서비스의 버전 11.8.0용으로 어떻게 업데이트되었는지를 보여줍니다. 전화 모듈이 있는 앱인 경우 업데이트가 Wear 모듈과 비슷할 수 있습니다.

Google Play 서비스 종속성 업데이트

앱에서 이전 버전의 Google Play 서비스가 사용될 수 있기 때문에 Wear 모듈의 build.gradle 파일에서 다음 종속성을 업데이트합니다.

    dependencies {
    ...
    compile 'com.google.android.gms:play-services-wearable:11.8.0'
    }
    

앱의 import 문 업데이트

Tasks API의 클래스를 포함하여 필요한 클래스를 가져옵니다.

예를 들어 이전 Wear 데이터 영역 샘플MainActivity.java 파일에는 다음 import 문이 포함되었습니다. 이 import 문을 삭제해야 합니다.

Kotlin

    ...
    import com.google.android.gms.common.api.GoogleApiClient
    ...
    

자바

    ...
    import com.google.android.gms.common.api.GoogleApiClient;
    ...
    

Wear 데이터 영역 샘플에서 위와 같은 import 문은 다음과 같이 바뀌었습니다(두 번째는 작업 예외 처리를 위한 문임).

Kotlin

    ...
    import com.google.android.gms.tasks.Tasks
    import java.util.concurrent.ExecutionException
    ...
    

자바

    ...
    import com.google.android.gms.tasks.Tasks;
    import java.util.concurrent.ExecutionException;
    ...
    

새 클라이언트 인터페이스 구현

사용된 모든 GoogleApiClient 클래스와 관련 인터페이스(ConnectionCallbacks, OnConnectionFailedListener 등)를 삭제하고 그 밖의 구현된 리스너를 새 버전으로 바꿉니다. 재정의할 실제 메서드에 사용되는 이름은 이전과 동일하므로 주요 변화는 아래에 나와 있는 예와 유사합니다.

Wear 데이터 영역 샘플의 기본 작업( GitHub의 차이점 보고서에 표시됨)은 CapabilityApi.CapabilityListener 같은 인터페이스를 구현했습니다. 그러나 이제는 CapabilityClient.OnCapabilityChangedListener를 구현합니다.

다음은 클래스 정의를 비교한 것입니다.

아래는 Google Play 서비스 버전 11.8.0 사용 전의 스니펫입니다.

Kotlin

    class MainActivity :
            Activity(),
            GoogleApiClient.ConnectionCallbacks,
            GoogleApiClient.OnConnectionFailedListener,
            DataApi.DataListener,
            MessageApi.MessageListener,
            CapabilityApi.CapabilityListener
    

자바

    public class MainActivity extends Activity implements
      ConnectionCallbacks,
      OnConnectionFailedListener,
      DataApi.DataListener,
      MessageApi.MessageListener,
      CapabilityApi.CapabilityListener
    

아래는 Google Play 서비스 버전 11.8.0 사용 후의 스니펫입니다.

Kotlin

    class MainActivity :
            Activity(),
            DataClient.OnDataChangedListener,
            MessageClient.OnMessageReceivedListener,
            CapabilityClient.OnCapabilityChangedListener
    

자바

    public class MainActivity extends Activity implements
      DataClient.OnDataChangedListener,
      MessageClient.OnMessageReceivedListener,
      CapabilityClient.OnCapabilityChangedListener
    

리스너 삭제 및 추가

새 클라이언트 개체는 캐시되고 GoogleApi 인스턴스 간에 공유되기 때문에 멤버 변수를 보관할 필요가 없으며 클라이언트를 저렴한 비용으로 만들 수 있고 리스너를 놓칠 일이 없습니다.

아래는 수정된 Wear 데이터 영역 샘플의 스니펫입니다.

Kotlin

    override fun onResume() {
        super.onResume()
        Wearable.getDataClient(this).addListener(this)
        Wearable.getMessageClient(this).addListener(this)
        Wearable.getCapabilityClient(this)
                .addListener(
                        this,
                        Uri.parse("wear://"),
                        CapabilityClient.FILTER_REACHABLE
                )
    }

    override fun onPause() {
        super.onPause()
        Wearable.getDataClient(this).removeListener(this)
        Wearable.getMessageClient(this).removeListener(this)
        Wearable.getCapabilityClient(this).removeListener(this)
    }
    

자바

    @Override
    protected void onResume() {
      super.onResume();
      Wearable.getDataClient(this).addListener(this);
      Wearable.getMessageClient(this).addListener(this);
      Wearable.getCapabilityClient(this)
      .addListener(
        this, Uri.parse("wear://"), CapabilityClient.FILTER_REACHABLE);
    }

    @Override
    protected void onPause() {
      super.onPause();
      Wearable.getDataClient(this).removeListener(this);
      Wearable.getMessageClient(this).removeListener(this);
      Wearable.getCapabilityClient(this).removeListener(this);
    }
    

Tasks API를 사용하여 정보 요청

데이터 변경이 있을 때 앱을 업데이트하는 리스너의 외부에서 정보를 요청하려는 경우가 있을 수 있습니다. 이러한 경우 Tasks API와 result 클래스(예: Task<ResultType>)와 함께 DataClient 같은 클라이언트를 사용하여 정보를 요청합니다.

예를 들어 Wear 데이터 영역 샘플 에 나와 있는 것처럼 Tasks API를 사용하여, 지정된 기능에 연결되어 있는 노드를 찾을 수 있습니다.

Kotlin

    private fun showNodes(vararg capabilityNames: String) {
        Wearable.getCapabilityClient(this)
                .getAllCapabilities(CapabilityClient.FILTER_REACHABLE).apply {
                    addOnSuccessListener { capabilityInfoMap ->
                        val nodes: Set<Node> = capabilityInfoMap
                                .filter { capabilityNames.contains(it.key) }
                                .flatMap { it.value.nodes }
                                .toSet()
                        showDiscoveredNodes(nodes)
                    }
                }
    }

    private fun showDiscoveredNodes(nodes: Set<Node>) {
        val nodesList: Set<String> = nodes.map { it.displayName }.toSet()
        val msg: String = if (nodesList.isEmpty()) {
            Log.d(TAG, "Connected Nodes: No connected device was found for the given capabilities")
            getString(R.string.no_device)
        } else {
            Log.d(TAG, "Connected Nodes: ${nodesList.joinToString(separator = ", ")}")
            getString(R.string.connected_nodes, nodesList)
        }
        Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show()
    }
    

자바

    private void showNodes(final String... capabilityNames) {
      Task<Map<String, CapabilityInfo>> capabilitiesTask =
        Wearable.getCapabilityClient(this)
                .getAllCapabilities(CapabilityClient.FILTER_REACHABLE);
      capabilitiesTask.addOnSuccessListener(new
        OnSuccessListener<Map<String, CapabilityInfo>>() {
          @Override
          public void onSuccess(Map<String, CapabilityInfo>
            capabilityInfoMap) {
              Set<Node> nodes = new HashSet<>();
              if (capabilityInfoMap.isEmpty()) {
                showDiscoveredNodes(nodes);
                return;
              }
              for (String capabilityName : capabilityNames) {
                CapabilityInfo capabilityInfo = capabilityInfoMap.get(capabilityName);
                if (capabilityInfo != null) {
                  nodes.addAll(capabilityInfo.getNodes());
                }
              }
              showDiscoveredNodes(nodes);
          }
      });
    }

    private void showDiscoveredNodes(Set<Node> nodes) {
      List<String> nodesList = new ArrayList<>();
      for (Node node : nodes) {
        nodesList.add(node.getDisplayName());
      }
      LOGD(TAG, "Connected Nodes: " + (nodesList.isEmpty()
        ? "No connected device was found for the given capabilities"
        : TextUtils.join(",", nodesList)));
      String msg;
      if (!nodesList.isEmpty()) {
        msg = getString(R.string.connected_nodes, TextUtils.join(", ", nodesList));
      } else {
        msg = getString(R.string.no_device);
      }
      Toast.makeText(MainActivity.this, msg, Toast.LENGTH_LONG).show();
    }
    

Wearable API와 Tasks API를 활용하는 다른 코드를 보려면 Wear 데이터 영역 샘플 을 참조하세요. UI 스레드 또는 서비스의 작업을 과도하게 사용하는 경우 다른 옵션을 이용할 수 있습니다. 다음은 작업을 차단하고 동기적으로 결과를 얻는 방법의 예입니다.

Kotlin

    override fun doInBackground(vararg params: Asset): Bitmap? {
        if (params.isNotEmpty()) {
            val asset = params[0]
            val getFdForAssetResponseTask: Task<DataClient.GetFdForAssetResponse> =
                    Wearable.getDataClient(applicationContext).getFdForAsset(asset)
            return try {
                // Block on a task and get the result synchronously. This is generally done
                // when executing a task inside a separately managed background thread. Doing
                // this on the main (UI) thread can cause your application to become
                // unresponsive.
                val getFdForAssetResponse: DataClient.GetFdForAssetResponse =
                        Tasks.await(getFdForAssetResponseTask)
                getFdForAssetResponse.inputStream?.let { assetInputStream ->
                    BitmapFactory.decodeStream(assetInputStream)
                } ?: run {
                    Log.w(TAG, "Requested an unknown Asset.")
                    null
                }

            } catch (exception: ExecutionException) {
                Log.e(TAG, "Failed retrieving asset, Task failed: $exception")
                return null
            } catch (exception: InterruptedException) {
                Log.e(TAG, "Failed retrieving asset, interrupt occurred: $exception")
                return null
            }

        } else {
            Log.e(TAG, "Asset must be non-null")
            return null
        }
    }

    override fun onPostExecute(bitmap: Bitmap?) {
        bitmap?.also {
            Log.d(TAG, "Setting background image on second page..")
            moveToPage(1)
            assetFragment.setBackgroundImage(it)
        }
    }
    

자바

    @Override
    protected Bitmap doInBackground(Asset... params) {
      if (params.length > 0) {
        Asset asset = params[0];
        Task<DataClient.GetFdForAssetResponse> getFdForAssetResponseTask =
          Wearable.getDataClient(getApplicationContext()).getFdForAsset(asset);
        try {
          // Block on a task and get the result synchronously. This is generally done
          // when executing a task inside a separately managed background thread. Doing
          // this on the main (UI) thread can cause your application to become
          // unresponsive.
          DataClient.GetFdForAssetResponse getFdForAssetResponse =
            Tasks.await(getFdForAssetResponseTask);
          InputStream assetInputStream = getFdForAssetResponse.getInputStream();
          if (assetInputStream != null) {
            return BitmapFactory.decodeStream(assetInputStream);
          } else {
            Log.w(TAG, "Requested an unknown Asset.");
            return null;
          }

        } catch (ExecutionException exception) {
          Log.e(TAG, "Failed retrieving asset, Task failed: " + exception);
          return null;
        } catch (InterruptedException exception) {
          Log.e(TAG, "Failed retrieving asset, interrupt occurred: " + exception);
          return null;
        }
      } else {
        Log.e(TAG, "Asset must be non-null");
        return null;
      }
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
      if (bitmap != null) {
        LOGD(TAG, "Setting background image on second page..");
        moveToPage(1);
        assetFragment.setBackgroundImage(bitmap);
      }
    }