Skip to content

Commit 0934225

Browse files
committed
architecture review: vendor 'vbpd'
https://github.com/androidbroadcast/ViewBindingPropertyDelegate/ TODO: needs adding to licenses
1 parent 9447fe8 commit 0934225

7 files changed

Lines changed: 595 additions & 37 deletions

File tree

AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import com.ichi2.themes.Themes
4646
import com.ichi2.utils.dp
4747
import com.ichi2.utils.increaseHorizontalPaddingOfMenuIcons
4848
import com.mrudultora.colorpicker.ColorPickerPopUp
49+
import dev.androidbroadcast.vbpd.viewBinding
4950
import kotlinx.coroutines.flow.combine
5051
import kotlinx.coroutines.flow.launchIn
5152
import kotlinx.coroutines.flow.onEach
@@ -62,23 +63,11 @@ class WhiteboardFragment :
6263
WhiteboardViewModel.factory(AnkiDroidApp.sharedPrefs())
6364
}
6465

65-
// binding pattern to handle onCreateView/onDestroyView
66-
private var fragmentBinding: FragmentWhiteboardBinding? = null
67-
private val binding: FragmentWhiteboardBinding get() = fragmentBinding!!
66+
private val binding: FragmentWhiteboardBinding by viewBinding(FragmentWhiteboardBinding::bind)
6867

6968
private var eraserPopup: PopupWindow? = null
7069
private var strokeWidthPopup: PopupWindow? = null
7170

72-
override fun onCreateView(
73-
inflater: LayoutInflater,
74-
container: ViewGroup?,
75-
savedInstanceState: Bundle?,
76-
) = FragmentWhiteboardBinding
77-
.inflate(inflater, container, false)
78-
.apply {
79-
fragmentBinding = this
80-
}.root
81-
8271
/**
8372
* Sets up the view, observers, and event listeners.
8473
*/
@@ -92,19 +81,14 @@ class WhiteboardFragment :
9281
viewModel.loadState(isNightMode)
9382

9483
setupUI()
95-
observeViewModel(binding.whiteboardView)
84+
observeViewModel()
9685

9786
binding.whiteboardView.onNewPath = viewModel::addPath
9887
binding.whiteboardView.onEraseGestureStart = viewModel::startPathEraseGesture
9988
binding.whiteboardView.onEraseGestureMove = viewModel::erasePathsAtPoint
10089
binding.whiteboardView.onEraseGestureEnd = viewModel::endPathEraseGesture
10190
}
10291

103-
override fun onDestroyView() {
104-
super.onDestroyView()
105-
fragmentBinding = null
106-
}
107-
10892
private fun setupUI() {
10993
binding.overflowMenuButton.setOnClickListener {
11094
val popupMenu = PopupMenu(requireContext(), binding.overflowMenuButton)
@@ -148,24 +132,26 @@ class WhiteboardFragment :
148132
/**
149133
* Sets up observers for the ViewModel's state flows.
150134
*/
151-
private fun observeViewModel(whiteboardView: WhiteboardView) {
152-
viewModel.paths.onEach(whiteboardView::setHistory).launchIn(lifecycleScope)
135+
private fun observeViewModel() {
136+
viewModel.paths.onEach(binding.whiteboardView::setHistory).launchIn(lifecycleScope)
153137

154138
combine(
155139
viewModel.brushColor,
156140
viewModel.activeStrokeWidth,
157141
) { color, width ->
158-
whiteboardView.setCurrentBrush(color, width)
142+
if (view == null) return@combine
143+
binding.whiteboardView.setCurrentBrush(color, width)
159144
}.launchIn(lifecycleScope)
160145

161146
combine(
162147
viewModel.isEraserActive,
163148
viewModel.eraserMode,
164149
viewModel.eraserDisplayWidth,
165150
) { isActive, mode, width ->
166-
whiteboardView.isEraserActive = isActive
167-
fragmentBinding?.eraserButton?.updateState(isActive, mode, width)
168-
whiteboardView.eraserMode = mode
151+
if (view == null) return@combine
152+
binding.whiteboardView.isEraserActive = isActive
153+
binding.eraserButton.updateState(isActive, mode, width)
154+
binding.whiteboardView.eraserMode = mode
169155
if (!isActive) {
170156
eraserPopup?.dismiss()
171157
}
@@ -185,7 +171,8 @@ class WhiteboardFragment :
185171

186172
viewModel.isStylusOnlyMode
187173
.onEach { isEnabled ->
188-
whiteboardView.isStylusOnlyMode = isEnabled
174+
if (view == null) return@onEach
175+
binding.whiteboardView.isStylusOnlyMode = isEnabled
189176
}.launchIn(lifecycleScope)
190177

191178
viewModel.toolbarAlignment
@@ -457,21 +444,21 @@ class WhiteboardFragment :
457444
* Updates the toolbar's constraints and orientation.
458445
*/
459446
private fun updateLayoutForAlignment(alignment: ToolbarAlignment) {
460-
val fragmentBinding = fragmentBinding ?: return
447+
if (view == null) return
461448

462449
val isVertical = alignment == ToolbarAlignment.LEFT || alignment == ToolbarAlignment.RIGHT
463-
fragmentBinding.innerControlsLayout.orientation = if (isVertical) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
450+
binding.innerControlsLayout.orientation = if (isVertical) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
464451

465452
if (isVertical) {
466-
fragmentBinding.brushScrollViewHorizontal.visibility = View.GONE
467-
fragmentBinding.brushScrollViewVertical.visibility = View.VISIBLE
453+
binding.brushScrollViewHorizontal.visibility = View.GONE
454+
binding.brushScrollViewVertical.visibility = View.VISIBLE
468455
} else {
469-
fragmentBinding.brushScrollViewHorizontal.visibility = View.VISIBLE
470-
fragmentBinding.brushScrollViewVertical.visibility = View.GONE
456+
binding.brushScrollViewHorizontal.visibility = View.VISIBLE
457+
binding.brushScrollViewVertical.visibility = View.GONE
471458
}
472459

473460
val dp = 1.dp.toPx(requireContext())
474-
val dividerParams = fragmentBinding.controlsDivider.layoutParams as LinearLayout.LayoutParams
461+
val dividerParams = binding.controlsDivider.layoutParams as LinearLayout.LayoutParams
475462
val dividerMargin = 4 * dp
476463
if (isVertical) {
477464
dividerParams.width = LinearLayout.LayoutParams.MATCH_PARENT
@@ -482,11 +469,11 @@ class WhiteboardFragment :
482469
dividerParams.height = LinearLayout.LayoutParams.MATCH_PARENT
483470
dividerParams.setMargins(dividerMargin, 0, dividerMargin, 0)
484471
}
485-
fragmentBinding.controlsDivider.layoutParams = dividerParams
472+
binding.controlsDivider.layoutParams = dividerParams
486473

487474
val constraintSet = ConstraintSet()
488-
constraintSet.clone(fragmentBinding.root)
489-
val containerId = fragmentBinding.controlsContainer.id
475+
constraintSet.clone(binding.root)
476+
val containerId = binding.controlsContainer.id
490477
constraintSet.clear(containerId)
491478

492479
when (alignment) {
@@ -519,7 +506,7 @@ class WhiteboardFragment :
519506
}
520507
}
521508

522-
constraintSet.applyTo(fragmentBinding.root)
509+
constraintSet.applyTo(binding.root)
523510
}
524511

525512
override fun onMenuItemClick(item: MenuItem): Boolean {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2020-2025 Kirill Rozov
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
@file:JvmName("ActivityViewBindings")
18+
19+
package dev.androidbroadcast.vbpd
20+
21+
import android.app.Activity
22+
import android.app.Application.ActivityLifecycleCallbacks
23+
import android.os.Bundle
24+
import android.view.View
25+
import androidx.annotation.IdRes
26+
import androidx.annotation.RestrictTo
27+
import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP
28+
import androidx.viewbinding.ViewBinding
29+
import dev.androidbroadcast.vbpd.internal.findRootView
30+
import dev.androidbroadcast.vbpd.internal.requireViewByIdCompat
31+
import dev.androidbroadcast.vbpd.internal.weakReference
32+
import kotlin.reflect.KProperty
33+
34+
@RestrictTo(LIBRARY_GROUP)
35+
public class ActivityViewBindingProperty<in A : Activity, T : ViewBinding>(
36+
viewBinder: (A) -> T,
37+
) : LazyViewBindingProperty<A, T>(viewBinder) {
38+
private var lifecycleCallbacks: ActivityLifecycleCallbacks? = null
39+
private var activity: Activity? by weakReference(null)
40+
41+
override fun getValue(
42+
thisRef: A,
43+
property: KProperty<*>,
44+
): T =
45+
super
46+
.getValue(thisRef, property)
47+
.also { registerLifecycleCallbacksIfNeeded(thisRef) }
48+
49+
private fun registerLifecycleCallbacksIfNeeded(activity: Activity) {
50+
if (lifecycleCallbacks != null) return
51+
this.activity = activity
52+
VBActivityLifecycleCallbacks()
53+
.also { callbacks -> this.lifecycleCallbacks = callbacks }
54+
.let(activity.application::registerActivityLifecycleCallbacks)
55+
}
56+
57+
override fun clear() {
58+
super.clear()
59+
val lifecycleCallbacks = lifecycleCallbacks
60+
if (lifecycleCallbacks != null) {
61+
activity?.application?.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
62+
}
63+
64+
this.activity = null
65+
this.lifecycleCallbacks = null
66+
}
67+
68+
private inner class VBActivityLifecycleCallbacks : ActivityLifecycleCallbacks {
69+
override fun onActivityCreated(
70+
activity: Activity,
71+
savedInstanceState: Bundle?,
72+
) {
73+
}
74+
75+
override fun onActivityStarted(activity: Activity) {
76+
}
77+
78+
override fun onActivityResumed(activity: Activity) {
79+
}
80+
81+
override fun onActivityPaused(activity: Activity) {
82+
}
83+
84+
override fun onActivityStopped(activity: Activity) {
85+
}
86+
87+
override fun onActivitySaveInstanceState(
88+
activity: Activity,
89+
outState: Bundle,
90+
) {
91+
}
92+
93+
override fun onActivityDestroyed(activity: Activity) {
94+
if (activity === this@ActivityViewBindingProperty.activity) clear()
95+
}
96+
}
97+
}
98+
99+
/**
100+
* Create new [ViewBinding] associated with the [Activity].
101+
* Cached [ViewBinding] will be cleaned after [Activity.onDestroy]
102+
*
103+
* @param viewBinder Function that creates a new instance of [ViewBinding]. Use `MyViewBinding::bind` as default
104+
*
105+
* @return [ViewBindingProperty] associated with the [Activity]'s view
106+
*/
107+
@JvmName("viewBindingActivityWithCallbacks")
108+
@Suppress("UnusedReceiverParameter")
109+
public fun <A : Activity, T : ViewBinding> Activity.viewBinding(viewBinder: (A) -> T): ViewBindingProperty<A, T> =
110+
ActivityViewBindingProperty(viewBinder = viewBinder)
111+
112+
/**
113+
* Create new [ViewBinding] associated with the [Activity].
114+
* Cached [ViewBinding] will be cleaned after [Activity.onDestroy]
115+
*
116+
* @param vbFactory Function that creates a new instance of [ViewBinding]. Use `MyViewBinding::bind` as default
117+
* @param viewProvider Function that provides a root view for the view binding
118+
*
119+
* @return [ViewBindingProperty] associated with the [Activity]'s view
120+
*/
121+
@JvmName("viewBindingActivityWithCallbacks")
122+
public inline fun <A : Activity, T : ViewBinding> Activity.viewBinding(
123+
crossinline vbFactory: (View) -> T,
124+
crossinline viewProvider: (A) -> View = ::findRootView,
125+
): ViewBindingProperty<A, T> = viewBinding { activity -> vbFactory(viewProvider(activity)) }
126+
127+
/**
128+
* Create new [ViewBinding] associated with the [Activity][this] and allow customization of how
129+
* a [View] will be bound to the view binding.
130+
*
131+
* @param vbFactory Function that creates a new instance of [ViewBinding]. `MyViewBinding::bind` can be used
132+
* @param viewBindingRootId Root view's id that will be used as a root for the view binding
133+
*
134+
* @return [ViewBindingProperty] associated with the [Activity]'s view
135+
*/
136+
@JvmName("viewBindingActivity")
137+
public inline fun <A : Activity, T : ViewBinding> Activity.viewBinding(
138+
crossinline vbFactory: (View) -> T,
139+
@IdRes viewBindingRootId: Int,
140+
): ViewBindingProperty<A, T> =
141+
viewBinding { activity ->
142+
vbFactory(activity.requireViewByIdCompat(viewBindingRootId))
143+
}

0 commit comments

Comments
 (0)