【问题标题】:How should I get Resources(R.string) in viewModel in Android (MVVM and databinding)我应该如何在 Android 的 viewModel 中获取 Resources(R.string)(MVVM 和数据绑定)
【发布时间】:2018-05-17 15:21:42
【问题描述】:

我目前正在为安卓使用databindingMVVM architecture。在 ViewModel 中获取字符串资源的最佳方法是什么。

我没有使用新的AndroidViewModel 组件eventbusRxJava

我正在经历 Activity 将负责提供资源的接口方法。但最近我在this 的答案中发现了一个类似的问题,其中使用应用程序上下文的单个类正在提供所有资源。

哪种方法更好?或者还有什么我可以尝试的吗?

【问题讨论】:

  • 这里的资源是什么意思?用于应用程序的 XML 值,如字符串或在编程中使用的资源,如数据等?
  • @EmreAktürk 是的 XML 值,如字符串

标签: android mvvm android-databinding


【解决方案1】:

创建从 Application 扩展的 MyApplication 类,您可以在每个 Activity 和类中使用。

MyApplication.getContext().getResources().getString(R.string.blabla);

【讨论】:

  • Afaik 这是不可能的!
  • 请尝试@aksh1618
  • 使用 MVVM 架构时无法做到这一点
【解决方案2】:

您可以通过实现 AndroidViewModel 而不是 ViewModel 来访问上下文。

class MainViewModel(application: Application) : AndroidViewModel(application) {
    fun getSomeString(): String? {
        return getApplication<Application>().resources.getString(R.string.some_string)
    }
}

【讨论】:

  • 这不会在配置更改时产生错误(例如区域设置更改)。由于应用程序的资源不知道这些配置更改?
  • 其实 google 开发人员刚刚发布了一篇关于在视图模型中访问资源的中型文章。 medium.com/androiddevelopers/…
  • 不要这样做! @11mo 你是对的,当用户更改设备语言时它会产生错误,但 ViewModel 将引用过时的语言资源。
  • 优先使用 ViewModel 而不是 AndroidViewModel 以避免资源泄漏。
【解决方案3】:

您也可以使用 Resource Id 和 ObservableInt 来完成这项工作。

视图模型

val contentString = ObservableInt()

contentString.set(R.string.YOUR_STRING)

然后你的视图可以得到这样的文本:

android:text="@{viewModel.contentString}"

这样您可以将上下文排除在 ViewModel 之外

【讨论】:

  • @SrishtiRoy 抱歉,应该说内容字符串!
  • 这需要数据绑定。远离它,因为 XML 中的噪音。
  • 如果字符串有一些参数怎么办?
  • 这就是我在 textview 只显示字符串资源时所做的,因为它很简单。当文本可以来自字符串和字符串资源时,不幸的是不能这样做。
【解决方案4】:

您可以使用资源 ID 来完成这项工作。

视图模型

 val messageLiveData= MutableLiveData<Any>()

messageLiveData.value = "your text ..."

messageLiveData.value = R.string.text

然后像这样在片段或活动中使用它:

