Skip to content

Commit d09ede2

Browse files
Prepare UseCase3 for exercise about flow exception handling
1 parent a9eab07 commit d09ede2

File tree

12 files changed

+230
-2
lines changed

12 files changed

+230
-2
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
android:name=".usecases.flow.usecase2.FlowUseCase2Activity"
104104
android:exported="true" />
105105

106+
<activity
107+
android:name=".usecases.flow.usecase3.FlowUseCase3Activity"
108+
android:exported="true" />
109+
106110
</application>
107111

108112
</manifest>

app/src/main/java/com/lukaslechner/coroutineusecasesonandroid/base/UseCase.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.lukaslechner.coroutineusecasesonandroid.usecases.coroutines.usecase8.
2525
import com.lukaslechner.coroutineusecasesonandroid.usecases.coroutines.usecase9.DebuggingCoroutinesActivity
2626
import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.usecase1.FlowUseCase1Activity
2727
import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.usecase2.FlowUseCase2Activity
28+
import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.usecase3.FlowUseCase3Activity
2829
import kotlinx.parcelize.Parcelize
2930

3031
@Parcelize
@@ -150,6 +151,7 @@ private val coroutinesUseCases =
150151

151152
const val flowUseCase1Description = "#1 Flow Basics"
152153
const val flowUseCase2Description = "#2 Basic Intermediate operators"
154+
const val flowUseCase3Description = "#3 Exception Handling"
153155

154156
private val flowUseCases =
155157
UseCaseCategory(
@@ -161,6 +163,9 @@ private val flowUseCases =
161163
),UseCase(
162164
flowUseCase2Description,
163165
FlowUseCase2Activity::class.java
166+
),UseCase(
167+
flowUseCase3Description,
168+
FlowUseCase3Activity::class.java
164169
)
165170
)
166171
)

app/src/main/java/com/lukaslechner/coroutineusecasesonandroid/usecases/flow/usecase2/FlowUseCase2ViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,9 @@ class FlowUseCase2ViewModel(
6969
.onStart {
7070
emit(UiState.Loading)
7171
}
72+
.catch { throwable ->
73+
Timber.tag("Flow").d("Handle exception in catch operator $throwable")
74+
emit(UiState.Error("Something went wrong..."))
75+
}
7276
.asLiveData(defaultDispatcher)
7377
}

app/src/main/java/com/lukaslechner/coroutineusecasesonandroid/usecases/flow/usecase2/MockApiBehavior.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ fun mockApi(context: Context) =
1313
path = "/current-stock-prices",
1414
body = { Gson().toJson(fakeCurrentStockPrices(context)) },
1515
status = 200,
16-
delayInMs = 1500,
17-
errorFrequencyInPercent = 50
16+
delayInMs = 1500
1817
)
1918
)

app/src/main/java/com/lukaslechner/coroutineusecasesonandroid/usecases/flow/usecase2/StockPriceDataSource.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.mock.Stock
55
import kotlinx.coroutines.delay
66
import kotlinx.coroutines.flow.Flow
77
import kotlinx.coroutines.flow.flow
8+
import kotlinx.coroutines.flow.retry
9+
import retrofit2.HttpException
810
import timber.log.Timber
911

