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

[JAVA][Android]livedata, viewmodel 활용해 타이머 만들기

by teamnova 2024. 7. 22.
728x90

오늘은 livedata와 viewmodel을 활용해 간단한 타이머 앱을 만들어 보겠습니다.

 

 

 

 

레이아웃 xml 파일 코드(activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/timerText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="48sp"
        android:textColor="@android:color/black"
        android:gravity="center"
        android:text="00:00:00" />

    <EditText
        android:id="@+id/inputTime"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter seconds"
        android:inputType="number" />

    <Button
        android:id="@+id/startButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start Timer"
        android:layout_gravity="center"
        android:layout_marginTop="16dp" />

    <Button
        android:id="@+id/stopButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Stop Timer"
        android:layout_gravity="center"
        android:layout_marginTop="16dp" />

    <Button
        android:id="@+id/resumeButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Resume Timer"
        android:layout_gravity="center"
        android:layout_marginTop="16dp" />

    <Button
        android:id="@+id/resetButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Reset Timer"
        android:layout_gravity="center"
        android:layout_marginTop="16dp" />
</LinearLayout>

 

 

 

 

액티비티 자바 코드

public class MainActivity extends AppCompatActivity {
    private TimerViewModel timerViewModel; //시간 값과 타이머 시작, 멈춤, 리셋 등의 로직을 담고있는 뷰모델 클래스 객체
    private EditText inputTime; //타이머로 설정할 시간 값을 넣을 뷰
    private Button startButton, stopButton, resumeButton, resetButton; //타이머 시작, 중지, 재개, 리셋 버튼 뷰

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

        //뷰모델 객체 생성 및 뷰모델이 인식해야하는 생명주기를 가진 객체(액티비티)를 뷰모델에 셋팅
        timerViewModel = new ViewModelProvider(this).get(TimerViewModel.class);

        TextView timerText = findViewById(R.id.timerText);
        inputTime = findViewById(R.id.inputTime);
        startButton = findViewById(R.id.startButton);
        stopButton = findViewById(R.id.stopButton);
        resumeButton= findViewById(R.id.resumeButton);
        resetButton = findViewById(R.id.resetButton);

        // viewmodel이 관리하고있는 시간 결과값(remainingTime 변수 값 변화에 따른 결과 값)이
        // 변화되는 경우 livedata 관련 observer의 콜백 메서드 호출
        // livedata의 postValue() 메서드 호출시 이벤트를 감지해 아래의 onchanged()메서드가 호출된다 보면됨
        // 변화된 값 textview에 적용
        timerViewModel.getTimerValue().observe(this, new Observer<String>() {
            @Override
            public void onChanged(String value) {
                timerText.setText(value);
            }
        });


        // 터치시 타이머 시작 처리 설정
        // 입력한 초 시간을 기반으로 타이머 작동
        startButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String input = inputTime.getText().toString();
                if (!input.isEmpty()) {
                    int seconds = Integer.parseInt(input);
                    timerViewModel.startTimer(seconds);
                }
            }
        });


        // 터치시 타이머 멈춤 처리 설정
        stopButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                timerViewModel.stopTimer();
            }
        });

        // 터치시 타이머 재개 처리 설정
        resumeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                timerViewModel.resumeTimer();
            }
        });

        // 터치시 타이머 리셋 처리 설정
        resetButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                timerViewModel.resetTimer();
            }
        });
    }
}

 

 

TimerViewModel 클래스 자바 파일 코드(ViewModel 상속 클래스)

public class TimerViewModel extends ViewModel {
    private final MutableLiveData<String> timerValue = new MutableLiveData<>();// 시간값 변경 되었음을 알려줄 livedata 객체
    private Timer timer; //백그라운드 스레드에서 향후 실행할 작업을 예약하는 기능을 제공해주는 객체
    //작업은 일회성 실행 또는 정기적인 간격으로 반복 실행되도록 예약함
    private int remainingTime; //남은 시간

    private boolean isPaused = false; // 타이머가 일시정지 상태인지 확인하는 변수

    public LiveData<String> getTimerValue() { //ViewModel 객체가 가지고 있는 livedata 객체를 리턴해주는 메서드
        return timerValue;
    }


