Jetpack Compose Snackbar above Dialog - modal-dialog

How can a Snackbar be shown above a Dialog or AlertDialog in Jetpack Compose? Everything I have tried has resulted in the snack bar being below the scrim of the dialog not to mention the dialog itself.
According to Can I display material design Snackbar in dialog? it is possible in non-Compose Android by using a custom or special (like getDialog().getWindow().getDecorView()) view, but that isn't accessible from Compose I believe (at least not without a lot of effort).

I came up with a solution that mostly works. It uses the built-in Snackbar() composable for the rendering but handles the role of SnackbarHost() with a new function SnackbarInDialogContainer().
Usage example:
var error by remember { mutableStateOf<String?>(null) }
AlertDialog(
...
text = {
...
if (error !== null) {
SnackbarInDialogContainer(error, dismiss = { error = null }) {
Snackbar(it, Modifier.padding(WindowInsets.ime.asPaddingValues()))
}
}
}
...
)
It has the following limitations:
Has to be used in place within the dialog instead of at the top level
There is no host to queue messages, instead that has to be handled elsewhere if desired
Dismissal is done with a callback (i.e. { error = null} above) instead of automatically
Actions currently do nothing at all, but that could be fixed (I had no use for them, the code do include everything necessary to render the actions I believe, but none of the interaction).
This has built-in support for avoiding the IME (software keyboard), but you may still need to follow https://stackoverflow.com/a/73889690/582298 to make it fully work.
Code for the Composable:
#Composable
fun SnackbarInDialogContainer(
text: String,
actionLabel: String? = null,
duration: SnackbarDuration =
if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite,
dismiss: () -> Unit,
content: #Composable (SnackbarData) -> Unit
) {
val snackbarData = remember {
SnackbarDataImpl(
SnackbarVisualsImpl(text, actionLabel, true, duration),
dismiss
)
}
val dur = getDuration(duration, actionLabel)
if (dur != Long.MAX_VALUE) {
LaunchedEffect(snackbarData) {
delay(dur)
snackbarData.dismiss()
}
}
val popupPosProvider by imeMonitor()
Popup(
popupPositionProvider = popupPosProvider,
properties = PopupProperties(clippingEnabled = false),
) {
content(snackbarData)
}
}
#Composable
private fun getDuration(duration: SnackbarDuration, actionLabel: String?): Long {
val accessibilityManager = LocalAccessibilityManager.current
return remember(duration, actionLabel, accessibilityManager) {
val orig = when (duration) {
SnackbarDuration.Short -> 4000L
SnackbarDuration.Long -> 10000L
SnackbarDuration.Indefinite -> Long.MAX_VALUE
}
accessibilityManager?.calculateRecommendedTimeoutMillis(
orig, containsIcons = true, containsText = true, containsControls = actionLabel != null
) ?: orig
}
}
/**
* Monitors the size of the IME (software keyboard) and provides an updating
* PopupPositionProvider.
*/
#Composable
private fun imeMonitor(): State<PopupPositionProvider> {
val provider = remember { mutableStateOf(ImePopupPositionProvider(0)) }
val context = LocalContext.current
val decorView = remember(context) { context.getActivity()?.window?.decorView }
if (decorView != null) {
val ime = remember { WindowInsetsCompat.Type.ime() }
val bottom = remember { MutableStateFlow(0) }
LaunchedEffect(Unit) {
while (true) {
bottom.value = ViewCompat.getRootWindowInsets(decorView)?.getInsets(ime)?.bottom ?: 0
delay(33)
}
}
LaunchedEffect(Unit) {
bottom.collect { provider.value = ImePopupPositionProvider(it) }
}
}
return provider
}
/**
* Places the popup at the bottom of the screen but above the keyboard.
* This assumes that the anchor for the popup is in the middle of the screen.
*/
private data class ImePopupPositionProvider(val imeSize: Int): PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect, windowSize: IntSize,
layoutDirection: LayoutDirection, popupContentSize: IntSize
) = IntOffset(
anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2, // centered on screen
anchorBounds.top + (anchorBounds.height - popupContentSize.height) / 2 + // centered on screen
(windowSize.height - imeSize) / 2 // move to the bottom of the screen
)
}
private fun Context.getActivity(): Activity? {
var currentContext = this
while (currentContext is ContextWrapper) {
if (currentContext is Activity) {
return currentContext
}
currentContext = currentContext.baseContext
}
return null
}
private data class SnackbarDataImpl(
override val visuals: SnackbarVisuals,
val onDismiss: () -> Unit,
) : SnackbarData {
override fun performAction() { /* TODO() */ }
override fun dismiss() { onDismiss() }
}
private data class SnackbarVisualsImpl(
override val message: String,
override val actionLabel: String?,
override val withDismissAction: Boolean,
override val duration: SnackbarDuration
) : SnackbarVisuals

