From 277f1407f4de1416b6a1acbcdbfc55e8382affaa Mon Sep 17 00:00:00 2001 From: aurelioklv Date: Mon, 11 Mar 2024 20:24:54 +0700 Subject: [PATCH] feat: add details screen --- app/src/main/AndroidManifest.xml | 3 + .../githubuser/data/response/UserResponse.kt | 128 +++++++++--------- .../java/com/aurelioklv/githubuser/ui/Util.kt | 10 ++ .../githubuser/ui/details/DetailsActivity.kt | 97 +++++++++++++ .../githubuser/ui/details/DetailsViewModel.kt | 45 ++++++ .../githubuser/ui/main/MainActivity.kt | 10 ++ .../githubuser/ui/main/UserSearchAdapter.kt | 8 ++ app/src/main/res/layout/activity_details.xml | 89 ++++++++++++ app/src/main/res/values/strings.xml | 11 ++ 9 files changed, 337 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/com/aurelioklv/githubuser/ui/Util.kt create mode 100644 app/src/main/java/com/aurelioklv/githubuser/ui/details/DetailsActivity.kt create mode 100644 app/src/main/java/com/aurelioklv/githubuser/ui/details/DetailsViewModel.kt create mode 100644 app/src/main/res/layout/activity_details.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f16c8c5..e73a3ec 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,9 @@ android:supportsRtl="true" android:theme="@style/Theme.GithubUser" tools:targetApi="31"> + diff --git a/app/src/main/java/com/aurelioklv/githubuser/data/response/UserResponse.kt b/app/src/main/java/com/aurelioklv/githubuser/data/response/UserResponse.kt index 40f955f..2b2b014 100644 --- a/app/src/main/java/com/aurelioklv/githubuser/data/response/UserResponse.kt +++ b/app/src/main/java/com/aurelioklv/githubuser/data/response/UserResponse.kt @@ -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, ) diff --git a/app/src/main/java/com/aurelioklv/githubuser/ui/Util.kt b/app/src/main/java/com/aurelioklv/githubuser/ui/Util.kt new file mode 100644 index 0000000..0358446 --- /dev/null +++ b/app/src/main/java/com/aurelioklv/githubuser/ui/Util.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurelioklv/githubuser/ui/details/DetailsActivity.kt b/app/src/main/java/com/aurelioklv/githubuser/ui/details/DetailsActivity.kt new file mode 100644 index 0000000..2777970 --- /dev/null +++ b/app/src/main/java/com/aurelioklv/githubuser/ui/details/DetailsActivity.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurelioklv/githubuser/ui/details/DetailsViewModel.kt b/app/src/main/java/com/aurelioklv/githubuser/ui/details/DetailsViewModel.kt new file mode 100644 index 0000000..0c0035d --- /dev/null +++ b/app/src/main/java/com/aurelioklv/githubuser/ui/details/DetailsViewModel.kt @@ -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() + val user: LiveData = _user + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + fun getUserDetails(username: String) { + _isLoading.value = true + val client = ApiConfig.getApiService().getUserDetails(username) + client.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + _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, t: Throwable) { + Log.e(TAG, "onFailure: ${t.message}") + } + }) + } + + companion object { + const val TAG = "DetailsViewModel" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurelioklv/githubuser/ui/main/MainActivity.kt b/app/src/main/java/com/aurelioklv/githubuser/ui/main/MainActivity.kt index 2cb5a72..ff046d9 100644 --- a/app/src/main/java/com/aurelioklv/githubuser/ui/main/MainActivity.kt +++ b/app/src/main/java/com/aurelioklv/githubuser/ui/main/MainActivity.kt @@ -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) { diff --git a/app/src/main/java/com/aurelioklv/githubuser/ui/main/UserSearchAdapter.kt b/app/src/main/java/com/aurelioklv/githubuser/ui/main/UserSearchAdapter.kt index 9f6d5a6..f3f8d15 100644 --- a/app/src/main/java/com/aurelioklv/githubuser/ui/main/UserSearchAdapter.kt +++ b/app/src/main/java/com/aurelioklv/githubuser/ui/main/UserSearchAdapter.kt @@ -1,5 +1,6 @@ package com.aurelioklv.githubuser.ui.main +import android.content.Intent import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil @@ -7,6 +8,7 @@ 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 : @@ -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 { diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml new file mode 100644 index 0000000..b17c8b9 --- /dev/null +++ b/app/src/main/res/layout/activity_details.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b133ba5..05898e3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,15 @@ GithubUser Search user username + Followers + Following + full name + + %s follower + %s follower + %s followers + + %d following + follower + bio \ No newline at end of file