I am an beginner. My experience when i create an apps, usually i using clean archi and mvvm for the architect, and when using room, it will be use live data also.
But when i create a simple apps don't want to use them, i have trouble with the data which is cannot load directly, please help me. Below is my code
WisataDao.kt
#Dao
interface WisataDao {
#Query("SELECT * from wisata")
fun getAll(): List<WisataEntity>
#Query("SELECT * from wisata WHERE id = :id")
fun getById(id: String): Boolean
#Insert(onConflict = REPLACE)
fun insert(wisata: WisataEntity)
#Delete
fun delete(wisata: WisataEntity)
}
WisataDatabase.kt
#Database(entities = [WisataEntity::class], version = 1)
abstract class WisataDatabase : RoomDatabase() {
abstract fun wisataDao(): WisataDao
companion object {
private var INSTANCE: WisataDatabase? = null
fun getInstance(context: Context): WisataDatabase? {
if (INSTANCE == null) {
synchronized(WisataDatabase::class) {
INSTANCE = Room.databaseBuilder(context.applicationContext,
WisataDatabase::class.java, "wisata.db")
.build()
}
}
return INSTANCE
}
fun destroyInstance() {
INSTANCE = null
}
}
}
FavoriteFrament.kt
class FavoriteFragment : Fragment() {
private lateinit var binding: FragmentFavoriteBinding
private lateinit var database: WisataDatabase
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentFavoriteBinding.inflate(layoutInflater, container, false)
return binding.root
}
#DelicateCoroutinesApi
#SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
database = WisataDatabase.getInstance(requireContext())!!
binding.rvWisata.layoutManager = LinearLayoutManager(context)
val list = ArrayList<WisataEntity>()
var adapter = HomeAdapter(list)
GlobalScope.launch {
coroutineContext.run {
list.addAll(database.wisataDao().getAll())
adapter = HomeAdapter(list)
adapter.notifyDataSetChanged()
}
}
binding.rvWisata.adapter = adapter
adapter.setOnItemClickCallback(object : HomeAdapter.OnItemClickCallback {
override fun onItemClicked(data: WisataEntity) {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra("data", data)
startActivity(intent)
}
})
}
}
Please help me guys
The problem is you are initializing the adapter twice but only assigning it once.
//Here 1st initialization
var adapter = HomeAdapter(list)
GlobalScope.launch {
coroutineContext.run {
list.addAll(database.wisataDao().getAll())
//Here 2nd initialization
//but no assignment now the RecyclerView has an adapter
//that doesn't exist anymore because you overwrote it
adapter = HomeAdapter(list)
adapter.notifyDataSetChanged()
}
}
//Here 1 assignment
//You assign the 1st initialized adapter
//that has no data.
binding.rvWisata.adapter = adapter
What you want to do is add a function to the Adapter that accepts a List and pass in the new List to the Adapter and notify the changes.
GlobalScope.launch {
coroutineContext.run {
list.addAll(database.wisataDao().getAll())
//Here 2nd initialization
adapter.setList(list)
adapter.notifyDataSetChanged()
}
}
Related
StateFlow is emitting new data after change, but ListAdapter is not being updated/notified, but when configuration is changed(i.e device is rotated from Portrait to Landscape mode) update is occurred:
class TutorialListFragment : Fragment() {
private lateinit var binding: FragmentTutorialListBinding
private val viewModel: ITutorialViewModel by viewModels<TutorialViewModelImpl>()
private lateinit var adapter: TutorialAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentTutorialListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = binding.recyclerView
adapter = TutorialAdapter()
recyclerView.adapter = adapter
loadData()
}
private fun loadData() {
viewModel
.getTutorialList()
val tutorialList: MutableList<TutorialResponse> = mutableListOf()
viewModel
.tutorialListStateFlow
.onEach { list ->
list.forEach {tutorialResponse->
tutorialList.add(tutorialResponse)
Log.e("TUTORIAL_LIST_FRAG", "$tutorialResponse")
}
adapter.submitList(tutorialList)
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
}
View model is:
class TutorialViewModelImpl: ViewModel(), ITutorialViewModel {
private val mTutorialRepository: ITutorialRepository = TutorialRepositoryImpl()
private val _tutorialListStateFlow = MutableStateFlow<List<TutorialResponse>>(mutableListOf())
override val tutorialListStateFlow: StateFlow<List<TutorialResponse>>
get() = _tutorialListStateFlow.asStateFlow()
init {
mTutorialRepository
.getTutorialListSuccessListener {
viewModelScope
.launch {
_tutorialListStateFlow.emit(it)
Log.e("TUTORIAL_GL_VM", "$it")
}
}
}
override fun getTutorialList() {
// Get list
mTutorialRepository.getTutorialList()
}
}
When I look into Logcat I see this line:
Log.e("TUTORIAL_GL_VM", "$it")
prints all the changes, but no update in ListAdapter.
I assume your data from mTutorialRepository is not a flow ,so you must add .toList() if you want to emit list in stateFlow to get notified
mTutorialRepository.getTutorialListSuccessListener {
viewModelScope.launch {
// here add .toList()
_tutorialListStateFlow.emit(it.toList())
}
}
or if it still does not works, try to change your loadData() like this
private fun loadData() {
// idk what are doing with this ??
viewModel.getTutorialList()
lifecycleScope.launch {
viewModel.tutorialListStateFlow.collect { list ->
adapter.submitList(list)
}
}
}
I am missing some basic coding knowledge here I think, I want to present value to the fragment by assigning the function to the variable in a viewModel. When I call the function directly, I get correct value. When I assign function to variable and pass the variable to the fragment it is always null, why?
View Model
class CartFragmentViewModel : ViewModel() {
private val repository = FirebaseCloud()
private val user = repository.getUserData()
val userCart = user?.switchMap {
repository.getProductsFromCart(it.cart)
}
private fun calculateCartValue(): Long? {
val list = userCart?.value
return list?.map { it.price!! }?.sum()
}
//val cartValue = userCart?.value?.sumOf { it.price!! } <- THIS will be null
val cartValue = calculateCartValue() <- THIS will be null
val cartSize = userCart?.value?.size <- THIS will be null
}
Fragment
class CartFragment : RootFragment(), OnProductClick, View.OnClickListener {
private lateinit var cartViewModel: CartFragmentViewModel
private lateinit var binding: FragmentCartBinding
private val cartAdapter = CartAdapter(this)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_cart,
container,
false
)
setAnimation()
cartViewModel = CartFragmentViewModel()
binding.buttonToCheckout.setOnClickListener(this)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerCart.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = cartAdapter
}
cartViewModel.userCart?.observe(viewLifecycleOwner, { list ->
cartAdapter.setCartProducts(list)
updateCart()
})
}
override fun onClick(view: View?) {
when (view) {
binding.buttonToCheckout -> {
navigateToCheckout(cartViewModel.cartValue.toString())
cartViewModel.sendProductEvent(
cartAdapter.cartList,
ProductEventType.CHECKOUT
)
}
}
}
override fun onProductClick(product: Product, position: Int) {
cartViewModel.removeFromCart(product)
cartAdapter.removeFromCart(product, position)
updateCart()
}
private fun updateCart() {
binding.textCartTotalValue.text = cartViewModel.cartValue.toString() <- NULL
binding.textCartQuantityValue.text = cartViewModel.cartSize.toString() <- NULL
}
}
Thanks!
It looks like userCart is some sort of observable variable which initially holds a null value and then gets populated with the data from your repository after the network call (or something similar) completes.
The reason that all your variables are null are because you are declaring their value immediately, so by the time those statements get executed, the network call hasn't yet completed and userCart?.value is null. However calling the calculateCartValue() function later on in the code might yield a value if the fetch is complete.
I have a ShopFilterFragmentProductFilter which is inside a ShopFilterFragmentHolder which itself holds a ViewPager2. This ShopFilterFragmentHolder is a DialogFragment which is opened inside my ShopFragment. So ShopFragment -> ShopFilterFragmentHolder (Dialog, ViewPager2) -> ShopFilterFragmentProductFilter. ALL of these Fragments should share the same navgraphscoped viewmodel.
The problem I have is, that when I attach an observer inside my ShopFilterFragmentProductFilter to get my recyclerview list from cloud-firestore, this observer never gets called and therefore I get the error message "No Adapter attached, skipping layout". I know that this is not a problem with how I instantiate and assign the adapter to my recyclerview, because when I set a static list (e.g creating a list inside my ShopFilterFragmentProductFilter) everything works.
Why do I don't get the livedata value? To my mind, there is a problem with the viewmodel creation.
Here is my current approach:
ShopFilterFragmentProductFilter
#AndroidEntryPoint
class ShopFilterFragmentProductFilter : Fragment() {
private var _binding: FragmentShopFilterItemBinding? = null
private val binding: FragmentShopFilterItemBinding get() = _binding!!
private val shopViewModel: ShopViewModel by navGraphViewModels(R.id.nav_shop) { defaultViewModelProviderFactory }
#Inject lateinit var shopFilterItemAdapter: ShopFilterItemAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentShopFilterItemBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindObjects()
submitAdapterList()
}
override fun onDestroyView() {
super.onDestroyView()
binding.rvShopFilter.adapter = null
_binding = null
}
private fun bindObjects() {
with(binding) {
adapter = shopFilterItemAdapter
}
}
private fun submitAdapterList() {
shopViewModel.shopProductFilterList.observe(viewLifecycleOwner) {
shopFilterItemAdapter.submitList(it)
shopFilterItemAdapter.notifyDataSetChanged()
toast("SUBMITTED LIST") // this does never get called
}
/* // this works
shopFilterItemAdapter.submitList(
listOf(
ShopFilterItem(0, "ITEM 1"),
ShopFilterItem(0, "ITEM 2"),
ShopFilterItem(0, "ITEM 3"),
ShopFilterItem(0, "ITEM 4"),
ShopFilterItem(0, "ITEM 5"),
)
)
*/
}
}
ViewModel
class ShopViewModel #ViewModelInject constructor(
private val shopRepository: ShopRepository,
private val shopFilterRepository: ShopFilterRepository
) : ViewModel() {
private val query = MutableLiveData(QueryHolder("", ""))
val shopPagingData = query.switchMap { query -> shopRepository.search(query).cachedIn(viewModelScope) }
val shopProductFilterList: LiveData<List<ShopFilterItem>> = liveData { shopFilterRepository.getProductFilterList() }
val shopListFilterList: LiveData<List<ShopFilterItem>> = liveData { shopFilterRepository.getListFilterList() }
fun search(newQuery: QueryHolder) {
this.query.value = newQuery
}
}
ShopFilterRepositoryImpl
class ShopFilterRepositoryImpl #Inject constructor(private val db: FirebaseFirestore) : ShopFilterRepository {
override suspend fun getProductFilterList(): List<ShopFilterItem> = db.collection(FIREBASE_SERVICE_INFO_BASE_PATH)
.document(FIREBASE_SHOP_FILTER_BASE_PATH)
.get()
.await()
.toObject<ShopFilterItemHolder>()!!
.productFilter
override suspend fun getListFilterList(): List<ShopFilterItem> = db.collection(FIREBASE_SERVICE_INFO_BASE_PATH)
.document(FIREBASE_SHOP_FILTER_BASE_PATH)
.get()
.await()
.toObject<ShopFilterItemHolder>()!!
.listFilter
}
Nav_graph
Probably, you should define it as MutableLiveData:
private val shopProductFilterList: MutableLiveData<List<ShopFilterItem>> = MutableLiveData()
And in a method in your viewModel that gets the data through repository, you should post the LiveData value:
fun getProductFilterList() = viewModelScope.launch {
val dataFetched = repository. getProductFilterList()
shopProductFilterList.postValue(dataFetched)
}
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 am going through Guide to app architecture and trying to implement MVVM and LiveData in one of my apps. I am using realm and I am using this to create a RealmLiveData as shown below
class RealmLiveData<T : RealmModel>(private val results: RealmResults<T>) : MutableLiveData<RealmResults<T>>() {
private val listener = RealmChangeListener<RealmResults<T>> { results -> value = results }
override fun onActive() {
results.addChangeListener(listener)
}
override fun onInactive() {
results.removeChangeListener(listener)
}
}
This how I am updating the list to recyclerview
var mList:ArrayList<Notes> = ArrayList()
lateinit var historyViewModel: HistoryViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_history, container, false)
mRCview = view.findViewById(R.id.list)
historyViewModel = ViewModelProviders.of(activity!!).get(HistoryViewModel::class.java)
// this is how I observe
historyViewModel.getList().observe(this, Observer{
(mRCview.adapter as MyHistoryRecyclerViewAdapter).setData(it)
})
with(mRCview) {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(mContext)
mList = ArrayList()
adapter = MyHistoryRecyclerViewAdapter(
mContext as OnListFragmentInteractionListener
)
}
return view
}
This is how I get the data in my repository class
class HistoryRepository {
fun getHistory(): RealmLiveData<Notes> {
val realmInstance = Realm.getDefaultInstance()
val realmResults = realmInstance
.where(Notes::class.java)
.findAll()
.sort("lastUpdatedTimeStamp", Sort.DESCENDING)
return realmResults.asLiveData()
}
fun <T:RealmModel> RealmResults<T>.asLiveData() = RealmLiveData(this)
}
EDIT
Here is the ViewModel
class HistoryViewModel: ViewModel() {
val repository = HistoryRepository()
fun getList(): RealmLiveData<Notes> {
return repository.getHistory()
}
}
The issue is that the observer is not getting triggered for the first time. If I update the realmresult, the live data update gets invoked and updates my list. Please let me know how I can fix the issue.
We need to notify the Observer of the existing data. When the first Observer registers to historyViewModel.getList() you are registering the realm callback. At this point we need to trigger a change just to notify this Observer of the existing data.
Something like
class RealmLiveData<T : RealmModel>(private val results: RealmResults<T>) : MutableLiveData<RealmResults<T>>() {
private val listener = RealmChangeListener<RealmResults<T>> { results -> value = results }
override fun onActive() {
results.addChangeListener(listener)
listener.onChange(results) // notify the added Observer of the existing data.
}
override fun onInactive() {
results.removeChangeListener(listener)
}
}