You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:38:16