
在android应用开发中,直接将r.drawable的整数id存储在外部数据(如json)中并在运行时使用会导致resources$notfoundexception,因为这些id在每次编译后可能不稳定。本文将详细介绍如何通过将drawable的字符串名称存储在数据模型中,并利用context.getresources().getidentifier()方法在运行时动态查找并加载对应的drawable资源,从而优雅地解决这一问题。
Android资源ID的本质与挑战
在Android项目中,我们通过R.drawable.image_name来引用图片资源。这里的R.drawable.image_name实际上是一个由AAPT(Android Asset Packaging Tool)在编译时生成的整数ID。这个ID在每次项目编译时都可能发生变化,因此它并不是一个稳定的、可以长期存储在外部文件(如JSON、数据库或SharedPreferences)中的标识符。
当我们将这些不稳定的整数ID存储在JSON文件中,并在运行时尝试通过ImageView.setImageResource()方法加载时,如果当前的编译生成的ID与JSON中存储的ID不匹配,系统将无法找到对应的资源,从而抛出android.content.res.Resources$NotFoundException异常。
例如,原始代码中尝试直接使用从JSON中读取的整数ID:
// Ingredient.java
public class Ingredient {
// ...
private int ingredientDrawableTag; // 存储的是整数ID
// ...
}
// IngredientRecyclerViewAdapter.java
@Override
public void onBindViewHolder(@NonNull IngredientViewHolder holder, int position) {
holder.imageView.setImageResource(this.ingredients[position].getIngredientDrawableTag()); // 运行时可能找不到资源
}而JSON数据可能如下:
{
"ingredient_name": "Tomato",
"ingredient_drawable_tag": 700003 // 这个ID在下次编译时可能就变了
}这种做法虽然直观,但由于资源ID的不稳定性,在实际应用中极易导致运行时错误。
解决方案:基于名称的动态资源查找
为了解决资源ID不稳定的问题,最佳实践是避免在外部数据中存储资源ID的整数值。取而代之,我们应该存储资源的字符串名称。Android提供了一个API,允许我们根据资源的名称在运行时动态查找其对应的整数ID。
核心思路如下:
- 修改数据模型: 将存储资源ID的字段类型从int改为String,并存储Drawable文件的名称(不带扩展名)。
- 运行时查找: 在需要加载Drawable的地方,使用Context.getResources().getIdentifier()方法,传入Drawable的名称、资源类型("drawable")和包名,动态获取当前的资源ID。
1. 修改数据模型
首先,我们需要更新Ingredient数据类,将ingredientDrawableTag字段的类型从int更改为String。
package me.eyrim.foodrecords2;
import com.google.gson.annotations.SerializedName;
public class Ingredient {
@SerializedName("ingredient_name")
private String ingredientName;
@SerializedName("ingredient_drawable_tag")
private String ingredientDrawableTag; // 类型改为String
public String getIngredientName() {
return this.ingredientName;
}
public String getIngredientDrawableTag() { // 返回类型也改为String
return this.ingredientDrawableTag;
}
}相应的,JSON数据也需要更新,存储Drawable的名称,例如"tomato"对应R.drawable.tomato。
{
"recipe_name": "My test recipe 1 updated",
"recipe_id": "0",
"recipe_desc": "this is a test desc for my test recipe 1",
"ingredients": [{
"ingredient_name": "Tomato",
"ingredient_drawable_tag": "tomato" // 存储Drawable的名称
},
{
"ingredient_name": "Pepper",
"ingredient_drawable_tag": "pepper" // 存储Drawable的名称
}
]
}2. 在RecyclerView适配器中实现动态查找
接下来,在RecyclerView.Adapter的onBindViewHolder方法中,我们需要利用Context实例来调用getResources().getIdentifier()方法。这意味着IngredientRecyclerViewAdapter需要能够访问到Context。
首先,修改适配器的构造函数以接收Context并将其保存为成员变量:
package me.eyrim.foodrecords2.recipeviewactivity; import android.content.Context; // 导入Context import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import me.eyrim.foodrecords2.Ingredient; import me.eyrim.foodrecords2.R; public class IngredientRecyclerViewAdapter extends RecyclerView.Adapter{ private final Ingredient[] ingredients; private final Context context; // 保存Context实例 public IngredientRecyclerViewAdapter(Ingredient[] ingredients, Context context) { this.ingredients = ingredients; this.context = context; // 初始化Context } // ... 其他方法不变 }
然后,在onBindViewHolder方法中,使用context.getResources().getIdentifier()来获取Drawable的实际ID:
public class IngredientRecyclerViewAdapter extends RecyclerView.Adapter{ // ... @Override public void onBindViewHolder(@NonNull IngredientViewHolder holder, int position) { String drawableName = this.ingredients[position].getIngredientDrawableTag(); int drawableId = context.getResources().getIdentifier( drawableName, // Drawable的名称 "drawable", // 资源类型 context.getPackageName() // 应用包名 ); if (drawableId != 0) { // 检查是否成功找到资源 holder.imageView.setImageResource(drawableId); } else { // 处理资源未找到的情况,例如设置一个默认图片 holder.imageView.setImageResource(R.drawable.default_ingredient_image); // 或者记录日志 // Log.w("IngredientAdapter", "Drawable not found for name: " + drawableName); } } // ... }
注意事项与最佳实践
- 资源命名一致性: 确保JSON中存储的Drawable名称(例如"tomato")与项目res/drawable目录下的实际文件名(tomato.png或tomato.xml)完全一致。资源名称是大小写敏感的。
- 处理资源未找到: getIdentifier()方法在找不到对应资源时会返回0。在实际应用中,务必对返回值进行检查,并提供一个备用方案(例如显示一个默认图片、占位符或错误提示),以增强应用的健壮性。
- Context的正确传递: 确保RecyclerView.Adapter能够获取到有效的Context实例。通常,在创建适配器时从Activity或Fragment中传入Context是常见的做法。
- 性能考量: getIdentifier()方法在每次调用时都会进行查找操作。对于包含大量Item且频繁滚动的RecyclerView,如果性能成为瓶颈,可以考虑在数据加载时(例如从JSON解析后)一次性将所有Drawable名称转换为对应的ID并缓存起来,而不是在onBindViewHolder中重复查找。然而,对于大多数常规应用场景,getIdentifier()的性能开销通常在可接受范围内。
- 避免硬编码包名: context.getPackageName()是获取当前应用包名的推荐方式,避免直接将包名字符串硬编码到代码中。
总结
通过将Drawable资源的字符串名称存储在外部数据中,并结合Context.getResources().getIdentifier()方法在运行时动态查找资源ID,我们能够有效地解决Android资源ID不稳定性带来的问题。这种方法不仅提高了应用的健壮性和可维护性,也使得数据模型与UI资源的解耦更加优雅,是Android开发中处理动态资源加载的推荐实践。










