0

I have a fragment which shows images and possible sub-albums of an album. The fragment has a single argument which is the ID of the album to show. The ID is a POD long. For navigation incl. proper navigation history and passing arguments to the fragment, I use the navigation graph and the SafeArgs library.

Clicking on a sub-album navigates to the same fragment with the ID of sub-album to show. The loading of images and sub-albums is done in a view model.

Question: How do I pass the ID of the album as an argument of the constructor to my view model.

Here are some code snippets which might help to understand the problem

The fragment

public class AlbumFragment extends Fragment implements Observer<List<AlbumEntry>> {
  private AlbumViewModel viewModel;
  private long albumID; /*< The ID of the album which is shown by this instance of the fragment */

  public static AlbumFragment newInstance( final long albumID ) {
    AlbumFragment fragment = new AlbumFragment();
    // The class `AlbumFragmentArgs` is auto-generated by the SafeArgs library
    fragment.setArguments(
      (new AlbumFragmentArgs.Builder()).setAlbumID( albumID ).build().toBundle()
    );
    return fragment;
  }

  @Override
  public void onCreate( Bundle savedInstanceState ) {
    super.onCreate( savedInstanceState );
    albumID = AlbumFragmentArgs.fromBundle( getArguments() ).getAlbumID();
    // TODO: We somehow need to pass the ID of the album to the view model
    viewModel = new ViewModelProvider( this ).get( AlbumViewModel.class );
  }

  @Override
  public View onCreateView( LayoutInflater inflater, ViewGroup container,
                            Bundle savedInstanceState ) {
    // Inflates the view from XML layout file, skipped here
    return view;
  }

  @Override
  public void onViewCreated( @NonNull final View view, final Bundle savedInstanceState ) {
    // PROBLEM: The list of album entries actually depends on the current album
    // The ID of the current album should have been passed when then model was created
    LiveData<? extends List<AlbumEntry>> liveAlbumEntries = viewModel.getAlbumEntries();
    liveAlbumEntries.observe( this.getViewLifecycleOwner(), this );
  }

  // Methods for observing live data skipped here

  protected void onClick( final View view ) {
    // The code how the ID of the target album is obtained from the
    // clicked view is left out here
    // The variable is targetAlbumID
    NavGraphDirections.ActionToAlbumFragment action = NavGraphDirections.actionToAlbumFragment();
    action.setAlbumID( targetAlbumID );
    Navigation.findNavController( this.getView() ).navigate( action );
  }
}

The view model

public class AlbumViewModel extends AndroidViewModel {
  
  private long albumID; /*< The ID of the album which is shown by this instance of the fragment */

  public AlbumViewModel( Application application /*, final long albumID */ ) {
    super( application );
    // TODO: How to write a constructor which gets an Application object and an albumID?
    /* this.albumID = albumID */
    
    // Remaining code, esp. initialization of the repository, is left out
  }

  public LiveData<? extends List<AlbumEntry>> getAlbumEntries() {
    // Code left out, but depends on `this.albumID`
  }
}

Some final remarks

Of course, it might be possible to not pass the ID of the current album to the view model, but always pass the album ID explicitly as an additional argument to each method of the view model, for example LiveData<? extends List<AlbumEntry>> liveAlbumEntries = viewModel.getAlbumEntries( albumID ).

However, having one instance of the view model for each album ID would be much easier especially with respect to caching. If a single instance of the view model is bound to a particular album ID, the view model only needs to deal with the album entries of this album. If the corresponding fragment for the same album ID ends its life-cycle permanently, the corresponding view model will be destroyed and hence the album entries are removed from memory. If the fragment is only re-created (due to configuration changes) the corresponding view model is kept in memory and so are the album entries.

Contrary, if I only had one application-wide global instance of AlbumViewModel for all albums and all instances of the fragment and if the album ID is passed explicitly to each method of AlbumViewModel I would never know, when a particular fragment is destroyed and when the album entries can be removed from memory.

user2690527
  • 1,729
  • 1
  • 22
  • 38
  • Does this answer your question? [How to use a ViewModelProvider.Factory when extends from AndroidViewModel](https://stackoverflow.com/questions/51829280/how-to-use-a-viewmodelprovider-factory-when-extends-from-androidviewmodel) – denvercoder9 Mar 29 '21 at 10:45
  • The title for the question I linked may be different but it has the same issue you're having. The solution is to create a custom viewmodel factory for your viewmodel and pass in the album id to the constructor. https://medium.com/koderlabs/viewmodel-with-viewmodelprovider-factory-the-creator-of-viewmodel-8fabfec1aa4f – denvercoder9 Mar 29 '21 at 10:47

1 Answers1

0

You should create a ViewModelFactory, like:

public class AlbumViewModelFactory implements ViewModelProvider.Factory {
    private Application application;
    private long id;


    public AlbumViewModelFactory(Application application, long id) {
        this.application = application;
        this.id = id;
    }


    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        return (T) new AlbumViewModel(application, id);
    }
}
Róbert Nagy
  • 6,720
  • 26
  • 45