19、DataBinding与多语言:动态语言切换、资源文件绑定、RTL布局适配、字符串格式化
多语言支持,说白了就是让你的App能跟不同国家的人“说人话”。
我早年做海外项目时,最头疼的不是功能逻辑,而是语言切换那点破事——用户切了语言,界面纹丝不动,还得重启App。嗯,那体验简直灾难。后来用上DataBinding,这事儿才算真正理顺了。
19.1 资源文件绑定的底层逻辑
DataBinding对多语言的支持,其实比你想象的要深。它不只是把@string/xxx塞进布局里那么简单。
先看最基础的写法:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{@string/greeting}" />
这行代码,DataBinding会在编译时生成对应的getString()调用。但注意——它绑定的不是静态字符串,而是当前Configuration下的资源引用。
为什么会这样?因为DataBinding生成的Binding类里,会监听Locale变化。你想想看,如果你手动改了语言,Activity重建后,Binding会重新读取资源文件,自动匹配新的语言包。
核心要点:DataBinding的@string绑定,本质上是延迟解析。它不存字符串值,只存资源ID。每次渲染时,从当前Configuration里取对应语言的文本。
19.2 动态语言切换:不重启App的方案
我见过很多团队做语言切换,直接重启Activity,甚至重启App。用户正填表单呢,啪一下没了,你说气不气人?
我个人习惯的做法是:只更新Configuration,不重建Activity。配合DataBinding,能做到无缝切换。
核心代码长这样:
fun applyLocale(context: Context, locale: Locale) {
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
context.resources.updateConfiguration(config, context.resources.displayMetrics)
}
然后在Activity里:
binding?.apply {
// 重新绑定,触发所有@string刷新
invalidateAll()
}
invalidateAll()会通知所有绑定的表达式重新求值。注意,这里有个坑——只有通过@{@string/xxx}绑定的文本才会刷新。如果你在Java/Kotlin代码里硬编码了字符串,那DataBinding管不了。
我曾经踩过的坑:在ViewModel里用LiveData<String>存了翻译后的文本,结果语言切换后,LiveData的值没变,UI也不更新。后来改成存资源ID,用Transformations.map实时转换,才彻底解决。
19.3 RTL布局适配:不只是镜像
阿拉伯语、希伯来语这些从右往左读的语言,适配起来比想象中麻烦。很多人以为设个android:layoutDirection="rtl"就完事了,其实远远不够。
DataBinding里处理RTL,我建议遵循几个原则:
- 用
start/end代替left/right——这是最基础的,但很多人忘。DataBinding里也一样,android:paddingStart比android:paddingLeft安全。 - 图标和箭头要翻转——比如返回箭头,LTR下朝左,RTL下朝右。可以用
@drawable/ic_back_rtl这种命名,在资源文件里区分。 - 文本对齐用
textAlignment="viewStart"——别写死left或right。
在DataBinding表达式里,你可以这样判断当前布局方向:
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:scaleX="@{ContextCompat.getInteger(context.resources.configuration,
android.R.integer.config_layoutDirection) == 1 ? -1f : 1f}"
android:src="@drawable/ic_arrow" />
嗯,这写法有点丑。我一般会在ViewModel里封装一个isRtl的LiveData,然后在布局里直接用:
android:scaleX="@{viewModel.isRtl ? -1f : 1f}"
小技巧:测试RTL布局时,不用真机切语言。开发者选项里有个“强制使用从右到左布局”,打开就能看到效果。我每次适配完都会用这个跑一遍,省得来回切换系统语言。
19.4 字符串格式化:参数化与复数
多语言里最烦人的是什么?是占位符的位置不一样。
中文说“你有3条新消息”,英语说“You have 3 new messages”。数字的位置不同,你不能在代码里拼字符串。
Android资源文件支持%s、%d这类占位符。DataBinding里怎么用?
先在strings.xml里定义:
<string name="new_messages">你有 %d 条新消息</string>
然后在布局里:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{@string/new_messages(viewModel.messageCount)}" />
注意这个语法——@string/new_messages(viewModel.messageCount)。DataBinding会把messageCount的值传进去,替换%d。
如果有多个参数:
<string name="welcome_back">欢迎回来,%s!上次登录:%s</string>
<TextView
android:text="@{@string/welcome_back(viewModel.userName, viewModel.lastLoginTime)}" />
参数顺序跟strings.xml里%s出现的顺序一致。但注意——不同语言的参数顺序可能不同。比如英语可能是“Welcome back, %s! Last login: %s”,阿拉伯语可能把时间放在名字前面。这时候用%1$s、%2$s指定位置:
<string name="welcome_back">%2$s,欢迎回来!上次登录:%1$s</string>
这样不管语言怎么变,参数位置都不会乱。
复数形式的处理
英语里“1 message”和“2 messages”不一样,中文倒没这问题。但如果你做海外市场,plurals必须用起来。
资源文件:
<plurals name="message_count">
<item quantity="one">%d message</item>
<item quantity="other">%d messages</item>
</plurals>
DataBinding里绑定:
android:text="@{@plurals/message_count(viewModel.messageCount, viewModel.messageCount)}"
注意这里传了两次参数——第一个是数量值(用于选择复数形式),第二个是格式化用的值。这是Android API的设计,DataBinding只是照搬。
避坑指南:复数形式里,one、other这些标签名是固定的。中文虽然只有一种形式,但也要写other标签,否则会崩溃。我曾经因为这个被QA提过bug,嗯,很尴尬。
19.5 知识体系总览
下面这张图,是我对DataBinding多语言支持的整体理解。你可以把它当作一个检查清单:
19.6 实战中的几个建议
最后,分享几个我实际项目里总结出来的经验:
- 所有用户可见文本,必须走资源文件——哪怕你只做中文版。因为一旦要加英文,你不可能去代码里翻硬编码的字符串。
- 语言切换后,记得刷新当前页面的Binding——
invalidateAll()是最简单的方式。如果页面层级深,考虑用notifyPropertyChanged(BR.all)。 - RTL适配不要等到最后——我见过太多团队,功能做完了才想起来适配RTL,结果布局全乱套。从一开始就用
start/end,能省很多事。 - 复数形式一定要测试边界值——0、1、2、100,每个语言的处理方式都不一样。阿拉伯语甚至有6种复数形式,嗯,那才是真正的噩梦。
多语言这件事,说白了就是把文本的控制权交给资源文件,把刷新的控制权交给DataBinding。你只要搭好框架,剩下的就是翻译人员的事了。