Как повторно использовать Fragment и ViewModel с другой реализацией репозитория, внедренной Dagger2.2 - PullRequest
2 голосов
/ 21 февраля 2020

Я в некотором роде новичок в разработке 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 (interface)
  • 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 для создания ViewModel и фрагментов.

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

  • Мой ViewModel имеет параметры, поэтому их можно создавать только через ViewModelFactory.
  • ViewModel не может быть введен конструктором в Fragment и должен быть создан внутри с использованием viewModelFactory i. и создать. На данный момент невозможно сказать 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?
  • Как этого добиться? Любой совет?
  • Вы обычно используете другой шаблон, который может вписаться в тот же вариант использования?

Ответы [ 2 ]

2 голосов
/ 21 февраля 2020

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

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

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

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

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

1 голос
/ 21 февраля 2020

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

interface ListRepository {

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

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

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

Здесь используются LiveData, при условии, что вы будете использовать room.

Затем спроектируйте мой 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")
        }
    }
}

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

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