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/BindingAdapter.kt b/app/src/main/java/place/pic/android/plus/BindingAdapter.kt new file mode 100644 index 0000000..f1acada --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/BindingAdapter.kt @@ -0,0 +1,49 @@ +package place.pic.android.plus + +import android.widget.ImageView +import androidx.databinding.BindingAdapter +import com.bumptech.glide.Glide + +object BindingAdapter { + @BindingAdapter("setImageUrl") + @JvmStatic + fun loadImage(imageView: ImageView, url: String) { + Glide.with(imageView.context) + .load(url) + .circleCrop() + .into(imageView) + } + + @BindingAdapter("setButtonChange") + @JvmStatic + fun setButtonChange(button: ImageView, compass: Boolean) { + when (compass) { + true -> button.setBackgroundResource(R.drawable.ic_baseline_clear_24) + else -> button.setBackgroundResource(R.drawable.ic_baseline_search_24) + } + } + + /* + @BindingAdapter("TextChangeWatcher") + @JvmStatic + fun onEditTextWatcher(): TextWatcher { + return object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + } + } + } + + @BindingAdapter("bindRecyclerView") + @JvmStatic + fun bindRecyclerView(recyclerView: RecyclerView, searchUserData: MutableList?) + { + if (recyclerView.adapter != null) { + with(recyclerView.adapter as SearchUserAdapter) { + submitList(searchUserData) + } + } + } + */ +} 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/SingleLiveEvent.kt b/app/src/main/java/place/pic/android/plus/SingleLiveEvent.kt new file mode 100644 index 0000000..15ee031 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/SingleLiveEvent.kt @@ -0,0 +1,43 @@ +package place.pic.android.plus + +import android.util.Log +import androidx.annotation.MainThread +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +open class SingleLiveEvent : MutableLiveData() { + + private val mPending = AtomicBoolean(false) + + override fun observe(owner: LifecycleOwner, observer: Observer) { + if (hasActiveObservers()) { + Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") + } + + super.observe( + owner, + Observer { t -> + if (mPending.compareAndSet(true, false)) { + observer.onChanged(t) + } + } + ) + } + + @MainThread + override fun setValue(t: T?) { + mPending.set(true) + super.setValue(t) + } + + @MainThread + fun call() { + value = null + } + + companion object { + private val TAG = "SingleLiveEvent" + } +} 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..a113e5b --- /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 +) : Serializable + +data class SearchUserData( + val login: String, + val avatar_url: String, + val 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..b0aaed5 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/search/SearchUserActivity.kt @@ -0,0 +1,97 @@ +package place.pic.android.plus.search + +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() + private lateinit var searchUserAdapter: SearchUserAdapter + private val userList = mutableListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, R.layout.activity_search_user) + searchUserAdapter = SearchUserAdapter() + binding.searchUserActivity = searchUserViewModel + binding.lifecycleOwner = this + binding.rvUserSearch.adapter = searchUserAdapter + + searchUser() + changeButton() + deleteText() + gotoDetail() + } + + private fun searchUser() { + searchUserViewModel.recyclerListData.observe(this) { + searchUserAdapter.submitList(it) + } + + 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 + } + }) + } + + // livedata 쓰기 + private fun changeButton() { + 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 + } + }) + } + + // Btn 바인딩으로빼기 + private fun deleteText() { + binding.btnUserSearchDelete.setOnClickListener { + binding.etUserSearch.text.clear() + } + } + + // 여기 바꿔야함요 ㅎㅎ + 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", userList[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..e209986 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/search/adapter/SearchUserAdapter.kt @@ -0,0 +1,51 @@ +package place.pic.android.plus.search.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import place.pic.android.plus.data.remote.response.SearchUserData +import place.pic.android.plus.databinding.ItemUserSearchBinding + +class SearchUserAdapter : ListAdapter(SearchUserDiffUtil()) { + 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) { + val user = getItem(position) + holder.bind(user) + if (itemClick != null) { + holder.itemView.setOnClickListener { v -> + itemClick?.onClick(v, position) + } + } + } + + class SearchUserViewHolder(private val binding: ItemUserSearchBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(searchUserData: SearchUserData) { + binding.user = searchUserData + binding.executePendingBindings() + } + } + + private class SearchUserDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SearchUserData, newItem: SearchUserData): Boolean { + return oldItem.login == newItem.login + } + + override fun areContentsTheSame(oldItem: SearchUserData, newItem: SearchUserData): Boolean { + return oldItem == newItem + } + } + + interface ItemClick { + fun onClick(view: View, position: Int) + } + + 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..e698bf9 --- /dev/null +++ b/app/src/main/java/place/pic/android/plus/search/viewmodel/SearchUserViewModel.kt @@ -0,0 +1,53 @@ +package place.pic.android.plus.search.viewmodel + +import android.util.Log +import androidx.lifecycle.LiveData +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.SingleLiveEvent +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 = SingleLiveEvent>() + val recyclerListData: LiveData> + get() = _recyclerListData + + /* + private val _compass = MutableLiveData(false) + val compass: LiveData = _compass + + private val _searchText = MutableLiveData() + val searchText: LiveData = _searchText + + fun compassIcon() { + _compass.value = !_compass.value!! + } + + fun searchTextWacher(){ + } + */ + + 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() + } + } + userClick() + } + + private fun userClick() { + _recyclerListData.call() + } +} 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 @@ + + + + + + + + + + + + + +