I have a simple Android Kotlin app that i am working on which involves multiple fragments communicating with one viewModel. The problem is the LiveData observer in my first fragment will not update every time the list changes in my view model. Can anyone explain where i might be going wrong?
Here is my fragment:
class ShoeDetailsFragment : Fragment() {
lateinit var binding: FragmentShoeDetailsBinding
private val viewModel: ShoeViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_shoe_details, container, false)
setClickListeners()
watchForChanges()
return binding.root
}
private fun watchForChanges(){
viewModel.shoeList.observe(viewLifecycleOwner) { list ->
Log.i("Contents of list: ", list.toString())
binding.viewModelTestEditText.setText(list.toString())
}
}
private fun createShoeFromInputs(): Shoe {
val shoeNameString = binding.shoeNameEditText.text.toString()
val shoeColourString = binding.shoeColourEditText.text.toString()
val shoeMakerString = binding.shoeMakerEditText.text.toString()
val shoeSizeString = binding.shoeSizeEditText.text.toString()
return Shoe(shoeNameString, shoeColourString, shoeMakerString, shoeSizeString)
}
private fun setClickListeners(){
binding.saveButtonShoeDetails.setOnClickListener{
saveShoeToList(createShoeFromInputs())
}
binding.cancelButtonShoeDetails.setOnClickListener{
viewModel.removeShoeFromShoeList()
}
}
private fun saveShoeToList(shoe: Shoe){
if (validateFields()){
viewModel.addShoeToShoeList(shoe)
}
}
private fun validateFields(): Boolean{
return if (binding.shoeNameEditText.text.isEmpty()
|| binding.shoeColourEditText.text.isEmpty()
|| binding.shoeMakerEditText.text.isEmpty()
|| binding.shoeSizeEditText.text.isEmpty()){
Toast.makeText(requireContext(), "Please complete all fields", Toast.LENGTH_SHORT).show()
false
} else {
true
}
}
}
And here is my viewModel:
class ShoeViewModel: ViewModel() {
private val _shoeList = MutableLiveData<MutableList<Shoe>>()
val shoeList: LiveData<MutableList<Shoe>> get () =_shoeList
init {
_shoeList.value = mutableListOf()
}
fun addShoeToShoeList(shoe: Shoe){
_shoeList.value!!.add(shoe)
Log.i("Contents of list in view model: ", _shoeList.value!!.size.toString())
}
fun removeShoeFromShoeList(){
_shoeList.value!!.removeAt(0)
Log.i("Contents of list in view model after cancel: ", _shoeList.value!!.size.toString())
}
}
I have checked the code over and over again but there must be something i am missing
You haven't changed the value of the LiveData. It's still pointing at the same instance of a MutableList. You modified the contents of that MutableList, but the LiveData doesn't know anything about you doing that, so it will not notify observers.
I strongly recommend that you only use read-only Lists with LiveData. Instead of mutating the list, you create a new list and set it as the new value of the LiveData.
class ShoeViewModel: ViewModel() {
private val _shoeList = MutableLiveData<List<Shoe>>()
val shoeList: LiveData<List<Shoe>> get () =_shoeList
init {
_shoeList.value = emptyList()
}
fun addShoeToShoeList(shoe: Shoe){
_shoeList.value = _shoeList.value.orEmpty() + shoe
Log.i("Contents of list in view model: ", _shoeList.value.orEmpty().size.toString())
}
fun removeShoeFromShoeList(){
_shoeList.value = _shoeList.value.orEmpty().drop(1)
Log.i("Contents of list in view model after cancel: ", _shoeList.value.orEmpty().size.toString())
}
}
Note, it is possible to use a MutableList, and then call liveData.value = liveData.value each time after you mutate the list to trigger it to notify observers. The reason I recommend you not do this is that some view classes (notably RecyclerView's ListAdapter) are "smart" and compare old and new data to determine whether they actually need to show any changes. If the old and new data are both the same instance of MutableList, it will not detect any changes so the UI will not update.
You need to call MuableLiveData.setValue() or MutableLiveData.postValue() for event to be emited.
try :
fun addShoeToShoeList(shoe: Shoe){
val currentList = _shoeList.value ?: mutableListOf()
currentList.add(shoe)
_shoeList.value = Collections.copy(currentList)
Log.i("Contents of list in view model: ", _shoeList.value!!.size.toString())
}
fun removeShoeFromShoeList(){
val currentList = _shoeList.value ?: mutableListOf()
currentList.removeAt(0)
_shoeList.value=currentList
Log.i("Contents of list in view model after cancel: ", _shoeList.value!!.size.toString())
}
Related
I'm trying to rewrite my program and start using Kotlin Coroutines.
That is my function to retrieve a list of products for a given group. After debugging it looks like everything is correct.
class FirebaseRepository {
private val db = FirebaseFirestore.getInstance()
private val auth = FirebaseAuth.getInstance()
fun getCurrentUserId(): String{
return auth.currentUser!!.uid
}
suspend fun getLista(): MutableLiveData<List<Produkt>> {
val result = MutableLiveData<List<Produkt>>()
val lista = mutableListOf<Produkt>()
db.collection(Constants.GROUP)
.document("xGRWy21hwQ7yuBGIJtnA")
.collection("Przedmioty")
.orderBy("dataDodaniaProduktu", Query.Direction.DESCENDING)
.get().await().forEach {
val singleProdukt = it.toObject(Produkt::class.java)
singleProdukt.produktId = it.id
lista.add(singleProdukt)
result.postValue(lista)
}
return result
}
That is my ViewModel class:
class ListaViewModel: ViewModel() {
private val repository = FirebaseRepository()
var _produkty = MutableLiveData<List<Produkt>>()
val produkty : LiveData<List<Produkt>> = _produkty
init {
viewModelScope.launch {
_produkty = repository.getLista()
}
}
And finally in my fragment I'm trying to observe live data but looks like nothing is being passed to my adapter. What am I doing wrong?
class ListaFragment : Fragment(), ListaAdapter.OnItemClickListener {
private var _binding: FragmentListaBinding? = null
private val binding get() = _binding!!
private lateinit var recyclerView : RecyclerView
private lateinit var listAdapter : ListaAdapter
private val listaViewModel by viewModels<ListaViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentListaBinding.inflate(inflater, container, false)
recyclerView = binding.recyclerView
listAdapter = ListaAdapter(emptyList(), this)
recyclerView.adapter = listAdapter // Zapobiega "No adapter attached; skipping layout"
recyclerView.layoutManager = LinearLayoutManager(requireActivity())
recyclerView.setHasFixedSize(true)
listaViewModel.produkty.observe(viewLifecycleOwner, Observer {
listAdapter = ListaAdapter(it, this)
}
return binding.root
}
Try replacing this:
val produkty : LiveData<List<Produkt>> = _produkty
with this
val produkty : LiveData<List<Produkt>> get() = _produkty
This way you'll have "getter" rather than "initializer". Initializer will compute its value once (to the empty live data) and after you reassign that var it won't change the value of your val.
The problem in your code lies in the fact that you're creating a new instance of your ListaAdapter class inside the observe() method, without notifying the adapter about the changes. That's the reason why you're getting no results in the adapter. To solve this, simply create a method inside your adapter class:
fun setProduktList(produktList: List<Produkt>) {
this.produktList = produktList
notifyDataSetChanged()
}
Then inside your observe() method, use the following line of code:
listaViewModel.produkty.observe(viewLifecycleOwner, Observer {
//listAdapter = ListaAdapter(it, this) //Removed
listAdapter.setProduktList(it) 👈
}
I am trying to use the Firebase API in my project but Transformations.map for the variable authenticationState in the View Model does not run. I have been following Google's tutorial here (link goes to the ViewModel of that project).
I want to be able to add the Transformations.map code to the FirebaseUserLiveData file later but I cant seem to figure out why it doesn't run.
FirebaseUserLiveData
class FirebaseUserLiveData: LiveData<FirebaseUser?>() {
private val firebaseAuth = FirebaseAuth.getInstance()
private val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
value = firebaseAuth.currentUser
}
override fun onActive() {
firebaseAuth.addAuthStateListener { authStateListener }
}
override fun onInactive() {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
SearchMovieFragmentViewModel
class SearchMovieFragmentViewModel : ViewModel() {
enum class AuthenticationState {
AUTHENTICATED, UNAUTHENTICATED, INVALID_AUTHENTICATION
}
var authenticationState = Transformations.map(FirebaseUserLiveData()) { user ->
Log.d("TEST", "in the state function")
if (user != null) {
AuthenticationState.AUTHENTICATED
} else {
AuthenticationState.UNAUTHENTICATED
}
}
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
binding.signOutButton.setOnClickListener {
AuthUI.getInstance().signOut(requireContext())
}
searchMovieFragmentViewModel.authenticationState.observe(viewLifecycleOwner, Observer { state ->
when (state) {
AUTHENTICATED -> searchMovieFragmentViewModel.signedIn = View.VISIBLE
UNAUTHENTICATED -> searchMovieFragmentViewModel.signedIn = View.GONE
}
})
return binding.root
}
}
Should be .addAuthStateListener(authStateListener) instead of { authStateListener }
That is because you are not keeping the reference of FirebaseUserLiveData() once you start observing it like Transformations.map(FirebaseUserLiveData()) { user ->.
You have to have the reference of the Livedata you are mapping or transferring to another form of Livedata.
It is like a chain of observation, All LiveData in the chain should be observed or should have some kind of observer down the line, The main use-case is to transform some form of livedata to something you want, For Example:
class YourRepository{ // your repo, that connected to a network that keeps up to date some data
val IntegerResource: LiveData<Int> = SomeRetrofitInstance.fetchFromNetwork() //updating some resource from network
}
class YourViewModel{
val repository = YourRepository()
//this will start observe the repository livedata and map it to string resource
var StringResource: Livedata<String> = Transformations.map( repository.IntegerResource ) { integerValue ->
integerValue.toString()
}
My Point is you have to keep alive the LiveData you are transforming. Hope helped.
I have a Fragment with a RecyclerView in it. I use a ViewModel to hold the LiveData to show from a Room database and try to update the RecyclerView by observing the data in the ViewModel. But the Observer only ever gets called once when I open the fragment. I update the Room databse from a different Fragment than the Observer is on.
Wheter I add a new Event or delete or update one, the Observer never gets called! How can I get the Observer to be called properly? Where is my mistake?
Fragment
The code in onViewCreated does not work in onCreate, it return null on the line val recyclerview = upcoming_recycler.
You also see at the end of onViewCreated where I open a new fragment, from which the database gets updated. Note that the UpcomingFragment is in a different FragmentLayout than the EventEditFragment!
class UpcomingFragment : Fragment(R.layout.fragment_upcoming) {
private val clubDb by lazy {
ClubDatabase.getClubDatabase(requireContext().applicationContext)
}
private val eventAdapter = EventAdapter(null, this)
private val upcomingViewModel: UpcomingViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recyclerView = upcoming_recycler
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.setHasFixedSize(true)
upcomingViewModel.eventsToShow.observe(viewLifecycleOwner, Observer { events ->
Log.d(TAG, "Live data changed in upcomingfragment!!!")
eventAdapter.setData(events.toTypedArray())
})
recyclerView.adapter = eventAdapter
// add a new Event
upcoming_fab.setOnClickListener {
parentFragmentManager.beginTransaction()
.replace(R.id.main_fragment_layout_overlay, EventEditFragment())
.addToBackStack(EVENT_EDIT_FRAGMENT)
.commit()
}
// and more stuff...
}
//the rest of the class
}
ViewModel
class UpcomingViewModel(application: Application) : ViewModel() {
val eventsToShow: LiveData<List<Event>>
init {
val roundToDay = SimpleDateFormat("dd.MM.yyy", Locale.GERMAN)
var today = Date()
today = roundToDay.parse(roundToDay.format(today))!!
val tomorrow = Date(today.time + 86400000L)
eventsToShow = ClubDatabase.getClubDatabase(application.applicationContext).clubDao()
.getEventsByClubIdAfterDate(CURRENT_CLUB_ID, tomorrow)
}
}
EventAdapter
class EventAdapter(
private var dataSet: Array<Event>?,
private val onEventItemClickListener: OnEventItemClickListener
) : RecyclerView.Adapter<EventAdapter.EventViewHolder>() {
class EventViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.event_item_layout, parent, false)
return EventViewHolder(view)
}
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
// show the item & add onEventItemClickListener for updating
}
fun setData(new: Array<Event>) {
this.dataSet = new
this.notifyDataSetChanged()
}
override fun getItemCount(): Int {
return dataSet?.size ?: 0
}
}
Database
#Database(
entities = [Event::class, Member::class, RequiredMembersForEvents::class, AttendedMembersForEvents::class],
version = 9,
exportSchema = false
)
#TypeConverters(Converters::class)
abstract class ClubDatabase : RoomDatabase() {
abstract fun clubDao(): ClubDao
companion object {
#Volatile
private var INSTANCE: ClubDatabase? = null
fun getClubDatabase(context: Context): ClubDatabase {
return INSTANCE ?: synchronized(this) {
val instance = INSTANCE
return if (instance != null) {
instance
} else {
Room.databaseBuilder(
context.applicationContext,
ClubDatabase::class.java,
"club-db"
)
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build()
}
}
}
}
}
DAO
#Dao
interface ClubDao {
#Query("SELECT * FROM events WHERE clubId = :clubId AND dateTimeFrom > :date ORDER BY dateTimeFrom ASC")
fun getEventsByClubIdAfterDate(clubId: String, date: Date): LiveData<List<Event>>
// the rest of the DAO
}
Check your database singleton implementation, since variable INSTANCE there - is always null. You should set it at first time when you've got the instance of the class. Otherwise your app has a deal with different instances of your Database class.
Probably that causes a problem, when though some changes were made to database, but LiveData's observer for these changes was not triggered.
I am implementing a RecyclerView in a fragment. The XML should be correct since I tried it with my hard-coded data, and the API call does return the correct json data from the server according to the Log in the console. The problem is that the RecyclerView adapter does not get any data from my Observable. Here is my implementation
In PostDataService interface I used Retrofit to get an Observable>
interface PostDataService {
#GET(".")
fun getPosts(
#Query(value = "offset") offset: Long = 0,
#Query(value = "limit") limit: Long = 10,
#Query(value = "subscribedOnly") subscribedOnly: Boolean = false
): Observable<List<Post>>
companion object {
val retrofit: PostDataService = Retrofit.Builder()
.baseUrl("http:aws/api/post/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
.create(PostDataService::class.java)
}
}
In PostListRepository, I used RxJava operators to get the LiveData
class PostListRepository {
private val postListLiveData: MutableLiveData<List<Post>> = MutableLiveData()
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
fun getPostListLiveData(): MutableLiveData<List<Post>> {
val postList: MutableList<Post> = ArrayList()
val retrofitInstance = PostDataService.retrofit
val postListObservable = retrofitInstance.getPosts()
compositeDisposable.add(
postListObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMapIterable { it }
.subscribeWith(object : DisposableObserver<Post>() {
override fun onError(e: Throwable) {
// if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(post: Post) {
postList.add(post)
}
override fun onComplete() {
postListLiveData.postValue(postList)
}
})
)
return postListLiveData
}
fun clear() {
compositeDisposable.clear()
}
}
In PostListViewModel, I passed the LiveData from the repository into this ViewModel.
class PostListViewModel : ViewModel() {
private var postListRepository: PostListRepository = PostListRepository()
fun getPostList(): MutableLiveData<List<Post>> {
return postListRepository.getPostListLiveData()
}
fun clear() {
postListRepository.clear()
}
}
Here is the Fragment that contains the RecyclerView. I think the .oberserve function in getPostList() is not called since I tried Log it but got nothing.
class PostListFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var swipeLayout: SwipeRefreshLayout
private lateinit var postListViewModel: PostListViewModel
private val postListAdapter = PostRecyclerViewAdapter()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout.view_post_list, container, false)
recyclerView = rootView.findViewById(R.id.postRecyclerView)
recyclerView.apply {
setHasFixedSize(true)
addItemDecoration(VerticalSpaceItemDecoration(36))
layoutManager = LinearLayoutManager(context)
adapter = postListAdapter
}
postListViewModel = ViewModelProviders.of(this).get(PostListViewModel::class.java)
getPostList()
swipeLayout = rootView.findViewById(R.id.swipeLayout)
swipeLayout.setColorSchemeResources(R.color.colorPrimary)
swipeLayout.setOnRefreshListener {
getPostList()
swipeLayout.isRefreshing = false
}
return rootView
}
override fun onDestroy() {
super.onDestroy()
postListViewModel.clear() // to avoid memory leak
}
private fun getPostList() {
postListViewModel.getPostList().observe(this, Observer<List<Post>> { resource ->
postListAdapter.setPostList(resource)
postListAdapter.notifyDataSetChanged()
})
}
}
Here is the adapter for the RecyclerView:
class PostRecyclerViewAdapter : RecyclerView.Adapter<PostViewHolder>() {
private var postList: List<Post> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
// create a new view
val postView = PostView(parent.context)
// set the view's size, margins, paddings and layout parameters
return PostViewHolder.from(postView)
}
override fun getItemCount(): Int = postList.size
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val curPost = postList[position]
holder.postView.apply {
setPostOwnerDisplayName(curPost.content.userDisplayedName)
setPostOwnerRole(curPost.content.role)
setPostOwnerAvatar(R.mipmap.ic_launcher_round)
setPostText(curPost.content.text)
setPostImage(curPost.content.smallMediaPaths[0])
setLikeState(curPost.liked)
setBookmarkState(curPost.bookmarked)
}
}
fun setPostList(postList: List<Post>) {
this.postList = postList
}
}
As I mentioned above, I think the .oberserve function in getPostList() in PostListFragment is not called since I tried Log it but got nothing, so there is no data passed into the RecyclerView. Can anyone help me find the reason why it's not being called, or why it's not getting the data from the ViewModel?
I wouldn't think of this is related to your issue, but your code has potential problems.
To move observe part to onActivityCreated would be better to ensure view is created.
when your fragment view is re-created, a new Observer will be added, while previous one still alive, because your Observer is anonymous. So, you have to manage the observers to prevent it.
I just found out that I forgot to catch the exception in RxJava onNext() in case to get the moshi serialization error. After getting that, I got some moshi conversion errors.
Posted it in case anyone carelessly forgot to catch the moshi error.
Thanks!
Trying to create room database with two related table one to one. Conncetion via foreign key work, but i found difficulties getting data with live data, I don't know if this correct to get data.
class GraphFragment : Fragment() {
private lateinit var graphVM: GraphVM
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_graph, container, false)
graphVM = ViewModelProviders.of(this).get(GraphVM::class.java)
graphVM.graphFiles.observe(this, Observer<List<GraphEntity>> {
it?.let {
graphVM.getDataFiles( it.map { it.fileID })
}
})
graphVM.dataFiles.observe(this, Observer<List<FileEntity>> {
it?.let {
// work with FileEntity
}
})
}
Viewmodel
class GraphVM(application: Application) : AndroidViewModel(application) {
private var graphFilesRepository: GraphRepository = GraphRepository(application)
private var fileRepository: FileRepository = FileRepository(application)
var graphFiles: LiveData<List<GraphEntity>> = MutableLiveData()
private set
var dataFiles: LiveData<List<FileEntity>> = MutableLiveData()
private set
init {
// this work
graphFiles = graphFilesRepository.getAllFiles()
}
fun getDataFiles(listOfFileIDs: List<Long?>) {
// this not
dataFiles = fileRepository.getFilesDataByID(listOfFileIDs)
}
}
FileRepository
class FileRepository(application: Application) {
private var fileDao: FileDao
init {
val database: FileDatabase = FileDatabase.getInstance(application)!!
fileDao = database.fileDao()
}
/..
../
fun getFilesDataByID(listOfFileIDs: List<Long?>): LiveData<List<FileEntity>> {
return fileDao.queryFilesEntityByID(listOfFileIDs)
}
}
Dao
#Dao
interface FileDao {
/..
../
#Query("SELECT * FROM file_data WHERE id IN (:listOfFileIDs)")
fun queryFilesEntityByID(listOfFileIDs : List<Long?>): LiveData<List<FileEntity>>
}
So, when I have assignment in init, live data is trigger correctly, but when I try to:
graphVM.getDataFiles( it.map { it.fileID })
Livedata is assignment, but don't trigger. I know it is assignment correctly, because when I remove, change values from FileRepository, livedata recive onChange and observer is inform. I would like to know is there any way to fix this, so I can use livedata to receive values from room database while assignments.
#PS found the problem. When I am trying to getDataFiles in Observer{...}, it dosen't work, but when I am calling function from onCreateView{...}, it work.
Any solution?
The main issue here is you need observe and update one instance of LiveData, but call of getDataFiles() override dataFiles field.
Prefer solution is provide request LiveData to client and observe updates on client side:
class GraphVM(application: Application) : AndroidViewModel(application) {
//...
fun getDataFiles(listOfFileIDs: List<Long?>): LiveData<List<FileEntity>> {
return fileRepository.getFilesDataByID(listOfFileIDs)
}
}
If this approach is not appropriate, you can use MediatorLiveData to switch data sources:
class GraphVM(application: Application) : AndroidViewModel(application) {
private var fileRepository: FileRepository = FileRepository(application)
private val dataFiles = MediatorLiveData<List<FileEntity>>()
private var request: LiveData<List<FileEntity>>? = null
// expose dataFiles as LiveData
fun getDataFiles(): LiveData<List<FileEntity>> = dataFiles
#MainThread
fun getDataFiles(listOfFileIDs: List<Long?>) {
// create new request to repository
val newRequest = fileRepository.getFilesDataByID(listOfFileIDs)
val observer = Observer<List<FileEntity>> { list ->
// remove previous subscription
request?.run { dataFiles.removeSource(this) }
// setup fresh data
dataFiles.value = list
// remember last request
request = newRequest
}
// register new observable data source (LiveData)
dataFiles.addSource(newRequest, observer)
}
}