Viewbinding within Android and Exoplayer - android

I'm using Android Exoplayer in one of my Fragment.
Within Exoplayer I use a custom control layout"#layout/custom_player" for the controls.
I have different elements within the layout for example I have a button element"optionBtn" which I want to connect to onclicklistener from my Kotlin code. Unfortunately that doesn't go very smoothly with view binding.
This is the XML Exoplayer
<com.google.android.exoplayer2.ui.PlayerView
android:id="#+id/playerVIew"
app:resize_mode="fill"
android:animateLayoutChanges="true"
app:controller_layout_id="#layout/custom_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
This is the kotlin code
...
private var binding: FragmentVideoBinding? = null
private var btnsheetOptions: SheetOptionsBinding? = null
private var sheetDialog: BottomSheetDialog? = null
private var customPlayer: CustomPlayerBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
btnsheetOptions = SheetOptionsBinding.inflate(inflater, null, false)
sheetDialog = BottomSheetDialog(requireContext(), R.style.BottomSheetDialogTheme)
binding = FragmentVideoBinding.inflate(inflater, container, false)
customPlayer = CustomPlayerBinding.inflate(inflater, binding!!.root, true)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val simpleExoPlayer = SimpleExoPlayer.Builder(requireContext()).build()
binding!!.playerVIew.player = simpleExoPlayer
val mediaItem = MediaItem.fromUri(video.toString())
simpleExoPlayer.addMediaItem(mediaItem)
simpleExoPlayer.prepare()
simpleExoPlayer.playWhenReady = true
customPlayer!!.optionBtn.setOnClickListener {
...
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
btnsheetOptions = null
sheetDialog= null
customPlayer = null
}
}
...
This way the layout is double-inflated on top of each other and one layout works with onclick listener and the other does not, which is not very useful.
Does anyone know the correct solution for this, I've been working on this for almost all afternoon.

You cannot use view binding with ExoPlayer's custom HUD layout. View Binding only works with layouts specifically inflated for the activity/fragment layouts. The custom HUD layout does NOT belong to the parent layout which the player is set in. It is inflated in a stand-alone fashion and not included in the layout (Hence the double-inflation). Since the custom layout is inflated and is not part of the original layout, you can't use view binding with all the ids contained in it.
So what can you do if View Binding does not work with the custom layout's buttons ?
You should use findViewById which is a function that belongs to Activity class.
It's very easy to use and I assume you already know how as well:
findViewById<ImageButton>(R.id.optionBtn).setOnClickListener {...}
//The next line is for usage inside a fragment class
activity?.findViewById<ImageButton>(R.id.optionBtn).setOnClickListener {...}
Make sure you give the button an ID in your layout, for example:
android:id="#id/optionBtn"
What if you COULDN'T find the (R.id.optionBtn) ?
This is a common problem, there are two R directories to be aware of.
There is android.R which is usually used as R only. And there is the app's R directory. In order to distinguish between the two and avoid Unresolved reference issue, you should import your app's resources under a different name, this is done in the import section before your class code begins, add this:
import com.example.app.R as appR
Then you can try using appR.id.optionBtn instead. There is a very low chance you'd face this particular R.idissue but follow the solution above in case it happens.
Bottom Line:
1- Viewbinding only works for the activity/fragment layout connected to their context classes, it binds the parent layout's id and all their child views with actual binding variables.
2- If you wanna reach a layout that is not part of the activity/fragment layout directly, you should use findViewById instead.
3- If you have problems using the 'R.id', you should import your app's resources under a different name. I usually use 'X' instead of 'R'. But it's all a personal preference..

