TextView设置文本时存资源ID:Hook final方法实现免重建Locale切换
Hey there! Let's break this down clearly—you're right, you can't override a final method in Java/Kotlin; that's a core language restriction designed to prevent unintended behavior. So we need workarounds to capture that resId when setText(int) is called. Here are the most practical approaches for your library:
1. Custom TextView Subclass (Explicit but Direct)
Instead of fighting the final method, create a wrapper method in your own TextView subclass. This gives you full control over storing the resource ID:
class LocalizedTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.textViewStyle ) : AppCompatTextView(context, attrs, defStyleAttr) { private var currentTextResId: Int? = null fun setLocalizedText(resId: Int) { currentTextResId = resId super.setText(resId) } // Expose a method to update text when locale changes fun updateForNewLocale() { currentTextResId?.let { super.setText(it) } } }
- Pros: Simple to implement, no external dependencies.
- Cons: Requires users to replace all
TextViewinstances in their layouts with your subclass. Not ideal for a library aiming for minimal integration work.
2. Aspect-Oriented Programming (AOP) with AspectJ (Non-Intrusive)
Use AOP to intercept calls to TextView.setText(int) without modifying existing code or layouts. This is perfect for a library since it works transparently:
First, add AspectJ dependencies to your project. Then define an aspect that captures the resId on method calls:
@Aspect class TextViewHookAspect { // WeakHashMap to avoid memory leaks (associate TextView with its last resId) private val textViewResIds = WeakHashMap<TextView, Int>() @Before("call(void android.widget.TextView.setText(int)) && args(resId) && target(textView)") fun beforeSetText(textView: TextView, resId: Int) { textViewResIds[textView] = resId } // Expose a method to update all tracked TextViews when locale changes fun updateAllTextViewsForNewLocale() { textViewResIds.forEach { (textView, resId) -> textView.setText(resId) } } }
- Pros: No changes needed to user layouts or existing code. Works with all
TextViewsubclasses (likeAppCompatTextView,Button, etc.). - Cons: Adds a dependency on AspectJ, which requires some build configuration setup. Also, be cautious with edge cases (e.g.,
setTextcalled with non-string resources, though that's rare).
3. LayoutInflater.Factory2 (Global TextView Replacement)
Hook into the layout inflation process to replace every TextView (and its subclasses) with a wrapped version that tracks the resId. This is another non-intrusive approach:
class LocalizedLayoutInflaterFactory( private val delegate: LayoutInflater.Factory2, private val textViewResIds: WeakHashMap<TextView, Int> ) : LayoutInflater.Factory2 { override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet ): View? { val view = delegate.onCreateView(parent, name, context, attrs) return if (view is TextView) { // Wrap the TextView to intercept setText calls object : TextView(context, attrs) { init { // Copy all properties from the original view // (You'll need to handle text, textColor, etc., manually or use reflection) } override fun setText(resId: Int) { textViewResIds[this] = resId super.setText(resId) } // Override other setText overloads if needed } } else { view } } override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { return onCreateView(null, name, context, attrs) } }
Then, set this factory in your Application class:
class MyApp : Application() { private val textViewResIds = WeakHashMap<TextView, Int>() override fun onCreate() { super.onCreate() LayoutInflater.from(this).factory2 = LocalizedLayoutInflaterFactory( LayoutInflater.from(this).factory2, textViewResIds ) } fun updateAllTextViews() { textViewResIds.forEach { (tv, resId) -> tv.setText(resId) } } }
- Pros: Global coverage, works with all inflated TextViews without user changes.
- Cons: Requires careful handling of view properties to match the original TextView (since you're replacing it). Reflection might be needed to copy some internal state, which can be fragile across Android versions.
Key Considerations
- Memory Leaks: Always use
WeakHashMapto store the TextView-resId association, so TextViews can be garbage collected when they're no longer in use. - Edge Cases: Handle cases where
setTextis called with aCharSequenceinstead of a resId—you might want to track those too (if the CharSequence was originally from a resource) by adding hooks for othersetTextoverloads. - Locale Change Trigger: When the user switches locale, you'll need to trigger your update method (like
updateAllTextViewsForNewLocale()) to refresh all tracked TextViews with the new language resources.
For a library aiming for ease of use, AspectJ or LayoutInflater.Factory2 are your best bets—they minimize the work users need to do to integrate your solution.
内容的提问来源于stack exchange,提问作者Vladyslav Matviienko

