

package kr.co.fastcampus.part1.chapter5_9
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import dagger.hilt.android.AndroidEntryPoint
import kr.co.fastcampus.part1.chapter5_9.screen.DetailScreen
import kr.co.fastcampus.part1.chapter5_9.screen.MainScreen
import kr.co.fastcampus.part1.chapter5_9.ui.theme.PokemonTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PokemonTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TopLevel()
}
}
}
}
}
@Composable
fun TopLevel(
navController: NavHostController = rememberNavController(),
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
"Home",
modifier = modifier
) {
composable("Home") {
MainScreen(
onPokemonClick = {
val pokemonId = it.substringAfter("pokemon/")
.substringBefore("/")
.toInt()
navController.navigate("Detail/${pokemonId}")
}
)
}
composable(
"Detail/{pokemonId}",
arguments = listOf(
navArgument("pokemonId") {
type = NavType.IntType
}
)
) {
val pokemonId = it.arguments?.getInt("pokemonId") as Int
DetailScreen(
pokemonId = pokemonId,
onUpButtonClick = {
navController.navigate("Home") {
popUpTo("Home") {
inclusive = true
}
}
}
)
}
}
}
package kr.co.fastcampus.part1.chapter5_9
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface PokeAPI {
@GET("pokemon/")
suspend fun getPokemons(): Response
@GET("pokemon/")
suspend fun getPokemons(
@Query("offset") offset: Int,
@Query("limit") limit: Int
): Response
@GET("pokemon/{pid}/")
suspend fun getPokemon(@Path("pid") pid: Int): PokemonResponse
}
data class Response(
val count: Int,
val previous: String?,
val next: String?,
val results: List<Result>
) {
data class Result(
val url: String,
val name: String
)
}
data class PokemonResponse(
val species: Species,
val sprites: Sprites
) {
data class Species(var name: String)
data class Sprites(var frontDefault: String)
}
package kr.co.fastcampus.part1.chapter5_9
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class PokemonApp : Application()
package kr.co.fastcampus.part1.chapter5_9.di
import kr.co.fastcampus.part1.chapter5_9.PokeAPI
import com.google.gson.FieldNamingPolicy
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Named
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
class AppModules {
@Singleton
@Provides
@Named("API_URI")
fun provideWebAPI(): String = "https://pokeapi.co/api/v2/"
@Singleton
@Provides
fun provideGson(): Gson =
GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create()
@Singleton
@Provides
fun provideConverterFactory(
gson: Gson
): Converter.Factory = GsonConverterFactory.create(gson)
@Singleton
@Provides
fun provideRetrofit(
@Named("API_URI") apiUrl: String,
converterFactory: Converter.Factory
): Retrofit = Retrofit.Builder()
.baseUrl(apiUrl)
.addConverterFactory(converterFactory)
.build()
@Singleton
@Provides
fun provideGithubService(
retrofit: Retrofit
): PokeAPI = retrofit.create(PokeAPI::class.java)
}
package kr.co.fastcampus.part1.chapter5_9.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.compose.AsyncImage
import kr.co.fastcampus.part1.chapter5_9.viewmodel.PokemonViewModel
@Composable
fun DetailScreen(
pokemonId: Int,
onUpButtonClick: () -> Unit,
viewModel: PokemonViewModel = hiltViewModel()
) {
viewModel.getPokemon(pokemonId)
Card(
elevation = 8.dp,
modifier = Modifier.padding(8.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(8.dp)
) {
val result = viewModel.pokemonResult
val pokemonName = result.species.name
Text(pokemonName)
AsyncImage(
model = result.sprites.frontDefault,
contentDescription = pokemonName,
modifier = Modifier.size(100.dp)
)
Button(onClick = onUpButtonClick) {
Text("위로")
}
}
}
}
package kr.co.fastcampus.part1.chapter5_9.screen
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import kr.co.fastcampus.part1.chapter5_9.viewmodel.PokemonViewModel
@Composable
fun MainScreen(
onPokemonClick: (String) -> Unit,
viewModel: PokemonViewModel = hiltViewModel()
) {
val items = viewModel.pokemonList.collectAsLazyPagingItems()
LazyColumn {
items(items, key = { it.url }) {
it?.let {
Card(
elevation = 8.dp,
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(8.dp)
) {
Column {
Text("포케몬: ${it.name}")
Text(
text = it.url,
Modifier.alpha(0.4f)
)
}
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = {
onPokemonClick(it.url)
}) {
Text("보기")
}
}
}
}
}
}
}
package kr.co.fastcampus.part1.chapter5_9.viewmodel
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kr.co.fastcampus.part1.chapter5_9.PokeAPI
import kr.co.fastcampus.part1.chapter5_9.Response
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.*
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kr.co.fastcampus.part1.chapter5_9.PokemonResponse
import javax.inject.Inject
@HiltViewModel
class PokemonViewModel @Inject constructor(
private val pokeAPI: PokeAPI
) : ViewModel() {
val pokemonList: Flow<PagingData<Response.Result>> = getPokemons().cachedIn(viewModelScope)
var pokemonResult by mutableStateOf(
PokemonResponse(
PokemonResponse.Species(""),
PokemonResponse.Sprites("")
)
)
private fun getPokemons(): Flow<PagingData<Response.Result>> = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = true,
prefetchDistance = 5
),
pagingSourceFactory = {
object : PagingSource<Int, Response.Result>() {
override fun getRefreshKey(state: PagingState<Int, Response.Result>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Response.Result> {
try {
val pokemons = if (params.key != null) {
pokeAPI.getPokemons(params.key as Int, params.loadSize)
} else {
pokeAPI.getPokemons()
}
return LoadResult.Page(
data = pokemons.results,
prevKey = pokemons.previous?.substringAfter("offset=")
?.substringBefore("&")?.toIntOrNull(),
nextKey = pokemons.next?.substringAfter("offset=")
?.substringBefore("&")?.toIntOrNull(),
)
} catch (e: Exception) {
Log.e("EEE", "error: $e")
e.printStackTrace()
return LoadResult.Error(e)
}
}
}
}
).flow
fun getPokemon(pokemonId: Int) {
viewModelScope.launch {
pokemonResult = pokeAPI.getPokemon(pokemonId)
}
}
}
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
namespace 'kr.co.fastcampus.part1.chapter5_9'
compileSdk 32
defaultConfig {
applicationId "kr.co.fastcampus.part1.chapter5_9"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.1.1'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation "com.google.dagger:hilt-android:$dagger_version"
kapt "com.google.dagger:hilt-android-compiler:$dagger_version"
implementation "androidx.hilt:hilt-navigation-compose:$hilt_version"
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation 'io.coil-kt:coil:2.2.2'
implementation 'io.coil-kt:coil-compose:2.2.2'
implementation 'androidx.paging:paging-common-ktx:3.1.1'
implementation "androidx.paging:paging-compose:1.0.0-alpha15"
implementation "androidx.navigation:navigation-compose:2.5.3"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.1.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}
buildscript {
ext {
compose_ui_version = '1.1.1'
dagger_version = '2.40.5'
hilt_version = '1.0.0'
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$dagger_version"
}
}
plugins {
id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
}