I wanna make a BaseFragment. For this, I have to use ViewDataBinding and ViewModel. using generic, I can use variable but not static field. For example I have to Inflate writing this code "FragmentSecondBinding.inflate(layoutInflater, container, false) ". So I tried this code "T.inflate(layoutInflater, container, false)" but got some error. Also ViewModel is like this.
How can I make this code to BaseCode?
abstract class BaseFragment<T: ViewDataBinding, M : ViewModel> : DaggerFragment(){
abstract val layoutId : T
private lateinit var binding : T
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<M> { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = T.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this.viewLifecycleOwner
}
There is a way to abstract out the particular ViewDataBinding, however, it would require to provide a layout resource reference for each concrete fragment implementation:
protected abstract val layoutResource: Int
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<M> { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layoutResource, container, false).apply {
viewmodel = viewModel
}
return binding.root
}
Related
I am working on a basic bluetooth app in android studio and I am having trouble with view binding. So far I have this in my MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
...
Later in the onCreate I have this line
val btnONOFF = findViewById<View>(R.id.btnONOFF) as Button
and I would like to replace the findViewById using viewbinding but I'm not sure what to replace that line with.
The android developers website says to use something like this
binding.name.text = viewModel.name
but I'm also not sure what a viewModel is.
Here is my ViewFragment.kt incase that is helpful
class ViewFragment : Fragment() {
private var _binding: ActivityMainBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): RelativeLayout {
_binding = ActivityMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}}
Any help is appreciated, thanks!
You can find the tutorial of viewmodel from here.
ViewFragment.kt
class ViewFragment : Fragment() {
private var _binding: ActivityMainBinding? = null
private val binding get() = _binding!!
private val viewModel by viewModel<ActivityViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): RelativeLayout {
_binding = ActivityMainBinding.inflate(inflater, container, false)
setupView()
return binding.root
}
private fun setupView(){
binding.text = viewModel.name
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
You can customise your ActivityViewModel.kt
class ActivityViewModel : ViewModel() {
val name = "Hello"
}
Note: by viewModel is koin injection.
Here is the code :
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)
binding.btnOpen.setOnClickListener {
Navigation.findNavController(view).navigate(R.id.secondFragment)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
At line: Navigation.findNavController(view).navigate(R.id.secondFragment) I'm getting error as Type mismatch required view found view?
My question is why we can't combine navcontroller with view binding?
And is there any resources to learn restrictions of using view binding
Instead of view use binding.root
Like this:
Navigation.findNavController(binding.root).navigate(R.id.secondFragment)
binding.root is reference to root view.
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
}
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()
}
The following code is from the project architecture-samples, you can see it here.
I'm not sure where I should place viewDataBinding.lifecycleOwner = this.viewLifecycleOwner between onCreateView() and onActivityCreated(), could you tell me?
class TasksFragment : Fragment() {
private lateinit var viewDataBinding: TasksFragBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = TasksFragBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
setHasOptionsMenu(true)
//viewDataBinding.lifecycleOwner = this.viewLifecycleOwner Can I place here?
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Set the lifecycle owner to the lifecycle of the view
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
}
..
}
onActivityCreated is deprecated. You should use onViewCreated or onCreateView.
private var binding: TasksFragBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val viewDataBinding = TasksFragBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
lifecycleOwner = viewLifecycleOwner
}
this.binding = viewDataBinding
return viewDataBinding.root
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}