Skip to content
This repository has been archived by the owner on Jan 7, 2022. It is now read-only.

Commit

Permalink
Merge pull request #2 from subinkrishna/feature/remote-filter
Browse files Browse the repository at this point in the history
Feature / Remote jobs filter
  • Loading branch information
subinkrishna authored Dec 19, 2018
2 parents fbfbfac + fdefd1f commit c08198f
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.subinkrishna.androidjobs.R
import com.subinkrishna.androidjobs.ext.isExpanded
import com.subinkrishna.androidjobs.ext.isExpandedOrPeeked
import com.subinkrishna.androidjobs.service.model.JobListing
import com.subinkrishna.androidjobs.ui.listing.JobListingEvent.ItemSelectEvent
import com.subinkrishna.androidjobs.ui.listing.JobListingEvent.RemoteToggleEvent
import com.subinkrishna.androidjobs.ui.widget.DividerDecoration
import com.subinkrishna.ext.setGifResource
import io.reactivex.subjects.PublishSubject

Expand All @@ -61,10 +62,12 @@ class JobListingActivity : AppCompatActivity() {
private lateinit var shutter: View
private lateinit var jobDetailsSheet: JobDetailsSheet
private lateinit var bottomSheetBehavior: BottomSheetBehavior<JobDetailsSheet>
private lateinit var errorImage: ImageView
private lateinit var errorText: TextView
private lateinit var statusImage: ImageView
private lateinit var statusText: TextView
private lateinit var remoteToggle: TextView

private val itemSelectEvent = PublishSubject.create<ItemSelectEvent>()
private val remoteToggleEvent = PublishSubject.create<RemoteToggleEvent>()

private val jobListAdapter by lazy {
val itemClickListener = View.OnClickListener { v ->
Expand All @@ -79,11 +82,9 @@ class JobListingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_job_listing)

configureToolbar()
initializeUi(savedInstanceState)

viewModel.start(itemSelectEvent).observe(this, Observer {
configureUi(savedInstanceState)
viewModel.start(itemSelectEvent, remoteToggleEvent).observe(this, Observer {
render(it)
})
}
Expand Down Expand Up @@ -111,19 +112,21 @@ class JobListingActivity : AppCompatActivity() {
supportActionBar?.title = ""
}

private fun initializeUi(savedInstanceState: Bundle? = null) {
private fun configureUi(savedInstanceState: Bundle? = null) {
toolbarContainer = findViewById(R.id.toolbarContainer)
progressContainer = findViewById(R.id.progressIndicatorContainer)
shutter = findViewById(R.id.shutter)
errorImage = findViewById(R.id.androidGifImage)
errorText = findViewById(R.id.errorMessageText)
statusImage = findViewById(R.id.androidGifImage)
statusText = findViewById(R.id.statusMessageText)
remoteToggle = findViewById<TextView>(R.id.remoteToggle).apply {
setOnClickListener { remoteToggleEvent.onNext(RemoteToggleEvent) }
isVisible = false // Show it only after fetching the listing
}

jobList = findViewById<RecyclerView>(R.id.jobList).apply {
this.adapter = jobListAdapter
setHasFixedSize(true)
addItemDecoration(DividerItemDecoration(
this@JobListingActivity,
DividerItemDecoration.VERTICAL))
addItemDecoration(DividerDecoration())
}

jobDetailsSheet = findViewById<JobDetailsSheet>(R.id.bottomSheetContainer).apply {
Expand Down Expand Up @@ -162,21 +165,27 @@ class JobListingActivity : AppCompatActivity() {

progressContainer.isVisible = isLoading && !hasContent
jobList.isVisible = hasContent
errorImage.isVisible = hasError && !hasContent
errorText.isVisible = hasError && !hasContent
remoteToggle.isVisible = true
remoteToggle.isSelected = state.filter == Filter.Remote
statusImage.isVisible = !isLoading && !hasContent
statusText.isVisible = !isLoading && !hasContent

if (hasContent) {
jobListAdapter.submitList(state.content)
if (hasItemInFocus) {
jobDetailsSheet.bind(state.itemInFocus!!)
bottomSheetBehavior.isExpanded = true
}
} else if (hasError) {
errorImage.setGifResource(R.raw.gif_androidify_basketball)
} else if (!isLoading && !hasContent) {
statusImage.setGifResource(R.raw.gif_androidify_basketball)
statusText.setText(R.string.empty_no_listing)
}
else if (hasError) {
statusImage.setGifResource(R.raw.gif_androidify_basketball)
val errorMessage = if (isOnline())
R.string.error_job_listing_unknown
else R.string.error_job_listing_offline
errorText.setText(errorMessage)
statusText.setText(errorMessage)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ import androidx.lifecycle.MutableLiveData
import com.subinkrishna.androidjobs.model.Lce
import com.subinkrishna.androidjobs.service.AndroidJobsApi
import com.subinkrishna.androidjobs.service.RetrofitAndroidJobsApi
import com.subinkrishna.androidjobs.ui.listing.JobListingEvent.FetchJobsEvent
import com.subinkrishna.androidjobs.ui.listing.JobListingEvent.ItemSelectEvent
import com.subinkrishna.androidjobs.ui.listing.JobListingResult.FetchJobsResult
import com.subinkrishna.androidjobs.ui.listing.JobListingResult.ItemSelectResult
import com.subinkrishna.androidjobs.service.model.JobListing
import com.subinkrishna.androidjobs.ui.listing.JobListingEvent.*
import com.subinkrishna.androidjobs.ui.listing.JobListingResult.*
import io.reactivex.BackpressureStrategy
import io.reactivex.Observable
import io.reactivex.ObservableTransformer
Expand All @@ -46,15 +45,24 @@ import timber.log.Timber
*/
class JobListingViewModel(val app: Application) : AndroidViewModel(app) {

private var viewState = JobListingViewState(isLoading = true)
// Current view state
private var viewState = JobListingViewState(isLoading = true, filter = Filter.All)
private val viewStateLive by lazy { MutableLiveData<JobListingViewState>() }

// Holds all the job listings
private var itemsLive = MutableLiveData<List<JobListing>>()

// Holds current listing filter
private var filter = Filter.All

private val disposable by lazy { CompositeDisposable() }
private val api: AndroidJobsApi by lazy { RetrofitAndroidJobsApi() }

// Job fetch is not a user triggered event, hence moving it to ViewModel
private var isJobFetchTriggered = false
private val fetchJobsEvent: Observable<FetchJobsEvent>
get() {
// This event needs to be triggered only once
return if (!isJobFetchTriggered) {
isJobFetchTriggered = true
Observable.just(FetchJobsEvent)
Expand All @@ -69,22 +77,24 @@ class JobListingViewModel(val app: Application) : AndroidViewModel(app) {
}

fun start(
itemClickEvent: Observable<ItemSelectEvent>
itemClickEvent: Observable<ItemSelectEvent>,
remoteToggleEvent: Observable<RemoteToggleEvent>
): LiveData<out JobListingViewState> {
// Merge events and get results
val results: Observable<Lce<out JobListingResult>> = Observable.merge(
fetchJobsEvent.compose(onFetchJobs()),
remoteToggleEvent.compose(onRemoteToggle()),
itemClickEvent.compose(onJobItemClick()))

// Reduce to state & update LiveData
disposable.add(reduceResultToViewState(results)
.distinctUntilChanged()
.log("State")
.doOnNext {
// todo: switch b/w viewStateLive & eventLive here?
viewState = it
viewStateLive.postValue(it)
}
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread())
.subscribe())

Expand All @@ -98,7 +108,7 @@ class JobListingViewModel(val app: Application) : AndroidViewModel(app) {
return ObservableTransformer { upstream ->
upstream.switchMap { _ ->
api.getJobs().toObservable()
.subscribeOn(Schedulers.io())
.doOnNext { itemsLive.postValue(it) }
.map { FetchJobsResult(items = it) }
.onErrorReturn { FetchJobsResult(error = it) }
.map { if (it.error != null) Lce.Error(it) else Lce.Content(it) }
Expand All @@ -107,6 +117,23 @@ class JobListingViewModel(val app: Application) : AndroidViewModel(app) {
}
}

private fun onRemoteToggle(): ObservableTransformer<RemoteToggleEvent, Lce<FilteredListingResult>> {
return ObservableTransformer { upstream ->
upstream
.takeWhile { itemsLive.value?.isNotEmpty() == true }
.map { _ ->
filter = if (filter == Filter.All) Filter.Remote else Filter.All
val filteredItems = when (filter) {
Filter.All -> itemsLive.value
Filter.Remote -> {
itemsLive.value?.filter { it.location.toLowerCase().contains("remote") }
}
}
Lce.Content(FilteredListingResult(items = filteredItems, filter = filter))
}
}
}

private fun onJobItemClick(): ObservableTransformer<ItemSelectEvent, Lce<ItemSelectResult>> {
return ObservableTransformer { upstream ->
upstream.map {
Expand All @@ -119,6 +146,7 @@ class JobListingViewModel(val app: Application) : AndroidViewModel(app) {
results: Observable<Lce<out JobListingResult>>
): Observable<JobListingViewState> {
return results.scan(viewState) { viewState, result ->
Timber.d("result: $result")
when (result) {
is Lce.Loading -> {
viewState.copy(isLoading = true, error = null)
Expand All @@ -129,8 +157,16 @@ class JobListingViewModel(val app: Application) : AndroidViewModel(app) {
viewState.copy(
isLoading = false,
error = null,
content = result.payload.items) }
is ItemSelectResult -> { viewState.copy(itemInFocus = result.payload.item) }
content = result.payload.items)
}
is FilteredListingResult -> {
viewState.copy(
content = result.payload.items,
filter = result.payload.filter)
}
is ItemSelectResult -> {
viewState.copy(itemInFocus = result.payload.item)
}
}
}
is Lce.Error -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.subinkrishna.androidjobs.service.model.JobListing
data class JobListingViewState(
val isLoading: Boolean = false,
val content: List<JobListing>? = null,
val filter: Filter,
val error: Any? = null,
val itemInFocus: JobListing? = null
) {
Expand All @@ -33,8 +34,11 @@ data class JobListingViewState(
}
}

enum class Filter { All, Remote }

sealed class JobListingEvent {
object FetchJobsEvent : JobListingEvent()
object RemoteToggleEvent : JobListingEvent()
data class ItemSelectEvent(val item: JobListing?) : JobListingEvent()
}

Expand All @@ -44,5 +48,10 @@ sealed class JobListingResult {
val error: Throwable? = null
) : JobListingResult()

data class FilteredListingResult(
val items: List<JobListing>? = null,
val filter: Filter
) : JobListingResult()

data class ItemSelectResult(val item: JobListing?) : JobListingResult()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (C) 2018 Subinkrishna Gopi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.subinkrishna.androidjobs.ui.widget

import android.graphics.Canvas
import android.graphics.Paint
import android.view.View
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView

/**
* Divider decoration
*
* @author Subinkrishna Gopi
*/
class DividerDecoration : RecyclerView.ItemDecoration() {

private val dividerPaint by lazy {
Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
color = 0xffcccccc.toInt()
strokeWidth = 1f // 1 px
}
}

override fun onDrawOver(
c: Canvas,
parent: RecyclerView,
state: RecyclerView.State
) {
parent.children.forEach { child ->
if (shouldDrawDividerAbove(parent, child)) {
val top = child.top + child.translationY
c.drawLine(0f, top, parent.width.toFloat(), top, dividerPaint)
}
}
}

private fun shouldDrawDividerAbove(
parent: RecyclerView,
child: View
): Boolean {
return parent.getChildAdapterPosition(child) > 0
}
}
7 changes: 7 additions & 0 deletions app/src/main/res/color/bg_remote_toggle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#00675b" android:state_pressed="true" android:state_selected="true" />
<item android:color="@color/colorAccent" android:state_pressed="false" android:state_selected="true" />
<item android:color="#dcdcdc" android:state_pressed="true" />
<item android:color="@android:color/transparent" />
</selector>
5 changes: 5 additions & 0 deletions app/src/main/res/color/bg_remote_toggle_text.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_selected="true"/>
<item android:color="@color/text" />
</selector>
6 changes: 6 additions & 0 deletions app/src/main/res/drawable/bg_remote_toggle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="20dp" />
<solid android:color="@color/bg_remote_toggle" />
</shape>
2 changes: 1 addition & 1 deletion app/src/main/res/layout/view_job_listing_error.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
tools:background="#ddd" />

<TextView
android:id="@+id/errorMessageText"
android:id="@+id/statusMessageText"
style="@style/AppTextAppearance.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Expand Down
21 changes: 19 additions & 2 deletions app/src/main/res/layout/view_toolbar.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/toolbarContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
Expand All @@ -16,7 +17,23 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Toolbar title"
android:textAppearance="@style/ToolbarTitleTextAppearance" />
android:textAppearance="@style/ToolbarTitleTextAppearance"
tools:text="Toolbar title" />

<TextView
android:id="@+id/remoteToggle"
android:layout_width="wrap_content"
android:layout_height="26dp"
android:layout_gravity="center_vertical|end"
android:gravity="center"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:fontFamily="@font/product_sans"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textColor="@color/bg_remote_toggle_text"
android:background="@drawable/bg_remote_toggle"
android:text="@string/job_listing_remote"
android:textAppearance="@style/AppTextAppearance.Small" />

</FrameLayout>
Loading

0 comments on commit c08198f

Please sign in to comment.