Как повторно использовать фрагмент и ViewModel с другой реализацией репозитория, внедренной Dagger2.2

Я новичок в разработке Android, и я застрял в поисках способа сделать этот шаблон, используя некоторые библиотеки Android, такие как Dagger2, Fragments и ViewModel.

Я надеюсь, что некоторые из вас помогут мне с этим или расскажут, как это обычно делается на Android.

Я ищу что-то вроде этого:

class FragmentPager: Fragment() {

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

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

//Use fragments in the viewPager. 

}

Что я пытаюсь сделать:

У меня есть фрагмент, который показывает список элементов. Он также имеет ViewModel, отвечающий, помимо прочего, за обновление списка. ViewModel получает список из репозитория, который запрашивает базу данных. Пока все довольно просто.

Мой вариант использования состоит в том, что мое приложение показывает список элементов во множестве разных областей внутри приложения, но с разными данными. Например, полный список, отфильтрованный список ...

Моя идея заключалась в том, чтобы создать репозиторий как интерфейс с одним методом: fun getItems(): List<Item> и разными экземплярами для каждого источника данных. В результате у меня:

  • ListFragment (класс, наследующий от Fragment)
  • ListViewModel (класс, наследующий от ViewModel)
  • ListRepository (интерфейс)
  • FullListRepository (класс, реализующий ListRepository) - ›Получает все элементы из БД
  • FilterListRepository (класс, реализующий ListRepository) - ›Получает элементы фильтра из БД.
  • JoinedListRepository (класс, реализующий ListRepository) - ›Получает элементы из БД, но путем объединения разных таблиц

Эти элементы будут работать вместе в таком идеальном мире:

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())

Как я могу сделать что-то подобное с помощью Dagger2 для внедрения зависимостей, ViewModelFactory для создания ViewModels и фрагментов.

Ограничения, с которыми я сталкиваюсь:

  • Моя ViewModel имеет параметры, поэтому может быть создана только через ViewModelFactory.
  • ViewModel не может быть введен конструктором во фрагмент и должен быть создан внутри с помощью viewModelFactory в onCreate. На данный момент невозможно указать Dagger2, какую реализацию ListRepository следует использовать для создания ViewModelFactory.
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)
    }

}
  • Я знаю, что вы можете использовать аннотацию @Named для запроса внедрения различных реализаций репозитория, но когда фабрика создается внутри тела фрагмента, уже слишком поздно запрашивать репозиторий @Named, потому что вы знаете, какой репозиторий вам нужен, прежде чем создавать фрагмент не после.

Вопросов:

  • Возможно ли это решение с использованием Dagger2 и ViewModels?
  • Как я могу этого добиться? Есть какой-нибудь совет?
  • Используете ли вы обычно другой шаблон, который подходит к тому же варианту использования?

person MarMass    schedule 21.02.2020    source источник
comment
Вы упомянули, что ваша модель представления параметризована. Является ли конкретный репозиторий только одним параметром или есть другие, и они являются динамическими (например, передаются с исходного экрана)?   -  person ror    schedule 21.02.2020
comment
Помимо репозитория, есть еще параметры. Немного статики и еще одного динамического.   -  person MarMass    schedule 21.02.2020


Ответы (2)


Обычно я проверяю свои ответы перед тем, как публиковать их, но с мистером. кинжал, это было бы слишком. Тем не менее, это мое обоснованное предположение:

Наличие одного фрагмента, который умеет извлекать 3 разных набора данных (полные, отфильтрованные, объединенные), означает, что его нужно как-то параметризовать. Я предполагаю, что это можно сделать с помощью именованной инъекции, но я бы просто использовал MyFragment.newInstanceA(), MyFragment.newInstanceB() и т. Д., Когда это необходимо.

Внутри фрагмента, вероятно, с использованием Android-инъекции, как я думаю, вы уже делаете, одна фабрика свободной формы вводится конструктором со всеми 3 репозиториями, реализующими единый интерфейс. Эта фабрика обернет вашу реализацию ViewModelProvider.Factory и будет иметь метод, скажем create, с параметром, с которым был создан фрагмент.

На основе значения параметра factory создаст и вернет правильно параметризованную реализацию ViewModelProvider.Factory. После этого поставщик модели представления сможет get правильно параметризовать модель представления. Я знаю, что это не так уж и много кинжала, но теоретически он должен работать :)

PS: Я бы не стал создавать 3 разных репозитория, если данные, по-видимому, поступают из одного хранилища. Возможно, этот вызов различных методов репо должен выполняться в рамках модели представления.

person ror    schedule 21.02.2020

Я бы начал с перепроектирования репозитория следующим образом:

interface ListRepository {

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

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

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

LiveData используется здесь при условии, что вы используете комнату.

Затем спроектируйте мою ViewModel, чтобы иметь возможность получить желаемый список с учетом ввода listType.

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 используется здесь для предотвращения возврата репозиторием нового экземпляра LiveData каждый раз, когда мы получаем список из фрагмента. Затем идет фрагмент, чтобы подвести итог.

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")
        }
    }
}

Для всех трех типов списков используется только один фрагмент, потому что я предполагаю, что эти фрагменты более или менее идентичны с точки зрения пользовательского интерфейса. Список будет извлечен и отображен в соответствии с переданным аргументом listType.

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