Skip to content

Commit

Permalink
feat: add followers and following list
Browse files Browse the repository at this point in the history
  • Loading branch information
aurelioklv committed Mar 12, 2024
1 parent 277f140 commit 271d471
Show file tree
Hide file tree
Showing 17 changed files with 389 additions and 90 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Setting Up API Key and Base URL
To ensure seamless integration with the APIs, please follow these steps:

1. Open the project in Android Studio.
2. Locate the `local.properties` file in the root directory of the project.
3. Add the following lines to the `local.properties` file:

```properties
API_KEY=your_api_key_here
BASE_URL=your_base_url_here
```
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ dependencies {

implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.activity.ktx)
implementation(libs.androidx.fragment.ktx)


testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.aurelioklv.githubuser.data.api

import com.aurelioklv.githubuser.data.response.FollowersResponse
import com.aurelioklv.githubuser.data.response.FollowingResponse
import com.aurelioklv.githubuser.data.response.FollowerFollowingItem
import com.aurelioklv.githubuser.data.response.UserResponse
import com.aurelioklv.githubuser.data.response.UserSearchResponse
import retrofit2.Call
Expand All @@ -16,9 +15,9 @@ interface ApiService {
@GET("users/{username}")
fun getUserDetails(@Path("username") username: String): Call<UserResponse>

@GET("users/{username}/followers")
fun getFollowers(@Path("username") username: String): Call<FollowersResponse>
@GET("users/{username}/followers?per_page=100")
fun getFollowers(@Path("username") username: String): Call<List<FollowerFollowingItem>>

@GET("users/{username}/following")
fun getFollowing(@Path("username") username: String): Call<FollowingResponse>
@GET("users/{username}/following?per_page=100")
fun getFollowing(@Path("username") username: String): Call<List<FollowerFollowingItem>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,6 @@ package com.aurelioklv.githubuser.data.response

import com.google.gson.annotations.SerializedName

data class FollowersResponse(

@field:SerializedName("FollowersResponse")
val followers: List<FollowerFollowingItem>,
)

data class FollowingResponse(

@field:SerializedName("FollowingResponse")
val following: List<FollowerFollowingItem>,
)

data class FollowerFollowingItem(

@field:SerializedName("gists_url")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,72 +4,72 @@ import com.google.gson.annotations.SerializedName

data class UserSearchResponse(

@field:SerializedName("total_count")
val totalCount: Int,
@field:SerializedName("total_count")
val totalCount: Int,

@field:SerializedName("incomplete_results")
val incompleteResults: Boolean,
@field:SerializedName("incomplete_results")
val incompleteResults: Boolean,

@field:SerializedName("items")
val items: List<UserSearchItem>
@field:SerializedName("items")
val items: List<UserSearchItem>,
)

data class UserSearchItem(

@field:SerializedName("gists_url")
val gistsUrl: String,
@field:SerializedName("gists_url")
val gistsUrl: String,

@field:SerializedName("repos_url")
val reposUrl: String,
@field:SerializedName("repos_url")
val reposUrl: String,

@field:SerializedName("following_url")
val followingUrl: String,
@field:SerializedName("following_url")
val followingUrl: String,

@field:SerializedName("starred_url")
val starredUrl: String,
@field:SerializedName("starred_url")
val starredUrl: String,

@field:SerializedName("login")
val login: String,
@field:SerializedName("login")
val login: String,

@field:SerializedName("followers_url")
val followersUrl: String,
@field:SerializedName("followers_url")
val followersUrl: String,

@field:SerializedName("type")
val type: String,
@field:SerializedName("type")
val type: String,

@field:SerializedName("url")
val url: String,
@field:SerializedName("url")
val url: String,

@field:SerializedName("subscriptions_url")
val subscriptionsUrl: String,
@field:SerializedName("subscriptions_url")
val subscriptionsUrl: String,

@field:SerializedName("score")
val score: Any,
@field:SerializedName("score")
val score: Any,

@field:SerializedName("received_events_url")
val receivedEventsUrl: String,
@field:SerializedName("received_events_url")
val receivedEventsUrl: String,

@field:SerializedName("avatar_url")
val avatarUrl: String,
@field:SerializedName("avatar_url")
val avatarUrl: String,

@field:SerializedName("events_url")
val eventsUrl: String,
@field:SerializedName("events_url")
val eventsUrl: String,

@field:SerializedName("html_url")
val htmlUrl: String,
@field:SerializedName("html_url")
val htmlUrl: String,

@field:SerializedName("site_admin")
val siteAdmin: Boolean,
@field:SerializedName("site_admin")
val siteAdmin: Boolean,

@field:SerializedName("id")
val id: Int,
@field:SerializedName("id")
val id: Int,

@field:SerializedName("gravatar_id")
val gravatarId: String,
@field:SerializedName("gravatar_id")
val gravatarId: String,

@field:SerializedName("node_id")
val nodeId: String,
@field:SerializedName("node_id")
val nodeId: String,

@field:SerializedName("organizations_url")
val organizationsUrl: String
@field:SerializedName("organizations_url")
val organizationsUrl: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import android.util.Log
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.viewpager2.widget.ViewPager2
import com.aurelioklv.githubuser.R
import com.aurelioklv.githubuser.data.response.UserResponse
import com.aurelioklv.githubuser.databinding.ActivityDetailsBinding
import com.aurelioklv.githubuser.ui.formatCount
import com.bumptech.glide.Glide
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator

class DetailsActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailsBinding
Expand All @@ -35,16 +39,27 @@ class DetailsActivity : AppCompatActivity() {

if (intent != null && intent.hasExtra(EXTRA_USERNAME)) {
username = intent.getStringExtra(EXTRA_USERNAME)
viewModel.getUserDetails(username!!)
viewModel.getUserDetails(username!!, this)
}

viewModel.user.observe(this) {
setUserDetails(it)
}
setupViewPager()
observeLiveData()
}

viewModel.isLoading.observe(this) {
showLoading(it)
}
private fun setupViewPager() {
val sectionsPagerAdapter = SectionsPagerAdapter(this, username!!)
val viewPager: ViewPager2 = findViewById(R.id.view_pager)
viewPager.adapter = sectionsPagerAdapter

val tabs: TabLayout = findViewById(R.id.tabs)
TabLayoutMediator(tabs, viewPager) { tab, position ->
tab.text = resources.getString(TAB_TITLES[position])
}.attach()
}

private fun observeLiveData() {
viewModel.user.observe(this) { setUserDetails(it) }
viewModel.isLoading.observe(this) { showLoading(it) }
}

private fun setUserDetails(userResponse: UserResponse) {
Expand Down Expand Up @@ -93,5 +108,8 @@ class DetailsActivity : AppCompatActivity() {
companion object {
const val TAG = "DetailsActivity"
const val EXTRA_USERNAME = "username"

@StringRes
private val TAB_TITLES = intArrayOf(R.string.tab_text_1, R.string.tab_text_2)
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.aurelioklv.githubuser.ui.details

import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.aurelioklv.githubuser.R
import com.aurelioklv.githubuser.data.api.ApiConfig
import com.aurelioklv.githubuser.data.response.UserResponse
import retrofit2.Call
Expand All @@ -17,24 +20,35 @@ class DetailsViewModel : ViewModel() {
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading

fun getUserDetails(username: String) {
fun getUserDetails(username: String, context: Context) {
_isLoading.value = true
val client = ApiConfig.getApiService().getUserDetails(username)
client.enqueue(object : Callback<UserResponse> {
override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) {
_isLoading.value = false
if (response.isSuccessful) {
val responseBody = response.body()
if (responseBody != null) {
_user.value = responseBody
responseBody?.let {
_user.value = it
}
} else {
Log.e(TAG, "onResponse !isSuccessFul: $response")
Toast.makeText(
context,
context.getString(R.string.error_message),
Toast.LENGTH_LONG
).show()
}
}

override fun onFailure(call: Call<UserResponse>, t: Throwable) {
_isLoading.value = false
Log.e(TAG, "onFailure: ${t.message}")
Toast.makeText(
context,
context.getString(R.string.error_message),
Toast.LENGTH_LONG
).show()
}
})
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.aurelioklv.githubuser.ui.details

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.aurelioklv.githubuser.ui.details.follow.FollowFragment

class SectionsPagerAdapter(activity: AppCompatActivity, private val username: String) :
FragmentStateAdapter(activity) {
override fun getItemCount(): Int {
return 2
}

override fun createFragment(position: Int): Fragment {
val fragment = FollowFragment()
fragment.arguments = Bundle().apply {
putInt(FollowFragment.ARG_SECTION_NUMBER, position + 1)
putString(FollowFragment.ARG_USERNAME, username)
}
return fragment
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.aurelioklv.githubuser.ui.details.follow

import android.content.Intent
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.aurelioklv.githubuser.data.response.FollowerFollowingItem
import com.aurelioklv.githubuser.databinding.UserSearchItemBinding
import com.aurelioklv.githubuser.ui.details.DetailsActivity
import com.bumptech.glide.Glide

class FollowAdapter :
ListAdapter<FollowerFollowingItem, FollowAdapter.MyViewHolder>(DIFF_CALLBACK) {
class MyViewHolder(private val binding: UserSearchItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: FollowerFollowingItem) {
Glide.with(itemView.context)
.load(item.avatarUrl)
.into(binding.ivUserAvatar)
binding.tvUsername.text = item.login
}
}

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

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val userSearchItem = getItem(position)
holder.bind(userSearchItem)

holder.itemView.setOnClickListener {
val intent = Intent(holder.itemView.context, DetailsActivity::class.java)
intent.putExtra(DetailsActivity.EXTRA_USERNAME, userSearchItem.login)
holder.itemView.context.startActivity(intent)
}
}

companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<FollowerFollowingItem>() {
override fun areItemsTheSame(
oldItem: FollowerFollowingItem,
newItem: FollowerFollowingItem,
): Boolean {
return oldItem == newItem
}

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

0 comments on commit 271d471

Please sign in to comment.