diff --git a/app/src/main/java/co/nimblehq/compose/crypto/extension/FlowExt.kt b/app/src/main/java/co/nimblehq/compose/crypto/extension/FlowExt.kt new file mode 100644 index 00000000..63133a89 --- /dev/null +++ b/app/src/main/java/co/nimblehq/compose/crypto/extension/FlowExt.kt @@ -0,0 +1,19 @@ +package co.nimblehq.compose.crypto.extension + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import kotlinx.coroutines.flow.* +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +@SuppressLint("ComposableNaming") +@Composable +fun Flow.collectAsEffect( + context: CoroutineContext = EmptyCoroutineContext, + block: suspend (T) -> Unit, +) { + LaunchedEffect(key1 = Unit) { + onEach(block).flowOn(context).launchIn(this) + } +} diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/navigation/AppDestination.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/navigation/AppDestination.kt index ae7311b9..84f2f837 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/navigation/AppDestination.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/navigation/AppDestination.kt @@ -16,6 +16,8 @@ sealed class AppDestination(val route: String = "") { object Home : AppDestination("home") + object NoNetwork : AppDestination("no_network") + /** * We can define route as "coin/details" without "coinId" parameter because we're passing it as argument already. * So either passing "coinId" via arguments or passing it via route. diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/navigation/AppNavigation.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/navigation/AppNavigation.kt index edcf0125..f54d3ba3 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/navigation/AppNavigation.kt @@ -1,16 +1,35 @@ package co.nimblehq.compose.crypto.ui.navigation import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.* import androidx.navigation.compose.* +import co.nimblehq.compose.crypto.R +import co.nimblehq.compose.crypto.extension.collectAsEffect +import co.nimblehq.compose.crypto.ui.common.AppDialogPopUp +import co.nimblehq.compose.crypto.ui.screens.MainViewModel import co.nimblehq.compose.crypto.ui.screens.detail.DetailScreen import co.nimblehq.compose.crypto.ui.screens.home.HomeScreen @Composable fun AppNavigation( navController: NavHostController = rememberNavController(), + mainViewModel: MainViewModel = hiltViewModel(), startDestination: String = AppDestination.Home.destination ) { + + mainViewModel.isNetworkConnected.collectAsEffect { isNetworkConnected -> + if (isNetworkConnected == false) { + val destination = AppDestination.NoNetwork + + val currentRoute = navController.currentBackStackEntry?.destination?.route + if (currentRoute == AppDestination.NoNetwork.route) { + navController.popBackStack() + } + + navController.navigate(destination) + } + } NavHost( navController = navController, startDestination = startDestination @@ -27,6 +46,16 @@ fun AppNavigation( coinId = it.arguments?.getString(KEY_COIN_ID).orEmpty() ) } + + dialog(AppDestination.NoNetwork.route) { + AppDialogPopUp( + onDismiss = { navController.popBackStack() }, + onClick = { navController.popBackStack() }, + message = R.string.no_internet_message, + actionText = android.R.string.ok, + title = R.string.no_internet_title + ) + } } } diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/MainViewModel.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/MainViewModel.kt new file mode 100644 index 00000000..27b2c3e8 --- /dev/null +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/MainViewModel.kt @@ -0,0 +1,40 @@ +package co.nimblehq.compose.crypto.ui.screens + +import androidx.lifecycle.viewModelScope +import co.nimblehq.compose.crypto.domain.usecase.GetConnectionStatusUseCase +import co.nimblehq.compose.crypto.ui.base.* +import co.nimblehq.compose.crypto.util.DispatchersProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +interface Input : BaseInput + +interface Output : BaseOutput { + val isNetworkConnected: SharedFlow +} + +@HiltViewModel +class MainViewModel @Inject constructor( + getConnectionStatusUseCase: GetConnectionStatusUseCase, + dispatchersProvider: DispatchersProvider, +) : BaseViewModel(dispatchersProvider), Input, Output { + private val _isNetworkConnected = MutableSharedFlow() + override val isNetworkConnected: SharedFlow + get() = _isNetworkConnected + + override val input: BaseInput = this + override val output: BaseOutput = this + + init { + getConnectionStatusUseCase() + .catch { + _error.emit(it) + } + .onEach { + _isNetworkConnected.emit(it) + } + .flowOn(dispatchersProvider.io) + .launchIn(viewModelScope) + } +} diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreen.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreen.kt index 373807d6..36b02841 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreen.kt @@ -23,7 +23,6 @@ import co.nimblehq.compose.crypto.R import co.nimblehq.compose.crypto.extension.boxShadow import co.nimblehq.compose.crypto.lib.IsLoading import co.nimblehq.compose.crypto.ui.base.LoadingState -import co.nimblehq.compose.crypto.ui.common.AppDialogPopUp import co.nimblehq.compose.crypto.ui.navigation.AppDestination import co.nimblehq.compose.crypto.ui.preview.HomeScreenParams import co.nimblehq.compose.crypto.ui.preview.HomeScreenPreviewParameterProvider @@ -57,9 +56,6 @@ fun HomeScreen( } } - // TODO remove in integration ticket - val hasConnection by viewModel.hasConnection.collectAsState() - val showMyCoinsLoading: IsLoading by viewModel.output.showMyCoinsLoading.collectAsState() val showTrendingCoinsLoading: LoadingState by viewModel.output.showTrendingCoinsLoading.collectAsState() val myCoins: List by viewModel.output.myCoins.collectAsState() @@ -90,17 +86,6 @@ fun HomeScreen( onRefresh = { viewModel.input.loadData(isRefreshing = true) }, onTrendingCoinsLoadMore = { viewModel.input.getTrendingCoins(loadMore = true) } ) - - // TODO remove in integration ticket - if (hasConnection == false) { - AppDialogPopUp( - onDismiss = { /*TODO*/ }, - onClick = { /*TODO*/ }, - message = R.string.no_internet_message, - actionText = android.R.string.ok, - title = R.string.no_internet_title - ) - } } @OptIn(ExperimentalMaterialApi::class) diff --git a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModel.kt b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModel.kt index 2d6529ff..c59a27ca 100644 --- a/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModel.kt @@ -52,7 +52,6 @@ class HomeViewModel @Inject constructor( dispatchers: DispatchersProvider, private val getMyCoinsUseCase: GetMyCoinsUseCase, private val getTrendingCoinsUseCase: GetTrendingCoinsUseCase, - private val getConnectionStatusUseCase: GetConnectionStatusUseCase, ) : BaseViewModel(dispatchers), Input, Output { override val input = this @@ -84,19 +83,8 @@ class HomeViewModel @Inject constructor( private var trendingCoinsPage = MY_COINS_INITIAL_PAGE - // TODO remove in integration ticket - private val _hasConnection = MutableStateFlow(null) - val hasConnection: StateFlow = _hasConnection - init { loadData() - // TODO remove in integration ticket - execute { - getConnectionStatusUseCase() - .collect { - _hasConnection.emit(it) - } - } } override fun loadData(isRefreshing: Boolean) { diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreenTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreenTest.kt index 69937f12..4c5a4b61 100644 --- a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreenTest.kt +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeScreenTest.kt @@ -54,16 +54,12 @@ class HomeScreenTest : BaseViewModelTest() { private val mockGetMyCoinsUseCase = mockk() private val mockGetTrendingCoinsUseCase = mockk() - // TODO remove in integration ticket - private val mockGetConnectionStatusUseCase = mockk() - private lateinit var viewModel: HomeViewModel private var appDestination: AppDestination? = null @Before fun setUp() { - every { mockGetConnectionStatusUseCase() } returns flowOf(null) composeAndroidTestRule.activity.setContent { HomeScreen( viewModel = viewModel, @@ -219,8 +215,7 @@ class HomeScreenTest : BaseViewModelTest() { viewModel = HomeViewModel( dispatchers = testDispatcherProvider, getMyCoinsUseCase = mockGetMyCoinsUseCase, - getTrendingCoinsUseCase = mockGetTrendingCoinsUseCase, - getConnectionStatusUseCase = mockGetConnectionStatusUseCase + getTrendingCoinsUseCase = mockGetTrendingCoinsUseCase ) } } diff --git a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModelTest.kt b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModelTest.kt index f34fcd90..2c473630 100644 --- a/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModelTest.kt +++ b/app/src/test/java/co/nimblehq/compose/crypto/ui/screens/home/HomeViewModelTest.kt @@ -24,9 +24,6 @@ class HomeViewModelTest : BaseViewModelTest() { private val mockGetTrendingCoinsUseCase = mockk() private lateinit var viewModel: HomeViewModel - // TODO remove in integration ticket - private val mockGetConnectionStatusUseCase = mockk() - @Before fun setUp() { every { mockGetMyCoinsUseCase.execute(any()) } returns flowOf(MockUtil.myCoins) @@ -145,8 +142,7 @@ class HomeViewModelTest : BaseViewModelTest() { viewModel = HomeViewModel( testDispatcherProvider, mockGetMyCoinsUseCase, - mockGetTrendingCoinsUseCase, - mockGetConnectionStatusUseCase + mockGetTrendingCoinsUseCase ) } }