I have a fragment with a loading overlay and a loading progress bar, from time to time it crashes throwing me this exception:
It's visibility is changing by overrided method:
class ObservableProgressBar #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : ContentLoadingProgressBar(context, attrs) {
var onVisibilityChangedListener: ((Int) -> Unit)? = null
override fun setVisibility(visibility: Int) {
super.setVisibility(visibility)
onVisibilityChangedListener?.invoke(visibility)
}
}
It is a schrinked code of the fragment where the problem appears randomly:
class InitResetPasswordSessionFragment : BaseFragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var authenticationViewModel: AuthenticationViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(
R.layout.fragment_init_authentication_session,
container,
false
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
appComponent.inject(this)
authenticationViewModel = getViewModelFromActivity(viewModelFactory)
authenticationAvailabilityLoadingProgressBar.onVisibilityChangedListener = {
authenticationAvailabilityLoadingOverlay.visibility = it
}
authenticationViewModel.authenticationSessionState.observe(viewLifecycleOwner) {
when (it) {
is AuthenticationViewModel.AuthenticationSessionState.Idle -> {
findNavController().navigateUp()
}
is AuthenticationViewModel.AuthenticationSessionState.Initialized -> {
enableViews(true)
authenticationAvailabilityLoadingProgressBar.hide()
}
is AuthenticationViewModel.AuthenticationSessionState.Creating -> {
enableViews(false)
authenticationAvailabilityLoadingProgressBar.show()
}
}
}
}
I don't know why it is appearing, I guess it might be because of fragment lifecycle, but I'm not sure
Apparently the problem was solved by removing custom listener of this loading overlay in onDestroy() method, because it was trying to get a reference for this and a related view before the fragment was in Resumed state.
Related
My app should be able to change color of one part of my screen in a Fragment (called Color Preview) from Color Picker that is DialogFragment and appears once a Button is clicked. I used the same ViewModel with MutableLiveData in it within Fragment and DialogFragment. However I am not able to get Data about color in Fragment when I pick some color in DialogFragment. Code below.
Dialog Fragment:
class ColorFragment : DialogFragment() {
private var _binding: FragmentColorBinding? = null
private val binding get() = _binding!!
private val viewModel: MainViewModel by lazy {
getViewModel {
MainViewModel()
}
}
#SuppressLint("DialogFragmentInsteadOfSimpleDialog")
override fun onStart() {
super.onStart()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
FragmentColorBinding.inflate(inflater, container, false).apply {
_binding = this
lifecycleOwner = this#ColorFragment
mainViewModel = getViewModel()
return root
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.color02.setOnClickListener {
viewModel.currentLightProfileColor.value = "#00ffff"
}
}
Main Fragment (where color should be shown) looks like this:
class MainFragment : Fragment() {
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
private var viewCreated = false
private val viewModel: MainViewModel by lazy {
getViewModel {
MainViewModel()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
FragmentMainBinding.inflate(inflater, container, false).apply {
_binding = this
lifecycleOwner = this#MainFragment
mainViewModel = getViewModel()
executePendingBindings()
viewCreated = true
return root
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.colorPreview.holder?.addCallback(this)
viewModel.currentLightProfileColor.observe(viewLifecycleOwner) { color ->
Color.parseColor(color.toString()).let { color ->
Timber.d("color LiveDataGet: ${color}")
binding.colorPreview.setBackgroundColor(color)
}
}
binding.color.setOnClickListener {
findNavController().navigate(R.id.ColorFragment)
}
}
In my ViewModel there is only MutableLiveData for color:
class MainViewModel : ViewModel() {
val currentLightProfileColor = MutableLiveData<String>()}
I dont get any errors but once I click on color2 I should get from Timber in Log: "color LiveDataGet: #00ffff" but I dont and color preview does not change color.
Did I miss something?
I would appreciate if someone could quickly take a look at my code. Thanks!
I found a solution. In Kotlin, data is not shared if viewModels() is instantiated without activityViewModels().
So I changed my code, instead of:
private val viewModel: MainViewModel by lazy {
getViewModel {
LightmvpViewModel()
}
}
I wrote:
private val viewModel: MainViewModel by activityViewModels()
With that simple change, everything should work.
I created a bottom sheet dialog and am implementing cancel function.
It has to be not dismissed when outside of the dialog is touched, but the bottom navigation should catch the touch. After a long trial, I realized I cannot use "setCanceledOnTouchOutside" on Kotlin. "isCancelable=false" is working but cannot use backbutton on the bottom navigation. How should I do if I want to make the bottom navigation touchable only?
Any help will be greatly appreciated
My code is here
class BiometricChangeDetectDialog: BottomSheetDialogFragment() {
private var _binding: DialogBiometricChangeDetectBinding? = null
private val binding get() = _binding!!
private lateinit var mContext: MainActivity
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context as MainActivity
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AlertDialog.Builder(requireContext()).apply {
isCancelable = false
//change here to setCanceledOnTouchOutside
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
_binding = DialogBiometricChangeDetectBinding.inflate(inflater, container, false)
val view = binding.root
binding.btnEmail.setOnClickListener {
dismiss()
findNavController().navigate(R.id.action_biometricChangeDetectDialog_to_biometricChangeEmailDialog)
}
binding.btnSms.setOnClickListener {
dismiss()
findNavController().navigate(R.id.action_biometricChangeDetectDialog_to_biometricChangeEmailDialog)
}
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
add this line in onCreateView
dialog?.setCanceledOnTouchOutside(false)
So, I am trying to migrate from kotlin synthetic to Jetpack view binding.
Here is the kotlin synthetic code (works fine) that simply set visibility to invisible of TextView in the parent activity from fragment.
import kotlinx.android.synthetic.main.activity_main.*
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_first, container, false)
requireActivity().textView.visibility = View.INVISIBLE
return view
}
}
And here is what I'm doing to migrate:
import com.mypc.myapp.databinding.FragmentFirstBinding
import com.mypc.myapp.databinding.ActivityMainBinding
class FirstFragment : Fragment() {
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textView.visibility = View.INVISIBLE
binding.textview.setOnClickListener {
Navigation.findNavController(view).navigate(R.id.goto_secondfragment)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
I'm getting error as 'Unsolved reference' at 'textview':
binding.textView.visibility = View.INVISIBLE
And at:
binding.textview.setOnClickListener {
Navigation.findNavController(view).navigate(R.id.goto_secondfragment)
}
Obviously the compiler is not able to find TextView that is in Activity
I've added this line:
import com.mypc.myapp.databinding.ActivityMainBinding
Since your binding is private to MainActivity you can refer to your textView from the MainActivity only. To show/hide this view from FirstFragment you can create a public function in MainActivity and call it from your FirstFragment.
class MainActivity: AppCompatActivity {
private var _binding: ActivityMainBinding? = null
private val binding get() = _binding!!
fun showHideTextView(visible: Boolean) {
binding.textView.isVisible = visible
}
}
And in your fragment, you can call:
(requireActivity() as MainActivity).showHideTextView(false) // This will hide the textView
First of all, you should define instance of activity view binding in baseActivity which is a parent class of your MainActivity, and then define method to change your text view like 'showTextView' , after that in the base fragment class initalize base activity instance with casting context object in onAttach method.
I provide you some code:
abstract class BaseRegisterActivity : BaseActivity() {
//---
protected lateinit var binding: ActivityRegisterBinding
private val navHostFragment by lazy {
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as
NavHostFragment
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_register)
//---
}
fun showTextView() {
binding.textView.visibility = View.VISIBLE
binding.textView.setOnClickListener {
val fragment =
navHostFragment.childFragmentManager.fragments[0]
if (fragment is FirstFragment) {
//todo
}
}
}
}
abstract class BaseFragment : Fragment(), Injectable {
//--
lateinit var baseActivity: BaseRegisterActivity
override fun onAttach(context: Context) {
super.onAttach(context)
baseActivity = context as BaseRegisterActivity
}
//--
}
class ShopFragment : Fragment() {
var binding:FragmentShopBinding ?= null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentShopBinding.inflate(inflater,container,false)
return binding?.root
}
I have a fragment that uses the delegation pattern when a button is clicked.
class FutureMeetingEventViewFragment #Inject constructor(): Fragment() {
#Inject
lateinit var bundleUtilityModule: BundleUtilityModule
lateinit var parcelableMeetingEvent: ParcelableMeetingEvent
private var delegate: IEventDetailsDelegate? = null
override fun onAttach(context: Context) {
super.onAttach(context)
if(context is IEventDetailsDelegate) {
delegate = context
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val binding: ViewDataBinding = DataBindingUtil.inflate<ViewDataBinding>(inflater, R.layout.layout_future_meeting_event_fragment, container,false)
DaggerUtilityModuleComponent.create().inject(this)
this.parcelableMeetingEvent = this.bundleUtilityModule.getTypeFromBundle(BundleData.MEETING_EVENT_DATA.name, arguments)
binding.setVariable(BR.meetingEvent, this.parcelableMeetingEvent)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
this.setCTAClickEvent()
}
private fun setCTAClickEvent() {
future_event_card.setOnClickListener {
delegate?.onEventClicked(this, this.parcelableMeetingEvent)
}
}
}
A problem I can see in the unit test is that when I click the button, because the IEventDetailsDelegate field will be null, the test will always fail. My unit test so far is simply testing if the correct data is displayed on the view:
#RunWith(AndroidJUnit4::class)
class GivenAFutureMeetingEventFragmentIsDisplayed {
private var fragmentId: Int = 0
private lateinit var fragmentArgs: Bundle
private lateinit var scenario: FragmentScenario<FutureMeetingEventViewFragment>
#Before
fun setup() {
fragmentId = FutureMeetingEventViewFragment().id
fragmentArgs = Bundle()
fragmentArgs.apply {
putParcelable(BundleData.MEETING_EVENT_DATA.name, ParcelableMeetingEvent(
"Test Description",
"Test Summary",
"12344556",
DateTime("2020-03-14T17:57:59+00:00"),
DateTime("2020-03-14T18:57:59+00:00"),
"Somewhere across the universe"
))
}
scenario = launchFragmentInContainer<FutureMeetingEventViewFragment>(
fragmentArgs,
fragmentId
)
}
#Test
fun thenACorrectlyMappedMeetingEventShouldBePassedToTheFragment() {
onView(withId(R.id.spec_future_meeting_event_start_day)).check(matches(withText("Saturday")))
onView(withId(R.id.spec_future_meeting_event_start_time)).check(matches(withText("5:57:59 PM")))
onView(withId(R.id.spec_future_meeting_event_end_time)).check(matches(withText("6:57:59 PM")))
onView(withId(R.id.spec_future_meeting_event_summary)).check(matches(withText("Test Summary")))
onView(withId(R.id.spec_future_meeting_event_description)).check(matches(withText("Test Description")))
}
#Test
fun thenTheContainerShouldBeClickable() {
onView(withId(R.id.future_event_card)).check(matches(isClickable()))
}
}
I guess there's two questions in this post:
Can I mock out a context that implements the IEventDetailsDelegate and assign it to my mock fragment?
Should I test the event on an actual activity that implements the interface?
Im using ViewModel with states and LiveData with one observer .
I have an activity and a fragment . everything works fine until Im rotating the screen
and then, the states I want to observe , clash.
What can I do in order to prevent it and make it work as expected?
I know I can add more observers but I don't want to solve it in this way , it may lead to problems with the other code.
MainActivity code :
private var appsDetailsHmap = HashMap< String , AppsDetails>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
progressBar = CircleProgressBarDialog(this)
viewModel.getState().observe(this, Observer {state->
when(state){
is SettingsViewModelStates.GetAppsDetails->initUI(state.list)
is SettingsViewModelStates.ShowDialog->progressBar.showOrHide(state.visibility)
is SettingsViewModelStates.GetCachedData->setCachedSettings(state.appDetailsHmap,state.selectedApp,state.speechResultAppName)
}
})
if (savedInstanceState==null){
viewModel.initSettingsActivityUI(appsDetailsHmap)
}
else{
viewModel.initCachedSettingsActivityUI()
}
Fragment code
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view= inflater.inflate(R.layout.fragment_added_apps, container, false)
viewModel.getCachedApplist()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getState().observe(viewLifecycleOwner, Observer {state->
when(state){
is SettingsViewModelStates.PassAppsToFragment->{
initRecyclerView(state.list)
}
}
})
}
ViewModel
private var state=MutableLiveData<SettingsViewModelStates>()
private var appsDetailsHmap=HashMap<String,AppsDetails>()
private var addedApps= HashMap<String,Drawable?>()
fun getState()=state as LiveData<SettingsViewModelStates>
fun getCachedApplist(){
if (addedApps.isEmpty()){
getAppsList()
println("empty")
}
else
state.setValue(SettingsViewModelStates.PassAppsToFragment(addedApps))
}
fun initCachedSettingsActivityUI(){
state.setValue(SettingsViewModelStates.GetCachedData(appsDetailsHmap,selectedApp,speechResultAppName))
}
fun initSettingsActivityUI(list:HashMap<String, AppsDetails>) {
appsDetailsHmap=list
state.setValue( SettingsViewModelStates.GetAppsDetails(list))
}
states:
sealed class SettingsViewModelStates {
data class GetAppsDetails(val list:HashMap<String, AppsDetails>):SettingsViewModelStates()
data class ShowDialog(val visibility: Int) : SettingsViewModelStates()
data class PassAppsToFragment(val list:HashMap<String,Drawable?>) : SettingsViewModelStates()
data class GetCachedData(val appDetailsHmap:HashMap<String,AppsDetails>,
val selectedApp: AppsDetails,
val speechResultAppName:String ) : SettingsViewModelStates()
}