I struggle to do a property delegate for ViewBinding on BottomSheets.
The general Idea is similar to this
For Fragments i use something like this
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
On BottomSheetsDialogFragments which are Fragments it does not accept the delegate.
fun <T : ViewBinding> BottomSheetDialogFragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
The Lifecycle of BottomSheets would be the same as on regular Fragments, so i would not expect any problems.
Anyone came up with a solution on this ?
You can still use the FragmentViewBindingDelegate and extension from Gabor.
You just also need to inflate the view inside onCreateView().
For example:
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.zhuinden.fragmentviewbindingdelegatekt.viewBinding
class ExampleBottomSheet : BottomSheetDialogFragment() {
//Using ::bind here since the view is already inflated in onCreateView()
private val binding by viewBinding(YourCustomViewBinding::bind)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.your_custom_view, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Use binding here or wherever you need it
}
}
Implement ViewBinding in BottomSheetFragment like this (Works for me):
class CustomBottomSheet : BottomSheetDialogFragment() {
private lateinit var binding: CustomBottomSheetBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = CustomBottomSheetBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//onClick listener
binding.button.setOnClickListener {
Toast.makeText(context, "Clicked", Toast.LENGTH_LONG).show()
}
}
}
This works:
class CampaignBottomSheet : BottomSheetDialogFragment() {
private var _binding: MenuCampaignsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = MenuCampaignsBinding.inflate(layoutInflater)
binding.startCampaign.setOnClickListener { println("__print::CampaignBottomSheet") }
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Unlike Fragment class, BottomSheetDialogFragment doesn't have a constructor that accepts layout resource.
You can create a custom dialog for it. Basically, copy everything from AppCompatDialogFragment and BottomSheetDialogFragment.
Related
To use viewbinding in an android app I am basically creating base classes for Activity & Fragment to remove boilerplate of everytime writing inflating code.
ACTIVITY:
BaseActivity with viewbinding:
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = getViewBinding()
}
abstract fun getViewBinding(): VB
}
MainActivity:
class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
//we can directly use binding now and it works fine inside activity
//binding.view.doSomething()
}
override fun getViewBinding(): ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
}
FRAGMENTS :
BaseFragment:
abstract class BaseFragment<VB : ViewBinding> : Fragment() {
var binding: VB? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = getViewBinding(view)
}
abstract fun getViewBinding(view: View): VB
}
DemoFragment:
class DemoFragment : BaseFragment<DemoFragmentBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//problem is here
binding.txtData.text="Something"
}
override fun getViewBinding(view: View): DemoFragmentBinding = DemoFragmentBinding.bind(view)
}
demo_fragment.xml:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.DemoFragment">
<TextView
android:id="#+id/txt_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Hello" />
</FrameLayout>
Problem : Unable to access views using binding inside Demofragment. I don't know why it works with activity and not with fragment.
2nd way that I don't want todo:
implementation 'androidx.fragment:fragment-ktx:1.3.1'
class DemoFragment : Fragment(R.layout.demo_fragment) {
lateinit var binding: DemoFragmentBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = DemoFragmentBinding.bind(view).apply {
txtData.text = "Hello World"
}
}
}
You need to override onCreateView in BaseFragment and initialize the viewbinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = getViewBinding()
return binding.root
}
Then change this line
override fun getViewBinding(view: View): DemoFragmentBinding = DemoFragmentBinding.bind(view)
with
override fun getViewBinding() = DemoFragmentBinding.inflate(layoutInflater)
BaseFragment:
abstract class BaseFragment<VB : ViewBinding> : Fragment() {
private var _binding: VB? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = getViewBinding()
return binding.root
}
abstract fun getViewBinding(): VB
}
DemoFragment:
class DemoFragment : BaseFragment<DemoFragmentBinding>() {
override fun getViewBinding() = DemoFragmentBinding.inflate(layoutInflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
txtData.text = "Something"
}
}
}
Here is another way to implement this factory abstraction with ViewBinding. I am sharing the implementation code below. I am using genrics here. If anyone needs further explanation, I am here for that. Make sure you have enabled viewbinding feature already into the build.gradle file. Then use the following BaseFragment.kt as your fragment abstraction.
BaseFragment:
typealias Inflate<T> = (LayoutInflater, ViewGroup?, Boolean) -> T
abstract class BaseFragment<V: ViewBinding>(
private val inflate: Inflate<V>
) : Fragment() {
private lateinit var _binding: V
val binding get() = _binding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = inflate(inflater, container, false)
return binding.root
}
}
N:B: Know more about typealias.
HomeFragment:
// Implement the BaseFragment like below
class HomeFragment : BaseFragment<FragmentHomeBinding>(FragmentHomeBinding::inflate) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// usages by calling public variable 'binding' from base class
binding.message.text = "update $value"
}
}
I looked through the documentation, I'm not sure if this breaks what you wanted with the abstract pattern in the BaseFragment, but I tested your code with this change and it worked. Only change was in the DemoFragment:
//Add this for the onCreateView implementation
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DemoFragmentBinding.inflate(LayoutInflater.from(context), null, false)
val view = binding!!.root
return view
}
Then I tested in onViewCreated:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//problem is here
binding?.txtData?.text="Something"
}
And it worked. The documentation I looked at was this section
I think doing this still allows you to avoid some boilerplate, but the onCreateView override is going to be necessary for every fragment, because of the different viewbinding inflation (e.g. DemoFragmentBinding.inflate etc etc)
Uses https://github.com/hoc081098/ViewBindingDelegate
abstract class BaseFragment<VB : ViewBinding>(#LayoutRes layoutId: Int) : Fragment(layoutId) {
protected abstract val binding: VB
}
import com.hoc081098.viewbindingdelegate.*
class MainFragment: BaseFragment<FragmentMainBinding>(R.layout.fragment_main) {
override val binding by viewBinding()
}
I'm new to Kotlin and android development but i can't find why my program isn't working.
I'm trying to be able to communicate from my first fragment to his child, and testing it with a string but it won't display.
Thanks in advance for your help !!!
My first fragment :
class FirstFragment : Fragment() {
private lateinit var viewModel : Communicator
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = activity?.run {
ViewModelProvider(this).get(Communicator::class.java) // .of supprimé
} ?: throw Exception("Invalid Activity")
viewModel.message.value = "test"
view.findViewById<Button>(R.id.button_stall_selection).setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
}
}
Here is my second :
class SecondFragment() : Fragment() {
private lateinit var viewModel :Communicator
private var msg: String? = ""
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = activity?.run {
ViewModelProvider(this).get(Communicator::class.java) // .of deleted
} ?: throw Exception("Invalid Activity")
viewModel.message.observe(viewLifecycleOwner, Observer {
msg = viewModel.message.value
})
view.findViewById<TextView>(R.id.textView_1).text = msg
view.findViewById<Button>(R.id.button_second).setOnClickListener {
findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment)
}
}
}
and finally here is the viewModel class i'm trying to use in order to communicate :
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class Communicator() : ViewModel(){
val message =MutableLiveData<String>()
fun setMsgCommunicator(msg:String){
message.setValue(msg)
}
}
In your FirstFragment, try to call viewModel.setMsgCommunicator("test") instead of directly calling viewModel.message.value = "test"
I have tested this code in another project where I didn't use fragment and wrote it in MainActivity.kt, it works. But a problem comes when I make another project using fragment so I write it in a new class (HerbalFragment.kt). It's getting error in "findViewById" for initiate Recyclerview and also getting error in "this". Here is my code (HerbalFragment.kt)
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* A simple [Fragment] subclass.
*/
class HerbalFragment : Fragment() {
private lateinit var rvHerbal: RecyclerView
private var list: ArrayList<Herbal> = arrayListOf()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
super.onViewCreated(view, savedInstanceState)
return inflater.inflate(R.layout.fragment_herbal, container, false)
rvHerbal = findViewById(R.id.rv_herbal)
rvHerbal.setHasFixedSize(true)
list.addAll(HerbalData.listData)
showRecyclerList()
}
private fun showRecyclerList() {
rvHerbal.layoutManager = LinearLayoutManager(this)
val listHerbalAdapter = ListHerbalAdapter(list)
rvHerbal.adapter = listHerbalAdapter
}
}[![enter image description here][1]][1]
Help me please, thanks before
As far as i have noticed your code is unreachable,
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
super.onViewCreated(view, savedInstanceState)
return inflater.inflate(R.layout.fragment_herbal, container, false)
// unreachable code you are already return the view so below code will never execute
rvHerbal = findViewById(R.id.rv_herbal)
rvHerbal.setHasFixedSize(true)
list.addAll(HerbalData.listData)
showRecyclerList()
}
To fix this issue you need to change your code as i have suggested.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_herbal, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// do what ever you want here like find view ids and etc...
rvHerbal = findViewById(R.id.rv_herbal)
rvHerbal.setHasFixedSize(true)
list.addAll(HerbalData.listData)
showRecyclerList()
}
}
private fun showRecyclerList() {
rvHerbal.layoutManager = LinearLayoutManager(activity!!)
val listHerbalAdapter = ListHerbalAdapter(list)
rvHerbal.adapter = listHerbalAdapter
}
You do not have to find view if you use the kotlin data binding by this easy method
add import to this (fragment_herbal is your xml layout view file name)
import kotlinx.android.synthetic.main.fragment_herbal.*
then
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_herbal, container, false)
}
then use your view item without findviewbyid
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rvHerbal.setHasFixedSize(true)
list.addAll(HerbalData.listData)
showRecyclerList()
}
I am using RecyclerView in Fragment ImagesliderFragment First it gives an expection of "recyclerview must not be null" so on stackover flow I got an answer that you should intialized your recyclerview by using following line
frag_rv = view.findViewById(R.id.frag_rv) as RecyclerView
But problem is that it gives me error on view. that only safe or non null asserted calls are allowed also my RecyclerView is not resolving can anyone explain me this what is the problem ?
ImagesliderFragment
class ImagesliderFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
inflater.inflate(R.layout.fragment_imageslider, container, false)
frag_rv = view.findViewById(R.id.frag_rv) as RecyclerView
frag_rv.layoutManager = LinearLayoutManager(context, LinearLayout.HORIZONTAL, false)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
}
Try this
class ImagesliderFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val myView = inflater.inflate(R.layout.fragment_imageslider, container, false)
val frag_rv = myView.findViewById(R.id.frag_rv) as RecyclerView
frag_rv.layoutManager = LinearLayoutManager(context, LinearLayout.HORIZONTAL, false)
return myView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
}
Also now no need to do findViewById()
SAMPLE CODE
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
// import here your layout file like this
import kotlinx.android.synthetic.main.fragment_imageslider.view.*
class ImagesliderFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val myView = inflater.inflate(R.layout.fragment_imageslider, container, false)
myView.frag_rv.layoutManager = LinearLayoutManager(context, LinearLayout.HORIZONTAL, false)
return myView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
}
}
You could change your onCreateView method to:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
) =
inflater.inflate(R.layout.fragment_imageslider, container, false).apply {
frag_rv = findViewById(R.id.frag_rv) as RecyclerView //val frag_rv if it's not declared elsewhere
frag_rv.layoutManager = LinearLayoutManager(context, LinearLayout.HORIZONTAL, false)
}
This way the view is inflated(via inflater.inflate) and you are using it to find the RecyclerView.
You should only use the onCreateView method to inflate the view and bind the iew in the onViewCreated method
class ImagesliderFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?) = inflater.inflate(R.layout.fragment_imageslider, container, false)!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val frag_rv = view.findViewById<RecyclerView>(R.id.frag_rv) frag_rv.layoutManager = LinearLayoutManager(context, LinearLayout.HORIZONTAL,
false)
}
}
Before you use view.findViewById you have to initialize it, also use val before frag_rv like the following.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// here you have to initialize your view
View view = inflater.inflate(R.layout.fragment_imageslider, container, false)
val frag_rv = view.findViewById(R.id.frag_rv) as RecyclerView
frag_rv.layoutManager = LinearLayoutManager(context, LinearLayout.HORIZONTAL, false)
return view
}
In an android app I have an activity with several fragments. In one of these fragments I have an ImageButton. On click I want to let something happen in the activity directly, not in the fragment. In this case I want to set an image. Summed up, the ImageButton is in the fragment, but the ImageView I want to change on click is in the activity.
How do I achieve that? Whatever I've tried resulted in an app crash.
This is how my fragment.kt looks:
class Fragment1 : Fragment() {
companion object{
fun newInstance() = Fragment1()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_1, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
imageButton.setOnClickListener{
imageView.setImageResource(R.drawable.example)
}
}
}
class Fragment1 : Fragment() {
var callbacks: OnFragmentCallbacks? = null
override fun onAttach(context: Context?) {
super.onAttach(context)
callbacks = activity as OnFragmentCallbacks
}
interface OnFragmentCallbacks{
fun changeImage(resourceId: Int)
}
companion object{
fun newInstance() = Fragment1()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_1, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
imageButton.setOnClickListener{
callbacks?.changeImage(R.drawable.example)
}
}
In your Activity:
// Change FragmentName with name of your Fragment class
class MainActivity : AppCompatActivity(), FragmentName.OnFragmentCallbacks{
override fun changeImage(resouseId: Int){
imageView.setImageDrawable(getResources().getDrawable(resourceId))
// or
imageView.setImageResource(resourceId)
}
Just coded it in stackoverflow, may not be perfect