Navigation Error: action/destination cannot be found from the current destination - android

I am using Navigation component to navigate between two Fragments. The landing fragment has a recycler view and the detail fragment has a view pager. I am using a call back listener to trigger navigation action from the recycler view adapter.
The action to be trigger is a zoom event, with the library ZoomHelper ZoomHelper
When the event happens the app crashes with the error above.
However it works well with onclick event listener.
View Holder
class CampaignListViewHolder<T : ViewBinding>(private val binding: T) : RecyclerView.ViewHolder(binding.root) {
var campaignDetails: CampaignDetails? = null
#SuppressLint("ClickableViewAccessibility")
fun bindTo(campaignDetails: CampaignDetails?, position: Int, listener: ItemZoomListener) {
this.campaignDetails = campaignDetails
binding as CampaignItemLayoutBinding
binding.campaignNameTv.text = campaignDetails?.name
binding.campaignImageIv.load(campaignDetails?.image?.url) {
crossfade(true)
placeholder(R.drawable.westwing_placeholder)
}
ViewCompat.setTransitionName(binding.campaignImageIv, campaignDetails?.name)
binding.root.setOnClickListener {
if (campaignDetails != null) {
listener.navigate(position)
}
}
ZoomHelper.addZoomableView(binding.campaignImageIv)
ZoomHelper.getInstance().addOnZoomScaleChangedListener(object :
ZoomHelper.OnZoomScaleChangedListener {
override fun onZoomScaleChanged(
zoomHelper: ZoomHelper,
zoomableView: View,
scale: Float,
event: MotionEvent?
) {
// called when zoom scale changed
if (campaignDetails != null && scale > 1.4) {
listener.navigate(position)
}
}
})
}
}
Landing Fragment
class LandingFragment : Fragment(R.layout.fragment_landing), ItemZoomListener, FragmentUiStateListener {
private val TAG by lazy { getName() }
private val binding by viewBinding(FragmentLandingBinding::bind)
private val campaignListViewModel: CampaignListViewModel by activityViewModels()
lateinit var campaignViewAdapter: CampaignListViewAdapter
lateinit var activityUiState: ActivityUiStateListener
lateinit var fragmentUiUpdate: FragmentUiStateListener
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
campaignViewAdapter = CampaignListViewAdapter(this)
fragmentUiUpdate = this
}
override fun onResume() {
super.onResume()
setupView()
setUpData()
}
private fun setUpData() {
setUpUiState(campaignListViewModel.campaignUiState, fragmentUiUpdate)
}
private fun setupView() {
val orientation = checkOrientation()
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.campaignLandscapeRcv?.apply {
layoutManager = GridLayoutManager(requireContext(), 2)
adapter = campaignViewAdapter
addItemDecoration(ItemSpaceDecoration(8, 2))
}
} else {
binding.campaignListRcv?.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = campaignViewAdapter
addItemDecoration(ItemSpaceDecoration(8, 1))
}
}
}
override fun navigate(position: Int) {
val direction = LandingFragmentDirections.actionLandingFragmentToCampaignDetailsFragment(position)
goto(direction)
}
I understand that one of the reason for the error is probably the zoom event calling the navigation controller multiple times but I can not figure how debug that and what could be a way around this.

As you guessed, the issue is most likely caused by the navController being fired multiple times. You can handle this by "navigating safely". Here's a sample implementation below:
fun NavController.safelyNavigate(#IdRes resId: Int, args: Bundle? = null) =
try { navigate(resId, args) }
catch (e: Exception) { Timber.e(e) }
}
You can then make your navigation call like this:
findNavController().safelyNavigate(your_id)
This way, any extra call to NavController.navigate() gets absorbed in the try and catch. Crash prevented :D

If someone had the same issues due to multiple clicks on the screen. It can be resolved by checking the current destination first before navigating
For example
Fragments A, B, and C
navigating from A to B while clicking on a button in fragment A that navigates to C might lead to crashes in some cases
for that you should check the current destination first as follows:
if(findNavController().currentDestination?.id==R.id.AFragment)
findNavController().navigate(
AFragmentDirections.actionAFragmentToCFragment()
)

If it is being called twice, then you can remove the 'ZoomHelper.OnZoomScaleChangedListener' before the navigation occurs.
I haven't tested the code below, but you can explore the library's code here https://github.com/Aghajari/ZoomHelper/blob/master/ZoomHelper/src/main/java/com/aghajari/zoomhelper/ZoomHelper.kt. You will find a method called 'removeOnZoomScaleChangedListener' which from what I understand, will remove a listener of type 'ZoomHelper.OnZoomScaleChangedListener'.
Ex.
val onZoomScaleChangedListener = object :
ZoomHelper.OnZoomScaleChangedListener {
override fun onZoomScaleChanged(
zoomHelper: ZoomHelper,
zoomableView: View,
scale: Float,
event: MotionEvent?
) {
// called when zoom scale changed
if (campaignDetails != null && scale > 1.4) {
ZoomHelper.getInstance().removeOnZoomScaleChangedListener(onZoomScaleChangedListener) // Remove this event listener to avoid multiple triggers as you only need 1.
listener.navigate(position)
}
}
}
ZoomHelper.getInstance().addOnZoomScaleChangedListener(onZoomScaleChangedListener) // You can add the listener above like this.

Related

Problem with RecyclerView in Night Mode android kotlin NullPointerException

I have a RecyclerView in my Fragment and two themes in app: Day, Night and System Default.
There is a strange problem that causes a NullPointerException. If I switch the theme to night and exit the application, and then enter it again, then a NullPointerException crashes and the application will not open again until I delete it from the phone or emulator. However, if I stay on the light theme all the time and close and open the application again, then everything will be fine.
Code for Fragment:
private var _binding: FragmentListBinding? = null
private val binding get() = _binding!!
private lateinit var rvAdapter: RvStatesAdapter
private var statesList = ArrayList<State>()
private var databaseReferenceStates: DatabaseReference? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentListBinding.inflate(inflater, container, false)
checkTheme()
initDatabase()
getStates()
binding.rvStates.layoutManager = LinearLayoutManager(requireContext())
binding.ibMenu.setOnClickListener {
openMenu()
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun getStates() {
databaseReferenceStates?.addValueEventListener(object: ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
for (stateSnapshot in snapshot.children) {
val state = stateSnapshot.getValue(State::class.java)
statesList.add(state!!)
}
rvAdapter = RvStatesAdapter(statesList)
binding.rvStates.adapter = rvAdapter
}
}
override fun onCancelled(error: DatabaseError) {
}
})
}
private fun initDatabase() {
FirebaseApp.initializeApp(requireContext());
databaseReferenceStates = FirebaseDatabase.getInstance().getReference("States")
}
private fun openMenu() {
binding.drawerLayout.openDrawer(GravityCompat.START)
binding.navigationView.setNavigationItemSelectedListener {
when (it.itemId) {
R.id.about_app -> Toast.makeText(context, "item clicked", Toast.LENGTH_SHORT).show()
R.id.change_theme -> {
chooseThemeDialog()
}
}
binding.drawerLayout.closeDrawer(GravityCompat.START)
true
}
}
private fun checkTheme() {
when (ThemePreferences(requireContext()).darkMode) {
0 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
(activity as AppCompatActivity).delegate.applyDayNight()
}
1 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
(activity as AppCompatActivity).delegate.applyDayNight()
}
2 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
(activity as AppCompatActivity).delegate.applyDayNight()
}
}
}
private fun chooseThemeDialog() {
val builder = AlertDialog.Builder(requireContext())
builder.setTitle("Choose Theme")
val themes = arrayOf("Light", "Dark", "System default")
val checkedItem = ThemePreferences(requireContext()).darkMode
builder.setSingleChoiceItems(themes, checkedItem) {dialog, which ->
when (which) {
0 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
(activity as AppCompatActivity).delegate.applyDayNight()
ThemePreferences(requireContext()).darkMode = 0
dialog.dismiss()
}
1 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
(activity as AppCompatActivity).delegate.applyDayNight()
ThemePreferences(requireContext()).darkMode = 1
dialog.dismiss()
}
2 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
(activity as AppCompatActivity).delegate.applyDayNight()
ThemePreferences(requireContext()).darkMode = 2
dialog.dismiss()
}
}
}
val dialog = builder.create()
dialog.show()
}
ThemePreferences class:
companion object {
private const val DARK_STATUS = ""
}
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
var darkMode = preferences.getInt(DARK_STATUS, 0)
set(value) = preferences.edit().putInt(DARK_STATUS, value).apply()
RecyclerView in .xml code:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvStates"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:background="#color/background"
app:layoutManager="LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tvLabelDescription"
tools:listitem="#layout/rv_state_list" />
and also code from RecyclerView Adapter:
inner class MyViewHolder(val binding: RvStateListBinding): RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(RvStateListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentItem = stateList[position]
with(holder) {
with(stateList[position]) {
binding.tvState.text = this.name
Picasso.with(itemView.context)
.load(this.image)
.into(binding.ibState, object: Callback {
override fun onSuccess() {
binding.progressBar.visibility = View.GONE
}
override fun onError() {
}
})
itemView.ibState.setOnClickListener {
val action = StatesFragmentDirections.actionListFragmentToAttractionsFragment(currentItem)
itemView.findNavController().navigate(action)
}
}
}
}
override fun getItemCount(): Int {
return stateList.size
}
Your crash is coming from that onDataChange callback - you're calling getStates (after binding has been set) but by the time the results come back and onDataChange tries to access binding, it's null again.
If I had to guess, when you call checkTheme and it calls applyDayNight, that probably does nothing if the activity is already using the theme you're applying. So if you're setting a light theme, and it's already using a light theme, no problem. (You could test this by seeing if it stops crashing if you set the system to a dark theme, assuming your app theme is a DayNight one)
But if it needs to change to a dark theme, that means recreating the Activity and Fragment. I don't know the specifics of what gets recreated now, but at the very least you're probably going to be recreating your view layout with the new theme. Which means the layout is getting destroyed, which means onDestroyView is getting called - and in there, you're setting binding to null
So I'm assuming your onDataChange callback is either arriving between layout (and binding) destruction and recreation, or the whole Fragment is getting destroyed and the callback is just calling a binding variable that's never getting restored.
Easiest fix is just to not set binding to null. Make it lateinit like Emmanuel says, and it'll get initialised/overwritten every time onCreateView is called. If the callback updates an old binding layout, it's cool, the new one will ask for an update in onCreateView anyway
Just make sure you're cleaning up the event listeners you're setting on databaseReferenceStates if you need to - if that's the reason you're clearing the binding in onDestroyView, the listener still has a reference to the fragment that holds that variable, and you could end up keeping dead ones in memory (otherwise you could just null-check binding)
You should just use
private lateinit var binding: FragmentListBinding
onCreateView()
binding = FragmentListBinding.inflate(inflater, container, false)
Don't set it to null anywhere
Also move any code not responsible to create view from onCreateView to onViewCreated
I believe your issue is not related to the theme itself, but more to fragment/activity lifecycle. If you would turn on don't keep activities on and background then resume the app it would crash.
I think the issue is that you are leaking the fragment with the addValueEventListener. Since you implemented it as an anonymous class you are not able to remove it on onStop so your fragment leaks hence getting the null pointer exception because the view was destroyed.

