How can I save data to room just first time in kotlin? - android

I get some data from api in kotlin and save it to room. I do the saving to Room in the viewmodel of the splash screen. However, each application saves the data to the room when it is opened, I want it to save only once. In this case I tried to do something with shared preferences but I couldn't implement it. Any ideas on this or anyone who has done something similar to this before?
hear is my code
my splash screen ui
#Composable
fun SplashScreen(
navController: NavController,
viewModel : SplashScreenViewModel = hiltViewModel()
) {
val context: Context = LocalContext.current
checkDate(context,viewModel)
val scale = remember {
Animatable(0f)
}
LaunchedEffect(key1 = true) {
scale.animateTo(
targetValue = 0.3f,
animationSpec = tween(
durationMillis = 500,
easing = {
OvershootInterpolator(2f).getInterpolation(it)
}
)
)
delay(2000L)
navController.navigate(Screen.ExchangeMainScreen.route)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_currency_exchange_logo_512),
contentDescription = "Logo"
)
}
}
fun checkDate(context:Context,viewModel: SplashScreenViewModel){
val sharedPreferences = context.getSharedPreferences("date",Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
val date = Date(System.currentTimeMillis())
val millis = date.time
editor.apply{
putLong("currentDate", millis)
}.apply()
val sharedDate = sharedPreferences.getLong("currentDate",1)
println(sharedDate)
val isDayPassed = (System.currentTimeMillis() - sharedDate) >= TimeUnit.DAYS.toMillis(1)
if(isDayPassed){
viewModel.update()
}
}
}
my splash screen view model
#HiltViewModel
class SplashScreenViewModel #Inject constructor(
private val insertExchangeUseCase: InsertExchangeUseCase,
private val updateExchangeUseCase: UpdateExchangeUseCase,
private val getAllExchangeUseCase: GetAllExchangeUseCase
) : ViewModel() {
init {
insertExchanges()
}
private fun insertExchanges() {
viewModelScope.launch {
insertExchangeUseCase.insert(DEFAULT_CURRENCY)
}
}
fun update(){
viewModelScope.launch {
updateExchangeUseCase.updateExchange(getAllExchangeUseCase.get(DEFAULT_CURRENCY))
}
}
}
In view model, insert is started automatically in init.

You can’t do something like this properly inside a Composable. Composables are only for creating UI. Remember the rule about no side effects. A function like your checkDate() should be called directly in onCreate() so it isn't called over and over as your Composition recomposes.
The problem with your current checkDate function: You save the "currentDate" every single this time this function is called, and you do it before checking what value was saved there before, so it will perpetually overwrite the old value before you can use it. You're also overcomplicating things by transforming the Long time into a Date and back to a Long for no reason.
Think about the logic of what you're doing:
Update the data if it has been a day since the last time you updated it.
So to implement this, we first check if the previously saved date is more than a day old. Only if it is old do we write the new date and update the data.
Here's a basic implementation. This is assuming you want to update when it has been at least 24 hours since the last update, since that seems like what you were trying to do.
fun checkDate(context: Context, viewModel: SplashScreenViewModel) {
val sharedPreferences = context.getSharedPreferences("date", Context.MODE_PRIVATE)
val lastTimeUpdatedKey = "lastTimeUpdated"
val now = System.currentTimeMillis()
val lastTimeUpdated = sharedPreferences.getLong(lastTimeUpdatedKey, 0)
val isDayPassed = now - lastTimeUpdated > TimeUnit.DAYS.toMillis(1)
if (isDayPassed) {
sharedPreferences.edit { // this is the shortcut way to edit and apply all in one
putLong(lastTimeUpdatedKey, today)
}
viewModel.update()
}
}
I would also change "date" to something longer and more unique so you don't accidentally use it somewhere else for different shared preferences. And I would rename your update() function to something more descriptive.
Don't forget to move the function call out of your Composable!

Related

Refresh Data - Kotlin - Flows - MVVM - Jetpack Compose

I'm trying to implement a refresh button. I want to be able to trigger the api call again when the refresh button is clicked. Kind of confused on what the best practice is. Here is my view model and composable.
View Model:
#HiltViewModel
class CoinListViewModel #Inject constructor(
private val getAllCoinsUseCase: GetListOfCoinsUseCase
): ViewModel() {
private val _state = mutableStateOf(CoinsListState()) // not exposed because mutable
val state: State<CoinsListState> = _state // expose this to composable because immutable
init {
getData()
}
// method to call the use case - put the data in the state object - then display state in the ui
private fun getData(){
getAllCoinsUseCase().onEach { resourceResult ->
when(resourceResult){
is Resource.Loading -> {
_state.value = CoinsListState(isLoading = true)
}
is Resource.Success -> {
_state.value = CoinsListState(coins = resourceResult.data ?: emptyList())
}
is Resource.Error -> {
_state.value = CoinsListState(
error = resourceResult.message ?: "Unexpected Error"
)
}
}
}.launchIn(viewModelScope) // must launch in coroutine scope because using a flow
}
}
Refresh Button:
#Composable
fun RefreshButton(navController: NavController, viewModel: CoinListViewModel) {
// Refresh Button
IconButton(
onClick = {
// Refresh Data
},
modifier = Modifier
.semantics {
contentDescription = "Refresh Button"
testTag = "Refresh Button Test Tag"
},
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = "Refresh Icon"
)
}
}
Keep your getData function private and add another function you can call it onRefreshDataEvent for example, and on this function call getData. You may say why I can just call getData directly, but by this approach we are separating the refresh event from getData function because you can have another function called getCachedData and you call it instead or you can have a limit, for example you will not refresh data only one time per minute, so all of this logic will be on onRefreshDataEvent and your first getData function stay clean and do it's job.
fun onRefreshDataEvent() {
getData()
}
You can add a time check for example so the user couldn't spam the refresh button, and the refresh could be used only a single time for each minute:
private var lastRefreshTime = 0
fun onRefreshDataEvent() {
val currentTime = System.currentTimeMillis()
if (currentTime - lastRefreshTime > (1000 * 60)) {
getData()
lastRefreshTime = currentTime
}
}
So imagine that the last logic is implemented in getData function, the code will be messy.

How to get updated results in StateFlow depending on parameter (sorting a list) with Jetpack Compose

I made a state with StateFlow with 2 lists. This is working good. I want to sort these lists according to a parameter that user will decide how to sort.
This is my code in ViewModel:
#HiltViewModel
class SubscriptionsViewModel #Inject constructor(
subscriptionsRepository: SubscriptionsRepository
) : ViewModel() {
private val _sortState = MutableStateFlow(
SortSubsType.ByDate
)
val sortState: StateFlow<SortSubsType> = _sortState.asStateFlow()
val uiState: StateFlow<SubscriptionsUiState> = combine(
subscriptionsRepository.getActiveSubscriptionsStream(_sortState.value),
subscriptionsRepository.getArchivedSubscriptionsStream(_sortState.value)
) { activeSubscriptions, archiveSubscriptions ->
SubscriptionsUiState.Subscriptions(
activeSubscriptions = activeSubscriptions,
archiveSubscriptions = archiveSubscriptions,
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SubscriptionsUiState.Loading
)
fun sortSubscriptions(sortType: SortSubsType) {
_sortState.value = sortType
}
}
sealed interface SubscriptionsUiState {
object Loading : SubscriptionsUiState
data class Subscriptions(
val activeSubscriptions: List<Subscription>,
val archiveSubscriptions: List<Subscription>,
) : SubscriptionsUiState
object Empty : SubscriptionsUiState
}
sortSubscriptions - is the function called from #Composable screen. Like this:
fun sortSubscriptions() {
viewModel.sortSubscriptions(sortType = selectedSortType.asSortSubsType())
isSortDialogVisible = false
}
Without the sort function, everything works. My question is how to fix this code so that the state changes when the sortState is changed. This is my first try working with StateFlow.
The problem is that when you create your uiState flow with combine, you just use the current value of sortState and never react to its changes.
You need something like this:
val uiState = sortState.flatMapLatest { sortValue ->
combine(
getActiveSubscriptionsStream(sortValue),
getArchivedSubscriptionsStream(sortValue)
) { ... }
}.stateIn(...)

Best practice for hiltviewmodel in compose

So i have a few questions about using hiltviewmodels, states and remember in compose.
For some context, i have a ViewPager set up
HorizontalPager(
count = 4,
modifier = Modifier.fillMaxSize(),
state = pagerState,
) { page ->
when (page) {
0 -> PagerOne()
1 -> PagerTwo()
2 -> PagerThree()
3 -> PagerFour()
}
}
Lets say i have a State in my viewmodel declared like this
private val _data: MutableState<DataClass> = mutableStateOf(DataClass())
var data: State<DataClass> = _data
First, where do i inject my viewmodel? Is it fine to do it in the constructor of my pager composable?
#Composable
fun PagerOne(viewmodel : PagerOneViewmodel = hiltViewModel()) {
...
And if i want to get the value from that viewmodel state, do i need to wrap it into a remember lambda?
#Composable
fun PagerOne(viewmodel : PagerOneViewmodel = hiltViewModel()) {
val myState = viewmodel.data or var myState by remember { viewmodel.data }
Next question about flow and .collectasstate. Lets say i have a function in my viewmodel which returns a flow of data from Room Database.
fun getRoomdata() = roomRepository.getLatesData()
Is it correct to get the data like this in my composable?
val roomData = viewmodel.getRoomdata().collectasState(initial = emptyRoomdata())
Everything is working like expected, but im not sure these are the best approaches.

Can I replace produceState with mutableStateOf in the Compose sample project?

The following Code A is from the project.
uiState is created by the delegate produceState, can I use mutableStateOf instead of produceState? If so, how can I write code?
Why can't I use Code B in the project?
Code A
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
...
}
#HiltViewModel
class DetailsViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val cityName = savedStateHandle.get<String>(KEY_ARG_DETAILS_CITY_NAME)!!
val cityDetails: Result<ExploreModel>
get() {
val destination = destinationsRepository.getDestination(cityName)
return if (destination != null) {
Result.Success(destination)
} else {
Result.Error(IllegalArgumentException("City doesn't exist"))
}
}
}
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
Code B
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val cityDetailsResult = viewModel.cityDetails
val uiState=if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
...
uiState is created by the delegate produceState, can I use mutableStateOf instead of produceState? If so, how can I write code?
No, you can't write it using the mutableStateOf (direct initialization not possible). In order to understand why it not possible we need to understand the use of produceState
According to documentation available here
produceState launches a coroutine scoped to the Composition that can
push values into a returned State. Use it to convert non-Compose state
into Compose state, for example bringing external subscription-driven
state such as Flow, LiveData, or RxJava into the Composition.
So basically it is compose way of converting non-Compose state to compose the state.
if you still want to use mutableStateOf you can do something like this
var uiState = remember { mutableStateOf(DetailsUIState())}
LaunchedEffect(key1 = someKey, block = {
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
})
Note: here someKey might be another variable which handles the recomposition of the state
What is wrong with this approach?
As you can see it's taking another variable someKey to recomposition. and handling it is quite tough compared to produceState
Why can't I use Code B in the project?
The problem with code B is you don't know whether the data is loaded or not while displaying the result. It's not observing the viewModel's data but its just getting the currently available data and based on that it gives the composition.
Imagine if the viewModel is getting data now you will be having UiState with isLoading = true but after some time you get data after a successful API call or error if it fails, at that time the composable function in this case DetailsScreen doesn't know about it at all unless you are observing the Ui state somewhere above this composition and causing this composition to recompose based on newState available.
But in produceState the state of the ui will automatically changed once the suspended network call completes ...

Why this function is called multiple times in Jetpack Compose?

I'm currently trying out Android Compose. I have a Text that shows price of a crypto coin. If a price goes up the color of a text should be green, but if a price goes down it should be red. The function is called when a user clicks a button. The problem is that the function showPrice() is called multiple times (sometimes just once, sometimes 2-4 times). And because of that the user can see the wrong color. What can I do to ensure that it's only called once?
MainActivity:
#Composable
fun MainScreen() {
val priceLiveData by viewModel.trackLiveData.observeAsState()
val price = priceLiveData ?: return
when (price) {
is ViewState.Response -> showPrice(price = price.data)
is ViewState.Error -> showError(price.text)
}
Button(onClick = {viewModel.post()} )
}
#Composable
private fun showPrice(price: Double) {
lastPrice = sharedPref.getFloat("eth", 0f).toDouble()
val color by animateColorAsState(if (price >= (lastPrice)) Color.Green else
Color.Red)
Log.v("TAG", "last=$lastPrice new = $price")
editor.putFloat("eth", price.toFloat()).apply()
Text(
text = price.toString(),
color = color,
fontSize = 28.sp,
fontFamily = fontFamily,
fontWeight = FontWeight.Bold
)
}
ViewModel:
#HiltViewModel
class MyViewModel #Inject constructor(
private val repository: Repository
): ViewModel() {
private val _trackLiveData: MutableLiveData<ViewState<Double>> = MutableLiveData()
val trackLiveData: LiveData<ViewState<Double>>
get() = _trackLiveData
fun post(
) = viewModelScope.launch(Dispatchers.Default) {
try {
val response = repository.post()
_trackLiveData.postValue(ViewState.Response(response.rate.round(7)))
} catch (e: Exception) {
_trackLiveData.postValue(ViewState.Error())
Log.v("TAG: viewmodelPost", e.message.toString())
}
}
}
ViewState:
sealed class ViewState<out T : Any> {
class Response<out T : Any>(val data: T): ViewState<T>()
class Error(val text:String = "Unknown error"): ViewState<Nothing>()
}
So when I press Button to call showPrice(). I can see these lines on Log:
2021-06-10 16:39:18.407 16781-16781/com.myapp.myapp V/TAG: last=2532.375732421875 new = 2532.7403716
2021-06-10 16:39:18.438 16781-16781/com.myapp.myapp V/TAG: last=2532.740478515625 new = 2532.7403716
2021-06-10 16:39:18.520 16781-16781/com.myapp.myapp V/TAG: last=2532.740478515625 new = 2532.7403716
What can I do to ensure that it's only called once?
Nothing, that's how it's meant to work. In the View system you would not ask "Why is my view invalidated 3 times?". The framework invalidates (recomposes) the view as it needs, you should not need to know or care when that happens.
The issue with your code is that your Composable is reading the old value from preferences, that is not how it should work, that value should be provided by the viewmodel as part of the state. Instead of providing just the new price, expose a Data Class that has both the new and old price and then use those 2 values in your composable to determine what color to show, or expose the price and the color to use.

Categories

Resources