简体   繁体   English

"我应该如何在 Android 的 viewModel 中获取 Resources(R.string)(MVVM 和数据绑定)"

[英]How should I get Resources(R.string) in viewModel in Android (MVVM and databinding)

I am currently using databinding<\/code> and MVVM architecture<\/code> for android.我目前正在为 android 使用databinding<\/code>和MVVM architecture<\/code> 。 What would be the best way to get string resources in ViewModel.在 ViewModel 中获取字符串资源的最佳方法是什么。

I am not using the new AndroidViewModel<\/code> component, eventbus<\/code> or RxJava<\/code><\/strong>我没有使用新的AndroidViewModel<\/code>组件、事件RxJava<\/code>或eventbus<\/code><\/strong>

I was going through the aproach of interfaces where Activity will be responsible for providing resources.我正在经历 Activity 将负责提供资源的接口方法。 But recently I found a similar question with this<\/a> answer where a single class using application context is providing all resources.但最近我发现了一个与此<\/a>答案类似的问题,其中使用应用程序上下文的单个类提供所有资源。

Which would be the better approach?哪种方法更好? or is there something else that I can try?或者还有什么我可以尝试的吗?

"

You can access the context by implementing AndroidViewModel instead of ViewModel.您可以通过实现 AndroidViewModel 而不是 ViewModel 来访问上下文。

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

You can also use the Resource Id and ObservableInt to make this work.您还可以使用 Resource Id 和 ObservableInt 来完成这项工作。

ViewModel :视图模型

val contentString = ObservableInt()

contentString.set(R.string.YOUR_STRING)

And then your view can get the text like this:然后你的视图可以得到这样的文本:

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

This way you can keep the context out of your ViewModel通过这种方式,您可以将上下文排除在 ViewModel 之外

Just create a ResourceProvider class that fetch resources using Application context.只需创建一个使用应用程序上下文获取资源的 ResourceProvider 类。 In your ViewModelFactory instantiate the resource provider using App context.在您的 ViewModelFactory 中,使用 App 上下文实例化资源提供者。 You're Viewmodel is Context free and can be easily testable by mocking the ResourceProvider.您的 Viewmodel 是无上下文的,并且可以通过模拟 ResourceProvider 轻松进行测试。

Application应用

public class App extends Application {

private static Application sApplication;

@Override
public void onCreate() {
    super.onCreate();
    sApplication = this;

}

public static Application getApplication() {
    return sApplication;
}

ResourcesProvider资源提供者

public class ResourcesProvider {
private Context mContext;

public ResourcesProvider(Context context){
    mContext = context;
}

public String getString(){
    return mContext.getString(R.string.some_string);
}

ViewModel视图模型

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");
}

} }

You can use the Resource Id to make this work.您可以使用资源 ID 来完成这项工作。

ViewModel视图模型

 val messageLiveData= MutableLiveData<Any>()

messageLiveData.value = "your text ..."

or或者

messageLiveData.value = R.string.text

And then use it in fragment or activity like this:然后在片段或活动中使用它,如下所示:

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()
        }
    }
}

an updated version of Bozbi's answer using Hilt使用 Hilt 的 Bozbi 答案的更新版本

ViewModel.kt视图模型.kt

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

ResourcesProvider.kt资源提供者.kt

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

Ideally Data Binding should be used with which this problem can easily be solved by resolving the string inside the xml file.理想情况下,应该使用数据绑定,通过解析 xml 文件中的字符串可以轻松解决此问题。 But implementing data binding in an existing project can be too much.但是在现有项目中实现数据绑定可能太多了。

For a case like this I created the following class.对于这样的情况,我创建了以下类。 It covers all cases of strings with or without arguments and it does NOT require for the viewModel to extend AndroidViewModel and this way also covers the event of Locale change.它涵盖了带或不带参数的所有字符串情况,并且不需要 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)
        }
    }
}

USAGE用法

for example we have this resource string with two arguments例如,我们有这个带有两个参数的资源字符串

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

In ViewModel class:在 ViewModel 类中:

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

In Fragment class (or anywhere with available context)在 Fragment 类(或任何具有可用上下文的地方)

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

Keep in mind that the * on *args.toArray() is not a typing mistake so do not remove it.请记住,在**args.toArray()是不是打错了,所以不要删除它。 It is syntax that denotes the array as Object...objects which is used by Android internaly instead of Objects[] objects which would cause a crash.将数组表示为Object...objects语法是 Android 内部使用的Object...objects ,而不是会导致崩溃的Objects[] objects