One shouldn't inflate the data-binding, while also applying attribute app:controller_layout_id:
customPlayer = CustomPlayerBinding.inflate(inflater, binding!!.root, true)
One can only have that either way.
Somehow the question is pointless, unless providing custom_player.xml ...because it may be lacking some mandatory resource IDs, which would be expected to be present (there are certain limitations to what "custom" may permit, which may include: having to provide certain resource ID, even if hiding these from the user). XML markup is quite important on Android - as all the code runs against it. ExoPlayer supports overriding layout files, unless giving them a different name.
Please refer to the original layout resources, in particular their file names and resId:
https://github.com/google/ExoPlayer/tree/release-v2/library/ui/src/main/res/layout
I'd assume, that when overriding by file name, it should also be possible to data-bind.
Because, when only the include has data-binding, then the parent still cannot bind it.
The parent layout XML would need to generate a data-binding, to begin with.
With .setControllerLayoutId(), one can actually data-bind the View before assigning it:
customPlayer = CustomPlayerBinding.inflate(inflater, binding!!.root, true)
binding.playerView.setControllerLayoutId(customPlayer!!.root)
In this case app:controller_layout_id must not be set.

Related

Alternative to findviewbyid to find button

I have 26 buttons which all have an id like button_0 button_1 etc.
I've saw that findviewbyid is deprecated and I don't want to have to call all those 26 button line by line.
So my first idea was to make an array of them, but there is still the same problem I need to write them all at least one time.
I didn't find a way to achieve that with view binding or by calling the button with a string and need help.
Thanks!
Wrap your buttons inside ViewGroup (constraintLayout, LinearLayout, etc), give it an Id
call findViewById to the ViewGroup , not the button
get ViewGroup children by using children method
// just an example
val buttonContainer = findviewbyid<LinearLayout>()
val buttons = buttonContainer.children //return Sequence<View>
buttons.forEach { btn ->
if(btn is Button){
// do something
}
}
In the app level build.gradle file, just add viewBinding.
buildFeatures {
viewBinding true
}
In your activity create a variable for the binding of your activity layout. The binding will be camel notation in reverse.
lateinit var bindingActivity: ActivityMainBinding
Then add this to the onCreate() method.
bindingActivity = ActivityMainBinding.inflate(layoutInflater)
setContentView(bindingActivity.root)
After you have that in place, you can access your views by just writing binding.viewId and it’s the equivalent of you using find view by id. If you want to access more than one view at the same time using Kotlin just use apply.
// access single view
val text = binding.textTitle.text
// access multiple views without calling binding each time
binding.apply {
textTitle.text = “hello”
val data = inputText.text.toString()
}

Kotlin Android - Is there a way to define the Views only one time in a class?

In my code I make use of the following Views in XML:
val googleButton: Button = findViewById<View>(R.id.google_login) as Button
val loginWithEmailText: TextView = findViewById(R.id.login_with_email_text)
val emailLoginButton: Button = findViewById(R.id.email_login_button)
val createAccountButton: Button = findViewById(R.id.email_create_account_button)
This code is extracted from a function inside my Kotlin class. Whenever I have to access these views, I need to write this code all over again.
Is there any way that I can access them from only one place in my class code? I tried putting them outside but the app won't start.
Thank you
You need to define these fields as a part of your class and initialize them once you set the layout resource for your Activity/Fragment. If you put these lines 1:1 in the class body, the initialization will fail, since the layout has not been inflated yet.
Please get familiar with the concept of lifecycle, so that you can understand how to approach View related topics: https://developer.android.com/guide/components/activities/activity-lifecycle
Please check out this snippet for a sample code:
class MyActivity: Activity() {
lateinit var textView: TextView
lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// initialize your views here
textView = findViewById(R.id.text_view_id)
button = findViewById(R.id.button_id)
}
fun someOtherFunction(){
// you can reference your views here like normal properties
button.setOnClickListener { v -> callAnotherFunction() }
// ...
}
}
Since you are on Android, you might be interested in using Kotlin synthetic properties for referencing views without the whole boilerplate of finding them: https://antonioleiva.com/kotlin-android-extensions/. It's no longer a recommended practice to make use of it, but it's handy in some cases anyway.

DataBindingUtil inflates layout as null

I am developing a single activity application using the Android Jetpack navigation component. On one of the fragments I utilize the built-in data binding tools. Strangely enough, even though it was working just the week before, it just completely broke today for seemingly no reason.
The setup:
The fragment I use with binding has the following layout file:
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/example_layout_root"
tools:context=".example.ExampleFragment"
android:background="#color/main_1"
>
...
</ScrollView>
</layout>
I have stripped the main content but it shows that I have a <layout> element as the root with both the data and the fragment layout part defined.
The fragment code is the following:
class ExamleFragment : Fragment() {
private val viewModel: ExampleViewModel by sharedViewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
val binding : FragmentExampleBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_example, container, false)
// This one also does not work
// val binding = FragmentExampleBinding.inflate(inflater, container, false)
binding.vm = viewModel
binding.lifecycleOwner = this
return binding.root
}
}
I use Koin to inject the viewmodel into the fragment.
When I try to inflate the layout (either with DataBindingUtil or the generated FragmentExampleBinding class) it results in the following exception:
java.lang.IllegalStateException: FragmentExampleBin…flater, container, false) must not be null at
com.example.fragments.ExampleFragment.onCreateView(ExampleFragment.kt:38)
at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2698)
at ...
And the stack goes straight down into OS territory so basically it doesn't give any useful information. I can't figure out why the result is null.
What I have tried:
I have tried inflating the layout with inflater.inflate(...) and it
works just fine but that way I can't use the data bindings.
I have tried removing EVERYTHING from the layout description that's
not absolutely neccessary and it still does not work.
I have tried reverting to previous commits (as far back as the first
working version with binding) and even though it used to work before,
it does not work now.
I even restarted and reset the emulator, tried different images, but
to no avail.
Has anyone encountered anything similar to this? The code really 'broke' over the weekend as I have just tested it the Friday before and it worked properly.
EDIT:
Okay, so I somehow managed to fix it.
I started experimenting with creating another fragment with data binding to see whether things are broken for all fragments or just that specific one. The new one seemed to work just fine but strangely, the old, broken fragment still refused to work even though they were basically the same.
So I did Clean + Rebuild (which I swear I performed as a first attempt to fix the issue) and it kind of just fixed it.
In my multi-module project, the reason for this problem was the fact that there was some kind of package name clash.
If you are getting this error without an apparent reason, double check the AndroidManifest.xml files of your data binding enabled modules.
Making sure all of the data binding enabled modules use unique package names solved the problem for me.
I spent hours to find the root of this problem hopefully others read this answer and won't spend that much.
Dont use DataBindingUtil for fragments, run your app once and android studio will generate a binding class for that fragment based off the name of your xml file, in your case the xml file is call fragment_example, so the class generated will be FragmentExampleBinding, then you call FragmentExampleBinding.inflate(....) like this:
val binding = FragmentExampleBinding.inflate(inflater, container, false);
Do it this way :
class ExamleFragment : Fragment() {
private val viewModel: ExampleViewModel by sharedViewModel()
lateinit binding:FragmentExmaple
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_example, container, false)
binding.vm = viewModel
binding.lifecycleOwner = this
return binding.root
}
}
please I need you to paste your error here
in my case I just forgot to add #AndroidEntryPoint to my fragment class

Should we use butterknife or findViewById() with Kotlin Android project as we can directly access views by id

