Skip to content

Commit

Permalink
feat: add details screen
Browse files Browse the repository at this point in the history
  • Loading branch information
aurelioklv committed Mar 11, 2024
1 parent 75d0f56 commit 277f140
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 64 deletions.
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.GithubUser"
tools:targetApi="31">
<activity
android:name=".ui.details.DetailsActivity"
android:exported="false" />
<activity
android:name=".ui.main.MainActivity"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,99 +4,99 @@ import com.google.gson.annotations.SerializedName

data class UserResponse(

@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("twitter_username")
val twitterUsername: String,
@field:SerializedName("twitter_username")
val twitterUsername: String,

@field:SerializedName("bio")
val bio: Any,
@field:SerializedName("bio")
val bio: String?,

@field:SerializedName("created_at")
val createdAt: String,
@field:SerializedName("created_at")
val createdAt: String,

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

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

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

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

@field:SerializedName("updated_at")
val updatedAt: String,
@field:SerializedName("updated_at")
val updatedAt: String,

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

@field:SerializedName("company")
val company: Any,
@field:SerializedName("company")
val company: String,

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

@field:SerializedName("public_repos")
val publicRepos: Int,
@field:SerializedName("public_repos")
val publicRepos: Int,

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

@field:SerializedName("email")
val email: Any,
@field:SerializedName("email")
val email: String,

@field:SerializedName("organizations_url")
val organizationsUrl: String,
@field:SerializedName("organizations_url")
val organizationsUrl: String,

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

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

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

@field:SerializedName("public_gists")
val publicGists: Int,
@field:SerializedName("public_gists")
val publicGists: Int,

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

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

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

@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("following")
val following: Int,
@field:SerializedName("following")
val following: Int,

@field:SerializedName("name")
val name: String,
@field:SerializedName("name")
val name: String? = null,

@field:SerializedName("location")
val location: Any,
@field:SerializedName("location")
val location: String,

@field:SerializedName("node_id")
val nodeId: String
@field:SerializedName("node_id")
val nodeId: String,
)
10 changes: 10 additions & 0 deletions app/src/main/java/com/aurelioklv/githubuser/ui/Util.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.aurelioklv.githubuser.ui

fun formatCount(count: Long): String {
return when {
count < 1000L -> count.toString()
count < 100000L -> String.format("%.1fk", count / 1000.0)
count < 1000000L -> String.format("%dk", count / 1000L)
else -> String.format("%.1fM", count / 1000000L)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.aurelioklv.githubuser.ui.details

import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
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

class DetailsActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailsBinding
private val viewModel: DetailsViewModel by viewModels()

private var username: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

binding = ActivityDetailsBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}

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

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

viewModel.isLoading.observe(this) {
showLoading(it)
}
}

private fun setUserDetails(userResponse: UserResponse) {
with(userResponse) {
Glide.with(this@DetailsActivity)
.load(avatarUrl)
.into(binding.ivUserAvatar)
Log.i(TAG, "name: $name\nusername: $login")
if (name.isNullOrEmpty() || name.isBlank()) {
binding.tvName.visibility = View.INVISIBLE

val usernameLayoutParams =
binding.tvUsername.layoutParams as ConstraintLayout.LayoutParams
usernameLayoutParams.topToTop = binding.ivUserAvatar.id
usernameLayoutParams.bottomToBottom = binding.ivUserAvatar.id
} else {
binding.tvName.text = name
}
binding.tvUsername.text = userResponse.login

if (bio.isNullOrEmpty() || bio.isBlank()) {
binding.tvBio.visibility = View.INVISIBLE

val followersLayoutParams =
binding.tvFollowers.layoutParams as ConstraintLayout.LayoutParams
followersLayoutParams.topToBottom = binding.ivUserAvatar.id
} else {
binding.tvBio.text = bio
}

Log.i(TAG, "followers: $followers\nfollowing: $following")
binding.tvFollowers.text =
resources.getQuantityString(
R.plurals.followers,
followers,
formatCount(followers.toLong())
)
binding.tvFollowing.text = getString(R.string.following, following)
}
}

private fun showLoading(isLoading: Boolean) {
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}

companion object {
const val TAG = "DetailsActivity"
const val EXTRA_USERNAME = "username"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.aurelioklv.githubuser.ui.details

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.aurelioklv.githubuser.data.api.ApiConfig
import com.aurelioklv.githubuser.data.response.UserResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class DetailsViewModel : ViewModel() {
private val _user = MutableLiveData<UserResponse>()
val user: LiveData<UserResponse> = _user

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

fun getUserDetails(username: String) {
_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
}
} else {
Log.e(TAG, "onResponse !isSuccessFul: $response")
}
}

override fun onFailure(call: Call<UserResponse>, t: Throwable) {
Log.e(TAG, "onFailure: ${t.message}")
}
})
}

companion object {
const val TAG = "DetailsViewModel"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ class MainActivity : AppCompatActivity() {
viewModel.isLoading.observe(this) {
showLoading(it)
}

with(binding) {
searchView.setupWithSearchBar(searchBar)
searchView.editText.setOnEditorActionListener { v, actionId, event ->
searchBar.setText(searchView.text)
searchView.hide()
viewModel.searchUser(searchView.text.toString())
false
}
}
}

private fun showLoading(isLoading: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.aurelioklv.githubuser.ui.main

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.UserSearchItem
import com.aurelioklv.githubuser.databinding.UserSearchItemBinding
import com.aurelioklv.githubuser.ui.details.DetailsActivity
import com.bumptech.glide.Glide

class UserSearchAdapter :
Expand All @@ -30,6 +32,12 @@ class UserSearchAdapter :
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 {
Expand Down
Loading

0 comments on commit 277f140

Please sign in to comment.