ojAlgo - Optimization issue with contiguous block logic? - kotlin

I am using ojAlgo to work through a classroom scheduling problem I'm doing as an exercise. The source code can be found here on GitHub in the kotlin_solution folder:
https://github.com/thomasnield/optimized-scheduling-demo
Everything was going fine until I started to implement contiguous block logic which I've described over on Math Exchange. Bascially, if a class session requires 4 blocks then those 4 blocks need to be together.
For some reason, this modeling logic screeches to a halt when I implement the contiguous logic in this part of the code. It is churning infinitely.
Here is the Kotlin code in it's entirety:
import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.util.concurrent.atomic.AtomicInteger
// declare model
val model = ExpressionsBasedModel()
val funcId = AtomicInteger(0)
val variableId = AtomicInteger(0)
fun variable() = Variable(variableId.incrementAndGet().toString().let { "Variable$it" }).apply(model::addVariable)
fun addExpression() = funcId.incrementAndGet().let { "Func$it"}.let { model.addExpression(it) }
// Any Monday through Friday date range will work
val operatingDates = LocalDate.of(2017,10,16)..LocalDate.of(2017,10,20)
val operatingDay = LocalTime.of(8,0)..LocalTime.of(17,0)
val breaks = listOf<ClosedRange<LocalTime>>(
//LocalTime.of(11,30)..LocalTime.of(13,0)
)
// classes
val scheduledClasses = listOf(
ScheduledClass(id=1, name="Psych 101", hoursLength=1.0, repetitions=2),
ScheduledClass(id=2, name="English 101", hoursLength=1.5, repetitions=3),
ScheduledClass(id=3, name="Math 300", hoursLength=1.5, repetitions=2),
ScheduledClass(id=4, name="Psych 300", hoursLength=3.0, repetitions=1),
ScheduledClass(id=5, name="Calculus I", hoursLength=2.0, repetitions=2),
ScheduledClass(id=6, name="Linear Algebra I", hoursLength=2.0, repetitions=3),
ScheduledClass(id=7, name="Sociology 101", hoursLength=1.0, repetitions=2),
ScheduledClass(id=8, name="Biology 101", hoursLength=1.0, repetitions=2)
)
fun main(args: Array<String>) {
println("Job started at ${LocalTime.now()}")
applyConstraints()
println(model.minimise())
Session.all.forEach {
println("${it.name}-${it.repetitionIndex}: ${it.start.dayOfWeek} ${it.start.toLocalTime()}-${it.end.toLocalTime()}")
}
println("Job ended at ${LocalTime.now()}")
}
data class Block(val dateTimeRange: ClosedRange<LocalDateTime>) {
val timeRange = dateTimeRange.let { it.start.toLocalTime()..it.endInclusive.toLocalTime() }
fun addConstraints() {
val f = addExpression().upper(1)
OccupationState.all.filter { it.block == this }.forEach {
f.set(it.occupied, 1)
}
}
companion object {
// Operating blocks
val all by lazy {
generateSequence(operatingDates.start.atTime(operatingDay.start)) {
it.plusMinutes(15).takeIf { it.plusMinutes(15) <= operatingDates.endInclusive.atTime(operatingDay.endInclusive) }
}.filter { it.toLocalTime() in operatingDay }
.map { Block(it..it.plusMinutes(15)) }
.toList()
}
}
}
data class ScheduledClass(val id: Int,
val name: String,
val hoursLength: Double,
val repetitions: Int) {
val sessions by lazy {
Session.all.filter { it.parentClass == this }
}
fun addConstraints() {
//guide 3 repetitions to be fixed on MONDAY, WEDNESDAY, FRIDAY
if (repetitions == 3) {
sessions.forEach { session ->
val f = addExpression().level(session.blocksNeeded)
session.occupationStates.asSequence()
.filter {
it.block.dateTimeRange.start.dayOfWeek ==
when(session.repetitionIndex) {
1 -> DayOfWeek.MONDAY
2 -> DayOfWeek.WEDNESDAY
3 -> DayOfWeek.FRIDAY
else -> throw Exception("Must be 1/2/3")
}
}
.forEach {
f.set(it.occupied,1)
}
}
}
//guide two repetitions to be 48 hours apart (in development)
if (repetitions == 2) {
val first = sessions.find { it.repetitionIndex == 1 }!!
val second = sessions.find { it.repetitionIndex == 2 }!!
}
}
companion object {
val all by lazy { scheduledClasses }
}
}
data class Session(val id: Int,
val name: String,
val hoursLength: Double,
val repetitionIndex: Int,
val parentClass: ScheduledClass) {
val blocksNeeded = (hoursLength * 4).toInt()
val occupationStates by lazy {
OccupationState.all.asSequence().filter { it.session == this }.toList()
}
val start get() = occupationStates.asSequence().filter { it.occupied.value.toInt() == 1 }
.map { it.block.dateTimeRange.start }
.min()!!
val end get() = occupationStates.asSequence().filter { it.occupied.value.toInt() == 1 }
.map { it.block.dateTimeRange.endInclusive }
.max()!!
fun addConstraints() {
val f1 = addExpression().level(0)
//block out exceptions
occupationStates.asSequence()
.filter { os -> breaks.any { os.block.timeRange.start in it } || os.block.timeRange.start !in operatingDay }
.forEach {
// b = 0, where b is occupation state
// this means it should never be occupied
f1.set(it.occupied, 1)
}
//sum of all boolean states for this session must equal the # blocks needed
val f2 = addExpression().level(blocksNeeded)
occupationStates.forEach {
f2.set(it.occupied, 1)
}
//ensure all occupied blocks are consecutive
// PROBLEM, not finding a solution and stalling
/*
b1, b2, b3 .. bn = binary from each group
all binaries must sum to 1, indicating fully consecutive group exists
b1 + b2 + b3 + .. bn = 1
*/
val consecutiveStateConstraint = addExpression().level(1)
(0..occupationStates.size).asSequence().map { i ->
occupationStates.subList(i, (i + blocksNeeded).let { if (it > occupationStates.size) occupationStates.size else it })
}.filter { it.size == blocksNeeded }
.forEach { grp ->
/*
b = 1,0 binary for group
n = blocks needed
x1, x2, x3 .. xn = occupation states in group
x1 + x2 + x3 .. + xn - bn >= 0
*/
val binaryForGroup = variable().binary()
consecutiveStateConstraint.set(binaryForGroup, 1)
addExpression().lower(0).apply {
grp.forEach {
set(it.occupied,1)
}
set(binaryForGroup, -1 * blocksNeeded)
}
}
}
companion object {
val all by lazy {
ScheduledClass.all.asSequence().flatMap { sc ->
(1..sc.repetitions).asSequence()
.map { Session(sc.id, sc.name, sc.hoursLength, it, sc) }
}.toList()
}
}
}
data class OccupationState(val block: Block, val session: Session) {
val occupied = variable().binary()
companion object {
val all by lazy {
Block.all.asSequence().flatMap { b ->
Session.all.asSequence().map { OccupationState(b,it) }
}.toList()
}
}
}
fun applyConstraints() {
Session.all.forEach { it.addConstraints() }
ScheduledClass.all.forEach { it.addConstraints() }
Block.all.forEach { it.addConstraints() }
}
** UPDATE **
I created a self-contained example that simplifies what I'm trying to do above. It seems the contiguous logic is indeed the problem, and the more "slots" the problem has the slower it performs. At 48000 variables, the contiguous logic seems to churn forever.
import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
import org.ojalgo.optimisation.integer.IntegerSolver
import java.util.concurrent.ThreadLocalRandom
import java.util.concurrent.atomic.AtomicInteger
// declare ojAlgo Model
val model = ExpressionsBasedModel()
// custom DSL for expression inputs, eliminate naming and adding
val funcId = AtomicInteger(0)
val variableId = AtomicInteger(0)
fun variable() = Variable(variableId.incrementAndGet().toString().let { "Variable$it" }).apply(model::addVariable)
fun addExpression() = funcId.incrementAndGet().let { "Func$it"}.let { model.addExpression(it) }
val letterCount = 9
val numberCount = 480
val minContiguousBlocks = 4
val maxContiguousBlocks = 4
fun main(args: Array<String>) {
Letter.all.forEach { it.addConstraints() }
Number.all.forEach { it.addConstraints() }
model.countVariables().run { println("$this variables") }
model.options.debug(IntegerSolver::class.java)
model.minimise().run(::println)
Letter.all.joinToString(prefix = "\t", separator = "\t").run(::println)
Letter.all.map { it.slotsNeeded }.joinToString(prefix = "\t", separator = "\t").run(::println)
Number.all.forEach { n ->
Letter.all.asSequence().map { l -> l.slots.first { it.number == n }.occupied.value.toInt() }
.joinToString(prefix = "$n ", separator = "\t").run { println(this) }
}
}
class Letter(val value: String, val slotsNeeded: Int = 1) {
val slots by lazy {
Slot.all.filter { it.letter == this }.sortedBy { it.number.value }
}
fun addConstraints() {
// Letter must be assigned once
addExpression().level(1).apply {
slots.forEach { set(it.occupied, 1) }
}
//handle recurrences
if (slotsNeeded > 1) {
slots.rollingBatches(slotsNeeded).forEach { batch ->
val first = batch.first()
addExpression().upper(0).apply {
batch.asSequence().flatMap { it.number.slots.asSequence() }
.forEach {
set(it.occupied, 1)
}
set(first.number.cumulativeState, -1)
}
}
}
//prevent scheduling at end of window
// all slots must sum to 0 in region smaller than slots needed
addExpression().level(0).apply {
slots.takeLast(slotsNeeded - 1)
.forEach {
set(it.occupied, 1)
}
}
}
override fun toString() = value
companion object {
val all = ('A'..'Z').asSequence()
.take(letterCount)
.map { it.toString() }
.map { Letter(it, ThreadLocalRandom.current().nextInt(minContiguousBlocks, maxContiguousBlocks + 1)) }
.toList()
}
}
class Number(val value: Int) {
val slots by lazy {
Slot.all.filter { it.number == this }
}
// b_x
val cumulativeState = variable().lower(0).upper(1)
fun addConstraints() {
// Number can only be assigned once
addExpression().upper(1).apply {
slots.forEach { set(it.occupied, 1) }
}
}
companion object {
val all = (1..numberCount).asSequence()
.map { Number(it) }
.toList()
}
override fun toString() = value.toString().let { if (it.length == 1) "$it " else it }
}
data class Slot(val letter: Letter, val number: Number) {
val occupied = variable().binary()
companion object {
val all = Letter.all.asSequence().flatMap { letter ->
Number.all.asSequence().map { number -> Slot(letter, number) }
}.toList()
}
override fun toString() = "$letter$number: ${occupied?.value?.toInt()}"
}
fun <T> List<T>.rollingBatches(batchSize: Int) = (0..size).asSequence().map { i ->
subList(i, (i + batchSize).let { if (it > size) size else it })
}.filter { it.size == batchSize }

