[Android - Kotlin] Jetpack Compose - 8

민채·2024년 2월 27일
0

Android - Codelab

목록 보기
8/10

Retrofit

  • 웹 서비스의 콘텐츠를 기반으로 앱의 네트워크 API를 만듦
  • 웹 서비스에서 데이터를 가져오고 데이터를 디코딩하여 객체 형식(예: String)으로 반환하는 방법을 알고 있는 별도의 변환기 라이브러리를 통해 데이터를 라우팅함
  • Retrofit에는 XML 및 JSON과 같이 많이 사용되는 데이터 형식을 위한 지원이 내장되어 있음

Retrofit 의존성 추가

build.gradle.kts (Module :app)의 dependencies 섹션에 다음 줄을 추가

// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0"`

// Retrofit with Scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
  • 첫 번째 항목은 Retrofit2 라이브러리 자체를 위한 것
  • 두 번째 항목은 Retrofit 스칼라 변환기를 위한 것 -> Retrofit2는 Retrofit 라이브러리의 업데이트 버전으로 스칼라 변환기를 사용하면 Retrofit이 JSON 결과를 String으로 반환할 수 있음

Retrofit 서비스 만들기

  • network 패키지를 따로 만들어서 사용

API를 요청할 URL 추가

private const val BASE_URL =
    "https://android-kotlin-fun-mars-server.appspot.com"

Retrofit 객체 추가

  • 네트워크에서 받아온 값을 String으로 변경해주는 팩토리가 있어야 함 ->
    팩토리는 네트워크에서 얻은 데이터로 해야할 일을 알려주는 역할
  • String으로 변경하기 위해 ScalarsConverterFactory를 사용
  • baseUrl() 메서드를 사용하여 웹 서비스의 기본 URL을 추가
  • build()를 호출하여 Retrofit 객체를 만듦
private val retrofit = Retrofit.Builder()
    .addConverterFactory(ScalarsConverterFactory.create())
    .baseUrl(BASE_URL)
    .build()

인터페이스를 정의

  • Retrofit이 HTTP 요청을 사용하여 웹 서버와 통신하는 방법을 정의
  • 네트워크 통신을 하면 시간이 딜레이 되기 때문에 suspend 함수로 만듦
interface MarsApiService {
    @GET("photos")
    suspend fun getPhotos(): String
}

객체 선언

  • MarsApi.retrofitService를 호출할 때마다 호출자는 첫 번째 액세스에서 생성된 MarsApiService를 구현하는 싱글톤 Retrofit 객체와 동일한 객체에 액세스
object MarsApi {
    val retrofitService : MarsApiService by lazy {
       retrofit.create(MarsApiService::class.java) // retrofitService 변수를 초기화
    }
}

ViewModel에서 웹 서비스 호출

  • 싱글톤 객체 MarsApi를 사용하여 retrofitService 인터페이스에서 getPhotos() 메서드를 호출
  • 반환된 응답을 listResult에 저장
  • 최근의 웹 요청 상태를 나타내는 변경 가능한 상태 객체인 marsUiState에 결과 저장
// MarsViewModel.kt

fun getMarsPhotos() {
	viewModelScope.launch {
		val listResult = MarsApi.retrofitService.getPhotos()
		marsUiState = listResult
	}
}

인터넷 권한 추가

현재 상태로 앱을 실행하면 아래와 같은 에러가 발생 -> 인터넷 권한을 허용하지 않아서 발생하는 것

    --------- beginning of crash
22803-22865/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
    Process: com.example.android.marsphotos, PID: 22803
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)
...

manifests/AndroidManifest.xml의 <application> 태그 바로 앞에 다음 코드 추가

<uses-permission android:name="android.permission.INTERNET"/>

예외 추가

네트워크가 꺼져있는 경우 아래와 같은 에러가 발생

3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.android.marsphotos, PID: 3302

서버에 연결하는 동안 발생할 수 있는 문제

  • API에 사용된 URL 또는 URI가 잘못됨
  • 서버를 사용할 수 없어 앱을 서버에 연결할 수 없음
  • 네트워크 지연 문제가 있음
  • 기기의 인터넷 연결이 불안정하거나 기기가 인터넷에 연결되지 않음

try-catch문 사용

viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       marsUiState = listResult
   } catch (e: IOException) {

   }
}

상태 UI 추가

  • Loading : 앱이 데이터를 기다리고 있음을 나타냄
  • Success : 웹 서비스에서 데이터를 성공적으로 가져왔음을 나타냄
  • Error : 네트워크 오류 또는 연결 오류를 나타냄

ViewModel 맨 위에 추가

sealed interface MarsUiState {
    data class Success(val photos: String) : MarsUiState
    object Error : MarsUiState
    object Loading : MarsUiState
}

상태 객체 수정

MarsViewModel.kt

var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
  private set
  
fun getMarsPhotos() {
	viewModelScope.launch {
		try {
			val listResult = MarsApi.retrofitService.getPhotos()
			marsUiState = MarsUiState.Success(listResult)
		} catch (e: IOException) {
			marsUiState = MarsUiState.Error
		}
	}
}

