Skip to content

Commit

Permalink
Merge pull request #6 from LloydBlv/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
LloydBlv authored Mar 22, 2021
2 parents d3b6754 + 55538bf commit e6b8363
Show file tree
Hide file tree
Showing 17 changed files with 450 additions and 13 deletions.
7 changes: 7 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ android {
}
}

buildFeatures {
dataBinding = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
Expand All @@ -56,9 +60,12 @@ dependencies {
implementation(Dependencies.AndroidX.material)
implementation(Dependencies.AndroidX.constraintLayout)
implementation(Dependencies.AndroidX.viewModelKtx)
implementation(Dependencies.AndroidX.lifeCycleCommon)

/*Misc*/
implementation(Dependencies.Misc.timber)
implementation(Dependencies.Misc.viewBindingDelegate)
implementation(Dependencies.Misc.coil)

/*Dagger*/
implementation(Dependencies.Dagger.Hilt.android)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package ir.zinutech.android.flickrsearch.core.extensions

import android.widget.ImageView
import androidx.databinding.BindingAdapter
import coil.load
import coil.size.Scale

@BindingAdapter("imageUrl")
fun ImageView.imageUrl(url: String?) {
load(url){
crossfade(true)
scale(Scale.FILL)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ir.zinutech.android.flickrsearch.core.extensions

import androidx.recyclerview.widget.RecyclerView

class CloseKeyboardScrollListener : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
recyclerView.hideKeyboard()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ir.zinutech.android.flickrsearch.core.extensions

import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.core.content.ContextCompat.getSystemService
import androidx.recyclerview.widget.ListAdapter


fun View.inflater(): LayoutInflater {
return LayoutInflater.from(context)
}

fun ListAdapter<*, *>.clear() {
submitList(null)
}

fun View.toGone() {
this.visibility = View.GONE
}

fun View.toInvisible() {
this.visibility = View.INVISIBLE
}

fun View.toVisible() {
this.visibility = View.VISIBLE
}

fun View.hideKeyboard() {
val imm: InputMethodManager? = getSystemService(context, InputMethodManager::class.java)
imm?.hideSoftInputFromWindow(windowToken, 0)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package ir.zinutech.android.flickrsearch.core.util

import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
* A lazy property that gets cleaned up when the fragment's view is destroyed.
*
* Accessing this variable while the fragment's view is destroyed will throw NPE.
*/
class AutoClearedValue<T : Any>(val fragment: Fragment) : ReadWriteProperty<Fragment, T> {
private var _value: T? = null

init {
fragment.lifecycle.addObserver(object: DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner?.lifecycle?.addObserver(object: DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_value = null
}
})
}
}
})
}

override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return _value ?: throw IllegalStateException(
"should never call auto-cleared-value get when it might not be available"
)
}

override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
_value = value
}
}

