android) KaKao Open Api 책 검색 앱 만들기 - 1
KaKao Open Api 책 검색 앱 - 1
사용 기술 스택&아키텍처
- Room
- Retrofit2
- Navigation
- Flow
- DataStore
- Paging
- WorkManager
- Hilt
- CleanArchitector
https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
카카오 OEPN API를 활용하여 책검색 앱을 만들었다.
API 처리
ㅁ API는 고유 값으로 각 개인마다 하나의 각기 다른 API를 부여 받는다. 따라서 Android Studio내에 API값을 넣어 주면 Secret하게 보관해야 한다. Android는 Secrets Gradle Plugin for Android을 지원 한다.
plugins{
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
// Version관리 다른 사람들이 볼 수 없으므로 API KEY 값등을 넣어준다.
ㅁ Util-Constants Object
package com.example.booksearchapp.util
import com.example.booksearchapp.BuildConfig
object Constants {
const val BASE_URL = "https://dapi.kakao.com/"
const val API_KEY = BuildConfig.bookApiKey
const val SEARRCH_BOOK_DELAY = 100L
const val DATASTORE_NAME = "preferences_datastore"
const val PAGING_SIZE = 15
}
위의 Object는 고유 값을 넣어 주었다. 즉 const를 통해 바뀌지 않는 상수값들을 따로 지정해주어 Objet 참조가 가능하다.
API_KEY를 BuildConfig를 통해 LocalProperty에 저장을 해주었다. 이러면 다른 사람들이 절대 볼 수 없게 된다.
ㅁ local.properties
sdk.dir=/Users/test/Library/Android/sdk
bookApiKey= sample
ㅁ BookSearchApi
interface BookSearchApi {
@Headers("Authorization: KakaoAK $API_KEY")
@GET("v3/search/book")
suspend fun searchBooks(
@Query("query") query: String,
@Query("sort") sort: String,
@Query("page") page: Int,
@Query("size") size: Int,
): Response<SearchResponse>
}
interface 형태로 BookSearchApi를 만들어준다. 이렇게 API_KEY를 사용 할 수 있다.
모델 생성
제일 첫째로 책의 정보가 담긴 Model을 생성해야 한다.
- 카카오 책검색 API Request / Response
위와같은 Json 형식을 Model로 바꿔준다.
- 여기서 meta / documents의 Response를 받기 위해 일단 Model을 만들어 준다. 모델을 만드는 방법은 저번에와 Json To Kotlin플러그인을 이용하여 쉽게 만들어 줄 수 있다. 우선 Response Json을 복사하여 아래와 같이 PlugIn에 붙여준다.
↓
이 와같이 자동 적으로 모델들이 생성이 된다.
ㅁ Room DB를 사용해야 할 것이기 때문에 Book Entity를 Room에 맞게 TableName을 지정해 주고 Primary Key를 설정해 준다.
@Parcelize
@JsonClass(generateAdapter = true)
@Entity(tableName = "books")
data class Book(
val authors: List<String>,
val contents: String, // 실습 단계별 명쾌한 멘토링! 대학이나 IT 전문 학원의 안드로이드 프로그래밍 과목 수강생을 대상으로 합니다. 기본적인 프로그래밍을 접해본 독자라면 안드로이드에 꼭 필요한 Java 기초부터 안드로이드 앱 개발까지 학습 가능하도록 구성되어 있습니다. 안드로이드 프로그래밍을 하면서 부딪힐 수 있는 다양한 오류나 실수까지 친절하게 안내하여 시행착오 없이 안드로이드를 빠르게 정복할 수 있습니다. ※ 본 도서는 대학 강의용 교재로 개발되었으므로 연습문제 해답
val datetime: String, // 2022-01-22T00:00:00.000+09:00
@PrimaryKey(autoGenerate = false)
val isbn: String, // 1156645840 9791156645849
val price: Int, // 33000
val publisher: String, // 한빛아카데미
@ColumnInfo(name = "sale_price")
@field:Json(name = "sale_price")
val salePrice: Int, // 33000
val status: String, // 정상판매
val thumbnail: String, // https://search1.kakaocdn.net/thumb/R120x174.q85/?fname=http%3A%2F%2Ft1.daumcdn.net%2Flbook%2Fimage%2F5958356%3Ftimestamp%3D20220411163627
val title: String, // Android Studio를 활용한 안드로이드 프로그래밍(7판)(IT CookBook)
val translators: List<String>,
val url: String // https://search.daum.net/search?w=bookpage&bookId=5958356&q=Android+Studio%EB%A5%BC+%ED%99%9C%EC%9A%A9%ED%95%9C+%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C+%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%287%ED%8C%90%29%28IT+CookBook%29
) : Parcelable
ㅁ Parcelize를 해준 이유는 Navigation을 통해 Book Data를 intent로 다른 페이지에 넘길 수 있게 해주기 위해 처리해주었다.
MainActivity UI 구성
- BottomNavigationBar
3개의 BottomNavi를 설정해야한다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
private lateinit var navController: NavController
private lateinit var appBarConfiguration: AppBarConfiguration // AppBar 타이틀 변경을 위해 Navi마다 다른 타이틀 !
override
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupJetpackNavigation()
}
private fun setupJetpackNavigation() {
val host =
supportFragmentManager.findFragmentById(R.id.booksearch_nav_host_fragment) as NavHostFragment?
?: return
navController = host.navController
binding.bottomNavi.setupWithNavController(navController)
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.fragment_search,
R.id.fragment_favorite,
R.id.fragment_settings // 모두가 TopLevel로 지정돼서 뒤로가기 없어짐
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() // 뒤로가기 버튼 활ㄹ성화
}
}
ㅁ 과정은 간단하다. 우선 NavController를 가져온다.
//navi
def nav_version = "2.5.1"
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
ㅁ App 수준 BuildGradle에 nvigation-fragment를 추가하면 가능하다. 그이후
<FrameLayout
android:id="@+id/frame_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottomNavi"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/booksearch_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/booksearch_nav_graph" />
</FrameLayout>
ㅁ Frame을 변경해주는 함수를 작성하면 적용이 된다.
Hilt 의존성 주입
ㅁ 모델과 API가 설정 됐다면 API 데이터를 불러올 수 있다. 여기서는 의존성 주입인 Hilt 모듈을 사용하여 Retrofit, Room, WorkManager를 정의 해준다.
우선 가장 최상 단에 @HiltAndroidApp을 설정해준다.
@HiltAndroidApp
class BookSearchApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
}
여기서는 WorkManager를 사용하였기 때문에 Configuration을 추가해서 Build() 시켜준다.
여기서 WorkManager란
- 앱이 종료되거나 기기가 다시 시작되어도 실행 예정인 지연 가능한 비동기 작업을 쉽게 예약할 수 있게 해준다
- 안드로이드의 백그라운드 작업을 처리하는 방법 중 하나, Android Jetpack 아키텍처의 구성 요소 중 하나이다
- 하나의 코드로 API Level 마다 비슷한 동작을 보장한다
그 이후 Main Activity를 @AndroidEntryPoint로 지정하여 의존성 주입이 가능하게 설정해 준다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
그리고 가장 중요한 Module을 생성 해준다. Module에 구현된 함수들은 모두 Hilt를 이용한 의존성 주입이 가능하기 때문에 바로 사용할 수 있다.
ㅁ AppModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
// Retrofit
@Singleton
@Provides
fun provideOKHttpClient(): OkHttpClient {
val httpLoggingInterceptor =
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
return OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.build()
}
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create())
.client(okHttpClient)
.baseUrl(BASE_URL)
.build()
}
@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): BookSearchApi {
return retrofit.create(BookSearchApi::class.java)
}
//Room
@Singleton
@Provides
fun provideBookSearchDatabase(@ApplicationContext context: Context): BookSearchDatabase =
Room.databaseBuilder(
context.applicationContext,
BookSearchDatabase::class.java,
"favorite-books"
).build()
@Singleton
@Provides
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStore<androidx.datastore.preferences.core.Preferences> =
PreferenceDataStoreFactory.create(
produceFile = { context.preferencesDataStoreFile(DATASTORE_NAME) }
)
// WorkManager
@Singleton
@Provides
fun provideWorkManager(@ApplicationContext context: Context): WorkManager =
WorkManager.getInstance(context)
@Singleton
@Provides
fun provideCacheDeleteResult(): String = "Cache has deleted by Hilt"
}
installIn()을 통해 구현 해준 것은 Retrofit, Room, WorkManager가 있다. fragment마다 따로 함수를 호출, 객체 생성이 필요 없이 hilt di를 통해 바로 사용이 가능하다.
BookSearchRepository 작성
CleanArchitector의 기본 구조는 Presentation - Domain - Data이다. 여기서 Data를 다루기 위해서는 Repository로 접근 해서 사용하면 된다.
interface BookSearchRepository {
suspend fun searchBooks(
query: String,
sort: String,
page: Int,
size: Int
): Response<SearchResponse>
suspend fun insertBooks(book: Book)
suspend fun deleteBooks(book: Book)
fun getFavortieBooks(): Flow<List<Book>>
// DataStore
suspend fun saveSortMode(mode: String)
suspend fun getSortMode(): Flow<String>
// Paging
fun getFavoritePagingBooks(): Flow<PagingData<Book>>
fun searchBooksPaging(query: String, sort: String): Flow<PagingData<Book>>
// WorkManager
suspend fun saveCacheDeleteMode(mode: Boolean)
suspend fun getCacheDeleteMode(): Flow<Boolean>
}
RoomDB에 필요한 insert, delete, Read의 함수들을 interface정의 시키고, Datastore, paging, workmanager 또한 interface형태로 놔준다.
@Singleton
class BookSearchRespositoryImp @Inject constructor(
private val db: BookSearchDatabase,
private val api: BookSearchApi,
private val dataStore: DataStore<Preferences>
) :
BookSearchRepository {
override suspend fun searchBooks(
query: String,
sort: String,
page: Int,
size: Int
): Response<SearchResponse> {
return api.searchBooks(query, sort, page, size)
}
override suspend fun insertBooks(book: Book) {
db.bookSearchDao().insertBook(book)
}
override suspend fun deleteBooks(book: Book) {
db.bookSearchDao().deleteBook(book)
}
override fun getFavortieBooks(): Flow<List<Book>> {
return db.bookSearchDao().getFavoriteBooks()
}
private object PreferenceKeys {
val SORT_MODE = stringPreferencesKey("sort_mode")
val CACHE_DELETE_MODE = booleanPreferencesKey("cache_delete_mode")
}
override suspend fun saveSortMode(mode: String) {
dataStore.edit { prefs -> //전달 받은 모드값을 edit 에서 접근
prefs[SORT_MODE] = mode
}
}
override suspend fun getSortMode(): Flow<String> {
return dataStore.data
.catch { exception ->
if (exception is IOException) {
exception.printStackTrace()
emit(emptyPreferences())
} else {
throw exception
}
}.map { prefs ->
prefs[SORT_MODE] ?: Sort.ACCURACY.value
}
}
override fun getFavoritePagingBooks(): Flow<PagingData<Book>> {
val pagingSourceFactory = { db.bookSearchDao().getFavoritePaginfBooks() }
return Pager(
config = PagingConfig(
pageSize = PAGING_SIZE,
enablePlaceholders = false,
maxSize = PAGING_SIZE * 3
),
pagingSourceFactory = pagingSourceFactory
).flow
}
override fun searchBooksPaging(query: String, sort: String): Flow<PagingData<Book>> {
val pagingSourceFactory = { BookSearchPagingSource(api, query, sort) }
return Pager(
config = PagingConfig(
pageSize = PAGING_SIZE,
enablePlaceholders = false,
maxSize = PAGING_SIZE * 3
),
pagingSourceFactory = pagingSourceFactory
).flow
}
override suspend fun saveCacheDeleteMode(mode: Boolean) {
dataStore.edit { prefs ->
prefs[CACHE_DELETE_MODE] = mode
}
}
override suspend fun getCacheDeleteMode(): Flow<Boolean> {
return dataStore.data
.catch { exception ->
if (exception is IOException) {
exception.printStackTrace()
emit(emptyPreferences())
} else {
throw exception
}
}
.map { prefs ->
prefs[CACHE_DELETE_MODE] ?: false
}
}
}
ㅁ db, api, datastore 의존성을 주입 받아 interface에 명세한 함수를 imp respository를 overide한 클래스에 구현 해준다.
정리
이번시간에는 Hilt의존성 주입, API, Model DATA 다루기, Retrofit2 api 가져오기를 구현을 다시 리팩토링해봤다.
다음은 WorkManager, Paging, RecyclerView의 구현 과정을 설명 할것!
<!--마무리 끝->