Kotlin android extensions in custom view with databinding - android

I have a Custom Linear layout and an xml file in with 3 checkboxes. The Custom linear layout looks roughly like this:
class AdvancedBox : LinearLayout, DefineExchangesDialog.DefineExchangesDialogListener {
private lateinit var mBinding: AdvancedBoxBinding
private lateinit var viewModel: GlobalConfigViewModel
constructor(c: Context) : super(c) {
initLayout()
}
constructor(c: Context, a: AttributeSet) : super(c, a) {
initLayout()
}
private fun initLayout() {
val inflater = LayoutInflater.from(context)
mBinding = AdvancedBoxBinding.inflate(inflater)
}
override fun getRootView(): View {
return mBinding.root
}
fun setViewModel(viewModel: GlobalConfigViewModel){
mBinding.viewModel = viewModel
}
override fun onFinishInflate() {
super.onFinishInflate()
//cbVolumeChange is an id defined in advanced_box.xml
cbVolumeChange.setOnClickListener(this::onShowAdvanced)
cbExchanges.setOnClickListener(this::onShowAdvanced)
cbPriceFilter.setOnClickListener(this::onShowAdvanced)
}
The problem is that setting an onClickListener on any of the checkboxes in onFinishInflate() will cause a NullPointerException. I thought overriding the getRootView() would be the solution but that did not work. Something that would work is the following
val root = mBinding.root
root.cbVolumeChange ...
But that is not something I would like to do. So what would be the right way to use the Android Kotlin extension with databinding in my case?

The issue is, that the inflated view is not attached to the layout. You should use AdvancedBoxBinding.inflate(inflater, this, true) for it.
After that onFinishInflate() doesn't matter for you, as it's only relevant for inflating the view and the hierarchy from XML not from code as you're doing.

Related

Should I inflate the layout in onCreateView or onViewCreated?

I am using the following fragment to show an onboarding screen on the first launch of the application. Should I inflate my layout in onCreateView or in onViewCreated? I don't quite understand how to decide on this. Also, do I need to create a ViewModel for my code?
class OnBoardingFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var dotsLayout: LinearLayout
private lateinit var sliderAdapter: SliderAdapter
private lateinit var dots: Array<TextView?>
private lateinit var letsGetStarted: Button
private lateinit var next: Button
private lateinit var animation: Animation
private var currentPos: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navOptions = NavOptions.Builder().setPopUpTo(R.id.onBoardingFragment, true).build()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_onboarding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.slider);
dotsLayout = view.findViewById(R.id.dots);
letsGetStarted = view.findViewById(R.id.get_started_btn);
next = view.findViewById(R.id.next_btn)
sliderAdapter = SliderAdapter(requireContext())
viewPager.adapter = sliderAdapter;
addDots(0);
viewPager.addOnPageChangeListener(changeListener);
next.setOnClickListener {
viewPager.currentItem = currentPos + 1
}
letsGetStarted.setOnClickListener {
findNavController().navigate(R.id.action_onBoardingFragment_to_loginFragment)
}
}
private fun addDots(position: Int) {
dots = arrayOfNulls(2)
dotsLayout.removeAllViews();
for (i in dots.indices) {
dots[i] = TextView(requireContext())
dots[i]!!.text = HtmlCompat.fromHtml("•", HtmlCompat.FROM_HTML_MODE_LEGACY)
dots[i]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
android.R.color.darker_gray
)
)
dots[i]!!.textSize = 35F
dotsLayout.addView(dots[i])
}
if (dots.isNotEmpty()) {
dots[position]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.wine_red
)
)
}
}
private var changeListener: ViewPager.OnPageChangeListener =
object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
addDots(position)
currentPos = position
animation =
AnimationUtils.loadAnimation(requireContext(), android.R.anim.fade_in)
if (position == 0) {
letsGetStarted.visibility = View.INVISIBLE
next.animation = animation
next.visibility = View.VISIBLE
} else {
letsGetStarted.animation = animation
letsGetStarted.visibility = View.VISIBLE
next.visibility = View.INVISIBLE
}
}
override fun onPageScrollStateChanged(state: Int) {}
}
}`
The Android framework calls Fragment's onCreateView to create the view object hierarchy. Therefore, it's correct to inflate the layout here as you did.
onViewCreated is called afterwards, usually you find views and setup them. So, your code is ok.
Regarding the ViewModel, in your sample code you're just configuring the UI so you won't need it. If instead, you need to obtain some data from an API service, transform it, show the states of "loading data", "data retrieved" and "there was an error retrieving data", then you would like not to do those things in the fragment and you could consider using an MVVM approach.
Some references:
https://developer.android.com/guide/fragments/lifecycle#fragment_created_and_view_initialized
https://guides.codepath.com/android/Creating-and-Using-Fragments
https://developer.android.com/topic/architecture
onCreateView is where you inflate the view hierarchy, and return it (so the Fragment can display it). If you're handling that inflation yourself, you need to override onCreateView so you can take care of it when the system makes that request. That's why it's named that way - when the view (displayed layout) is being created, this function is called, and it provides a View.
onViewCreated is called after the Fragment's view has already been created and provided to it for display. You get a reference to that view passed in, so you can do setup stuff like assigning click listeners, observing View Models that update UI elements, etc. You don't inflate your layout here because it won't be displayed (unless you're explicitly inflating other stuff and adding it to the existing view for some reason, which is more advanced and probably not what you're talking about).
So onCreateView is really concerned with creating a view hierarchy for display, and onViewCreated is for taking that displayed hierarchy and initialising your stuff. You might not need to implement onCreateView at all (e.g. if you use the Fragment constructor that takes a layout ID, so it sets it up for you) in which case you'd just implement onViewCreated instead. Or if you are handling it yourself in onCreateView, and you don't have much setup code, you might run that on the View you've inflated before you return it, and not bother with onViewCreated at all.
It's worth getting familiar with the Fragment lifecycle if you haven't already, just so you know the basic way the system moves between states and the callbacks it calls as it does so (and have a look at the documentation for the callback methods too!)

"Lateinit property has not been initialized" error on a variable initialized in onCreateView

The app I'm developing has three classes. MainActivity (not important here), MyFragment and MyClass.
I have a lateinit List inside MyFragment - it is lateinit because it will be used by some methods. (I'm using kotlin synthetics by the way)
class MyFragment: Fragment()
lateinit var myViewsList: List<View>
lateinit var viewA: View
lateinit var viewB: View
lateinit var viewC: View
override fun onCreateView(
//inflating the layout...
): View? {
val view = inflater.inflate(R.layout.fragment_timer, container, false)
val button: Button = view.myButton
viewA = view.myViewA
viewB = view.myViewB
viewC = view.myViewC
myViewsList =
listOf<View>(viewA,viewB,viewC)
myButton.setOnClickListener() {
MyClass().myMethod()
}
return view
}
And then MyClass:
class MyClass() {
fun showViews {
MyFragment().myViewsList.forEach { // this line is causing the error
it.visibility = View.VISIBLE
}
}
fun myMethod() {
//do some stuff
//delay...
showViews()
}
}
But I get the "lateinit property myViewsList not initialized" error whenever showViews() is called (and it isn't called when the fragment is destroyed, started or something like that. In my app, it'd take you ~10 seconds to call it - I've just put it in a OnClickListener for simplicity - and there aren't any activity changes during those 10 seconds).
I don't understand why, as it is initialized in OnCreateView myViewsList = listOf<View>(viewA,viewB,viewC). Any calls involving myViewsList inside MyFragment work just fine, but not inside MyClass. So, why is this happening? and what would be the correct way of calling or initializing it?
I may be wrong here, but I dont think an Android fragment class will work as any other regular class. When you are invoking MyFragment().myViewsList you are not calling the same instance of your fragment and as you are calling this variable "myViewsList" that hasnt been initialized, it crashes.
To make it work, you will have to pass your list and the view that would you like to make it visible.
class MyFragment: Fragment()
lateinit var myViewsList: List<View>
lateinit var viewA: View
lateinit var viewB: View
lateinit var viewC: View
override fun onCreateView(
//inflating the layout...
): View? {
val view = inflater.inflate(R.layout.fragment_timer, container, false)
val button: Button = view.myButton
viewA = view.myViewA
viewB = view.myViewB
viewC = view.myViewC
myViewsList =
listOf<View>(viewA,viewB,viewC)
myButton.setOnClickListener() {
MyClass(myViewsList).myMethod()
}
return view
}
and your class should be like this:
class MyClass(private val views: List<View>) {
fun showViews {
views.forEach { // this line is causing the error
it.visibility = View.VISIBLE
}
}
fun myMethod() {
//do some stuff
//delay...
showViews()
}
}

How to use View Binding on custom views

View Binding got released as part of Android Jetpack
Docs: https://developer.android.com/topic/libraries/view-binding
My question is, how to use view binding with custom views. Google documentation has only show-cased Activity and fragment.
I tried this, but nothing was shown.
LayoutInflater inflater = LayoutInflater.from(getContext());
And then, I used this one, but again, no luck.
LayoutInflater inflater = (LayoutInflater)
getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
I guess maybe I don't target the correct layout inflater for my view but not sure.
Just inform the root, and whether you want to attach to it
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this)
}
or
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this, true)
}
which inflate method to use will depend on the root layout type in xml.
To use the view binding, you need to use the generated binding class not the LayoutInflater, for example, if the layout name is result_profile.xml then you need to use ResultProfileBinding as:
class CustomView #kotlin.jvm.JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private lateinit var binding: ResultProfileBinding
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context))
addView(binding.root)
}
}
Auto generated class : result_profile.xml -> ResultProfileBinding(name of layout, appended with Binding )
Inflate the binding
ResultProfileBinding.inflate(LayoutInflater.from(context))
Use addView to add the view in the hierarchy as:
addView(binding.root)
Note: If you are extending from ConstraintLayout(is the parent class) then use constraint set
You can initialize the view binding property right away
private val binding = CustomViewBinding.inflate(LayoutInflater.from(context), this)
If you are trying to use View Binding with the root view, this is working for me:
class CustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private lateinit var binding: CustomViewBinding
override fun onFinishInflate() {
super.onFinishInflate()
binding = CustomViewBinding.bind(this)
}
}
This is the simplest kotlin answer I can think of. It's a custom view that just wraps a single TextView and provides an update(s:String) function to update the text.
<!-- view_stub.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="#+id/myTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</layout>
// StubView.kt
class StubView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context,attrs,defStyleAttr) {
val binding = ViewStubBinding.inflate(context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.also { addView(it.root) }
fun update(updatedText: String) {
binding.myTextView.text = updatedText
}
}
The two things I like about this answer are:
binding is a val instead of a var. I try to limit the number of vars as much as possible.
The addView is closely associated with the val binding using the also {} scope function instead of an init {} clause, making the instantiation of the View feel much more declarative.
One could argue that the addView() is really a side effect and should be in the init {} section so that it is separate from the declaration of the binding val. I would argue the opposite -- declaring a val then feeding it to a section of code that needs it does not feel like a side effect to me.
You can use DataBindingUtil
binding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.your_layout_id,
this,
true
)

Custom view doesn't update inside a Fragment

I have a project in which I have to build views inside a custom Layout. This layout represents the concept of View in MVP architecture and it lives in a Fragment. The view should be updated by the Presenter whenever an event happens, by calling the View and finally that will update TextViews inside the View. But it seems that after the View is initialized, nothing gets updated anymore.
If my presenter calls the View that contains my TextView - nothing. If I try to update the TextView directly, from the fragment then it works. I can't really understand what is happening and why it doesn't get updated from within the layout that contains that TextView.
MyCustomView:
class MyCustomView(fragment: MyFragment): MyViewInterface, FrameLayout(fragment.context) {
init {
View.inflate(context, R.layout.my_fancy_layout, this)
}
override fun getView(): View {
return this
}
override fun setData(uiModel: UiModel) {
textview_name.text = uiModel.name
}
}
MyFragment:
class MyFragment : Fragment() {
#Inject lateinit var view: MyViewInterface
#Inject lateinit var presenter: MyCustomPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
... dagger injection ...
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return this.view.getView()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.setData(...some ID to fetch data from API...)
//textview_name.text = "blue" //this works instead
}
}
MyPresenter:
class MyPresenter #Inject constructor(
private val repo: MyRepository,
private val view: MyViewInterface
) {
fun setData(productCode: String) {
.. some code ...
view.setData(it) //call to my view
}
}
MyViewInterface:
interface MyViewInterface {
fun getView(): View
fun setData(uiModel: UiModel)
}
All I can think of is the view's instance is not the same in your UI and presenter. I dont know your Dagger's code so I do not have any suggestions to fix it.
You could move the view away from MyPresenter's constructor and set it in MyFragment.onCreate after injection.
Because of you just inflate when create view
init {
View.inflate(context, R.layout.my_fancy_layout, this)
}
add invalidate view in update data function
override fun setData(uiModel: UiModel) {
textview_name.text = uiModel.name
this.invalidate()
this.requestLayout()
}

Accessing views from the Activity with Anko

I know I can use an id attribute with Anko to identify a view:
class MainActivityUI : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
frameLayout {
textView {
id = R.id.text
}
}
}
}
Then obtain it in the Activity using the find() function (or by using Kotlin Android Extensions):
class MainActivity : AppCompatActivity() {
private val textView by lazy {
find<TextView>(R.id.text)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MainActivityUI().setContentView(this)
textView.text = "Hello World"
}
}
But I feel like I am missing something; the only place the README mentions the find function or Kotlin Android Extensions is in the section titled Supporting Existing Code:
You don't have to rewrite all your UI with Anko. You can keep your old
classes written in Java. Moreover, if you still want (or have) to
write a Kotlin activity class and inflate an XML layout for some
reason, you can use View properties, which would make things easier:
// Same as findViewById(), simpler to use
val name = find<TextView>(R.id.name)
name.hint = "Enter your name"
name.onClick { /*do something*/ }
You can make your code even more compact by using Kotlin Android
Extensions.
Which makes it seem like the find function is only meant for supporting "old" XML code.
So my question is this; is using an id along with the find function the correct way of accessing a View from the Activity using Anko? Is there a more "Anko" way of handling this? Or am I missing some other benefit of Anko that makes accessing the View from the Activity irrelevant?
And a second related question; if this is the correct way of accessing a View from the Activity, is there a way of creating an id resource (i.e. "#+id/") from within an AnkoComponent? Rather than creating each id in the ids.xml file.
So, why still use XML id to locate the View? since we already use the Anko instead of the XML.
In my opinion, we can store the view elements inside the AnkoComponent instead of the find view's id method. Check the code blow:
class MainActivityUI : AnkoComponent<MainActivity> {
lateinit var txtView: TextView
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
frameLayout {
txtView = textView {
id = R.id.text // the id here is useless, we can delete this line.
}
}
}
}
class MainActivity : AppCompatActivity() {
lateinit var mainUI : MainActivityUI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainUI = MainActivityUI()
mainUI.setContentView(this)
mainUI.txtView.text = "Hello World"
}
}
Do not use id to identify views with Anko DSL! It is unnecessary and useless because Anko was designed to get rid off XML layouts. Instead use this pattern:
class ActivityMain : AppCompatActivity() {
var mTextView: TextView // put it here to be accessible everywhere
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityMainUI().setContentView(this)
}
fun yourClassMethod() {
// So this is am example how to get the textView
// defined in your Anko DSL class (not by id!):
mTextView.text = "bla-bla-bla"
}
}
class ActivityMainUI : AnkoComponent<ActivityMain> {
override fun createView(ui: AnkoContext<ActivityMain>) = with(ui) {
// your fancy interface with Anko DSL:
verticalLayout {
owner.mTextView = textView
}
}
}
Please note the UI class definition:
class ActivityMainUI : AnkoComponent<ActivityMain> {
If you put there your activity class name in brackets then all its public variables become accessible via owner in UI class body so you can assing them there.
But you may put AppCompatActivity easily and make some universal class which might be cloned. In this case use lateinit var mTextView :
TextView in the body of UI class as described in Jacob's answer here.
I believe that, as you can add behavior to your Anko files, you don't have to instantiate your views in the activity at all.
That can be really cool, because you can separate the view layer even more. All the code that acts in your views can be inserted in the Anko files. So all you have to do is to call your activity's methods from the Anko and not instantiate any view.
But if you need to instantiate any view... you can use Kotlin Android Extensions in your activity.
Exemple:
Code in your activity:
seekBar.setOnSeekBarChangeListener(object: OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
// Something
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
// Just an empty method
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
// Another empty method
}
})
Code in Anko:
seekBar {
onSeekBarChangeListener {
onProgressChanged { seekBar, progress, fromUser ->
// Something
}
}
}
Now the code is in AnkoComponent. No need to instantiate the view.
Conclusion:
It's a more 'Anko' way to program if you put all your view logic in the AnkoComponents, not in your activities.
Edit:
As an exemple of a code where you don't have to instantiate a view:
In your Anko:
var networkFeedback : TextView = null
override fun createView(ui: AnkoContext<MainActivity>) = with(ui) {
frameLayout {
textView {
id = R.id.text2
networkFeedback = this
onClick {
ui.owner.doSomething(2, this)
}
}
}
}
fun networkFeedback(text: String){
networkFeedback.text = text
}
In your activity:
class MainActivity : AppCompatActivity() {
overriding fun onCreate{
[...]
val mainUi = AnkoUi()
// some dynamic task...
mainUi.networkFeedback("lalala")
}
fun doSomething(number: Int, callback: TextView){
//Some network or database task goes here!
//And then, if the operation was successful
callback.text = "Something has changed..."
}
This is a very different approach. I'm not so sure if I like it or not, but this is a whole different discussion...
To generalize the question a bit: How can one make an AnkoComponent that is encapsulated, can be used from the DSL and can have its data programmatically set after creation?
Here is how I did it using the View.tag:
class MyComponent: AnkoComponent<Context> {
lateinit var innerds: TextView
override fun createView(ui: AnkoContext<Context>): View {
val field = with(ui) {
linearLayout {
innerds = complexView("hi")
}
}
field.setTag(this) // store the component in the View
return field
}
fun setData(o:SomeObject) { innerds.setStuff(o.getStuff()) }
}
inline fun ViewManager.myComponent(theme: Int = 0) = myComponent(theme) {}
inline fun ViewManager.myComponent(theme: Int = 0, init: MyComponent.() -> Unit) =
ankoView({ MyComponent(it) }, theme, init)
// And then you can use it anywhere the Anko DSL is used.
class SomeUser : AnkoComponent<Context>
{
lateinit var instance:View
override fun createView(ui: AnkoContext<Context>): View {
linearLayout {
instance = myComponent {}
}
}
fun onDataChange(o:SomeObject) {
(instance.Tag as MyComponent).setData(o)
}
}
}

Categories

Resources