MVVM 注入 Activity 字符串资源 reader
MVVM injecting Activity string resource reader
因此,每个人 都知道将上下文引用(不是应用程序)传递给 ViewModel
是一件坏事。在我的例子中,有一些项目使用 android 字符串资源表示按字母顺序排列(因此,要阅读它,我需要一个 Activity 上下文)。
推荐的方法是什么?将项目列表从 ViewModel
传递到 Activity,以读取这些字符串并返回到 ViewModel
确实看起来有点不像 MVVM-ish
,并注入 ViewModel
字符串资源 reader 会泄漏 Context..
有什么想法吗?
一个选项是从 AndroidViewModel 扩展,它引用了应用程序上下文。然后您可以使用 Context
加载字符串资源并将它们传送回您的 Activity
.
public class MyViewModel extends AndroidViewModel {
private final LiveData<String> stringResource = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
statusLabel.setValue(context.getString(R.string.labelString));
}
public LiveData<String> getStringResource() {
return stringResource;
}
}
但是,正如此 Android Developers Medium Post by Jose Alcerreca 中所指出的那样,这不是推荐的做法,因为例如,如果 Locale
发生变化并且 Activity 得到重建,则ViewModel 不会对此配置更改做出反应,并将继续提供过时的字符串(来自之前的 Locale
)。
因此,建议的方法是仅 return 来自 ViewModel 的资源 ID 并获取 Activity 上的字符串。
public class MyViewModel extends ViewModel {
public final LiveData<Integer> stringResource = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
stringResource.setValue(R.string.labelString);
}
public LiveData<Integer> getStringResource() {
return stringResource;
}
}
更新
由于您必须从 Activity
中获取字符串资源,但在 ViewModel
中应用排序逻辑,我认为您无法避免传递 List<String>
回到你的 ViewModel
进行排序:
public class MyViewModel extends ViewModel {
public final MutableLiveData<Integer> stringArrayId = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
stringArrayId.setValue(R.array.string_array_id);
}
public LiveData<Integer> getStringArrayId() {
return stringArrayId;
}
}
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
viewModel.getStringArrayId().observe(this, strArrayId -> {
String[] resolvedStrings = getResources().getStringArray(strArrayId);
List<String> sortedStrings = viewModel.sortList(Arrays.asList(resolvedStrings));
updateUi(sortedStrings);
});
}
}
如果您认为这还不够 MVVM,也许您可以在 ViewModel
中继续解析 List<String>
,并在排序列表中添加一个额外的 LiveData
,这将被更新每次保存原始字符串列表的 LiveData
发生变化时。
public class MyViewModel extends ViewModel {
public final MutableLiveData<Integer> stringArrayId = new MutableLiveData<>();
public final MutableLiveData<List<String>> stringsList = new MutableLiveData<>();
public final LiveData<List<String>> sortedStringList;
public MyViewModel(Application context) {
super(context);
stringArrayId.setValue(R.array.string_array_id);
sortedStringList = Transformations.map(stringsList, l -> {
Collections.sort(l);
return l;
});
}
public LiveData<Integer> getStringArrayId() {
return stringArrayId;
}
public LiveData<List<String>> sortedStringList() {
return sortedStringList;
}
public void setStringsList(List<String> resolvedStrings) {
stringsList.setValue(resolvedStrings);
}
}
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
viewModel.getStringArrayId().observe(this, strArrayId -> {
String[] resolvedStrings = getResources().getStringArray(strArrayId);
viewModel.setStringsList(Arrays.asList(resolvedStrings));
});
viewModel.sortedStringList().observe(this, sortedStrings -> updateUi(sortedStrings));
}
}
我觉得设计过度了,您仍然必须将 List<String>
发送回您的 ViewModel
。但是,如果排序顺序取决于可以在运行时更改的过滤器,那么采用这种方式可能会有所帮助。然后,您可以添加一个 MediatorLiveData
以在 Filter 更改或字符串列表更改时做出反应,然后您的视图只需将这些更改通知 ViewModel
并观察排序列表。
理想情况下应该使用数据绑定,通过解析 xml 文件中的字符串可以轻松解决此问题。但是在现有项目中实现数据绑定可能会太多。
对于这样的案例,我创建了以下 class。它涵盖了带或不带参数的所有字符串情况,并且不需要 viewModel 扩展 AndroidViewModel,并且这种方式还涵盖了区域设置更改的事件。
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 中 class:
myViewModelString.value = ViewModelString(R.string.resource_with_args, arrayListOf(val1, val2))
在片段中 class(或任何具有可用上下文的地方)
textView.text = viewModel.myViewModelString.value?.resolve(context)
请记住 *args.toArray()
上的 *
不是打字错误,因此不要删除它。它是将数组表示为 Object...objects
的语法,它被 Android 内部使用,而不是 Objects[] objects
,后者会导致崩溃。
因此,每个人 都知道将上下文引用(不是应用程序)传递给 ViewModel
是一件坏事。在我的例子中,有一些项目使用 android 字符串资源表示按字母顺序排列(因此,要阅读它,我需要一个 Activity 上下文)。
推荐的方法是什么?将项目列表从 ViewModel
传递到 Activity,以读取这些字符串并返回到 ViewModel
确实看起来有点不像 MVVM-ish
,并注入 ViewModel
字符串资源 reader 会泄漏 Context..
有什么想法吗?
一个选项是从 AndroidViewModel 扩展,它引用了应用程序上下文。然后您可以使用 Context
加载字符串资源并将它们传送回您的 Activity
.
public class MyViewModel extends AndroidViewModel {
private final LiveData<String> stringResource = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
statusLabel.setValue(context.getString(R.string.labelString));
}
public LiveData<String> getStringResource() {
return stringResource;
}
}
但是,正如此 Android Developers Medium Post by Jose Alcerreca 中所指出的那样,这不是推荐的做法,因为例如,如果 Locale
发生变化并且 Activity 得到重建,则ViewModel 不会对此配置更改做出反应,并将继续提供过时的字符串(来自之前的 Locale
)。
因此,建议的方法是仅 return 来自 ViewModel 的资源 ID 并获取 Activity 上的字符串。
public class MyViewModel extends ViewModel {
public final LiveData<Integer> stringResource = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
stringResource.setValue(R.string.labelString);
}
public LiveData<Integer> getStringResource() {
return stringResource;
}
}
更新
由于您必须从 Activity
中获取字符串资源,但在 ViewModel
中应用排序逻辑,我认为您无法避免传递 List<String>
回到你的 ViewModel
进行排序:
public class MyViewModel extends ViewModel {
public final MutableLiveData<Integer> stringArrayId = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
stringArrayId.setValue(R.array.string_array_id);
}
public LiveData<Integer> getStringArrayId() {
return stringArrayId;
}
}
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
viewModel.getStringArrayId().observe(this, strArrayId -> {
String[] resolvedStrings = getResources().getStringArray(strArrayId);
List<String> sortedStrings = viewModel.sortList(Arrays.asList(resolvedStrings));
updateUi(sortedStrings);
});
}
}
如果您认为这还不够 MVVM,也许您可以在 ViewModel
中继续解析 List<String>
,并在排序列表中添加一个额外的 LiveData
,这将被更新每次保存原始字符串列表的 LiveData
发生变化时。
public class MyViewModel extends ViewModel {
public final MutableLiveData<Integer> stringArrayId = new MutableLiveData<>();
public final MutableLiveData<List<String>> stringsList = new MutableLiveData<>();
public final LiveData<List<String>> sortedStringList;
public MyViewModel(Application context) {
super(context);
stringArrayId.setValue(R.array.string_array_id);
sortedStringList = Transformations.map(stringsList, l -> {
Collections.sort(l);
return l;
});
}
public LiveData<Integer> getStringArrayId() {
return stringArrayId;
}
public LiveData<List<String>> sortedStringList() {
return sortedStringList;
}
public void setStringsList(List<String> resolvedStrings) {
stringsList.setValue(resolvedStrings);
}
}
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
viewModel.getStringArrayId().observe(this, strArrayId -> {
String[] resolvedStrings = getResources().getStringArray(strArrayId);
viewModel.setStringsList(Arrays.asList(resolvedStrings));
});
viewModel.sortedStringList().observe(this, sortedStrings -> updateUi(sortedStrings));
}
}
我觉得设计过度了,您仍然必须将 List<String>
发送回您的 ViewModel
。但是,如果排序顺序取决于可以在运行时更改的过滤器,那么采用这种方式可能会有所帮助。然后,您可以添加一个 MediatorLiveData
以在 Filter 更改或字符串列表更改时做出反应,然后您的视图只需将这些更改通知 ViewModel
并观察排序列表。
理想情况下应该使用数据绑定,通过解析 xml 文件中的字符串可以轻松解决此问题。但是在现有项目中实现数据绑定可能会太多。
对于这样的案例,我创建了以下 class。它涵盖了带或不带参数的所有字符串情况,并且不需要 viewModel 扩展 AndroidViewModel,并且这种方式还涵盖了区域设置更改的事件。
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 中 class:
myViewModelString.value = ViewModelString(R.string.resource_with_args, arrayListOf(val1, val2))
在片段中 class(或任何具有可用上下文的地方)
textView.text = viewModel.myViewModelString.value?.resolve(context)
请记住 *args.toArray()
上的 *
不是打字错误,因此不要删除它。它是将数组表示为 Object...objects
的语法,它被 Android 内部使用,而不是 Objects[] objects
,后者会导致崩溃。