/**
* Creates an [AutoClearedValue] associated with this fragment.
*/
fun <T : Any> Fragment.autoCleared() = AutoClearedValue<T>(this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ir.zinutech.android.flickrsearch.features.search

import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import ir.zinutech.android.flickrsearch.R
import ir.zinutech.android.flickrsearch.core.extensions.inflater
import ir.zinutech.android.flickrsearch.databinding.ItemSearchResultLayoutBinding
import ir.zinutech.android.flickrsearch.domain.features.search.models.FlickrPhoto

class SearchAdapter : ListAdapter<FlickrPhoto, SearchAdapter.ViewHolder>(DIFF_UTIL) {

companion object {
private val DIFF_UTIL = object : DiffUtil.ItemCallback<FlickrPhoto>() {
override fun areItemsTheSame(oldItem: FlickrPhoto, newItem: FlickrPhoto): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: FlickrPhoto, newItem: FlickrPhoto): Boolean {
return oldItem.title == newItem.title &&
oldItem.url == newItem.url
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.create(parent)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}

class ViewHolder private constructor(val binding: ItemSearchResultLayoutBinding) : RecyclerView.ViewHolder(binding.root) {

fun bind(flickrPhoto: FlickrPhoto) {
binding.run {
photo = flickrPhoto
executePendingBindings()
}
}

companion object {
fun create(parent: ViewGroup): ViewHolder {
val binding = ItemSearchResultLayoutBinding.inflate(parent.inflater(), parent, false)
return ViewHolder(binding = binding)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,175 @@
package ir.zinutech.android.flickrsearch.features.search

import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import androidx.activity.addCallback
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import ir.zinutech.android.flickrsearch.R
import ir.zinutech.android.flickrsearch.core.extensions.CloseKeyboardScrollListener
import ir.zinutech.android.flickrsearch.core.extensions.toGone
import ir.zinutech.android.flickrsearch.core.extensions.toInvisible
import ir.zinutech.android.flickrsearch.core.extensions.toVisible
import ir.zinutech.android.flickrsearch.core.util.autoCleared
import ir.zinutech.android.flickrsearch.databinding.FragmentSearchLayoutBinding
import timber.log.Timber

@AndroidEntryPoint
class SearchFragment : Fragment(R.layout.fragment_search_layout) {

private val searchViewModel: SearchViewModel by viewModels()
private val viewBinding: FragmentSearchLayoutBinding by viewBinding()
private var searchResultAdapter by autoCleared<SearchAdapter>()
private var keyboardCloseListener by autoCleared<CloseKeyboardScrollListener>()
private var onSearchQueryTextListener: SearchView.OnQueryTextListener? = null
private var errorSnackBar: Snackbar? = null
private var searchView: SearchView? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
activity?.onBackPressedDispatcher?.addCallback(this, enabled = true) {
if (!isSearchViewOpen()) {
isEnabled = false
activity?.onBackPressed()
return@addCallback
}
collapseSearchView()
}
}

private fun collapseSearchView() {
searchView?.isIconified = true
}

private fun isSearchViewOpen() = searchView?.isIconified == false

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_search, menu)
searchView = menu.findItem(R.id.action_search)?.actionView as? SearchView
setupSearchMenuItem()
}

override fun onDestroyOptionsMenu() {
super.onDestroyOptionsMenu()
onSearchQueryTextListener = null
searchView = null
}

private fun setupSearchMenuItem() {
searchView ?: return
onSearchQueryTextListener = createSearchQueryTextListener()
searchView?.setOnQueryTextListener(onSearchQueryTextListener)
}

private fun createSearchQueryTextListener(): SearchView.OnQueryTextListener {
return object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
searchViewModel.onQueryChanged(query)
return false
}

override fun onQueryTextChange(newText: String?): Boolean {
searchViewModel.onQueryChanged(newText)
return true
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.d("onViewCreated()")
searchViewModel.uiState.observe(viewLifecycleOwner, {
Timber.d("uiState.observe(), uiState:[%s]", it)
})
setupUi()
setupObservers()
}

private fun setupUi() {
viewBinding.searchFragmentResultRv.apply {
keyboardCloseListener = CloseKeyboardScrollListener()
addOnScrollListener(keyboardCloseListener)
adapter = SearchAdapter().also {
searchResultAdapter = it
}
}
}

searchViewModel.onQueryChanged("hack")
private fun setupObservers() {
searchViewModel.uiState.observe(viewLifecycleOwner, ::bindUiState)
}

private fun bindUiState(uiState: SearchViewModel.SearchUiState?) {
Timber.d("uiState.observe(), uiState:[%s]", uiState)
viewBinding.searchFragmentResultPb.isVisible = uiState == SearchViewModel.SearchUiState.Loading
dismissErrorSnackBar()
when (uiState) {
SearchViewModel.SearchUiState.Loading -> {
showLoadingUi()
}
is SearchViewModel.SearchUiState.Success -> {
showSearchResultUi(uiState)
}

is SearchViewModel.SearchUiState.Error -> {
showErrorUi(uiState)
}
SearchViewModel.SearchUiState.Idle -> {
showIdleUi()
}
SearchViewModel.SearchUiState.EmptyResult -> {
showEmptyResultUi()
}
}

}

private fun showEmptyResultUi() {
viewBinding.searchFragmentSearchGuideTv.toVisible()
viewBinding.searchFragmentResultRv.toInvisible()
viewBinding.searchFragmentResultPb.toGone()
showSnackBar(getString(R.string.search_has_no_result))
}

private fun showIdleUi() {
viewBinding.searchFragmentSearchGuideTv.toVisible()
viewBinding.searchFragmentResultRv.toInvisible()
viewBinding.searchFragmentResultPb.toGone()
}

private fun showErrorUi(uiState: SearchViewModel.SearchUiState.Error) {
val message = uiState.exception.localizedMessage ?: getString(R.string.something_went_wrong)
showSnackBar(message)
}

private fun showSnackBar(message: CharSequence) {
errorSnackBar = Snackbar.make(viewBinding.root, message, Snackbar.LENGTH_INDEFINITE)
errorSnackBar?.show()
}

private fun dismissErrorSnackBar() {
errorSnackBar?.dismiss()
errorSnackBar = null
}

override fun onDestroyView() {
super.onDestroyView()
dismissErrorSnackBar()
}

private fun showSearchResultUi(uiState: SearchViewModel.SearchUiState.Success) {
viewBinding.searchFragmentSearchGuideTv.toGone()
viewBinding.searchFragmentResultRv.toVisible()
searchResultAdapter.submitList(uiState.photos)
}

private fun showLoadingUi() {
viewBinding.searchFragmentSearchGuideTv.toGone()
viewBinding.searchFragmentResultRv.toInvisible()
}
}
Loading

0 comments on commit e6b8363

Please sign in to comment.