[Android Basics in Kotlin] Internet: Retrofit / BindingAdapter
https://developer.android.com/courses/android-basics-kotlin/unit-4
Android Kotlin Basics in Kotlin | Android Basics in Kotlin - Internet | Android Developers
Write coroutines for complex code, and learn about HTTP and REST to get data from the internet.
developer.android.com
해당 글은 위 사이트의 PATHWAY 2 과정인 데이터 가져오기 및 표시하기을 공부하며 작성한 글입니다.
이 과정에서는 HTTP와 REST를 사용해 인터넷에서 이미지를 가져와 표시합니다.
인터넷을 통한 데이터를 주고 받을 때 고려해야 할 두 가지 부분이 있는데, 바로 클라이언트와 서버이다.
클라이언트는 브라우저, 웹 또는 장치의 다른 앱 등으로 서버에 요청을 보내고, 서버는 각 요청을 처리해 적절한 응답을 보낸다.
서버와 클라이언트는 HTTP라는 프로토콜을 사용하며 인터넷을 통해 서로 통신한다.
HTTP 프로토콜에는 요청 및 응답 방법과 같은 구체적인 정보가 포함되어 있어 서버와 클라이언트가 서로 이해할 수 있다.
HTTP 요청은 HTTP 메서드를 통해 어떤 요청을 하는지 식별된다.
HTTP 메서드로는 GET, POST, PUT, DELETE 등이 있다.
HTTP 응답에는 상태 코드(200, 404와 같은), Content-Type 등이 포함되어 있다.
HTTP 상태 코드는 5가지 기본 범주(100 ~ 599)로 나뉘며 주로 200, 400, 500대가 많이 사용된다.
200 대는 성공적인 요청, 400대는 클라이언트 오류, 500대는 서버 측 오류를 의미한다.
URL는 여러 컴포넌트들로 구성된다.
https://google.com/search?q=android
- https://는 현재 사용하고 있는 프로토콜 유형을 나타낸다.
- google.com은 도메인에 해당하는 서버를 나타낸다.
- search는 자원에 어떠한 목적으로 접근하는지를 표시한 것이다.
- ?q=는 매개변수를 뜻한다.
웹 서버에 저장된 데이터를 앱으로 가져오려며 연결을 설정하고 인터넷에서 서버와 통신해야 한다.
대부분의 웹 서버는 REST(Representational State Transfer)라는 일반적인 stateless 웹 아키텍처를 사용해 웹 서비스를 실행한다.
표준화된 방법으로는 URI를 통해 웹 서비스에 요청을 보낸다.
웹 서비스 요청
웹 서비스 요청은 URI를 포함하고 있으며 HTTP 프로토콜을 사용해 서버에 전송된다.
일반적인 HTTP 작업은 다음과 같다.
- GET: 서버 데이터 검색
- POST/PUT: 서버에 새로운 데이터를 추가/생성/업데이트
- DELETE: 서버에서 데이터를 삭제
웹 서비스의 응답은 일반적으로 키-값 쌍으로 구조화된 데이터를 나타내는 XML 또는 JSON 등의 일반적인 웹 형식으로 지정된다.
Retrofit
Retrofit 라이브러리는 백엔트와 통신하며 웹 서비스에 전달하는 매개변수를 기반으로 웹 서비스의 URI를 만든다.
Retrofit은 웹 서비스의 콘텐츠를 기반으로 앱의 네트워크 API를 만든다.
웹 서비스에서 데이터를 가져온 후 데이터를 디코딩해 String 같은 객체 형식으로 변환하는 별도의 변환기 라이브러리를 통해 라우팅한다.
Retrofit 종속 항목 추가
- build.gradle(project)
repositories {
google()
jcenter()
}
- build.gradle(Module)
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Moshi Converter -> JSON 결과를 String으로 변환
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
* Retrofit2를 비롯한 많은 타사 라이브러리는 자바 8 언어 기능을 사용하기 때문에 특정 자바 8 언어 기능을 위한 지원이 내장되어 있음
Retrofit 서비스 API 구현
구현 단계는 다음과 같다.
1) network/MarsApiService.kt 파일
- 웹 서버 상수 추가
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
- Retrofit 빌더를 통해 Retrofit 객체 빌드
addConverterFactory
웹 서비스의 기본 URI 및 변환기 팩토리를 통해 웹 서비스 API 빌드가 가능하다.
변환기는 웹 서비스에서 얻은 데이터로 해야할 일을 Retrofit에 알린다.
현재 예제의 경우 JSON 응답을 String으로 변환하는 일을 알린다.
baseUrl() 를 이용해 웹 서비스의 기본 URI 추가 / build() 호출해 Retrofit 객체 생성
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build
- MarsApiService(인터페이스): Retrofit이 HTTP 요청을 사용해 웹 서버와 통신하는 방법을 정의
@GET 주석을 사용해 Retrofit에 GET 요청임을 알리고 이 서비스 메서드의 엔드포인트를 지정한다.
즉, 위에서 선언한 BASE_URL/photos라는 URI로 서비스를 요청하는 것이다.
interface MarsApiService {
@GET("photos")
fun getPhotos(): String
}
- MarsApi(object): Retrofit 서비스 초기화 -> 앱의 나머지 부분에 액세스할 수 있는 공개 싱글톤 객체
싱글톤 패턴은 객체의 인스턴스가 하나만 생성되도록 보장하며 객체의 전역 접근 포인트는 하나 뿐이다.
코틀린에서는 싱글톤 객체 선언에 항상 object 키워드를 사용한다.
최초 사용 시 초기화되도록 하기 위해 지연 초기화 사용
create() 메소드 통해 retrofitService 변수 초기화
MarsApi.retrofitService를 호출할 때 마다 MarsApiService를 구현하는 것과 동일한 싱글톤 Retrofit 객체에 액세스한다.
즉, MarsApiService에 직접적으로 접근하지 않고 MarsApi를 통해 간접적으로 접근!
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java) }
}
2) OverviewViewModel
- 코루틴 구현
앱의 각 ViewModel을 대상으로 정의된 기본 제공 코루틴 스콥인 ViewModelScope을 사용한다.
이 스콥에서 실행된 모든 코루틴은 ViewModel이 삭제되면 자동으로 취소된다.
ViewModelScope을 사용해 코루틴을 실행하고, 백그라운드에서 Retrofit 네트워크를 호출한다.
** 코루틴 내에서 외부 함수를 호출할 경우 외부 함수는 suspend 함수여야 한다.
3) AndroidManifest 에서 권한 추가
Android에서 권한의 목적은 사용자의 개인정보를 보호하는 것이다.
Android 앱은 연락처, 통화기록 같은 사용자 데이터와 카메라, 인터넷 같은 특정 시스템 기능에 액세스할 수 있는 권한을 선언하거나 요청해야 한다.
앱이 인터넷에 액세스하려면 INTERNET 권한이 필요하다.
사용자 권한은 AndroidManifest 파일의 <uses-permission> 태그 내에 선언된다.
<uses-permission android:name="android.permission.INTERNET" />
- 예외 처리
예외(Exception)는 컴파일 시간이 아닌 런타임 시 발생할 수 있는 오류로, 사용자에게 알리지 않고 앱을 갑자기 종료한다.
이유도 모른 채 앱이 갑자기 종료되면 좋은 UX가 아니므로 이러한 예외를 처리하는 것이 필요하다.
예외 처리는 앱이 갑자기 종료되지 않도록 하는 메커니즘이며 사용자 친화적인 방법으로 처리된다.
예외 처리에는 try-catch문이 사용된다. 사용 방식은 다음과 같다.
try {
예외가 발생할 것으로 예상되는 코드
}
catch (e: SomeException) {
예외가 있는 경우 실행되는 코드
-> 갑자기 종료되는 대신 복구하는 역할
}
** 에러 Unable to resolve host "android-kotlin-fun-mars-server.appspot.com":No address associated with hostname
기기에 와이파이 연결이 안되어 있어 발생한 문제였다.
3) JSON 응답 파싱
JSON 응답의 구조는 다음과 같다. Ex) [ { "key": value } ]
- JSON 응답은 대괄호로 표시된 배열이며 배열 안에는 JSON 객체가 들어있다.
- JSON 객체는 중괄호로 묶여있다.
- JSON 객체는 이름-값 쌍의 집합으로, 각 값은 콜론으로 구분된다.
- 이름은 따옴표로 묶여있다.
- 값은 숫자, 문자열, boolean, 배열, 객체(JSON 객체) 또는 null일 수 있다.
JSON 문자열을 Kotlin 객체로 변환하는 Android JSON 파서인 Moshi 라이브러리를 사용해 JSON 응답을 Kotlin 객체로 파싱한다.
- 종속 항목 추가 -> 기존 Retrofit 관련 종속 코드를 다음 코드로 변경
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
// Retrofit with Moshi Converter
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
- 데이터 클래스
Moshi는 Kotlin 데이터 클래스가 있어야 파싱된 결과를 저장할 수 있으므로 데이터 클래스를 만든다.
JSON 객체의 키를 변수로 선언한다. Moshi는 JSON을 파싱할 때 이름과 일치하는 키를 찾아 데이터 객체에 적절한 값을 넣어준다.
데이터 클래스에 JSON 객체의 키와 다른 변수 이름을 사용하려면 @Json 주석을 사용한다.
아래 코드에서 데이터 클래스의 변수 이름은 imgSrcUrl이지만 @Json 주석을 통해 명시해뒀기 때문에 JSON 속성 img_src에 매핑할 수 있다.
data class MarsPhoto (
val id:String,
@Json(name = "img_src") val img_src: String
)
- 모시 객체 빌드
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
요약
인터넷 이미지 표시하기
웹 URL을 이용해 사진을 표시하기 하려면 다음 과정이 필요하다.
이미지 다운로드 -> 내부적으로 저장 -> 압축 형식을 Andorid가 사용할 수 있는 이미지로 디코딩
UI가 응답성을 유지하기 위해 이 모든 작업은 우선순위가 낮은 백그라운드 스레드에서 이루어져야 한다.
Coil 라이브러리를 통해 이미지를 다운로드하고 버퍼링 및 디코딩, 캐싱이 가능하다.
종속 항목 추가
// Coil
implementation "io.coil-kt:coil:1.1.1"
결합 어댑터(BindingAdapter)
결합 어댑터는 뷰의 맞춤 속성을 위한 맞춤 setter를 만드는 데 사용되는 주석 처리된 메서드이다.
쉽게 말하면 뷰의 특정 속성에 값을 쉽게 설정하기 위해 사용되는 메서드이다.
아래 어댑터는 imageView에서 imageUrl 속성을 통해 이미지를 출력하는 메서드이다.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
// Load the image in the background using Coil.
}
}
}
- @BindingAdapter 주석은 속성 이름을 매개변수로 사용한다.
- Coil 라이브러리를 사용해 UI 스레드에서 이미지를 로트해 ImageView로 설정한다.
* let 범위 함수
let은 Kotlin의 범위 함수 중 하나로, 호출 체인의 결과에서 함수 하나 이상을 호출하는 데 사용된다.
let 함수는 안전 호출 연산자(.?)와 함께 사용되며 let 코드 블록은 객체가 null이 아닌 경우에만 실행된다.
* DiffUtil: RecyclerView에서 일부 항목이 추가되거나 삭제 및 변경될 때마다 전체 목록 대신 변경된 항목만 새로고침 된다.
* GridView에 데이터 출력
GridView에 BindingAdapter를 통해 PhotoGridAdapter를 MarsPhoto 객체 리스트로 초기화한다.
쉽게 표현하면 BindingAdapter( PhotoGridAdpater[MarsPhoto] ) -> 연결 -> GridView
JSON으로 받아온 데이터인 MarsPhoto 데이터가 변경되면 결합 어댑터가 자동으로 호출돼 UI에 반영된다.
* ViewModel에 상태 추가 -> 상태별 UI를 지정해 UX 개선
코틀린에서 enum은 상수 집합을 보유할 수 있는 데이터 유형이다.
아래와 같이 크래스 정의 앞에 키워드 enum을 붙여 정의한다.
객체 참조는 클래스 이름 뒤에 점 연산자와 상수 이름을 사용해 접근할 수 있다.
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
val direction = Direction.NORTH