Android App Links 구성, 구현, 확인

1. 시작하기 전에

딥 링크를 따를 때 사용자의 주된 목표는 보고 싶은 콘텐츠로 이동하는 것입니다. 딥 링크에는 사용자가 이 목표를 달성하는 데 필요한 모든 기능이 있습니다. Android는 다음 유형의 링크를 처리합니다.

  • 딥 링크: 사용자를 앱의 특정 부분으로 안내하는 스키마의 URI입니다.
  • 웹 링크: HTTP 및 HTTPS 스키마가 있는 딥 링크입니다.
  • Android App Links: android:autoVerify 속성이 포함된 HTTP 및 HTTPS 스키마가 있는 웹 링크입니다.

딥 링크, 웹 링크, Android App Links에 관한 자세한 내용은 Android 문서YouTube매체 집중 과정을 참고하세요.

모든 기술적 세부정보를 알고 있다면 함께 제공되는 블로그 게시물의 간단한 구현을 참고하여 이를 몇 단계로 설정할 수 있습니다.

Codelab 목표

이 Codelab에서는 Android App Links를 사용하는 앱의 구성과 구현, 확인 프로세스에 관한 권장사항을 설명합니다.

Android App Links의 이점 중 하나는 안전하다는 것입니다. 즉, 승인되지 않은 앱은 링크를 처리할 수 없습니다. Android OS에서는 Android App Links로 자격을 부여하려면 개발자가 소유한 웹사이트로 링크를 확인해야 합니다. 이 프로세스를 웹사이트 연결이라고 합니다.

이 Codelab에서는 웹사이트와 Android 앱이 있는 개발자에게 초점을 맞춥니다. Android App Links는 앱과 웹사이트 간 원활한 통합을 가능하게 하여 더 나은 사용자 환경을 제공합니다.

기본 요건

학습할 내용

  • Android App Links용 URL 설계에 관한 권장사항을 알아봅니다.
  • Android 애플리케이션에서 모든 유형의 딥 링크를 구성합니다.
  • 경로 와일드 카드(path, pathPrefix, pathPattern, pathAdvancePattern)를 이해합니다.
  • Android App Links 확인 프로세스를 알아봅니다. 여기에는 Google 디지털 애셋 링크(DAL) 파일 업로드, Android App Links 수동 확인 프로세스, Play Console 딥 링크 대시보드가 포함됩니다.
  • 다양한 위치에 있는 식당이 여러 개 포함된 Android 앱을 빌드합니다.

식당 웹 애플리케이션 최종 모습 식당 Android 애플리케이션 최종 모습

필요한 항목

  • Android 스튜디오 Dolphin(2021.3.1) 이상
  • Google 디지털 애셋 링크(DAL) 파일을 호스팅할 도메인. 선택사항: 이 블로그 게시물을 읽으면 빠르게 준비할 수 있습니다.
  • 선택사항: Google Play Console 개발자 계정. 이를 통해 Android App Links 구성을 또 다른 방식으로 디버그할 수 있습니다.

2. 코드 설정

빈 Compose 애플리케이션 만들기

Compose 프로젝트를 시작하려면 다음 단계를 따르세요.

  1. Android 스튜디오에서 File > New > New Project를 선택합니다.

New Project 선택" class="l10n-absolute-url-src" l10n-attrs-original-order="alt,style,src,srcset,sizes,class" l10n-encrypted-style="2dV7hbnGKYSuLgQu/MlX0k8zz2uYoEmRhrQeP6/mnhY=" sizes="(max-width: 840px) 100vw, 856px" src="https://developer.android.com/static/codelabs/android-app-links-introduction/img/3d30cedf28504241.png" srcset="/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_36.png 36w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_48.png 48w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_72.png 72w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_96.png 96w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_480.png 480w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_720.png 720w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_856.png 856w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_960.png 960w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_1440.png 1440w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_1920.png 1920w,/static/codelabs/android-app-links-introduction/img/3d30cedf28504241_2880.png 2880w" style="width: 378.00px" />

  1. 제공되는 템플릿에서 Empty Compose Activity를 선택합니다.

'Empty Compose Activity'가 선택된 Android 스튜디오 'New Project' 모달

  1. Next를 클릭하고 Deep Links Basics라는 프로젝트를 구성합니다. Minimum SDK 버전은 API 수준 21 이상으로 선택해야 합니다. 이는 Compose에서 지원하는 최소 API입니다.

다음 메뉴 값과 옵션이 적용된 Android 스튜디오 New Project 설정 모달 Name은 'Deep Links Basics' Package Name은 'com.devrel.deeplinksbasics' Save location은 기본값 Language는 'Kotlin' Minimum SDK는 API 21

  1. Finish를 클릭하고 프로젝트가 생성되기를 기다립니다.
  2. 애플리케이션을 시작합니다. 앱이 실행되는지 확인합니다. Hello Android! 메시지가 있는 빈 화면이 표시되어야 합니다.

'Hello Android' 텍스트가 표시된 빈 Compose Android 앱 화면

Codelab 솔루션

GitHub에서 이 Codelab의 솔루션 코드를 다운로드할 수 있습니다.

git clone https://github.com/android/deep-links

또는 저장소를 ZIP 파일로 다운로드할 수도 있습니다.

먼저 deep-links-introduction 디렉터리로 이동합니다. solution 디렉터리에 앱이 있습니다. 자신의 속도에 맞게 Codelab을 단계별로 진행하고 필요한 경우 솔루션을 확인하는 것이 좋습니다. Codelab을 진행하는 중에 프로젝트에 추가해야 하는 코드 스니펫이 제공됩니다.

3. 딥 링크 지향 URL 설계 검토

RESTful API 설계

링크는 웹 개발의 필수 부분이며 링크 설계는 수많은 반복을 거치므로 다양한 표준이 만들어집니다. 웹 개발 링크 설계 표준을 살펴보고 적용하는 것이 좋습니다. 그래야 링크를 사용하고 유지관리하기 쉽습니다.

이러한 표준 중 하나가 REST(Representational State Transfer)로, 웹 서비스용 API를 빌드하는 데 일반적으로 사용하는 아키텍처입니다. Open API는 REST API를 표준화하는 이니셔티브입니다. 또는 REST를 사용하여 딥 링크용 URL을 설계할 수 있습니다.

웹 서비스를 빌드하는 것이 아닙니다. 이 섹션에서는 URL 설계에만 집중합니다.

URL 설계

먼저 웹사이트에서 결과 URL을 검토하고 Android 앱에서 이 URL이 무엇을 표현하는지 파악합니다.

  • /restaurants - 관리하는 모든 식당을 나열합니다.
  • /restaurants/:restaurantName - 개별 식당의 세부정보를 표시합니다.
  • /restaurants/:restaurantName/orders - 식당의 주문을 표시합니다.
  • /restaurants/:restaurantName/orders/:orderNumber - 식당의 특정 주문을 표시합니다.
  • /restaurants/:restaurantName/orders/latest - 식당의 최신 주문을 표시합니다.

URL 설계가 중요한 이유

Android에는 다른 앱 구성요소의 작업을 처리하고 URL을 포착하는 데 사용되는 인텐트 필터가 있습니다. URL을 포착하도록 인텐트 필터를 정의할 때는 경로 접두사와 간단한 와일드 카드를 사용하는 구조를 따라야 합니다. 다음은 식당 웹사이트의 기존 URL에서 구성되는 방식을 보여주는 예입니다.

https://example.com/pawtato-3140-Skinner-Hollow-Road

이 URL이 식당과 그 위치를 지정한다고 해도 Android가 URL을 포착하도록 인텐트 필터를 정의할 때 경로가 문제가 될 수 있습니다. 애플리케이션이 다음과 같은 다양한 식당 URL에 기반하기 때문입니다.

https://example.com/rawrbucha-2064-carriage-lane

https://example.com/pizzabus-1447-davis-avenue

이러한 URL을 포착하도록 경로와 와일드 카드를 사용하여 인텐트 필터를 정의한다면 기본적으로 작동하는 https://example.com/*와 같은 주소를 사용할 수 있습니다. 그래도 문제를 완전히 해결한 것은 아닙니다. 다음과 같이 웹사이트의 여러 섹션에 관한 다른 기존 경로가 있기 때문입니다.

배달: https://example.com/deliveries

관리자: https://example.com/admin

일부는 내부용이므로 이러한 URL을 Android에서 포착하는 것을 원하지 않을 수 있지만 정의된 인텐트 필터 https://example.com/*는 존재하지 않는 URL을 비롯하여 이러한 URL을 포착합니다. 사용자가 이러한 URL 중 하나를 클릭하면 브라우저에서 열리거나(> Android 12) 확인 대화상자가 표시될 수 있습니다(< Android 12). 이는 이 설계에서 의도하는 동작이 아닙니다.

현재 Android에서는 이 문제를 해결하는 경로 접두사를 제공하지만 아래 URL을 다시 설계해야 합니다.

https://example.com/*

아래와 같이 다시 설계합니다.

https://example.com/restaurants/*

계층적 중첩 구조를 추가하면 인텐트 필터가 명확하게 정의되고 Android는 개발자가 포착하도록 지시한 URL을 포착합니다.

URL 설계 권장사항

다음은 Open API에서 수집되고 딥 링크 관점에 적용된 권장사항입니다.

  • 노출하는 비즈니스 항목에 URL 설계를 집중합니다. 예를 들어 전자상거래에서는 고객주문이 될 수 있습니다. 여행의 경우 티켓항공편이 될 수 있습니다. 식당 앱과 웹사이트에서는 식당주문을 사용합니다.
  • 대부분의 HTTP 메서드(GET, POST, DELETE, PUT)는 진행 중인 요청을 설명하는 동사지만 URL의 엔드포인트에 동사를 사용하는 것은 혼란스러울 수 있습니다.
  • 컬렉션을 설명하려면 /restaurants/:restaurantName과 같은 항목의 복수형을 사용합니다. 이렇게 하면 URL을 읽고 유지관리하기가 더 쉬워집니다. 다음은 각 HTTP 메서드가 포함된 예입니다.

GET /restaurants/pawtato

POST /restaurants

DELETE /restaurants

PUT /restaurants/pawtato

각 URL은 읽기도 쉽고 용도를 파악하기도 쉽습니다. 이 Codelab에서는 웹 서비스 API 설계와 각 메서드가 하는 기능을 다루지 않습니다.

  • 논리적 중첩을 사용하여 관련 정보가 포함된 URL을 그룹 처리합니다. 예를 들어 앱에 포함된 식당 중 하나의 URL에는 작업 중인 주문이 있을 수 있습니다.

/restaurants/1/orders

4. 데이터 요소 검토

AndroidManifest.xml 파일은 Android의 필수 부분으로, Android 빌드 도구와 Android OS, Google Play에 앱 정보를 설명합니다.

딥 링크의 경우 세 가지 기본 태그 <action>, <category> <data>를 사용하여 인텐트 필터를 정의해야 합니다. 이 섹션에서는 <data> 태그에 중점을 둡니다.

<data> 요소는 사용자가 링크를 클릭하면 Android OS에 링크의 URL 구조를 알려줍니다. 인텐트 필터에서 사용할 수 있는 URL 형식과 구조는 다음과 같습니다.

<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>|<pathAdvancedPattern>|<pathSuffix>]

Android는 인텐트 필터의 모든 <data> 요소를 읽고 파싱하고 병합하여 속성의 모든 변형을 고려합니다. 예를 들면 다음과 같습니다.

AndroidManifest.xml

<intent-filter>
  ...
  <data android:scheme="http" />
  <data android:scheme="https" />
  <data android:host="example.com" />
  <data android:path="/restaurants" />
  <data android:pathPrefix="/restaurants/orders" />
</intent-filter>

Android는 다음 URL을 포착합니다.

  • http://example.com/restaurants
  • https://example.com/restaurants
  • http://example.com/restaurants/orders/*
  • https://example.com/restaurants/orders/*

경로 속성

path(API 1에서 제공)

이 속성은 인텐트의 전체 경로와 일치하는 /로 시작하는 전체 경로를 지정합니다. 예를 들어 android:path="/restaurants/pawtato"/restaurants/pawtato 웹사이트 경로와만 일치하고 /restaurant/pawtato가 있는 경우 s가 누락되어 이 URL은 일치하지 않습니다.

pathPrefix(API 1에서 제공)

이 속성은 인텐트 경로의 시작 부분과만 일치하는 부분 경로를 지정합니다. 예를 들면 다음과 같습니다.

android:pathPrefix="/restaurants"/restaurants/pawtato, /restaurants/pizzabus 등의 식당 경로와 일치합니다.

pathSuffix(API 31에서 제공)

이 속성은 인텐트 경로의 끝부분과 정확히 일치하는 경로를 지정합니다. 예를 들면 다음과 같습니다.

android:pathSuffix="tato"/restaurants/pawtato, /restaurants/corgtatotato로 끝나는 모든 식당 경로와 일치합니다.

pathPattern(API 1에서 제공)

이 속성은 인텐트의 와일드 카드가 있는 전체 경로와 일치하는 전체 경로를 지정합니다.

  • *: 별표를 사용하면 선행 문자가 0번 이상 나오는 일치 항목을 찾습니다.
  • .*: 마침표 다음에 별표를 사용하면 0자 이상 일치하는 항목을 찾습니다.

