Skip to content

Commit

Permalink
Add State Management for the UI (#5)
Browse files Browse the repository at this point in the history
* Feat(test): add required dependencies for testing

* test(network): add test file for the app NetworkModule

* Feat(network): add header provider to the NetworkModule

* docs(Readme): move test item to done section

* Test(coroutine): add test requirement rule for the Coroutines

* Test(dagger): add required test implementations for the Dagger Hilt

* Chore(gradle): add CustomTestRunner to support Dagger Hilt

* Test(ui): add some UI tests

* Chore(gradle): add new test dependencies for the project

* Fix(test): fix failed test when HomeFragmentTest executed (test passed)

* Feat(test): add a mock file used to hold some fake data for testing

* Test(usecase): add new passed test cases for the current and location weather UseCases

* Test(usecase): add new passed test cases for the GetNextDaysForecastsUseCase

* Chore(IDE): Update project configs

* Refactor(project): Change project name

* Chore(docs): add items to the backlog

* Feat(icon): add app icon

* Chore(IDE): Update project settings

* Feat(logo): Add application logo

* Feat(common): Add extension for Fragment to easier using SnackBar

* Feat(vm): Add base class to provide network error into ViewState

* Feat(vm): Add new error constants

* Refactor(ui): changed to network connection error

* Refactor(data): moved into safe getResult call body

* Refactor(ui): Use new extension to show snackBar

* Fix(vm): Fix failure detection issues and notify the view from event properly

* Refactor(image-api): Use better API to showing random images

* Refactor(package): move di to a new package

* Feat(di): Add new di ImageModule for the Coil + use in UI

* Fix(package): Fix package name

* Chore(IDE): Update project settings

* Feat(release): Add required implementations for generating release signed APK

* Feat(art): Add some shots from the release app

* Feat(ci): Add different jobs for run tests, release apk and upload

* Chore(art): Add extension

* Feat(art): Add new screen shots

* Fix(ci): Fix typo

* Fix(docs): Fix typo

* Chore(docs): Add item to done section

* WIP(ci): Upload entire directory to artifacts

* WIP(ci): Change the path

* Refactor(ci): Change master to main. Upload only release apk file

* Refactor(ci): Run only on main and develop branch

* Docs(readme): Add release information to docs

* Docs(readme): Add reference to all releases

* Chore(IDE): Update Project settings

* Chore(anim): Add new fading effect on transitions

* Feat(data): Add new Error handling code for the project. Because the current API does not have any http result wrapper

* Docs(readme): Add some notes in docs

* Fix(domain): Remove useless code

* Feat(docs): Add new ui screen shot

* Chore(docs): Add new release version url

* Chore(docs): Move offline item to done section

* Feat(gradle): add required dependencies for the offline db

* Feat(base-db): add new base implementations required for offline room database

* Feat(data): add new base Mapper class to transform Entities to Domain models

* Feat(data): add new data access object for the places table

* Refactor(di): Provide domain repositories to use in UseCases, and remove data repository wrong dependencies in UseCases and ViewModels

* Refactor(core): Repackaged

* Feat(domain): Add some domain models

* Feat(data): Add db configs

* Chore(docs): Add cache strategy info

* Chore(project): Upgrade version to create publish release

* Refactor(all): remove useless file

* Feat(data): add new DAOs to the app database

* Refactor(di): Remove useless context from class

* Refactor(data): repackaged

* Feat(data): add new entity in database

* Feat(data): add new helper class to communicate with dao

* Refactor(data): Update Base Mapper for domain-entity to use with inject method in other classes

* Refactor(data): repackaged, Update WebServices data type

* Feat(domain): Add new domain models for the project

* Feat(domain): Add new domain repository to use in domain useCases to be implemented in data layer

* Refactor(domain): change id type to long

* Refactor(presentation-domain): change used model type from entity to domain

* Refactor(test-domain): change used data schema

* Feat(data): add new data mapper to map domain models to entity or inverse

* WIP(data): add new base data source factory to get related datasource instance

* Feat(data): add new datasource with related implementor for local and remote useCases

* Feat(data): add new remote network data entity response

* Feat(common): add new context extension to check network

* Feat(data): add new enum class to handle different cache types of First Offline and First Online data management

* Refactor(data): Remove unused datasource

* Feat(domain): Add new useCase to get data from Location details repository

* Feat(data): Add new data layer repository to get just remote data from location details

* Feat(data): Add new data layer repository to get remote/local data with cache strategy methods of First Online and First Offline

* Feat(di-domain): Add new repository

* Feat(test,domain): Add new UseCase to get data from repository

* Refactor(all): Add new Logger Tag retrieve possible length size for TAG

* Fix(ci): Add branch to ci job

* Test(dagger): add required test implementations for the Dagger Hilt

* Fix(test): fix failed test when HomeFragmentTest executed (test passed)

* Chore(docs): Move offline item to done section

* Feat(gradle): add required dependencies for the offline db

* Feat(base-db): add new base implementations required for offline room database

* Feat(data): add new base Mapper class to transform Entities to Domain models

* Feat(data): add new data access object for the places table

* Refactor(di): Provide domain repositories to use in UseCases, and remove data repository wrong dependencies in UseCases and ViewModels

* Refactor(core): Repackaged

* Feat(domain): Add some domain models

* Feat(data): Add db configs

* Chore(docs): Add cache strategy info

* Chore(project): Upgrade version to create publish release

* Refactor(all): remove useless file

* Feat(data): add new DAOs to the app database

* Refactor(di): Remove useless context from class

* Refactor(data): repackaged

* Feat(data): add new entity in database

* Feat(data): add new helper class to communicate with dao

* Refactor(data): Update Base Mapper for domain-entity to use with inject method in other classes

* Refactor(data): repackaged, Update WebServices data type

* Feat(domain): Add new domain models for the project

* Feat(domain): Add new domain repository to use in domain useCases to be implemented in data layer

* Refactor(domain): change id type to long

* Refactor(presentation-domain): change used model type from entity to domain

* Refactor(test-domain): change used data schema

* Feat(data): add new data mapper to map domain models to entity or inverse

* WIP(data): add new base data source factory to get related datasource instance

* Feat(data): add new datasource with related implementor for local and remote useCases

* Feat(data): add new remote network data entity response

* Feat(common): add new context extension to check network

* Feat(data): add new enum class to handle different cache types of First Offline and First Online data management

* Refactor(data): Remove unused datasource

* Feat(domain): Add new useCase to get data from Location details repository

* Feat(data): Add new data layer repository to get just remote data from location details

* Feat(data): Add new data layer repository to get remote/local data with cache strategy methods of First Online and First Offline

* Feat(di-domain): Add new repository

* Feat(test,domain): Add new UseCase to get data from repository

* Refactor(all): Add new Logger Tag retrieve possible length size for TAG

* Fix(ci): Add branch to ci job

* Test(dagger): add required test implementations for the Dagger Hilt

* Fix(test): fix failed test when HomeFragmentTest executed (test passed)

* Feat(data): add new base Mapper class to transform Entities to Domain models

* Feat(data): add new data access object for the places table

* Refactor(all): remove useless file

* Feat(data): add new datasource with related implementor for local and remote useCases

* Refactor(data): Remove unused datasource

* Test(dagger): add required test implementations for the Dagger Hilt

* Fix(test): fix failed test when HomeFragmentTest executed (test passed)

* Test(usecase): add new passed test cases for the current and location weather UseCases

* Feat(data): add new base Mapper class to transform Entities to Domain models

* Feat(data): add new data access object for the places table

* Refactor(all): remove useless file

* Feat(data): add new datasource with related implementor for local and remote useCases

* Refactor(data): Remove unused datasource

* Feat(presentation): Add ui state to show data in view

* Refactor(common): Add Main Dispatcher

* Refactor(data, domain): Update package

* Feat(presentation): Add UI state management

* Fix(data): Fix package name

* Refactor(domain): Use DataState instead of result to emit network data state from useCase to viewModel

* Test(dagger): add required test implementations for the Dagger Hilt

* Fix(test): fix failed test when HomeFragmentTest executed (test passed)

* Feat(data): add new base Mapper class to transform Entities to Domain models

* Feat(data): add new data access object for the places table

* Refactor(all): remove useless file

* Feat(data): add new datasource with related implementor for local and remote useCases

* Refactor(data): Remove unused datasource

* Test(dagger): add required test implementations for the Dagger Hilt

* Fix(test): fix failed test when HomeFragmentTest executed (test passed)

* Test(usecase): add new passed test cases for the current and location weather UseCases

* Feat(data): add new base Mapper class to transform Entities to Domain models

* Feat(data): add new data access object for the places table

* Refactor(all): remove useless file

* Feat(data): add new datasource with related implementor for local and remote useCases

* Refactor(data): Remove unused datasource

* Test(dagger): add required test implementations for the Dagger Hilt

* Fix(test): fix failed test when HomeFragmentTest executed (test passed)

* Feat(data): add new base Mapper class to transform Entities to Domain models

* Feat(data): add new data access object for the places table

* Refactor(all): remove useless file

* Feat(data): add new datasource with related implementor for local and remote useCases

* Refactor(data): Remove unused datasource

* Test(dagger): add required test implementations for the Dagger Hilt

* Fix(test): fix failed test when HomeFragmentTest executed (test passed)

* Test(usecase): add new passed test cases for the current and location weather UseCases

* Feat(data): add new base Mapper class to transform Entities to Domain models

* Feat(data): add new data access object for the places table

* Refactor(all): remove useless file

* Feat(data): add new datasource with related implementor for local and remote useCases

* Refactor(data): Remove unused datasource

* Feat(presentation): Add ui state to show data in view

* Refactor(common): Add Main Dispatcher

* Refactor(data, domain): Update package

* Feat(presentation): Add UI state management

* Fix(data): Fix package name

* Refactor(domain): Use DataState instead of result to emit network data state from useCase to viewModel
  • Loading branch information
MortezaNedaei authored May 29, 2022
1 parent 8d58d2d commit 163bec5
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 170 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

Expand All @@ -19,7 +20,7 @@ import kotlinx.coroutines.launch
*/
context(Fragment)
fun <T> StateFlow<T>.observe(block: (value: T) -> Unit) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect { state ->
block(state)
Expand Down Expand Up @@ -52,7 +53,7 @@ fun <T> StateFlow<T>.observe(block: (value: T) -> Unit) {
*/
context(Fragment)
fun <T> StateFlow<T>.observe(vararg block: (value: T) -> Unit) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
block.forEach { flow ->
launch {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mooncascade.data.mapper
package com.mooncascade.data.mapper.location

import com.mooncascade.data.network.entity.location.*
import com.mooncascade.domain.model.location.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.mooncascade.data.network.datasource

import android.util.Log
import com.mooncascade.common.extensions.TAG
import com.mooncascade.data.mapper.location.toDomain
import com.mooncascade.data.mapper.toDomain
import com.mooncascade.data.network.service.WeatherApi
import com.mooncascade.data.respository.datasource.base.BaseDataSource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.mooncascade.data.respository

import android.util.Log
import com.mooncascade.common.extensions.TAG
import com.mooncascade.data.mapper.toDomain
import com.mooncascade.data.mapper.location.toDomain
import com.mooncascade.data.network.service.WeatherApi
import com.mooncascade.data.respository.datasource.base.BaseDataSource
import com.mooncascade.di.qualifier.IoDispatcher
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.mooncascade.domain.interactor

import com.mooncascade.domain.model.DataState
import com.mooncascade.di.qualifier.IoDispatcher
import com.mooncascade.domain.model.location.Location
import com.mooncascade.domain.respository.LocationDetailsRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject

/**
Expand All @@ -17,8 +19,17 @@ data class LocationWeatherParams(
class GetLocationDetailsUseCase @Inject constructor(
private val repository: LocationDetailsRepository,
@IoDispatcher private val coroutineDispatcher: CoroutineDispatcher
) : UseCase<LocationWeatherParams?, Flow<Result<Location?>>>(coroutineDispatcher) {
) : UseCase<LocationWeatherParams?, Flow<DataState<Location?>>>(coroutineDispatcher) {

override suspend fun execute(parameters: LocationWeatherParams?): Flow<Result<Location?>> =
repository.fetchLocationWeather(parameters?.locationId!!)
override suspend fun execute(parameters: LocationWeatherParams?): Flow<DataState<Location?>> =
flow {
emit(DataState.Loading)
repository.fetchLocationWeather(parameters?.locationId!!).collect { result ->
if (result.isSuccess)
emit(DataState.Success(result.getOrNull()))
else
emit(DataState.Error(result.exceptionOrNull()?.localizedMessage ?: ""))
}

}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.mooncascade.domain.interactor

import com.mooncascade.domain.model.DataState
import com.mooncascade.di.qualifier.IoDispatcher
import com.mooncascade.domain.model.forecast.Forecast
import com.mooncascade.domain.respository.ForecastRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject

/**
Expand All @@ -16,8 +18,17 @@ class ForecastParams
class GetNextDaysForecastsUseCase @Inject constructor(
private val repository: ForecastRepository,
@IoDispatcher private val coroutineDispatcher: CoroutineDispatcher
) : UseCase<ForecastParams?, Flow<Result<List<Forecast>?>>>(coroutineDispatcher) {
) : UseCase<ForecastParams?, Flow<DataState<List<Forecast>?>>>(coroutineDispatcher) {

override suspend fun execute(parameters: ForecastParams?): Flow<Result<List<Forecast>?>> =
repository.fetchForecasts()
override suspend fun execute(parameters: ForecastParams?): Flow<DataState<List<Forecast>?>> =
flow {
emit(DataState.Loading)
repository.fetchForecasts().collect { result ->
if (result.isSuccess)
emit(DataState.Success(result.getOrNull()))
else
emit(DataState.Error(result.exceptionOrNull()?.localizedMessage ?: ""))
}

}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.mooncascade.domain.interactor

import com.mooncascade.domain.model.DataState
import com.mooncascade.di.qualifier.IoDispatcher
import com.mooncascade.domain.model.current.Observation
import com.mooncascade.domain.respository.ObservationRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject

/**
Expand All @@ -16,8 +18,18 @@ class ObservationsUseCaseParams
class GetObservationsUseCase @Inject constructor(
private val repository: ObservationRepository,
@IoDispatcher private val coroutineDispatcher: CoroutineDispatcher
) : UseCase<ObservationsUseCaseParams?, Flow<Result<List<Observation>?>>>(coroutineDispatcher) {
) : UseCase<ObservationsUseCaseParams?, Flow<DataState<List<Observation>?>>>(coroutineDispatcher) {

override suspend fun execute(parameters: ObservationsUseCaseParams?): Flow<DataState<List<Observation>?>> =
flow {
emit(DataState.Loading)
repository.fetchObservations().collect { result ->
if (result.isSuccess)
emit(DataState.Success(result.getOrNull()))
else
emit(DataState.Error(result.exceptionOrNull()?.localizedMessage ?: ""))
}

}

override suspend fun execute(parameters: ObservationsUseCaseParams?): Flow<Result<List<Observation>?>> =
repository.fetchObservations()
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
package com.mooncascade.domain.model

import com.mooncascade.domain.model.ViewState.Status.*
import com.mooncascade.domain.model.DataState.Status.*


/**
* a utility sealed class that communicates the current state of Network Call to the UI Layer.
*/
sealed class ViewState<out T>(
sealed class DataState<out T>(
val data: T? = null,
val message: String? = null,
val status: Status,
val networkError: Boolean? = false
) {
class Success<T>(data: T?) : ViewState<T>(data, null, SUCCESS)
class Success<T>(data: T?) : DataState<T>(data, null, SUCCESS)
class Error<T>(message: String, data: T? = null, networkError: Boolean? = false) :
ViewState<T>(data, message, ERROR, networkError)
DataState<T>(data, message, ERROR, networkError)

object Loading : ViewState<Nothing>(status = LOADING)
object Idle : ViewState<Nothing>(status = IDLE)
object Loading : DataState<Nothing>(status = LOADING)
object Idle : DataState<Nothing>(status = IDLE)

enum class Status {
SUCCESS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package com.mooncascade.presentation.base

import androidx.lifecycle.ViewModel
import com.mooncascade.domain.model.ViewState
import com.mooncascade.domain.model.DataState
import com.mooncascade.presentation.utils.Constants
import java.io.IOException
import javax.inject.Inject

open class BaseViewModel @Inject constructor() : ViewModel() {

protected fun <T> makeError(e: Throwable?, TAG: String, message: String): ViewState.Error<T> {
protected fun <T> makeError(e: Throwable?, TAG: String, message: String): DataState.Error<T> {
return if (e is IOException) {
ViewState.Error(
DataState.Error(
Constants.ERROR_NETWORK, null, true
)
} else {
ViewState.Error(
DataState.Error(
e?.message ?: "$TAG: $message"
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.mooncascade.common.materialContainerTransform
import com.mooncascade.data.network.service.WeatherApi
import com.mooncascade.databinding.FragmentPlaceDetailsBinding
import com.mooncascade.di.qualifier.MainDispatcher
import com.mooncascade.domain.model.ViewState
import com.mooncascade.domain.model.current.Observation
import com.mooncascade.domain.model.location.Location
import com.mooncascade.presentation.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
Expand Down Expand Up @@ -72,53 +72,30 @@ class PlaceDetailsFragment : BaseFragment() {
super.onViewCreated(view, savedInstanceState)

initView()

initLocationWeatherData()
subscribeUi()
}


private fun initLocationWeatherData() = viewModel.locationWeatherFlow.observe { forecast ->
when (forecast.status) {
ViewState.Status.SUCCESS -> {
binding.includeLoading.progressBar.gone()
dispatchNewDataToView(forecast.data)
private fun subscribeUi() = viewModel.uiState.observe { uiState ->
with(uiState) {
updateProgressBar(isLoading)
observation?.let {
setObservation(it)
}
ViewState.Status.ERROR -> {
binding.includeLoading.progressBar.gone()
snack(forecast.message ?: "")
location?.let {
setLocationDetail(it)
}
ViewState.Status.LOADING -> {
binding.includeLoading.progressBar.visible()
error?.let {
snack(it)
}
else -> {}
}

}


private fun initView() {

initAppBar()

binding.animationView.setAnimation(
viewModel.getCurrentWeatherAnimation(place.phenomenon)
)
binding.tvCityName.text = place.name.also { binding.tvCityName2.text = it }
binding.tvTemp.text = viewModel.getLocationWeatherDegrees(place.airtemperature)
binding.tvTemp2.text = viewModel.getLocationWeatherDegrees(place.airtemperature, true)
binding.tvWeatherStatus.text = place.phenomenon.also {
binding.tvWeatherStatus2.text = getString(R.string.format_weather_status, it)
}
binding.tvWindSpeed.text =
getString(
R.string.format_wind_speed,
place.windspeed
).also { binding.tvWindSpeed2.text = it }
binding.tvAirPressure.text =
getString(
R.string.format_air_pressure,
place.airpressure
).also { binding.tvAirPressure2.text = it }

with(binding.imgCover) {
val request = ImageRequest.Builder(context)
.data(WeatherApi.Endpoints.GET_RANDOM_IMAGE)
Expand All @@ -129,10 +106,37 @@ class PlaceDetailsFragment : BaseFragment() {
}
}

/**
* show extra arg data from previous screen
*/
private fun setObservation(data: Observation?) = data?.let { observation ->
with(observation) {
binding.animationView.setAnimation(
viewModel.getCurrentWeatherAnimation(phenomenon)
)
binding.tvCityName.text = name.also { binding.tvCityName2.text = it }
binding.tvTemp.text = viewModel.getLocationWeatherDegrees(airtemperature)
binding.tvTemp2.text = viewModel.getLocationWeatherDegrees(airtemperature, true)
binding.tvWeatherStatus.text = phenomenon.also {
binding.tvWeatherStatus2.text = getString(R.string.format_weather_status, it)
}
binding.tvWindSpeed.text =
getString(
R.string.format_wind_speed,
windspeed
).also { binding.tvWindSpeed2.text = it }
binding.tvAirPressure.text =
getString(
R.string.format_air_pressure,
airpressure
).also { binding.tvAirPressure2.text = it }
}
}

/**
* show new API call data in the view
*/
private fun dispatchNewDataToView(data: Location?) = data?.let { location ->
private fun setLocationDetail(data: Location?) = data?.let { location ->
with(location) {
binding.tvVisibility.text = getString(R.string.format_visibility2, visibility)
binding.tvWindDirection.text = getString(
Expand Down Expand Up @@ -164,6 +168,14 @@ class PlaceDetailsFragment : BaseFragment() {
}
}

private fun updateProgressBar(isLoading: Boolean) {
if (isLoading) {
binding.includeLoading.progressBar.visible()
} else {
binding.includeLoading.progressBar.gone()
}
}


override fun onDestroyView() {
super.onDestroyView()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.mooncascade.presentation.ui.details

import com.mooncascade.domain.model.current.Observation
import com.mooncascade.domain.model.location.Location


/**
* PlaceDetails UI State
*/
data class PlaceDetailsUiState(
val isLoading: Boolean = false,
val observation: Observation? = null, // extra arg from navigation
val location: Location? = null, // new network response data
val error: String? = null,
)
Loading

0 comments on commit 163bec5

Please sign in to comment.