LiveData分层数据结构设计及DataBinding下UI重绘优化问询
Great question—this is a super common pain point when working with nested data structures in LiveData + DataBinding, especially when using Room to fetch hierarchical data. Let's break this down step by step, starting with structuring your data, then solving the partial redraw issue for your Workout Fragment.
1. Building a Reasonable Hierarchical Data Structure for LiveData + DataBinding
The core idea is to avoid monolithic "god objects" that force full UI redraws when any tiny detail changes. Instead, design each layer of your data to be independently observable, so changes only propagate to the relevant UI components:
- Make each entity observable: Have
Workout,Group,Lift, andSetextendBaseObservable(for Java) or use observable property wrappers (for Kotlin). This lets DataBinding listen to changes on individual properties without needing the entire parent object to refresh. - Use observable lists for child collections: Replace regular
List/ArrayListwithObservableArrayListfor nested collections (likeGroup.liftsorLift.sets). This tells DataBinding to update only added/removed/moved items in the UI, not rebuild the entire container. - Keep Room entities separate: Don’t cram all nested data into a single Room entity. Use Room's relationships (like
@Relation) to fetch the full hierarchy, then map those immutable Room entities to your observable data classes in your repository.
Example Kotlin implementation for a Set:
class Set : BaseObservable() { @get:Bindable var weight: Double = 0.0 set(value) { field = value notifyPropertyChanged(BR.weight) } @get:Bindable var reps: Int = 0 set(value) { field = value notifyPropertyChanged(BR.reps) } }
And a Lift with an observable list of sets:
class Lift : BaseObservable() { @get:Bindable var name: String = "" set(value) { field = value notifyPropertyChanged(BR.name) } val sets = ObservableArrayList<Set>() }
2. Minimizing UI Redraws for Partial Changes in Your Workout Fragment
Your current problem—full UI redraws when a single Set changes, or focus loss when splitting into multiple LiveData streams—can be solved by combining observable entities with targeted Room updates. Here's how:
Step 1: Map Room's Immutable Entities to Observable Classes
When you fetch the full Workout->Group->Lift->Set hierarchy from Room, convert those immutable data classes into your observable versions (like the Set and Lift examples above). This way, changes to individual properties trigger only the relevant UI updates, not a full LiveData emission.
In your repository, use Transformations.map to handle this conversion:
fun getWorkoutWithObservableHierarchy(workoutId: Long): LiveData<Workout> { return Transformations.map(workoutDao.getWorkoutWithHierarchy(workoutId)) { roomWorkout -> roomWorkout.toObservableWorkout() } } // Extension function to convert Room's entity to observable Workout private fun RoomWorkout.toObservableWorkout(): Workout { val workout = Workout().apply { date = this@toObservableWorkout.date } roomWorkout.groups.forEach { roomGroup -> val group = Group().apply { title = roomGroup.title } roomGroup.lifts.forEach { roomLift -> val lift = Lift().apply { name = roomLift.name } roomLift.sets.forEach { roomSet -> lift.sets.add(Set().apply { weight = roomSet.weight reps = roomSet.reps }) } group.lifts.add(lift) } workout.groups.add(group) } return workout }
Step 2: Bind to Observable Properties in Layouts
In your DataBinding layout, bind directly to the observable properties of each entity. For example, a TableRow for a Set would look like this:
<TableRow> <EditText android:text="@={String.valueOf(set.weight)}" android:inputType="numberDecimal" /> <EditText android:text="@={String.valueOf(set.reps)}" android:inputType="number" /> </TableRow>
When you update a Set's weight or reps, notifyPropertyChanged(BR.weight) tells DataBinding to refresh only that EditText—no full UI redraw.
Step 3: Handle Add/Remove Actions Without Losing Focus
Since you're using ObservableArrayList for Group.lifts and Lift.sets, adding a new Lift or Set will trigger DataBinding to insert the new UI element without rebuilding the entire TableLayout or LinearLayout. This preserves input focus because existing views aren't destroyed and recreated.
For example, adding a Set to a Lift in your ViewModel:
fun addSetToLift(lift: Lift) { lift.sets.add(Set().apply { weight = 0.0; reps = 0 }) // No need to update the parent LiveData—ObservableArrayList handles the UI update }
Step 4: Optimize Room Updates to Avoid Full Hierarchy Refreshes
Instead of updating the entire Workout entity when a Set changes, update only the Set entity in Room. Your repository can listen to individual Set updates and sync them to the observable Lift.sets list, instead of re-emitting the entire Workout LiveData. This avoids unnecessary LiveData emissions entirely.
You can use a MediatorLiveData in your repository to listen to both the initial Workout fetch and individual Set updates, then update the observable list in place.
Key Takeaways
- Avoid monolithic LiveData objects: Break your hierarchy into observable entities so changes only affect the relevant UI.
- Use observable lists:
ObservableArrayListensures add/remove actions only update the affected UI elements, preserving focus. - Map Room entities to observable classes: Separate immutable database models from mutable UI-bound models to control when updates propagate.
内容的提问来源于stack exchange,提问作者user1713450

