ViewBinding with 2 possible layouts - android

I am doing the migration to view binding and I have a fragment where I use 2 different layouts depending on a variable. Basically it goes like this.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = inflater.inflate(
when (ussdType) {
UssdType.USSD_TYPE -> R.layout.fragment_transaction
else -> R.layout.fragment_balance
}, container, false
)
The problem is that if I refactor it to viewbinding I will need 2 types of viewbindings (FragmentTransactionBinding and FragmentBalanceBinding) so for example to use a button element.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
continue_button.setOnClickListener { onContinueAction(it) }
}
I don't know how to proceed since I have 2 different viewbindings

Simply create two variable that represent viewBinding
private lateinit var bindingTransaction: FragmentTransactionBinding
private lateinit var bindingBalance: FragmentBalanceBinding
and inflate one of them depend on your ussdType
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return when (ussdType) {
UssdType.USSD_TYPE -> {
bindingTransaction = FragmentTransactionBinding.inflate(inflater, container, false)
bindingTransaction.root
}
else -> {
bindingBalance = FragmentBalanceBinding.inflate(inflater, container, false)
bindingBalance.root
}
}
}

I'm also given a project which has 2 possible layouts in a fragment.
There are only 3 ways out. (If you don't have time to read, go with the 3rd option which is the best way)
One is to continue with findViewById() or Kotlin Synthetics. But both are not a good solution.
2nd way is to use two nullable viewBinding variables and use ? To do task So the one which is not null will be executed. If you want some value out of views, then you can get either of the bindings using smart null checks (Elvis operator) ex: val text = b1.tv.text ?: b2.tv.text
3rd (best way) is, create another fragment for BalanceFragment and use respective binding in it. This will also help you separate out the code so you have clear separation of concerns.

Related

How to implement two types of return in a fragment's onCreateView?

I am trying to create a registration page where there is return for a button that can be clicked to a login page and then I need to return a binding command for the registration form to the firebase but I cannot use two types of return because onCreateView only allows return to be implemented once. But the button's "return view" and the registration form's "return binding.root" are two different types of returns that cannot be combined.
How can I implement both of these returns at the same time?
Here is the code:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
val view: View = inflater!!.inflate(R.layout.fragment_register_email, container, false)
view.navigate_to_login_register_email.setOnClickListener { view ->
requireActivity().run {
startActivity(Intent(this, LogIn::class.java))
finish()
}
}
return view
binding = FragmentRegisterEmailBinding.inflate(layoutInflater, container, false)
binding.registerButtonEmail.setOnClickListener {
validateData()
}
return binding.root
You're inflating the same layout twice, in two different ways. That's creating two copies, and you're setting one click listener in one layout, and another in the other layout. And your fragment can only use one of them for its view - whichever you pass back, it'll be missing a click listener.
If you're using view binding, and you're keeping a reference to the binding object (which you are, and that's how you'd normally do it) then just do this:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// generate your binding class by inflating the layout
binding = FragmentRegisterEmailBinding.inflate(layoutInflater, container, false)
// set your listeners on the views in the layout
binding.navigateToLoginRegisterEmail.setOnClickListener {
requireActivity().run {
startActivity(Intent(this, LogIn::class.java))
finish()
}
}
binding.registerButtonEmail.setOnClickListener {
validateData()
}
// return the root of the view hierarchy (i.e. the inflated layout)
return binding.root
}

Unresolved reference in Fragment, even when layout is there, won't use deprecated plugin kotlin-extensions

