diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..639c779
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..c224ad5
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..02a7102
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5791375..30651aa 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,12 +1,18 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
+ id("kotlin-kapt")
+ id("dagger.hilt.android.plugin")
}
android {
namespace = "com.example.bcsd_android_2025_1"
compileSdk = 34
+ buildFeatures {
+ viewBinding = true
+ }
+
defaultConfig {
applicationId = "com.example.bcsd_android_2025_1"
minSdk = 26
@@ -42,7 +48,22 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.paging.common.android)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
+ implementation("com.squareup.retrofit2:retrofit:2.9.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.9.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+ implementation("androidx.paging:paging-runtime:3.3.0")
+ implementation("com.google.dagger:hilt-android:2.48")
+ kapt("com.google.dagger:hilt-compiler:2.48")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+ implementation("androidx.recyclerview:recyclerview:1.3.1")
+}
+
+
+kapt {
+ correctErrorTypes = true
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4c80941..9e8646d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,10 @@
+
+
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt
deleted file mode 100644
index 3ffa0eb..0000000
--- a/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.example.bcsd_android_2025_1
-
-import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-
-class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/AppModule.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/AppModule.kt
new file mode 100644
index 0000000..ca56f4d
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/AppModule.kt
@@ -0,0 +1,30 @@
+package com.example.bcsd_android_2025_1.app
+
+import com.example.bcsd_android_2025_1.data.GithubApi
+import com.example.bcsd_android_2025_1.domain.GithubRepository
+import com.example.bcsd_android_2025_1.domain.GithubRepositoryImpl
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppModule {
+ @Provides
+ fun provideGitHubApi(): GithubApi {
+ return Retrofit.Builder()
+ .baseUrl("https://api.github.com/")
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ .create(GithubApi::class.java)
+ }
+
+ @Provides
+ fun provideGitHubRepository(api: GithubApi): GithubRepository{
+ return GithubRepositoryImpl(api)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/MainActivity.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/MainActivity.kt
new file mode 100644
index 0000000..5bc594c
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/MainActivity.kt
@@ -0,0 +1,63 @@
+package com.example.bcsd_android_2025_1.app
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import androidx.core.widget.doOnTextChanged
+import androidx.lifecycle.lifecycleScope
+import androidx.paging.LoadState
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.example.bcsd_android_2025_1.databinding.ActivityMainBinding
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class MainActivity : AppCompatActivity() {
+ private val viewModel by viewModels()
+ private lateinit var binding: ActivityMainBinding
+ private val adapter = RepositoryAdapter {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.htmlUrl)))
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+
+ binding.repositoryRecyclerview.layoutManager = LinearLayoutManager(this)
+ binding.repositoryRecyclerview.adapter = adapter
+
+ lifecycleScope.launch {
+ viewModel.repositories.collectLatest { pagingData ->
+ adapter.submitData(pagingData)
+ }
+ }
+
+ lifecycleScope.launch {
+ adapter.loadStateFlow.collectLatest { loadState ->
+ binding.progressBar.isVisible = loadState.refresh is LoadState.Loading
+ }
+ }
+
+ binding.searchEdittext.doOnTextChanged { text, _, _, _ ->
+ viewModel.onQueryChanged(text.toString())
+ }
+
+ lifecycleScope.launch {
+ viewModel.repositories.collectLatest { pagingData ->
+ binding.progressBar.isVisible = true
+ adapter.submitData(pagingData = pagingData)
+ binding.progressBar.isVisible = false
+ }
+ }
+
+ adapter.addLoadStateListener {
+ binding.progressBar.isVisible = it.refresh is LoadState.Loading
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/MainViewModel.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/MainViewModel.kt
new file mode 100644
index 0000000..f299f5a
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/MainViewModel.kt
@@ -0,0 +1,32 @@
+package com.example.bcsd_android_2025_1.app
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.paging.cachedIn
+import com.example.bcsd_android_2025_1.domain.SearchRepositoryUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import javax.inject.Inject
+
+@HiltViewModel
+class MainViewModel @Inject constructor(
+ private val searchUseCase: SearchRepositoryUseCase
+) : ViewModel() {
+
+ private val _query = MutableStateFlow("")
+ val query: StateFlow = _query
+
+ val repositories = _query
+ .debounce(300)
+ .distinctUntilChanged()
+ .flatMapLatest { searchUseCase(it) }
+ .cachedIn(viewModelScope)
+
+ fun onQueryChanged(input: String) {
+ _query.value = input
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/MyGithubApp.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/MyGithubApp.kt
new file mode 100644
index 0000000..c27d5cf
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/MyGithubApp.kt
@@ -0,0 +1,7 @@
+package com.example.bcsd_android_2025_1.app
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class MyGithubApp : Application()
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/RepositoryAdapter.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/RepositoryAdapter.kt
new file mode 100644
index 0000000..3bd0391
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/RepositoryAdapter.kt
@@ -0,0 +1,36 @@
+package com.example.bcsd_android_2025_1.app
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.paging.PagingDataAdapter
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.example.bcsd_android_2025_1.data.Repository
+import com.example.bcsd_android_2025_1.databinding.ItemRepositoryBinding
+
+class RepositoryAdapter (
+ private val onClick: (Repository) -> Unit
+) : PagingDataAdapter(diffUtil) {
+
+ inner class ViewHolder(private val binding: ItemRepositoryBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ fun bind(repo: Repository) {
+ binding.repositoryNameTextview.text = repo.name
+ binding.root.setOnClickListener { onClick(repo) }
+ }
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ getItem(position)?.let { holder.bind(it) }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+ ViewHolder(ItemRepositoryBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+
+ companion object {
+ val diffUtil = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(old: Repository, new: Repository) = old.id == new.id
+ override fun areContentsTheSame(old: Repository, new: Repository) = old == new
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/data/GithubApi.kt b/app/src/main/java/com/example/bcsd_android_2025_1/data/GithubApi.kt
new file mode 100644
index 0000000..b8c036a
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/data/GithubApi.kt
@@ -0,0 +1,13 @@
+package com.example.bcsd_android_2025_1.data
+
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+interface GithubApi {
+ @GET("search/repositories")
+ suspend fun searchRepositories(
+ @Query("q") query:String,
+ @Query("page") page:Int,
+ @Query("per_page") perPage:Int = 30
+ ):SearchResponseDto
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/data/Repository.kt b/app/src/main/java/com/example/bcsd_android_2025_1/data/Repository.kt
new file mode 100644
index 0000000..1d0c5d9
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/data/Repository.kt
@@ -0,0 +1,7 @@
+package com.example.bcsd_android_2025_1.data
+
+data class Repository(
+ val id: Long,
+ val name: String,
+ val htmlUrl: String,
+)
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/data/RepositoryDto.kt b/app/src/main/java/com/example/bcsd_android_2025_1/data/RepositoryDto.kt
new file mode 100644
index 0000000..8b60cee
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/data/RepositoryDto.kt
@@ -0,0 +1,9 @@
+package com.example.bcsd_android_2025_1.data
+
+import com.google.gson.annotations.SerializedName
+
+data class RepositoryDto (
+ val id: Long,
+ val name: String,
+ @SerializedName("html_url") val htmlUrl: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/data/RepositoryMapper.kt b/app/src/main/java/com/example/bcsd_android_2025_1/data/RepositoryMapper.kt
new file mode 100644
index 0000000..6d862d8
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/data/RepositoryMapper.kt
@@ -0,0 +1,5 @@
+package com.example.bcsd_android_2025_1.data
+
+fun RepositoryDto.toDomain(): Repository {
+ return Repository(id, name, htmlUrl)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/data/SearchResponseDto.kt b/app/src/main/java/com/example/bcsd_android_2025_1/data/SearchResponseDto.kt
new file mode 100644
index 0000000..7d15e99
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/data/SearchResponseDto.kt
@@ -0,0 +1,8 @@
+package com.example.bcsd_android_2025_1.data
+
+import com.google.gson.annotations.SerializedName
+
+data class SearchResponseDto (
+ @SerializedName("items")
+ val items: List
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubPagingSource.kt b/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubPagingSource.kt
new file mode 100644
index 0000000..295fa3b
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubPagingSource.kt
@@ -0,0 +1,25 @@
+package com.example.bcsd_android_2025_1.domain
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.example.bcsd_android_2025_1.data.GithubApi
+import com.example.bcsd_android_2025_1.data.RepositoryDto
+
+class GithubPagingSource(private val api: GithubApi, private val query: String) : PagingSource() {
+ override suspend fun load(params: LoadParams): LoadResult {
+ return try {
+ val page = params.key ?: 1
+ val response = api.searchRepositories(query, page)
+ val items = response.items
+ LoadResult.Page(
+ data = items,
+ prevKey = if (page == 1) null else page - 1,
+ nextKey = if (items.isEmpty()) null else page + 1
+ )
+ } catch (e: Exception) {
+ LoadResult.Error(e)
+ }
+ }
+
+ override fun getRefreshKey(state: PagingState): Int? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubRepository.kt b/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubRepository.kt
new file mode 100644
index 0000000..ec1989e
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubRepository.kt
@@ -0,0 +1,9 @@
+package com.example.bcsd_android_2025_1.domain
+
+import androidx.paging.PagingData
+import com.example.bcsd_android_2025_1.data.Repository
+import kotlinx.coroutines.flow.Flow
+
+interface GithubRepository {
+ fun searchRepositories(query: String): Flow>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubRepositoryImpl.kt b/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubRepositoryImpl.kt
new file mode 100644
index 0000000..c36858e
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/domain/GithubRepositoryImpl.kt
@@ -0,0 +1,22 @@
+package com.example.bcsd_android_2025_1.domain
+
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.paging.map
+import com.example.bcsd_android_2025_1.data.GithubApi
+import com.example.bcsd_android_2025_1.data.Repository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+import com.example.bcsd_android_2025_1.data.toDomain
+
+
+class GithubRepositoryImpl@Inject constructor(private val api: GithubApi) : GithubRepository{
+ override fun searchRepositories(query: String): Flow> {
+ return Pager(
+ config = PagingConfig(pageSize = 30),
+ pagingSourceFactory = { GithubPagingSource(api, query) }
+ ).flow.map { pagingData -> pagingData.map { it.toDomain() } }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/domain/SearchRepositoryUseCase.kt b/app/src/main/java/com/example/bcsd_android_2025_1/domain/SearchRepositoryUseCase.kt
new file mode 100644
index 0000000..36acb52
--- /dev/null
+++ b/app/src/main/java/com/example/bcsd_android_2025_1/domain/SearchRepositoryUseCase.kt
@@ -0,0 +1,14 @@
+package com.example.bcsd_android_2025_1.domain
+
+import androidx.paging.PagingData
+import com.example.bcsd_android_2025_1.data.Repository
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class SearchRepositoryUseCase @Inject constructor(
+ private val repository: GithubRepository
+) {
+ operator fun invoke(query: String): Flow> {
+ return repository.searchRepositories(query)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 311f3cb..db2bf7d 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -5,15 +5,40 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".MainActivity">
+ tools:context=".app.MainActivity">
-
+
+
+
+
+ app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginTop="10dp"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_repository.xml b/app/src/main/res/layout/item_repository.xml
new file mode 100644
index 0000000..1f9e7c8
--- /dev/null
+++ b/app/src/main/res/layout/item_repository.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c6c4daf..5331955 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
BCSD_Android_2025-1
+ 레포지토리 검색
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 922f551..8f4d920 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,4 +2,14 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
+}
+
+buildscript {
+ dependencies {
+ classpath("com.google.dagger:hilt-android-gradle-plugin:2.48")
+ }
+ repositories {
+ google()
+ mavenCentral()
+ }
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 283fec9..02e505f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -9,6 +9,8 @@ appcompat = "1.7.0"
material = "1.12.0"
activity = "1.9.3"
constraintlayout = "2.2.1"
+pagingCommonAndroid = "3.3.6"
+roomCommonJvm = "2.7.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -19,6 +21,8 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-paging-common-android = { group = "androidx.paging", name = "paging-common-android", version.ref = "pagingCommonAndroid" }
+androidx-room-common-jvm = { group = "androidx.room", name = "room-common-jvm", version.ref = "roomCommonJvm" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }