
引言:管理近期使用食谱
在许多应用程序中,显示用户近期查看或操作过的项目是一项常见且有用的功能,例如电商应用的“最近浏览”、文档编辑器的“最近打开文件”等。对于食谱应用而言,实现一个“近期食谱”功能可以显著提升用户体验,允许用户快速回溯他们感兴趣的食谱。本教程将以一个食谱应用为例,演示如何高效地管理并获取最近使用的食谱索引。
假设我们有一个食谱数据库 ReceiptsBase,其中包含多个食谱,每个食谱由一个 ArrayList
核心概念:固定大小的近期列表
要实现“近期使用”功能,最核心的思路是维护一个固定大小的数据结构(如数组或 ArrayList),当用户使用(查看)某个食谱时,将其添加到这个数据结构中。当数据结构已满时,需要采用一种“滑动窗口”机制:移除最旧的元素,并添加新的元素,从而始终保持列表中存储的是最新的N个项目。
在这个场景中,由于我们的食谱存储在 ReceiptsBase 的 recipesAll 中,并且通过索引访问,因此在“近期食谱”列表中存储食谱的索引是最有效的方式。
实现策略:基于数组/列表的滑动窗口
我们将创建一个专门的类来管理近期使用的食谱索引。这个类将包含一个固定大小的 ArrayList 或数组来存储这些索引。
立即学习“Java免费学习笔记(深入)”;
1. 定义 RecentRecipesManager 类
首先,我们创建一个 RecentRecipesManager 类,它将负责存储、添加和获取近期食谱的索引。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RecentRecipesManager {
private static final int MAX_RECENT_RECIPES = 3; // 定义近期食谱的最大数量
private List recentRecipeIndices; // 存储近期食谱的索引
public RecentRecipesManager() {
// 使用 LinkedList 在添加/删除两端元素时效率更高
// 如果对访问中间元素有较高要求,也可以使用 ArrayList
recentRecipeIndices = new ArrayList<>();
}
/**
* 将一个食谱索引添加到近期食谱列表。
* 如果食谱已在列表中,则将其移到最前面(表示最近使用)。
* 如果列表已满,则移除最旧的食谱。
*
* @param recipeIndex 要添加的食谱索引
*/
public void addRecipeToRecent(int recipeIndex) {
// 1. 移除现有条目(如果已存在),确保不重复且是最新使用
recentRecipeIndices.remove(Integer.valueOf(recipeIndex));
// 2. 将新食谱索引添加到列表的最前面 (索引0)
recentRecipeIndices.add(0, recipeIndex);
// 3. 如果列表大小超过最大限制,则移除最旧的食谱 (列表末尾)
if (recentRecipeIndices.size() > MAX_RECENT_RECIPES) {
recentRecipeIndices.remove(recentRecipeIndices.size() - 1);
}
}
/**
* 获取当前近期食谱的索引列表。
* 返回的列表是不可修改的,以防止外部意外修改。
*
* @return 近期食谱索引的列表
*/
public List getRecentRecipeIndices() {
return Collections.unmodifiableList(recentRecipeIndices);
}
/**
* 清空近期食谱列表。
*/
public void clearRecentRecipes() {
recentRecipeIndices.clear();
}
} 代码解析:
- MAX_RECENT_RECIPES: 定义了我们希望维护的近期食谱的最大数量。
- recentRecipeIndices: 一个 List
,用于存储食谱在 ReceiptsBase.recipesAll 中的索引。 - addRecipeToRecent(int recipeIndex) 方法是核心:
- 它首先尝试从列表中移除 recipeIndex 的任何现有实例。这是为了确保如果用户再次查看一个已经存在于“近期”列表中的食谱,该食谱会被视为最新使用,并被移到列表的最前面,而不是在末尾重复添加。
- 然后,它将 recipeIndex 添加到列表的索引0位置。这意味着 recentRecipeIndices.get(0) 总是返回最近使用的食谱。
- 最后,如果列表的大小超过 MAX_RECENT_RECIPES,它会移除列表中的最后一个元素(即最旧的食谱)。
- getRecentRecipeIndices(): 返回当前近期食谱的索引列表。使用 Collections.unmodifiableList() 是一个良好的实践,可以防止外部代码直接修改这个列表,从而维护 RecentRecipesManager 的内部状态一致性。
2. 集成到食谱应用
在 MainActivity 或其他负责显示食谱详情的 Activity 中,当用户点击一个食谱时,需要调用 RecentRecipesManager 来更新近期食谱列表。
假设 ReceiptsBase 类保持不变。
在 MainActivity 中:
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import androidx.appcompat.app.AppCompatActivity;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private RecentRecipesManager recentRecipesManager;
private ReceiptsBase receiptsBase; // 假设 ReceiptsBase 是单例或通过某种方式获取实例
// 假设这些是近期食谱的ImageButton
private ImageButton recent1, recent2, recent3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// hideSystemUI(); // 根据需要保留或移除
// 初始化 RecentRecipesManager
recentRecipesManager = new RecentRecipesManager();
// 初始化 ReceiptsBase (这里仅作示例,实际应用中可能通过依赖注入或单例模式获取)
receiptsBase = new ReceiptsBase();
// 查找按钮
Button button_recipes = findViewById(R.id.button2);
button_recipes.setOnClickListener(view -> openRecipes());
Button button_search = findViewById(R.id.button3);
button_search.setOnClickListener(view -> openSearch());
Button button_supriseme = findViewById(R.id.button4);
button_supriseme.setOnClickListener(view -> openSupriseMe());
// 查找近期食谱的 ImageButton
recent1 = findViewById(R.id.rec1);
recent2 = findViewById(R.id.rec2);
recent3 = findViewById(R.id.rec3);
// 初始化显示近期食谱
updateRecentRecipesUI();
}
@Override
protected void onResume() {
super.onResume();
// 每次回到主界面时,刷新近期食谱显示
updateRecentRecipesUI();
}
// 当用户点击一个食谱时调用此方法,并传递食谱索引
public void openRecipe(int recipeIndex){
// 1. 更新近期食谱管理器
recentRecipesManager.addRecipeToRecent(recipeIndex);
// 2. 启动食谱详情页面
Intent intent = new Intent(this, Example.class); // 假设 Example 是食谱详情页
intent.putExtra("recipeIndex", recipeIndex);
startActivity(intent);
// finish(); // 根据应用流程决定是否关闭当前Activity
}
/**
* 更新UI上近期食谱的显示。
*/
private void updateRecentRecipesUI() {
List recentIndices = recentRecipesManager.getRecentRecipeIndices();
// 隐藏所有近期食谱按钮,然后根据实际数量显示
recent1.setVisibility(View.GONE);
recent2.setVisibility(View.GONE);
recent3.setVisibility(View.GONE);
// 遍历近期食谱索引,并设置对应的ImageButton
if (recentIndices.size() > 0) {
int index0 = recentIndices.get(0);
// 假设食谱的第一个元素是图片资源ID
recent1.setImageDrawable(getDrawable(receiptsBase.getReceipt(index0).get(0)));
recent1.setOnClickListener(view -> openRecipe(index0));
recent1.setVisibility(View.VISIBLE);
}
if (recentIndices.size() > 1) {
int index1 = recentIndices.get(1);
recent2.setImageDrawable(getDrawable(receiptsBase.getReceipt(index1).get(0)));
recent2.setOnClickListener(view -> openRecipe(index1));
recent2.setVisibility(View.VISIBLE);
}
if (recentIndices.size() > 2) {
int index2 = recentIndices.get(2);
recent3.setImageDrawable(getDrawable(receiptsBase.getReceipt(index2).get(0)));
recent3.setOnClickListener(view -> openRecipe(index2));
recent3.setVisibility(View.VISIBLE);
}
}
// 其他 openXXX 方法保持不变
public void openRecipes(){
Intent rec = new Intent(this, recipes.class);
startActivity(rec);
// finish();
}
public void openSearch(){
Intent sea = new Intent(this, search.class);
startActivity(sea);
// finish();
}
public void openSupriseMe(){
Intent sup = new Intent(this, Example.class); // 假设 Example 是一个通用页面
startActivity(sup);
// finish();
}
} 关键点:
- 在 MainActivity 的 onCreate 中初始化 RecentRecipesManager。
- openRecipe(int recipeIndex) 方法现在在启动食谱详情页之前,会调用 recentRecipesManager.addRecipeToRecent(recipeIndex) 来更新近期列表。
- updateRecentRecipesUI() 方法负责根据 recentRecipesManager 中存储的索引来更新三个 ImageButton 的图片和点击事件。它会隐藏所有按钮,然后根据实际的近期食谱数量来显示和设置它们。
- 在 onResume() 中调用 updateRecentRecipesUI() 确保每次用户返回主界面时,近期食谱显示都是最新的。
数据持久化考量
目前 RecentRecipesManager 仅在内存中维护近期食谱列表。这意味着一旦应用程序进程被杀死,所有近期食谱数据都会丢失。为了在应用重启后依然保留这些数据,我们需要进行数据持久化。
Yes!Sun基于PHP+MYSQL技术,体积小巧、应用灵活、功能强大,是一款为企业网站量身打造的WEB系统。其创新的设计理念,为企业网的开发设计及使用带来了全新的体验:支持前沿技术:动态缓存、伪静态、静态生成、友好URL、SEO设置等提升网站性能、用户体验、搜索引擎友好度的技术均为Yes!Sun所支持。易于二次开发:采用独创的平台化理念,按需定制项目中的各种元素,如:产品属性、产品相册、新闻列表
在 Android 开发中,常用的持久化方式包括:
-
SharedPreferences: 适用于存储少量键值对数据。对于存储近期食谱的索引列表非常适用。你可以将索引列表序列化成一个字符串(例如,用逗号分隔),然后存储在 SharedPreferences 中。
- 存储: 当 recentRecipeIndices 列表发生变化时,将其转换为字符串并保存。
-
加载: 在 RecentRecipesManager 构造函数中,从 SharedPreferences 读取字符串,解析回 List
。
// 在 RecentRecipesManager 中添加持久化逻辑 import android.content.Context; import android.content.SharedPreferences; import java.util.Arrays; import java.util.stream.Collectors; public class RecentRecipesManager { // ... (现有代码) ... private static final String PREFS_NAME = "RecentRecipesPrefs"; private static final String KEY_RECENT_INDICES = "recentRecipeIndices"; private Context context; // 需要在构造函数中传入Context public RecentRecipesManager(Context context) { this.context = context.getApplicationContext(); // 使用ApplicationContext防止内存泄漏 recentRecipeIndices = new ArrayList<>(); loadRecentRecipes(); // 加载已保存的近期食谱 } @Override // addRecipeToRecent 方法修改 public void addRecipeToRecent(int recipeIndex) { // ... (现有逻辑) ... saveRecentRecipes(); // 每次更新后保存 } private void saveRecentRecipes() { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); // 将List转换为逗号分隔的字符串 String indicesString = recentRecipeIndices.stream() .map(String::valueOf) .collect(Collectors.joining(",")); editor.putString(KEY_RECENT_INDICES, indicesString); editor.apply(); // 异步保存 } private void loadRecentRecipes() { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); String indicesString = prefs.getString(KEY_RECENT_INDICES, ""); if (!indicesString.isEmpty()) { recentRecipeIndices = Arrays.stream(indicesString.split(",")) .map(Integer::parseInt) .collect(Collectors.toCollection(ArrayList::new)); } } } 并在 MainActivity 中相应地实例化 RecentRecipesManager: recentRecipesManager = new RecentRecipesManager(this);
Room Persistence Library: 如果食谱数据本身也存储在本地数据库中,或者近期食谱列表需要更复杂的查询和管理,可以考虑使用 Room。将近期食谱的索引作为数据库中的一个单独表或字段进行管理。
优化与注意事项
-
数据结构选择: 尽管示例使用了 ArrayList,但对于频繁在列表两端进行添加和删除操作的场景,LinkedList 或 ArrayDeque (作为双端队列) 可能提供更好的性能,因为它们不需要移动大量元素。
- LinkedList: add(0, element) 和 remove(size - 1) 操作的平均时间复杂度为 O(1)。
- ArrayList: add(0, element) 操作需要移动所有后续元素,时间复杂度为 O(N);remove(size - 1) 为 O(1)。
- 对于 MAX_RECENT_RECIPES 较小(如3)的情况,ArrayList 的性能差异不明显。但如果近期列表较大,LinkedList 会是更好的选择。
处理重复项: 示例代码中的 recentRecipeIndices.remove(Integer.valueOf(recipeIndex)) 已经处理了重复项,确保一个食谱只出现一次,并且每次使用都会将其移到列表的最前面。
-
线程安全: 如果 RecentRecipesManager 可能在多个线程中被访问(例如,在后台线程加载食谱详情,同时主线程更新近期列表),则需要考虑线程安全。可以使用 Collections.synchronizedList() 包装 recentRecipeIndices,或者在 addRecipeToRecent 等方法中使用 synchronized 关键字。
// 线程安全版本 private List
recentRecipeIndices = Collections.synchronizedList(new ArrayList<>()); 错误处理: 在 updateRecentRecipesUI 中,确保 receiptsBase.getReceipt(index) 返回的 ArrayList
不为空,并且 get(0) 访问的索引是有效的。在实际应用中,应该添加空指针检查和索引越界检查。 -
抽象 Recipe 对象: 当前 ReceiptsBase 使用 ArrayList
> 来表示食谱。更好的做法是定义一个 Recipe 类,包含图片ID、名称ID等属性,这样代码的可读性和可维护性会大大提高。 // 示例 Recipe 类 public class Recipe { private int imageResId; private int nameResId; private int infoResId; // ... 其他属性 public Recipe(int imageResId, int nameResId, int infoResId /*, ... */) { this.imageResId = imageResId; this.nameResId = nameResId; this.infoResId = infoResId; } public int getImageResId() { return imageResId; } public int getNameResId() { return nameResId; } // ... getters }然后 ReceiptsBase 可以存储 ArrayList
,RecentRecipesManager 仍然存储 int 类型的索引。
总结
通过本教程,我们学习了如何在 Java 应用中实现一个“近期使用”功能。核心在于维护一个固定大小的列表,并利用滑动窗口机制(移除最旧,添加最新)来管理元素。我们还探讨了如何将此功能集成到 Android 应用的 UI 中,以及如何通过 SharedPreferences 实现数据持久化,确保用户体验的连贯性。在实际开发中,根据项目需求选择合适的数据结构、考虑线程安全和错误处理,并采用良好的面向对象设计(如抽象 Recipe 类),将有助于构建更健壮、更易维护的应用程序。








