I have a problem that happened to me during creating internal live data observer in My view model and connecting it with data binding. Right now part of my code look's like this:
Fragment:
class SearchFragment : Fragment(), Injectable {
#Inject lateinit var viewModelFactory: ViewModelProvider.Factory
#Inject lateinit var searchViewModel: SearchViewModel
private lateinit var disposable: Disposable
private var searchView: SearchView? = null
private var adapter: SearchCityListAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: SearchFragmentBinding = DataBindingUtil.inflate(inflater, R.layout.search_fragment, container, false)
binding.apply { viewModel = searchViewModel
setLifecycleOwner(this#SearchFragment)}
setHasOptionsMenu(true)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
searchViewModel = ViewModelProviders.of(this, viewModelFactory).get(SearchViewModel::class.java)
submitViewModel()
}
private fun submitViewModel(){
viewModel.searchCityList.observe(this, Observer {it ->
if (adapter == null){
it?.let { adapter = SearchCityListAdapter(it)}.also {createAdapter()}
} else {
it?.let { adapter!!.updateList(it) }.also { adapter!!.notifyDataSetChanged() }
}
})
viewModel.responseStatus.observe(this, Observer {
it?.let { it ->
when(it.status){
Status.SUCCESS -> progressBar.visibility = View.INVISIBLE
Status.FAILURE -> progressBar.visibility = View.INVISIBLE
Status.ERROR -> progressBar.visibility = View.INVISIBLE
Status.LOADING ->progressBar.visibility = View.VISIBLE
} }
})
}
ViewModel:
class SearchViewModel #Inject constructor(val haloApplication: HaloApplication): ViewModel() {
val progressBarVisibility: MediatorLiveData<Boolean> = MediatorLiveData()
private val responseStatus: MutableLiveData<Response> = MutableLiveData()
val searchCityList: MutableLiveData<SearchCityList> = MutableLiveData()
fun searchCity(cityName: String){
haloApplication.networkRepository.fetchCityList(cityName, responseStatus, searchCityList)
}
init {
progressBarVisibility.value = false
progressBarVisibility.addSource(responseStatus){
it?.let { when(it.status) {
Status.FAILURE -> progressBarVisibility.value = false
Status.ERROR -> progressBarVisibility.value = false
Status.SUCCESS -> progressBarVisibility.value = false
Status.LOADING -> progressBarVisibility.value = true
} }
}
}
}
XML layout file:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.mbojec.halo.viewmodel.SearchViewModel"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.SearchFragment">
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/progressBar"
android:layout_gravity="center"
android:visibility="#{viewModel.progressBarVisibility, default=invisible}"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/searchCityListRecycleView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</layout>
DataBinder file:
import android.view.View
import androidx.databinding.BindingAdapter
#BindingAdapter("android:visibility")
fun setVisibility(view: View, value: Boolean) {
view.visibility = if (value) View.VISIBLE else View.INVISIBLE
}
For some unknown reason's the Fragment part works fine but the viewmodel part never triggers when there are changes in the responseStatus and the connection via data binding is useless. This isn't my first time with creating mediator's in view model or tranformation's but it's the first time I have such problem. I tried dozen of scenario's and each time the result is the same. First I ws blaming the new AndroidX library but their Sunflower Sample from github works also fine so I have no clue on this moment.
MediatorLiveData only starts observing sources added via addSource when it has something observing the MediatorLiveData itself. Since nothing is observing progressBarVisibility, you won't get any callbacks.
You have to observe the progressBarVisibility to receive onChanged callbacks. (Every observable needs an observe to work with)
Let`s make some changes:
Your progressBarVisibility does not need to be a MediatorLiveData;
You dont need to make the responseStatuspublic and observe it;
Let your progressBarVisibility add responseStatus as source.
internal val progressBarVisibility: LiveData<Boolean> = Transformations.map(responseStatus) {
when (it.status) {
Status.LOADING -> true
Status.ERROR, Status.SUCCESS, Status.FAILURE -> false
}
}
Now, you can observe the progressBarVisibilityand receive callbacks.
Related
I am learning implementation of Rx Java in Kotlin via MVVM architecture. I have a simple app that takes input from the user regarding the name of a dish like 'pasta' , 'pizza' etc and subsequently on press of a button , I call Spoonacular Api via Rx java to display the name of the dish returned by the API call , in the logcat.
Now every time I press the button, the number of call increases by two and I receive multiple names every time. Can someone please guide me in this.
Spoonacular Api Data Class ( It is a very big class hence I have just included the important part) ->
object FilteredDishes {
data class DishesFromAPI(
val recipes: List<Recipe>
)
data class Recipe(
val aggregateLikes: Int,
val analyzedInstructions: List<AnalyzedInstruction>,
val cheap: Boolean,
.....
Interface Code ->
interface DishInterface {
#GET(Constants.spoonacularEndPoint)
fun getDishes(
#Query(Constants.apiKey) apiKey : String,
#Query(Constants.limitLicense) limitLicense : Boolean,
#Query(Constants.tags) tags : String,
#Query(Constants.number) number : Int
) : Single<FilteredDishes.DishesFromAPI>
}
Retrofit builder Code ->
class DishApiService {
private val api=Retrofit.Builder().baseUrl(Constants.spoonacularBaseURL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
.create(DishInterface::class.java)
fun getDishFromInternet(type:String) : Single<FilteredDishes.DishesFromAPI> {
return api.getDishes(Constants.spoonacularAPiKeyValue,false,type,1)
}
}
View Model code ->
class DishApiViewModel : ViewModel(){
private val dishApiService : DishApiService = DishApiService()
private val compositeDisposable : CompositeDisposable = CompositeDisposable()
val loadDish = MutableLiveData<Boolean>()
val dishResponse = MutableLiveData<FilteredDishes.DishesFromAPI>()
val dishLoadingError = MutableLiveData<Boolean>()
fun getRecipesFromAPI(filter:String){
loadDish.value=true
compositeDisposable.add(
dishApiService.getDishFromInternet(filter)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object : DisposableSingleObserver<FilteredDishes.DishesFromAPI>(){
override fun onSuccess(value: FilteredDishes.DishesFromAPI?) {
loadDish.value=false
dishResponse.value=value!!
dishLoadingError.value=false
}
override fun onError(e: Throwable?) {
loadDish.value=false
dishLoadingError.value=true
e!!.printStackTrace()
}
})
)
}
}
XML File ->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.SearchFragment">
<EditText
android:id="#+id/et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="#dimen/_16sdp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:autofillHints="" />
<Button
android:id="#+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/clcick"
android:layout_marginTop="#dimen/_200sdp"
app:layout_constraintTop_toBottomOf="#id/et"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment Code ->
class SearchFragment : Fragment() {
private var mBinding: FragmentSearchBinding? = null
private lateinit var mDishApiViewModel : DishApiViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
mBinding = FragmentSearchBinding.inflate(inflater, container, false)
return mBinding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDishApiViewModel= ViewModelProvider(this)[DishApiViewModel::class.java]
mBinding!!.btn.setOnClickListener{
if(mBinding!!.et.text.isEmpty()){
Toast.makeText(requireContext(), "Please enter dish type", Toast.LENGTH_SHORT).show()
}else{
apiCall(mBinding!!.et.text.toString())
}
}
}
private fun apiCall(filter:String){
mDishApiViewModel.getRecipesFromAPI(filter)
dishViewModelObserver()
}
private fun dishViewModelObserver(){
mDishApiViewModel.dishResponse.observe(viewLifecycleOwner,
{
if(it!=null){
if(it.recipes.isNotEmpty()){
Log.e("Response", it.recipes[0].title)
}else{
Toast.makeText(requireContext(), "error", Toast.LENGTH_SHORT).show()
}
}
})
mDishApiViewModel.dishLoadingError.observe(viewLifecycleOwner,
{
if(it!=null){
Log.e("loadingError","$it")
}
}
)
mDishApiViewModel.loadDish.observe(viewLifecycleOwner,
{
if(it!=null){
Log.e("load","$it")
}
})
}
override fun onDestroyView() {
super.onDestroyView()
mBinding = null
}
}
This is because you call dishViewModelObserver and every time you add yet another observer to the observers list of your LiveData.
You need to move that function from apiCall to onViewCreated:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mDishApiViewModel = ViewModelProvider(this)[DishApiViewModel::class.java]
dishViewModelObserver()
mBinding!!.btn.setOnClickListener{
if (mBinding!!.et.text.isEmpty()) {
Toast.makeText(requireContext(), "Please enter dish type", Toast.LENGTH_SHORT).show()
} else {
apiCall(mBinding!!.et.text.toString())
}
}
}
private fun apiCall(filter: String) {
mDishApiViewModel.getRecipesFromAPI(filter)
}
Tip: you also need to get rid of he habit of using m prefixes, this convention was added to Java to differentiate property types, but in Kotlin getter and setter JVM functions are generated for your properties.
I.e. in Java
void setThing(thing: Thing) {
mThing = thing
}
made some sense over
void setThing(thing: Thing) {
this.Thing = thing
}
I am trying to transfer a value from one LiveData (Repository.getMovieList(editTextContent.value.toString()).value) to another LiveData (this.movieList.postValue) using postValue().
I am observing the movieList and want to change it's value from the Repo depending on different buttons that were clicked but I when it runs, it only gets the null value and doesn't wait till the Repo's LiveData gets their value.
Fragment xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.example.movieapp.ui.search.SearchMovieFragmentViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.search.SearchMovieFragment">
<EditText
android:id="#+id/search_movie_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={viewmodel.editTextContent}"
android:inputType="text"
android:hint="Movie Name" />
<Button
android:id="#+id/search_fragment_search_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Search"
android:onClick="#{() -> viewmodel.getMovieSearchList()}"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/search_movie_fragment_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</layout>
SearchMovieFragment
class SearchMovieFragment : Fragment(), MovieSearchItemViewModel {
companion object {
fun newInstance() = SearchMovieFragment()
}
private lateinit var searchMovieFragmentViewModel: SearchMovieFragmentViewModel
private lateinit var binding: SearchMovieFragmentBinding
private lateinit var movieRecyclerView: RecyclerView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.search_movie_fragment, container, false)
searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
binding.lifecycleOwner = this
binding.viewmodel = searchMovieFragmentViewModel
setUpRecyclerView(container!!.context)
return binding.root
}
private fun setUpRecyclerView(context: Context) {
movieRecyclerView = binding.searchMovieFragmentRecyclerView.apply {
this.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
val adapter = MovieListAdapter()
adapter.setCallback(this)
binding.searchMovieFragmentRecyclerView.adapter = adapter
searchMovieFragmentViewModel.getMovieListLiveData().observe(viewLifecycleOwner, Observer {movieList ->
adapter.submitList(movieList)
})
}
}
SearchMovieViewModel
class SearchMovieFragmentViewModel : ViewModel() {
val editTextContent = MutableLiveData<String>()
var movieList: MutableLiveData<List<Movie>> = MutableLiveData()
fun getMovieSearchList(){
this.movieList.postValue(Repository.getMovieList(editTextContent.value.toString()).value)
}
fun getTrendingMovies() {
movieList.postValue(Repository.getTrendingMovies().value)
}
fun getMovieDetail(movieId: String): MutableLiveData<Movie> {
return Repository.getMovieDetail(movieId)
}
fun getMovieListLiveData() : LiveData<List<Movie>> {
return movieList
}
private fun getMovieList(movieSearch: String): MutableLiveData<List<Movie>> = Repository.getMovieList(movieSearch)
}
I think you are implementing it the wrong way, Instead, using the MediatorLiveData will be a good and practical solution as it allows you to observe multiple LiveData objects and select between them based on your preferences (a specific action for example).
This is an example of how to implement it in your case
val editTextContent = MutableLiveData<String>()
val finalList = MediatorLiveData<List<Movie>>()
// Here.. Define all of your LiveData objects
private val movieList = repository.getMovieList(editTextContent.value.toString())
private val trendingMovies = repository.getTrendingMovies()
private val movieDetail = repository.getMovieDetail()
fun setSelection(selection: String) {
finalList.addSource(movieList) { result ->
if (selection == "movieList") {
result?.let { finalList.value = it }
}
}
finalList.addSource(trendingMovies) { result ->
if (selection == "trendingMovies") {
result?.let { finalList.value = it }
}
}
finalList.addSource(movieDetail) { result ->
if (selection == "movieDetail") {
result?.let { finalList.value = it }
}
}
}
So what you have to do is to only observe the MediatorLiveData and then call the setSelection function and send the correspondent selection action to it as a parameter and it will switch the observation to another LiveData
EDIT - I tried to set android:focusable and android:clickable attributes to true in the XML, but it didn't changed anything. Still looking!
I can't understand why my onClick handler isn't working on this particular case.
I want to make a CardView clickable, using databinding. I've seen a lot of code for onClickListener, but i decided to use this pattern, as described in the Android doc.
I have a class, named Recipe(), which contains 3 elements : an ID, a Title and an URL. The ultimate goal of this click handler is to navigate to a new fragment, passing the ID as a parameter to display new elements. I'll later get the ID in the Fragment using observer.
In this example, I already use data provided by the ViewModel in the XML (recipe.image and recipe.title), and it works just fine: data is correctly binded and displayed.
However, clicking on the CardView don't result in anything: as the Log isn't display, I suppose clicking don't trigger the onClick event.
You'll find element from the ViewModel and XML below. Thanks in advance!
class DefaultRecipeListViewModel : ViewModel() {
private val _recipeList = MutableLiveData<List<Recipe>>()
val recipeList: LiveData<List<Recipe>>
get() = _recipeList
//Defining coroutine
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main )
//Livedata observed in the Fragment
private var _navigateToRecipe = MutableLiveData<Int>()
val navigateToRecipe: LiveData<Int>
get() = _navigateToRecipe
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
init {
getRecipesForNewbie()
}
fun getRecipesForNewbie() {
coroutineScope.launch {
var getRecipes = service.getRecipe().await()
try {
_recipeList.value = getRecipes.results
} catch (e: Exception) {
Log.i("ViewModel","Error: $e")
}
}
}
fun onRecipeClicked(id: Int) {
_showSnackbarEvent.value = true
_navigateToRecipe.value = id
Log.i("ViewModel", "Item clicked, id: $id")
}
fun doneNavigating(){
Log.i("ViewModel", "done navigating, navigateToRecipe set to -1")
_navigateToRecipe.value = -1
}
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Here's the XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="recipe"
type="com.example.recipesfornewbies.recipes.Recipe" />
<variable
name="viewModel"
type="com.example.recipesfornewbies.defaultrecipelist.DefaultRecipeListViewModel" />
</data>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<androidx.cardview.widget.CardView
android:id="#+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
app:cardCornerRadius="10dp"
app:cardElevation="6dp"
android:onClick="#{()-> viewModel.onRecipeClicked(recipe.id)}">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/recipe_image"
android:layout_width="match_parent"
android:layout_height="100dp"
android:contentDescription="#{recipe.title}"
app:imageFromUrl="#{recipe.image}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="#+id/recipe_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="#{recipe.title}"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="#id/recipe_image"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</layout>
Fragment code:
class DefaultRecipeListFragment: Fragment(){
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentDefaultRecipeListBinding.inflate(inflater)
val viewModel: DefaultRecipeListViewModel by lazy {
ViewModelProviders.of(this).get(DefaultRecipeListViewModel::class.java)
}
binding.viewModel = viewModel
// Allows Data Binding to Observe LiveData with the lifecycle of this Fragment
binding.setLifecycleOwner(this)
//Creating the RecyclerView
val manager = LinearLayoutManager(activity)
binding.recyclerRecipeList.layoutManager = manager
binding.recyclerRecipeList.adapter = RecipeListAdapter()
viewModel.navigateToRecipe.observe(this, Observer{id ->
if (id != -1){
Log.i("Fragment","Navigate to ${id}")
}
viewModel.doneNavigating()
})
viewModel.showSnackBarEvent.observe(this, Observer {
if (it == true) {
Snackbar.make(
activity!!.findViewById(android.R.id.content),
"Clicked!",
Snackbar.LENGTH_SHORT
).show()
viewModel.doneShowingSnackbar()
}
})
return binding.root
}
}
ViewHolder class, used in the RecyclerView Adapter
class RecipeViewHolder(private var binding: RecipeViewBinding):
RecyclerView.ViewHolder(binding.root) {
fun bind(Recipe: Recipe) {
val imageURI = "https://spoonacular.com/recipeImages/"
Recipe.image = imageURI + Recipe.image
binding.recipe = Recipe
// Forces the data binding to execute immediately,to correctly size RecyclerVieW
binding.executePendingBindings()
}
I finally decided to use this solution on my code, which works pretty well.
I stopped trying to access the function in my ViewModel directly from the XML: I actually listen for event on the Fragment, even if I find the solution to be less pretty that the one I wanted.
In the Fragment, this is how I handle the click:
binding.recyclerRecipeList.addOnItemTouchListener(RecyclerItemClickListener(this.context!!,
binding.recyclerRecipeList, object : RecyclerItemClickListener.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
viewModel.recipeList.value?.let {
val ident = it[position].id
findNavController().navigate(
DefaultRecipeListFragmentDirections.actionDefaultRecipeListFragmentToDetailedRecipeFragment(ident))
Log.i("Fragment", "id: $ident")
}
}
override fun onItemLongClick(view: View?, position: Int) {
TODO("do nothing")
}
}))
Still haven't understood why my first solution didn't work.
I am using Bottom Navigation with Navigation Architecture Component. When the user navigates from one item to another(via Bottom navigation) and back again view model call repository function to fetch data again. So if the user goes back and forth 10 times the same data will be fetched 10 times. How to avoid re-fetching when the fragment is recreated data is already there?.
Fragment
class HomeFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var productsViewModel: ProductsViewModel
private lateinit var productsAdapter: ProductsAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
initViewModel()
initAdapters()
initLayouts()
getData()
}
private fun initViewModel() {
(activity!!.application as App).component.inject(this)
productsViewModel = activity?.run {
ViewModelProviders.of(this, viewModelFactory).get(ProductsViewModel::class.java)
}!!
}
private fun initAdapters() {
productsAdapter = ProductsAdapter(this.context!!, From.HOME_FRAGMENT)
}
private fun initLayouts() {
productsRecyclerView.layoutManager = LinearLayoutManager(this.activity)
productsRecyclerView.adapter = productsAdapter
}
private fun getData() {
val productsFilters = ProductsFilters.builder().sortBy(SortProductsBy.NEWEST).build()
//Products filters
productsViewModel.setInput(productsFilters, 2)
//Observing products data
productsViewModel.products.observe(viewLifecycleOwner, Observer {
it.products()?.let { products -> productsAdapter.setData(products) }
})
//Observing loading
productsViewModel.networkState.observe(viewLifecycleOwner, Observer {
//Todo showing progress bar
})
}
}
ViewModel
class ProductsViewModel
#Inject constructor(private val repository: ProductsRepository) : ViewModel() {
private val _input = MutableLiveData<PInput>()
fun setInput(filters: ProductsFilters, limit: Int) {
_input.value = PInput(filters, limit)
}
private val getProducts = map(_input) {
repository.getProducts(it.filters, it.limit)
}
val products = switchMap(getProducts) { it.data }
val networkState = switchMap(getProducts) { it.networkState }
}
data class PInput(val filters: ProductsFilters, val limit: Int)
Repository
#Singleton
class ProductsRepository #Inject constructor(private val api: ApolloClient) {
val networkState = MutableLiveData<NetworkState>()
fun getProducts(filters: ProductsFilters, limit: Int): ApiResponse<ProductsQuery.Data> {
val products = MutableLiveData<ProductsQuery.Data>()
networkState.postValue(NetworkState.LOADING)
val request = api.query(ProductsQuery
.builder()
.filters(filters)
.limit(limit)
.build())
request.enqueue(object : ApolloCall.Callback<ProductsQuery.Data>() {
override fun onFailure(e: ApolloException) {
networkState.postValue(NetworkState.error(e.localizedMessage))
}
override fun onResponse(response: Response<ProductsQuery.Data>) = when {
response.hasErrors() -> networkState.postValue(NetworkState.error(response.errors()[0].message()))
else -> {
networkState.postValue(NetworkState.LOADED)
products.postValue(response.data())
}
}
})
return ApiResponse(data = products, networkState = networkState)
}
}
Navigation main.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation.xml"
app:startDestination="#id/home">
<fragment
android:id="#+id/home"
android:name="com.nux.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home"/>
<fragment
android:id="#+id/search"
android:name="com.nux.ui.search.SearchFragment"
android:label="#string/title_search"
tools:layout="#layout/fragment_search" />
<fragment
android:id="#+id/my_profile"
android:name="com.nux.ui.user.MyProfileFragment"
android:label="#string/title_profile"
tools:layout="#layout/fragment_profile" />
</navigation>
ViewModelFactory
#Singleton
class ViewModelFactory #Inject
constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = viewModels[modelClass]
?: viewModels.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
?: throw IllegalArgumentException("unknown model class $modelClass")
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
One simple solution would be to change the ViewModelProvider owner from this to requireActivity() in this line of code:
ViewModelProviders.of(this, viewModelFactory).get(ProductsViewModel::class.java)
Therefore, as the activity is the owner of the viewmodel and the lifcycle of viewmodel attached to the activity not to the fragment, navigating between fragments within the activity won't recreated the viewmodel.
In onActivityCreated(), you are calling getData(). In there, you have:
productsViewModel.setInput(productsFilters, 2)
This, in turn, changes the value of _input in your ProductsViewModel. And, every time that _input changes, the getProducts lambda expression will be evaluated, calling your repository.
So, every onActivityCreated() call triggers a call to your repository.
I do not know enough about your app to tell you what you need to change. Here are some possibilities:
Switch from onActivityCreated() to other lifecycle methods. initViewModel() could be called in onCreate(), while the rest should be in onViewCreated().
Reconsider your getData() implementation. Do you really need to call setInput() every time we navigate to this fragment? Or, should that be part of initViewModel() and done once in onCreate()? Or, since productsFilters does not seem to be tied to the fragment at all, should productsFilters and the setInput() call be part of the init block of ProductsViewModel, so it only happens once?
When you select other pages via bottom navigation and come back, fragment destroy and recreate. So the onCreate, onViewCreated and onActivityCreate
will run again. But viewModel is still alive.
So you can call your function (getProducts) inside the "init" in viewModel to run it once.
init {
getProducts()
}
define your ProductsViewModel by static in mainActivity and initialize in onCreate method.
Now just use it this way in fragment:
MainActivity.productsViewModel.products.observe(viewLifecycleOwner, Observer {
it.products()?.let { products -> productsAdapter.setData(products) }
})
I have this code in ViewModel class to display a progressBar when data is loading :
class DetailViewModel(
context: Application,
private val schedulerProvider: BaseSchedulerProvider,
private val dataSource: RemoteDataSource
) : AndroidViewModel(context) {
val isFeedsLoading = ObservableBoolean(false)
fun showFeeds(goal: SavingsGoal): Disposable? {
EspressoIdlingResource.increment() // App is busy until further notice
isFeedsLoading.set(true)
return dataSource.getFeeds(goal.id)
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement()
}
isFeedsLoading.set(false)
}
?.subscribe({ feeds ->
}
) {
Timber.e(it)
}
}
And the layout :
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
app:items="#{vm.feeds}"/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:visibleGone="#{vm.isFeedsLoading}"/>
</FrameLayout>
Problem : When we RESUME the Fragment. It shows ProgressBar and the visibility is not Gone as expected. What could be the reason?
And my Fragment :
#ActivityScoped
class DetailFragment #Inject
constructor() // Required empty public constructor
: DaggerFragment() {
#Inject
lateinit var viewModelFactory: DetailViewModel.DetailViewModelFactory
#Inject
lateinit var goal: SavingsGoal
private val compositeDisposable = CompositeDisposable()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val viewModel = ViewModelProviders.of(this, viewModelFactory)[DetailViewModel::class.java]
val root = inflater.inflate(R.layout.fragment_detail, container, false)
val binding = FragmentDetailBinding.bind(root).apply {
setVariable(BR.vm, viewModel)
goal = this#DetailFragment.goal
lifecycleOwner = this#DetailFragment
}
with(root) {
with(activity as AppCompatActivity) {
setupActionBar(binding.toolbar) {
setDisplayShowTitleEnabled(false)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
}
}
binding.vm?.showFeeds(goal)?.let { compositeDisposable.add(it) }
return root
}
override fun onDestroyView() {
super.onDestroyView()
compositeDisposable.clear()
}
}
BindingAdapter :
#BindingAdapter("visibleGone")
fun View.visibleGone(visible: Boolean) {
visibility = if (visible) View.VISIBLE else View.GONE
}
Not sure if this is what you were looking for..?
android:visibility="#{vm.isFeedsLoading} ? View.VISIBLE : View.GONE"
https://stackoverflow.com/a/47746579/7697633