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
Related
I've recently picked up an Android Studio project I started a year ago in Kotlin.
It features three fragments that can be navigated through by a bottom navigation bar.
Now, to break my current issue down to a simple example that even doesn't work for me:
Given there's a the editText object exercise in fragment_home.xml and I want to call and alter it in HomeFragment.kt.
I checked every source of advice I could find from Google & Stackoverflow and came up with the following code in HomeFragment.kt (partially pre-coded by AndroidStudio):
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
_binding = FragmentHomeBinding.inflate(inflater, container, false)
val root: View = binding.root
val view: View = inflater!!.inflate(R.layout.fragment_home,container,false)
view.exercise.setText("This is an exceptionally hardcoded string")
The last line stands for every object I tried to reach. I also tried onClickListening for buttons like so:
val btnNewExercise = view.findViewById<Button>(R.id.btn_new_exercise)
btnNewExercise.setOnClickListener {view
Toast.makeText(view.context, "New exercise will be generated", Toast.LENGTH_SHORT).show()
println("Generated a new exercise")
}
but nothing happens when I start the app/ hit the buttons - I seem to can't get through to the actual view's objects to access them. Even ran into NullPointerExceptions on my way to a solution.
I could supply the fragment and layout files if needed - just thought this way it might be easier at first.
If anybody could tell me where I'm wrong I'd be really grateful! Thanks in advance!
You inflated the layout twice.
Remove this. You already inflated the layout using view binding in the FragmentHomeBinding.inflate... call
val view: View = inflater!!.inflate(R.layout.fragment_home,container,false)
and replace
view.exercise.setText("This is an exceptionally hardcoded string")
with (using binding
binding.exercise.setText("This is an exceptionally hardcoded string")
then the last line on your onCreateView should be
return binding.root
Note: You should have these class properties:
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
So it will look like this:
//move your view model as a class property so it will be accessible by other class methods
private val homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!//transform to immutable
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
//use the immutable view binding property
binding.exercise.setText("This is an exceptionally hardcoded string")
return binding.root
}
For more info, read view binding
Try this solution.
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.exercise.text="This is an exceptionally hardcoded string"
binding.btnNewExercise.setOnClickListener {
Toast.makeText(view.context, "New exercise will be generated", Toast.LENGTH_SHORT).show()
println("Generated a new exercise")
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
You need to return the view you made in the OnCreateView .
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 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
}
I create a data binding for my fragment.
// fragment_my
<layout ...>
<data>
<variable
name="myData"
type="com.example.myproject.MyData" />
</data>
...
</layout>
// MyFragment
class MyFragment : Fragment() {
private lateinit var binding: FragmentMyBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_my, container, false
)
Log.d(TAG, ${binding.myData.item})
return binding.root
}
}
// MyData
data class MyData(
var item: String = "123"
)
But when I run my project, it show the item not init correctly and it always return an optional nullable value.
You should bind the data before accessing/using it.
Try this:
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_my, container, false
)
binding.myData = MyData()
Log.d(TAG, ${binding.myData.item})
return binding.root
binding.myData = MyData() is missing in your code.
Source: https://developer.android.com/topic/libraries/data-binding/expressions#binding_data
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"
)
}
}