Is there any way to get onClickListener from View in Kotlin?

For example, I've got a complex fragment layout with some visibility changes that appear very frequently, so the state of my layout that I've set in onViewCreated will change many times during the user works with app. I want to store its state in my view model in case of activity recreation (so my layout will stay in the state before recreation). I managed to fully restore my layout with exception of my listeners. Although I can just refactor my code and reassign them manually, I just want to know if there any way to store listeners as well. Here is some abstract code to show what I'm doing and trying to archive.
class MyFragment: Fragment {
private val viewModel: MyViewModel by viewModels()
override fun onCreateView {
//binding
}
override fun onViewCreated {
if (viewModel.isStateSaved.value == true) {
restoreState()
} else {
myView1.visibility = Visibility.GONE
//i can move listeren assignment from if else but maby there are better way to do it
myView.setOnClickListener(listener) // some listener. Realization isnt really important
myView2.visibility = Visibility.VISIBLE
}
}
//abstract function to show what how my fragment layout will change during user interaction
fun onUserInteractsWithApp() {
myView1.visibility = Visibility.VISIBLE
myView1.isEnabled = false
}
override fun onStop() {
//for example fun store state just copies state of id, isEnabled, visibility and so on
//in that method i want to also get OnClickListener from a view if that possible
viewModel.storeState(myView1)
viewModel.storeState(myView2)
}
fun restoreState() {
//finding view by stored id and assigning stored values
}
}
Hello Mr George Actually i did not understand your question completely but what i get from your title of question to get clickListner from View this is answer for that
make an object of onClickLisner and assign to your view
val tempListner =object : View.OnClickListener {
override fun onClick(p0: View?) {
TODO("Not yet implemented")
}
}
..................
..................
..........
myView.setOnClickListener(tempListner)
Here is an real example from my app
private fun setOnPageChangedAction(
tab_viewpager: ViewPager?,
bottomNavigationView: BottomNavigationView
) {
tab_viewpager?.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
pageId = position
if (position == 0) {
bottomNavigationView.setSelectedItemId(R.id.homeFragment);
} else if (position == 1) {
bottomNavigationView.setSelectedItemId(R.id.dashboardFragment);
} else if (position == 2) {
bottomNavigationView.setSelectedItemId(R.id.notificationsFragment);
} else if (position == 3) {
bottomNavigationView.setSelectedItemId(R.id.profileFragment);
}
}
})
}
you can replace PageListener texts with ClickListener. It should show you that as an action.