So, findViewbyId does not work in this case, binding does not seem to work as well, at least I haven't found a working solution to it, and I'm stuck. I've read countless answers here
but none of them seem to do the trick since it looks like it's different for fragments.
Please do not answer with kotlin-android-extension, that is deprecated and I'd like to not use it
since most of my code is already under binding.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
frameLayout = inflater.inflate(R.layout.fragment_augmented_face, container, false) as FrameLayout
surfaceView = frameLayout?.findViewById<View>(R.id.surface_view) as GLSurfaceView
surfaceView?.let {
it.preserveEGLContextOnPause = true
it.setEGLContextClientVersion(2)
it.setEGLConfigChooser(8, 8, 8, 8, 16, 0) // Alpha used for plane blending.
it.setRenderer(this)
it.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
it.setWillNotDraw(false)
}
return frameLayout
}
These two lines here, they are unresolved references:
frameLayout = inflater.inflate(R.layout.fragment_augmented_face, container, false) as FrameLayout
surfaceView = frameLayout?.findViewById<View>(R.id.surface_view) as GLSurfaceView
I'm not sure if it is because it's a Fragment and not AppCompatActivity that I can't make it work?
What are my options here?
Edit here to clarify what I've tried:
I use instead id 'kotlin-parcelize', and viewBinding true
Also tried the Fragment binding instructions on the documentation, so it would look like this.:
lateinit var _binding: AugmentedFaceFragment
private val binding get() = _binding!!
...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
_binding = AugmentedFaceListener.inflate(inflater, container, false)
And now I get unresolved reference on inflate. (Even before adding the rest of the code).
Seems apart from writing the right way here, I was also setting the wrong name of the binding, instead of the xml file I was writing the class name. Here is the working code:
private var _binding: FragmentAugmentedFaceBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAugmentedFaceBinding.inflate(inflater, container, false)
frameLayout = binding.root
surfaceView = binding.surfaceView as GLSurfaceView
surfaceView?.let {
it.preserveEGLContextOnPause = true
it.setEGLContextClientVersion(2)
it.setEGLConfigChooser(8, 8, 8, 8, 16, 0) // Alpha used for plane blending.
it.setRenderer(this)
it.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
it.setWillNotDraw(false)
}
return frameLayout
}
//Also need to remember to nullify the binding onDestroy:
override fun onDestroy() {
_binding = null
super.onDestroy()
}
Did you get rid of the kotlin-android-extension declaration? I think that would be the problem on why it cannot find these method declration.
Either way, you should really use viewBinding/dataBinding instead of this old-school way of grabbing onto your UI components. Is a lot easier to use and much more convenient.
Here are some links
View binding: https://developer.android.com/topic/libraries/view-binding
Data binding: https://developer.android.com/topic/libraries/data-binding
Choose your weapons wisely!
UPDATE:
While using viewBinding, all of your XML classes should be automatically generating a `binding class that you can then use to access your UI components.
For the example above with a fragment called AugmentedFaceFragment, you should be able to access your binding class and inflate it by calling:
_binding = AugmentedFaceFragmentBinding.inflate(inflater, container, false)

How to create Reusable Android BottomSheet Presentation

I have a bunch of popup Dialogs throughout the app I'm working on. What I wanted to do is turn them all into a BottomSheet presentation. Right now, I have one class where I'm instantiating the Dialogs from and able to reuse them throughout the app.
What would I need to do: to do the same to be able to reuse BottomSheetDialogFragments? Rather than creating a BottomSheet presentation from a screen to screen basis, is there a way to have all of them in one class and just call them when I need to from a different screen?
Adding a little bit more context. Let's say I have a CloseDialog, LogoutConfirmationDialog and I use them on multiple screens currently. I would like to do the same with the Android BottomSheet presentation modal if I was to turn these two Dialogs into a BottomSheet presentation.
You can create a custom fragment and inherit "BottomSheetDialogFragment"
class CustomBtmSheetFragment : BottomSheetDialogFragment()
{
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View?
{
return inflater.inflate(
R.layout.fragment_dialog,
container,
false
)
}
override fun getTheme(): Int = R.style.CustomBottomSheetDialog
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
BottomSheetDialog(requireContext(), theme)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
}

Android Jetpack NavController | Get list of actions for current destination

I'm creating a portfolio app that uses Android's new Jetpack Navigation NavController. I've created a fragment with a RecyclerView that will be populated with views that you can click on to navigate to various app demos (and maybe actual apps I've made if I can figure that out). I'm currently populating the list manually, but I'd like to automate that process.
class PortfolioFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_portfolio, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = view.findNavController()
val currentDestination = navController.currentDestination
// How get list of actions?
val appActions = listOf(
R.id.action_portfolioFragment_to_testFragment
)
portfolio_app_list.layoutManager = LinearLayoutManager(context)
portfolio_app_list.adapter = PortfolioAdapter(appActions)
}
}
I couldn't find any methods on NavGraph, nor NavDestination to get the list of actions available for currentDestination. I know that my navigation_main.xml has them, and I could use XMLPullParser or something like that to get them, but I may start using the Safe Args plugin which we'll mean that getting the actual class from the XML would be a pain.
This might be considered a bit hacky.
NavDestination has a private property called mActions which contains the NavActions associated with the NavDestination.
I solved this by using reflection to get access to it.
val currentDestination = findNavController().currentDestination
val field = NavDestination::class.java.getDeclaredField("mActions")
field.isAccessible = true
val fieldValue = field.get(currentDestination) as SparseArrayCompat<*>
val appActions = arrayListOf<Any>()
fieldValue.forEach { key, any ->
appActions.add(any)
}
The NavActions are returned as a SparseArrayCompat which is then iterated to obtain the List

Different ways to use Fragments

I am developing an app with Firebase. But whenever I use the onViewCreated method, the button does not respond to any clicks. But when I use the onCreateView, it works.
Here is my LoginFragment (Button does not respond to clicks):
class LoginFragment : Fragment(R.layout.fragment_login) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = FragmentLoginBinding.inflate(layoutInflater)
binding.buttonGoogleSignin.setOnClickListener {
toast("THIS IS NOT WORKING")
Authentication.getInstance().signIn(context!!, getString(R.string.default_web_client_id)) {
startActivityForResult(mGoogleClient.signInIntent, RC_GOOGLE_SIGN_IN)
}
}
}
}
In this code, my button responds to clicks:
class LoginFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInFlater,
container: ViewGroup?,
savedInstanceState: Bundle?
) {
val view = inflater.inflate(R.layout.fragment_login, container, false)
val binding = FragmentLoginBinding.bind(view)
binding.buttonGoogleSignin.setOnClickListener {
toast("THIS IS WORKING")
Authentication.getInstance().signIn(context!!, getString(R.string.default_web_client_id)) {
startActivityForResult(mGoogleClient.signInIntent, RC_GOOGLE_SIGN_IN)
}
}
return view
}
}
Can someone explain to me why the first approach did not work?
The problem is in the fact that in onViewCreated you are creating a binding object with FragmentLoginBinding.inflate(layoutInflater) but you are not connecting that binding to the view, so whatever you do with that object will not have effect on the view.
FragmentLoginBinding.inflate(layoutInflater) creates a new binding object and also inflate a new view to which it is connected. But you are not using that view in your fragment, so using that method is not the correct choice.
So you can do something like:
val binding = FragmentLoginBinding.bind(getView())
inside onViewCreated if you really want, and that will create a binding with the view you have in your fragment.
Said that, creating the binding already in onCreateView is actually recommended by the Android documentation.

Categories

Resources