You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TextView设置文本时存资源ID:Hook final方法实现免重建Locale切换

How to Hook into TextView.setText(int) for Locale Switching Without Activity Recreation

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 TextView instances 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 TextView subclasses (like AppCompatTextView, Button, etc.).
  • Cons: Adds a dependency on AspectJ, which requires some build configuration setup. Also, be cautious with edge cases (e.g., setText called 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 WeakHashMap to store the TextView-resId association, so TextViews can be garbage collected when they're no longer in use.
  • Edge Cases: Handle cases where setText is called with a CharSequence instead of a resId—you might want to track those too (if the CharSequence was originally from a resource) by adding hooks for other setText overloads.
  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:28:34