I have a list with four elements: created_at, text, name, screen_name. The first represent a date of creation, the second the texto of a tweet and the latest the name and screen name of user.
I want to storage this information with lifespan, a random lifespan. For this i thinking using the cache and the implementation of this link https://medium.com/#kezhenxu94/how-to-build-your-own-cache-in-kotlin-1b0e86005591.
My questions is:
use a map key-value and save in value a string with all information (created_at, text, name, screen_name)?
how add this information in map with this code?
Please, give me a sample example for storage this data. Or if there is another way to make what i want more correctly, tell me.
My code in the moment:
class ExpirableCache(private val delegate: Cache, private val flushInterval: Long = TimeUnit.MINUTES.toMillis(1000)) : Cache {
private val dataTweet: Map<Long, Long>? = null
private var lastFlushTime = System.nanoTime()
override val size: Int
get() = delegate.size
override fun set(key: Any, value: Any) {
delegate[key] = value
}
override fun remove(key: Any): Any? {
recycle()
return delegate.remove(key)
}
override fun get(key: Any): Any? {
recycle()
return delegate[key]
}
override fun add(key: Any, value: Any) {
dataTweet[0, value]
}
override fun clear() = delegate.clear()
private fun recycle() {
val shouldRecycle = System.nanoTime() - lastFlushTime >= TimeUnit.MILLISECONDS.toNanos(flushInterval)
if (!shouldRecycle) return
delegate.clear()
}
}
Related
I have data class, MySection, which has the ID in it. I have another data class MyArticle whdoesn'ton't care about its content
To get the Section,
private fun getSectionFlow(): Flow<List<MySection>> =
callbackFlow {
val listCallBackFlow = object : SectionCallback<List<MySection>>() {
override fun onSuccess(sections: List<MySection>) {
trySend(sections)
}
override fun onError(errorResponse: ErrorResponse) {
close(Exception(errorResponse.reason))
}
}
provider.getSections(
listCallBackFlow
)
awaitClose()
}
And for Article,
private fun getArticleFlow(id: Long): Flow<List<MyArticle>> =
callbackFlow {
val listCallBackFlow = object : ArticleCallback<List<MyArticle>>() {
override fun onSuccess(sections: List<MyArticle>) {
trySend(sections)
}
override fun onError(errorResponse: ErrorResponse) {
close(Exception(errorResponse.reason))
}
}
provider.getArticle(
id, // to get the article list it needs section id
listCallBackFlow
)
awaitClose()
}
And finally, all values need to bind in List<MySectionWithArticle>
data class MySectionWithArticle (
val id: Long,
val title: String,
var myArticlesList: List<MyArticle> = emptyList()
)
So the inputs to getArticleFlow(id) should be the IDs of the values from getSectionFlow()
So the main question is how can I combine two callbackflow functions in such a way it gives me combine values MySection ,List<MyArticle> to MySectionWithArticle?
So for List<MySection>, each MySection need to call getArticleFlow(id) function and then List<MyArticle> combine with MySectionWithArticle data class
How can I achieve this functionality ?
My application uses Google Places API which data I later use to get weather from openweather.
I have a SearchFragment with RecyclerView where this happens.
Inside SearchFragment I observe the list I'm getting:
viewModel.predictions.observe(viewLifecycleOwner) {
citiesAdapter.submitList(it)
}
<...>
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_fragment_weather, menu)
<...>
searchView.onQueryTextChanged {
viewModel.searchQuery.value = it
}
}
My viewModel:
class SearchViewModel #Inject constructor(
private val repository: AutocompleteRepository,
private val weatherRepository: WeatherRepository
) : ViewModel() {
fun provideClient(client: PlacesClient) {
repository.provideClient(client)
}
val searchQuery = MutableStateFlow("")
private val autocompleteFlow = searchQuery.flatMapLatest {
repository.getPredictions(it)
}
val predictions = autocompleteFlow.asLiveData()
fun onAddPlace(place: PlacesPrediction, added: Boolean) {
viewModelScope.launch {
repository.update(place, added)
if (added) weatherRepository.addWeather(place)
else weatherRepository.delete(place)
}
}
fun onDestroy() = viewModelScope.launch {repository.clearDb()}
}
Inside adapter I bind my items like this:
inner class CityViewHolder(private val binding: ItemCityToAddBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.apply {
btnAdd.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val place = getItem(position)
btnAdd.animate().rotation(if (place.isAdded) 45f else 0f).start()
println("Current item state (isAdded): ${place.isAdded}")
listener.onAddClick(place, !place.isAdded)
}
}
}
}
fun bind(prediction : PlacesPrediction) {
binding.apply {
val cityName = prediction.fullText.split(", ")[0]
locationName.text = cityName
fullName.text = prediction.fullText
btnAdd.animate().rotation(if (prediction.isAdded) 45f else 0f).start()
}
}
}
Where listener is passed to my adapter as a parameter from my fragment:
override fun onAddClick(place: PlacesPrediction, isAdded: Boolean) {
viewModel.onAddPlace(place, isAdded)
println("Parameter passed to onClick: $isAdded, placeId = ${place.placeId}")
}
<...>
val citiesAdapter = CitiesAdapter(this)
My repository's update() method looks like this:
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().update(place.copy(isAdded = added))
And finally, my dao's update:
#Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(prediction: PlacesPrediction)
This is all tied up on PlacesPrediction class, an here it is:
#Entity(tableName = "autocomplete_table")
data class PlacesPrediction(
val fullText: String,
val latitude: Double,
val longitude: Double,
val placeId: String,
val isAdded: Boolean = false
) {
#PrimaryKey(autoGenerate = true) var id: Int = 0
}
So, my problem is that PlacesPredictions entries in my database are not getting updated. Actually, the only field I want to update with the code provided above is isAdded, but it stays the same after I press btnAdd of my list item. I used Android Studio's Database Inspector to verify that.
I tried using #Insert instead like so:
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(prediction: PlacesPrediction)
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.copy(isAdded = added))
But strangely it only inserts a copy of place, the original item I clicked on stays the same.
Workaround
I get the desired behavior only if I hack my way to it:
#Entity(tableName = "autocomplete_table")
data class PlacesPrediction(
val fullText: String,
val latitude: Double,
val longitude: Double,
val placeId: String,
var isAdded: Boolean = false,
#PrimaryKey(autoGenerate = true) var id: Int = 0
)
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.copy(isAdded = added, id = place.id))
And I don't like this soution at all. So my question is: how do I make #Update work?
As you probably already understood, the generated copy method of data classes ignores all members declared outside the constructor. So place.copy(isAdded = added) will generate a copy of all constructor parameters, but leave the id as the default 0, meaning a new element should be inserted, instead of updating an existing one.
Now this is my personal opinion:
Having the id as constructor parameter is the most elegant solution, as updates will work out of the box.
However if you dislike it that much, maybe an extension function might help you:
inline fun PlacesPrediction.preserveId(copyBuilder: PlacesPrediction.() -> PlacesPrediction): PlacesPrediction{
val copy = copyBuilder(this)
copy.id = this.id
return copy
}
//usage
suspend fun update(place: PlacesPrediction, added: Boolean) =
database.dao().insert(place.preserveId { copy(isAdded = added) })
I have this code in my class OutlookItemsAdapter: RecyclerView.Adapter<OutlookItemsViewHolder>():
companion object {
lateinit var outlookItems: SortedList<OutlookItem>
}
init {
outlookItems = SortedList(OutlookItem::class.java, object: SortedListAdapterCallback<OutlookItem>(this){
override fun areItemsTheSame(item1: OutlookItem, item2: OutlookItem): Boolean = item1 == item2
override fun compare(o1: OutlookItem, o2: OutlookItem): Int = o1.DateTime.compareTo(o2.DateTime)
override fun areContentsTheSame(oldItem: OutlookItem, newItem: OutlookItem): Boolean = oldItem.EntryId.equals(newItem.EntryId)
})
}
Where OutlookItem is:
class OutlookItem (
val Subject: String,
val EntryId: String,
val DateTime: LocalDateTime,
val MeetingUrl: String?
)
I need to write a function that receives an EntryId and deletes its equivalent OutlookItem from the list. Unfortunately SortedList doesn't have this capability (i.e. find/remove element determined by e.g. some lambda).
Is there an easy way to accomplish that or do I actually need to implement this finding mechanism on my own?
Since SortedList doesn't actually implement the List interface, you can't use any of Kotlin's helper higher-order functions for Lists on it.
A finding higher-order function can be written like this:
inline fun <T> SortedList<T>.firstOrNull(predicate: (T) -> Boolean): T? {
for (index in 0 until size()){
this[index].let {
if (predicate(it)) return it
}
}
return null
}
Then you could use this to perform your described task like this:
fun SortedList<OutlookItem>.removeByEntryId(entryId: String) {
val item = firstOrNull { it.EntryId == entryId }
if (item != null) remove(item)
}
By the way, by convention, property names should always start with a lower-case letter unless they are constants (in which case they are all caps).
I have Room Entity Class "Symptom" with name of Symptom and id of it.
#Entity(tableName = "symptoms")
data class Symptom(
#PrimaryKey #NonNull val id: Int,
val name: String) {
override fun toString(): String {
return "Symptom $id: $name"
}
}
I'm getting it in the following classses:
SymptomDao
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms WHERE id=:id LIMIT 1")
fun getSymptom(id: Int): Symptom
#Query("SELECT * FROM symptoms")
fun getAllSymptoms(): LiveData<List<Symptom>>
}
SymptomRepository
class SymptomRepository(private val symptomDao: SymptomDao) {
fun getSymptom(id: Int) = symptomDao.getSymptom(id)
fun getAllSymptoms() = symptomDao.getAllSymptoms()
}
SymptomsViewModel
class SymptomsViewModel(symptomRepository: SymptomRepository): ViewModel() {
private val symptomsList = symptomRepository.getAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
fun getAllSymptoms(): LiveData<List<Symptom>> {
return symptomsList
}
fun getAllSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
I have RecyclerView with list of SymptomItem with Checkboxes to remember which Symptoms of a list users chooses:
data class SymptomItem(
val symptom: Symptom,
var checked: Boolean = false)
Question
My question is how can I get LiveData<List<SymptomItem>> by LiveData<List<Symptom>>? I have just started learning MVVM and I can't find a simply answer how to do that. I have already tried to fill this list in various ways, but It loses checked variable every time I rotate my phone. I'll be grateful for any hints.
You'll need to store which items are checked by storing their Ids in a List within the ViewModel. Then you'll have combine the list of your Symptom objects and the list of which items are checked, and generate the list of SymptomItem objects.
I'm going to use Kotlin Flow to achieve this.
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms")
fun flowAllSymptoms(): Flow<List<Symptom>>
}
class SymptomRepository(private val symptomDao: SymptomDao) {
fun flowAllSymptoms() = symptomDao.flowAllSymptoms()
}
class SymptomsViewModel(
private val symptomRepository: SymptomRepository
) : ViewModel() {
private val symptomsListFlow = symptomRepository.flowAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
private var checkedIdsFlow = MutableStateFlow(emptyList<Int>())
init {
viewModelScope.launch {
collectSymptomsItems()
}
}
private suspend fun collectSymptomsItems() =
flowSymptomsItems().collect { symptomsItems ->
symptomsItemsList.postValue(symptomsItems)
}
private fun flowSymptomsItems() =
symptomsListFlow
.combine(checkedIdsFlow) { list, checkedIds ->
list.map { SymptomItem(it, checkedIds.contains(it.id)) }
}
fun checkItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).add(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun uncheckItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).remove(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun getSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
In your Fragment, observe getSymptomsItems() and update your adapter data.
The code is not tested, you may have to make small adjustments to make it compile.
I am trying to implement an infinite list with the Paging library, MVVM and LiveData.
In my View (in my case my fragment) I ask for data from the ViewModel and observe the changes:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getItems("someSearchQuery")
viewModel.pagedItems.observe(this, Observer<PagedList<Item>> {
// ItemPagedRecyclerAdapter
// EDIT --> Found this in the official Google example
// Workaround for an issue where RecyclerView incorrectly uses the loading / spinner
// item added to the end of the list as an anchor during initial load.
val layoutManager = (recycler.layoutManager as LinearLayoutManager)
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
if (position != RecyclerView.NO_POSITION) {
recycler.scrollToPosition(position)
}
})
}
In the ViewModel I fetch my data like this:
private val queryLiveData = MutableLiveData<String>()
private val itemResult: LiveData<LiveData<PagedList<Item>>> = Transformations.map(queryLiveData) { query ->
itemRepository.fetchItems(query)
}
val pagedItems: LiveData<PagedList<Item>> = Transformations.switchMap(itemResult) { it }
private fun getItems(queryString: String) {
queryLiveData.postValue(queryString)
}
In the repository I fetch for the data with:
fun fetchItems(query: String): LiveData<PagedList<Item>> {
val boundaryCallback = ItemBoundaryCallback(query, this.accessToken!!, remoteDataSource, localDataSource)
val dataSourceFactory = localDataSource.fetch(query)
return dataSourceFactory.toLiveData(
pageSize = Constants.PAGE_SIZE_ITEM_FETCH,
boundaryCallback = boundaryCallback)
}
As you might have already noticed, I used the Codelabs from Google as an example, but sadly I could not manage to make it work correctly.
class ItemBoundaryCallback(
private val query: String,
private val accessToken: AccessToken,
private val remoteDataSource: ItemRemoteDataSource,
private val localDataSource: Item LocalDataSource
) : PagedList.BoundaryCallback<Item>() {
private val executor = Executors.newSingleThreadExecutor()
private val helper = PagingRequestHelper(executor)
// keep the last requested page. When the request is successful, increment the page number.
private var lastRequestedPage = 0
private fun requestAndSaveData(query: String, helperCallback: PagingRequestHelper.Request.Callback) {
val searchData = SomeSearchData()
remoteDataSource.fetch Items(searchData, accessToken, lastRequestedPage * Constants.PAGE_SIZE_ITEMS_FETCH, { items ->
executor.execute {
localDataSource.insert(items) {
lastRequestedPage++
helperCallback.recordSuccess()
}
}
}, { error ->
helperCallback.recordFailure(Throwable(error))
})
}
#MainThread
override fun onZeroItemsLoaded() {
helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) {
requestAndSaveData(query, it)
}
}
#MainThread
override fun onItemAtEndLoaded(itemAtEnd: Item) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
requestAndSaveData(query, it)
}
}
My adapter for the list data:
class ItemPagedRecyclerAdapter : PagedListAdapter<Item, RecyclerView.ViewHolder>(ITEM_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ItemViewHolder(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
(holder as ItemViewHolder).bind(item, position)
}
}
companion object {
private val ITEM_COMPARATOR = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean =
olItem.id == newItem.id
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean =
oldItem == newItem
}
}
}
My problem right now is: The data is fetched and saved locally and is even displayed correctly in my list. But the data seems to be "looping", so there is always the same data showing despite there are different objects in the database (I checked with Stetho, about several hundred). Curiously the last item in the list is also always the same and sometimes there are items reloading while scrolling. Another problem is that it stops reloading at some point (sometimes 200, sometimes 300 data items).
I thought it might be because my ITEM_COMPARATOR was checking wrongly and returning the wrong boolean, so I set both to return true just to test, but this changed nothing.
I was also thinking of adding a config to the LivePagedListBuilder, but this also changed nothing. So I am a little bit stuck. I also looked into some examples doing it with a PageKeyedDataSource etc., but Google's example is working without it, so I want to know why my example is not working.
https://codelabs.developers.google.com/codelabs/android-paging/index.html?index=..%2F..index#5
Edit:
Google does have another example in their blueprints. I added it to the code. https://github.com/android/architecture-components-samples/blob/master/PagingWithNetworkSample/app/src/main/java/com/android/example/paging/pagingwithnetwork/reddit/ui/RedditActivity.kt.
Now it is loading correctly, but when the loading happens, some items in the list still flip.
Edit 2:
I edited the BoundaryCallback, still not working (now provided the PagingRequestHelper suggested by Google).
Edit 3:
I tried it just with the remote part and it works perfectly. There seems to be a problem with Room/the datasource that room provides.
Ok, just to complete this issue, I found the solution.
To make this work, you must have a consistent list-order from your backend/api-data. The flipping was caused by the data that was constantly sent in another order than before and therefore made some items in the list "flip" around.
So you have to save an additional field to your data (an index-like field) to order your data accordingly. The fetch from the local database in the DAO is then done with a ORDER BY statement. I hope I can maybe help someone who forgot the same as I did:
#Query("SELECT * FROM items ORDER BY indexFromBackend")
abstract fun fetchItems(): DataSource.Factory<Int, Items>
You have to override PageKeyedDataSource to implement paging logic with Paging library. Check out this link