Remove Current Fragment and Launch Another Fragment from View Model

below is my ViewModel class which accepts application:Application as parameter.I want to launch another fragment from this class.But in remove() method,how do I pass fragment.
class EmailConfirmationFragmentViewModel(application: Application) : AndroidViewModel(application) {
private lateinit var viewModelApplication: Application
init {
this.viewModelApplication = application
}
var email = MutableLiveData<String>()
private var emailMutableLiveData: MutableLiveData<UserEmail>? = null
val userEmail: MutableLiveData<UserEmail>
get() {
if (emailMutableLiveData == null) {
emailMutableLiveData = MutableLiveData<UserEmail>()
}
return emailMutableLiveData!!
}
fun onEmailChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.toString() != null && !s.toString().equals(""))
email.value = s.toString()
}
fun onConfirmClicked(view: View) {
userEmail.value = UserEmail(email.value.toString())
launchResetPasswordFragment()
}
private fun launchResetPasswordFragment() {
try {
(viewModelApplication as FragmentActivity).supportFragmentManager.beginTransaction()
.replace(R.id.fl_Wrapper, OtpVerificationFragement()).remove(viewModelApplication.applicationContext).commit()
}
catch(e:Exception)
{
Log.e("Error","$e")
}
}
}
Lifecycle events and Fragment transactions should never take place inside of a view model. As discussed in the ViewModel Overview, a "ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context." While the AndroidViewModel does introduce an anti-pattern by exposing a reference to the application, this specific use case is not an appropriate one. In situations where the view model should invoke a fragment transaction, it's most commonly handled by the general concept of an event dispatched from the view model to the Lifecycle Owner. I believe employing such a pattern can resolve your issue. While I don't know the state of your Fragment, I've devised a likely solution.
class EmailConfirmationViewModel() : ViewModel() {
val email: MutableLiveData<String> = MutableLiveData()
private val _resetFragment: MutableLiveData<Event> = MutableLiveData()
val resetFragment: LiveData<Event> = _resetFragment
val userEmail: UserEmail?
get() = email.value?.let { UserEmail(it) }
fun onEmailChanged(s: CharSequence) {
email.value = s.toString()
}
fun onConfirmClicked() {
resetFragment()
}
private fun resetFragment() {
_resetFragment.value = Event()
}
}
Where the supporting event classes could appear as such:
class Event : EventWithValue<Unit>(Unit)
open class EventWithValue<T>(
private val value: T,
) {
private var isHandled = false
fun getValueIfUnhandled(): T? = if (isHandled) {
null
} else {
handleValue()
}
private fun handleValue(): T {
isHandled = true
return value
}
}
class EventObserver<T>(
private val eventIfUnhandled: (value: T) -> Unit,
) : Observer<EventWithValue<T>?> {
override fun onChanged(event: EventWithValue<T>?) {
event?.getValueIfUnhandled()?.let { eventIfUnhandled(it) }
}
}
Through observing the event in the Fragment itself, you eliminate the need to reference any sort of view in the view model while maintaining the view model's role as the dispatcher. Here's a brief description of how you would listen to the event from your Lifecycle Owner, in this case, a Fragment.
class EmailConfirmationFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view: View? = super.onCreateView(inflater, container, savedInstanceState)
val viewModel: EmailConfirmationViewModel by viewModels()
viewModel.resetFragment.observe(viewLifecycleOwner, EventObsever {
// Call a function of the activity's viewModel (ideal), or complete the transaction here through referencing the activity directly (ill-advised)
})
return view
}
}
I think exposing userEmail is a bit of a code smell in itself. Alternatively, you could define the resetFragment event as
private val _resetFragment: MutableLiveData<EventWithValue<UserEmail>> = MutableLiveData()
val resetFragment: LiveData<EventWithValue<UserEmail>> = _resetFragment
and receive the value of the userEmail directly within the event listener featured above. This would remove the need to expose the userEmail of the view model.

