I am using SimpleDateFormat in my ViewModel to format some data. As you can see this way is not flexible because I couldn't change my format pattern whenever I want. What should I do to improve this ?
class DateFormat #Inject constructor() : SimpleDateFormat("EEE, MMM dd", Locale.US) {
fun convertUnixTimeToDate(unixTime: Long): String {
return try {
val date = Date(unixTime * 1000)
this.format(date)
} catch (e: NumberFormatException) {
e.toString()
}
}
}
#HiltViewModel
class WeatherViewModel #Inject constructor(
private val repository: WeatherRepository,
) : ViewModel() {
#Inject
lateinit var dateFormat: DateFormat
I change to use function extentions for this.
const val DATE_PATTERN = "EEE, MMM dd"
private const val DAY_NAME_IN_WEEK_PATTERN = "EEE"
fun Long.toDateString(pattern: String): String {
val sdf = SimpleDateFormat(pattern, Locale.getDefault())
val date = Date(this * 1000)
return sdf.format(date)
}
fun Long.toDayNameInWeek(currentTimestamp: Long): String {
return when {
(this - currentTimestamp) * 1000 < DateUtils.HOUR_IN_MILLIS * 2 -> "Today"
(this - currentTimestamp) * 1000 < DateUtils.DAY_IN_MILLIS * 2 -> "Tomorrow"
else -> this.toDateString(DAY_NAME_IN_WEEK_PATTERN)
}
}
Related
I am begginer in testing android apps. I have problem while testing my viewmodel. I want to test view model function which gets data from repository and make some logic with it.
class CurrencyViewModel(
private val repository: Repository
) : ViewModel() {
private var day: LocalDate = LocalDate.now()
private val listWithFormattedCurrencies = mutableListOf<CurrencyInfo>()
var currencyList = MutableLiveData<MutableList<CurrencyInfo>>()
fun getRecordsFromFixer() {
CoroutineScope(Dispatchers.IO).launch {
var listWithCurrencies: MutableList<CurrencyInfo>
val formatter = DateTimeFormatter.ISO_LOCAL_DATE
val formatted = day.format(formatter)
listWithCurrencies = repository.getRecordsFromApi(formatted)
for (i in listWithCurrencies) {
val formattedRate = formattingRates(i.currency, i.rate)
listWithFormattedCurrencies.add(CurrencyInfo(i.date, i.currency, formattedRate))
}
currencyList.postValue(listWithFormattedCurrencies)
day = day.minusDays(1)
}
}
private fun formattingRates(name : String, rate : String): String {
return if(name =="BTC" && name.isNotEmpty() && rate.isNotEmpty()){
String.format("%.8f", rate.toDouble())
}else if (name.isNotEmpty() && rate.isNotEmpty()){
String.format("%.4f", rate.toDouble())
}else{
rate
}
}
}
class CurrencyViewModelTest {
#ExperimentalCoroutinesApi
private val testDispatcher = StandardTestDispatcher()
#get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var viewModel: CurrencyViewModel
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#After
fun tearDown(){
Dispatchers.resetMain()
}
#Test
fun `formatting empty name returns empty rate` () = runBlocking{
val currencyRepository = mock(currencyRepository::class.java)
val viewModel = CurrencyViewModel(currencyRepository)
val date = "2022-02-02"
val listOfCurrencies = mutableListOf(CurrencyInfo(date,"", "0.222"))
Mockito.`when` (currencyRepository.getRecordsFromApi(date)).thenReturn(listOfCurrencies)
viewModel.getRecordsFromFixer()
assertThat(viewModel.currencyList.getOrAwaitValue().contains(CurrencyInfo(date,"",""))).isTrue()
}
}
Error
Exception in thread "DefaultDispatcher-worker-1 #coroutine#2" java.lang.NullPointerException
.
.
.
LiveData value was never set.
java.util.concurrent.TimeoutException: LiveData value was never set.
The problem is that currencyRepository.getRecordsFromApi(date)) returns null object.
I tried to check if currencyRepository.getRecordsFromApi(date)) returns proper data after initializing it apart from ViewModel function and it works fine.
Why currencyRepository.getRecordsFromApi(date)) returns null when it is invoked in ViewModel function?
I finally found answer. I used Mockk library and rebulid my ViewModel.
class CurrencyViewModel(
private val repository: Repository
) : ViewModel() {
private var day: LocalDate = LocalDate.now()
private var formattedDate = getDate(day)
var listWithFormattedCurrencies = mutableListOf<CurrencyInfo>()
var currencyList = MutableLiveData<MutableList<CurrencyInfo>>()
var listWithCurrencies = mutableListOf<CurrencyInfo>()
fun getRecordsFromFixer() {
CoroutineScope(Dispatchers.IO).launch {
listWithCurrencies = getRecords()
for (i in listWithCurrencies) {
val formattedRate = formattingRates(i.currency, i.rate)
listWithFormattedCurrencies.add(CurrencyInfo(i.date, i.currency, formattedRate))
}
currencyList.postValue(listWithFormattedCurrencies)
day = day.minusDays(1)
formattedDate = getDate(day)
}
}
private fun formattingRates(name : String, rate : String): String {
return if(name =="BTC" && name.isNotEmpty() && rate.isNotEmpty()){
String.format("%.8f", rate.toDouble())
}else if (name.isNotEmpty() && rate.isNotEmpty()){
String.format("%.4f", rate.toDouble())
}else{
rate
}
}
private suspend fun getRecords() : MutableList<CurrencyInfo>{
return repository.getRecordsFromApi(formattedDate)
}
private fun getDate(day : LocalDate) : String{
val formatter = DateTimeFormatter.ISO_LOCAL_DATE
return day.format(formatter)
}
}
class CurrencyViewModelTest {
private lateinit var repository: currencyRepository
private lateinit var viewModel: CurrencyViewModel
#ExperimentalCoroutinesApi
private val testDispatcher = StandardTestDispatcher()
#get:Rule
val rule = InstantTaskExecutorRule()
#Before
fun setUp() {
repository = currencyRepository()
viewModel = spyk(CurrencyViewModel(repository), recordPrivateCalls = true)
Dispatchers.setMain(testDispatcher)
}
#After
fun tearDown(){
Dispatchers.resetMain()
}
#Test
fun `formatting empty name and rate returns empty name and rate` () = runBlocking{
val date = "2022-02-02"
val listOfCurrencies = mutableListOf(CurrencyInfo(date,"", ""))
coEvery{ viewModel["getRecords"]()} returns listOfCurrencies
coEvery { viewModel.getRecordsFromFixer() } answers {callOriginal()}
viewModel.getRecordsFromFixer()
assertEquals(mutableListOf(CurrencyInfo(date,"", "")), viewModel.currencyList.getOrAwaitValue())
}
#Test
fun `receiving proper formatted data` () = runBlocking{
val date = "2022-02-02"
val listOfCurrencies = mutableListOf(CurrencyInfo(date,"USD", "0.4567234124"))
coEvery{ viewModel["getRecords"]()} returns listOfCurrencies
coEvery { viewModel.getRecordsFromFixer() } answers {callOriginal()}
viewModel.getRecordsFromFixer()
assertEquals(mutableListOf(CurrencyInfo(date,"USD", "0,4567")), viewModel.currencyList.getOrAwaitValue())
}
#Test
fun `receiving proper formatted BTC rate` () = runBlocking{
val date = "2022-02-02"
val listOfCurrencies = mutableListOf(CurrencyInfo(date,"BTC", "0.45672341241412345435654367645"))
coEvery{ viewModel["getRecords"]()} returns listOfCurrencies
coEvery { viewModel.getRecordsFromFixer() } answers {callOriginal()}
viewModel.getRecordsFromFixer()
assertEquals(mutableListOf(CurrencyInfo(date,"BTC", "0,45672341")), viewModel.currencyList.getOrAwaitValue())
}
I persist time state in viewModel and need to store current state in preferences and load time state again when app is closed and opened again by user. Here is my current code.
ViewModel
class TimeViewModel(): ViewModel(){
private val _time = MutableLiveData<Long>()
val time: LiveData<Long> = _time
fun onTimeChange(newTime: Long) {
_time.value = newTime
}
}
Composable function
#Composable
fun Timer(timeViewModel:TimeViewModel = viewModel()){
LaunchedEffect(key1 = time ){
delay(1000L)
timeViewModel.onTimeChange(time + 1)
}
val time: Long by timeViewModel.time.observeAsState(0L)
val dec = DecimalFormat("00")
val min = time / 60
val sec = time % 60
Text(
text = dec.format(min) + ":" + dec.format(sec),
style = MaterialTheme.typography.body1
)
}
Try using dagger for dependency injection you could create a singleton with your store this way:
#Module
#InstallIn(SingletonComponent::class)
object MyModule {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "preferences")
#Singleton
#Provides
fun provideDataStore(#ApplicationContext app: Context ) : DataStore<Preferences> = app.dataStore
}
then just inject in your viewModels and use it!
#HiltViewModel
class HomeViewModel #Inject constructor(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
private val myKey = stringPreferencesKey("USER_KEY")// init your key identifier here
fun mySuperCoolWrite(){
viewModelScope.launch {
dataStore.edit {
it[myKey] = body.auth
}
}
}
fun mySuperCoolRead(){
viewModelScope.launch {
val preferences = dataStore.data.first()
preferences[myKey]?.let {
// here access your stored value with "it"
}
}
}
}
Or just inject in your controller constructor with
#ApplicationContext app: Context
here you can find more info
I've got a TypeConverter class linked to my database, but it doesn't seem to be getting called and therefore i'm getting the following error
ForecastAQSpecified.java:9: error: Cannot figure out how to read this field from a cursor.
private final java.time.LocalDate dt = null;
dt is the datetime I get in my JSON response in the form of an epoch Int. I'm looking to convert this to a LocalDate. My Entity class is as follows
#Entity(tableName = "future_AQ", indices = [Index(value = ["dt"], unique = true)])
data class ForecastAQ(
#PrimaryKey(autoGenerate = true)
val id: Int? = null,
#Embedded
val components: Components,
val dt: Int,
#Embedded
val main: Main
)
val dt: Int is the what I am looking to convert. My TypeConverter class is as follows
object DateConverter {
#TypeConverter
#JvmStatic
fun stringToDate(str: String?) = str?.let {
LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
}
#TypeConverter
#JvmStatic
fun dateToString(localDate: LocalDate?) = localDate?.format(
DateTimeFormatter.ISO_LOCAL_DATE
)
#TypeConverter
#JvmStatic
fun timestampToDateTime(dt : Int?): LocalDate? {
try {
val sdf = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss")
val netDate = dt?.times(1000L)?.let { Date(it) }
val sdf2 = sdf.format(netDate)
return LocalDate.parse(sdf2, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
Log.d("TAG", "date " + LocalDate.parse(sdf2, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toString());
} catch (e : Exception) {
return null
e.printStackTrace()
}
}
The first two are required in another part of the database where I receive the date response as a String. They are getting called just fine. My The final one is not however - and this is the one I need to convert the Int to a DateTime object
The database class is as follows for reference
#Database(
entities = [CurrentConditions::class, ConditionsLocation::class, ForecastWeather::class, ForecastAQ::class],
version = 1
)
#TypeConverters(DateConverter::class)
abstract class ConditionsDatabase : RoomDatabase() {
abstract fun getCurrentConditionsDAO() : CurrentConditionsDAO
abstract fun getConditionsLocationDAO() : ConditionsLocationDAO
abstract fun getForecastWeatherDAO() : ForecastWeatherDAO
abstract fun getForecastAirQualDAO() : ForecastAqDAO
companion object{
#Volatile private var instance: ConditionsDatabase? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDatabase(context).also {
instance = it
}
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext,
ConditionsDatabase::class.java, CURRENT_CONDITIONS_DATABASE_NAME)
.build()
}
}
Many thanks
You should create a class instead of object
replace
object DateConverter {
to
class DateConverter {
remove JvmStatic annotation
I'm making an app with NASA APOD api with paging library. I use boundarycallback class for caching. My problem is that when there is already cached apod items in the database from yesterday, the new pictures won't show in the recycelrview unless I use onItemAtFrontLoaded() and scroll up. The only way I found was to nuke the database and insert all items again.
class ApodBoundaryCallback(
private var api: ApodApi,
private val cache: ApodDao,
private val disposable: CompositeDisposable
)
: PagedList.BoundaryCallback<ApodEntity>() {
val networkState = MutableLiveData<NetworkState>()
private val TAG = ApodBoundaryCallback::class.java.simpleName
private var formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
override fun onZeroItemsLoaded() {
Log.d(TAG, "onZeroItemsLoaded")
if (networkState.value?.status == Status.RUNNING)
return
networkState.postValue(NetworkState.LOADING)
getApodByDate(addOrSubFromDate(formatter, -20), "")
}
override fun onItemAtEndLoaded(itemAtEnd: ApodEntity) {
Log.d(TAG, "onItemAtEndLoaded")
if (networkState.value?.status == Status.RUNNING)
return
networkState.postValue(NetworkState.LOADING)
val endDate = addOrSubFromStringDate(formatter, -1, itemAtEnd.date)
val startDate = addOrSubFromStringDate(formatter, -21, itemAtEnd.date)
getApodByDate(startDate, endDate)
}
override fun onItemAtFrontLoaded(itemAtFront: ApodEntity) {
if (networkState.value?.status == Status.RUNNING)
return
networkState.postValue(NetworkState.LOADING)
getApodByDate(addOrSubFromStringDate(formatter, +1, itemAtFront.date), "")
}
private fun getApodByDate(startDate:String, endDate:String){
disposable.add(
api
.getPictures(
startDate,
endDate,
true,
Constants.API_KEY)
.subscribeOn(Schedulers.io())
.map { it.filter {apodEntity -> apodEntity.mediaType == "image" } }
.subscribe(
{
networkState.postValue(NetworkState.LOADED)
cache.insertApodList(it)
},
{
Log.e("ApodBoundary", it.message)
networkState.postValue(NetworkState.error(it.message))
}
)
)
}
private fun addOrSubFromStringDate(formatter: SimpleDateFormat, days: Int, Date: String): String{
val cal = Calendar.getInstance()
cal.time = formatter.parse(Date);
cal.add(Calendar.DATE, days)
return formatter.format(cal.time)
}
private fun addOrSubFromDate(formatter: SimpleDateFormat, days: Int): String{
val cal = Calendar.getInstance()
cal.add(Calendar.DATE, days)
return formatter.format(cal.time)
}
}
I want to select past date to max current date. There is methods like setMinDate() and setMaxDate() in single date selection in date picker. But, how i can do that in date range selection in material date range picker?
You have to use CalendarConstraints for Minimum and Maximum dates restriction in Date Range picker.
val constraintsBuilderRange = CalendarConstraints.Builder()
val dateValidatorMin: DateValidator = DateValidatorPointForward.from(millisOfMinimum)
val dateValidatorMax: DateValidator = DateValidatorPointBackward.before(millisOfMaximum)
val listValidators = ArrayList<DateValidator>()
listValidators.add(dateValidatorMin)
listValidators.add(dateValidatorMax)
val validators = CompositeDateValidator.allOf(listValidators)
constraintsBuilderRange.setValidator(validators)
val datePicker = MaterialDatePicker.Builder.dateRangePicker()
.setTitleText("Select range")
.setCalendarConstraints(constraintsBuilderRange.build())
.build()
Set your current date in setMaxDate()
And setMinDate() whatever you want to show.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
showRangePickerDialog()
}
private fun showRangePickerDialog() {
val builderRange = MaterialDatePicker.Builder.dateRangePicker()
builderRange.setCalendarConstraints(defineRange().build())
val pickerRange = builderRange.build()
pickerRange.show(supportFragmentManager, pickerRange.toString())
}
private fun defineRange(): CalendarConstraints.Builder {
val constraintsBuilderRange = CalendarConstraints.Builder()
val calendarStart: Calendar = GregorianCalendar.getInstance()
val calendarEnd: Calendar = GregorianCalendar.getInstance()
val year = 2020
calendarStart.set(year, 1, 1)
calendarEnd.set(year, 6, 30)
val minDate = calendarStart.timeInMillis
val maxDate = calendarEnd.timeInMillis
constraintsBuilderRange.setStart(minDate)
constraintsBuilderRange.setEnd(maxDate)
constraintsBuilderRange.setValidator(RangeValidator(minDate, maxDate))
return constraintsBuilderRange
}
}
class RangeValidator(private val minDate:Long, private val maxDate:Long) : CalendarConstraints.DateValidator{
constructor(parcel: Parcel) : this(
parcel.readLong(),
parcel.readLong()
)
override fun writeToParcel(dest: Parcel?, flags: Int) {
}
override fun describeContents(): Int {
}
override fun isValid(date: Long): Boolean {
return !(minDate > date || maxDate < date)
}
companion object CREATOR : Parcelable.Creator<RangeValidator> {
override fun createFromParcel(parcel: Parcel): RangeValidator {
return RangeValidator(parcel)
}
override fun newArray(size: Int): Array<RangeValidator?> {
return arrayOfNulls(size)
}
}
}
Try the above code but have to do some changes in range as per your requirement.
Just add these two lines of code in your code and you can prevent future date selection.
maxDate: moment().subtract(0, 'days'),
"This Month": [moment().startOf("month"), moment().subtract(0, 'days')],
var rangesVal = {
position: "inherit",
drops: "down",
maxDate: moment().subtract(0, 'days'),
minDate: minDate,
opens: "center",
maxSpan: {
months: 12,
},
ranges: {
Today: [moment(), moment()],
Yesterday: [moment().subtract(1, "days"), moment().subtract(1, "days")],
"Last 7 Days": [moment().subtract(6, "days"), moment()],
"Last 30 Days": [moment().subtract(29, "days"), moment()],
"This Month": [moment().startOf("month"), moment().subtract(0, 'days')],
"Last Month": [
moment().subtract(1, "month").startOf("month"),
moment().subtract(1, "month").endOf("month"),
],
},
};