-
Notifications
You must be signed in to change notification settings - Fork 0
15주차 과제 PR입니다. #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
15주차 과제 PR입니다. #12
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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 | ||
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
| } | ||
| } | ||
| } |
| 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> |
| 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> |
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by viewModels로 선언해주세요!