Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .idea/AndroidProjectSystem.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/migrations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions .idea/runConfigurations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
5 changes: 4 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

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

<application
android:name=".app.MyGithubApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -13,7 +16,7 @@
android:theme="@style/Theme.BCSD_Android_20251"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".app.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
14 changes: 0 additions & 14 deletions app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt

This file was deleted.

30 changes: 30 additions & 0 deletions app/src/main/java/com/example/bcsd_android_2025_1/app/AppModule.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<MainViewModel>()
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 비동기 실행으로 알고있습니다 즉 결과값이 다 반환되기 전에 progressbar.visible 이 false 가 되어버립니다.
근데 앞서 adapter loading flow에 따라 progressbar를 보여주고 있어서 progressbar.visible을 쓸 필요가 없는게 아닌지 생각이 드는군요

binding.progressBar.isVisible = false
}
}

adapter.addLoadStateListener {
binding.progressBar.isVisible = it.refresh is LoadState.Loading
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> = _query

val repositories = _query
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { searchUseCase(it) }
.cachedIn(viewModelScope)

fun onQueryChanged(input: String) {
_query.value = input
}
}
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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<Repository, RepositoryAdapter.ViewHolder>(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<Repository>() {
override fun areItemsTheSame(old: Repository, new: Repository) = old.id == new.id
override fun areContentsTheSame(old: Repository, new: Repository) = old == new
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.bcsd_android_2025_1.data

data class Repository(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우진님과 중복이지만 이런 데이터 객체의 경우
data 뿐만이 아닌 presentation 에서도 쓰기에 domain 에 있어야할 거 같습니다. (그리고 보통 domain 에 만듭니다.)

val id: Long,
val name: String,
val htmlUrl: String,
)
Original file line number Diff line number Diff line change
@@ -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,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

명칭이 같으면 어노테이션을 안넣어도 되긴 합니다.
다만 Koin 에서는 확인차 넣고있기에 넣어보시는걸 추천합니다.

@SerializedName("html_url") val htmlUrl: String
)
Loading