Mark Scheer
Mark Scheer

Reputation: 117

Unable to use results from Observing Room entity

My Goal

I am trying to make a RecyclerView that contains a list of Routine entities. I have created a many to many relationship between Routine entities and Exercise entities. My RecyclerView needs to contain the name of the Routine and a string of the Exercise names associated with a Routine.

Exercise

@Entity
data class Exercise(
    @PrimaryKey(autoGenerate = true)
    val exerciseId: Int,
    val exerciseName: String
)

Routine

@Entity
data class Routine(
    @PrimaryKey(autoGenerate = true)
    val routineId: Int,
    val routineName: String
)

ExerciseRoutineJoin

@Entity(
    primaryKeys = arrayOf("exerciseId", "routineId"),
    foreignKeys = arrayOf(
        ForeignKey(
            entity = Exercise::class,
            parentColumns = arrayOf("exerciseId"),
            childColumns = arrayOf("exerciseId"),
            onDelete = ForeignKey.NO_ACTION
        ),
        ForeignKey(
            entity = Routine::class,
            parentColumns = arrayOf("routineId"),
            childColumns = arrayOf("routineId"),
            onDelete = ForeignKey.NO_ACTION
        )
    )
)
data class ExerciseRoutineJoin(val exerciseId: Int, val routineId: Int)

AppDatabase

@Database(entities = arrayOf(Routine::class, Exercise::class, ExerciseRoutineJoin::class), version = 1, exportSchema = false)
public abstract class AppDatabase : RoomDatabase() {

   abstract fun routineDao(): RoutineDao
   abstract fun exerciseDao(): ExerciseDao
   abstract fun exerciseRoutineJoinDao(): ExerciseRoutineJoinDao

   companion object {
        // Singleton prevents multiple instances of database opening at the
        // same time. 
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        AppDatabase::class.java, 
                        "app_database"
                    ).build()
                INSTANCE = instance
                return instance
            }
        }
   }
}

In the Fragment that contains this RecyclerView, I am observing the RoutineViewModel and sending the Routines to the RecyclerView adapter.

private fun sendRoutinesToAdapter(adapter: RoutineAdapter) {
    routineViewModel.allRoutines.observe(viewLifecycleOwner, Observer { routines ->
        routines?.let { adapter.setRoutines(it) }
    })
}

My Problem

Because the ExerciseRoutineJoin does not have the Exercise name in its table, I am creating a MutableMap of the ExerciseRoutineJoins with the Routine ID as the key and a string of all the Exercise names associated with that ID as the value.

private fun combineExerciseNamesForRoutine(
        routineId: Int,
        joins: List<ExerciseRoutineJoin>
    ): String {

        exerciseViewModel = ViewModelProvider(this).get(ExerciseViewModel::class.java)

        val builder = StringBuilder()

        joins.forEach {
            if (it.routineId == routineId) {
                // TODO Why is builder not appending the string?
                exerciseViewModel.getExerciseById(it.exerciseId)?.observe(viewLifecycleOwner,
                    Observer { exercise -> builder.append(exercise.exerciseName) })
            }
        }
        return builder.toString()
    }

My Observer does not seem to be providing an Exercise name to my StringBuilder. I have tried having the ExerciseViewModel return a List instead of LiveData<List<Exercise>> but that cause a ton of problems with calling the database from the main thread.

How can I use the results from observing the Exercises? Is there a smarter way to achieve this goal of the RecyclerView with content from different entities?

StartWorkoutFragment.kt

class StartWorkoutFragment : Fragment() {
    private lateinit var routineViewModel: RoutineViewModel
    private lateinit var exerciseViewModel: ExerciseViewModel
    private lateinit var exerciseRoutineJoinViewModel: ExerciseRoutineJoinViewModel
    private var adapter: RoutineAdapter? = null

    companion object {
        fun newInstance(): StartWorkoutFragment {
            return StartWorkoutFragment()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_start_workout, container, false)
        val activity = activity as Context

        val recyclerView: RecyclerView = view.findViewById(R.id.rv_routines)
        val adapter = RoutineAdapter(activity)
        recyclerView.layoutManager = GridLayoutManager(activity, 2)
        recyclerView.adapter = adapter

        routineViewModel = ViewModelProvider(this).get(RoutineViewModel::class.java)

        setUpRecyclerViewContent(adapter)
        setUpAddRoutineButton(view)

        return view
    }

    private fun setUpRecyclerViewContent(adapter: RoutineAdapter) {
        sendRoutinesToAdapter(adapter)
        getExerciseRoutineJoinsFromDatabase()
    }

    private fun sendRoutinesToAdapter(adapter: RoutineAdapter) {
        routineViewModel.allRoutines.observe(viewLifecycleOwner, Observer { routines ->
            routines?.let { adapter.setRoutines(it) }
        })
    }

    private fun getExerciseRoutineJoinsFromDatabase() {
        val routineIdWithExercisesAsStringPairs = mutableMapOf<Int, String>() // create empty map

        exerciseRoutineJoinViewModel =
            ViewModelProvider(this).get(ExerciseRoutineJoinViewModel::class.java)
        exerciseRoutineJoinViewModel.allExerciseRoutineJoins.observe(
            viewLifecycleOwner,
            Observer { joins ->
                joins?.let { it ->
                    it.forEach {
                        if (!routineIdWithExercisesAsStringPairs.containsKey(it.routineId))   // if the routine ID hasn't already been mapped, map the routine ID to the exercise String
                            routineIdWithExercisesAsStringPairs[it.routineId] =
                                combineExerciseNamesForRoutine(it.routineId, joins)
                    }
                }
            })
        adapter?.setRoutineIdWithExercisesAsStringPairs(routineIdWithExercisesAsStringPairs)   // send the map to the RecyclerView adapter

    }

    private fun combineExerciseNamesForRoutine(
        routineId: Int,
        joins: List<ExerciseRoutineJoin>
    ): String {

        exerciseViewModel = ViewModelProvider(this).get(ExerciseViewModel::class.java)

        val builder = StringBuilder()

        joins.forEach {
            if (it.routineId == routineId) {
                // TODO Why is builder not appending the string?
                exerciseViewModel.getExerciseById(it.exerciseId)?.observe(viewLifecycleOwner,
                    Observer { exercise -> builder.append(exercise.exerciseName) })
            }
        }
        return builder.toString()
    }

    private fun setUpAddRoutineButton(view: View) {
        val button: Button = view.findViewById(R.id.buttonAddRoutine)
        button.setOnClickListener {
            view.findNavController().navigate(R.id.navigation_add_routine)
        }
    }
}

Upvotes: 0

Views: 58

Answers (1)

Bram Stoker
Bram Stoker

Reputation: 1252

You can query routines with exercises from the database as explained here:

data class RoutineWithExercises(
    @Embedded val routine: Routine,
    @Relation(
         parentColumn = "routineId",
         entityColumn = "exerciseId",
         associateBy = @Junction(ExerciseRoutineJoin::class)
    )
    val exercices: List<Exercise>
)

And then in your RoutineDao:

@Transaction
@Query("SELECT * FROM Routine")
fun getRoutinesWithExercises(): List<RoutineWithExercises>

Then you have both the Routine and it's Exercises.

Upvotes: 1

Related Questions