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)
Related
I have two fragments that share information with each other, in the first one I have an edit text and button widget. The second fragment is just a listview. When the user clicks the button, it displays whatever is in the edit text widget in the second fragment.
So if the user enters the text study and clicks the button the second fragment will display
Study
If the user then enters the text eat and clicks the button, the second fragment will display
Study
Eat
I am having so issues with displaying the texts
So far this is what I have done
class FirstFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.one_fragment, container, false)
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
}
return view;
}
}
class SecondFragment : Fragment() {
lateinit var viewModel: MyViewModel
#SuppressLint("MissingInflatedId")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.page3_fragment, container, false)
val valueView = v.findViewById<TextView>(R.id.textView)
return view
The problem I am having is how to display the texts
If I undestand you correctly, you want to share data between fragments? If yes, you can do that with "shared" viewModel. For example:
class FirstFragment : Fragment() {
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
binding.buttonChangeFragment.setOnClickListener {
/*
You can change data here, or in navigateWithNavController() from
activity (You already have an instance of your viewModel in activity)
*/
sharedViewModel.changeData(binding.myEditText.text.toString())
if (requireActivity() is YourActivity)
(requireActivity() as YourActivity).navigateWithNavController()
}
return binding.root
}
}
class SecondFragment : Fragment() {
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
binding.secondFragmentText.text = sharedViewModel.someData.value
return binding.root
}
}
and your activity:
class YourActivity: AppCompatActivity() {
private lateinit var binding: YourActivityBinding
private lateinit var appBarConfiguration: AppBarConfiguration
private val sharedViewModel: SharedViewModel by lazy {
ViewModelProvider(
this
)[SharedViewModel::class.java]
}
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = YourActivityBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
navController = this.findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph)
}
/*
This function is just for test
*/
fun navigateWithNavController() {
navController.navigate(R.id.secondFragment)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration)
}
}
And your viewModel should look something like this:
class SharedViewModel : ViewModel() {
private val _someData = MutableLiveData("")
val someData: LiveData<String>
get() = _someData
fun changeData(newData: String?) {
_someData.value = newData ?: _someData.value
}
}
Your view model should have a backing list of the entered words. When a word is added, the list can be updated, and in turn you can update a LiveData that publishes the latest version of the list.
class MyViewModel: ViewModel() {
private val backingEntryList = mutableListOf<String>()
private val _entryListLiveData = MutableLiveData("")
val entryListLiveData : LiveData<String> get() = _entryListLiveData
fun addEntry(word: String) {
backingEntryList += word
_entryListLiveData.value = backingEntryList.toList() // use toList() to to get a safe copy
}
}
Your way of creating the shared view model is the hard way. The easy way is by using by activityViewModels().
I also suggest using the Fragment constructor that takes a layout argument, and then setting things up in onViewCreated instead of onCreateView. It's less boilerplate code to accomplish the same thing.
In the first fragment, you can add words when the button's clicked:
class FirstFragment : Fragment(R.layout.one_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
viewModel.addEntry(value.text.toString())
}
}
}
In the second fragment, you observe the live data:
class SecondFragment : Fragment(R.layout.page3_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val valueView = view.findViewById<TextView>(R.id.textView)
viewModel.entryListLiveData.observe(viewLifecycleOwner) { entryList ->
valueView.text = entryList.joinToString(" ")
}
}
}
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.
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
}
here's my sample code that i need to kill the fragment after load it in the navHost activity (Main Activity) and i'm using navigation component BTW the Fragment is a splash screen Fragment so how to kill it to not shows after loading with back button press.
class SplashFragment : Fragment() {
private var _binding: FragmentSplashBinding? = null
private val binding get() = _binding!!
private lateinit var loadingAnim : AnimationDrawable
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSplashBinding.inflate(inflater, container, false)
lunchLogoAnimation()
lunchLoadingAnimation()
Handler(Looper.getMainLooper()).postDelayed({
findNavController().navigate(R.id.action_splashFragment_to_homeFragment)
},6000)
return binding.root
}
private fun lunchLogoAnimation(){
val loadAnimation = AnimationUtils.loadAnimation(activity,R.anim.splash_screen_logo_translate)
Handler(Looper.getMainLooper()).postDelayed({
binding.monkeyLogoSplash.visibility = View.VISIBLE
binding.monkeyLogoSplash.startAnimation(loadAnimation)
},500)
}
private fun lunchLoadingAnimation(){
loadingAnim = binding.loadingDots.drawable as AnimationDrawable
Handler(Looper.getMainLooper()).postDelayed({
binding.loadingDots.animate().setDuration(500).alpha(1f).withEndAction {
loadingAnim.start()
}
},1500)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
Fragment can be removed easily,
inside the main activity-> kotlin,
supportFragmentManager.beginTransaction().remove(myFragment).commit()
or inside the particular fragment,
parentFragmentManager.beginTransaction().remove(this).commit()
java
getSupportFragmentManager().beginTransaction().remove(myFragment).commit();
getParentFragmentManager.beginTransaction().remove(this).commit();
I loved using Kotlin synthetic for its simplicity and code elegance but now they made it depricated and push you to use those ugly view bindings.
There are plenty of answers on how to use it in activites and Fragments, but could not find any examples for custom layout alert dialogs.
Here is the code which worked perfectly with Kontlin synthetic.
import kotlinx.android.synthetic.main.dialog_reward.*
class RewardDialog: DialogFragment() {
private var mView: View? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return mView
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
mView = it.layoutInflater.inflate(R.layout.dialog_reward, null)
AlertDialog.Builder(it).apply {
setView(mView)
}.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//reference layout elements by name freely
}
override fun onDestroyView() {
super.onDestroyView()
mView = null
}
}
How do I migrate this to view bindings?
You can simply use generated ViewBinding views here and not use onCreateDialog
#AndroidEntryPoint
class RewardDialog : DialogFragment() {
private var binding: DialogRewardBinding by autoCleared()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.Theme_MaterialComponents_Light_Dialog_MinWidth)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DialogRewardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//reference layout elements by name freely
binding.tvReward.setOnClickListener { }
}
}
autoCleared() is an extension function which will null out all the view in onDestroy() taken from google's architecture component sample here
You can set R.style.Theme_MaterialComponents_Light_Dialog_MinWidth theme in onCreate() so that DialogFragment follows the minWidth defined in Material Compnent theme on deferent screen sizes just like AlertDialog
Edit:
If you are not using the material component library then you can set the width in onViewCreated() using the Kotlin extension.
setWidthPercent(ResourcesCompat.getFloat(resources, R.dimen.dialogWidthPercent).toInt())
Kotlin extenstion function
fun DialogFragment.setWidthPercent(percentage: Int) {
val percent = percentage.toFloat() / 100
val displayMetrics = Resources.getSystem().displayMetrics
val rect = displayMetrics.run { Rect(0, 0, widthPixels, heightPixels) }
val percentWidth = rect.width() * percent
dialog?.window?.setLayout(percentWidth.toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)
}
I have ended up with the following solution. Thanks to #Kishan Maurya for the hint about binding.root.
private var _binding: DialogRewardBinding? = null
private val binding get() = _binding!!
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.run {
//initiate the binding here and pass the root to the dialog view
_binding = DialogRewardBinding.inflate(layoutInflater).apply {
//reference layout elements by name freely here
}
AlertDialog.Builder(this).apply {
setView(binding.root)
}.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
class RewardDialog : DialogFragment() {
private var mView: View? = null
private lateinit var dialogBinding: DialogRewardBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// either this way we can init dialogBinding
dialogBinding = DialogRewardBinding.inflate(inflater, container, false)
return dialogBinding.root
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
// either this way we can init dialogBinding
dialogBinding = DataBindingUtil.setContentView(it, R.layout.dialog_reward)
AlertDialog.Builder(it).apply { setView(mView) }.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(view) {
myText.text = "Demo"
}
}
override fun onDestroyView() {
super.onDestroyView()
mView = null
}
}
instead of mView, you can use dialogBinding.root
layout
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/myText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="demo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>