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

[#102] [Integrate] As a user, I can see dialog when there is no internet connection #105

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
41 changes: 41 additions & 0 deletions app/src/main/java/co/nimblehq/compose/crypto/CryptoAppState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package co.nimblehq.compose.crypto

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import co.nimblehq.compose.crypto.domain.usecase.IsNetworkConnectedUseCase
import co.nimblehq.compose.crypto.util.DispatchersProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*

@Composable
fun rememberCryptoAppState(
isNetworkConnectedUseCase: IsNetworkConnectedUseCase,
dispatchersProvider: DispatchersProvider,
) = remember(isNetworkConnectedUseCase, dispatchersProvider) {
CryptoAppState(isNetworkConnectedUseCase, dispatchersProvider)
}

@Stable
class CryptoAppState(
isNetworkConnectedUseCase: IsNetworkConnectedUseCase,
dispatchersProvider: DispatchersProvider,
) {
private val _isNetworkConnected = MutableStateFlow<Boolean?>(null)
val isNetworkConnected = _isNetworkConnected.asStateFlow()

private val _networkError = MutableSharedFlow<Throwable?>()
val networkError = _networkError.asSharedFlow()

init {
isNetworkConnectedUseCase()
.catch {
_networkError.emit(it)
}
.onEach {
_isNetworkConnected.emit(it)
}
.flowOn(dispatchersProvider.io)
.launchIn(CoroutineScope(dispatchersProvider.io))
}
}
19 changes: 19 additions & 0 deletions app/src/main/java/co/nimblehq/compose/crypto/extension/FlowExt.kt
Original file line number Diff line number Diff line change
@@ -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 <T> Flow<T>.collectAsEffect(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend (T) -> Unit,
) {
LaunchedEffect(key1 = Unit) {
onEach(block).flowOn(context).launchIn(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import co.nimblehq.compose.crypto.ui.theme.*

@Composable
fun AppDialogPopUp(
onDismiss: () -> Unit,
onClick: () -> Unit,
title: String,
message: String,
actionText: String,
dialogActions: List<DialogActionModel>,
onClickAction: () -> Unit
) {
Dialog(
onDismissRequest = onDismiss
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnClickOutside = false
)
) {
Surface {
Column(
Expand All @@ -36,34 +40,50 @@ fun AppDialogPopUp(
color = AppTheme.colors.text,
modifier = Modifier.padding(Dp16)
)
TextButton(
onClick = onClick,
Row(
modifier = Modifier
.align(Alignment.End)
.padding(bottom = Dp16, end = Dp8)
) {
Text(
text = actionText,
style = AppTheme.styles.semiBold16,
color = AppTheme.colors.dialogText,
)
for(action in dialogActions) {
TextButton(
onClick = {
action.onClickAction()
onClickAction()
},
) {
Text(
text = action.actionText,
style = AppTheme.styles.semiBold16,
color = AppTheme.colors.dialogText,
)
}
}
}
}
}
}
}

data class DialogActionModel(
val actionText: String,
val onClickAction: () -> Unit
)

@Composable
@Preview(showSystemUi = true)
fun AppDialogPopUpPreview() {
ComposeTheme {
Box {
AppDialogPopUp(
onDismiss = { /*TODO*/ },
onClick = { /*TODO*/ },
onDismiss = {},
message = "No internet connection was found. Please check your internet connection and try again.",
actionText = "OK",
title = "Oops!"
title = "Oops!",
dialogActions = listOf(DialogActionModel(
actionText = "OK",
onClickAction = {}
)),
onClickAction = {}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
package co.nimblehq.compose.crypto.ui.navigation

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.navigation.*
import androidx.navigation.compose.*
import co.nimblehq.compose.crypto.R
import co.nimblehq.compose.crypto.ui.common.AppDialogPopUp
import co.nimblehq.compose.crypto.ui.common.DialogActionModel
import co.nimblehq.compose.crypto.ui.screens.detail.DetailScreen
import co.nimblehq.compose.crypto.ui.screens.home.HomeScreen

@Composable
fun AppNavigation(
navController: NavHostController = rememberNavController(),
startDestination: String = AppDestination.Home.destination
navController: NavHostController,
startDestination: String = AppDestination.Home.destination,
) {

var dialogActions: List<DialogActionModel> = emptyList()

NavHost(
navController = navController,
startDestination = startDestination
) {
composable(AppDestination.Home) {
HomeScreen(
navigator = { destination -> navController.navigate(destination) }
navigator = { destination -> navController.navigate(destination) },
onShowGlobalDialog = { actions ->
dialogActions = actions
}
)
}

composable(AppDestination.CoinDetail) {
DetailScreen(
navigator = { destination -> navController.navigate(destination) },
coinId = it.arguments?.getString(KEY_COIN_ID).orEmpty()
coinId = it.arguments?.getString(KEY_COIN_ID).orEmpty(),
onShowGlobalDialog = { actions ->
dialogActions = actions
}
)
}

dialog(AppDestination.NoNetwork.route) {
AppDialogPopUp(
onDismiss = { navController.popBackStack() },
onClickAction = {
navController.popBackStack()
},
message = stringResource(id = R.string.no_internet_message),
title = stringResource(id = R.string.no_internet_title),
dialogActions = dialogActions
)
}
}
Expand All @@ -43,7 +68,7 @@ private fun NavGraphBuilder.composable(
)
}

private fun NavHostController.navigate(destination: AppDestination) {
fun NavHostController.navigate(destination: AppDestination) {
when (destination) {
is AppDestination.Up -> popBackStack()
else -> navigate(route = destination.destination)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package co.nimblehq.compose.crypto.ui.navigation

import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import co.nimblehq.compose.crypto.CryptoAppState
import co.nimblehq.compose.crypto.extension.collectAsEffect

@Composable
fun ComposeCryptoApp(
navController: NavHostController = rememberNavController(),
cryptoAppState: CryptoAppState
) {
val context = LocalContext.current

val isNetworkConnected by cryptoAppState.isNetworkConnected.collectAsState()

LaunchedEffect(isNetworkConnected) {
if (isNetworkConnected == false) {
val destination = AppDestination.NoNetwork

val currentRoute = navController.currentBackStackEntry?.destination?.route
if (currentRoute != AppDestination.NoNetwork.route) {
navController.navigate(destination)
}
}
}

cryptoAppState.networkError.collectAsEffect { error ->
Toast.makeText(context, error?.message, Toast.LENGTH_SHORT).show()
}

AppNavigation(navController = navController)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,35 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.view.WindowCompat
import co.nimblehq.compose.crypto.ui.navigation.AppNavigation
import co.nimblehq.compose.crypto.domain.usecase.IsNetworkConnectedUseCase
import co.nimblehq.compose.crypto.rememberCryptoAppState
import co.nimblehq.compose.crypto.ui.navigation.ComposeCryptoApp
import co.nimblehq.compose.crypto.ui.theme.ComposeTheme
import co.nimblehq.compose.crypto.util.DispatchersProvider
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

@Inject
lateinit var isNetworkConnectedUseCase: IsNetworkConnectedUseCase

@Inject
lateinit var dispatchersProvider: DispatchersProvider

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

WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
ComposeTheme {
AppNavigation()
ComposeCryptoApp(
cryptoAppState = rememberCryptoAppState(
isNetworkConnectedUseCase = isNetworkConnectedUseCase,
dispatchersProvider = dispatchersProvider,
)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import co.nimblehq.compose.crypto.data.extension.orZero
import co.nimblehq.compose.crypto.domain.model.CoinPrice
import co.nimblehq.compose.crypto.extension.toFormattedString
import co.nimblehq.compose.crypto.lib.IsLoading
import co.nimblehq.compose.crypto.ui.common.DialogActionModel
import co.nimblehq.compose.crypto.ui.common.price.PriceChangeButton
import co.nimblehq.compose.crypto.ui.components.chartintervals.ChartIntervalsButtonGroup
import co.nimblehq.compose.crypto.ui.components.chartintervals.TimeIntervals
Expand Down Expand Up @@ -54,6 +55,7 @@ fun DetailScreen(
viewModel: DetailViewModel = hiltViewModel(),
navigator: (destination: AppDestination) -> Unit,
coinId: String,
onShowGlobalDialog: (dialogActions: List<DialogActionModel>) -> Unit
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
Expand Down Expand Up @@ -83,6 +85,21 @@ fun DetailScreen(
LaunchedEffect(Unit) {
viewModel.input.getCoinId(coinId = coinId)
}

onShowGlobalDialog(
listOf(
DialogActionModel(
actionText = stringResource(id = android.R.string.ok),
onClickAction = {}
),
DialogActionModel(
actionText = stringResource(id = R.string.detail_dialog_action_retry),
onClickAction = {
viewModel.input.getCoinId(coinId = coinId)
}
)
)
)
}

@Suppress("LongMethod", "LongParameterList")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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.common.DialogActionModel
import co.nimblehq.compose.crypto.ui.navigation.AppDestination
import co.nimblehq.compose.crypto.ui.preview.HomeScreenParams
import co.nimblehq.compose.crypto.ui.preview.HomeScreenPreviewParameterProvider
Expand All @@ -41,8 +41,10 @@ const val TestTagCoinsLoader = "CoinsLoader"
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
navigator: (destination: AppDestination) -> Unit
navigator: (destination: AppDestination) -> Unit,
onShowGlobalDialog: (dialogActions: List<DialogActionModel>) -> Unit
) {

val context = LocalContext.current
var rememberRefreshing by remember { mutableStateOf(false) }

Expand All @@ -57,9 +59,6 @@ fun HomeScreen(
}
}

// TODO remove in integration ticket
val isNetworkConnected by viewModel.isNetworkConnected.collectAsState()

val showMyCoinsLoading: IsLoading by viewModel.output.showMyCoinsLoading.collectAsState()
val showTrendingCoinsLoading: LoadingState by viewModel.output.showTrendingCoinsLoading.collectAsState()
val myCoins: List<CoinItemUiModel> by viewModel.output.myCoins.collectAsState()
Expand Down Expand Up @@ -91,16 +90,14 @@ fun HomeScreen(
onTrendingCoinsLoadMore = { viewModel.input.getTrendingCoins(loadMore = true) }
)

// TODO remove in integration ticket
if (isNetworkConnected == false) {
AppDialogPopUp(
onDismiss = { /*TODO*/ },
onClick = { /*TODO*/ },
message = stringResource(id = R.string.no_internet_message),
actionText = stringResource(id = android.R.string.ok),
title = stringResource(id = R.string.no_internet_title)
onShowGlobalDialog(
listOf(
DialogActionModel(
actionText = stringResource(id = android.R.string.ok),
onClickAction = {}
)
)
}
)
}

@OptIn(ExperimentalMaterialApi::class)
Expand Down
Loading
Loading