Search code examples
androidandroid-architecture-components

Can an Android Architecture Components ViewModel compose an object from multiple LiveData returning models?


I've been unable to create an Android Architecuture Components ViewModel that composes multiple LiveData models into one LiveData class for my Fragment to observe. I want to hide the model details from the Fragment and respond to external data changes through the individual models.

The problem is I need the ViewModel to observe the model changes but ViewModel is not a LifecycleOwner so it can't observe. Since I don't want to pass the LiveData objects through to the UI, I'm stuck.

Is this possible? Do I need to abandon LiveData for my models and resort to a different observation pattern / tool?

Edit: Pseudocode added. My actual classes are much more complex and lengthy. I hope my intent is understandable.

// OneDataModel.kt
class oneDataModel {
    val oneDataElement = ""
}

// AnotherDataModel.kt
class anotherDataModel {
    val anotherDataElement = 19
}

// OneDataRepository.kt
class OneDataRepository {
    val oneDataSet = MutableLiveData<MutableList<oneDataModel>>()

    private val dataListener = object: ChildEventListener {
        override fun onChildAdded(snapshot: DataSnapshot, p1: String?) {
            val newChild = snapshot.getValue(oneDataModel::class.java)
            if (newChild != null) {
                oneDataSet.value?.add(newChild)
            }
        }
    }

    init {
        oneDataSet.value = mutableListOf<oneDataModel>()
        OneNetworkDataTable.addListener(dataListener)
    }
}

// AnotherDataRepository.kt
class AnotherDataRepository {
    var anotherDataSet = MutableLiveData<MutableList<anotherDataModel>>()

    private val dataListener = object: ChildEventListener {
        override fun onChildAdded(snapshot: DataSnapshot, p1: String?) {
            val newChild = snapshot.getValue(anotherDataModel::class.java)
            if (newChild != null) {
                anotherDataSet.value?.add(newChild)
            }
        }
    }

    init {
        anotherDataSet.value = mutableListOf<anotherDataModel>()
        AnotherNetworkDataTable.addListener(dataListener)
    }
}

// ComposedViewModel.kt
class ComposedViewModel: ViewModel() {
    class ComposedItem {
        var dataName: String = ""   // From OneDataRepository items
        var dataValue: Int = -1     // From AnotherDataRepository items
    }
    var publishedDataSet = MutableLiveData<MutableList<ComposedItem>>()

    //***
    //*** WHAT GOES HERE? HOW DO I LISTEN TO EACH OF THE DATA REPOSITORIES AND BUILD UP COMPOSED
    //*** ITEMS FOR THE UI?
    //***
}

// MyFragment.kt
class MyFragment : Fragment() {
    private val composedViewModel: ComposedViewModel by lazy { ViewModelProviders.of(activity).get(ComposedViewModel::class.java) }

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

        recyclerView.adapter = UIAdapter

        composedViewModel.publishedDataSet.observe(this, Observer {
            UIAdapter.notifyDataSetChanged()
        })

        return view
    }
}

Solution

  • Hope my work satisfy your question.

    I have made a sample project before and now integrate another fake livedata into this view model. Difference of the file

    Use MediatorLiveData class to wrap multi data source to one.

    FeedEntryListViewModel.java

    public class FeedEntryListViewModel extends ViewModel {
    
      //list all your live data here
      private LiveData<List<FeedEntry>> feedEntries = new MutableLiveData<>();
      private MutableLiveData<String> userId = new MutableLiveData<>();
      //show your composite model here
      private MediatorLiveData<CompositeModel> compositeModelLiveData;
    
      //list all your repository
      private FeedEntryRepository feedEntryDBRepository;
    
      /*
      The complete model to show the data
       */
      public static class CompositeModel {
    
        String userId = "SystemId";
        private List<FeedEntry> feedEntries = new ArrayList<>();
    
    
        public String getUserId() {
          return userId;
        }
    
        public void setUserId(String userId) {
          this.userId = userId;
        }
    
        public List<FeedEntry> getFeedEntries() {
          return feedEntries;
        }
    
        public void setFeedEntries(
            List<FeedEntry> feedEntries) {
          this.feedEntries = feedEntries;
        }
    
    
      }
    
      public FeedEntryListViewModel(
          FeedEntryRepository feedEntryDBRepository) {
        this.feedEntryDBRepository = feedEntryDBRepository;
        this.feedEntries = feedEntryDBRepository.getAll();
        this.compositeModelLiveData = new MediatorLiveData<>();
        this.compositeModelLiveData.addSource(feedEntries, data ->
        {
          CompositeModel compositeModel = compositeModelLiveData.getValue();
          compositeModel.setFeedEntries(data);
          compositeModelLiveData.postValue(compositeModel);
        });
        this.compositeModelLiveData.addSource(userId, data -> {
          CompositeModel compositeModel = compositeModelLiveData.getValue();
          compositeModel.setUserId(data);
          compositeModelLiveData.postValue(compositeModel);
        });
        //initialize the composite model to avoid NullPointerException
        this.compositeModelLiveData.postValue(new CompositeModel());
      }
    
      public void loadUserId(String userId) {
        this.userId.setValue(userId);
      }
    
      public LiveData<List<FeedEntry>> getFeedEntrys() {
        return feedEntryDBRepository.getAll();
      }
    
      public LiveData<CompositeModel> getCompositeEntrys() {
        return compositeModelLiveData;
      }
    
      public LiveData<List<FeedEntry>> insert(FeedEntry... feedEntries) {
        feedEntryDBRepository.insertAll(feedEntries);
        return feedEntryDBRepository.getAll();
      }
    
      public void delete(FeedEntry feedEntry) {
        feedEntryDBRepository.delete(feedEntry);
      }
    
    
      public int update(FeedEntry feedEntry) {
        return feedEntryDBRepository.update(feedEntry);
      }
    
    }
    

    In the Activity, you still can get the composite with the statement

    viewModel.getCompositeEntrys().observe(this, entries -> {...});
    

    View Model Constructor add the live data and bind to the composite live data

     public FeedEntryListViewModel(
          FeedEntryRepository feedEntryDBRepository) {
        this.feedEntryDBRepository = feedEntryDBRepository;
        this.feedEntries = feedEntryDBRepository.getAll();
        this.compositeModelLiveData = new MediatorLiveData<>();
        this.compositeModelLiveData.addSource(feedEntries, data ->
        {
          CompositeModel compositeModel = compositeModelLiveData.getValue();
          compositeModel.setFeedEntries(data);
          compositeModelLiveData.postValue(compositeModel);
        });
        this.compositeModelLiveData.addSource(userId, data -> {
          CompositeModel compositeModel = compositeModelLiveData.getValue();
          compositeModel.setUserId(data);
          compositeModelLiveData.postValue(compositeModel);
        });
        //initialize the composite model to avoid NullPointerException
        this.compositeModelLiveData.postValue(new CompositeModel());
      }