From ac35d14f127bc76f215540a08991e94b561ebbb3 Mon Sep 17 00:00:00 2001 From: SooJeong Lee Date: Wed, 17 Nov 2021 20:11:37 +0900 Subject: [PATCH 1/3] =?UTF-8?q?mvvm=20=EA=B3=BC=EC=A0=9C=20STEP1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 22 ++++ app/src/main/AndroidManifest.xml | 10 +- .../place/pic/android/plus/MainActivity.kt | 11 -- .../android/plus/data/remote/GithubService.kt | 12 ++ .../plus/data/remote/RetrofitBuilder.kt | 14 +++ .../remote/response/ResponseUserSearch.kt | 15 +++ .../android/plus/detail/DetailUserActivity.kt | 39 ++++++ .../android/plus/search/SearchUserActivity.kt | 111 ++++++++++++++++++ .../plus/search/adapter/SearchUserAdapter.kt | 58 +++++++++ .../search/viewmodel/SearchUserViewModel.kt | 34 ++++++ .../drawable/border_white_fill_round_20.xml | 6 + .../drawable/ic_baseline_arrow_back_24.xml | 10 ++ .../res/drawable/ic_baseline_clear_24.xml | 10 ++ .../ic_baseline_not_interested_24.xml | 5 + .../ic_baseline_people_outline_24.xml | 10 ++ .../res/drawable/ic_baseline_search_24.xml | 10 ++ .../main/res/layout/activity_detail_user.xml | 55 +++++++++ app/src/main/res/layout/activity_main.xml | 18 --- .../main/res/layout/activity_search_user.xml | 88 ++++++++++++++ app/src/main/res/layout/item_user_search.xml | 41 +++++++ app/src/main/res/values/strings.xml | 4 +- app/src/main/res/values/themes.xml | 10 +- 22 files changed, 554 insertions(+), 39 deletions(-) delete mode 100644 app/src/main/java/place/pic/android/plus/MainActivity.kt create mode 100644 app/src/main/java/place/pic/android/plus/data/remote/GithubService.kt create mode 100644 app/src/main/java/place/pic/android/plus/data/remote/RetrofitBuilder.kt create mode 100644 app/src/main/java/place/pic/android/plus/data/remote/response/ResponseUserSearch.kt create mode 100644 app/src/main/java/place/pic/android/plus/detail/DetailUserActivity.kt create mode 100644 app/src/main/java/place/pic/android/plus/search/SearchUserActivity.kt create mode 100644 app/src/main/java/place/pic/android/plus/search/adapter/SearchUserAdapter.kt create mode 100644 app/src/main/java/place/pic/android/plus/search/viewmodel/SearchUserViewModel.kt create mode 100644 app/src/main/res/drawable/border_white_fill_round_20.xml create mode 100644 app/src/main/res/drawable/ic_baseline_arrow_back_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_clear_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_not_interested_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_people_outline_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_search_24.xml create mode 100644 app/src/main/res/layout/activity_detail_user.xml delete mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_search_user.xml create mode 100644 app/src/main/res/layout/item_user_search.xml diff --git a/app/build.gradle b/app/build.gradle index 71490b5..1c710cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' id 'kotlin-android' + id 'kotlin-kapt' } android { @@ -16,6 +17,11 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + buildFeatures { + dataBinding true + viewBinding true + } + buildTypes { release { minifyEnabled false @@ -40,4 +46,20 @@ dependencies { testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'androidx.activity:activity-ktx:1.1.0' + implementation 'androidx.fragment:fragment-ktx:1.2.5' + + // Lifecycle + implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" + + // Retrofit + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation "com.squareup.retrofit2:adapter-rxjava2:2.5.0" + + //Glide + implementation 'com.github.bumptech.glide:glide:4.12.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8508fca..9b74e56 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + android:name=".detail.DetailUserActivity" + android:exported="false" /> + @@ -19,5 +24,4 @@ - \ No newline at end of file diff --git a/app/src/main/java/place/pic/android/plus/MainActivity.kt b/app/src/main/java/place/pic/android/plus/MainActivity.kt deleted file mode 100644 index b8670ea..0000000 --- a/app/src/main/java/place/pic/android/plus/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package place.pic.android.plus - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/place/pic/android/plus/data/remote/GithubService.kt b/app/src/main/java/place/pic/android/plus/data/remote/GithubService.kt new file mode 100644 index 0000000..9011575 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/data/remote/GithubService.kt @@ -0,0 +1,12 @@ +package place.pic.android.plus.data.remote + +import place.pic.android.plus.data.remote.response.ResponseUserSearch +import retrofit2.http.GET +import retrofit2.http.Query + +interface GithubService { + @GET("/search/users") + suspend fun userList( + @Query("q") param: String? + ): ResponseUserSearch +} diff --git a/app/src/main/java/place/pic/android/plus/data/remote/RetrofitBuilder.kt b/app/src/main/java/place/pic/android/plus/data/remote/RetrofitBuilder.kt new file mode 100644 index 0000000..12bf031 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/data/remote/RetrofitBuilder.kt @@ -0,0 +1,14 @@ +package place.pic.android.plus.data.remote + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object RetrofitBuilder { + private const val baseUrl = "https://api.github.com" + + private var retrofit = Retrofit.Builder().baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + var service: GithubService = retrofit.create(GithubService::class.java) +} diff --git a/app/src/main/java/place/pic/android/plus/data/remote/response/ResponseUserSearch.kt b/app/src/main/java/place/pic/android/plus/data/remote/response/ResponseUserSearch.kt new file mode 100644 index 0000000..038f111 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/data/remote/response/ResponseUserSearch.kt @@ -0,0 +1,15 @@ +package place.pic.android.plus.data.remote.response + +import java.io.Serializable + +data class ResponseUserSearch( + val total_count: Int, + val incomplete_results: Boolean, + val items: List +) + +data class SearchUserData( + var login: String, + var avatar_url: String, + var html_url: String +) : Serializable diff --git a/app/src/main/java/place/pic/android/plus/detail/DetailUserActivity.kt b/app/src/main/java/place/pic/android/plus/detail/DetailUserActivity.kt new file mode 100644 index 0000000..39ebf92 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/detail/DetailUserActivity.kt @@ -0,0 +1,39 @@ +package place.pic.android.plus.detail + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.bumptech.glide.Glide +import place.pic.android.plus.data.remote.response.SearchUserData +import place.pic.android.plus.databinding.ActivityDetailUserBinding + +class DetailUserActivity : AppCompatActivity() { + private lateinit var binding: ActivityDetailUserBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityDetailUserBinding.inflate(layoutInflater) + setContentView(binding.root) + setBindingUserData() + } + + private fun setBindingUserData() { + val user = intent.getSerializableExtra("user") as SearchUserData + + with(binding) { + tvUser.text = user.login + + Glide.with(this@DetailUserActivity) + .load(user.avatar_url) + .circleCrop() + .into(imgUser) + + btnUser.setOnClickListener { + val webIntent = + Intent(Intent.ACTION_VIEW, Uri.parse(user.html_url)) + startActivity(webIntent) + } + } + } +} diff --git a/app/src/main/java/place/pic/android/plus/search/SearchUserActivity.kt b/app/src/main/java/place/pic/android/plus/search/SearchUserActivity.kt new file mode 100644 index 0000000..b227b06 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/search/SearchUserActivity.kt @@ -0,0 +1,111 @@ +package place.pic.android.plus.search + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import place.pic.android.plus.R +import place.pic.android.plus.data.remote.response.SearchUserData +import place.pic.android.plus.databinding.ActivitySearchUserBinding +import place.pic.android.plus.detail.DetailUserActivity +import place.pic.android.plus.search.adapter.SearchUserAdapter +import place.pic.android.plus.search.viewmodel.SearchUserViewModel + +class SearchUserActivity : AppCompatActivity() { + private lateinit var binding: ActivitySearchUserBinding + private val searchUserViewModel: SearchUserViewModel by viewModels() + lateinit var searchUserAdapter: SearchUserAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_search_user) + searchUserAdapter = SearchUserAdapter() + binding.rvUserSearch.adapter = searchUserAdapter + binding.searchUserActivity = searchUserViewModel + binding.lifecycleOwner = this + + searchUser() + changeButton() + deleteText() + gotoDetail() + } + + private fun searchUser() { + binding.etUserSearch.setOnEditorActionListener(object : TextView.OnEditorActionListener { + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + lifecycleScope.launch { + searchUserViewModel.requestUserData(binding.etUserSearch.text.toString()) + } + return true + } + return false + } + }) + createData() + } + + @SuppressLint("NotifyDataSetChanged") + private fun createData() { + searchUserViewModel.recyclerListData.observe( + this, + { recyclerListData -> + recyclerListData?.let { + if (it.isNotEmpty()) { + binding.rvUserSearch.visibility = View.VISIBLE + searchUserAdapter.datas = it as MutableList + searchUserAdapter.notifyDataSetChanged() + } else { + binding.rvUserSearch.visibility = View.INVISIBLE + } + } + } + ) + } + + private fun changeButton() { + // 검색 -> x 로 변경 + binding.etUserSearch.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + binding.btnUserSearch.visibility = View.VISIBLE + binding.btnUserSearchDelete.visibility = View.INVISIBLE + } + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + } + + override fun afterTextChanged(p0: Editable?) { + binding.btnUserSearch.visibility = View.INVISIBLE + binding.btnUserSearchDelete.visibility = View.VISIBLE + } + }) + } + + private fun deleteText() { + binding.btnUserSearchDelete.setOnClickListener { + binding.rvUserSearch.visibility = View.INVISIBLE + binding.etUserSearch.text.clear() + searchUserAdapter.clearData() + } + } + + private fun gotoDetail() { + searchUserAdapter.itemClick = object : SearchUserAdapter.ItemClick { + override fun onClick(view: View, position: Int) { + val intent = Intent(this@SearchUserActivity, DetailUserActivity::class.java) + intent.putExtra("user", searchUserAdapter.datas[position]) + startActivity(intent) + } + } + } +} diff --git a/app/src/main/java/place/pic/android/plus/search/adapter/SearchUserAdapter.kt b/app/src/main/java/place/pic/android/plus/search/adapter/SearchUserAdapter.kt new file mode 100644 index 0000000..ae356cb --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/search/adapter/SearchUserAdapter.kt @@ -0,0 +1,58 @@ +package place.pic.android.plus.search.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import place.pic.android.plus.data.remote.response.SearchUserData +import place.pic.android.plus.databinding.ItemUserSearchBinding + +class SearchUserAdapter : RecyclerView.Adapter() { + var datas: MutableList = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchUserViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = ItemUserSearchBinding.inflate(layoutInflater, parent, false) + return SearchUserViewHolder(binding) + } + + override fun onBindViewHolder(holder: SearchUserViewHolder, position: Int) { + holder.bind(datas[position]) + if (itemClick != null) { + holder.itemView.setOnClickListener { v -> + itemClick?.onClick(v, position) + } + } + } + + override fun getItemCount(): Int { + return datas.size + } + + class SearchUserViewHolder(private val binding: ItemUserSearchBinding) : RecyclerView.ViewHolder(binding.root) { + private val layout = binding.clItemUserSearch + private val userName = binding.tvUserName + private val userImage = binding.imgUser + + fun bind(searchUserData: SearchUserData) { + userName.text = searchUserData.login + Glide.with(itemView) + .load(searchUserData.avatar_url) + .circleCrop() + .into(userImage) + layout.isEnabled = true + } + } + + interface ItemClick { + fun onClick(view: View, position: Int) + } + + fun clearData() { + datas.clear() + notifyDataSetChanged() + } + + var itemClick: ItemClick? = null +} diff --git a/app/src/main/java/place/pic/android/plus/search/viewmodel/SearchUserViewModel.kt b/app/src/main/java/place/pic/android/plus/search/viewmodel/SearchUserViewModel.kt new file mode 100644 index 0000000..b2c9931 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/search/viewmodel/SearchUserViewModel.kt @@ -0,0 +1,34 @@ +package place.pic.android.plus.search.viewmodel + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import place.pic.android.plus.data.remote.RetrofitBuilder +import place.pic.android.plus.data.remote.response.SearchUserData +import retrofit2.HttpException +import java.io.IOException + +class SearchUserViewModel : ViewModel() { + + private val _recyclerListData = MutableLiveData>() + val recyclerListData: LiveData> + get() = _recyclerListData + + fun requestUserData(toString: String) { + viewModelScope.launch(Dispatchers.IO) { + try { + val userData = RetrofitBuilder.service.userList(toString) + _recyclerListData.postValue(userData.items) + } catch (e: HttpException) { + Log.d("request", e.toString()) + } catch (e: IOException) { + this.cancel() + } + } + } +} diff --git a/app/src/main/res/drawable/border_white_fill_round_20.xml b/app/src/main/res/drawable/border_white_fill_round_20.xml new file mode 100644 index 0000000..c6bfcbb --- /dev/null +++ b/app/src/main/res/drawable/border_white_fill_round_20.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml new file mode 100644 index 0000000..bab545a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_clear_24.xml b/app/src/main/res/drawable/ic_baseline_clear_24.xml new file mode 100644 index 0000000..16d6d37 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_clear_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_not_interested_24.xml b/app/src/main/res/drawable/ic_baseline_not_interested_24.xml new file mode 100644 index 0000000..22864ca --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_not_interested_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_people_outline_24.xml b/app/src/main/res/drawable/ic_baseline_people_outline_24.xml new file mode 100644 index 0000000..30d1c94 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_people_outline_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_search_24.xml b/app/src/main/res/drawable/ic_baseline_search_24.xml new file mode 100644 index 0000000..07b76d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_search_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_detail_user.xml b/app/src/main/res/layout/activity_detail_user.xml new file mode 100644 index 0000000..b0454f4 --- /dev/null +++ b/app/src/main/res/layout/activity_detail_user.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + +