본문 바로가기
안드로이드 자바

[JAVA][Android] 알람 앱 구현하기 - (2) SharedPreferences로 알람 데이터 CRUD

by teamnova 2024. 7. 8.
728x90

안녕하세요.

오늘은 알람 앱 구현하기 두번째 시간입니다.

 

첫번째 시간에는 알람 목록을 띄우기 위한 리사이클러뷰를 만들었는데요.

이번에는 그 코드를 바탕으로 SharedPreferences를 사용해서 알람 데이터를 CRUD(생성, 조회, 수정, 삭제) 해보겠습니다.

이전 코드는 아래 링크를 참조해주세요.

2024.07.05 - [안드로이드 자바] - [JAVA][Android] 알람 앱 구현하기 - (1) 리사이클러뷰로 목록 만들기

 

[JAVA][Android] 알람 앱 구현하기 - (1) 리사이클러뷰로 목록 만들기

안녕하세요.안드로이드에서 알람 앱 예제를 구현해보려고 합니다. 이 앱에서는 매일 원하는 시간에 알람이 울리도록 설정하고, 이미 설정된 알람을 수정하거나 삭제할 수 있도록 할 것입니다.

stickode.tistory.com

 


1. Gson 라이브러리 의존성 추가

이 예제에서는 알람 데이터를 Alarm 클래스를 사용해 객체로 만들어서 관리하고 있는데요.

SharedPreferences 에는 객체를 직접 저장할 수 없기 때문에, 저장 가능한 형태로 변환해주는 과정을 거쳐야 합니다.

이 글에서는 Gson 라이브러리를 사용해서 객체를 Json 형태의 String 데이터로 변환해서 저장하기 때문에, Gson 라이브러리에 대한 의존성을 앱 수준의 build.gradle 에 아래와 같이 추가합니다.

implementation 'com.google.code.gson:gson:2.11.0'

 

Gson 라이브러리에 대해 자세히 알고 싶다면, 아래 공식 깃허브를 참고하세요.

https://github.com/google/gson

 

GitHub - google/gson: A Java serialization/deserialization library to convert Java Objects into JSON and back

A Java serialization/deserialization library to convert Java Objects into JSON and back - google/gson

github.com

 

2. Alarm 데이터 클래스에 속성값 id 추가

SharedPreferences에는 Key, valuse 형태로 데이터가 저장됩니다.  따라서 각 알람을 구분하기 위한 key 값을 지정할 수 있도록 Alarm 객체는 고유의 식별값을 가져야 합니다.

또한 메인 액티비티에서 리사이클러뷰에 알람 목록을 보여줄 때, 시간순으로 정렬해서 보여주려고 하는데요. 이를 위해 Alarm 클래스에 Comparable 인터페이스를 구현합니다.

수정된 코드는 아래와 같습니다.

import java.util.Locale;

public class Alarm implements Comparable<Alarm> {
    private int id; // 식별을 위한 고유값
    private int hour; // 시
    private int minute; // 분
    private boolean onOff; // 현재 알람이 켜져있는지, 꺼져있는지 

    public Alarm(int id, int hour, int minute) {
        this.id = id;
        this.hour = hour;
        this.minute = minute;
        this.onOff = true; // 기본값 on으로 설정
    }

    public int getId() {
        return id;
    }

    public int getHour() {
        return hour;
    }

    public void setHour(int hour) {
        this.hour = hour;
    }

    public int getMinute() {
        return minute;
    }

    public void setMinute(int minute) {
        this.minute = minute;
    }

    public boolean isOnOff() {
        return onOff;
    }

    public void setOnOff(boolean onOff) {
        this.onOff = onOff;
    }

    public String getAmPm() {
        return hour < 12 ? "오전" : "오후";
    }

    public String getFormattedTime() {
        int displayHour = (hour == 0 || hour == 12) ? 12 : hour % 12;
        return String.format(Locale.getDefault(), "%02d:%02d", displayHour, minute);
    }

// 시간순으로 정렬되도록 compareTo 메서드를 오버라이드
    @Override
    public int compareTo(Alarm other) {
        if (this.hour != other.hour) {
            return this.hour - other.hour;
        }
        return this.minute - other.minute;
    }
}

 

3. AlarmStorage 클래스 생성

이제 SharedPreferences를 사용해서 알람 데이터를 추가, 조회, 수정, 삭제 하기 위한 AlarmStorage 클래스를 만듭니다.

 

이 글에서는 알람의 id를 1,2,3,4 .. 순의 정수로 부여하려고 합니다. 이 때 주의할 것은 현재 쉐어드에 저장된 데이터의 갯수를 가지고 id 값을 부여해서는 안된다는 것입니다. 만약 중간에 특정 알람을 삭제하게 되면 중복된 id 값을 가진 데이터가 생겨날 수 있기 때문인데요.

 

모든 알람이 중복되지 않는 고유한 id를 가지도록 하는 방법에는 여러가지가 있습니다.

이 글에서는 일반적인 DB에서 auto increment로 id값을 관리하는 것에 착안해서, next_id 라는 키값에 다음에 생성될 알람의 id를 저장해두는 방식을 사용하였습니다. 그리고 next_id 의 default 값을 1로 지정해서, 만약 해당 쉐어드 파일이 생성된 처음으로 알람을 생성하는 것이라면 1을 반환하도록 하였습니다.

 

그리고 AlarmStorage 객체가 여러개 생성되는 것을 방지하기 위해 싱글톤 패턴으로 구현하였습니다.

import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.Gson;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AlarmStorage {
    private static final String PREFS_NAME = "alarms";
    private static final String NEXT_ID_KEY = "next_id";
    private static AlarmStorage instance;
    private SharedPreferences pref;
    private SharedPreferences.Editor editor;
    private Gson gson;

    // 싱글톤 패턴 사용
    private AlarmStorage(Context context) {
        pref = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
        editor = pref.edit();
        gson = new Gson();
    }

    // 싱글톤 인스턴스 가져오기
    public static synchronized AlarmStorage getInstance(Context context) {
        if (instance == null) {
            instance = new AlarmStorage(context.getApplicationContext());
        }
        return instance;
    }

    // 새로운 알람 객체를 생성할 때, 부여할 id 값을 가져오는 메서드
    public int getNextId() {
        return pref.getInt(NEXT_ID_KEY, 1);
    }

    // 새로운 알람 객체를 저장할 때, nextId 값을 수정하기 위한 메서드
    public void setNextId() {
        editor.putInt(NEXT_ID_KEY, getNextId() + 1).apply();
    }

    // 저장된 모든 알람을 가져오는 메서드
    public List<Alarm> getAlarms() {
        List<Alarm> alarms = new ArrayList<>();
        for (String key : pref.getAll().keySet()) {
            if (!key.equals(NEXT_ID_KEY)) {
                String json = pref.getString(key, null);
                if (json != null) {
                    alarms.add(gson.fromJson(json, Alarm.class));
                }
            }
        }
        Collections.sort(alarms); // 알람 목록을 시간 순서대로 정렬
        return alarms;
    }

    // 알람을 저장하는 메서드
    public void saveAlarm(Alarm alarm) {
        String json = gson.toJson(alarm);
        editor.putString(String.valueOf(alarm.getId()), json).apply();
    }

    // 새로운 알람을 추가하는 메서드
    public void addAlarm(Alarm alarm) {
        saveAlarm(alarm);
        setNextId(); // nextId 값을 업데이트
    }

    // 알람을 삭제하는 메서드
    public void removeAlarm(Alarm alarm) {
        editor.remove(String.valueOf(alarm.getId())).apply();
    }

    public Alarm getAlarmById(int alarmId) {
        String json = pref.getString(String.valueOf(alarmId), null);
        return gson.fromJson(json, Alarm.class);
    }

}

 

이렇게 하게 되면, "alarms" 라는 이름으로 생성되는 쉐어드 파일에는 다음과 같이 각 알람 id에 해당하는 알람 데이터와 next_id 라는 키에 해당하는 데이터가 존재하게 됩니다.

 

 

4. EditAlarmActivity 생성

 

EditAlarmActivity 는 다음과 같은 두가지 경우에 사용됩니다.

1) MainActivity의 플로팅버튼을 클리해서 새 알람을 생성할 때

2) MainActivity의 리사이클러뷰에서 알람 아이템을 클릭시 해당 알람을 조회하고, 수정하고, 삭제할 때

 

두 경우에 각기 다른 액티비티를 사용해도 되지만, 이 예제와 같이 두 경우에 사용되는 화면의 레이아웃 구조가 매우 흡사한 경우 하나의 액티비티를 사용해서 처리할 수도 있습니다.

이를 위해서는 intent로 넘어온 Extra 데이터를 이용해서 경우에 따라 다르게 동작하도록 코드를 구현해야 합니다.

activity_edit_alarm.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".EditAlarmActivity">

    <Button
        android:id="@+id/btn_delete"
        style="@style/Widget.Material3.Button.IconButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="삭제"
        android:visibility="invisible"
        app:icon="@drawable/ic_delete"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TimePicker
        android:id="@+id/timePicker"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:layout_marginTop="100dp"
        android:timePickerMode="spinner"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_delete" />

    <Button
        android:id="@+id/btn_save"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="저장"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/timePicker"/>


</androidx.constraintlayout.widget.ConstraintLayout>
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TimePicker;

import androidx.appcompat.app.AppCompatActivity;

public class EditAlarmActivity extends AppCompatActivity {

    private AlarmStorage alarmStorage;
    private TimePicker timePicker;
    private Button btnDelete;
    private Button btnSave;
    private Alarm currentAlarm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_edit_alarm);

        alarmStorage = AlarmStorage.getInstance(this);
        timePicker = findViewById(R.id.timePicker);
        btnSave = findViewById(R.id.btn_save);
        btnDelete = findViewById(R.id.btn_delete);

        Intent intent = getIntent();
        int alarmId = intent.getIntExtra("alarmId", -1);

        if (alarmId == -1) { // 새 알람 생성인 경우
            currentAlarm = null;
            btnDelete.setVisibility(View.INVISIBLE);
        } else { // 기존 알람 수정인 경우
            currentAlarm = alarmStorage.getAlarmById(alarmId);
            btnDelete.setVisibility(View.VISIBLE);

            if (currentAlarm != null) {
                timePicker.setHour(currentAlarm.getHour());
                timePicker.setMinute(currentAlarm.getMinute());
            }

            btnDelete.setOnClickListener(v -> {
                new AlertDialog.Builder(this)
                        .setTitle("알람 삭제")
                        .setMessage("정말로 해당 알람을 삭제하시겠습니까?")
                        .setPositiveButton("삭제", (dialog, which) -> {
                            alarmStorage.removeAlarm(currentAlarm);
                            finish();
                        })
                        .setNegativeButton("취소", null)
                        .show();
            });
        }
        btnSave.setOnClickListener(v -> {
            int hour = timePicker.getHour();
            int minute = timePicker.getMinute();

            if (currentAlarm == null) {
                // 새 알람 생성
                int id = alarmStorage.getNextId();
                Alarm alarm = new Alarm(id, hour, minute);
                alarmStorage.addAlarm(alarm);
            } else {
                // 기존 알람 수정
                currentAlarm.setHour(hour);
                currentAlarm.setMinute(minute);
                alarmStorage.saveAlarm(currentAlarm);
            }
            finish();
        });
    }
}

 

 

5. MainActivity  & AlarmAdapter 수정

다음으로 AlarmAdapter에 OnAlarmClickListener 라는 클릭리스너 인터페이스를 만들어서, 특정 알람 클릭시 해당 알람의 id값을 intent로 전달하면서 EditAlarmActivity로 이동하도록 합니다.

그리고 MainActivity에서 저장된 최신의 알람데이터를 가져올 수 있도록, 쉐어드에서 데이터 목록을 가져오는 코드가 onStart()에서 실행되도록 하였습니다.

 

import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.switchmaterial.SwitchMaterial;

import java.util.List;

public class AlarmAdapter extends RecyclerView.Adapter<AlarmAdapter.AlarmViewHolder> {

    // 아이템클릭리스너 인터페이스 선언
    public interface OnAlarmClickListener {
        void onAlarmClick(Alarm alarm);
    }
    
    private OnAlarmClickListener onAlarmClickListener;
    private List<Alarm> alarmList;
    
    //아이템클릭리스너를 설정할 때 사용하는 메서드
    public void setOnAlarmClickListener(OnAlarmClickListener listener) {
        this.onAlarmClickListener = listener;
    }

    @SuppressLint("NotifyDataSetChanged")
    public void setAlarmList(List<Alarm> alarmList) {
        this.alarmList = alarmList;
        notifyDataSetChanged();
    }

    @NonNull
    @Override
    public AlarmViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_alarm, parent, false);
        return new AlarmViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull AlarmViewHolder holder, int position) {
        Alarm alarm = alarmList.get(position);
        holder.tvAmPm.setText(alarm.getAmPm());
        holder.tvTime.setText(alarm.getFormattedTime());
        holder.switchOnOff.setChecked(alarm.isOnOff());
        // 아이템 클릭시 미리 설정된 실행
        holder.itemView.setOnClickListener(v -> onAlarmClickListener.onAlarmClick(alarm));
    }

    @Override
    public int getItemCount() {
        return alarmList == null ? 0 : alarmList.size();
    }

    public class AlarmViewHolder extends RecyclerView.ViewHolder {
        TextView tvAmPm, tvTime;
        SwitchMaterial switchOnOff;
        public AlarmViewHolder(@NonNull View itemView) {
            super(itemView);
            tvAmPm = itemView.findViewById(R.id.tv_am_pm);
            tvTime = itemView.findViewById(R.id.tv_time);
            switchOnOff = itemView.findViewById(R.id.switch_on_off);
        }
    }
}

 

import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.floatingactionbutton.FloatingActionButton;

import java.util.List;

public class MainActivity extends AppCompatActivity implements AlarmAdapter.OnAlarmClickListener {

    private FloatingActionButton btnAddAlarm;
    private RecyclerView recyclerView;
    private AlarmAdapter alarmAdapter;
    private List<Alarm> alarmList;
    private AlarmStorage alarmStorage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnAddAlarm = findViewById(R.id.btn_add_alarm);
        recyclerView = findViewById(R.id.recycler_view);

        alarmStorage = AlarmStorage.getInstance(this);
        alarmList = new ArrayList<>();
        
        // RecyclerView 설정
        alarmAdapter = new AlarmAdapter();
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(alarmAdapter);
        //아래 오버라이드한 알람클릭메서드 설정
        alarmAdapter.setOnAlarmClickListener(this);

        btnAddAlarm.setOnClickListener(v -> {
            Intent intent = new Intent(this, EditAlarmActivity.class);
            startActivity(intent);
        });

    }

    @Override
    protected void onStart() {
        super.onStart();
        // 쉐어드에서 최신의 알람목록 가져오기
        alarmList.clear();
        alarmList.addAll(alarmStorage.getAlarms());
        alarmAdapter.setAlarmList(alarmList);
    }

    // 알람 클릭 메서드 오버라이드
    @Override
    public void onAlarmClick(Alarm alarm) {
        Intent intent = new Intent(MainActivity.this, EditAlarmActivity.class);
        intent.putExtra("alarmId", alarm.getId());
        startActivity(intent);
    }
}

 

 

6. 시연영상

 

 


 

 

오늘까지 리사이클러뷰와 쉐어드를 사용해서 알람데이터를 CRUD 해보았습니다.

다음 시간에는 AlarmManager 클래스를 사용해서 실제로 설정한 시간에 알람이 울리도록 해보겠습니다.

감사합니다.