Related

Generic data in compose ui state

I'm getting data from Marvel API, so the main screen you have different kinds of categories (Characters, Events, Comics etc.) When the user clicks on one of the categories, the app navigates to a list of the related data.
So I want this screen to hold different kinds of data (categories) without using a different screen for each one. Is this the best approach? and how can I do that?
code:
#kotlinx.serialization.Serializable
data class MarvelResponse(
val data:Data
)
#kotlinx.serialization.Serializable
data class Data(
var characters:List<Character>,
var series:List<Series>,
var stories:List<Story>,
var events:List<Event>,
var comics:List<Comic>,
var cartoons:List<Cartoon>
)
class DetailsViewModel #Inject constructor(
private val useCase: UseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = mutableStateOf<Resource<Any>>(Resource.Loading())
val uiState = _uiState
private fun getData(category: String) {
when (category) {
"Characters" -> {
getCharacters()
}
"Comics" -> {
getComics()
}
"Series" -> {
//
}
"Stories" -> {
//
}
}
}
private fun getCharacters() {
viewModelScope.launch {
val charactersResponse = useCase.getCharactersUseCase()
_uiState.value = Resource.Success(charactersResponse)
}
}
..........
fun Details(
vm: DetailsViewModel = hiltViewModel(),
navController:NavHostController
) {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
}
},
title = { Text(text = "Back") }
)
}
) { paddingValues ->
DetailsVerticalGrid(state, modifier = Modifier.padding(paddingValues))
}
}
#ExperimentalMaterialApi
#ExperimentalComposeUiApi
#Composable
fun DetailsVerticalGrid(
data: List<Any>,
modifier: Modifier = Modifier
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(30.dp),
modifier = modifier
) {
items(data.size) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
data.forEach {
DetailsGridItemCard(
image = "",
title = it.title
) {
}
}
}
}
}
}
Of course the above code will not work, I want it to work with any type of data, using a state that holds the data according to the category selected. How can I achieve that?

How can i make Http request again in retrofit to get updated data from api

I want to update the data I'm getting from api to display it in my lazy column, I'm trying to add swipe down to refresh functionality.
I'm getting the data from my viewmodel
#HiltViewModel
class MatchesViewModel #Inject constructor(
private val matchRepository: MatchRepository
): ViewModel() {
val response: MutableState<ApiState> = mutableStateOf(ApiState.Empty)
init {
getAllMatches()
}
private fun getAllMatches() = viewModelScope.launch {
cricketRepository.getAllMatches().onStart {
response.value = ApiState.Loading
} .catch {
response.value = ApiState.Failure(it)
}.collect {
response.value = ApiState.Success(it) }
}
}
then i made new kotlin file where I'm checking if I'm getting the data and passing it in my lazy column
#Composable
fun MainScreen(viewModel: MatchesViewModel = hiltViewModel()){
when (val result = viewModel.response.value){
is ApiState.Success -> {
HomeScreen(matches = result.data.data)
}
is ApiState.Loading -> {
}
is ApiState.Empty -> {
}
is ApiState.Failure -> {
}
}
}
i want to know how can i make the request again to get the updated data
after some googling i found out you can retry api calls with okhttp interceptors but could'nt find any documentation or tutorial to retry calls with interceptor
You can try Swipe to Refresh with the accompanist dependencie
implementation "com.google.accompanist:accompanist-swiperefresh:0.26.5-rc"
Implementation would be like this
val swipeRefreshState = rememberSwipeRefreshState(false)
SwipeRefresh(
state = swipeRefreshState,
onRefresh = {
swipeRefreshState.isRefreshing = false
viewModel.<FUNCTION TO LOAD YOUR DATA>
},
indicator = { swipeRefreshState, trigger ->
SwipeRefreshIndicator(
// Pass the SwipeRefreshState + trigger through
state = swipeRefreshState,
refreshTriggerDistance = trigger,
// Enable the scale animation
scale = true,
// Change the color and shape
backgroundColor = <UPDATE CIRLE COLOR>,
contentColor = <RELOADING ICON COLOR>,
shape = RoundedCornerShape(50),
)
}
) {
<CONTENT OF YOUR SCREEN>
}