messageLiveData.observe(this, Observer {
when (it) {
        is Int -> {
            Toast.makeText(context, getString(it), Toast.LENGTH_LONG).show()
        }
        is String -> {
            Toast.makeText(context, it, Toast.LENGTH_LONG).show()
        }
    }
}

【讨论】:

    【解决方案5】:

    只需创建一个使用应用程序上下文获取资源的 ResourceProvider 类。在您的 ViewModelFactory 中使用 App 上下文实例化资源提供程序。您的 Viewmodel 是无上下文的,可以通过模拟 ResourceProvider 轻松测试。

    应用程序

    public class App extends Application {
    
    private static Application sApplication;
    
    @Override
    public void onCreate() {
        super.onCreate();
        sApplication = this;
    
    }
    
    public static Application getApplication() {
        return sApplication;
    }
    

    资源提供者

    public class ResourcesProvider {
    private Context mContext;
    
    public ResourcesProvider(Context context){
        mContext = context;
    }
    
    public String getString(){
        return mContext.getString(R.string.some_string);
    }
    

    视图模型

    public class MyViewModel extends ViewModel {
    
    private ResourcesProvider mResourcesProvider;
    
    public MyViewModel(ResourcesProvider resourcesProvider){
        mResourcesProvider = resourcesProvider; 
    }
    
    public String doSomething (){
        return mResourcesProvider.getString();
    }
    

    ViewModelFactory

    public class ViewModelFactory implements ViewModelProvider.Factory {
    
    private static ViewModelFactory sFactory;
    
    private ViewModelFactory() {
    }
    
    public static ViewModelFactory getInstance() {
        if (sFactory == null) {
            synchronized (ViewModelFactory.class) {
                if (sFactory == null) {
                    sFactory = new ViewModelFactory();
                }
            }
        }
        return sFactory;
    }
    
    @SuppressWarnings("unchecked")
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        if (modelClass.isAssignableFrom(MainActivityViewModel.class)) {
            return (T) new MainActivityViewModel(
                    new ResourcesProvider(App.getApplication())
            );
        }
        throw new IllegalArgumentException("Unknown ViewModel class");
    }
    

    }

    【讨论】:

    • 'Resources' 类不是可模拟的吗?
    • 为什么不在ViewModelFactory 中使用Context 并删除ResourcesProvider 类?
    【解决方案6】:

    对我来说最快最简单的方法是使用 AndroidViewModel 代替 ViewModel:

    在您的 ViewModel (Kotlin) 中

    val resources = getApplication<Application>().resources
    
    // Then access it with
    resources.getString(R.string.myString)
    

    在您的 ViewModel (Java) 中

    getApplication().getResources().getString(status)
    

    【讨论】:

    • 这只能在AndroidViewModel 中,而不是在ViewModel
    【解决方案7】:

    理想情况下,应该使用数据绑定,通过解析 xml 文件中的字符串可以轻松解决此问题。但是在现有项目中实现数据绑定可能太多了。

    对于这样的情况,我创建了以下类。它涵盖了所有带或不带参数的字符串情况,并且不需要 viewModel 扩展 AndroidViewModel 并且这种方式也涵盖了 Locale 更改的事件。

    class ViewModelString private constructor(private val string: String?,
                                              @StringRes private val stringResId: Int = 0,
                                              private val args: ArrayList<Any>?){
    
        //simple string constructor
        constructor(string: String): this(string, 0, null)
    
        //convenience constructor for most common cases with one string or int var arg
        constructor(@StringRes stringResId: Int, stringVar: String): this(null, stringResId, arrayListOf(stringVar))
        constructor(@StringRes stringResId: Int, intVar: Int): this(null, stringResId, arrayListOf(intVar))
    
        //constructor for multiple var args
        constructor(@StringRes stringResId: Int, args: ArrayList<Any>): this(null, stringResId, args)
    
        fun resolve(context: Context): String {
            return when {
                string != null -> string
                args != null -> return context.getString(stringResId, *args.toArray())
                else -> context.getString(stringResId)
            }
        }
    }
    

    用法

    例如,我们有这个带有两个参数的资源字符串

    <string name="resource_with_args">value 1: %d and value 2: %s </string>
    

    在 ViewModel 类中:

    myViewModelString.value = ViewModelString(R.string.resource_with_args, arrayListOf(val1, val2))
    

    在 Fragment 类中(或任何有可用上下文的地方)

    textView.text = viewModel.myViewModelString.value?.resolve(context)
    

    请记住,*args.toArray() 上的 * 不是输入错误,因此请不要删除它。它的语法将数组表示为Object...objects,Android 内部使用它而不是Objects[] objects,这会导致崩溃。

    【讨论】:

    • 我们如何测试返回 ViewModelString 的视图模型?
    【解决方案8】:

    使用 Hilt 的 Bozbi 答案的更新版本

    ViewModel.kt

    @HiltViewModel
    class MyViewModel @Inject constructor(
        private val resourcesProvider: ResourcesProvider
    ) : ViewModel() {
        ...
        fun foo() {
            val helloWorld: String = resourcesProvider.getString(R.string.hello_world)
        }
        ...
    }
    

    ResourcesProvider.kt

    @Singleton
    class ResourcesProvider @Inject constructor(
        @ApplicationContext private val context: Context
    ) {
        fun getString(@StringRes stringResId: Int): String {
            return context.getString(stringResId)
        }
    }
    

    【讨论】:

    • 如果用户更改了应用程序的语言设置,这种方法是不是会在之前用户语言选择的基础上返回字符串值?例如,如果我以英语作为首选语言运行我的应用程序,然后决定将语言首选项更改为西班牙语,则 ResourceProvider 仍将返回英语字符串文字。
    • 而不是 Singleton 使用 ViewModelScoped
    【解决方案9】:

    一点也不。

    资源字符串操作属于 View 层,而不是 ViewModel 层。

    ViewModel 层应该不依赖于Context 和资源。定义 ViewModel 将发出的数据类型(类或枚举)。 DataBinding 可以访问 Context 和资源,并且可以在那里解决它。通过@BindingAdapter(如果你想要干净的外观)或一个普通的静态方法(如果你想要灵活性和冗长),它接受枚举和Context并返回Stringandroid:text="@{MyStaticConverter.someEnumToString(viewModel.someEnum, context)}"。 (context 是每个绑定表达式中的合成参数)

    但在大多数情况下,String.format 足以将资源字符串格式与 ViewModel 提供的数据结合起来。

    这可能看起来“在 XML 中太多了”,但 XML 和绑定是视图层。如果您丢弃上帝对象,视图逻辑的唯一位置:活动和片段。

    //编辑 - 更详细的示例(kotlin):

    object MyStaticConverter {  
        @JvmStatic
        fun someEnumToString(type: MyEnum?, context: Context): String? {
            return when (type) {
                null -> null
                MyEnum.EENY -> context.getString(R.string.some_label_eeny)
                MyEnum.MEENY -> context.getString(R.string.some_label_meeny)
                MyEnum.MINY -> context.getString(R.string.some_label_miny)
                MyEnum.MOE -> context.getString(R.string.some_label_moe)
            }
        }
    }
    

    在 XML 中的用法:

    <data>
        <import type="com.example.MyStaticConverter" />
    </data>
    ...
    <TextView
        android:text="@{MyStaticConverter.someEnumToString(viewModel.someEnum, context)}".
    

    对于更复杂的情况(例如将资源标签与 API 中的文本混合)而不是枚举,请使用密封类,它将动态 String 从 ViewModel 传递到将进行组合的转换器。

    “转换器”(一组不相关、静态和无状态的函数)是我经常使用的一种模式。它允许让所有 Android 的 View 相关类型远离 ViewModel,并在整个应用程序中重复使用小的、重复的部分(如将 bool 或各种状态转换为 VISIBILITY 或格式化数字、日期、距离、百分比等)。这消除了许多重叠@BindingAdapters 的需要,恕我直言,增加了 XML 代码的可读性。

    【讨论】:

    • 这个MyStaticConverter 会是什么样子?
    • @Starwave 添加示例
    【解决方案10】:

    我不使用数据绑定,但我想你可以为我的解决方案添加一个适配器。

    我在视图模型中保留资源 ID

    class ExampleViewModel: ViewModel(){
      val text = MutableLiveData<NativeText>(NativeText.Resource(R.String.example_hi))
    }
    

    并在视图层上获取文本。

    viewModel.text.observe(this) { text
      textView.text = text.toCharSequence(this)
    }
    

    您可以在the article阅读更多关于原生文本的信息

    【讨论】:

      【解决方案11】:

      对于您不想重构的旧代码,您可以创建一个 ad-hoc 类

      private typealias ResCompat = AppCompatResources
      
      @Singleton
      class ResourcesDelegate @Inject constructor(
          @ApplicationContext private val context: Context,
      ) {
      
          private val i18nContext: Context
              get() = LocaleSetter.createContextAndSetDefaultLocale(context)
      
          fun string(@StringRes resId: Int): String = i18nContext.getString(resId)
      
          fun drawable(@DrawableRes resId: Int): Drawable? = ResCompat.getDrawable(i18nContext, resId)
      
      }
      

      然后在您的AndroidViewModel 中使用它。

      @HiltViewModel
      class MyViewModel @Inject constructor(
          private val resourcesDelegate: ResourcesDelegate
      ) : AndroidViewModel() {
          
          fun foo() {
              val helloWorld: String = resourcesDelegate.string(R.string.hello_world)
          }
      

      【讨论】:

        【解决方案12】:

        如果您使用 Dagger Hilt,那么 @ApplicationContext context: Context 在您的 viewModel 构造函数中将起作用。 Hilt 可以使用此注解自动注入应用程序上下文。如果您使用的是 dagger,那么您应该通过模块类提供上下文,然后注入 viewModel 构造函数。最后使用该上下文,您可以访问字符串资源。比如 context.getString(R.strings.name)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-06-14
          • 2017-11-28
          • 2021-05-21
          • 1970-01-01
          • 2017-11-17
          • 2018-12-29
          • 1970-01-01
          相关资源
          最近更新 更多