HomeScreen.kt

@Composable
fun HomeScreen(
    marsUiState: MarsUiState,
    modifier: Modifier = Modifier,
    contentPadding: PaddingValues = PaddingValues(0.dp),
) {
    when (marsUiState) {
        // marsUiState가 MarsUiState.Success이면 ResultScreen을 호출
        is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
        is MarsUiState.Success -> ResultScreen(
            marsUiState.photos, modifier = modifier.fillMaxWidth()
        )

        is MarsUiState.Error -> ErrorScreen( modifier = modifier.fillMaxSize())
    }
}

// 로드 중 애니메이션 표시
@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
    Image(
        modifier = modifier.size(200.dp),
        painter = painterResource(R.drawable.loading_img),
        contentDescription = stringResource(R.string.loading)
    )
}

// 오류 메시지 표시
@Composable
fun ErrorScreen(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = painterResource(id = R.drawable.ic_connection_error), contentDescription = ""
        )
        Text(text = stringResource(R.string.loading_failed), modifier = Modifier.padding(16.dp))
    }
}

// 데이터 표시
@Composable
fun ResultScreen(photos: String, modifier: Modifier = Modifier) {
    Box(
        contentAlignment = Alignment.Center,
        modifier = modifier
    ) {
        Text(text = photos)
    }
}

실행 화면

Json 응답 파싱

JSON

  • 요청된 데이터는 일반적으로 XML 또는 JSON과 같은 일반적인 데이터 형식 중 하나로 지정
  • 호출할 때마다 구조화된 데이터가 반환되며, 앱은 이 구조에 관해 알아야 응답에서 데이터를 읽을 수 있음

kotlinx.serialization

  • JSON 문자열을 Kotlin 객체로 변환하는 일련의 라이브러리를 제공
  • Retrofit과 호환되도록 커뮤니티에서 개발한 서드 파티 라이브러리

종속 항목 추가

plugins 블록에 kotlinx serialization 플러그인을 추가

id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"

dependencies 섹션에 다음 코드를 추가

// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"

// 해당 코드 삭제
// Retrofit with scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")

// 다음 코드 추가
// Retrofit with Kotlin serialization Converter
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")

데이터 클래스 추가

MarsPhoto.kt

import kotlinx.serialization.Serializable

@Serializable // 직렬화 가능하게 함
data class MarsPhoto(
    val id: String,
    @SerialName(value = "img_src")
    val imgSrc: String
)

MarsApiService.kt

Retrofit 객체 수정

private val retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(BASE_URL)
        .build()

인터페이스 수정

interface MarsApiService {
    @GET("photos")
    suspend fun getPhotos(): List<MarsPhoto>
}

MarsViewModel.kt

fun getMarsPhotos() {
	viewModelScope.launch {
		try {
			val listResult = MarsApi.retrofitService.getPhotos()
			marsUiState = MarsUiState.Success(
				"Success: ${listResult.size} Mars photos retrieved"
			)
		} catch (e: IOException) {
			marsUiState = MarsUiState.Error
		}
	}
}

실행 화면

레이어 분리

  • 코드를 여러 레이어로 분리하면 앱의 확장성이 높아지며 앱이 더 견고해지고 테스트하기 더 쉬워짐
  • 경계가 명확히 정의된 여러 레이어를 사용하면 여러 개발자가 서로에게 부정적인 영향을 주지 않고 동일한 앱을 더 쉽게 작업할 수 있음

아래의 링크를 참고해 MarsPhoto앱을 레이어 분리하면 된다!

이미지 로드 및 표시

현재는 웹 서비스에 연결하여 Gson을 사용해 검색된 객체 수를 가져와 표시하고 있는데 실제 URL을 가지고 이미지를 로드해 볼 것이다.

Coil

  • 이미지를 다운로드하고 버퍼링 및 디코딩하고 캐시할 수 있음
  • 로드하고 표시할 이미지의 URL, 이미지를 실제로 표시하는 AsyncImage 컴포저블 두 가지가 필요

Coil 종속 항목 추가

build.gradle(app) dependencies 섹션에서 다음과 같은 Coil 라이브러리 줄을 추가

// Coil
implementation("io.coil-kt:coil-compose:2.4.0")

AsyncImage 컴포저블 추가

AsyncImage(
        model = ImageRequest.Builder(context = LocalContext.current)
            .data(photo.imgSrc) // url 전달
            .crossfade(true) // 요청이 성공적으로 완료될 때 크로스페이드 애니메이션이 사용되도록 함
            .build(),
        error = painterResource(R.drawable.ic_broken_image),
        placeholder = painterResource(R.drawable.loading_img),
        contentDescription = stringResource(R.string.mars_photo), // 이미지 설명
        contentScale = ContentScale.Crop // 화면 전체를 채움
    )

전체 코드

https://github.com/MinchaeKwon/AndroidCompose/tree/master/Chapter5/MarsPhotos

실행 화면

참조

profile
코딩계의 떠오르는 태양☀️

0개의 댓글