I figured it out. I'll update this answer later with the full mathematical modeling explanation. Essentially for each 15 minute block I queried for slot groups that include that block, and declared the sum of all of them must be no more than one. This ended up being acceptably efficient as it runs in 30-60 seconds.
The code is here on GitHub, as well as below:
https://github.com/thomasnield/optimized-scheduling-demo
import org.ojalgo.optimisation.integer.IntegerSolver
import java.time.LocalDate
import java.time.LocalTime
import org.ojalgo.optimisation.ExpressionsBasedModel
import org.ojalgo.optimisation.Variable
import java.time.DayOfWeek
import java.time.LocalDateTime
import java.util.concurrent.atomic.AtomicInteger
// Any Monday through Friday date range will work
val operatingDates = LocalDate.of(2017,10,16)..LocalDate.of(2017,10,20)
val operatingDay = LocalTime.of(8,0)..LocalTime.of(17,0)
val breaks = listOf<ClosedRange<LocalTime>>(
LocalTime.of(11,30)..LocalTime.of(13,0)
)
// classes
val scheduledClasses = listOf(
ScheduledClass(id=1, name="Psych 101",hoursLength=1.0, repetitions=2),
ScheduledClass(id=2, name="English 101", hoursLength=1.5, repetitions=3),
ScheduledClass(id=3, name="Math 300", hoursLength=1.5, repetitions=2),
ScheduledClass(id=4, name="Psych 300", hoursLength=3.0, repetitions=1),
ScheduledClass(id=5, name="Calculus I", hoursLength=2.0, repetitions=2),
ScheduledClass(id=6, name="Linear Algebra I", hoursLength=2.0, repetitions=3),
ScheduledClass(id=7, name="Sociology 101", hoursLength=1.0, repetitions=2),
ScheduledClass(id=8, name="Biology 101", hoursLength=1.0, repetitions=2)
)
fun main(args: Array<String>) {
println("Job started at ${LocalTime.now()}")
applyConstraints()
model.countVariables().run { println("$this variables") }
model.options.apply {
//debug(IntegerSolver::class.java)
iterations_suffice = 0
}
println(model.minimise())
ScheduledClass.all.forEach {
println("${it.name}- ${it.daysOfWeek.joinToString("/")} ${it.start.toLocalTime()}-${it.end.toLocalTime()}")
}
println("Job ended at ${LocalTime.now()}")
}
// declare model
val model = ExpressionsBasedModel()
val funcId = AtomicInteger(0)
val variableId = AtomicInteger(0)
fun variable() = Variable(variableId.incrementAndGet().toString().let { "Variable$it" }).apply(model::addVariable)
fun addExpression() = funcId.incrementAndGet().let { "Func$it"}.let { model.addExpression(it) }
data class Block(val dateTimeRange: ClosedRange<LocalDateTime>) {
val timeRange = dateTimeRange.let { it.start.toLocalTime()..it.endInclusive.toLocalTime() }
val available get() = (breaks.all { timeRange.start !in it } && timeRange.start in operatingDay)
//val cumulativeState = variable().apply { if (available) lower(0).upper(1) else level(0) }
val slots by lazy {
Slot.all.filter { it.block == this }
}
fun addConstraints() {
if (available) {
addExpression().lower(0).upper(1).apply {
ScheduledClass.all.asSequence().flatMap { it.anchorOverlapFor(this#Block) }
.filter { it.block.available }
.forEach {
set(it.occupied, 1)
}
}
} else {
ScheduledClass.all.asSequence().flatMap { it.anchorOverlapFor(this#Block) }
.forEach {
it.occupied.level(0)
}
}
}
companion object {
// Operating blocks
val all by lazy {
generateSequence(operatingDates.start.atStartOfDay()) {
it.plusMinutes(15).takeIf { it.plusMinutes(15) <= operatingDates.endInclusive.atTime(23,59) }
}.map { Block(it..it.plusMinutes(15)) }
.toList()
}
fun applyConstraints() {
all.forEach { it.addConstraints() }
}
}
}
data class ScheduledClass(val id: Int,
val name: String,
val hoursLength: Double,
val repetitions: Int,
val repetitionGapDays: Int = 2) {
val repetitionGapSlots = repetitionGapDays * 24 * 4
val slotsNeeded = (hoursLength * 4).toInt()
val slots by lazy {
Slot.all.asSequence().filter { it.session == this }.toList()
}
val batches by lazy {
slots.rollingRecurrences(slotsNeeded = slotsNeeded, gapSize = repetitionGapSlots, recurrencesNeeded = repetitions)
}
fun anchorOverlapFor(block: Block) = batches.asSequence()
.filter { it.flatMap { it }.any { it.block == block } }
.map { it.first().first() }
val start get() = slots.asSequence().filter { it.occupied.value.toInt() == 1 }.map { it.block.dateTimeRange.start }.min()!!
val end get() = start.plusMinutes((hoursLength * 60.0).toLong())
val daysOfWeek get() = (0..(repetitions-1)).asSequence().map { start.dayOfWeek.plus(it.toLong() * repetitionGapDays) }.sorted()
fun addConstraints() {
//sum of all boolean states for this session must be 1
addExpression().level(1).apply {
slots.forEach {
set(it.occupied, 1)
}
}
//guide Mon/Wed/Fri for three repetitions
if (repetitions == 3) {
addExpression().level(1).apply {
slots.filter { it.block.dateTimeRange.start.dayOfWeek == DayOfWeek.MONDAY }
.forEach {
set(it.occupied, 1)
}
}
}
//guide two repetitions to start on Mon, Tues, or Wed
if (repetitions == 2) {
addExpression().level(1).apply {
slots.filter { it.block.dateTimeRange.start.dayOfWeek in DayOfWeek.MONDAY..DayOfWeek.WEDNESDAY }.forEach {
set(it.occupied, 1)
}
}
}
}
companion object {
val all by lazy { scheduledClasses }
}
}
data class Slot(val block: Block, val session: ScheduledClass) {
val occupied = variable().apply { if (block.available) binary() else level(0) }
companion object {
val all by lazy {
Block.all.asSequence().flatMap { b ->
ScheduledClass.all.asSequence().map { Slot(b,it) }
}.toList()
}
}
}
fun applyConstraints() {
Block.applyConstraints()
ScheduledClass.all.forEach { it.addConstraints() }
}
fun <T> List<T>.rollingBatches(batchSize: Int) = (0..size).asSequence().map { i ->
subList(i, (i + batchSize).let { if (it > size) size else it })
}.filter { it.size == batchSize }
fun <T> List<T>.rollingRecurrences(slotsNeeded: Int, gapSize: Int, recurrencesNeeded: Int) =
(0..size).asSequence().map { i ->
(1..recurrencesNeeded).asSequence().map { (it - 1) * gapSize }
.filter { it + i < size}
.map { r ->
subList(i + r, (i + r + slotsNeeded).let { if (it > size) size else it })
}.filter { it.size == slotsNeeded }
.toList()
}.filter { it.size == recurrencesNeeded }

Related

Loading state adapter is always loading

My task is to load recyclerView and show pagination which I both implemented.
I also implemented LoadingState for adapter.
ApiCall:
#GET("top-headlines?sources=bbc-news,techcrunch&apiKey=${BuildConfig.API_KEY}")
suspend fun getTopHeadlinesArticles(
#Query("page") page:Int = 1,
#Query("q") query: String,
) : Response<ArticleListResponse>
I wont show paging because it is working so I will jump to repository:
fun getSearchResult(query: String) =
Pager(
config = PagingConfig(
pageSize = 1,
maxSize = 20,
enablePlaceholders = false
),
pagingSourceFactory = { ArticlePaging(newsService, query)}
).flow
ViewModel:
#OptIn(ExperimentalCoroutinesApi::class)
val news = _currentQuery.flatMapLatest { query ->
articleRepository.getSearchResult(query).cachedIn(viewModelScope)
}
Fragment:
binding.recyclerViewTop.layoutManager = LinearLayoutManager(context)
binding.recyclerViewTop.adapter = adapter.withLoadStateHeaderAndFooter(
header = ArticleLoadStateAdapter { adapter.retry() },
footer = ArticleLoadStateAdapter { adapter.retry() }
)
lifecycleScope.launchWhenCreated {
viewModel.news.collect { articles ->
adapter.submitData(articles)
}
}
And LoadStateViewHolder in LoadStateAdapter:
init {
binding.buttonRetry.setOnClickListener {
retry.invoke()
}
}
fun bind(loadState: LoadState) {
binding.apply {
progressBar.isVisible = loadState is LoadState.Loading
buttonRetry.isVisible = loadState !is LoadState.Loading
textViewError.isVisible = loadState !is LoadState.Loading
}
}
I already predefined DEFAULT_QUERY so I only get 1 Article.
Problem is that progressBar loading is always visible and I have no more articles to show.
Edit: when i have at least 10 items to show this works fine
Edit 2nd: Add ArticlePagingSource if that can help
override fun getRefreshKey(state: PagingState<Int, ArticleResponse>): Int? {
return state.anchorPosition?.let {
val anchorPage = state.closestPageToPosition(it)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
try {
val currentPageList = params.key ?: 1
response = if(query != ""){
newsService.getTopHeadlinesArticles(currentPageList, query)
} else{
newsService.getTopHeadlinesArticles(currentPageList)
}
val responseList = mutableListOf<ArticleResponse>()
val data = response.body()?.articleResponses ?: emptyList()
responseList.addAll(data)
val prevKey = if (currentPageList == 1) null else currentPageList - 1
return LoadResult.Page(
responseList,
prevKey,
currentPageList.plus(1)
)

Observing live data from an API is not updating ui when data changes

I am trying to develop a football app demo. Data comes from an API from the api
It loads data as expected when app started, but when score of match changes, ui is not updating for scores by itself. I am using DiffUtil getChangePayload() to detect changes in score and status fields of Match objects which comes from the response. But it is not triggering when live match data changes. What am i missing?
P.S. I put layout in SwipeRefreshLayout and when i refresh, it gets scores and update the ui. But i want to see the match status and scores updating by itself.
Here is my code:
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday: MutableLiveData<List<Matche>> = MutableLiveData()
init {
getMatchesToday()
}
fun getMatchesToday() = viewModelScope.launch {
safeMatchesToday()
}
private suspend fun safeMatchesToday() {
if (Constants.checkConnection(this)) {
val response = repository.getMatchesToday()
if (response.isSuccessful) {
response.body()?.let {
matchesToday.postValue(it.matches)
}
}
}
}
}
class MatchesTodayFragment : Fragment() {
private var _binding: FragmentMatchesTodayBinding? =null
private val binding get() = _binding!!
private lateinit var mMatchesAdapter: MatchesAdapter
private val viewModel: MatchesViewModel by viewModels {
MatchesViewModelFactory(requireActivity().application, (requireActivity().application as MatchesApplication).repository)
}
#RequiresApi(Build.VERSION_CODES.N)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
viewModel.matchesToday.observe(viewLifecycleOwner) { matches ->
mMatchesAdapter.differ.submitList(matches)
}
binding.srlMatchesToday.setOnRefreshListener {
viewModel.getMatchesToday()
binding.srlMatchesToday.isRefreshing = false
}
}
}
class MatchesAdapter(val fragment: Fragment): RecyclerView.Adapter<MatchesAdapter.ViewHolder>() {
private val differCallback = object: DiffUtil.ItemCallback<Matche>() {
override fun areItemsTheSame(oldItem: Matche, newItem: Matche): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Matche, newItem: Matche): Boolean {
return oldItem.status == newItem.status &&
oldItem.score.fullTime.home == newItem.score.fullTime.home &&
oldItem.score.fullTime.away == newItem.score.fullTime.away &&
oldItem == newItem
}
override fun getChangePayload(oldItem: Matche, newItem: Matche): Any? {
val bundle: Bundle = bundleOf()
if (oldItem.status != newItem.status) {
bundle.apply {
putString(Constants.MATCH_STATUS, newItem.status)
}
}
if (oldItem.score.fullTime.home != newItem.score.fullTime.home) {
bundle.apply {
putInt(Constants.HOME_SCORE, newItem.score.fullTime.home)
}
}
if (oldItem.score.fullTime.away != newItem.score.fullTime.away) {
bundle.apply {
putInt(Constants.AWAY_SCORE, newItem.score.fullTime.away)
}
}
if (bundle.size() == 0) {
return null
}
return bundle
}
}
val differ = AsyncListDiffer(this, differCallback)
#SuppressLint("UseCompatLoadingForDrawables")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val match = differ.currentList[position]
holder.apply {
Glide.with(fragment)
.load(match.homeTeam.crest)
.placeholder(fragment.resources.getDrawable(R.drawable.ic_ball))
.into(ivHomeTeamImage)
Glide.with(fragment)
.load(match.awayTeam.crest)
.placeholder(fragment.resources.getDrawable(R.drawable.ic_ball))
.into(ivAwayTeamImage)
tvHomeTeamName.text = match.homeTeam.name
tvAwayTeamName.text = match.awayTeam.name
when (match.status) {
Constants.TIMED -> {
tvMatchTime.text = Constants.toTimeForTR(match.utcDate)
tvHomeTeamScore.text = "-"
tvAwayTeamScore.text = "-"
}
Constants.PAUSED -> {
tvMatchTime.text = Constants.FIRST_HALF
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
Constants.FINISHED -> {
tvMatchTime.text = Constants.FINISHED
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
else -> {
tvMatchTime.text = Constants.IN_PLAY
tvHomeTeamScore.text = match.score.fullTime.home.toString()
tvAwayTeamScore.text = match.score.fullTime.away.toString()
}
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
val item = payloads[0] as Bundle
val status = item.getString(Constants.MATCH_STATUS)
val homeScore = item.getInt(Constants.HOME_SCORE)
val awayScore = item.getInt(Constants.AWAY_SCORE)
holder.apply {
tvMatchTime.text = status
tvHomeTeamScore.text = homeScore.toString()
tvAwayTeamScore.text = awayScore.toString()
Log.e("fuck", status.toString())
}
}
super.onBindViewHolder(holder, position, payloads)
}
override fun getItemCount(): Int {
return differ.currentList.size
}
}
LiveData only pushes new values if you command it to. Since you want to do it repeatedly, you need to create a loop. This is very easy to do using the liveData coroutine builder.
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday = liveData {
while (true) {
if (Constants.checkConnection(this)) {
val response = repository.getMatchesToday()
if (response.isSuccessful) {
response.body()?.let {
emit(it.matches)
}
}
}
delay(5000) // however many ms you want between fetches
}
}
}
If this is a Retrofit response, I think checking isSuccessful is redundant because body() will be non-null if and only if isSuccessful is true. So it could be simplified a bit from what you have:
class MatchesViewModel(
app: Application,
private val repository: MatchesRepository
): AndroidViewModel(app) {
val matchesToday = liveData {
while (true) {
if (Constants.checkConnection(this)) {
repository.getMatchesToday()?.body()?.matches?.let(::emit)
}
delay(5000) // however many ms you want between fetches
}
}
}

Problem saving data into room db from paging library api response

I have an application built using Jetpack Compose , where i also use paging library 3 to fetch data from db , i have multiple remote mediator where i fetch data and save it directly into database , the issue is that sometimes data gets saved , sometimes not , it goes to the point that sometimes one of the two only gets data stored.
Remote Mediator 1:
#ExperimentalPagingApi
class PopularClothingRemoteMediator #Inject constructor(
private val clothingApi: ClothingApi,
private val clothingDatabase: ClothingDatabase
) : RemoteMediator<Int, Clothing>(){
private val clothingDao = clothingDatabase.clothingDao()
private val clothingRemoteKeysDao = clothingDatabase.clothingRemoteKeysDao()
override suspend fun initialize(): InitializeAction {
val currentTime = System.currentTimeMillis()
val lastUpdated = clothingRemoteKeysDao.getRemoteKeys(clothingId = 1)?.lastUpdated ?: 0L
val cacheTimeout = 1440
val diffInMinutes = (currentTime - lastUpdated) / 1000 / 60
return if (diffInMinutes.toInt() <= cacheTimeout) {
// Log.d("RemoteMediator", "UP TO DATE")
InitializeAction.SKIP_INITIAL_REFRESH
} else {
// Log.d("RemoteMediator", "REFRESH")
InitializeAction.LAUNCH_INITIAL_REFRESH
}
}
override suspend fun load(loadType: LoadType, state: PagingState<Int, Clothing>): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextPage?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevPage = remoteKeys?.prevPage
?: return MediatorResult.Success(
endOfPaginationReached = remoteKeys != null
)
prevPage
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
val nextPage = remoteKeys?.nextPage
?: return MediatorResult.Success(
endOfPaginationReached = remoteKeys != null
)
nextPage
}
}
val response = clothingApi.getPopularClothing(page = page)
if (response.popularClothing.isNotEmpty()) {
clothingDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
clothingDao.deleteAllClothing()
clothingRemoteKeysDao.deleteAllRemoteKeys()
}
val prevPage = response.prevPage
val nextPage = response.nextPage
val keys = response.popularClothing.map { clothing ->
ClothingRemoteKeys(
clothingId = clothing.clothingId,
prevPage = prevPage,
nextPage = nextPage,
lastUpdated = response.lastUpdated
)
}
// When i debug this code , it works fine and the last line is executed
// the issue data sometimes gets saved , sometimes not
clothingRemoteKeysDao.addAllRemoteKeys(clothingRemoteKeys = keys)
clothingDao.addClothing(clothing = response.popularClothing)
}
}
MediatorResult.Success(endOfPaginationReached = response.nextPage == null)
} catch (e: Exception) {
return MediatorResult.Error(e)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Clothing>
): ClothingRemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.clothingId?.let { clothingId ->
clothingRemoteKeysDao.getRemoteKeys(clothingId = clothingId)
}
}
}
private suspend fun getRemoteKeyForFirstItem(
state: PagingState<Int, Clothing>
): ClothingRemoteKeys? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { clothing ->
clothingRemoteKeysDao.getRemoteKeys(clothingId = clothing.clothingId)
}
}
private suspend fun getRemoteKeyForLastItem(
state: PagingState<Int, Clothing>
): ClothingRemoteKeys? {
return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { clothing ->
clothingRemoteKeysDao.getRemoteKeys(clothingId = clothing.clothingId)
}
}
}
Remote Mediator 2:
class OuterwearRemoteMediator #Inject constructor(
private val clothingApi: ClothingApi,
private val clothingDatabase: ClothingDatabase
) : RemoteMediator<Int, Clothing>() {
private val clothingDao = clothingDatabase.clothingDao()
private val clothingRemoteKeysDao = clothingDatabase.clothingRemoteKeysDao()
override suspend fun initialize(): InitializeAction {
val currentTime = System.currentTimeMillis()
val lastUpdated = clothingRemoteKeysDao.getRemoteKeys(clothingId = 1)?.lastUpdated ?: 0L
val cacheTimeout = 1440
val diffInMinutes = (currentTime - lastUpdated) / 1000 / 60
return if (diffInMinutes.toInt() <= cacheTimeout) {
// Log.d("RemoteMediator", "UP TO DATE")
InitializeAction.SKIP_INITIAL_REFRESH
} else {
// Log.d("RemoteMediator", "REFRESH")
InitializeAction.LAUNCH_INITIAL_REFRESH
}
}
override suspend fun load(loadType: LoadType, state: PagingState<Int, Clothing>): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextPage?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
val prevPage = remoteKeys?.prevPage
?: return MediatorResult.Success(
endOfPaginationReached = remoteKeys != null
)
prevPage
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
val nextPage = remoteKeys?.nextPage
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
nextPage
}
}
val response = clothingApi.getOuterwear(page = page)
if (response.outerwear.isNotEmpty()) {
clothingDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
clothingDao.deleteAllClothing()
clothingRemoteKeysDao.deleteAllRemoteKeys()
}
val prevPage = response.prevPage
val nextPage = response.nextPage
val keys = response.outerwear.map { clothing ->
ClothingRemoteKeys(
clothingId = clothing.clothingId,
prevPage = prevPage,
nextPage = nextPage,
lastUpdated = response.lastUpdated
)
}
// the same thing here
// When i debug this code , it works fine and the last line is executed
// the issue data sometimes gets saved , sometimes not
clothingRemoteKeysDao.addAllRemoteKeys(clothingRemoteKeys = keys)
clothingDao.addClothing(clothing = response.outerwear)
}
}
MediatorResult.Success(endOfPaginationReached = response.nextPage == null)
} catch (e: Exception) {
return MediatorResult.Error(e)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Clothing>): ClothingRemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.clothingId?.let { clothingId ->
clothingRemoteKeysDao.getRemoteKeys(clothingId = clothingId)
}
}
}
private suspend fun getRemoteKeyForFirstItem(
state: PagingState<Int, Clothing>): ClothingRemoteKeys? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { clothing ->
clothingRemoteKeysDao.getRemoteKeys(clothingId = clothing.clothingId)
}
}
private suspend fun getRemoteKeyForLastItem(
state: PagingState<Int, Clothing>
): ClothingRemoteKeys? {
return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { clothing ->
clothingRemoteKeysDao.getRemoteKeys(clothingId = clothing.clothingId)
}
}