예:

  • /restaurants/piz*abus: 이 패턴은 pizzabus 식당과 일치하지만 /restaurants/pizzabus, /restaurants/pizzzabus, /restaurants/pizabus와 같이 이름에 z 문자가 0번 이상 나오는 식당과도 일치합니다.
  • /restaurants/.*: 이 패턴은 /restaurants/pizzabus, /restaurants/pawtato와 같이 /restaurants 경로가 있는 모든 식당 이름과 일치하고 /restaurants/wateriehall과 같이 앱이 알지 못하는 식당 이름과도 일치합니다.

pathAdvancePattern(API 31에서 제공)

이 속성은 다음과 같이 정규식과 유사한 패턴이 있는 전체 경로와 일치하는 전체 경로를 지정합니다.

  • 마침표(.)는 모든 문자와 일치합니다.
  • 대괄호 세트([...])는 문자 범위와 일치합니다. 이 세트는 not(^) 수정자를 지원합니다.
  • 별표(*)는 선행 패턴과 0번 이상 일치합니다.
  • 더하기 기호(+)는 선행 패턴과 1번 이상 일치합니다.
  • 중괄호({...})는 패턴이 일치할 수 있는 횟수를 나타냅니다.

이 속성은 pathPattern이 확장된 것으로 간주할 수 있습니다. 이를 통해, 일치시킬 URL에 더 많은 유연성이 제공됩니다. 예를 들면 다음과 같습니다.

  • /restaurants/[a-zA-Z]*/orders/[0-9]{3}은 길이가 최대 3자리인 모든 식당 주문과 일치합니다.
  • /restaurants/[a-zA-Z]*/orders/latest는 앱의 식당에서 받은 최신 주문과 일치합니다.

5. 딥 링크와 웹 링크 만들기

맞춤 스키마가 있는 딥 링크는 가장 일반적인 딥 링크 유형이며 가장 구현하기 쉽지만 단점도 있습니다. 이러한 링크는 웹사이트에서 열 수 없고 앱 매니페스트에서 해당 스키마 지원을 선언하는 앱에서 링크를 열 수 있습니다.

<data> 요소에서는 어떤 스키마도 사용할 수 있습니다. 예를 들어 이 Codelab에서는 food://restaurants/keybabs URL을 사용합니다.

  1. Android 스튜디오에서 다음 인텐트 필터를 매니페스트 파일에 추가합니다.

AndroidManifest.xml

<activity ... >
  <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <data android:scheme="food"/>
    <data android:path="/restaurants/keybabs"/>
  </intent-filter>
</activity>
  1. 애플리케이션에서 맞춤 스키마가 있는 링크를 열 수 있는지 확인하려면 기본 활동에 다음을 추가하여 홈 화면에 출력합니다.

MainActivity.kt

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Receive the intent action and data
        val action: String? = intent?.action;
        val data: Uri? = intent?.data;

        setContent {
            DeepLinksBasicsTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    // Add a Column to print a message per line
                    Column {
                        // Print it on the home screen
                        Greeting("Android")
                        Text(text = "Action: $action")
                        Text(text = "Data: $data")
                    }
                }
            }
        }
    }
}
  1. 인텐트가 수신되는지 테스트하려면 다음 명령어로 Android 디버그 브리지(adb)를 사용합니다.
adb shell am start -W -a android.intent.action.VIEW -d "food://restaurants/keybabs"

이 명령어는 VIEW 작업이 있는 인텐트를 시작하고 제공된 URL을 데이터로 사용합니다. 이 명령어를 실행하면 앱이 시작되고 인텐트를 수신합니다. 기본 화면 텍스트 섹션의 변경사항을 확인하세요. 하나는 Hello Android! 메시지를 표시하고 두 번째는 인텐트가 호출된 작업을 표시하며 세 번째는 인텐트가 호출된 URL을 표시합니다.

다음 이미지를 보면 Android 스튜디오 하단 섹션에서 언급된 adb 명령어가 실행되었습니다. 오른쪽에는 앱의 홈 화면에 인텐트 정보가 표시되므로 정보가 수신되었음을 나타냅니다. 'code view', 'emulator', 'terminal' 탭이 열려 있는 Android 스튜디오 전체 화면 code view에는 기본 MainActivity.kt 파일이 표시됩니다. emulator에는 성공적으로 수신되었음을 확인하는 딥 링크 텍스트 필드가 표시됩니다. terminal에는 이 Codelab에서 방금 설명한 adb 명령어가 표시됩니다.

웹 링크는 맞춤 스키마 대신 http, https를 사용하는 딥 링크입니다.

웹 링크를 구현하려면 식당에서 받은 최신 주문을 나타내는 /restaurants/keybabs/order/latest.html 경로를 사용하세요.

  1. 기존 인텐트 필터를 사용하여 매니페스트 파일을 조정합니다.

AndroidManifest.xml

<intent-filter>
  <action android:name="android.intent.action.VIEW"/>
  <category android:name="android.intent.category.BROWSABLE"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <data android:scheme="food"/>
  <data android:path="/restaurants/keybabs"/>

  <!-- Web link configuration -->
  <data android:scheme="http"/>
  <data android:scheme="https"/>
  <data android:host="sabs-deeplinks-test.web.app"/>
  <data android:path="/restaurants/keybabs/order/latest.html"/>
</intent-filter>

두 경로가 모두 공유되므로(/restaurants/keybabs) 동일한 인텐트 필터 아래 두는 것이 좋습니다. 이를 통해 구현이 더 간소화되고 매니페스트 파일이 읽기 쉬워집니다.

  1. 웹 링크를 테스트하기 전에 앱을 다시 시작하여 새로운 변경사항을 적용합니다.
  2. 동일한 adb 명령어를 사용하여 인텐트를 실행하지만 여기서는 URL을 업데이트합니다.
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/keybabs/orders/latest.html"

스크린샷을 보면 인텐트가 수신되고 웹브라우저가 열려 웹사이트를 표시합니다. 이는 Android 12 이상 버전의 기능입니다. Android 스튜디오 전체 뷰. 'Code 뷰' 탭에는 언급된 인텐트 필터를 보여주는 AndroidManifest.xml 파일이 표시되고 'Emulator 뷰' 탭에는 식당 웹 앱을 가리키는, 웹 링크로 열린 웹페이지가 표시되며 'Terminal 뷰' 탭에는 웹 링크의 adb 명령어가 표시됩니다.

6. Android App Links 구성

이러한 링크는 사용자가 링크를 클릭하면 확인 대화상자 없이 사용자가 앱으로 연결되도록 보장하므로 가장 원활한 사용자 환경을 제공합니다. Android App Links는 Android 6.0에서 구현되었고 가장 명확한 유형의 딥 링크입니다. http/https 스키마와 android:autoVerify 속성을 사용하는 웹 링크라서 앱이 모든 일치하는 링크의 기본 핸들러가 됩니다. Android App Links를 구현하려면 다음과 같은 두 가지 기본 단계가 필요합니다.

  1. 적절한 인텐트 필터로 매니페스트 파일 업데이트
  2. 확인을 위해 웹사이트 연결 추가

매니페스트 파일 업데이트

  1. Android App Links를 지원하려면 매니페스트 파일에서 이전 구성을 다음과 같이 바꿉니다.

AndroidManifest.xml

<!-- Replace deep link and web link configuration with this -->
<!-- Please update the host with your own domain -->
<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW"/>
  <category android:name="android.intent.category.BROWSABLE"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <data android:scheme="https"/>
  <data android:host="example.com"/>
  <data android:pathPrefix="/restaurants"/>
</intent-filter>

이 인텐트 필터는 android:autoVerify 속성을 추가하여 true로 설정합니다. 이렇게 하면 애플리케이션이 설치될 때와 새 업데이트가 실행될 때마다 Android OS에서 도메인을 확인할 수 있습니다.

웹사이트 연결

Android App Links를 확인하려면 애플리케이션과 웹사이트 간 연결을 만드세요. Google 디지털 애셋 링크(DAL) JSON 파일은 웹사이트에 게시해야 확인이 실행됩니다.

Google DAL은 다른 앱과 웹사이트에 관한 확인 가능한 문을 정의하는 프로토콜이자 API입니다. 이 Codelab에서는 assetlinks.json 파일에 있는 Android 앱에 관한 문을 만듭니다. 예를 들면 다음과 같습니다.

assetlinks.json

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.devrel.deeplinksbasics",
    "sha256_cert_fingerprints":
   ["B0:4E:29:05:4E:AB:44:C6:9A:CB:D5:89:A3:A8:1C:FF:09:6B:45:00:C5:FD:D1:3E:3E:12:C5:F3:FB:BD:BA:D3"]
  }
}]

이 파일은 문 목록을 저장할 수 있지만 예에서는 한 항목만 보여줍니다. 각 문에는 다음 필드가 포함되어야 합니다.

  • Relation. 타겟에 관해 선언되는 하나 이상의 관계를 설명합니다.
  • Target. 이 문이 적용되는 애셋입니다. 사용 가능한 두 가지 타겟 web 또는 android_app 중 하나일 수 있습니다.

Android 문의 target 속성에는 다음 필드가 포함되어 있습니다.

  • namespace. 모든 Android 앱의 android_app입니다.
  • package_name. 정규화된 패키지 이름(com.devrel.deeplinksbasics)입니다.
  • sha256_cert_fingerprints. 앱 인증서의 지문입니다. 다음 섹션에서 이 인증서를 생성하는 방법을 알아봅니다.

인증서 지문

인증서 지문을 가져오는 방법은 다양합니다. 이 Codelab에서는 두 가지 방법을 사용합니다. 하나는 애플리케이션 디버그 빌드용이고 다른 하나는 Google Play 스토어에 앱을 출시하는 데 도움이 됩니다.

디버그 구성

Android 스튜디오에서 처음 프로젝트를 실행하면 디버그 인증서를 사용하여 앱에 자동으로 서명합니다. 이 인증서는 $HOME/.android/debug.keystore에 있습니다. Gradle 명령어를 사용하여 이 SHA-256 인증서 지문을 가져올 수 있습니다. 단계는 다음과 같습니다.

  1. Control을 두 번 누르면 Run anything 메뉴가 표시됩니다. 표시되지 않으면 오른쪽 사이드바 Gradle 메뉴에서 찾아 Gradle 아이콘을 클릭하면 됩니다.

Gradle 아이콘이 선택되어 있는 Android 스튜디오 Gradle 메뉴 탭

  1. gradle signingReport라고 입력하고 Enter 키를 누릅니다. 명령어가 콘솔에서 실행되고 디버그 앱 변형의 지문 정보가 표시됩니다.

Gradle 서명 보고 결과가 표시된 Terminal 창

  1. 웹사이트 연결을 완료하려면 SHA-256 인증서 지문을 복사하고 JSON 파일을 업데이트하여 https://<domain>/.well-know/assetlinks.json 위치의 웹사이트에 업로드합니다. 이 Android App Links 블로그 게시물을 참고하여 설정을 완료하세요.
  2. 앱이 여전히 실행 중이라면 Stop을 눌러 애플리케이션을 중지합니다.
  3. 확인 프로세스를 다시 실행하려면 시뮬레이터에서 앱을 삭제합니다. 시뮬레이터에서 DeepLinksBasics 앱 아이콘을 길게 클릭하고 App Info를 선택합니다. 모달에서 UninstallConfirm을 클릭합니다. 그런 다음 애플리케이션을 실행하면 Android 스튜디오에서 연결을 확인할 수 있습니다.

f112e0d252c5eb48.gif

  1. app 실행 구성을 선택해야 합니다. 그러지 않으면 Gradle 서명 보고가 다시 실행됩니다. 'app' 구성이 선택된 구성 메뉴를 실행하는 Android 스튜디오
  2. 애플리케이션을 다시 시작하고 Android App Links URL로 인텐트를 실행합니다.
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/"
  1. 앱이 실행되고 인텐트가 홈 화면에 표시됩니다.

Android Emulator 홈 화면. Android App Links가 성공적으로 구현되었음을 보여주는 텍스트 필드가 포함되어 있습니다.

축하합니다. 첫 번째 Android App Links를 만들었습니다.

출시 구성

이제 Play 스토어에 Android App Links가 있는 애플리케이션을 업로드하려면 적절한 인증서 지문이 있는 출시 빌드를 사용해야 합니다. 이를 생성하고 업로드하려면 다음 단계를 따르세요.

  1. Android 스튜디오 기본 메뉴에서 Build > Generate Signed Bundle/APK를 클릭합니다.
  2. 다음 대화상자에서 Play 앱 서명용 Android App Bundle을 선택하거나 기기에 직접 배포하는 경우 APK를 선택합니다.
  3. 다음 대화상자에서 Key store path 아래 Create new를 클릭합니다. 그러면 새 창이 표시됩니다.
  4. 키 저장소경로를 선택하고 이름을 basics-keystore.jks로 지정합니다.
  5. 키 저장소의 비밀번호를 만들고 확인합니다.
  6. Alias는 기본값을 사용합니다.
  7. 비밀번호와 확인값이 키 저장소와 동일한지 확인합니다. 반드시 일치해야 합니다.
  8. Certificate 정보를 채우고 OK를 클릭합니다.

