Using Kotlin, Retrofit and Coroutines, I have defined an interface to get data from a remote server and most importantly pass the id of a selected RecyclerView item back to the server.
interface CourseService {
#GET("/mobile/feed/course_data.php")
suspend fun getCourseData(#Query("pathName") pathName: String): Response<List<Course>>
}
Here, i get the id of the selected item from a RecyclerView from my MainFragment and store it in "selectedItem" variable.
override fun onPathItemClick(path: Path) {
viewModel.selectedItem.value = path
selectedItem= viewModel.selectedItem.value!!.path_id
navController.navigate(R.id.action_mainFragment_to_courseFragment)
}
I pass the value of selected item to the getCourseData() function
class CourseRepository(val app: Application) {
val courseData = MutableLiveData<List<Course>>()
init {
CoroutineScope(Dispatchers.IO).launch {
callWebService()
}
}
#WorkerThread
suspend fun callWebService() {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
val serviceData = service.getCourseData(selectedItem).body() ?: emptyList()
courseData.postValue(serviceData)
}
}
But i get no results and it seems as though the value passed to getCourseData() function is null, but when checking the log is does have a value.
so if i give it a predefined value anywhere in my code like below, everything works completely fine
selectedItem= "MOB001"
val serviceData = service.getCourseData(selectedItem).body() ?: emptyList()
However, i cannot give it a fixed value prior to runtime because the value is retrieved when the user selects an item from a RecyclerView.
These are my multiple logs:
2020-05-01 13:56:30.431 23843-23843/ I/mylog: Main Fragment before item click: selectedItem =
2020-05-01 13:56:37.757 23843-23843/ I/mylog: Main Fragment after item click: selectedItem = WEB001
2020-05-01 13:56:37.763 23843-23843/ I/mylog: Course Fragment onCreateView(): selectedItem = WEB001
2020-05-01 13:56:37.772 23843-23901/ I/mylog: Course Fragment CourseRepository: selectedItem = WEB001
How can i overcome this issue?
You should call your CourseRepository's suspend function callWebService inside your ViewModel. Here is your repository:
class CourseRepository(val app: Application) {
suspend fun callWebService(path: Path): List<Course> {
return withContext(Dispatchers.IO) {
val retrofit = Retrofit.Builder().baseUrl(WEB_SERVICE_URL).addConverterFactory(MoshiConverterFactory.create()).build()
val service = retrofit.create(CourseService::class.java)
service.getCourseData(path.path_id).body() ?: emptyList()
}
}
}
Then you should call your repository function in your ViewModel as follows:
fun getCourseData(path: Path): LiveData<List<Course>> {
val response = MutableLiveData<List<Course>>()
viewModelScope.launch {
response.postValue(repository.callWebService(path))
}
return response
}
Then call viewModel. getCourseData(path) from your Activity or Fragment or anywhere when you get valid Path value.
Don't forget to include implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file.
Your code seems to be correct, however, it is highly possible that your RecyclerView is being populated the first time and and evertime you go back and choose another path it is being populated with the same data and view.
Therefore, your attentions should be focused on why the data is not being fetched again, which is the cause of the RecyclerView and Fragment holding on to the same first view.
After days of thinking my code was wrong, it turned out that my RecyclerView adapter was loading the same view everytime i wen back to select a different path becuase my RecyclerView was being inflated in the onCreateView() function which is only called once only, when a fragment is inflated the first time.
class CourseFragment : Fragment(),
CourseRecyclerAdapter.CourseItemListener {
private lateinit var viewModel: CourseViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var navController: NavController
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_course, container, false)
recyclerView = view.findViewById(R.id.courseRecyclerView)
navController = Navigation.findNavController(requireActivity(), R.id.nav_host )
viewModel = ViewModelProvider(requireActivity()).get(CourseViewModel::class.java)
viewModel.courseData.observe(viewLifecycleOwner, Observer {
val adapter =
CourseRecyclerAdapter(
requireContext(),
it,
this
)
recyclerView.adapter = adapter
} )
return view
}
override fun onCourseItemClick(course: Course) {
viewModel.selectedCourse.value = course
navController.navigate(R.id.action_courseFragment_to_detailFragment)
}
}
Related
I'm making a screen similar to the image.
The data set in advance is taken from the Room DB and the data is set for each tab.
Each tab is a fragment and displays the data in a RecyclerView.
Each tab contains different data, so i set Tab to LiveData in ViewModel and observe it.
Therefore, whenever tabs change, the goal is to get the data for each tab from the database and set it in the RecyclerView.
However, even if I import the data, it is not set in RecyclerView.
I think the data comes in well even when I debug it.
This is not an adapter issue.
What am I missing?
WorkoutList
#Entity
data class WorkoutList(
#PrimaryKey(autoGenerate = true)
val id: Long = 0,
val chest: List<String>,
val back: List<String>,
val leg: List<String>,
val shoulder: List<String>,
val biceps: List<String>,
val triceps: List<String>,
val abs: List<String>
)
ViewModel
class WorkoutListViewModel(application: Application) : AndroidViewModel(application){
private var _part :MutableLiveData<BodyPart> = MutableLiveData()
private var result : List<String> = listOf()
private val workoutDao = WorkoutListDatabase.getDatabase(application).workoutListDao()
private val workoutListRepo = WorkoutListRepository(workoutDao)
val part = _part
fun setList(part : BodyPart) : List<String> {
_part.value = part
viewModelScope.launch(Dispatchers.IO){
result = workoutListRepo.getWorkoutList(part)
}
return result
}
}
Repository
class WorkoutListRepository(private val workoutListDao: WorkoutListDao) {
suspend fun getWorkoutList(part: BodyPart) : List<String> {
val partList = workoutListDao.getWorkoutList()
return when(part) {
is BodyPart.Chest -> partList.chest
is BodyPart.Back -> partList.back
is BodyPart.Leg -> partList.leg
is BodyPart.Shoulder -> partList.shoulder
is BodyPart.Biceps -> partList.biceps
is BodyPart.Triceps -> partList.triceps
is BodyPart.Abs -> partList.abs
}
}
}
Fragment
class WorkoutListTabPageFragment : Fragment() {
private var _binding : FragmentWorkoutListTabPageBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: WorkoutListAdapter
private lateinit var part: BodyPart
private val viewModel: WorkoutListViewModel by viewModels()
companion object {
#JvmStatic
fun newInstance(part: BodyPart) =
WorkoutListTabPageFragment().apply {
arguments = Bundle().apply {
putParcelable("part", part)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let { bundle ->
part = bundle.getParcelable("part") ?: throw NullPointerException("No BodyPart Object")
}
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWorkoutListTabPageBinding.inflate(inflater, container, false)
binding.apply {
adapter = WorkoutListAdapter()
rv.adapter = adapter
}
val result = viewModel.setList(part)
// Set data whenever tab changes
viewModel.part.observe(viewLifecycleOwner) { _ ->
// val result = viewModel.setList(part)
adapter.addItems(result)
}
return binding.root
}
} viewModel.part.observe(viewLifecycleOwner) { _ ->
adapter.addItems(result)
}
return binding.root
}
}
The problem you are seeing is that in setList you start an asynchronous coroutine on the IO thread to get the list, but then you don't actually wait for that coroutine to run but just return the empty list immediately.
One way to fix that would be to observe a LiveData object containing the list, instead of observing the part. Then, when the asynchronous task is complete
you can post the retrieved data to that LiveData. That would look like this in the view model
class WorkoutListViewModel(application: Application) : AndroidViewModel(application) {
private val _list = MutableLiveData<List<String>>()
val list: LiveData<List<String>>
get() = _list
// "part" does not need to be a member of the view model
// based on the code you shared, but if you wanted it
// to be you could do it like this, then
// call "viewModel.part = part" in "onCreateView". It does not need
// to be LiveData if it's only ever set from the Fragment directly.
//var part: BodyPart = BodyPart.Chest
// calling getList STARTS the async process, but the function
// does not return anything
fun getList(part: BodyPart) {
viewModelScope.launch(Dispatchers.IO){
val result = workoutListRepo.getWorkoutList(part)
_list.postValue(result)
}
}
}
Then in the fragment onCreateView you observe the list, and when the values change you add them to the adapter. If the values may change several times you may need to clear the adapter before adding the items inside the observer.
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//...
// Set data whenever new data is posted
viewModel.list.observe(viewLifecycleOwner) { result ->
adapter.addItems(result)
}
// Start the async process of retrieving the list, when retrieved
// it will be posted to the live data and trigger the observer
viewModel.getList(part)
return binding.root
}
Note: The documentation currently recommends only inflating views in onCreateView and doing all other setup and initialization in onViewCreated - I kept it how you had it in your question for consistency.
I'm a rookie Android developer, and could use a little guidance regarding traversing a LiveData List in the ViewModel.
I am basing my app on the MVVM design, and it is simply scanning folders for images, and adding some folders to a favourites list I store in a database. During the scans, I need to check with the stored favourites to see if any of the scanned folders are favourites.
It is the "check against the stored favourites" part that gives me trouble.
Here are the relevant bits from my fragment:
class FoldersFragment : Fragment(), KodeinAware {
override val kodein by kodein()
private val factory: FoldersViewModelFactory by instance()
private var _binding: FragmentFoldersBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: FoldersViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFoldersBinding.inflate(inflater, container, false)
val root: View = binding.root
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(FoldersViewModel::class.java)
binding.rvFolderList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = FolderItemAdapter(listOf(), viewModel)
binding.rvFolderList.adapter = adapter
viewModel.getFolderList().observe(viewLifecycleOwner, {
adapter.folderItems = it
binding.rvFolderList.adapter = adapter // Forces redrawing of the recyclerview
})
...
}
Now, that observer work just fine - it picks up changes and my RecyclerView responds with delight; all is well.
Here are the relevant bits from my RecyclerView adapter:
class FolderItemAdapter(var folderItems: List<FolderItem>, private val viewModel: FoldersViewModel):
RecyclerView.Adapter<FolderItemAdapter.FolderViewHolder>() {
private lateinit var binding: FolderItemBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
binding = FolderItemBinding.inflate(LayoutInflater.from(parent.context))
val view = binding.root
return FolderViewHolder(view)
}
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
val currentItem = folderItems[position]
...
if (viewModel.isFavourite(currentItem)) {
// do stuff
}
...
}
}
And with that, my problem; the check viewModel.isFavourite(currentItem)always returns false.
The implementation in my ViewModel is:
class FoldersViewModel(private val repository: FoldersRepository) : ViewModel() {
fun getImageFolders() = repository.getImageFolders()
fun isFavourite(item: FolderItem): Boolean {
var retval = false
getImageFolders().value?.forEach {
if (it.path == item.path) {
retval = true
}
}
}
}
The `getImageFolders() function is straight from the repository, which again is straight from the Dao:
#Dao
interface FoldersDao {
#Query("SELECT * FROM image_folders")
fun getImageFolders(): LiveData<List<FolderItem>>
}
My problem is that I simply can't traverse that list of favourites in the ViewModel. The isFavourite(item: FolderItem) function always returns false because getImageFolders().value always is null. When I check getImageFolders() it is androidx.room.RoomTrackingLiveData#d0d6d31.
And the conundrum; the observer is doing the exact same thing? Or isn't it?
I suspect I am not understanding something basic here?
Your getImageFolders() function retrieves something asynchronously from the database, because you specified that it returns a LiveData. When you get the LiveData back, it will not immediately have a value available. That's why your .value?.forEach is never called. value is still null because you're trying to read it immediately. A LiveData is meant to be observed to obtain the value when it arrives.
There are multiple ways to make a DAO function return something without blocking the current thread. (Handy table here.) Returning a LiveData is one way, but it's pretty awkward to use if you only want one value back. Instead, you should use something from the One-shot read row in the linked table.
If you aren't using RxJava or Guava libraries, that leaves a Kotlin coroutines suspend function as the natural choice.
That would make your Dao look like:
#Dao
interface FoldersDao {
#Query("SELECT * FROM image_folders")
suspend fun getImageFolders(): List<FolderItem>
}
And then your ViewModel function would look like:
suspend fun isFavourite(item: FolderItem): Boolean {
return getImageFolders().any { it.path == item.path }
}
Note that since it is a suspend function, it can only be called from a coroutine. This is necessary to avoid blocking the main thread. If you're not ready to learn coroutines yet, you can replace this function with a callback type function like this:
fun isFavoriteAsync(item: FolderItem, callback: (Boolean)->Unit) {
viewModelScope.launch {
val isFavorite = getImageFolders().any { it.path == item.path }
callback(isFavorite)
}
}
and at the call site use it like
viewModel.isFavoriteAsync(myFolderItem) { isFavorite ->
// do something with return value when it's ready here
}
your getImageFolder() is an expensive function so
getImageFolders().value?.forEach {
if (it.path == item.path) {
retval = true
}
}
in this part the value is still null that is why it returns false.
the solution is to make sure the value is not null. Do not check null inside isFavorite function instead call isFavorite() function only when getImageFolder() is done the operation.
What you should do is something like this
observe the liveData of imageFolders
ondatachange check if the data is null or not
if it is not null update UI and use isFavourite() function
I am using nested recyclerview.
In the picture, the red box is the Routine Item (Parent Item), and the blue box is the Detail Item (Child Item) in the Routine Item.
You can add a parent item dynamically by clicking the ADD ROUTINE button.
Similarly, child items can be added dynamically by clicking the ADD button of the parent item.
As a result, this function works just fine.
But the problem is in the code I wrote.
I use a ViewModel to observe and update parent item addition/deletion.
However, it does not observe changes in the detail item within the parent item.
I think it's because LiveData only detects additions and deletions to the List.
So I put _items.value = _items.value code to make it observable when child items are added and deleted.
This way, I didn't even have to use update code like notifyDataSetChanged() in the child adapter.
In the end it is a success, but I don't know if this is the correct code.
Let me know if you have additional code you want!
In Fragment.kt
class WriteRoutineFragment : Fragment() {
private var _binding : FragmentWriteRoutineBinding? = null
private val binding get() = _binding!!
private lateinit var adapter : RoutineAdapter
private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)
adapter = RoutineAdapter(::addDetail, ::deleteDetail)
binding.rv.adapter = this.adapter
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getTabPageResult()
// RecyclerView Update
vm.items.observe(viewLifecycleOwner) { updatedItems ->
adapter.setItems(updatedItems)
}
}
private fun getTabPageResult() {
val navController = findNavController()
navController.currentBackStackEntry?.also { stack ->
stack.savedStateHandle.getLiveData<String>("workout")?.observe(
viewLifecycleOwner, Observer { result ->
vm.addRoutine(result) // ADD ROUTINE
stack.savedStateHandle?.remove<String>("workout")
}
)
}
}
private fun addDetail(pos: Int) {
vm.addDetail(pos)
}
private fun deleteDetail(pos: Int) {
vm.deleteDetail(pos)
}
}
ViewModel
class WriteRoutineViewModel : ViewModel() {
private var _items: MutableLiveData<ArrayList<RoutineModel>> = MutableLiveData(arrayListOf())
val items: LiveData<ArrayList<RoutineModel>> = _items
fun addRoutine(workout: String) {
val item = RoutineModel(workout, "TEST")
_items.value?.add(item)
// _items.value = _items.value
}
fun addDetail(pos: Int) {
val detail = RoutineDetailModel("TEST", "TEST")
_items.value?.get(pos)?.addSubItem(detail) // Changing the parent item's details cannot be observed by LiveData.
_items.value = _items.value // is this right way?
}
fun deleteDetail(pos: Int) {
if(_items.value?.get(pos)?.getSubItemSize()!! > 1)
_items.value?.get(pos)?.deleteSubItem() // is this right way?
else
_items.value?.removeAt(pos)
_items.value = _items.value // is this right way?
}
}
This is pretty standard practice when using a LiveData with a mutable List type. The code looks like a smell, but it is so common that I think it's acceptable and people who understand LiveData will understand what your code is doing.
However, I much prefer using read-only Lists and immutable model objects if they will be used with RecyclerViews. It's less error prone, and it's necessary if you want to use ListAdapter, which is much better for performance than a regular Adapter. Your current code reloads the entire list into the RecyclerView every time there is any change, which can make your UI feel laggy. ListAdapter analyzes automatically on a background thread your List for which items specifically changed and only rebinds the changed items. But it requires a brand new List instance each time there is a change, so it makes sense to only use read-only Lists if you want to support using it.
I am updating a LiveData value from a DialogFragment in the ViewModel, but not able to get the value in Fragment.
The ViewModel:
class OtpViewModel(private val otpUseCase: OtpUseCase, analyticsModel: IAnalyticsModel) : BaseViewModel(analyticsModel) {
override val globalNavModel = GlobalNavModel(titleId = R.string.otp_contact_title, hasGlobalNavBar = false)
private val _contactListLiveData = MutableLiveData<List<Contact>>()
val contactListLiveData: LiveData<List<Contact>>
get() = _contactListLiveData
private lateinit var cachedContactList: LiveData<List<Contact>>
private val contactListObserver = Observer<List<Contact>> {
_contactListLiveData.value = it
}
private lateinit var cachedResendOtpResponse: LiveData<LogonModel>
private val resendOTPResponseObserver = Observer<LogonModel> {
_resendOTPResponse.value = it
}
private var _resendOTPResponse = MutableLiveData<LogonModel>()
val resendOTPResponseLiveData: LiveData<LogonModel>
get() = _resendOTPResponse
var userSelectedIndex : Int = 0 //First otp contact selected by default
val selectedContact : LiveData<Contact>
get() = MutableLiveData(contactListLiveData.value?.get(userSelectedIndex))
override fun onCleared() {
if (::cachedContactList.isInitialized) {
cachedContactList.removeObserver(contactListObserver)
}
if (::cachedOtpResponse.isInitialized) {
cachedOtpResponse.removeObserver(otpResponseObserver)
}
super.onCleared()
}
fun updateIndex(pos: Int){
userSelectedIndex = pos
}
fun onChangeDeliveryMethod() {
navigate(
OtpVerificationHelpCodeSentBottomSheetFragmentDirections
.actionOtpContactVerificationBottomSheetToOtpChooseContactFragment()
)
}
fun onClickContactCancel() {
navigateBackTo(R.id.logonFragment, true)
}
fun retrieveContactList() {
cachedContactList = otpUseCase.fetchContactList()
cachedContactList.observeForever(contactListObserver)
}
fun resendOTP(contactId : String){
navigateBack()
cachedResendOtpResponse = otpUseCase.resendOTP(contactId)
cachedResendOtpResponse.observeForever(resendOTPResponseObserver)
}
}
The BaseViewModel:
abstract class BaseViewModel(val analyticsModel: IAnalyticsModel) : ViewModel() {
protected val _navigationCommands: SingleLiveEvent<NavigationCommand> = SingleLiveEvent()
val navigationCommands: LiveData<NavigationCommand> = _navigationCommands
abstract val globalNavModel: GlobalNavModel
/**
* Posts a navigation event to the navigationsCommands LiveData observable for retrieval by the view
*/
fun navigate(directions: NavDirections) {
_navigationCommands.postValue(NavigationCommand.ToDirections(directions))
}
fun navigate(destinationId: Int) {
_navigationCommands.postValue(NavigationCommand.ToDestinationId(destinationId))
}
fun navigateBack() {
_navigationCommands.postValue(NavigationCommand.Back)
}
fun navigateBackTo(destinationId: Int, isInclusive: Boolean) {
_navigationCommands.postValue(NavigationCommand.BackTo(destinationId, isInclusive))
}
open fun init() {
// DEFAULT IMPLEMENTATION - override to initialize your view model
}
/**
* Called from base fragment when the view has been created.
*/
fun onViewCreated() {
analyticsModel.onNewState(getAnalyticsPathCrumb())
}
/**
* gets the Path for the current page to be used for the trackstate call
*
* Override this method if you need to modify the path
*
* the page id for the track state call will be calculated in the following manner
* 1) analyticsPageId
* 2) titleId
* 3) the page title string
*/
protected fun getAnalyticsPathCrumb() : AnalyticsBreadCrumb {
return analyticsBreadCrumb {
pathElements {
if (globalNavModel.analyticsPageId != null) {
waPath {
path = PathElement(globalNavModel.analyticsPageId as Int)
}
} else if (globalNavModel.titleId != null) {
waPath {
path = PathElement(globalNavModel.titleId as Int)
}
} else {
waPath {
path = PathElement(globalNavModel.title ?: "")
}
}
}
}
}
}
The DialogFragment:
class OtpVerificationHelpCodeSentBottomSheetFragment : BaseBottomSheetDialogFragment(){
private lateinit var rootView: View
lateinit var binding: BottomSheetFragmentOtpVerificationHelpCodeSentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewModel = getViewModel<OtpViewModel>()
binding = DataBindingUtil.inflate(inflater, R.layout.bottom_sheet_fragment_otp_verification_help_code_sent, container, false)
rootView = binding.root
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val otpViewModel = (viewModel as OtpViewModel)
binding.viewmodel = otpViewModel
otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer {
it?.let { resendOtpResponse ->
if(resendOtpResponse.statusCode.equals("000")){
//valid status code
requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
}else{
//show the error model
//it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
}
}
})
}
}
I am calling the resendOTP(contactId : String) method of the viewmodel from the xml file of the DialogFragment:
<TextView
android:id="#+id/verification_help_code_sent_resend_code"
style="#style/TruTextView.SubText2.BottomActions"
android:layout_height="#dimen/spaceXl"
android:gravity="center_vertical"
android:text="#string/verification_help_resend_code"
android:onClick="#{() -> viewmodel.resendOTP(Integer.toString(viewmodel.userSelectedIndex))}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/top_guideline" />
Now whenever I try to call resendOTPResponseLiveData from the Fragment it does not gets called:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d("OtpVerify" , "OnViewCreatedCalled")
viewModel.onViewCreated()
val otpViewModel = (viewModel as OtpViewModel)
binding.lifecycleOwner = this
binding.viewmodel = otpViewModel
binding.toAuthenticated = OtpVerifyFragmentDirections.actionOtpVerifyFragmentToAuthenticatedActivity()
binding.toVerificationBtmSheet = OtpVerifyFragmentDirections.actionOtpVerifyFragmentToOtpContactVerificationCodeSentBottomSheet()
otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer {
if(it?.statusCode.equals("000")){
//valid status code
requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
}else{
//show the error model
it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
}
})
}
So what wrong I am doing here.
EDIT
Basically I need clicklistener(resend button click) in dialogfragment, and need to read it in the fragment. So I used the concept of SharedViewModel.
So I make necessary changes in the ViewModel:
private val selected = MutableLiveData<LogonModel>()
fun select(logonModel: LogonModel) {
selected.value = logonModel
}
fun getSelected(): LiveData<LogonModel> {
return selected
}
In the DialogFragment:
otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer{
otpViewModel.select(it);
})
And in the fragment where I want to read the value:
otpViewModel.getSelected().observe(viewLifecycleOwner, Observer {
Log.d("OtpVerify" , "ResendCalled")
// Update the UI.
if(it?.statusCode.equals("000")){
//valid status code
requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
}else{
//show the error model
it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
}
})
But it is still not working.
Edit:
ViewModel Source for fragment:
viewModel = getSharedViewModel<OtpViewModel>(from = {
Navigation.findNavController(container as View).getViewModelStoreOwner(R.id.two_step_authentication_graph)
})
ViewModel Source for dialogfragment:
viewModel = getViewModel<OtpViewModel>()
Being new-ish to the Jetpack library and Kotlin a few months back I ran into a similar issue, if I understand you correctly.
I think the issue here is that you are retrieving you ViewModel using the by viewModels which means the ViewModel you get back will only be scoped to the current fragments context... If you would like to share a view model across multiple parts of your application they have to be activity scoped.
So for example:
//this will only work for the current fragment, using this declaration here and anywhere else and observing changes wont work, the observer will never fire, except if the method is called within the same fragment that this is declared
private val viewModel: AddPatientViewModel by viewModels {
InjectorUtils.provideAddPatientViewModelFactory(requireContext())
}
//this will work for the ANY fragment in the current activies scope, using this code and observing anywhere else should work, the observer will fire, except if the method is called fro another activity
private val patientViewModel: PatientViewModel by activityViewModels {
InjectorUtils.providePatientViewModelFactory(requireContext())
}
Notice my viewModel of type AddPatientViewModel is scoped to the current fragments context only via viewModel: XXX by viewModels, any changes etc made to that particular ViewModel will only be propagated in my current fragment.
Where as patientViewModel of type PatientViewModel is scoped to the activities context via patientViewModel: XXX by activityViewModels.
This means that as long as both fragments belong to the same activity, and you get the ViewModel via ... by activityViewModels you should be able to observe any changes made to the ViewModel on a global scope (global meaning any fragment within the same activity where it was declared).
With all the above in mind if your viewModel is correctly scoped to your activity and in both fragments you retrieve the viewModel using the by activityViewModels and updating the value being observed via XXX.postValue(YYY) or XXX.value = YYY you should be able to observe any changes made to the ViewModel from anywhere within the same activity context.
Hope that makes sense, it's late here, and I saw this question just before I hit the sack!
The problem is that you are actually not sharing the ViewModel between the Fragment and the Dialog. To share instances of a ViewModel they must be retrieved from the same ViewModelStore.
The syntax you are using to retrieve the ViewModels seems to be from a third party framework. I feel like probably Koin.
If that is the case, note that in Koin, getViewModel retrieves the ViewModel from the Fragment's own ViewModelStore. So, you are retrieving the ViewModel in your DialogFragment from its own ViewModelStore. On the other hand, in your Fragment, you are retrieving it using getSharedViewModel, in which you can specify which ViewModelStore it should retrieve the ViewModel from. So you are retrieving the ViewModel from two different ViewModelStores, and so, getting two different ViewModel. Interacting with one of those does not affect the other, as they are not the same instance.
To solve it, you should retrieve the ViewModel in both your Fragment and DialogFragment from the same ViewModelStore. For example, you could use getSharedViewModel in both, maybe specifying the same ViewModelStore manually at each, or even, without even specifying, which Koin will default to their Activity's one.
You could also even just use getViewModel in your Fragment, then pass its own specific ViewModelStore to the DialogFragment, in which you could then use getSharedViewModel, specifying the passed Fragment's ViewModelStore.
I'm having trouble with deleting data from my Firestore collection when the user clicks a delete button in a recyclerview. I can delete it from the recyclerview without any problems, but I'm having trouble to make the connection between the adapter, the viewmodel and the repository that handles Firestore operations.
In my adapter, I remove the item the user clicked on from the recyclerview:
class ArticleAdapter : RecyclerView.Adapter<ArticleAdapter.ViewHolder>() {
var data = mutableListOf<Product>()
set(value) {
field = value
notifyDataSetChanged()
}
override fun getItemCount() = data.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.bind(item)
holder.deleteButton.setOnClickListener {
data.removeAt(position)
notifyDataSetChanged()
}
} ...
The recyclerview is populated after a query to the Firestore collection in my viewmodel:
class ArticleViewModel(private val repository: ProductRepository) : ViewModel() {
var savedProducts: MutableLiveData<MutableList<Product>> = MutableLiveData<MutableList<Product>>()
init {
savedProducts = getProducts()
}
fun getProducts(): MutableLiveData<MutableList<Product>> {
repository.getProducts().addSnapshotListener(EventListener<QuerySnapshot> { value, e ->
if (e != null) {
savedProducts.value = null
return#EventListener
}
val savedProductsList: MutableList<Product> = mutableListOf()
for (doc in value!!) {
val item = doc.toObject(Product::class.java)
item.id = doc.id
savedProductsList.add(item)
}
savedProductsList.sortBy { i -> i.productName }
savedProducts.value = savedProductsList
})
return savedProducts
} }
In my Fragment, I'm then observing any changes that might happen to savedProducts:
class ArticleOverviewFragment : Fragment(), KodeinAware {
override val kodein: Kodein by kodein()
private val factory: ArticleViewModelFactory by instance()
private lateinit var viewModel: ArticleViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentArticleOverviewBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_article_overview, container, false)
viewModel = ViewModelProviders.of(this, factory).get(ArticleViewModel::class.java)
binding.viewModel = viewModel
val adapter = ArticleAdapter()
binding.recyclerViewGoods.adapter = adapter
viewModel.savedProducts.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.data = it
}
})
...
} }
Is there a way that I can observe/save the ID of the deleted item in my adapter and "transfer" that ID from the adapter to the UI where I call a function declared in the viewmodel whenever that field holding the ID is populated? Or should I directly access the viewmodel from the adapter? Somehow, that feels kinda wrong...
Declare one local variable
var removedPosition : Int ? = null
then update this variable into onClick event of deleteButton
holder.deleteButton.setOnClickListener {
data.removeAt(position)
removedPosition = position
notifyDataSetChanged()
}
Please make one method in Adapter (ArticleAdapter)
fun getRemoveItemPosition() : Int {
var position = removedPosition
return position;
}
which return the position of removed Item and call that method in UI(ArticleOverviewFragment) where you will require to get position of removed item from recyclerview
var removedItemPosition = adapter.getRemoveItemPosition()
Now you will get value of remove item Position using variable called removedItemPosition
So You can get Position of removed Item in UI where you can call a function declared in the viewmodel (ArticleViewModel) to delete particular item in firestore collection.