-
Notifications
You must be signed in to change notification settings - Fork 1
[김예란_Android] 9주차 과제 제출 #9
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?
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.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import retrofit2.Retrofit | ||
| import retrofit2.converter.gson.GsonConverterFactory | ||
| import retrofit2.http.GET | ||
| import retrofit2.http.Path | ||
| import retrofit2.http.Query | ||
| import retrofit2.create | ||
|
|
||
| interface GitHubApiService { | ||
|
|
||
| @GET("search/users") | ||
| suspend fun searchUsers( | ||
| @Query("q") query: String, | ||
| @Query("page") page: Int, | ||
| @Query("per_page") perPage: Int | ||
| ): UserSearchResponse | ||
|
|
||
| @GET("users/{username}/repos") | ||
| suspend fun getUserRepositories( | ||
| @Path("username") username: String, | ||
| @Query("page") page: Int, | ||
| @Query("per_page") perPage: Int | ||
| ): List<Repository> | ||
|
|
||
| companion object { | ||
| fun create(): GitHubApiService { | ||
| val retrofit = Retrofit.Builder() | ||
| .baseUrl("https://api.github.com/") | ||
| .addConverterFactory(GsonConverterFactory.create()) | ||
| .build() | ||
|
|
||
| return retrofit.create() | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,84 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import android.os.Bundle | ||
| import androidx.activity.enableEdgeToEdge | ||
| import android.content.Intent | ||
| import android.util.Log | ||
| import androidx.lifecycle.ViewModelProvider | ||
| import androidx.appcompat.app.AppCompatActivity | ||
| import androidx.core.view.ViewCompat | ||
| import androidx.core.view.WindowInsetsCompat | ||
| import androidx.core.widget.addTextChangedListener | ||
| import androidx.recyclerview.widget.LinearLayoutManager | ||
| import androidx.recyclerview.widget.RecyclerView | ||
| import com.example.android_25_2.databinding.ActivityMainBinding | ||
|
|
||
| class MainActivity : AppCompatActivity() { | ||
| private lateinit var binding: ActivityMainBinding | ||
| private lateinit var viewModel: UserSearchViewModel | ||
| private lateinit var adapter: UserAdapter | ||
| companion object { | ||
| const val user_name = "username" | ||
| } | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
| enableEdgeToEdge() | ||
| setContentView(R.layout.activity_main) | ||
| ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> | ||
| Log.d("MainActivity", "🚀 onCreate 시작") | ||
|
Collaborator
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. 불필요한 log 같습니다. |
||
| binding = ActivityMainBinding.inflate(layoutInflater) | ||
| setContentView(binding.root) | ||
|
|
||
| ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> | ||
| val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) | ||
| insets | ||
| } | ||
|
|
||
| viewModel = ViewModelProvider(this)[UserSearchViewModel::class.java] | ||
|
|
||
| setupRecyclerView() | ||
| setupSearchView() | ||
| observeViewModel() | ||
| Log.d("MainActivity", "✅ onCreate 완료") | ||
| } | ||
|
|
||
| private fun setupRecyclerView() { | ||
| Log.d("MainActivity", "📦 RecyclerView 설정") | ||
| adapter = UserAdapter { username -> | ||
| val intent = Intent(this, RepositoryActivity::class.java) | ||
| intent.putExtra(user_name, username) | ||
| startActivity(intent) | ||
| } | ||
| binding.recyclerView.adapter = adapter | ||
| binding.recyclerView.layoutManager = LinearLayoutManager(this) | ||
| binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||
| override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||
| super.onScrolled(recyclerView, dx, dy) | ||
|
|
||
| val layoutManager = recyclerView.layoutManager as LinearLayoutManager | ||
| val lastVisible = layoutManager.findLastVisibleItemPosition() | ||
| val totalCount = layoutManager.itemCount | ||
|
|
||
| if (lastVisible >= totalCount - 5) { | ||
| viewModel.loadMore() | ||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| private fun setupSearchView() { | ||
| Log.d("MainActivity", "🔍 SearchView 설정") | ||
| binding.editSearch.addTextChangedListener { text -> | ||
| val query = text.toString() | ||
| Log.d("MainActivity", "⌨️ 텍스트 변경: '$query'") | ||
| viewModel.searchUsers(query) | ||
| } | ||
| Log.d("MainActivity", "✅ SearchView 리스너 등록 완료") | ||
| } | ||
|
|
||
| private fun observeViewModel() { | ||
| Log.d("MainActivity", "👀 ViewModel 관찰 시작") | ||
| viewModel.users.observe(this) { users -> | ||
| Log.d("MainActivity", "📱 사용자 목록 업데이트: ${users.size}개") | ||
| adapter.submitList(users) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| data class Repository( | ||
| val id: Long, | ||
| val full_name: String, | ||
| val description: String?, | ||
| val html_url: String | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import android.content.Intent | ||
| import android.net.Uri | ||
| import android.os.Bundle | ||
| import androidx.appcompat.app.AppCompatActivity | ||
| import androidx.lifecycle.ViewModelProvider | ||
| import androidx.recyclerview.widget.LinearLayoutManager | ||
| import androidx.recyclerview.widget.RecyclerView | ||
| import com.example.android_25_2.MainActivity.Companion.user_name | ||
| import com.example.android_25_2.databinding.ActivityRepositoryBinding | ||
|
|
||
| class RepositoryActivity : AppCompatActivity() { | ||
|
|
||
| private lateinit var binding: ActivityRepositoryBinding | ||
| private lateinit var viewModel: RepositoryViewModel | ||
| private lateinit var adapter: RepositoryAdapter | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| binding = ActivityRepositoryBinding.inflate(layoutInflater) | ||
| setContentView(binding.root) | ||
| viewModel = ViewModelProvider(this)[RepositoryViewModel::class.java] | ||
| val username = intent.getStringExtra(user_name) ?: "" | ||
|
|
||
| title = "$username's Repositories" | ||
|
Collaborator
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. texts.xml 도 사용할 수 있습니다. "%1$s's Repositories" 로 선언해서 getString 으로 가져올 수 있죠 |
||
|
|
||
| setupRecyclerView() | ||
| observeViewModel() | ||
|
|
||
| viewModel.loadRepositories(username) | ||
| } | ||
|
|
||
| private fun setupRecyclerView() { | ||
| adapter = RepositoryAdapter { url -> | ||
| val intent = Intent(Intent.ACTION_VIEW) | ||
| intent.data = Uri.parse(url) | ||
| startActivity(intent) | ||
| } | ||
|
|
||
| binding.recyclerView.adapter = adapter | ||
| binding.recyclerView.layoutManager = LinearLayoutManager(this) | ||
| binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { | ||
| override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||
| super.onScrolled(recyclerView, dx, dy) | ||
|
|
||
| val layoutManager = recyclerView.layoutManager as LinearLayoutManager | ||
| val lastVisible = layoutManager.findLastVisibleItemPosition() | ||
| val totalCount = layoutManager.itemCount | ||
|
|
||
| if (lastVisible >= totalCount - 5) { | ||
| viewModel.loadMore() | ||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| private fun observeViewModel() { | ||
| viewModel.repositories.observe(this) { repositories -> | ||
| adapter.submitList(repositories) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import android.view.LayoutInflater | ||
| import android.view.ViewGroup | ||
| import androidx.recyclerview.widget.DiffUtil | ||
| import androidx.recyclerview.widget.ListAdapter | ||
| import com.example.android_25_2.databinding.ItemRepositoryBinding | ||
|
|
||
| class RepositoryAdapter( | ||
| private val onItemClick: (String) -> Unit | ||
| ) : ListAdapter<Repository, RepositoryViewHolder>(RepositoryDiffCallback()) { | ||
|
Collaborator
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. GOOD |
||
|
|
||
| override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepositoryViewHolder { | ||
| val inflater = LayoutInflater.from(parent.context) | ||
| val binding = ItemRepositoryBinding.inflate(inflater, parent, false) | ||
| return RepositoryViewHolder(binding, onItemClick) | ||
| } | ||
|
|
||
| override fun onBindViewHolder(holder: RepositoryViewHolder, position: Int) { | ||
| val repository = getItem(position) | ||
| holder.bind(repository) | ||
| } | ||
| } | ||
|
|
||
| class RepositoryDiffCallback : DiffUtil.ItemCallback<Repository>() { | ||
| override fun areItemsTheSame(oldItem: Repository, newItem: Repository): Boolean { | ||
| return oldItem.id == newItem.id | ||
| } | ||
| override fun areContentsTheSame(oldItem: Repository, newItem: Repository): Boolean { | ||
| return oldItem == newItem | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import androidx.recyclerview.widget.RecyclerView | ||
| import com.example.android_25_2.databinding.ItemRepositoryBinding | ||
|
|
||
| class RepositoryViewHolder( | ||
| private val binding: ItemRepositoryBinding, | ||
| private val onItemClick: (String) -> Unit | ||
| ) : RecyclerView.ViewHolder(binding.root) { | ||
|
|
||
| fun bind(repository: Repository) { | ||
| binding.repository = repository | ||
| binding.executePendingBindings() | ||
| binding.root.setOnClickListener { | ||
| onItemClick(repository.html_url) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| import android.util.Log | ||
| import androidx.lifecycle.LiveData | ||
| import androidx.lifecycle.MutableLiveData | ||
| import androidx.lifecycle.ViewModel | ||
| import androidx.lifecycle.viewModelScope | ||
| import kotlinx.coroutines.launch | ||
|
|
||
| class RepositoryViewModel : ViewModel() { | ||
|
|
||
| private val apiService = GitHubApiService.create() | ||
| private val _repositories = MutableLiveData<List<Repository>>() | ||
| val repositories: LiveData<List<Repository>> = _repositories | ||
|
Collaborator
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. 이렇게 구조화 하는 이뉴는 뭘까요? |
||
| private var currentPage = 1 | ||
| private var currentUsername = "" | ||
|
|
||
| fun loadRepositories(username: String) { | ||
| currentUsername = username | ||
| currentPage = 1 | ||
|
|
||
| viewModelScope.launch { | ||
| try { | ||
| val repos = apiService.getUserRepositories(username, currentPage, 30) | ||
| _repositories.value = repos | ||
| } catch (e: Exception) { | ||
| _repositories.value = emptyList() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fun loadMore() { | ||
| if (currentUsername.isEmpty()) { | ||
| return | ||
| } | ||
|
|
||
| viewModelScope.launch { | ||
| try { | ||
| currentPage += 1 | ||
| val repos = apiService.getUserRepositories(currentUsername, currentPage, 30) | ||
| val currentList = _repositories.value ?: emptyList() | ||
| val newList = currentList + repos | ||
|
|
||
| _repositories.value = newList | ||
| } catch (e: Exception) { | ||
| currentPage -= 1 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.example.android_25_2 | ||
|
|
||
| data class User( | ||
| val id: Long, | ||
| val login: String, | ||
| val html_url: String | ||
| ) | ||
|
|
||
| data class UserSearchResponse( | ||
| val total_count: Int, | ||
| val items: List<User> | ||
| ) |
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.
개인적으로는 따로 빼두는게 더 좋을 것 같긴 합니다.
interface 내부에 실제 create 부분을 두는건 어색하네요