Android:将FCM通知保存至Room数据库的MVVM实现问题
解决FCM通知存储到Room并在MVVM架构下展示的问题
你遇到的核心问题是在FirebaseMessagingService中无法直接使用ViewModel——因为ViewModel依赖于拥有生命周期的组件(比如Activity/Fragment这类ViewModelStoreOwner),而Service并不具备这个特性。正确的思路是:在收到FCM通知时,直接通过Room数据库的DAO插入数据,然后在通知页面的ViewModel中观察Room的数据,实现UI展示。
下面是完整的实现步骤和代码修改:
1. 先定义Room相关组件
首先确保你已经创建了Room的Entity、DAO和Database类:
1.1 通知实体类(NotificationEntity)
@Entity(tableName = "notifications") public class NotificationEntity { @PrimaryKey(autoGenerate = true) private int id; private String title; private String body; private String imageUrl; private long timestamp; private boolean isRead; // 构造函数 public NotificationEntity(String title, String body, String imageUrl, long timestamp) { this.title = title; this.body = body; this.imageUrl = imageUrl; this.timestamp = timestamp; this.isRead = false; // 默认未读 } // Getter和Setter方法 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getImageUrl() { return imageUrl; } public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } public long getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } public boolean isRead() { return isRead; } public void setRead(boolean read) { isRead = read; } }
1.2 通知DAO接口
@Dao public interface NotificationDao { @Insert void insertNotification(NotificationEntity notification); @Query("SELECT * FROM notifications ORDER BY timestamp DESC") LiveData<List<NotificationEntity>> getAllNotifications(); @Update void markNotificationAsRead(NotificationEntity notification); }
1.3 Room数据库类
@Database(entities = {NotificationEntity.class}, version = 1, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { private static volatile AppDatabase INSTANCE; public abstract NotificationDao notificationDao(); // 获取单例数据库实例 public static AppDatabase getInstance(Context context) { if (INSTANCE == null) { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "app_notification_db") .build(); } } } return INSTANCE; } }
2. 修改FirebaseMessagingService,存储通知到Room
在收到FCM通知时,先把数据插入到Room,再显示系统通知。注意:Room不允许在主线程操作数据库,所以要用异步线程执行插入:
public class MessagingService extends FirebaseMessagingService { @Override public void onNewToken(String s) { super.onNewToken(s); Log.d("MessagingService", s); } @Override public void onMessageReceived(RemoteMessage remoteMessage) { super.onMessageReceived(remoteMessage); // 第一步:把通知数据存储到Room saveNotificationToRoom(remoteMessage); // 第二步:显示系统通知 if (remoteMessage.getData().isEmpty()){ showNotification(remoteMessage.getNotification().getTitle(), remoteMessage.getNotification().getBody()); } else { showNotification(remoteMessage.getData()); } } // 将通知数据插入Room数据库 private void saveNotificationToRoom(RemoteMessage remoteMessage) { NotificationEntity notification; long timestamp = System.currentTimeMillis(); // 记录收到通知的时间 if (remoteMessage.getData().isEmpty()) { // 处理纯通知payload的情况 String title = remoteMessage.getNotification().getTitle(); String body = remoteMessage.getNotification().getBody(); notification = new NotificationEntity(title, body, null, timestamp); } else { // 处理data payload的情况 Map<String, String> data = remoteMessage.getData(); String title = data.get("title"); String body = data.get("body"); String imageUrl = data.get("image"); notification = new NotificationEntity(title, body, imageUrl, timestamp); } // 异步线程执行插入,避免阻塞主线程 new Thread(() -> { AppDatabase.getInstance(getApplicationContext()).notificationDao().insertNotification(notification); }).start(); } // 你的原有showNotification方法保持不变 private void showNotification(Map<String, String> data) { // 原代码内容... } private void showNotification(String title, String body) { // 原代码内容... } public Bitmap getBitmapfromUrl(String imageUrl) { // 原代码内容... } }
3. 创建通知页面的ViewModel
ViewModel负责从Room获取数据,并暴露出LiveData给UI层观察:
public class NotificationViewModel extends ViewModel { private final LiveData<List<NotificationEntity>> allNotifications; private final NotificationDao notificationDao; public NotificationViewModel(Application application) { AppDatabase database = AppDatabase.getInstance(application); notificationDao = database.notificationDao(); allNotifications = notificationDao.getAllNotifications(); } // 提供给UI层观察的通知列表 public LiveData<List<NotificationEntity>> getAllNotifications() { return allNotifications; } // 标记通知为已读 public void markNotificationAsRead(NotificationEntity notification) { notification.setRead(true); new Thread(() -> notificationDao.markNotificationAsRead(notification)).start(); } }
4. 在通知页面(Fragment/Activity)展示数据
以Fragment为例,使用ViewModel观察Room数据,并用RecyclerView展示:
public class NotificationFragment extends Fragment { private NotificationViewModel notificationViewModel; private NotificationAdapter adapter; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_notification, container, false); RecyclerView recyclerView = view.findViewById(R.id.notification_recycler); // 初始化适配器 adapter = new NotificationAdapter(); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); // 获取ViewModel实例 notificationViewModel = new ViewModelProvider(this).get(NotificationViewModel.class); // 观察通知数据变化,自动更新UI notificationViewModel.getAllNotifications().observe(getViewLifecycleOwner(), notifications -> { adapter.submitList(notifications); }); return view; } // 自定义RecyclerView适配器 private class NotificationAdapter extends RecyclerView.Adapter<NotificationAdapter.NotificationViewHolder> { private List<NotificationEntity> notificationList = new ArrayList<>(); public void submitList(List<NotificationEntity> list) { notificationList = list; notifyDataSetChanged(); } @NonNull @Override public NotificationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_notification, parent, false); return new NotificationViewHolder(itemView); } @Override public void onBindViewHolder(@NonNull NotificationViewHolder holder, int position) { NotificationEntity notification = notificationList.get(position); holder.titleTv.setText(notification.getTitle()); holder.bodyTv.setText(notification.getBody()); // 区分已读/未读状态 if (notification.isRead()) { holder.titleTv.setTextColor(Color.GRAY); } else { holder.titleTv.setTextColor(getResources().getColor(R.color.black)); } // 点击标记为已读 holder.itemView.setOnClickListener(v -> { notificationViewModel.markNotificationAsRead(notification); }); // 加载通知图片(如果有) if (notification.getImageUrl() != null) { // 这里可以用Glide/Picasso加载图片,示例用Glide Glide.with(holder.itemView.getContext()) .load(notification.getImageUrl()) .placeholder(R.drawable.ic_notification_placeholder) .into(holder.imageIv); } else { holder.imageIv.setVisibility(View.GONE); } } @Override public int getItemCount() { return notificationList.size(); } class NotificationViewHolder extends RecyclerView.ViewHolder { TextView titleTv, bodyTv; ImageView imageIv; public NotificationViewHolder(@NonNull View itemView) { super(itemView); titleTv = itemView.findViewById(R.id.tv_notification_title); bodyTv = itemView.findViewById(R.id.tv_notification_body); imageIv = itemView.findViewById(R.id.iv_notification_image); } } } }
关键说明
- 为什么之前用ViewModel失败?:
FirebaseMessagingService是后台服务,不属于ViewModelStoreOwner,无法通过ViewModelProviders.of()获取ViewModel实例。ViewModel的设计初衷是给UI层(Activity/Fragment)提供数据,后台操作直接用Room的DAO更合适。 - 异步操作的必要性:Room严格禁止在主线程执行数据库操作,所以插入和更新都要放在子线程中(你也可以用Coroutines或RxJava替代Thread,代码会更简洁)。
- LiveData的作用:ViewModel中的
LiveData<List<NotificationEntity>>会自动监听Room数据变化,当有新通知插入时,UI层会收到回调并自动更新列表。
内容的提问来源于stack exchange,提问作者Akash Chaubey
相关产品推荐
相关产品推荐