Kotlin - replace item in a map

I'm write a function that should replace an item in map. I have reach it using HashMap but is possible to write something similar in a "kotlinmatic way"?
fun HashMap<Int, String>.ignoreFields(path: String, fieldsToIgnore: FieldsToIgnore) = {
val filtered: List<Field> = fieldsToIgnore.ignoreBodyFields.filter { it.tagFile == path }
filtered.forEach {
val updatedJson = JsonPath.parse(JsonPath.parse(this[it.order])
.read<String>(whatevervariable))
.delete(it.field)
.apply { set("equalJson", this) }
.jsonString()
this.replace(it.order, updatedJson)
}
return this
}
update using map based on answers:
fun Map<Int, String>.ignoreFields(path: String, fieldsToIgnore: FieldsToIgnore): Map<Int, String> {
val filtered = fieldsToIgnore.ignoreBodyFields.filter { it.tagFile == path }
return this.mapValues {m ->
val field = filtered.find { it.order == m.key }
if (field != null) {
JsonPath.parse(JsonPath.parse(this[field.order])
.read<String>(whatevervariable))
.delete(field.field)
.apply { set(pathBodyEqualToJson, this) }
.jsonString()
} else {
m.value
}
}
}
You can use mapValues to conditionally use different value for same key. This will return a new immutable map
Update: filtered will now be a map of order to updatedJson
fun HashMap<Int, String>.ignoreFields(path: String,
fieldsToIgnore: FieldsToIgnore): Map<Int, String> {
val filtered: Map<Int, String> = fieldsToIgnore.ignoreBodyFields
.filter { it.tagFile == path }
.map {
val updatedJson = JsonPath.parse(JsonPath.parse(this[it.order])
.read<String>(whatevervariable))
.delete(it.field)
.apply { set("equalJson", this) }
.jsonString()
it.order to updatedJson
}
return this.mapValues {
filtered.getOrElse(it.key) { it.value }
}
}
A possible solution is to use mapValues() operator, e.g.:
fun Map<Int, String>.ignoreFields(ignoredFields: List<Int>): Map<Int, String> {
return this.mapValues {
if (ignoredFields.contains(it.key)) {
"whatever"
} else {
it.value
}
}
}
// Example
val ignoredFields = listOf<Int>(1,3)
val input = mapOf<Int, String>(1 to "a", 2 to "b", 3 to "c")
val output = input.ignoreFields(ignoredFields)
print(output)
// prints {1=whatever, 2=b, 3=whatever}

