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
File renamed without changes.
File renamed without changes.
File renamed without changes.

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

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

File renamed without changes.
1 change: 0 additions & 1 deletion newProject/.idea/misc.xml → Assignment_14/.idea/misc.xml

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

File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("kotlin-kapt")
}

android {
Expand All @@ -10,7 +11,6 @@ android {
defaultConfig {
applicationId = "com.example.assignment"
minSdk = 34
targetSdk = 34
versionCode = 1
versionName = "1.0"

Expand All @@ -27,11 +27,15 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
buildFeatures {
dataBinding = true
viewBinding = true
}
}

Expand All @@ -42,6 +46,13 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)

implementation(libs.retrofit)
implementation(libs.converter.gson)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.kotlinx.coroutines.android)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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:allowBackup="true"
Expand All @@ -13,7 +14,7 @@
android:theme="@style/Theme.Assignment"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".presemtation.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.assignment.data.api

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object GitHubApiClient {
private const val BASE_URL = "https://api.github.com/"

val apiService: GitHubInterface by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GitHubInterface::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.assignment.data.api

import com.example.assignment.data.model.GitHubRepoResponse
import retrofit2.http.GET
import retrofit2.http.Query

interface GitHubInterface {
@GET("search/repositories")
suspend fun searchRepositories(
@Query("q") query: String,
@Query("per_page") perPage: Int = 30,
@Query("page") page: Int
): GitHubRepoResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.assignment.data.model

data class GitHubRepo(
val id: Long,
val name: String,
val html_url: String,
val description: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.assignment.data.model

class GitHubRepoResponse {
val items: List<GitHubRepo> = emptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.example.assignment.presemtation

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.assignment.databinding.ActivityMainBinding
import com.example.assignment.presemtation.main.MainViewModel
import com.example.assignment.presemtation.main.components.RepoAdapter
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

private lateinit var viewModel: MainViewModel

Choose a reason for hiding this comment

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

by viewModels로 선언해주세요!

private lateinit var adapter: RepoAdapter
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

adapter = RepoAdapter { url ->
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}

binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)

viewModel = ViewModelProvider(this)[MainViewModel::class.java]

viewModel.repos.observe(this) { repos ->
adapter.submitList(repos)
}

viewModel.isLoading.observe(this) { isLoading ->
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}

var debouncing: Job? = null
binding.searchInput.addTextChangedListener { text ->
debouncing?.cancel()
debouncing = lifecycleScope.launch {

Choose a reason for hiding this comment

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

repeatOnLifecycle도 알아보셨으면 좋을 것 같습니다!

delay(500)
text?.toString()?.let { query ->
if (query.isNotEmpty()) viewModel.searchRepositories(query)
}
}
}

binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)

if (!recyclerView.canScrollVertically(1)) {
viewModel.searchRepositories(viewModel.repos.value?.lastOrNull()?.name ?: "")
}
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.assignment.presemtation.main

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.assignment.data.api.GitHubApiClient
import com.example.assignment.data.model.GitHubRepo
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {
private val _repos = MutableLiveData<List<GitHubRepo>>()
val repos: LiveData<List<GitHubRepo>> get() = _repos

private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading

private var currentPage = 1
private var query = ""

fun searchRepositories(newQuery: String) {
if (_isLoading.value == true) return

if (query != newQuery) {
query = newQuery

Choose a reason for hiding this comment

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

trim()을 이용해 불필요한 공백 차이로 인해 중복 요청이 발생하는 것을 방지해주세요!

currentPage = 1
_repos.value = emptyList()
}

viewModelScope.launch {
_isLoading.value = true
try {
val response = GitHubApiClient.apiService.searchRepositories(query, page = currentPage)
val currentList = _repos.value ?: emptyList()

_repos.value = currentList + response.items
currentPage++
} catch (e: Exception) {
e.printStackTrace()
}
_isLoading.value = false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.assignment.presemtation.main.components

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.assignment.data.model.GitHubRepo
import com.example.assignment.databinding.ItemRepoBinding

class RepoAdapter(private val onItemClick: (String) -> Unit) :
ListAdapter<GitHubRepo, RepoAdapter.RepoViewHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<GitHubRepo>() {
override fun areItemsTheSame(oldItem: GitHubRepo, newItem: GitHubRepo): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: GitHubRepo, newItem: GitHubRepo): Boolean {
return oldItem == newItem
}

}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoAdapter.RepoViewHolder {
val binding = ItemRepoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return RepoViewHolder(binding)
}

override fun onBindViewHolder(holder: RepoAdapter.RepoViewHolder, position: Int) {
holder.bind(getItem(position))
}

inner class RepoViewHolder(private val binding: ItemRepoBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(repo: GitHubRepo) {
binding.repo = repo
binding.root.setOnClickListener {
onItemClick(repo.html_url)
}
}
}
}
41 changes: 41 additions & 0 deletions Assignment_14/app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<androidx.constraintlayout.widget.ConstraintLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presemtation.MainActivity">

<EditText
android:id="@+id/search_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/search_input_text"
android:padding="15dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_input" />

<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
46 changes: 46 additions & 0 deletions Assignment_14/app/src/main/res/layout/item_repo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data>

<variable
name="repo"
type="com.example.assignment.data.model.GitHubRepo" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="16dp">


<TextView
android:id="@+id/repo_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{repo.name}"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/repo_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="2"
android:text="@{repo.description}"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/repo_title" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
4 changes: 4 additions & 0 deletions Assignment_14/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<resources>
<string name="app_name">assignment</string>
<string name="search_input_text">검색어를 입력해주세요.</string>
</resources>
File renamed without changes.
File renamed without changes.
Loading