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")
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
interface MarsApiService {
@GET("photos")
suspend fun getPhotos(): String
}
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java) // retrofitService 변수를 초기화
}
}
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
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
} catch (e: IOException) {
}
}
Loading
: 앱이 데이터를 기다리고 있음을 나타냄Success
: 웹 서비스에서 데이터를 성공적으로 가져왔음을 나타냄Error
: 네트워크 오류 또는 연결 오류를 나타냄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)
}
}
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")
import kotlinx.serialization.Serializable
@Serializable // 직렬화 가능하게 함
data class MarsPhoto(
val id: String,
@SerialName(value = "img_src")
val imgSrc: String
)
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>
}
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을 가지고 이미지를 로드해 볼 것이다.
build.gradle(app) dependencies 섹션에서 다음과 같은 Coil 라이브러리 줄을 추가
// Coil
implementation("io.coil-kt:coil-compose:2.4.0")
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