1012
interface StockPriceDataSource {
@@ -20,5 +22,14 @@ class NetworkStockPriceDataSource(mockApi: FlowMockApi) : StockPriceDataSource {
2022
emit(currentStockList)
2123
delay(5_000)
2224
}
25+
}.retry { cause ->
26+
Timber.tag("Flow").d("Enter retry operator with $cause")
27+
val shouldRetry = cause is HttpException
28+
29+
if (shouldRetry) {
30+
delay(5_000)
31+
}
32+
33+
shouldRetry
2334
}
2435
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.lukaslechner.coroutineusecasesonandroid.usecases.flow.usecase3
2+
3+
import android.os.Bundle
4+
import androidx.activity.viewModels
5+
import com.lukaslechner.coroutineusecasesonandroid.base.BaseActivity
6+
import com.lukaslechner.coroutineusecasesonandroid.base.flowUseCase3Description
7+
import com.lukaslechner.coroutineusecasesonandroid.databinding.ActivityFlowUsecase1Binding
8+
import com.lukaslechner.coroutineusecasesonandroid.utils.setGone
9+
import com.lukaslechner.coroutineusecasesonandroid.utils.setVisible
10+
import com.lukaslechner.coroutineusecasesonandroid.utils.toast
11+
import org.joda.time.LocalDateTime
12+
import org.joda.time.format.DateTimeFormat
13+
14+
class FlowUseCase3Activity : BaseActivity() {
15+
16+
private val binding by lazy { ActivityFlowUsecase1Binding.inflate(layoutInflater) }
17+
private val adapter = StockAdapter()
18+
19+
private val viewModel: FlowUseCase3ViewModel by viewModels {
20+
ViewModelFactory(NetworkStockPriceDataSource(mockApi(applicationContext)))
21+
}
22+
23+
override fun onCreate(savedInstanceState: Bundle?) {
24+
super.onCreate(savedInstanceState)
25+
setContentView(binding.root)
26+
binding.recyclerView.adapter = adapter
27+
28+
viewModel.currentStockPriceAsLiveData.observe(this) { uiState ->
29+
if (uiState != null) {
30+
render(uiState)
31+
}
32+
}
33+
}
34+
35+
private fun render(uiState: UiState) {
36+
when (uiState) {
37+
is UiState.Loading -> {
38+
binding.progressBar.setVisible()
39+
binding.recyclerView.setGone()
40+
}
41+
is UiState.Success -> {
42+
binding.recyclerView.setVisible()
43+
binding.lastUpdateTime.text =
44+
"lastUpdateTime: ${LocalDateTime.now().toString(DateTimeFormat.fullTime())}"
45+
adapter.stockList = uiState.stockList
46+
binding.progressBar.setGone()
47+
}
48+
is UiState.Error -> {
49+
toast(uiState.message)
50+
binding.progressBar.setGone()
51+
}
52+
}
53+
}
54+
55+
override fun getToolbarTitle() = flowUseCase3Description
56+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.lukaslechner.coroutineusecasesonandroid.usecases.flow.usecase3
2+
3+
import androidx.lifecycle.LiveData
4+
import androidx.lifecycle.asLiveData
5+
import com.lukaslechner.coroutineusecasesonandroid.base.BaseViewModel
6+
import kotlinx.coroutines.flow.map
7+
import kotlinx.coroutines.flow.onCompletion
8+
import kotlinx.coroutines.flow.onStart
9+
import timber.log.Timber
10+
11+
class FlowUseCase3ViewModel(
12+
stockPriceDataSource: StockPriceDataSource
13+
) : BaseViewModel<UiState>() {
14+
15+
/*
16+
Exercise: Flow Exception Handling
17+
18+
Tasks:
19+
- Adjust code in StockPriceDataSource and FlowUseCase3ViewModel
20+
21+
Exception Handling Goals:
22+
- for HttpExceptions in the datasource
23+
- re-collect from the flow
24+
- delay for 5 seconds before re-collecting the flow
25+
- for all other Exceptions within the whole flow pipeline
26+
- show toast with error message by emitting UiState.Error
27+
*/
28+
29+
val currentStockPriceAsLiveData: LiveData<UiState> = stockPriceDataSource
30+
.latestStockList
31+
.map { stockList ->
32+
UiState.Success(stockList) as UiState
33+
}
34+
.onStart {
35+
emit(UiState.Loading)
36+
}
37+
.onCompletion {
38+
Timber.tag("Flow").d("Flow has completed.")
39+
}
40+
.asLiveData()
41+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.lukaslechner.coroutineusecasesonandroid.usecases.flow.usecase3
2+
3+
import android.content.Context
4+
import com.google.gson.Gson
5+
import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.mock.createFlowMockApi
6+
import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.mock.fakeCurrentStockPrices
7+
import com.lukaslechner.coroutineusecasesonandroid.utils.MockNetworkInterceptor
8+
9+
fun mockApi(context: Context) =
10+
createFlowMockApi(
11+
MockNetworkInterceptor()
12+
.mock(
13+
path = "/current-stock-prices",
14+
body = { Gson().toJson(fakeCurrentStockPrices(context)) },
15+
status = 200,
16+
delayInMs = 1500,
17+
errorFrequencyInPercent = 50
18+
)
19+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.lukaslechner.coroutineusecasesonandroid.usecases.flow.usecase3
2+
3+
import android.view.LayoutInflater
4+
import android.view.View
5+
import android.view.ViewGroup
6+
import androidx.recyclerview.widget.RecyclerView
7+
import com.lukaslechner.coroutineusecasesonandroid.R
8+
import com.lukaslechner.coroutineusecasesonandroid.databinding.RecyclerviewItemStockBinding
9+
import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.mock.Stock
10+
import java.text.NumberFormat
11+
12+
class StockAdapter: RecyclerView.Adapter<StockAdapter.ViewHolder>() {
13+
14+
var stockList: List<Stock>? = null
15+
set(value) {
16+
field = value
17+
notifyDataSetChanged()
18+
}
19+
20+
private val formatter: NumberFormat = NumberFormat.getCurrencyInstance()
21+
22+
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
23+
val binding = RecyclerviewItemStockBinding.bind(view)
24+
}
25+
26+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
27+
val view = LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_item_stock, parent, false)
28+
return ViewHolder(view)
29+
}
30+
31+
override fun onBindViewHolder(holder: ViewHolder, position: Int) = with(holder.binding){
32+
val stock = stockList?.get(position) ?: return@with
33+
rank.text = stock.rank.toString()
34+
name.text = stock.name
35+
val currentPriceFormatted: String = formatter.format(stock.currentPrice)
36+
currentPrice.text = currentPriceFormatted
37+
}
38+
39+
override fun getItemCount(): Int {
40+
return stockList?.size ?: 0
41+
}
42+
43+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.lukaslechner.coroutineusecasesonandroid.usecases.flow.usecase3
2+
3+
import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.mock.FlowMockApi
4+
import com.lukaslechner.coroutineusecasesonandroid.usecases.flow.mock.Stock
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.flow
8+
import timber.log.Timber
9+
10+
interface StockPriceDataSource {
11+
val latestStockList: Flow<List<Stock>>
12+
}
13+
14+
class NetworkStockPriceDataSource(mockApi: FlowMockApi) : StockPriceDataSource {
15+
16+
override val latestStockList: Flow<List<Stock>> = flow {
17+
while (true) {
18+
Timber.tag("Flow").d("Fetching current stock prices")
19+
val currentStockList = mockApi.getCurrentStockPrices()
20+
emit(currentStockList)
21+
delay(5_000)
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)