RxJava different output between Flowable and Observable with Window and Groupby

I'm using RxJava2 with code that boils down to something like this:
val whitespaceRegex = Regex("\\s+")
val queryRegex = Regex("query=([^&]+)", RegexOption.IGNORE_CASE)
val dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME
#JvmStatic
fun main(args: Array<String>) {
val cnt = AtomicLong()
val templateStr = "|date| /ignored/ query=|query|"
val random = ThreadLocalRandom.current()
var curDate = ZonedDateTime.of(LocalDate.of(2016, Month.JANUARY, 1), LocalTime.MIDNIGHT, ZoneId.of("UTC"))
val generator = Flowable.generate<String> { emitter ->
// normally these are read from a file, this is for the example
val next = cnt.incrementAndGet()
if (next % 3000 == 0L) {
curDate = curDate.plusDays(1)
}
if (next < 100000) {
val curStr = templateStr
.replace("|date|", dateTimeFormatter.format(curDate))
.replace("|query|", random.nextInt(1, 1000).toString())
emitter.onNext(curStr)
} else {
emitter.onComplete()
}
}
val source = generator
.map { line ->
val cols = line.split(whitespaceRegex)
val queryRaw = queryRegex.find(cols[2])?.groupValues?.get(1) ?: ""
val query = URLDecoder.decode(queryRaw, Charsets.UTF_8.name()).toLowerCase().replace(whitespaceRegex, " ").trim()
val date = dateTimeFormatter.parse(cols[0])
Pair(LocalDate.from(date), query)
}
.share()
source
.window(source.map { it.first }.distinctUntilChanged())
.flatMap { window ->
window
.groupBy { pair -> pair }
.flatMap({ grouping ->
grouping
.count()
.map {
Pair(grouping.key, it)
}.toFlowable()
})
}
.subscribe({ println("Result: $it}") }, { it.printStackTrace() }, { println("Done") })
}
When I use Observable.generate it works fine, but with Flowable.generate there is no output. This is counting how many queries occurred on a given day. The day increase sequentially so I form a window of each day, then count the queries with a groupBy. Do I need to do this differently with Flowable?
As akarnokd mentioned, this was due to flatMap having a default maxConcurrency of 128. I found this issue, https://github.com/ReactiveX/RxJava/issues/5126, which describes the reason in more detail. This fixes the problem:
val cnt = AtomicLong()
val templateStr = "|date| /ignored/ query=|query|"
val random = ThreadLocalRandom.current()
var curDate = ZonedDateTime.of(LocalDate.of(2016, Month.JANUARY, 1), LocalTime.MIDNIGHT, ZoneId.of("UTC"))
val generator = Flowable.generate<String> { emitter ->
val next = cnt.incrementAndGet()
if (next % 3000 == 0L) {
curDate = curDate.plusDays(1)
}
if (next < 1000000) {
val curStr = templateStr
.replace("|date|", dateTimeFormatter.format(curDate))
.replace("|query|", random.nextInt(1, 1000).toString())
emitter.onNext(curStr)
} else {
emitter.onComplete()
}
}
val source = generator
.map { line ->
val cols = line.split(whitespaceRegex)
val queryRaw = queryRegex.find(cols[2])?.groupValues?.get(1) ?: ""
val query = URLDecoder.decode(queryRaw, Charsets.UTF_8.name()).toLowerCase().replace(whitespaceRegex, " ").trim()
val date = dateTimeFormatter.parse(cols[0])
Pair(LocalDate.from(date), query)
}
.share()
source
.window(source.map { it.first }.distinctUntilChanged().doOnEach({println("Win: $it")}))
.flatMap( { window ->
window
.groupBy { pair -> pair }
.flatMap({ grouping ->
grouping
.count()
.map {
Pair(grouping.key, it)
}.toFlowable()
// fix is here
}, Int.MAX_VALUE)
// and here
}, Int.MAX_VALUE)
.subscribe({ println("Result: $it}") }, { it.printStackTrace() }, { println("Done") })