I am stuck here, help.
I've got the following code:
ProfileFragment:
#AndroidEntryPoint
class ProfileFragment : Fragment() {
private val profileViewModel: ProfileViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentProfileBinding.inflate(inflater, container, false)
binding.viewModel = profileViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
}
ProfileViewModel:
class ProfileViewModel #ViewModelInject constructor(
#ApplicationContext private val context: Context,
private val profileRepository: ProfileRepository
) : ViewModel() {
fun getUser() {
....
}
}
fragment_profile.xml:
<data>
<variable
name="viewModel"
type="my.app.viewmodel.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="55dp"
android:onClick="#{()->viewModel.getUser()}" />
</LinearLayout>
The problem is that the onClick is never triggered no matter what I do and how I try.
However, as soon as I do it like this in ProfileFragment it works just fine:
binding.myButton.setOnClickListener {
profileViewModel.getUser()
}
Any ideas? Cause I am stuck here
I am not sure if this issue still exist or not
but if you are using data binding and hilt for dependency injection please add below lines in your fragment onViewCreated
binding.lifecycleOwner = this
binding.viewModel = profileViewModel
What fixed this problem for me was adding the fragment reference to itself in onCreateView:
binding.<fragment_name> = this
Like so:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.v(TAG, "onCreateView")
binding = FragmentSettingsBinding.inflate(inflater, container, false)
binding.settingsFragment = this <---
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
Related
I would like to ask about ViewBinding.
The official
buildFeatures{
viewBinding = true
}
but it does not work.
Here is an example code.
Layout Code
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:clickable="true"
android:focusable="true"
android:tint="#color/white"
android:src="#drawable/ic_add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
Kotlin Code
val view = inflater.inflate(R.layout.fragment_list, container, false)
view.floatingActionButton.setOnClickListener { }
Is there any solution to this problem?
I don't know why it is not working.
Android studio Vierson4.1.2
Gradle Viesion 6.5
You need to use as a parent in layout.xml
Example:- my_layout.xml
<?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">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="#dimen/_4sdp">
<TextView
android:id="#+id/scanner"
android:layout_width="#dimen/_200sdp"
android:layout_height="#dimen/_200sdp"
android:layout_centerInParent="true"
app:squaredFinder="true" />
</RelativeLayout>
</layout>
and in Kotlin.
val binding = DataBindingUtil.inflate(android.view.LayoutInflater.from(context), R.layout.my_layout, null, false);
This is how you enable view binding
private var _binding: FragmentListBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentListBinding.inflate(inflater, container, false)
binding.floatingActionButton.setOnClickListener { }
return binding.root
Fragments outlive their views. Make sure you clean up any references to the binding class instance in the fragment's onDestroyView() method.
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
For more details, read.
Try like this.
class BaseActivity : AppCompatActivity(){
private lateinit var binding: (YourActivityBinding)
super.onCreate(savedInstanceState){
binding = YourActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.floatingActionButton.setOnClickListener (View.OnClickListener{
})
}
}
Try this. In Your Gradle (Modul):
android{
.
.
.
buildFeatures {
dataBinding true
}
}
After sync, in Your XML File, try to push (ALT+Enter) on your layout parent and choice
Convert To Data Binding
Now in your Activity
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
}
}
Or in Your fragment:
class BlankFragment: Fragment() {
private lateinit var binding: FragmentBlankBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentBlankBinding.inflate(inflater)
return binding.root
}
from now, for binding view, you should write something like this:
binding.textView.text = "test"
Thank you for your wonderful answers.
I had forgot to create a binding class.
I was able to solve the problem successfully and the error has been resolved.
ADD Code
private var _binding: FragmentListBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
_binding = FragmentListBinding.inflate(inflater, container, false)
binding.floatingActionButton.setOnClickListener {
}
return binding.root
}
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
}
In my app, i have a fragment that call a remote service for get user profile information and show it, and I've used DataBinding for show data.
This is my 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">
<data>
<variable
name="viewModel"
type="com.myapp.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{viewModel.profile.firstName+ ' '+ viewModel.profile.lastName}" />
<!-- Other textviews -->
</LinearLayout>
</layout>
this is ProfileViewModel class
class ProfileViewModel : ViewModel() {
#Inject
lateinit var profileRepository: ProfileRepository
private var _profile = MutableLiveData<Profile>()
val profile: LiveData<Profile>
get() = _profile
fun getProfile(token: String) {
profileRepository.profile(
token,
{
// success
_profile.value = it.value
},
{
//error
}
)
}
}
data class Profile(
firstName : String,
lastName : String,
// other fields
)
And this is fragment where profile should be showed:
class ProfileFragment : Fragment() {
private lateinit var binding: FragmentProfileBinding
private lateinit var viewModel: ProfileViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
viewModel = activity?.run {
ViewModelProviders.of(this)[ProfileViewModel::class.java]
} ?: throw Exception("Invalid Activity")
binding.viewModel = viewModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getProfile(
"aToken"
)
}
}
Now it happen that first time i open fragment, repository call service and get data correctly, but "null" is showed inside textviews. If i close fragment and i reopen it, edittext are populate correctly. What's wrong with my code?
Set binding.lifecycleOwner in your fragment class in order for updates in LiveData objects to be reflected in corresponding views. Your fragment class should look like this:
class ProfileFragment : Fragment() {
private lateinit var binding: FragmentProfileBinding
private lateinit var viewModel: ProfileViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
viewModel = activity?.run {
ViewModelProviders.of(this)[ProfileViewModel::class.java]
} ?: throw Exception("Invalid Activity")
binding.viewModel = viewModel
//add lifecycleOwner
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getProfile(
"aToken"
)
}
}
I've created a simple application that load a random joke from the API and shows it in the TextView. For learning reasons I've implemented the app with data binding. At first I created a ViewModel, that fetch data from the API
class RandomJokeViewModel: ViewModel() {
private val jokeService = RetrofitService()
fun getRandomJoke(): MutableLiveData<RandomJoke> ? {
Log.e("getRandomJokeData", "yes")
return jokeService.loadRandomJoke()
}
In my Fragment I call the method to load the jokes and update the view
class RandomJokeFragment : Fragment() {
private lateinit var binding: FragmentRandomJokeBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_random_joke, container, false)
binding.lifecycleOwner = this
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_random_joke, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadRandomJoke()
}
private fun loadRandomJoke() {
Log.e("getAndroidVersion", "yes")
val randomJokeViewModel =
ViewModelProviders.of(requireActivity()).get(RandomJokeViewModel::class.java)
randomJokeViewModel.getRandomJoke()?.observe(this, Observer<RandomJoke> { randomJoke ->
binding.randomJoke = randomJoke
Log.e("RandomJokeFragment", "This is your Joke: ${randomJoke.value}")
})
}
My data calls looks like this:
data class RandomJoke (
#SerializedName("categories") val categories: ArrayList<String>,
#SerializedName("created_at") val createdAt: String,
#SerializedName("icon_url") val iconUrl: String,
#SerializedName("id") val id: String,
#SerializedName("updated_at:") val updatedAt: String,
#SerializedName("url") val url: String,
#SerializedName("value") val value: String)
In my Fragment Layout I defined the data binding ressources like this
<data>
<variable
name="randomJoke" type="com.example.jokegenerator.data.remote.response.RandomJoke"
/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorPrimaryDark"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="1dp"
android:layout_marginStart="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="#{randomJoke.value}"
android:textColor="#ffff"
tools:text="Hello World"/>
</androidx.constraintlayout.widget.ConstraintLayout>
So when I run the app my TextView is empty. In Logcat and Debugger I verified, that the randomJoke response has data. Why is my view not updating after I get the data? What did I miss?
Not completely sure that this is the reason but:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_random_joke, container, false)
binding.lifecycleOwner = this
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_random_joke, container, false)
}
here you inflate your layout 2 times.
instead try like this:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// This will inflate your layout. You just have to return the root view.
binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_random_joke, container, false)
binding.lifecycleOwner = this
return binding.root // Returning your root view
}
The databinding is probably mixing of the 2 layouts.
In your onCreateView() your are returning:
return inflater.inflate(R.layout.fragment_random_joke, container, false)
but you need to return:
return binding.root
im trying to make a base bottom sheet dialog fragment class that supports data binding. here is my class:
abstract class RoundedBottomSheetDialogFragment<VM : BaseViewModel, DB : ViewDataBinding> :
BottomSheetDialogFragment() {
abstract val viewModel: VM
open lateinit var binding: DB
private fun init(inflater: LayoutInflater, container: ViewGroup?) {
binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, true)
}
abstract fun getLayoutRes(): Int
abstract fun configEvents()
abstract fun bindObservables()
/**
*
* You need override this method.
* And you need to set viewModel to binding: binding.viewModel = viewModel
*
*/
abstract fun initBinding()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val parentLayout = inflater.inflate(R.layout.rounded_bottom_sheet, container, false)
init(inflater, parentLayout.container)
showDialogAsExpanded()
return parentLayout
}
private fun showDialogAsExpanded() {
dialog?.setOnShowListener {
val bottomSheetInternal =
(dialog as BottomSheetDialog).findViewById<View>(R.id.design_bottom_sheet) ?: return#setOnShowListener
val behavior = BottomSheetBehavior.from(bottomSheetInternal)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
configEvents()
bindObservables()
}
}
if you see i'm inflating a layout inside this dialog fragment class and i want to use data binding inside that layout xml file.
this is an example of my xml file:
<layout>
<data>
<variable
name="vm"
type="com.mobtakerteam.walleto.ui.login.searchcountry.SearchCountryViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/search_list"
android:layout_width="match_parent"
android:layout_height="400dp"
app:data="#{vm.countries}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="20"
tools:listitem="#layout/fragment_search_country_row" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
but the problem is that it is not working in the xml layout and i have to manually observe the live data objects inside kotlin class like this:
viewModel.countries.observe(this, Observer {
adapter.submitList(it)
})
so what is the problem?
Using LiveData with binding you have to set lifecycle owner like this
binding.lifecycleOwner = this