MarMass
MarMass

Reputation: 5885

How to reuse a Fragment and ViewModel with different Repository Implementation injected by Dagger2.2

I'm kinda new to Android development and I have been stuck finding a way to do this pattern using some android libraries like Dagger2, Fragments, and ViewModel.

I hope some of you can help me with this or stating what's the common way to do this on android.

I'm looking for something like this:

class FragmentPager: Fragment() {

@Inject
@Named("FullListFragment")
lateinit var listFragment: ListFragment

@Inject
@Named("FilteredListFragment")
lateinit var filteredListFragment: ListFragment

//Use fragments in the viewPager. 

}

What I am trying to do:

I have a Fragment that shows a list of elements. It also has a ViewModel responsible for updating the list among other things. The ViewModel gets the list from a Repository that queries a database. Pretty straightforward so far.

My use case is that my app shows the list of elements in a lot of different areas within the application but with different data. For example full list, filtered list...

My idea was to create the repository as an interface with a single method: fun getItems(): List<Item> and different instances for each data source. As a result, I have:

These elements will work together in an ideal world like this:

fun buildListFragment(repository: ListRepository) {
    val viewModel = ListViewModel(repository)
    val fragment = ListFragment(viewModel)
    return fragment
}

val fullListFragment = buildListFragment(FullListRepository())
val filteredListFragment = buildListFragment(FilterListRepository())
val joinedListFragment = buildListFragment(JoinedListRepository())

How can I do something like these using Dagger2 to inject dependencies, ViewModelFactory to create ViewModels, and Fragments.

Limitations I encounter:

class ListFragment: Fragment() {
   @Inject
   lateinit var viewModelFactory: ViewModelProvider.Factory

   override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        viewModel = ViewModelProvider(this, viewModelFactory).get(CardListViewModel::class.java)
    }

}

Questions:

Upvotes: 2

Views: 1761

Answers (2)

khunzohn
khunzohn

Reputation: 11

I would start off with redesigning the repository like this:

interface ListRepository {

    fun getFullList(): LiveData<List<Product>>

    fun getFilteredList(): LiveData<List<Product>>

    fun getJoinedList(): LiveData<List<Product>>
}

LiveData is used here assuming you'd be using room.

Then design my ViewModel to be able to get the desired list given the listType input.

class ListViewModel @Inject constructor(
    private val listRepository: ListRepository
) : ViewModel() {

    private val listType = MutableLiveData<String>()

    val productList = Transformations.switchMap(listType) {
        getList(it)
    }

    // call from fragment
    fun fetchList(listType: String) {
        this.listType.value = listType
    }

    private fun getList(listType: String): LiveData<List<Product>> {
        return when (listType) {
            "full" -> listRepository.getFullList()
            "filter" -> listRepository.getFilteredList()
            "joined" -> listRepository.getJoinedList()
            else -> throw IllegalArgumentException("Unknown List Type")
        }
    }
}

switchMap is used here to prevent the repository from returning a new LiveData instance every time we fetch the list from fragment. Then comes the fragment to wrap things up.

class ListFragment : Fragment() {

    lateinit var listType: String

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    private val viewModel: ListViewModel by viewModels { viewModelFactory }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        listType = arguments?.getString("listType")
            ?: throw java.lang.IllegalArgumentException("No listType args found")

        viewModel.fetchList(listType)

        viewModel.productList.observe(viewLifecycleOwner) { products ->
            TODO("render products on recycler view")
        }
    }
}

Only one fragment is used for the whole three list types cause I'm assuming those fragments are more or less identical in terms of Ui. List will be fetched and displayed according to the listType argument passing in.

fragmentManager.beginTransaction()
    .add(R.id.content,ListFragment().apply { 
        arguments = Bundle().apply {
            putString("listType","full")
        }
    })
    .commitNow()

Upvotes: 1

ror
ror

Reputation: 3500

I'm typically testing my answers before posting them but with mr. dagger that would be too much. That said, this is my educated guesswork:

Having single fragment that knows how to pull 3 different data sets (full, filtered, joined) means it needs to be parametrized somehow. I guess it can be done with named injection but I'd simply use MyFragment.newInstanceA(), MyFragment.newInstanceB() etc when needed.

Inside the fragment, likely using android injection as I think you're doing already, single free form factory is constructor-injected with all 3 repositories that implement single interface. That factory would wrap your implementation of ViewModelProvider.Factory and have a method, say create, with parameter the fragment was instantiated with.

Based on parameter value, factory would create and return properly parametrized implementation of ViewModelProvider.Factory. View model provider would then be able to get properly parametrized view model. I know this is not a lot of dagger but in theory it should just work :)

PS.: I'd not create 3 different repos if data is essentially coming from single storage apparently. May be this calling of different repo methods needs to be done under view model.

Upvotes: 2

Related Questions