LiveData not able to observe the changes

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.

KOTLIN - How to set TextViews and buttons setting from Activity to fragment

I'm new on Android, in particular on Kotlin development.
How from title, i'm trying to understand how to achieve this:
I have an Activity with some buttons and textviews. I would to implement an hidden fragment opened after 5 clicks on UI. That fragment work look like the activity. I'm able to open the fragment properly and set the layout properly. I don't know how to replace buttons activity settings from activity to fragment. I have same problem with the textview. How could I achieve it?
Thanks in Advance.
Here Activity Kotlin part that open fragment:
override fun onTouchEvent(event: MotionEvent): Boolean {
var eventaction = event.getAction()
if (eventaction == MotionEvent.ACTION_UP) {
//get system current milliseconds
var time = System.currentTimeMillis()
//if it is the first time, or if it has been more than 3 seconds since the first tap ( so it is like a new try), we reset everything
if (startMillis == 0L || (time-startMillis> 3000) ) {
startMillis=time
count=1
}
//it is not the first, and it has been less than 3 seconds since the first
else{ // time-startMillis< 3000
count++
}
if (count==5) {
// Log.d("tag","start hidden layout")
// Get the text fragment instance
val textFragment = MyFragment()
val mytostring =board_status_tv.toString()
val mArgs = Bundle()
mArgs.putString(BOARDSTATE, mytostring)
textFragment.setArguments(mArgs)
// Get the support fragment manager instance
val manager = supportFragmentManager
// Begin the fragment transition using support fragment manager
val transaction = manager.beginTransaction()
// Replace the fragment on container
transaction.replace(R.id.fragment_container,textFragment)
transaction.addToBackStack(null)
// Finishing the transition
transaction.commit()
}
return true
}
return false
}
Fragment Kotlin class:
class MyFragment : Fragment(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val parentViewGroup = linearLayout
parentViewGroup?.removeAllViews()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Get the custom view for this fragment layout
val view = inflater!!.inflate(R.layout.my_own_fragment,container,false)
// Get the text view widget reference from custom layout
val tv = view.findViewById<TextView>(R.id.text_view)
// val tv1 = view.findViewById<TextView>(R.id.board_status_tv1)
// Set a click listener for text view object
tv.setOnClickListener{
// Change the text color
tv.setTextColor(Color.RED)
// Show click confirmation
Toast.makeText(view.context,"TextView clicked.",Toast.LENGTH_SHORT).show()
}
// Return the fragment view/layout
return view
}
override fun onPause() {
super.onPause()
}
override fun onAttach(context: Context?) {
super.onAttach(context)
}
override fun onDestroy() {
super.onDestroy()
}
override fun onDetach() {
super.onDetach()
}
override fun onStart() {
super.onStart()
}
override fun onStop() {
super.onStop()
}
}
Please note that you will need to get Text before converting it to string, like that in second line.
board_status_tv .getText(). toString()
val textFragment = MyFragment()
val mytostring = board_status_tv.getText().toString()
val mArgs = Bundle()
mArgs.putString(BOARDSTATE, mytostring)
textFragment.setArguments(mArgs)
Hope this will resolve your problem

Categories

Resources