Create vertical chain with respect to other element in jetpack compose ConstraintLayout?

I want to chain title and description text centered with respect to image with chainStyle.Packed how to achieve this in jetpack compose.
when i use createVerticalChain() its create chain with respect to parent container that's not what i want, is there a way to achieve this?
Like suggested inside the documentation of createVerticalChain(), you can "Use constrain with the resulting VerticalChainReference to modify the top and bottom constraints of this chain.":
ConstraintLayout(modifier = Modifier.fillMaxSize()) {
val (box, text1, text2) = createRefs()
val chainRef = createVerticalChain(text1, text2, chainStyle = ChainStyle.Packed)
constrain(chainRef) {
top.linkTo(box.top)
bottom.linkTo(box.bottom)
}
Box(modifier = Modifier
.size(100.dp)
.padding(16.dp)
.background(color = Color.Red, shape = RoundedCornerShape(50.dp))
.constrainAs(box) {
start.linkTo(parent.start)
top.linkTo(parent.top)
}
)
Text("Line 1 goes here",
modifier = Modifier
.constrainAs(text1) {
start.linkTo(box.end)
end.linkTo(parent.end)
top.linkTo(box.top)
bottom.linkTo(text2.top)
width = Dimension.fillToConstraints
}
)
Text("Line 2 goes here",
modifier = Modifier
.constrainAs(text2) {
start.linkTo(box.end)
end.linkTo(parent.end)
top.linkTo(text1.bottom)
bottom.linkTo(box.bottom)
width = Dimension.fillToConstraints
}
)
}
There are two solutions. The first solution requires that the height of the content on the right of the circle is fixed, while in the second solution, it is not fixed but is bound by constraints:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
Column(modifier = Modifier.fillMaxSize()) {
// First solution
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Box(
modifier = Modifier
.size(100.dp)
.background(color = Color.Red, shape = RoundedCornerShape(50.dp))
)
Column(
modifier = Modifier
.height(100.dp)
.padding(start = 20.dp), verticalArrangement = Arrangement.Center
) {
Text("Line 1 goes here")
Text("Line 2 goes here")
}
}
Spacer(modifier = Modifier
.requiredHeight(30.dp)
.fillMaxWidth())
// Second solution
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (left, right) = createRefs()
Box(modifier = Modifier
.size(100.dp)
.background(color = Color.Red, shape = RoundedCornerShape(50.dp))
.constrainAs(left) {
start.linkTo(parent.start)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
})
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.padding(start = 20.dp)
.constrainAs(right) {
start.linkTo(left.end)
top.linkTo(left.top)
end.linkTo(parent.end)
bottom.linkTo(left.bottom)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}) {
Text("Line 1 goes here")
Text("Line 2 goes here")
}
}
}
}
}
}

LIveData is observed more than twice everytime from acivity and emitting previous data