I don't use data bindig but I guess you can add an adapter for my solution.我不使用数据绑定,但我想您可以为我的解决方案添加一个适配器。

I keep resource ids in the view model我在视图模型中保留资源 ID

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

and get text on a view layer.并在视图层上获取文本。

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

You can read more about native text in the article您可以在文章中阅读有关原生文本的更多信息

Not at all.一点也不。

Resource string manipulation belongs the View layer, not ViewModel layer.资源字符串操作属于 View 层,而不是 ViewModel 层。

ViewModel layer should be free from dependencies to both Context and resources. ViewModel 层应该不依赖于Context和资源。 Define a data type (a class or enum) that ViewModel will emit.定义 ViewModel 将发出的数据类型(类或枚举)。 DataBinding has access to both Context and resources and can resolve it there. DataBinding 可以访问 Context 和资源,并且可以在那里解决它。 Either via @BindingAdapter (if you want the clean look) or a plain static method (if you want flexibility and verbosity) that takes the enum and Context and returns String : android:text="@{MyStaticConverter.someEnumToString(viewModel.someEnum, context)}" .通过@BindingAdapter (如果你想要干净的外观)或一个简单的静态方法(如果你想要灵活性和详细性),它接受枚举和Context并返回Stringandroid:text="@{MyStaticConverter.someEnumToString(viewModel.someEnum, context)}" ( context is synthetic param in every binding expression) context是每个绑定表达式中的合成参数)

But in most cases, String.format is enough to combine resource string format with data provided by ViewModel.但在大多数情况下, String.format足以将资源字符串格式与 ViewModel 提供的数据结合起来。

It may seem like "too much in XML", but XML and bindings are the View layer.看起来“在 XML 中太多了”,但 XML 和绑定是视图层。 The only places for view logic, if you discard god-objects: Activities and Fragments.如果您丢弃上帝对象,则视图逻辑的唯一位置:活动和片段。

//edit - more detailed example (kotlin): //编辑 - 更详细的例子(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)
        }
    }
}

usage in XML: XML 中的用法:

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

For more complicated cases (like mixing resource labels with texts from API) instead of enum use sealed class that will carry the dynamic String from ViewModel to the converter that will do the combining.对于更复杂的情况(例如将资源标签与来自 API 的文本混合),请使用密封类而不是枚举,该类将携带来自 ViewModel 的动态String到将进行组合的转换器。

"Converters" (a collection of unrelated, static and stateless functions) is a pattern that I use a lot. “转换器”(一组无关的、静态的和无状态的函数)是我经常使用的一种模式。 It allows to keep all the Android's View -related types away from ViewModel and reuse of small, repetitive parts across entire app (like converting bool or various states to VISIBILITY or formatting numbers, dates, distances, percentages, etc).它允许让所有 Android 的View相关类型远离 ViewModel 并在整个应用程序中重复使用小的重复部分(例如将 bool 或各种状态转换为 VISIBILITY 或格式化数字、日期、距离、百分比等)。 That removes the need of many overlapping @BindingAdapter s and IMHO increases readability of the XML-code.这消除了许多重叠@BindingAdapter的需要,恕我直言,增加了 XML 代码的可读性。

For old code which you don't want to refactor you can create an ad-hoc class as such对于您不想重构的旧代码,您可以创建一个临时类

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)

}

and then use it inside your AndroidViewModel .然后在您的AndroidViewModel使用它。

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

If you are using Dagger Hilt then @ApplicationContext context: Context in your viewModel constructor will work.如果您使用的是 Dagger Hilt,那么 @ApplicationContext context: Context 在您的 viewModel 构造函数中将起作用。 Hilt can automatically inject application context with this annotation. Hilt 可以使用此注释自动注入应用程序上下文。 If you are using dagger then you should provide context through module class and then inject in viewModel constructor.如果您使用的是 dagger,那么您应该通过模块类提供上下文,然后注入 viewModel 构造函数。 Finally using that context you can access the string resources.最后使用该上下文您可以访问字符串资源。 like context.getString(R.strings.name)像 context.getString(R.strings.name)

The quickest and easiest way for me was to use AndroidViewModel instead of ViewModel:对我来说最快最简单的方法是使用 AndroidViewModel 而不是 ViewModel:

In your ViewModel (Kotlin)在您的 ViewModel (Kotlin) 中

val resources = getApplication<Application>().resources

// Then access it with
resources.getString(R.string.myString)

In your ViewModel (Java)在您的 ViewModel (Java)

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

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM