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

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>
}

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?

Jetpack Compose Snackbar above 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

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!!!

alternative to using 'await' with lazy_static! macro in rust?

I want to use Async MongoDB in a project.
I don't want to pass around the client because it would need to go around multiple tasks and threads. So I kept a static client using lazy_static. However, I can't use await in the initialization block.
What can I do to work around this?
Suggestions for doing it without lazy_static are also welcome.
use std::env;
use futures::stream::StreamExt;
use mongodb::{
bson::{doc, Bson},
options::ClientOptions,
Client,
};
lazy_static! {
static ref MONGO: Option<Client> = {
if let Ok(token) = env::var("MONGO_AUTH") {
if let Ok(client_options) = ClientOptions::parse(&token).await
^^^^^
{
if let Ok(client) = Client::with_options(client_options) {
return Some(client);
}
}
}
return None;
};
}
I went with this approach based on someone's suggestion in rust forums.
static MONGO: OnceCell<Client> = OnceCell::new();
static MONGO_INITIALIZED: OnceCell<tokio::sync::Mutex<bool>> = OnceCell::new();
pub async fn get_mongo() -> Option<&'static Client> {
// this is racy, but that's OK: it's just a fast case
let client_option = MONGO.get();
if let Some(_) = client_option {
return client_option;
}
// it hasn't been initialized yet, so let's grab the lock & try to
// initialize it
let initializing_mutex = MONGO_INITIALIZED.get_or_init(|| tokio::sync::Mutex::new(false));
// this will wait if another task is currently initializing the client
let mut initialized = initializing_mutex.lock().await;
// if initialized is true, then someone else initialized it while we waited,
// and we can just skip this part.
if !*initialized {
// no one else has initialized it yet, so
if let Ok(token) = env::var("MONGO_AUTH") {
if let Ok(client_options) = ClientOptions::parse(&token).await {
if let Ok(client) = Client::with_options(client_options) {
if let Ok(_) = MONGO.set(client) {
*initialized = true;
}
}
}
}
}
drop(initialized);
MONGO.get()
}
However I can't use await in the initialization block.
You can skirt this with futures::executor::block_on
use once_cell::sync::Lazy;
// ...
static PGCLIENT: Lazy<Client> = Lazy::new(|| {
let client: Client = futures::executor::block_on(async {
let (client, connection) = tokio_postgres::connect(
"postgres:///?user=ecarroll&port=5432&host=/run/postgresql",
NoTls,
)
.await
.unwrap();
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
client
});
client
});
What we have is a non-async closure blocking in a single thread until the resolution of the future.
Create a new runtime from tokio::runtime::Runtime and use block_on to block the current thread until completion.
// database.rs
use tokio::runtime::Runtime;
use mongodb::Client;
pub fn connect_sync() -> Client {
Runtime::new().unwrap().block_on(async {
Client::with_uri_str("mongodb://localhost:27017").await.unwrap()
})
}
// main.rs
mod database;
lazy_static! {
static ref CLIENT: mongodb::Client = database::connect_sync();
}
#[actix_web::main]
async fn main() {
let collection = &CLIENT.database("db_name").collection("coll_name");
// ...
}
Use the async_once crate.
use async_once::AsyncOnce;
use lazy_static::lazy_static;
use mongodb::Client;
lazy_static! {
static ref CLIENT: AsyncOnce<Client> = AsyncOnce::new(async {
Client::with_uri_str(std::env::var("MONGO_URL").expect("MONGO_URL not set"))
.await
.unwrap()
});
}
then
CLIENT.get().await;

syncfusion chart does not display data

I added SFchart, it had no errors and it compiles. It shows an empty chartview.
I'm using MVVMcross in Xamarin.IOS
The data I requested is there, it contains about 200 rows, the data is requested from my api with the method override void viewAppearing.
My view in viewdidload:
//Initialize the Chart with required frame. This frame can be any rectangle, which bounds inside the view.
SFChart chart = new SFChart();
chart.Frame = this.headerView.Frame;
//Adding Primary Axis for the Chart.
SFCategoryAxis primaryAxis = new SFCategoryAxis();
chart.PrimaryAxis = primaryAxis;
//Adding Secondary Axis for the Chart.
SFNumericalAxis secondaryAxis = new SFNumericalAxis();
chart.SecondaryAxis = secondaryAxis;
chart.Series.Add(new SFColumnSeries()
{
ItemsSource = (this.ViewModel as UserCoinViewModel).CoinHistory,
XBindingPath = "price_btc",
YBindingPath = "timestamp"
});
this.View.AddSubview(chart);
viewmodel:
private List<CoinHistoryModel> _CoinHistory;
public List<CoinHistoryModel> CoinHistory
{
get
{
return _CoinHistory;
}
set
{
_CoinHistory = value;
RaisePropertyChanged(() => CoinHistory);
}
}
Because you are using MVVMcross, you should use bind method to set your Series's ItemsSource. You just set the ItemsSource to the viewModel instance's property, when the value changed it will not notify the View. So it seems to show an empty chart.
Modify your code to bind like:
SFColumnSeries series = new SFColumnSeries()
{
XBindingPath = "price_btc",
YBindingPath = "timestamp"
};
chart.Series.Add(series);
var set = this.CreateBindingSet<YourView, UserCoinViewModel>();
...
set.Bind(series).For(s => s.ItemsSource).To(vm => vm.CoinHistory);
...
set.Apply();
Then in your ViewModel, create a command like:
private MvxCommand loadDataCommand;
public ICommand LoadDataCommand
{
get
{
return loadDataCommand ?? (loadDataCommand = new MvxCommand(ExecuteloadDataCommand));
}
}
private void ExecuteloadDataCommand()
{
CoinHistory = new List<CoinHistoryModel>()
{
new CoinHistoryModel{ price_btc = "First", timestamp = 10 },
new CoinHistoryModel{ price_btc = "Second", timestamp = 20 },
new CoinHistoryModel{ price_btc = "Third", timestamp = 30 }
};
// Change it to your data request here
}
At last trigger this command in your ViewWillAppear() event:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
(ViewModel as UserCoinViewModel).LoadDataCommand.Execute(null);
// You can also bind a button to this command to trigger it manually.
}