I follow MVVM Login API, with Retrofit ,My problem is livedata is observed more than twice and always emitting previous response when observed from activity, But inside Repository its giving correct response
I tried a lot of solutions from stackoverflow and other websites but still no luck, Tried removing observers also but still getting previous data ,so plz suggest a working solution, I will post my code below,
LoginActivity.kt
private lateinit var loginViewModel: LoginViewModel
loginViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(
LoginViewModel::class.java
)
loginViewModel.login(userEmail, pwd)
loginViewModel.getLoginRepository().observe(this, Observer {
val loginResult = it ?: return#Observer
val accessToken = loginResult.user?.jwtToken.toString()
})
val statusMsgObserver = Observer<String> { statusMsg ->
showToast(statusMsg)
})
val errorMsgObserver = Observer<String> { errorMsg ->
// Update the UI
showToast(errorMsg)
})
loginViewModel.getStatusMessage()?.observe(this, statusMsgObserver)
loginViewModel.getErrorStatusMessage()?.observe(this, errorMsgObserver)
LoginViewModel.kt:
class LoginViewModel: ViewModel() {
private var loginRepository: LoginRepository? = null
private var _mutableLiveData = MutableLiveData<LoginAPIResponse?>()
val liveData: LiveData<LoginAPIResponse?> get() = _mutableLiveData
private var responseMsgLiveData:MutableLiveData<String>?= null
private var errorResponseMsgLiveData:MutableLiveData<String>?= null
fun login(username: String, password: String) {
loginRepository = LoginRepository.getInstance()!!
/* Query data from Repository */
//val _mutableLiveData: MutableLiveData<Response<LoginAPIResponse?>?>? = loginRepository?.doLogin(username, password)
_mutableLiveData = loginRepository?.doLogin(username, password)!!
responseMsgLiveData = loginRepository?.respMessage!!
errorResponseMsgLiveData = loginRepository?.loginResponseErrorData!!
}
fun getLoginRepository(): LiveData<LoginAPIResponse?> {
return liveData
}
fun getStatusMessage(): LiveData<String>? {
return responseMsgLiveData
}
fun getErrorStatusMessage(): LiveData<String>? {
return errorResponseMsgLiveData
}
}
LoginRepository.kt:
class LoginRepository {
private val loginApi: ApiEndpoints = RetrofitService.createService(ApiEndpoints::class.java)
val responseData = MutableLiveData<LoginAPIResponse?>()
var respMessage = MutableLiveData<String>()
var loginResponseErrorData = MutableLiveData<String>()
fun doLogin(username: String, password: String)
: MutableLiveData<LoginAPIResponse?> {
respMessage.value = null
loginResponseErrorData.value = null
val params = JsonObject()
params.addProperty("email", username)
params.addProperty("password",password)
val jsonParams = JsonObject()
jsonParams.add("user",params)
loginApi.loginToServer(jsonParams).enqueue(object : Callback<LoginAPIResponse?> {
override fun onResponse( call: Call<LoginAPIResponse?>, response: Response<LoginAPIResponse?> ) {
responseData.value = response.body()
respMessage.value = RetrofitService.handleError(response.code())
val error = response.errorBody()
if (!response.isSuccessful) {
val errorMsg = error?.charStream()?.readText()
println("Error Message: $errorMsg")
loginResponseErrorData.value = errorMsg
} else {
println("API Success -> Login, $username, ${response.body()?.user?.email.toString()}")
}
}
override fun onFailure(call: Call<LoginAPIResponse?>, t: Throwable) {
println("onFailure:(message) "+t.message)
loginResponseErrorData.value = t.message
responseData.value = null
}
})
return responseData
}
companion object {
private var loginRepository: LoginRepository? = null
internal fun getInstance(): LoginRepository? {
if (loginRepository == null) {
loginRepository = LoginRepository()
}
return loginRepository
}
}
}
In onDestroy(),I have removed the observers,
override fun onDestroy() {
super.onDestroy()
loginViewModel.getLoginRepository()?.removeObservers(this)
this.viewModelStore.clear()
}
In LoginActivity, when I observe loginResult it gives previous emitted accessToken first and then again called and giving current accessToken, Similarly observer is called more than twice everytime.
But inside repository,its giving recent data, plz check my code and suggest where I have to correct to get correct recent livedata
Finally i found the solution, In LoginRepository, I declared responseData outside doLogin(), It should be declared inside doLogin()
Since it was outside the method, it always gave previous data first and then current data,
Once I declare inside method problem was solved and now it is working Perfect!!!

Make progress bar visible when query is loading. ScalaFx

I have a login window view, and I want to display a progress bar when I click/press enter button while slick is querying the password. If I change the visible attribute for the progress bar at the button actionEvent it doesn´t appear until after the query is done. Also I don't want the progress bar to be taking space while its invisible. Does anybody know how to do these things?
object SPM extends JFXApp {
/*
* Primary stage: Log in
* */
stage = new PrimaryStage {
// error message hidden label
val errorLabel = new Label()
errorLabel.textFill = Color.Red
errorLabel.font = Font.font("Helvetica", FontWeight.ExtraLight, 12)
val usernameField = new TextField {
promptText = "User"
maxWidth = 250
prefHeight = 35
}
val passwordField = new PasswordField() {
promptText = "Password"
maxWidth = 250
prefHeight = 35
}
val progressBar = new ProgressBar {
maxWidth = 300
visible = false
}
title = "Software Project Management"
scene = new Scene(800, 600) {
root = new VBox {
spacing = 10
padding = Insets(20)
alignment = Pos.Center
children = List(
new ImageView {
image = new Image(
this.getClass.getResourceAsStream("/images/logo.png"))
margin = Insets(0, 0, 20, 0)
},
new Label {
text = "Software Project Management"
font = Font.font("Helvetica", FontWeight.ExtraLight, 32)
},
new Label {
text = "Sign in to get started"
font = Font.font("Helvetica", FontWeight.Thin, 18)
},
errorLabel,
progressBar,
usernameField,
passwordField,
new Button {
text = "Enter"
defaultButton = true
prefHeight = 35
font = Font.font("Helvetica", FontWeight.Thin, 18)
maxWidth = 250
onAction = (ae: ActionEvent) => {
progressBar.visible = true
val password = Users.checkPassword(usernameField.text.value)
if (password != passwordField.text.value)
errorLabel.text = "Please re-enter your password"
else root = chooseProject
}
}
) // children
} // root
} // scene
Your Button.onAction handler is running on JavaFX application thread. The same that is used to update UI. When you run long running task you should run it on a separate thread it will help UI to react properly. The common way to do that is to use JavaFX Task. General pattern is like this:
// Define your task
val task = new javafx.concurrent.Task[T] {
override def call(): T = {
// Do your task and return result
// Executed off JavaFX Application thread
}
override def succeeded(): Unit = {
// Update UI to finish processing
// Executed on JavaFX Application thread
}
override def failed(): Unit = {
// Handle errors, if any
// Executed on JavaFX Application thread
}
}
// Run your task
val t = new Thread(task, "My Task")
t.setDaemon(true)
t.start()
```
Here is how it could look in your code:
root = new VBox { _root =>
...
onAction = (ae: ActionEvent) => {
progressBar.visible = true
_root.disable = true
// progressBar.visible = true
val task = new javafx.concurrent.Task[Boolean] {
override def call(): Boolean = {
println("Checking password... ")
Thread.sleep(3000)
println("Password checked. ")
// Assume password is correct
true
}
override def succeeded(): Unit = {
progressBar.visible = false
_root.disable = false
val passwordOK = get()
if (passwordOK) {
new Alert(AlertType.Information) {
headerText = "Password OK"
}.showAndWait()
} else {
new Alert(AlertType.Warning) {
headerText = "Invalid Password"
}.showAndWait()
}
}
override def failed(): Unit = {
println("failed")
progressBar.visible = false
_root.disable = false
}
}
val t = new Thread(task, "Password Task")
t.setDaemon(true)
t.start()
}