-
-
Save gmk57/aefa53e9736d4d4fb2284596fb62710d to your computer and use it in GitHub Desktop.
import android.view.LayoutInflater | |
import android.view.View | |
import android.view.ViewGroup | |
import androidx.appcompat.app.AppCompatActivity | |
import androidx.fragment.app.DialogFragment | |
import androidx.fragment.app.Fragment | |
import androidx.lifecycle.DefaultLifecycleObserver | |
import androidx.lifecycle.Lifecycle | |
import androidx.lifecycle.LifecycleOwner | |
import androidx.viewbinding.ViewBinding | |
import kotlin.properties.ReadOnlyProperty | |
import kotlin.reflect.KProperty | |
/** Activity binding delegate, may be used since onCreate up to onDestroy (inclusive) */ | |
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(crossinline factory: (LayoutInflater) -> T) = | |
lazy(LazyThreadSafetyMode.NONE) { | |
factory(layoutInflater) | |
} | |
/** Fragment binding delegate, may be used since onViewCreated up to onDestroyView (inclusive) */ | |
fun <T : ViewBinding> Fragment.viewBinding(factory: (View) -> T): ReadOnlyProperty<Fragment, T> = | |
object : ReadOnlyProperty<Fragment, T>, DefaultLifecycleObserver { | |
private var binding: T? = null | |
override fun getValue(thisRef: Fragment, property: KProperty<*>): T = | |
binding ?: factory(requireView()).also { | |
// if binding is accessed after Lifecycle is DESTROYED, create new instance, but don't cache it | |
if (viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { | |
viewLifecycleOwner.lifecycle.addObserver(this) | |
binding = it | |
} | |
} | |
override fun onDestroy(owner: LifecycleOwner) { | |
binding = null | |
} | |
} | |
/** Binding delegate for DialogFragments implementing onCreateDialog (like Activities, they don't | |
* have a separate view lifecycle), may be used since onCreateDialog up to onDestroy (inclusive) */ | |
inline fun <T : ViewBinding> DialogFragment.viewBinding(crossinline factory: (LayoutInflater) -> T) = | |
lazy(LazyThreadSafetyMode.NONE) { | |
factory(layoutInflater) | |
} | |
/** Not really a delegate, just a small helper for RecyclerView.ViewHolders */ | |
inline fun <T : ViewBinding> ViewGroup.viewBinding(factory: (LayoutInflater, ViewGroup, Boolean) -> T) = | |
factory(LayoutInflater.from(context), this, false) |
class MainActivity : AppCompatActivity() { | |
private val binding by viewBinding(ActivityMainBinding::inflate) | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(binding.root) | |
binding.button.text = "Bound!" | |
} | |
} |
// Don't forget to pass layoutId in Fragment constructor | |
class RegularFragment : Fragment(R.layout.fragment) { | |
private val binding by viewBinding(FragmentBinding::bind) | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
binding.button.text = "Bound!" | |
} | |
} |
// DialogFragment with onCreateDialog doesn't have a view lifecycle, so we need a different delegate | |
class DialogFragment1 : DialogFragment() { | |
private val binding by viewBinding(FragmentBinding::inflate) | |
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | |
binding.button.text = "Bound!" | |
return AlertDialog.Builder(requireContext()).setView(binding.root).create() | |
} | |
} |
// For DialogFragment with full-blown view we can use a regular Fragment delegate (actually the | |
// whole code here is exactly the same as in RegularFragment) | |
// NB: Constructor with layoutId was only recently added (in Fragment 1.3.0) | |
class DialogFragment2 : DialogFragment(R.layout.fragment) { | |
private val binding by viewBinding(FragmentBinding::bind) | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
binding.button.text = "Bound!" | |
} | |
} |
// For RecyclerView we don't need any delegates, just a property. | |
// Unfortunately, here we have a name overloading: View Binding vs "binding" holder to data (onBindViewHolder). | |
// ViewGroup.viewBinding() helper function can reduce boilerplate a little. | |
class Adapter1 : ListAdapter<String, Adapter1.Holder>(Differ()) { | |
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { | |
return Holder(parent.viewBinding(ListItemBinding::inflate)) | |
} | |
override fun onBindViewHolder(holder: Holder, position: Int) { | |
holder.binding.textView.text = getItem(position) | |
} | |
class Holder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) | |
private class Differ : DiffUtil.ItemCallback<String>() { ... } | |
} |
// Alternatively, we can use generic BoundHolder for all Adapters | |
class Adapter2 : ListAdapter<String, BoundHolder<ListItemBinding>>(Differ()) { | |
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BoundHolder<ListItemBinding> { | |
return BoundHolder(parent.viewBinding(ListItemBinding::inflate)) | |
} | |
override fun onBindViewHolder(holder: BoundHolder<ListItemBinding>, position: Int) { | |
holder.binding.textView.text = getItem(position) | |
} | |
private class Differ : DiffUtil.ItemCallback<String>() { ... } | |
} | |
open class BoundHolder<T : ViewBinding>(val binding: T) : RecyclerView.ViewHolder(binding.root) |
// Personally, I prefer to encapsulate view creation & manipulation inside ViewHolder. | |
// In this case BoundHolder can be used as a superclass. | |
class Adapter3 : ListAdapter<String, Adapter3.Holder>(Differ()) { | |
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = Holder(parent) | |
override fun onBindViewHolder(holder: Holder, position: Int) = holder.bind(getItem(position)) | |
class Holder(parent: ViewGroup) : BoundHolder<ListItemBinding>(parent.viewBinding(ListItemBinding::inflate)) { | |
fun bind(item: String) { | |
binding.textView.text = item | |
} | |
} | |
private class Differ : DiffUtil.ItemCallback<String>() { ... } | |
} | |
abstract class BoundHolder<T : ViewBinding>(protected val binding: T) : RecyclerView.ViewHolder(binding.root) |
@gmk57 i cant share the code / stact trace tho cause this one are company code. but when i tried this code and accessing some binding on destroyView it wont throw that same error ._. dunno why this happen on our user device lol, any way thank for the feedback :)
@gmk57 i have tried to implement this approach in custom views but dont work, you could show a example of this implementation in custom views, please.
@hqfranca Since a view has a same lifecycle as its child views, you don't really need a delegate. Simple property works fine:
class SomeView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) :
FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
private val binding = SomeViewBinding.inflate(LayoutInflater.from(context), this, true)
}
My ViewGroup.viewBinding
helper does not fit well here, because it passes false
as a last parameter (as it should for RecyclerView.ViewHolder
). You can create a second helper, but I'm not sure if it's really worth the effort: it would only save you 27 chars. ;)
@KevinAngga, I've only seen this error when accessing ViewBinding outside the view lifecycle (
onViewCreated
-onDestroyView
). This may happen if you register some callbacks/listeners/observers (e.g. inonViewCreated
), and for some reason they get triggered when view does not exist. Coroutines, when incorrectly used, may cause this too. Can you share your code and/or stack trace?