ViewBindingKtx

299 阅读1分钟

在Android SDK中提供了

androidx.activity:activity-ktx:1.5.5
androidx.fragment:fragment-ktx:1.5.5

里面包含的对viewModel的扩展方法,可以直接获取viewModel

val viewModel by viewModels<IndexViewModel>()

在MVVM架构模式中主要使用的就是viewModelviewBinding (dataBinding 要在xml布局写逻辑,个人很不喜欢)。官方并没有提供相关扩展,所以就自己造一个。 原理基本上都是使用反射来实现的,我之前是写在BaseActivity中的,但是这样就有点耦合。所以使用委托的方式实现。

ActivityBinding

inline fun <reified T : ViewBinding> Activity.viewBinding() = ActivityViewBindingDelegate(T::class.java)

class ActivityViewBindingDelegate<T : ViewBinding>(
    private val bindingClass: Class<T>
) : ReadOnlyProperty<Activity, T> {
    private var binding: T? = null

    override fun getValue(thisRef: Activity, property: KProperty<*>): T {
        binding?.let { return it }

        val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java)
        @Suppress("UNCHECKED_CAST")
        binding = inflateMethod.invoke(null, thisRef.layoutInflater) as T
        return binding!!
    }
}

FragmentBinding

inline fun <reified T : ViewBinding> Fragment.viewBinding() = FragmentViewBindingDelegate(T::class.java, this)

class FragmentViewBindingDelegate<T : ViewBinding>(
    bindingClass: Class<T>,
    private val fragment: Fragment
) : ReadOnlyProperty<Fragment, T> {
    private val clearBindingHandler by lazy(LazyThreadSafetyMode.NONE) { Handler(Looper.getMainLooper()) }
    private var binding: T? = null

    private val bindMethod = bindingClass.getMethod("bind", View::class.java)

    init {
    // 这里处理了ViewBinding在Fragment中的内存泄漏问题
        fragment.lifecycleScope.launch {
            fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
                viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
                    override fun onDestroy(owner: LifecycleOwner) {
                        // Lifecycle listeners are called before onDestroyView in a Fragment.
                        // However, we want views to be able to use bindings in onDestroyView
                        // to do cleanup so we clear the reference one frame later.
                        clearBindingHandler.post { binding = null }
                    }
                })
            }
        }
    }

    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        if (binding != null && binding?.root !== thisRef.view) {
            binding = null
        }
        binding?.let { return it }

        val lifecycle = fragment.viewLifecycleOwner.lifecycle
        if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
            error("Cannot access view bindings. View lifecycle is ${lifecycle.currentState}!")
        }

        @Suppress("UNCHECKED_CAST")
        binding = bindMethod.invoke(null, thisRef.requireView()) as T
        return binding!!
    }
}

ViewGroupBinding

inline fun <reified T : ViewBinding> ViewGroup.viewBinding() = ViewBindingDelegate(T::class.java, this)

class ViewBindingDelegate<T : ViewBinding>(
    private val bindingClass: Class<T>,
    val viewGroup: ViewGroup
) : ReadOnlyProperty<ViewGroup, T> {
    private var binding: T? = null

    override fun getValue(thisRef: ViewGroup, property: KProperty<*>): T {
        binding?.let { return it }

        val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
        @Suppress("UNCHECKED_CAST")
        binding = inflateMethod.invoke(null, LayoutInflater.from(thisRef.context), viewGroup, true) as T
        return binding!!
    }
}

使用方法

Activity

class MainActivity : AppCompatActivity() {

    private val binding by viewBinding<ActivityMainBinding>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
    }
 }

Fragment

class BindFragment : Fragment(R.layout.fragment_bind) {
    private val binding by viewBinding<FragmentBindBinding>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.tvText.setOnClickListener {
            
        }
    }
}

ViewGroup

class BindView @JvmOverloads constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0): FrameLayout(context, attrs, defStyleAttr) {
    private val binding by viewBinding<ViewBindBinding>()
    init {
        binding.tvView.text = "text"
    }
}