Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Network Monitor to Detect Offline Mode #70

Merged
merged 16 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions app/src/main/kotlin/com/andreolas/movierama/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import com.andreolas.movierama.ui.MovieApp
import com.andreolas.movierama.ui.rememberMovieAppState
import com.divinelink.core.data.network.NetworkMonitor
import com.divinelink.core.designsystem.theme.AppTheme
import com.divinelink.core.designsystem.theme.Theme
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

@ExperimentalAnimationApi
class MainActivity : ComponentActivity() {
class MainActivity :
ComponentActivity(),
KoinComponent {

private val viewModel: MainViewModel by viewModel()

private val networkMonitor: NetworkMonitor by inject<NetworkMonitor>()

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.action == Intent.ACTION_VIEW) {
Expand All @@ -27,19 +35,23 @@ class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
val darkTheme = shouldUseDarkTheme(
uiState = viewModel.uiState.collectAsState().value,
selectedTheme = viewModel.theme.collectAsState().value,
)

val appState = rememberMovieAppState(
networkMonitor = networkMonitor,
)

AppTheme(
useDarkTheme = darkTheme,
dynamicColor = viewModel.materialYou.collectAsState().value,
blackBackground = viewModel.blackBackgrounds.collectAsState().value,
) {
MovieApp(
appState = appState,
uiState = viewModel.uiState.collectAsState().value,
uiEvent = viewModel.uiEvent.collectAsState().value,
onConsumeEvent = viewModel::consumeUiEvent,
Expand Down
17 changes: 14 additions & 3 deletions app/src/main/kotlin/com/andreolas/movierama/home/ui/HomeContent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ import com.divinelink.core.designsystem.theme.AppTheme
import com.divinelink.core.designsystem.theme.SearchBarShape
import com.divinelink.core.designsystem.theme.dimensions
import com.divinelink.core.model.home.HomeMode
import com.divinelink.core.model.home.HomePage
import com.divinelink.core.model.media.MediaItem
import com.divinelink.core.ui.EmptyContent
import com.divinelink.core.ui.Previews
import com.divinelink.core.ui.TestTags.MEDIA_LIST_TAG
import com.divinelink.core.ui.blankslate.BlankSlate
import com.divinelink.core.ui.components.Filter
import com.divinelink.core.ui.components.FilterBar
import com.divinelink.core.ui.components.LoadingContent
Expand All @@ -51,6 +52,7 @@ fun HomeContent(
onNavigateToDetails: (MediaItem) -> Unit,
onFilterClick: (Filter) -> Unit,
onClearFiltersClick: () -> Unit,
onRetryClick: () -> Unit,
onNavigateToSettings: () -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
Expand Down Expand Up @@ -102,8 +104,11 @@ fun HomeContent(
label = "HomeContentEmptyTransition",
) { isEmpty ->
when (isEmpty) {
true -> viewState.emptyContentUiState?.let {
EmptyContent(it)
true -> if (viewState.blankSlate != null) {
BlankSlate(
uiState = viewState.blankSlate,
onRetry = viewState.retryAction?.let { onRetryClick },
)
}
false -> AnimatedContent(
targetState = viewState.mode,
Expand Down Expand Up @@ -206,7 +211,12 @@ private fun HomeContentPreview() {
filteredResults = null,
isSearchLoading = false,
query = "",
pages = mapOf(
HomePage.Popular to 1,
HomePage.Search to 1,
),
mode = HomeMode.Browser,
retryAction = null,
),
onMarkAsFavoriteClicked = {},
onLoadNextPage = {},
Expand All @@ -216,6 +226,7 @@ private fun HomeContentPreview() {
onFilterClick = {},
onClearFiltersClick = {},
onNavigateToSettings = {},
onRetryClick = {},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ fun HomeScreen(
onNavigateToSettings = {
navigator.navigate(SettingsScreenDestination())
},
onRetryClick = viewModel::onRetryClick,
)
}
113 changes: 91 additions & 22 deletions app/src/main/kotlin/com/andreolas/movierama/home/ui/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import androidx.lifecycle.viewModelScope
import com.andreolas.movierama.home.domain.usecase.FetchMultiInfoSearchUseCase
import com.andreolas.movierama.home.domain.usecase.GetFavoriteMoviesUseCase
import com.andreolas.movierama.home.domain.usecase.GetPopularMoviesUseCase
import com.divinelink.core.commons.ErrorHandler
import com.divinelink.core.commons.domain.data
import com.divinelink.core.domain.MarkAsFavoriteUseCase
import com.divinelink.core.model.home.HomeMode
import com.divinelink.core.model.home.HomePage
import com.divinelink.core.model.media.MediaItem
import com.divinelink.core.network.media.model.movie.MoviesRequestApi
import com.divinelink.core.network.media.model.search.multi.MultiSearchRequestApi
import com.divinelink.core.ui.UIText
import com.divinelink.core.ui.blankslate.BlankSlateState
import com.divinelink.core.ui.components.Filter
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
Expand All @@ -24,16 +26,14 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import java.net.UnknownHostException

class HomeViewModel(
private val getPopularMoviesUseCase: GetPopularMoviesUseCase,
private val fetchMultiInfoSearchUseCase: FetchMultiInfoSearchUseCase,
private val markAsFavoriteUseCase: MarkAsFavoriteUseCase,
private val getFavoriteMoviesUseCase: GetFavoriteMoviesUseCase,
) : ViewModel() {
private var currentPage: Int = 1
private var searchPage: Int = 1

private var searchJob: Job? = null

private var allowSearchResult: Boolean = true
Expand All @@ -53,26 +53,44 @@ class HomeViewModel(
}

private fun fetchPopularMovies() {
if (getPage(HomePage.Popular) == 1) {
_viewState.setLoading()
}

viewModelScope.launch {
getPopularMoviesUseCase.invoke(
parameters = MoviesRequestApi(
page = currentPage,
page = getPage(HomePage.Popular),
),
).collectLatest { result ->
result.onSuccess {
incrementPage(HomePage.Popular)
_viewState.update { viewState ->
viewState.copy(
isLoading = false,
error = null,
retryAction = null,
popularMovies = viewState.popularMovies.addMore(result.data),
)
}
}.onFailure {
_viewState.update { viewState ->
viewState.copy(
isLoading = false,
error = UIText.StringText(it.message ?: "Something went wrong."),
)
}
ErrorHandler.create(it) {
on<UnknownHostException> {
if (getPage(HomePage.Popular) == 1) {
_viewState.update { viewState ->
viewState.copy(
error = BlankSlateState.Offline,
retryAction = HomeMode.Browser,
)
}
}
}
}
}
}
}
Expand All @@ -95,22 +113,20 @@ class HomeViewModel(
when (viewState.value.mode) {
HomeMode.Filtered -> return
HomeMode.Browser -> if (viewState.value.popularMovies.shouldLoadMore) {
currentPage++
fetchPopularMovies()
}
HomeMode.Search -> if (viewState.value.searchResults?.shouldLoadMore == true) {
searchPage++
fetchFromSearchQuery(
query = viewState.value.query,
page = searchPage,
page = getPage(HomePage.Search),
)
}
}
}

fun onSearchMovies(query: String) {
searchJob?.cancel()
searchPage = 1
resetPage(HomePage.Search)
allowSearchResult = true
if (query.isEmpty()) {
onClearClicked()
Expand All @@ -123,7 +139,7 @@ class HomeViewModel(
}
searchJob = viewModelScope.launch {
delay(timeMillis = 300)
if (cachedSearchResults.contains(query) && searchPage == 1) {
if (cachedSearchResults.contains(query) && getPage(HomePage.Search) == 1) {
Timber.d("Fetching cached results")
_viewState.update { viewState ->
latestQuery = query
Expand All @@ -136,8 +152,9 @@ class HomeViewModel(
)
}
// If cache found, set search page to last cached search page
searchPage = cachedSearchResults[query]?.page ?: 1
Timber.d("Setting page to: $searchPage")
// FIXME
// searchPage = cachedSearchResults[query]?.page ?: 1
// Timber.d("Setting page to: $searchPage")
} else {
Timber.d("Fetching data from web service..")
fetchFromSearchQuery(query = query, page = 1)
Expand All @@ -148,7 +165,7 @@ class HomeViewModel(

fun onClearClicked() {
searchJob?.cancel()
searchPage = 1
resetPage(HomePage.Search)
allowSearchResult = false
latestQuery = null
_viewState.update { viewState ->
Expand All @@ -174,8 +191,6 @@ class HomeViewModel(
latestQuery = query

viewModelScope.launch {
_viewState.setLoading()

fetchMultiInfoSearchUseCase.invoke(
parameters = MultiSearchRequestApi(
query = query,
Expand All @@ -185,12 +200,15 @@ class HomeViewModel(
.distinctUntilChanged()
.collectLatest { result ->
result.onSuccess {
incrementPage(HomePage.Search)
if (allowSearchResult && result.data.query == latestQuery) {
_viewState.update { viewState ->
viewState.copy(
isSearchLoading = false,
isLoading = false,
error = null,
mode = HomeMode.Search,
retryAction = null,
searchResults = if (isNewSearch) {
isNewSearch = false
MediaSection(
Expand All @@ -206,14 +224,32 @@ class HomeViewModel(
}
}
}.onFailure {
_viewState.update { viewState ->
viewState.copy(
isSearchLoading = false,
error = UIText.StringText(it.message ?: "Something went wrong."),
)
}
handleSearchError(it)
}
}
}
}

private fun handleSearchError(it: Throwable) {
_viewState.update { viewState ->
viewState.copy(
isLoading = false,
isSearchLoading = false,
)
}
ErrorHandler.create(it) {
on<UnknownHostException> {
if (getPage(HomePage.Search) == 1) {
_viewState.update { viewState ->
viewState.copy(
mode = HomeMode.Search,
searchResults = null,
error = BlankSlateState.Offline,
retryAction = HomeMode.Search,
)
}
}
}
}
}

Expand Down Expand Up @@ -266,6 +302,19 @@ class HomeViewModel(
}
}

fun onRetryClick() {
when (viewState.value.retryAction) {
HomeMode.Browser -> fetchPopularMovies()
HomeMode.Search -> {
latestQuery = null
onSearchMovies(viewState.value.query)
}
else -> {
// Do nothing
}
}
}

/**
* Handles the filters for the liked movies.
* This method fetches the liked movies from the database and updates the view state.
Expand Down Expand Up @@ -305,6 +354,8 @@ class HomeViewModel(
private fun updateFilters(homeFilter: HomeFilter?) {
_viewState.update { viewState ->
viewState.copy(
// TODO add test
retryAction = null,
filters = viewState.filters.map { currentFilter ->
if (currentFilter.name == homeFilter?.filter?.name) {
currentFilter.copy(isSelected = !currentFilter.isSelected)
Expand All @@ -324,6 +375,24 @@ class HomeViewModel(
)
}
}

private fun incrementPage(page: HomePage) {
_viewState.update { viewState ->
viewState.copy(
pages = viewState.pages + (page to (viewState.pages[page] ?: 1) + 1),
)
}
}

private fun resetPage(page: HomePage) {
_viewState.update { viewState ->
viewState.copy(
pages = viewState.pages + (page to 1),
)
}
}

private fun getPage(page: HomePage): Int = viewState.value.pages[page] ?: 1
}

data class SearchCache(
Expand Down
Loading
Loading