Skip to content
This repository has been archived by the owner on Jul 5, 2021. It is now read-only.

Commit

Permalink
Implement loading and no result state in travel search page
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamin-cheng committed Mar 5, 2020
1 parent 86152d3 commit 3765ad4
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 22 deletions.
6 changes: 6 additions & 0 deletions app/src/main/java/org/mozilla/focus/utils/ViewUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ public static void hideKeyboard(View view) {
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
}

public static void forceHideKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) view.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}

/**
* Create a snackbar with Focus branding (See #193).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import android.util.AttributeSet
import android.view.View
import android.widget.Button
import android.widget.FrameLayout

import androidx.annotation.DrawableRes
import kotlinx.android.synthetic.main.no_result_view.view.*
import org.mozilla.focus.R

class NoResultView @JvmOverloads constructor(
Expand All @@ -23,6 +24,18 @@ class NoResultView @JvmOverloads constructor(
isClickable = false
}

fun setIconResource(@DrawableRes resourceId: Int) {
no_result_view_image.setImageResource(resourceId)
}

fun setMessage(message: String) {
no_result_view_text.text = message
}

fun setButtonText(text: String) {
no_result_view_button.text = text
}

fun release() {
button.setOnClickListener(null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.Lazy
import kotlinx.android.synthetic.main.activity_search_city.*
import org.mozilla.focus.R
import org.mozilla.focus.telemetry.TelemetryWrapper
import org.mozilla.focus.utils.DialogUtils
import org.mozilla.focus.utils.ViewUtils
import org.mozilla.rocket.adapter.AdapterDelegatesManager
import org.mozilla.rocket.adapter.DelegateAdapter
import org.mozilla.rocket.content.appComponent
Expand Down Expand Up @@ -64,6 +67,7 @@ class TravelCitySearchActivity : AppCompatActivity() {
when (actionId) {
EditorInfo.IME_ACTION_SEARCH -> {
recyclerView.findViewHolderForAdapterPosition(1)?.itemView?.performClick()
?: ViewUtils.forceHideKeyboard(search_keyword_edit)
true
}
else -> false
Expand All @@ -77,6 +81,7 @@ class TravelCitySearchActivity : AppCompatActivity() {
initSearchOptionPrompt()
initCityList()
initGoogleSearchAction()
initNoResultView()
}

override fun onResume() {
Expand Down Expand Up @@ -106,6 +111,15 @@ class TravelCitySearchActivity : AppCompatActivity() {
})
}

private fun initNoResultView() {
no_result_view.setIconResource(R.drawable.no_finding)
no_result_view.setMessage(getString(R.string.travel_no_result_state_text))
no_result_view.setButtonText(getString(R.string.travel_google_search_button, getString(R.string.search_engine_name_google)))
no_result_view.setButtonOnClickListener(View.OnClickListener {
searchViewModel.onEmptyViewActionClicked(this@TravelCitySearchActivity, search_keyword_edit.text.toString())
})
}

private fun initCityList() {
recyclerView.let {
it.layoutManager = LinearLayoutManager(this@TravelCitySearchActivity)
Expand All @@ -116,23 +130,21 @@ class TravelCitySearchActivity : AppCompatActivity() {
})
it.adapter = adapter
}
searchViewModel.items.observe(this, Observer {
if (it != null) {
adapter.setData(it)
}

searchViewModel.viewState.observe(this, Observer { viewState ->
clear.visibility = viewState.clearButtonVisibility
adapter.setData(viewState.searchResult)
spinner.isVisible = viewState.isLoading
no_result_view.isVisible = (viewState.error == TravelCitySearchViewState.Error.NotFound)
})

searchViewModel.openCity.observe(this, Observer { city ->
startActivity(TravelCityActivity.getStartIntent(this@TravelCitySearchActivity, city, TelemetryWrapper.Extra_Value.EXPLORE))
})
searchViewModel.changeClearBtnVisibility.observe(this, Observer {
if (it != null) {
clear.visibility = it
}
})
}

companion object {
fun getStartIntent(context: Context) =
Intent(context, TravelCitySearchActivity::class.java).also { it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ class TravelCitySearchViewModel(
private val shouldTravelDiscoveryBeDefault: ShouldTravelDiscoveryBeDefaultUseCase
) : ViewModel() {

private val _items = MutableLiveData<List<DelegateAdapter.UiModel>>()
val items: LiveData<List<DelegateAdapter.UiModel>> = _items
private val _viewState = MutableLiveData<TravelCitySearchViewState>()
val viewState: LiveData<TravelCitySearchViewState> = _viewState

private var searchCityJob: Job? = null
val openCity = SingleLiveEvent<BaseCityData>()
val changeClearBtnVisibility = SingleLiveEvent<Int>()
val openGoogleSearch = SingleLiveEvent<String>()
val showSearchOptionPrompt = SingleLiveEvent<Unit>()

Expand All @@ -49,14 +48,17 @@ class TravelCitySearchViewModel(
searchCityJob?.cancel()
}

_viewState.value = TravelCitySearchViewState(
isLoading = true,
clearButtonVisibility = if (keyword.isEmpty()) View.GONE else View.VISIBLE
)

searchCityJob = viewModelScope.launch {
val btnVisibility: Int
val list = ArrayList<DelegateAdapter.UiModel>()

if (keyword.isEmpty()) {
btnVisibility = View.GONE
_viewState.value = TravelCitySearchViewState()
} else {
btnVisibility = View.VISIBLE
val result = searchCityUseCase(keyword)
if (result is Result.Success && result.data.result.isNotEmpty()) {
list.add(CitySearchResultCategoryUiModel(
Expand All @@ -83,12 +85,18 @@ class TravelCitySearchViewModel(

searchKeyword = keyword
defaultCity = cityResultList[0]
}

// TODO: handle error
_viewState.value = TravelCitySearchViewState(
clearButtonVisibility = View.VISIBLE,
searchResult = list
)
} else {
_viewState.value = TravelCitySearchViewState(
error = TravelCitySearchViewState.Error.NotFound,
clearButtonVisibility = View.VISIBLE
)
}
}
_items.postValue(list)
changeClearBtnVisibility.value = btnVisibility
}
}

Expand Down Expand Up @@ -135,8 +143,24 @@ class TravelCitySearchViewModel(
}
}

fun onEmptyViewActionClicked(context: Context, keyword: String) {
goToGoogleSearch(context, keyword)
}

private fun goToGoogleSearch(context: Context, keyword: String) {
openGoogleSearch.value = SearchUtils.createSearchUrl(context, keyword)
TelemetryWrapper.selectQueryContentHome(TelemetryWrapper.Extra_Value.TRAVEL, TelemetryWrapper.Extra_Value.GOOGLE)
}
}
}

data class TravelCitySearchViewState(
val isLoading: Boolean = false,
val error: Error = Error.None,
val clearButtonVisibility: Int = View.GONE,
val searchResult: List<DelegateAdapter.UiModel> = emptyList()
) {
sealed class Error {
object NotFound : Error()
object None : Error()
}
}
Binary file added app/src/main/res/drawable-xxhdpi/no_finding.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions app/src/main/res/layout/activity_search_city.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider" />

<ProgressBar
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider" />

<org.mozilla.rocket.content.common.ui.NoResultView
android:id="@+id/no_result_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider" />

</androidx.constraintlayout.widget.ConstraintLayout>

</org.mozilla.focus.widget.ResizableKeyboardLayout>
1 change: 1 addition & 0 deletions app/src/main/res/layout/no_result_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
tools:ignore="ContentDescription" />

<TextView
android:id="@+id/no_result_view_text"
style="@style/Body3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Expand Down

0 comments on commit 3765ad4

Please sign in to comment.