I'm able to access my layout views(like button, TextView, EditText etc) directly inside the activity by their ids defined in layout xml file in Kotlin android project.
So, do we need to use findviewbyId(), or butterknife lib in kotlin android project?
StudentActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val studentViewModel = getStudentViewModel()
updateButton.setOnClickListener {
val name = nameEditText.text.toString()
val age = ageEditText.text.toString()
val subject = subjectEditText.text.toString()
studentViewModel.updateStudentRecord(
Student(
name,
Integer.parseInt(age),
subject
)
)
}
}
}```
ButterKnife is an old solution for view binding. It has less boilerplate code than old findviewbyId way but because of annotation processors it impacts build time speed and doesn't provide Null safety and Type safety. A better solution is kotlinx.android.synthetic that you used in your example but it has some problems too. For example, if you set your content view to a layout, then type an id that only exists in a different layout, the IDE lets you autocomplete and add the new import statement. Unless the developer specifically checks to make sure their import statements only import the correct views, there is no safe way to verify that this won’t cause a runtime issue. As everything is global, one has to be careful to make sure that they only use views they are expecting and ignore the autocomplete. DataBinding and ViewBinding are the best solutions for now. They are similar at first look. Both generate binding classes that you can use to reference views directly with support Null safety and Type safety, but there are differences:
DataBinding approach needs you to add <layout> tag to your XML layout in order to enable the data binding process
ViewBinding doesn’t support layout variables or layout expressions, so it can’t be used to bind layouts with data in XML
ViewBinding is faster than DataBinding in build time because it does n't use annotation processors.
I think you will not use anymore, just if you want? But I believe that is not because of the Synthetic Accessors, it's because of the Data Bindings and the announced this year, View Binding
Nope, here is the the magic of kotlin. Just use your id from the layout file (xml) and directly use it. Like:
button.setOnClickListener {}
and so on. hope it will help.

Kotlin Android View Binding: findViewById vs Butterknife vs Kotlin Android Extension

I'm trying to figure out the best way to do Android View Binding in Kotlin. It seems like there are a few of options out there:
findViewById
val button: Button by lazy { findViewById<Button>(R.id.button) }
Butterknife
https://github.com/JakeWharton/butterknife
#BindView(R.id.button) lateinit var button: Button
Kotlin Android Extensions
https://kotlinlang.org/docs/tutorials/android-plugin.html
import kotlinx.android.synthetic.main.activity_main.*
I'm pretty familiar with findViewById and Butterknife in java land, but what are the pros and cons of each view binding approach in Kotlin?
Does Kotlin Android Extensions play well with the RecyclerView + ViewHolder pattern?
Also how does Kotlin Android Extensions handle view binding for nested views via include?
ex: For an Activity using activity_main.xml, how would View custom1 be accessed?
activity_main.xml
<...>
<include layout="#layout/custom" android:id="#+id/custom" />
</>
custom.xml
<...>
<View android:id="#+id/custom1" ... />
<View android:id="#+id/custom2" ... />
</>
There are a lot of ways to access views in Android. A quick overview:
My advise would be:
findViewById: old school. Avoid.
ButterKnife: old school, but less boilerplate and some added functionality. Still lacks compile time safety. Avoid if possible.
Kotlin Synthetic: really a elegant cached version of findViewbyId. Better performance and way less boilerplate but still no (real) compile time safety. Will be no longer supported from Kotlin 1.8. Avoid if possible.
ViewBinding: Google's recommendation nowadays. It's faster than databinding and prevents logic errors inside XML (hard to debug). I use this option for all new projects.
Data Binding: most versatile option, since it allows code inside XML. Still used on a lot of existing projects. But can slow down build times (uses annotation processor just like ButterKnife) and a lot of logic inside XML has become a bit of anti pattern.
See also: https://www.youtube.com/watch?v=Qxj2eBmXLHg
Funny to note that Jake Wharton (original author of ButterKnife) has now joined Google and works on ViewBinding.
kotlin-android-extensions is better for Kotlin. ButterKnife is also good but kotlin-android-extensions is a better and smart choice here.
Reason : Kotlin uses synthetic properties and those are called on demand using caching function(Hence slight fast Activity/Fragment loading) while ButterKnife binds all view at a time on ButterKnife.bind()(that consumes slight more time). With Kotlin you don't even need to use annotation for binding the views.
Yes it also plays good with RecyclerView + ViewHolder pattern, you just need to import kotlinx.android.synthetic.main.layout_main.view.*(if layout_main.xml is Activity/Fragment layout file name).
You do not need to do any extra effort for layout imported using include. Just use id of imported views.
Have a look at following official documentation notes:
Kotlin Android Extensions is a plugin for the Kotlin compiler, and it does two things:
Adds a hidden caching function and a field inside each Kotlin Activity. The method is pretty small so it doesn't increase the size of APK much.
Replaces each synthetic property call with a function call.
How this works is that when invoking a synthetic property, where the receiver is a Kotlin Activity/Fragment class that is in module sources, the caching function is invoked. For instance, given
class MyActivity : Activity()
fun MyActivity.a() {
this.textView.setText(“”)
}
a hidden caching function is generated inside MyActivity, so we can use the caching mechanism.
However in the following case:
fun Activity.b() {
this.textView.setText(“”)
}
We wouldn't know if this function would be invoked on only Activities from our sources or on plain Java Activities also. As such, we don’t use caching there, even if MyActivity instance from the previous example is the receiver.
Link to above documentation page
I hope it helps.
I can't flag this question as a duplicate, as you're asking multiple things that have been answered / discussed under different questions.
What are the pros and cons of each view binding approach in Kotlin?
This has been discussed here.
How does Kotlin Android Extensions handle view binding for nested views via include? ex: For an Activity using activity_main.xml, how would View custom1 be accessed?
All Kotlin Android Extensions does is call findViewById for you. See here.
Does Kotlin Android Extensions play well with the RecyclerView + ViewHolder pattern?
Yes, it does. However, you have to use save the Views you get from it into properties, as there is no cache for them like in Activities or Fragments. See here.
If you still have unanswered questions, feel free to ask for clarification.
Take care of using
val button: Button by lazy { findViewById<Button>(R.id.button) }
I already confront the problem when the view is destroyed, and as the instance of your fragment survive(I think in the case of acitivities it doesn't apply), they hold the lazy property referencing to the old view.
Example:
You have an static value in the layout, let say android:text="foo"
//calling first time
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
button.setText("bar")
// button is called for the first time,
// then button is the view created recently and shows "bar"
}
Then the fragment get destroyed because you replace it, but then ou comeback and it regenerated callin onCreateView again.
//calling second after destroyed
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
button.setText(Date().time.toString())
//button is already set, then you are setting the value the to old view reference
// and in your new button the value won't be assigned
// The text showed in the button will be "foo"
}
Now there is a fourth option which is called View Binding, available with Android Studio 3.6 Carnary 11
Quoting from docs.
View Binding
View binding is a feature that allows you to more easily write code
that interacts with views. Once view binding is enabled in a module,
it generates a binding class for each XML layout file present in that
module. An instance of a binding class contains direct references to
all views that have an ID in the corresponding layout.
In most cases, view binding replaces findViewById.
Differences from findViewById
View binding has important advantages over using findViewById:
Null safety: Since view binding creates direct references to views, there's no risk of a null pointer exception due to an invalid
view ID. Additionally, when a view is only present in some
configurations of a layout, the field containing its reference in the
binding class is marked with #Nullable.
Type safety: The fields in each binding class have types matching the views they reference in the XML file. This means that
there's no risk of a class cast exception.
Differences from the data binding library
View binding and the data binding library both generate binding
classes that you can use to reference views directly. However, there
are notable differences:
The data binding library processes only data binding layouts created using the <layout> tag.
View binding doesn't support layout variables or layout expressions, so it can't be used to bind layouts with data in XML.
Usage
To take advantage of View binding in a module of your project, add the
following line to its build.gradle file:
android {
viewBinding.enabled = true
}
For example, given a layout file called result_profile.xml:
<LinearLayout ... >
<TextView android:id="#+id/name" />
<ImageView android:cropToPadding="true" />
<Button android:id="#+id/button"
android:background="#drawable/rounded_button" />
</LinearLayout>
In this example, you can call ResultProfileBinding.inflate() in an
activity:
private lateinit var binding: ResultProfileBinding
#Override
fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
}
The instance of the binding class can now be used to reference any of
the views:
binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }
if you using datainding library. you should databinding view binding.
because it is explict more then kotlin-extensions
p.s findviewbyid is very inconvenience and boilerplate code

Categories

Resources