다음 값과 메뉴 항목이 포함된 Android 스튜디오 New Key Store 모달. 'Key store path'에는 디렉터리가 선택되어 있고 'Password' 및 'Confirm'에는 비밀번호가 선택되어 있으며 'Alias'에는 key0, 'Password' 및 'Confirm'에는 동일한 비밀번호, 'Validity'에는 기본값, 'First and Last Name'에는 Sabs sabs, 'Organizational Unit'에는 Android, 'Organization'에는 MyOrg, 'City or Locality'에는 MyCity, 'State or Province'에는 MyState, 'Country Code'에는 US가 입력되어 있습니다.

  1. 암호화된 키를 내보내는 옵션이 Play 앱 서명용으로 선택되어 있는지 확인하고 Next를 클릭합니다.

다음 값과 메뉴 항목이 포함된 Generate Sign Bundle or APK 메뉴 모달. 'Module'에는 기본값, 'Key store path'에는 생성된 경로, 'Key store password'에는 이전에 생성된 비밀번호, 'Key alias'에는 key0, 'Key password'에는 이전에 생성된 비밀번호가 채워져 있고 'Export encrypted key for enrolling published apps in Google Play App Signing'이 선택되어 있으며 'Encrypted key export path'에는 기본값이 선택되어 있습니다.

  1. 이 대화상자에서 출시 빌드 변형을 선택하고 Finish를 클릭합니다. 이제 앱을 Google Play 스토어에 업로드하고 Play 앱 서명을 사용할 수 있습니다.

Play 앱 서명

Play 앱 서명을 통해 Google에서는 개발자가 앱의 서명 키를 관리하고 보호할 수 있도록 지원합니다. 개발자는 앱의 서명된 번들을 업로드하기만 하면 됩니다(이전 단계에서 실행).

assetlinks.json 파일의 인증서 지문을 검색하고 Android App Links를 출시 변형 빌드에 포함하려면 다음 단계를 따르세요.

  1. Google Play Console에서 앱 만들기를 클릭합니다.
  2. 앱 이름에 Deep Links Basics를 입력합니다.
  3. 다음 두 옵션에서는 무료를 선택합니다. 다음 값이 업데이트된 '앱 만들기' 메뉴. '앱 이름'에는 Deep Links Basics가 입력되어 있고 '앱 또는 게임'에는 앱이, '무료 또는 유료'에는 무료가 선택되어 있으며 두 가지 선언이 수락되어 있습니다.
  4. 선언을 수락하고 앱 만들기를 클릭합니다.
  5. 번들을 업로드하고 Android App Links를 테스트하려면 왼쪽 메뉴에서 테스트 > 내부 테스트를 선택합니다.
  6. 새 버전 만들기를 클릭합니다.

Play Console '내부 테스트' 섹션. '새 버전 만들기' 버튼이 표시되어 있습니다.

  1. 다음 화면에서 업로드를 클릭하고 이전 섹션에서 생성된 번들을 선택합니다. DeepLinksBascis > app > release에서 app-release.aab 파일을 찾을 수 있습니다. 열기를 클릭하고 번들이 업로드될 때까지 기다립니다.
  2. 업로드되면 나머지 필드는 기본값으로 둡니다. 저장을 클릭합니다.

Play Console의 '내부 테스트 버전' 섹션. Deep Links Basics 앱이 업로드되어 있습니다. 기본값이 채워져 있습니다.

  1. 다음 섹션을 준비하려면 버전 검토를 클릭하고 다음 화면에서 내부 테스트로 출시 시작을 클릭합니다. 경고는 무시합니다. Play 스토어에 게시하는 작업은 이 Codelab의 범위가 아니기 때문입니다.
  2. 모달에서 출시를 클릭합니다.
  3. Play 앱 서명에서 만든 SHA-256 인증서 지문을 가져오려면 왼쪽 메뉴의 딥 링크 탭으로 이동하여 딥 링크 대시보드를 확인합니다.

Play Console의 딥 링크 대시보드. 최근 업로드된 딥 링크에 관한 딥 링크 정보가 모두 표시되어 있습니다.

  1. 도메인 섹션에서 웹사이트의 도메인을 클릭합니다. Google Play Console에서는 앱을 사용하여 도메인을 확인(웹사이트 연결)하지 않았다고 언급합니다.
  2. 도메인 문제 해결 섹션에서 더보기 화살표를 클릭합니다.
  3. 이 화면에서 Google Play Console은 인증서 지문으로 assetlinks.json 파일을 업데이트하는 방법을 보여줍니다. 코드 스니펫을 복사하고 assetlinks.json 파일을 업데이트하세요.

딥 링크 대시보드 도메인 확인 섹션. 올바른 인증서 지문으로 도메인을 업데이트하는 방법을 보여줍니다.

  1. assetlinks.json 파일이 업데이트되면 인증 재확인을 클릭합니다. 인증이 아직 통과되지 않았다면 인증 서비스에서 새 변경사항을 감지할 때까지 최대 5분이 걸릴 수 있습니다.
  2. 딥 링크 대시보드 페이지를 새로고침하면 인증 오류가 더 이상 표시되지 않습니다.

업로드된 앱 확인

시뮬레이터에 있는 앱을 확인하는 방법은 이미 알아봤습니다. 이제 Play 스토어에 업로드된 앱을 확인합니다.

에뮬레이터에 애플리케이션을 설치하고 Android App Links가 확인되었는지 확인하려면 다음 단계를 따르세요.

  1. 왼쪽 사이드바에서 출시 개요를 클릭하고 방금 업로드한 최신 버전 1(1.0) 버전을 선택합니다.
  2. 출시 세부정보(오른쪽 파란색 화살표)를 클릭하여 출시 세부정보를 확인합니다.
  3. 동일한 오른쪽 파란색 화살표 버튼을 클릭하여 App Bundle 정보를 가져옵니다.
  4. 모달에서 다운로드 탭을 선택하고 서명된 범용 APK 다운로드를 클릭합니다.
  5. 시뮬레이터에 이 번들을 설치하기 전에 Android 스튜디오에서 설치한 이전 애플리케이션을 삭제합니다.
  6. 시뮬레이터에서 DeepLinksBasics 앱 아이콘을 길게 클릭하고 App Info를 선택합니다. 모달에서 UninstallConfirm을 클릭합니다.

f112e0d252c5eb48.gif

  1. 다운로드된 번들을 설치하려면 다운로드된 1.apk 파일을 시뮬레이터 화면으로 드래그 앤 드롭하고 설치될 때까지 기다립니다.

