I have a TabLayout, contains three Fragment (Created by same instance) by SectionsPagerAdapter. Inside the fragment, I try with ViewModelProvider.Factory to create independent viewmodel, however, I found all fragments always update content together with same data.
I have debugged and found it always return the same viewmodel even
with difference BillType, and
something's weird that when enterence into activity, the
Factory.create is only invoked once.
// Log
D/BillType: OUTCOME
D/Factory crate BillType: OUTCOME
D/ViewModel Init BillType: OUTCOME
D/viewModel Bill:BillType: OUTCOME
D/BillType: INCOME
D/viewModel Bill:BillType: OUTCOME
D/BillType: TRANSFER
D/viewModel Bill:BillType: OUTCOME
I cannot figure out where is wrong, same code runs correctly before.
class BillViewModel(billType: BillType): ViewModel() {
val bill: MutableLiveData<Bill> = MutableLiveData()
init {
Log.d("ViewModel Init BillType", billType.toString())
bill.value = Bill.QBill().apply {
type = billType
}
}
class NewBillViewModelFactory(val billType: BillType): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
Log.d("Factory crate BillType", billType.toString())
return modelClass.getConstructor(BillType::class.java)
.newInstance(billType)
}
}
}
enum class BillType(val type: Int) {
OUTCOME(0),
INCOME(1),
TRANSFER(2);
}
class NewBillFragment: BaseFragment() {
...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
billType = BillType.values()[arguments?.getInt(BILLTYPE, 0) ?: 0]
Log.d("BillType", billType.toString())
viewModel = ViewModelProvider(requireActivity(), NewBillViewModel.NewBillViewModelFactory(billType))[NewBillViewModel::class.java]
Log.d("viewModel Bill:BillType", viewModel.bill.value?.type.toString())
_binding = FragmentBillNewBinding.inflate(layoutInflater, container, false)
with(binding) {
data = viewModel
lifecycleOwner = activity
... ui ...
return binding.root
}
companion object {
private const val BILLTYPE = "billtype"
#JvmStatic
fun newInstance(billType: Int): NewBillFragment {
return NewBillFragment().apply {
arguments = Bundle().apply {
putInt(BILLTYPE, billType)
}
}
}
}
}
class SectionsPagerAdapter(private val context: Context, fm: FragmentManager)
: FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment = BillFragment.newInstance(position)
override fun getPageTitle(position: Int): CharSequence = context.resources.getString(TAB_TITLES[position])
override fun getCount(): Int = 3
}
Because you are creating a Shared ViewModel with requireActivity() . So it will return ViewModel with reference to Activity not Fragment.
If you want to keep ViewModel Fragment scoped Then you should pass Fragment as ViewModelStoreOwner .
viewModel = ViewModelProvider(this, NewBillViewModel.NewBillViewModelFactory(billType))[NewBillViewModel::class.java]
Related
My app contains a Room db of cocktail recipes that it downloads via a Retrofit api call, all of that is working well. To focus in on where my problem lies, my use case is a user adding a cocktail to a list. This is done via a DialogFragment and here the DialogFragment displays, the transaction executes, the DialogFragment goes away and the Room db is updated. The cocktail fragment does not get the update though - if you navigate away and back, the update is visible so I know the transaction worked as expected. One thing I just noticed is if I rotate the device, the update gets picked up as well.
Here is the relevant section of my fragment:
class CocktailDetailFragment : BaseFragment<CocktailDetailViewModel, FragmentCocktailDetailBinding, CocktailDetailRepository>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.cocktail.observe(viewLifecycleOwner, Observer {
when(it){
is Resource.Success -> {
updateCocktail(it.value)
}
}
})
}
private fun updateCocktail(cocktail: Cocktail) {
with(binding){
detailCocktailName.text = cocktail.cocktailName
//...
//this is the piece of functionality i'm expecting the LiveData observer to execute and change the drawable
if(cocktail.numLists > 0) {
detailCocktailListImageView.setImageResource(R.drawable.list_filled)
} else {
detailCocktailListImageView.setImageResource(R.drawable.list_empty)
}
}
}
override fun getViewModel() = CocktailDetailViewModel::class.java
override fun getFragmentBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentCocktailDetailBinding.inflate(inflater,container,false)
override fun getFragmentRepository(): CocktailDetailRepository {
val dao = GoodCallDatabase(requireContext()).goodCallDao()
return CocktailDetailRepository(dao)
}
}
BaseFragment:
abstract class BaseFragment<VM: BaseViewModel, B: ViewBinding, R: BaseRepository>: Fragment() {
protected lateinit var userPreferences: UserPreferences
protected lateinit var binding: B
protected lateinit var viewModel: VM
protected val remoteDataSource = RemoteDataSource()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
userPreferences = UserPreferences(requireContext())
binding = getFragmentBinding(inflater, container)
val factory = ViewModelFactory(getFragmentRepository())
viewModel = ViewModelProvider(this, factory).get(getViewModel())
return binding.root
}
abstract fun getViewModel() : Class<VM>
abstract fun getFragmentBinding(inflater: LayoutInflater, container: ViewGroup?): B
abstract fun getFragmentRepository(): R
}
ViewModelFactory:
class ViewModelFactory(
private val repository: BaseRepository
): ViewModelProvider.NewInstanceFactory() {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return when{
modelClass.isAssignableFrom(CocktailDetailViewModel::class.java) -> CocktailDetailViewModel(repository as CocktailDetailRepository) as T
else -> throw IllegalArgumentException("ViewModel class not found")
}
}
}
ViewModel:
class CocktailDetailViewModel(
private val repository: CocktailDetailRepository
): BaseViewModel(repository) {
private val _cocktail: MutableLiveData<Resource<Cocktail>> = MutableLiveData()
val cocktail: LiveData<Resource<Cocktail>>
get() = _cocktail
fun getCocktailByCocktailId(cocktailId: Int) = viewModelScope.launch {
_cocktail.value = Resource.Loading
_cocktail.value = repository.getCocktail(cocktailId)
}
}
Repository:
class CocktailDetailRepository(
private val dao: GoodCallDao
):BaseRepository(dao) {
suspend fun getCocktail(cocktailId: Int) = safeApiCall {
dao.getCocktail(cocktailId)
}
}
Safe Api Call (I use this so db/api calls run on IO):
interface SafeApiCall {
suspend fun <T> safeApiCall(
apiCall: suspend () -> T
): Resource<T> {
return withContext(Dispatchers.IO) {
try {
Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
Resource.Failure(false, throwable.code(), throwable.response()?.errorBody())
}
else -> {
Resource.Failure(true, null, null)
}
}
}
}
}
}
Resource:
sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>()
data class Failure(
val isNetworkError: Boolean,
val errorCode: Int?,
val errorBody: ResponseBody?
): Resource<Nothing>()
object Loading: Resource<Nothing>()
}
Dao:
#Query("SELECT * FROM cocktail c WHERE c.cocktail_id = :cocktailId")
suspend fun getCocktail(cocktailId: Int): Cocktail
Thank you in advance for any help! Given the issue and how the app is working, I believe I have provided all the relevant parts but please advise if more of my code is required to figure this out.
i'm making an app and i want to separate my UI logic into multiple UI classes with BaseUi class being lifecycle aware. I'm using Kodein as my DI and i have an issue with fragment.viewLifecycleOwnerLiveData.observe not being called when instance of my ui class is being retrieved by Kodein.
Here is my Fragment class:
class ListFragment : Fragment(), DIAware {
override val di: DI by closestDI()
override val diTrigger: DITrigger = DITrigger()
private var binding: FragmentMoviesBinding? = null
private val fragmentBinding get() = binding
private val kodeinMoviesUi: MoviesUi by instance() //fragment does not observe viewLifecycleOwnerLiveData
private val moviesUi: MoviesUi = MoviesUi(this) //fragment now observe viewLifecycleOwnerLiveData
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMoviesBinding.inflate(inflater, container, false)
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
diTrigger.trigger()
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
BaseUi class:
abstract class BaseUi<F : Fragment>(private val fragment: F) : LifecycleObserver {
init {
fragment.viewLifecycleOwnerLiveData.observe(fragment, { subscribeToLifecycle() })
}
private fun subscribeToLifecycle() {
fragment.viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
onViewCreated()
}
})
}
abstract fun onViewCreated()
}
And UiModule:
val uiModule = DI.Module("uiModule") {
bind<ListFragment>() with provider { ListFragment() }
bind<MoviesUi>() with provider { MoviesUi(instance()) }
}
Cross post from https://github.com/Kodein-Framework/Kodein-DI/issues/353
Here is your problem bind<ListFragment>() with provider { ListFragment() }.
You bound the ListFragment with a provider, meaning every time you ask to the container it will create an instance of ListFragment. So, when you inject MoviesUi with private val kodeinMoviesUi: MoviesUi by instance(), it gets another instance of ListFragment.
I suggest that you define the binding for MoviesUi as a factory, waiting to receive a ListFragment instance:
bind<MoviesUi>() with factory {fragment: ListFragment -> MoviesUi(fragment) }
then you can inject it in the ListFragment like:
private val kodeinMoviesUi: MoviesUi by instance(args = this)
I am trying to figure out how to pass an integer from a fragment to a viewmodel while using hilt. I have ready that viewmodel factories can be used for this, I am not sure how this would be done using DI.
In the code below, I am trying to figure out how I can pass albumId to the viewModel. The albumId will be used when fetching data from an API endpoint.
Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_album_details, container, false)
val albumId = arguments?.getInt("album_id")
viewModel.songs.observe(viewLifecycleOwner) {
view.song_recyclerview.apply {
layoutManager = LinearLayoutManager(this.context)
adapter = SongAdapter(viewModel.songs)
}
}
return view
}
ViewModel
class SongViewModel #ViewModelInject constructor(
songRepo: SongRepository,
#Assisted savedStateHandle: SavedStateHandle
) : ViewModel(), LifecycleObserver {
val songs: LiveData<List<Song>> = songRepo.getSongs(1)
}
Repository
class SongRepository constructor(
private val musicService: MusicService
)
{
fun getSongs(album_id: Int): LiveData<List<Song>> {
val data = MutableLiveData<List<Song>>()
musicService.getAlbumTracks(album_id).enqueue(object : Callback<List<Song>> {
override fun onResponse(call: Call<List<Song>>, response: Response<List<Song>>) {
data.value = response.body()
}
override fun onFailure(call: Call<List<Song>>, t: Throwable) {
}
})
return data
}
}
I was finally able to figure out a solution to the problem. I added a field to the viewmodel, and a method to set a value for that field. Basically, I call viewModel.start(int) then call viewModel.songs.
ViewModel
class SongViewModel #ViewModelInject constructor(
songRepo: SongRepository,
#Assisted savedStateHandle: SavedStateHandle
) : ViewModel(), LifecycleObserver {
private val _id = MutableLiveData<Int>() // int field
private val _songs = _id.switchMap { id ->
songRepo.getSongs(id)
}
val songs: LiveData<List<Song>> = _songs
fun start(id: Int) { // method to update field
_id.value = id
}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// get data from bundle and pass to start()
arguments?.getInt(("album_id"))?.let { viewModel.start(it)}
viewModel.songs.observe(viewLifecycleOwner) {
view.song_recyclerview.apply {
layoutManager = LinearLayoutManager(this.context)
adapter = SongAdapter(viewModel.songs)
}
}
}
I'm using Dagger 2 in clean architecture project, I have 2 fragments. These 2 fragments should be scoped together to share the same instances, but unfortunately, I got empty object in the second fragment.
Application Component
#ApplicationScope
#Component(modules = [ContextModule::class, RetrofitModule::class])
interface ApplicationComponent {
fun exposeRetrofit(): Retrofit
fun exposeContext(): Context
}
Data layer - Repository
class MoviesParsableImpl #Inject constructor(var moviesLocalResult: MoviesLocalResult): MoviesParsable {
private val TAG = javaClass.simpleName
private val fileUtils = FileUtils()
override fun parseMovies() {
Log.d(TAG,"current thread is ".plus(Thread.currentThread().name))
val gson = Gson()
val fileName = "movies.json"
val jsonAsString = MyApplication.appContext.assets.open(fileName).bufferedReader().use{
it.readText()
}
val listType: Type = object : TypeToken<MoviesLocalResult>() {}.type
moviesLocalResult = gson.fromJson(jsonAsString,listType)
Log.d(TAG,"result size ".plus(moviesLocalResult.movies?.size))
}
override fun getParsedMovies(): Results<MoviesLocalResult> {
return Results.Success(moviesLocalResult)
}
}
Repo Module
#Module
interface RepoModule {
#DataComponentScope
#Binds
fun bindsMoviesParsable(moviesParsableImpl: MoviesParsableImpl): MoviesParsable
}
MoviesLocalResultsModule(the result need its instance across different fragments)
#Module
class MoviesLocalResultModule {
#DataComponentScope
#Provides
fun provideMovieLocalResults(): MoviesLocalResult{
return MoviesLocalResult()
}
}
Use case
class AllMoviesUseCase #Inject constructor(private val moviesParsable: MoviesParsable){
fun parseMovies(){
moviesParsable.parseMovies()
}
fun getMovies(): Results<MoviesLocalResult> {
return moviesParsable.getParsedMovies()
}
}
Presentation Component
#PresentationScope
#Component(modules = [ViewModelFactoryModule::class],dependencies = [DataComponent::class])
interface PresentationComponent {
fun exposeViewModel(): ViewModelFactory
}
First ViewModel, where I got the result to be shared with the other fragment when needed
class AllMoviesViewModel #Inject constructor(private val useCase: AllMoviesUseCase):ViewModel() {
private val moviesMutableLiveData = MutableLiveData<Results<MoviesLocalResult>>()
init {
moviesMutableLiveData.postValue(Results.Loading())
}
fun parseJson(){
viewModelScope.launch(Dispatchers.Default){
useCase.parseMovies()
moviesMutableLiveData.postValue(useCase.getMovies())
}
}
fun readMovies(): LiveData<Results<MoviesLocalResult>> {
return moviesMutableLiveData
}
}
Second ViewModel where no need to request data again as it's expected to be scoped
class MovieDetailsViewModel #Inject constructor(private val useCase: AllMoviesUseCase): ViewModel() {
var readMovies = liveData(Dispatchers.IO){
emit(Results.Loading())
val result = useCase.getMovies()
emit(result)
}
}
First Fragment, where data should be requested:
class AllMoviesFragment : Fragment() {
private val TAG = javaClass.simpleName
private lateinit var viewModel: AllMoviesViewModel
private lateinit var adapter: AllMoviesAdapter
private lateinit var layoutManager: LinearLayoutManager
private var ascendingOrder = true
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
DaggerAllMoviesComponent.builder()
.presentationComponent(
DaggerPresentationComponent.builder()
.dataComponent(
DaggerDataComponent.builder()
.applicationComponent(MyApplication.applicationComponent).build()
)
.build()
).build()inject(this)
viewModel = ViewModelProvider(this, viewModelFactory).get(AllMoviesViewModel::class.java)
startMoviesParsing()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_all_movies, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupRecyclerView()
viewModel.readMovies().observe(viewLifecycleOwner, Observer {
if (it != null) {
when (it) {
is Loading -> {
showResults(false)
}
is Success -> {
showResults(true)
Log.d(TAG, "Data observed ".plus(it.data))
addMoviesList(it.data)
}
is Error -> {
moviesList.snack(getString(R.string.error_fetch_movies))
}
}
}
})
}
Second Fragment, where I expect to get the same instance request in First Fragment as they are scoped.
class MovieDetailsFragment: Fragment() {
val TAG = javaClass.simpleName
#Inject
lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: MovieDetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val depend = DaggerAllMoviesComponent.builder()
.presentationComponent(
DaggerPresentationComponent.builder()
.dataComponent(
DaggerDataComponent.builder()
.applicationComponent(MyApplication.applicationComponent).build())
.build()
).build()
depend.inject(this)
viewModel = ViewModelProvider(this, viewModelFactory).get(MovieDetailsViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel.readMovies.observe(this, Observer {
if (it!=null){
Log.d(TAG,"Movies returned successfully")
}
})
return super.onCreateView(inflater, container, savedInstanceState)
}
}
Scopes tell a component to cache the results of a binding. It has nothing to do with caching instances of any components. As such, you are always creating a new DataComponent, PresentationComponent, and AllMoviesComponent in your fragments' onCreate methods.
In order to reuse the same AllMoviesComponent instance, you need to store it somewhere. Where you store it can depend on your app architecture, but some options include MyApplication itself, the hosting Activity, or in your navigation graph somehow.
Even after fixing this, you can't guarantee that parseMovies has already been called. The Android system could kill your app at any time, including when MoviesDetailFragment is the current fragment. If that happens and the user navigates back to your app later, any active fragments will be recreated, and you'll still get null.
I have an application using databinding, livedata, room, kotlin koroutines, viewmodel, navigation component and dagger.
I have one activity, and two fragments.
ListFragment: Show in a recyclerview a list of items.
DetalFragment: Show the item detail, and can update some fields of the item with a save button.
The problem is when I update some fields from detailfragment, then the changes isn´t visibles in the listfragment, but when I scroll down and up, the changes become visible.
ListFragment:
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val viewModel: ListViewModel by viewModels {
viewModelFactory
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val bindings = ListFragmentBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
bindings.lifecycleOwner = this
adapter = ItemsAdapter()
bindings.recyclerView.adapter = adapter
viewModel.items.observe(
viewLifecycleOwner,
Observer { adapter.submitList(it)})
return bindings.root
}
ListViewModel:
var items: LiveData<PagedList<Item>> = repository.items
Repository:
val items<PagedList<Item>>
get()=itemDao.getAllItemsPaged().toLiveData(pageSize=50)
fun getItemFlow(id: String): Flow<Item> = itemDao.getItemFlow(id)
suspend fun updateItem(item: Item) {
itemDao.updateItem(item)
}
ItemDao:
#Query("SELECT * FROM item")
fun getAllItemsPaged(): DataSource.Factory<Int,Item>
#Query("SELECT * FROM itemWHERE id=:id")
fun getItemFlow(id:String):Flow<Item>
#Update
suspend fun updateItem(item:Item)
ItemFragment:
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val viewModel: ItemViewModel by viewModels {
viewModelFactory
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel.loadItem(args.itemId)
val bindings = ItemFragmentBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
buttonSave.setOnClickListener{viewModel. viewModelScope.launch {
viewModel.saveItem()
findNavController().navigateUp()
}}
}
bindings.lifecycleOwner = this
return bindings.root
}
ItemViewModel:
var item: LiveData<Item>? = null
fun loadItem(id: String) {
viewModelScope.launch {
item = repository.getItemFlow(id).asLiveData()
}
}
suspend fun saveItem() {
item!!.value!!.someField = "hi"
repository.updateItem(item!!.value!!)
}
The problem is in the items adapter. The adapter needs a correct implementation of the DiffUtil.ItemCallback.
In this case:
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Contador>() {
// The ID property identifies when items are the same.
override fun areItemsTheSame(oldItem: Contador, newItem: Contador) =
oldItem.id == newItem.id
// Check the properties that can change, or implements the equals method in Item class
override fun areContentsTheSame(
oldItem: Item, newItem: Item) = oldItem.someField == newItem.someField
}
I can confirm that DiffUtil is the way. In case of a generic approach over a RecyclerView:
ItemListDiffUtil.kt
open class ItemListDiffUtil<T>(private val oldItems: ArrayList<T>, private val newItems: ArrayList<T>) : DiffUtil.Callback(){
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldItems[oldItemPosition] == newItems[newItemPosition]
}
override fun getOldListSize(): Int {
return oldItems.size
}
override fun getNewListSize(): Int {
return newItems.size
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldItems[oldItemPosition] == newItems[newItemPosition]
}
}
YOUR_OBJECTAdapter.kt
class YOUR_OBJECTAdapter() : RecyclerView.Adapter<YOUR_OBJECTAdapter.MyViewHolder>() {
class MyViewHolder(viewItem: View) : RecyclerView.ViewHolder(viewItem) {
//your code
}
private var myDataset = ArrayList<YOUR_OBJECT>()
fun setDataset(data : ArrayList<YOUR_OBJECT>){
/*diffUtil*/
val diffCallback = ItemListDiffUtil(myDataset,data)
val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(this)
myDataset.clear()
myDataset.addAll(data)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val viewItem = LayoutInflater.from(parent.context).inflate(R.layout.YOUR_OBJECT, parent, false)
return MyViewHolder(viewItem)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
//your code
}
override fun getItemCount(): Int {
return myDataset.size
}
}