    //타이머 시작 메서드
    public void startTimer(int seconds) {
        isPaused = false; // 타이머 시작 시 일시 정지 상태 false 값으로 초기화
        remainingTime = seconds; //사용자가 입력한 초 시간 값을 변수에 할당
        updateTimerText();  // 사용자가 입력한 값을 시:분:초 형태로 변경하여 텍스트 뷰의 초기값 설정
        if (timer != null) { //
            timer.cancel(); // 기존 timer 객체에 의해 실행되기위해 예약된 작업들을 삭제 처리
        }
        //timer 객체 생성
        //timer 객체는 한번 cancel하면 새로운 반복 작업 관련 예약이 불가하기에 새로 타이머 시작시 매번 새로 생성해줘야함
        timer = new Timer();
        // 1초 뒤 1초간격으로 반복되는 작업이 단일 백그라운드 스레드에서 실행되도록 처리
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() { // 실행될 작업 내용
                
                if (remainingTime > 0) {
                    // 남은 시간이 0보다 크면 1 줄이기
                    remainingTime--;
                    // 1 줄인 시간을 시:분:초 형태로 변환하여 업데이트 하라고 livedata로 알려주기
                    updateTimerText();
                } else {
                    //남은 시간이 0이면 timer 객체에 의해 실행할 작업 종료하기
                    timer.cancel();
                }
            }
        }, 1000, 1000);
    }


    //시간초 값 변경 되었음을 UI스레드로 알려주는 메서드
    private void updateTimerText() {
        long hours = TimeUnit.SECONDS.toHours(remainingTime);
        long minutes = TimeUnit.SECONDS.toMinutes(remainingTime) % 60;
        long secs = remainingTime % 60;
        String timeString = String.format("%02d:%02d:%02d", hours, minutes, secs);
        timerValue.postValue(timeString); //백그라운드 스레드에서 UI 스레드의 observer객체에게 이벤트 발생 알림
    }

    //타이머 멈춤(일시정지) 메서드
    public void stopTimer() {
        if (timer != null) {
            timer.cancel(); //기존 timer 객체에 의해 실행되고 있는 예약된 작업들 삭제 처리
            isPaused = true; // 타이머 일시 정지 상태로 설정
        }
    }

    //타이머 리셋 메서드
    public void resetTimer() {
        stopTimer(); //기존 timer 객체에 의해 실행되고 있는 예약된 작업들 삭제 처리
        remainingTime = 0;
        timerValue.postValue("00:00:00"); //보이는 UI 시간을 00:00:00초 형태로 변경하라고 observer 객체에게 알림
        isPaused = false; // 리셋 시 일시 정지 상태 초기화
    }

    // 타이머 재개 메서드
    public void resumeTimer() {
        //타이머가 일시 정지
        if (isPaused && remainingTime > 0) {
            if (timer != null) {
                timer.cancel(); // 기존 timer 객체에 의해 실행되기위해 예약된 작업들이  삭제 처리
            }
            //timer 객체 생성
            timer = new Timer();
            // 1초 뒤 1초간격으로 반복되는 작업이 단일 백그라운드 스레드에서 실행되도록 처리
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    if (remainingTime > 0) {
                        remainingTime--;
                        updateTimerText();
                    } else {
                        timer.cancel();
                    }
                }
            }, 1000, 1000);
            isPaused = false; // 타이머가 재개된 상태로 설정
        }
    }





    //뷰모델이 삭제될 때 즉 더 이상 사용하지 않을 경우 호출
    //구성변경 제외 뷰모델이 인식중인 액티비티나 프래그먼트가 destroy 된 경우 등
    //ViewModelStore 객체의 clear() 메서드가 호출 된 경우 호출됨
    @Override
    protected void onCleared() {
        super.onCleared();
        //기존 timer 객체에 의해 실행되고 있는 예약된 작업들 삭제 처리
        if (timer != null) {
            timer.cancel();
        }
    }
}

 

 

 

실행 영상

 

 

 

viewmodel을 활용하였기에 화면 회전, 글자 크기 변경 등 구성변경 상황에 대해서도 데이터 유지 대응이 가능합니다.

또한 LiveData에 의해 액티비티의 생명주기 인식을 하여 비활성화된 상태에 불필요한 UI 업데이트 방지(리소스 낭비 방지), 백그라운드 스레드에서 데이터 변경 발생시 UI 스레드에서 안전하게 업데이트 처리, Observer 객체 활용 관련 메모리 누수 방지 등의 처리가 됩니다.

 

livedata와 viewmodel을 함께 활용하면 비즈니스 로직과 UI 로직의 분리하는데 도움 되어 유지보수 측면에서도 좋습니다.

 

livedata와 viewmodel 관련 자세한 설명은 아래의 링크를 참고해 주세요.

https://developer.android.com/topic/libraries/architecture/viewmodel

https://developer.android.com/topic/libraries/architecture/livedata