I can't change value of Views by DataBinding or by Android Extensions, but it works by 'traditional way' (findViewById). Also, it throws NPE when I try by Android Extensions' way without safe call operator.
HourlyFragment.kt
class HourlyFragment : Fragment() {
private lateinit var binding: FragmentHourlyBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
var rootView = inflater.inflate(R.layout.fragment_hourly, container, false)
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_hourly, container, false
)
// doesn't work
//binding.tvHourly.text = "HOURLY!"
val tvHourly : TextView = rootView.findViewById(R.id.tv_hourly)
tvHourly.setText("HOURLY!!")
// doesn't work, without safe call operator throws NullPointerException
//tv_hourly?.text = "HOURLY!!!"
return rootView
}
}
fragment_hourly.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.HourlyFragment">
<TextView
android:id="#+id/tv_hourly"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="hourly fragment" />
</FrameLayout>
</layout>
Currently you are inflating your views twice and that's causing all the issues I believe, so instead just inflate your views once and see if it solves your issue
Try the following
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// var rootView = inflater.inflate(R.layout.fragment_hourly, container, false)
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_hourly, container, false
)
binding.tvHourly.text = "HOURLY!"
// val tvHourly : TextView = rootView.findViewById(R.id.tv_hourly)
tvHourly.setText("HOURLY!!")
return binding.root
}
Why your data binding doesn't work:
You are manually inflating with the line
var rootView = inflater.inflate(R.layout.fragment_hourly, container, false)
but then you use data binding to inflate another copy of this hierarchy in the next line of code. You then return the first layout from onCreateView, throwing away your second inflated data binding layout. You changed the text of the view in the data binding layout that you then threw away.
Why your Android Extensions doesn't work:
You can't use the synthetic view references before onCreateView() returns, because the Fragment still doesn't have any view attached to it for the synthetic property to use to find the child view.
You should be modifying view content in onViewCreated() instead.
By the way, Android Extensions is deprecated and should no longer be used.
In a more concise and more readable way:
private lateinit var binding: FragmentHourlyBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHourlyBinding.inflate(
inflater, container, false
).apply {
tvHourly.text = "HOURLY!"
}
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 wish to generate a column of buttons inside a fragment in Kotlin from a database. For now I tried with just one button, but I cannot seem to do it without hardcoding it in the XML file. Here is my code so far:
class NotesFragment() : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater!!.inflate(R.layout.fragment_notes, container, false)
//val root : ViewGroup = inflater.inflate(R.layout.fragment_notes, null) as ViewGroup
val button_Id: Int = 1111
val button = Button((activity as MainActivity?)!!)
button.setText("Button 4 added dynamically")
button.setLayoutParams(ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT))
button.setId(button_Id)
button.x = 250f
button.y = 500f
button.setOnClickListener(this)
view.add(button)
return view
}
}
I know I should probably look for the layout somewhere in this and do an addView with it... What is the next step?
I did it like this
view.rootView.findViewById<RelativeLayout>(R.id.button_container).addView(button)
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)
I was previously using Kotlin Synthetics.
Here are the relevant files:
view_error.xml (other layout XML)
RecipeDetailFragment.kt
fragment_recipe_detail.xml (corresponding Fragment XML)
Previous Code in short (Using Kotlin Synthetics)
import kotlinx.android.synthetic.main.view_error.*
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_recipe_detail, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// btnRetry is from view_error.xml. Accessed using Kotlin Synthetics
btnRetry.setOnClickListener {
viewModel.retryRecipeRequest(args.id)
}
}
Current Code Attempt in short: (Using ViewBinding)
So, here I successfully used ViewBinding for corresponding Fragment layout.
But I am not sure how to use ViewBinding for view_error.xml here to access btnRetry of view_error.xml?
What code is required to be added below?
import com.packagename.databinding.FragmentRecipeDetailBinding
private var _binding: FragmentRecipeDetailBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentRecipeDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
// NOW THE btnRetry gives error as I removed the kotlin synthetics imports.
// How to access btnRetry through ViewBinding?
btnRetry.setOnClickListener {
viewModel.retryRecipeRequest(args.id)
}
}
You must be using <include> element to use the external layout within fragment_recipe_detail. something like this
in fragment_recipe_detail.xml
<include
android:id="#+id/retryLayoutId"
layout="#layout/retryLayout"
/>
So now in the case of view binding you can access the viewbinding variable and access the external layout id and then access its children. Something like given below.
binding.retryLayoutId.btnRetry.setOnClickListener {
viewModel.retryRecipeRequest(args.id)
}
where layoutId is the included layout's id.
I am try change the font, but not working. Are returning "null" being that I set the value in the XML and also in the code.
Kotlin Code
val typeFace = Typeface.createFromAsset(tvThanks.context.assets, "dinpro_medium.ttf")
tvThanks.setTypeface(typeFace)
<TextView
android:id="#+id/tvThanks"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="#string/text_here"/>
ERROR
java.lang.IllegalStateException: tvThanks must not be null
at br.com.adrianofpinheiro.testesantander.fragment.ContatoEnviadoFragment.onCreate(ContatoEnviadoFragment.kt:21)
Your fragment should have the context property without getting from the tvThanks.
I think you can try doing this inside the onCreateView of your fragment
val typeFace = Typeface.createFromAsset(context.assets, "dinpro_medium.ttf")
tvThanks!!.setTypeface(typeFace)
class Photos : Fragment() {
private lateinit var rootView: View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
rootView = inflater.inflate(R.layout.skeleton_photos_tab, container, false)
return rootView
}
}
now you can do this.
rootView.tvThanks.setTypeface(typeFace)