8967dac36ae545ee.gif

  1. 확인을 테스트하려면 Android 스튜디오에서 터미널을 열고 다음 두 가지 명령어로 확인 프로세스를 실행합니다.
adb shell pm verify-app-links --re-verify com.devrel.deeplinksbasics
adb shell pm get-app-links com.devrel.deeplinksbasics
  1. get-app-links 명령어 다음에 콘솔에 verified 메시지가 표시되어야 합니다. legacy_failure 메시지가 표시되면 인증서 지문이 웹사이트용으로 업로드한 지문과 일치하는지 확인합니다. 일치하는데 여전히 인증 메시지가 표시되지 않으면 6, 7, 8단계를 다시 실행해 보세요.

콘솔 출력

7. Android App Links 구현

이제 모든 항목을 구성했으므로 앱을 구현해 보겠습니다.

구현에는 Jetpack Compose를 사용합니다. Jetpack Compose에 관한 자세한 내용은 Jetpack Compose로 더 빠르게 더 나은 앱 빌드를 참고하세요.

코드 종속 항목

이 프로젝트에 필요한 종속 항목을 포함하고 업데이트하려면 다음 단계를 따르세요.

  • ModuleProject Gradle 파일에 다음을 추가합니다.

build.gradle(Project)

buildscript {
  ...
  dependencies {
    classpath "com.google.dagger:hilt-android-gradle-plugin:2.43"
  }
}

build.gradle(Module)

plugins {
  ...
  id 'kotlin-kapt'
  id 'dagger.hilt.android.plugin'
}
...
dependencies {
  ...
  implementation 'androidx.compose.material:material:1.2.1'
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
  implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
  implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
  implementation "com.google.dagger:hilt-android:2.43"
  kapt "com.google.dagger:hilt-compiler:2.43"
}

project zip 파일에는 각 식당에 사용할 수 있는 로열티 없는 이미지 10개가 있는 이미지 디렉터리가 포함되어 있습니다. 자유롭게 사용하거나 자체 이미지를 포함해도 됩니다.

HiltAndroidApp의 기본 진입점을 추가하려면 다음 단계를 따르세요.

  • 새로운 Kotlin 클래스/파일 DeepLinksBasicsApplication.kt를 만들고 새 애플리케이션 이름으로 매니페스트 파일을 업데이트합니다.

DeepLinksBasicsApplication.kt

package com.devrel.deeplinksbasics

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class DeepLinksBasicsApplication : Application() {}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- Update name property -->
    <application
        android:name=".DeepLinksBasicsApplication"
        ...

데이터

Restaurant 클래스, 저장소, 로컬 데이터 소스로 식당의 데이터 레이어를 만들어야 합니다. 모든 항목은 만들어야 하는 data 패키지 아래에 있게 됩니다. 이를 위해서는 다음 단계를 따르세요.

  1. Restaurant.kt 파일에서 다음 코드 스니펫으로 Restaurant 클래스를 만듭니다.

Restaurant.kt

package com.devrel.deeplinksbasics.data

import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable

@Immutable
data class Restaurant(
    val id: Int = -1,
    val name: String = "",
    val address: String = "",
    val type: String = "",
    val website: String = "",
    @DrawableRes val drawable: Int = -1
)
  1. RestaurantLocalDataSource.kt 파일에서 데이터 소스 클래스에 식당을 추가합니다. 자체 도메인으로 데이터를 업데이트해야 합니다. 다음 코드 스니펫을 참조하세요.

RestaurantLocalDataSource.kt

package com.devrel.deeplinksbasics.data

import com.devrel.deeplinksbasics.R
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class RestaurantLocalDataSource @Inject constructor() {
    val restaurantList = listOf(
        Restaurant(
            id = 1,
            name = "Pawtato",
            address = "3140 Skinner Hollow Road, Medford, Oregon 97501",
            type = "Potato and gnochi",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/pawtato/",
            drawable = R.drawable.restaurant1,
        ),
        Restaurant(
            id = 2,
            name = "Rawrbucha",
            address = "2064 Carriage Lane, Mansfield, Ohio 44907",
            type = "Kombucha",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/rawrbucha/",
            drawable = R.drawable.restaurant2,
        ),
        Restaurant(
            id = 3,
            name = "Pizzabus",
            address = "1447 Davis Avenue, Petaluma, California 94952",
            type = "Pizza",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/pizzabus/",
            drawable = R.drawable.restaurant3,
        ),
        Restaurant(
            id = 4,
            name = "Keybabs",
            address = "3708 Pinnickinnick Street, Perth Amboy, New Jersey 08861",
            type = "Kebabs",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/keybabs/",
            drawable = R.drawable.restaurant4,
        ),
        Restaurant(
            id = 5,
            name = "BBQ",
            address = "998 Newton Street, Saint Cloud, Minnesota 56301",
            type = "BBQ",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/bbq/",
            drawable = R.drawable.restaurant5,
        ),
        Restaurant(
            id = 6,
            name = "Salades",
            address = "4522 Rockford Mountain Lane, Oshkosh, Wisconsin 54901",
            type = "salads",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/salades/",
            drawable = R.drawable.restaurant6,
        ),
        Restaurant(
            id = 7,
            name = "Gyros and moar",
            address = "1993 Bird Spring Lane, Houston, Texas 77077",
            type = "Gyro",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/gyrosAndMoar/",
            drawable = R.drawable.restaurant7,
        ),
        Restaurant(
            id = 8,
            name = "Peruvian ceviche",
            address = "2125 Deer Ridge Drive, Newark, New Jersey 07102",
            type = "seafood",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/peruvianCeviche/",
            drawable = R.drawable.restaurant8,
        ),
        Restaurant(
            id = 9,
            name = "Vegan burgers",
            address = "594 Warner Street, Casper, Wyoming 82601",
            type = "vegan",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/veganBurgers/",
            drawable = R.drawable.restaurant9,
        ),
        Restaurant(
            id = 10,
            name = "Taquitos",
            address = "1654 Hart Country Lane, Blue Ridge, Georgia 30513",
            type = "mexican",
            // TODO: Update with your own domain
            website = "https://your.own.domain/restaurants/taquitos/",
            drawable = R.drawable.restaurant10,
        ),
    )
}
  1. 이미지를 프로젝트로 가져와야 합니다.
  2. 이제 RestaurantRepository.kt 파일에서 다음 코드 스니펫과 같이 이름으로 식당을 가져오는 함수가 있는 Restaurant 저장소를 추가합니다.

RestaurantRepository.kt

package com.devrel.deeplinksbasics.data

import javax.inject.Inject

class RestaurantRepository @Inject constructor(
    private val restaurantLocalDataSource: RestaurantLocalDataSource
){
    val restaurants: List<Restaurant> = restaurantLocalDataSource.restaurantList

    // Method to obtain a restaurant object by its name
    fun getRestaurantByName(name: String): Restaurant ? {
        return restaurantLocalDataSource.restaurantList.find {
            val processedName = it.name.filterNot { it.isWhitespace() }.lowercase()
            val nameToTest = name.filterNot { it.isWhitespace() }.lowercase()
            nameToTest == processedName
        }
    }
}

ViewModel

앱과 Android App Links를 통해 식당을 선택하려면 선택된 식당 값을 변경하는 ViewModel을 만들어야 합니다. 다음 단계를 따르세요.

  • RestaurantViewModel.kt 파일에서 다음 코드 스니펫을 추가합니다.

RestaurantViewModel.kt

package com.devrel.deeplinksbasics.ui.restaurant

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.devrel.deeplinksbasics.data.Restaurant
import com.devrel.deeplinksbasics.data.RestaurantRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class RestaurantViewModel @Inject constructor(
    private val restaurantRepository: RestaurantRepository,
) : ViewModel() {
    // restaurants and selected restaurant could be used as one UIState stream
    // which will scale better when exposing more data.
    // Since there are only these two, it is okay to expose them as separate streams
    val restaurants: List<Restaurant> = restaurantRepository.restaurants

    private val _selectedRestaurant = MutableStateFlow<Restaurant?>(value = null)
    val selectedRestaurant: StateFlow<Restaurant?>
        get() = _selectedRestaurant

    // Method to update the current restaurant selection
    fun updateSelectedRestaurantByName(name: String) {
        viewModelScope.launch {
            val selectedRestaurant: Restaurant? = restaurantRepository.getRestaurantByName(name)
            if (selectedRestaurant != null) {
                _selectedRestaurant.value = selectedRestaurant
            }
        }
    }
}

Compose

이제 viewmodel과 데이터 레이어의 로직이 있으므로 UI 레이어를 추가해 보겠습니다. Jetpack Compose 라이브러리를 사용하면 단 몇 단계로 추가할 수 있습니다. 이 앱의 경우 카드 그리드에 식당을 렌더링하려고 합니다. 사용자는 각 카드를 클릭하고 각 식당의 세부정보로 이동할 수 있습니다. 기본 구성 가능한 함수 세 개와 상응하는 식당으로 라우팅하는 탐색 구성요소 하나가 필요합니다.

완성된 식당 앱을 보여주는 Android Emulator

UI 레이어를 추가하려면 다음 단계를 따르세요.

  1. 각 식당의 세부정보를 렌더링하는 구성 가능한 함수로 시작합니다. RestaurantCardDetails.kt 파일에서 다음 코드 스니펫을 추가하세요.

RestaurantCardDetails.kt

package com.devrel.deeplinksbasics.ui

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant

@Composable
fun RestaurantCardDetails (
    restaurant: Restaurant,
    onBack: () -> Unit,
) {
    BackHandler() {
       onBack()
    }
    Scaffold(
        topBar = {
            TopAppBar(
                backgroundColor = Color.Transparent,
                elevation = 0.dp,
            ) {
                Row(
                    horizontalArrangement = Arrangement.Start,
                    modifier = Modifier.padding(start = 8.dp)
                ) {
                    Icon(
                        imageVector = Icons.Default.ArrowBack,
                        contentDescription = "Arrow Back",
                       modifier = Modifier.clickable {
                            onBack()
                        }
                    )
                    Spacer(modifier = Modifier.width(8.dp))
                    Text(text = restaurant.name)
                }
            }
        }
    ) { paddingValues ->
        Card(
            modifier = Modifier
                .padding(paddingValues)
                .fillMaxWidth(),
            elevation = 2.dp,
            shape = RoundedCornerShape(corner = CornerSize(8.dp))
        ) {
            Column(
                modifier = Modifier
                    .padding(16.dp)
                    .fillMaxWidth()
            ) {
                Text(text = restaurant.name, style = MaterialTheme.typography.h6)
                Text(text = restaurant.type, style = MaterialTheme.typography.caption)
                Text(text = restaurant.address, style = MaterialTheme.typography.caption)
                SelectionContainer {
                    Text(text = restaurant.website, style = MaterialTheme.typography.caption)
                }
                Image(painter = painterResource(id = restaurant.drawable), contentDescription = "${restaurant.name}")
            }
        }
    }
}
  1. 이제 그리드 셀과 그리드 자체를 구현합니다. RastaurantCell.kt 파일에서 다음 코드 스니펫을 추가하세요.

RestaurantCell.kt

package com.devrel.deeplinksbasics.ui

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant

@Composable
fun RestaurantCell(
    restaurant: Restaurant
){
    Card(
        modifier = Modifier
            .padding(horizontal = 8.dp, vertical = 8.dp)
            .fillMaxWidth(),
        elevation = 2.dp,
        shape = RoundedCornerShape(corner = CornerSize(8.dp))
    ) {
        Column(
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text(text = restaurant.name, style = MaterialTheme.typography.h6)
            Text(text = restaurant.address, style = MaterialTheme.typography.caption)
            Image(painter = painterResource(id = restaurant.drawable), contentDescription = "${restaurant.name}")
        }
    }
}
  1. RestaurantGrid.kt 파일에서 다음 코드 스니펫을 추가합니다.

RestaurantGrid.kt

package com.devrel.deeplinksbasics.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.devrel.deeplinksbasics.data.Restaurant

@Composable
fun RestaurantGrid(
    restaurants: List<Restaurant>,
    onRestaurantSelected: (String) -> Unit,
    navigateToRestaurant: (String) -> Unit,
) {
    Scaffold(topBar = {
        TopAppBar(
            backgroundColor = Color.Transparent,
            elevation = 0.dp,
        ) {
            Text(text = "Restaurants", fontWeight = FontWeight.Bold)
        }
    }) { paddingValues ->
        LazyVerticalGrid(
            columns = GridCells.Adaptive(minSize = 200.dp),
            modifier = Modifier.padding(paddingValues)
        ) {
            items(items = restaurants) { restaurant ->
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable(onClick = {
                            onRestaurantSelected(restaurant.name)
                            navigateToRestaurant(restaurant.name)
                        })
                ) {
                    RestaurantCell(restaurant)
                }
            }
        }
    }
}
  1. 이제 애플리케이션 상태와 탐색 로직을 구현하고 MainActivity.kt를 업데이트해야 합니다. 사용자가 식당 카드를 클릭하면 특정 식당으로 이동할 수 있습니다. RestaurantAppState.kt 파일에서 다음 코드 스니펫을 추가하세요.

RestaurantAppState.kt

package com.devrel.deeplinksbasics.ui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController

sealed class Screen(val route: String) {
   object Grid : Screen("restaurants")
   object Name : Screen("restaurants/{name}") {
       fun createRoute(name: String) = "restaurants/$name"
   }
}

@Composable
fun rememberRestaurantAppState(
    navController: NavHostController = rememberNavController(),
) = remember(navController) {
    RestaurantAppState(navController)
}

class RestaurantAppState(
    val navController: NavHostController,
) {
    fun navigateToRestaurant(restaurantName: String) {
        navController.navigate(Screen.Name.createRoute(restaurantName))
    }

    fun navigateBack() {
        navController.popBackStack()
    }
}
  1. 탐색의 경우 NavHost를 만들고, 컴포저블 경로를 사용하여 각 식당으로 이동해야 합니다. RestaurantApp.kt 파일에서 다음 코드 스니펫을 추가하세요.

RestaurantApp.kt

package com.devrel.deeplinksbasics.ui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.devrel.deeplinksbasics.ui.restaurant.RestaurantViewModel

@Composable
fun RestaurantApp(
   viewModel: RestaurantViewModel = viewModel(),
   appState: RestaurantAppState = rememberRestaurantAppState(),
) {
    val selectedRestaurant by viewModel.selectedRestaurant.collectAsState()
    val onRestaurantSelected: (String) -> Unit = { viewModel.updateSelectedRestaurantByName(it) }

    NavHost(
        navController = appState.navController,
        startDestination = Screen.Grid.route,
    ) {
        // Default route that points to the restaurant grid
        composable(Screen.Grid.route) {
            RestaurantGrid(
                restaurants = viewModel.restaurants,
                onRestaurantSelected = onRestaurantSelected,
                navigateToRestaurant = { restaurantName ->
                    appState.navigateToRestaurant(restaurantName)
                },
            )
        }
        // Route for the navigation to a particular restaurant when a user clicks on it
        composable(Screen.Name.route) {
            RestaurantCardDetails(restaurant = selectedRestaurant!!, onBack = appState::navigateBack)
        }
    }
}
  1. 이제 애플리케이션 인스턴스로 MainActivity.kt를 업데이트할 수 있습니다. 파일을 다음 코드로 교체합니다.

MainActivity.kt

package com.devrel.deeplinksbasics

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import com.devrel.deeplinksbasics.ui.RestaurantApp
import com.devrel.deeplinksbasics.ui.theme.DeepLinksBasicsTheme
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DeepLinksBasicsTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    RestaurantApp()
                }
            }
        }
    }
}
  1. 애플리케이션을 실행하여 그리드를 탐색하고 특정 식당을 선택합니다. 식당을 선택하면 앱에 해당 식당과 식당의 세부정보가 표시됩니다.

fecffce863113fd5.gif

이제 Android App Links를 그리드와 모든 식당에 추가합니다. /restaurants 아래에 그리드에 관한 AndroidManifest.xml 섹션이 이미 있습니다. 정말 좋은 점은 모든 식당에 동일하게 사용할 수 있다는 것입니다. 로직에 새 경로 구성만 추가하면 됩니다. 이를 위해서는 다음 단계를 따르세요.

  1. /restaurants를 경로로 수신하도록 인텐트 필터를 사용하여 매니페스트 파일을 업데이트하고 도메인을 호스트로 포함해야 합니다. AndroidManifest.xml 파일에서 다음 코드 스니펫을 추가하세요.

AndroidManifest.xml

...
<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW"/>
  <category android:name="android.intent.category.BROWSABLE"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <data android:scheme="http"/>
  <data android:scheme="https"/>
  <data android:host="your.own.domain"/>
  <data android:pathPrefix="/restaurants"/>
</intent-filter>
  1. RestaurantApp.kt 파일에서 다음 코드 스니펫을 추가합니다.

RestaurantApp.kt

...
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink

fun RestaurantApp(...){
  NavHost(...){
    ...
    //  Route for the navigation to a particular restaurant when a user clicks on it
    //  and for an incoming deep link
    // Update with your own domain
        composable(Screen.Name.route,
            deepLinks = listOf(
                navDeepLink { uriPattern = "https://your.own.domain/restaurants/{name}" }
            ),
            arguments = listOf(
                navArgument("name") {
                    type = NavType.StringType
                }
            )
        ) { entry ->
            val restaurantName = entry.arguments?.getString("name")
            if (restaurantName != null) {
                LaunchedEffect(restaurantName) {
                    viewModel.updateSelectedRestaurantByName(restaurantName)
                }
            }
            selectedRestaurant?.let {
                RestaurantCardDetails(
                    restaurant = it,
                    onBack = appState::navigateBack
                )
            }
        }
  }
}

내부적으로 NavHost는 Android 인텐트 Uri 데이터를 컴포저블 경로와 일치시킵니다. 경로가 일치하면 composable이 렌더링됩니다.

composable 구성요소는 인텐트 필터에서 수신된 URI 목록이 포함되어 있는 deepLinks 매개변수를 사용할 수 있습니다. 이 Codelab에서는 생성된 웹사이트의 URL을 추가하고, id 매개변수를 정의하여 특정 식당을 수신하고 사용자를 해당 특정 식당으로 보냅니다.

  1. Android App Links를 클릭한 후 앱 로직이 사용자를 해당 식당으로 보내는지 확인하려면 다음과 같이 adb를 사용합니다.
adb shell am start -W -a android.intent.action.VIEW -d "https://sabs-deeplinks-test.web.app/restaurants/gyrosAndMoar"

앱에 해당 식당이 표시됩니다.

'Gyros and moar' 식당 화면이 표시된 Android Emulator 식당 앱

8. Play Console 대시보드 검토

딥 링크 대시보드는 이미 살펴봤습니다. 이 대시보드에서는 딥 링크가 제대로 작동하는지 확인하는 데 필요한 정보를 모두 제공합니다. 앱 버전별로도 확인할 수 있습니다. 매니페스트 파일에 추가된 도메인, 링크, 맞춤 링크가 표시됩니다. 문제가 발생하는 경우 assetlinks.json 파일을 업데이트할 위치도 표시됩니다.

Android App Links 하나가 확인된 Play Console 딥 링크 대시보드

9. 결론

축하합니다. 첫 번째 Android App Links 애플리케이션을 빌드했습니다.

Android App Links를 설계, 구성, 생성, 테스트하는 프로세스를 알아봤습니다. 이 프로세스는 다양한 부분으로 구성되므로 이 Codelab에서는 Android OS 개발에 성공할 수 있도록 이러한 모든 세부정보를 종합합니다.

이제 Android App Links가 작동하기 위한 주요 단계를 알게 되었습니다.

추가 자료

참조 문서