춤추는 개발자

[AOS] Room + ViewModel + LiveData + RecyclerView (1) 본문

Android/study_til

[AOS] Room + ViewModel + LiveData + RecyclerView (1)

Heon_9u 2021. 6. 28. 16:36
728x90
반응형

이번 포스팅부터는 구글에서 권장하는 Room과 MVVM을 활용하도록 하겠습니다. 위 이미지는 실제 구글에서 권장하는 Android MVVM 패턴의 데이터 통신 모델입니다.

 

 최근 개인 프로젝트로 앱을 개발하고 유지보수하는 과정에서 몇가지 불편함을 겪었습니다.

  • SQLite 기반에서는 테이블의 컬럼 하나를 변경하는데만 3~4개의 수정 사항이 필요.
  • AddEditActivity의 복잡한 코드, MVVM을 안쓰니 직접 처리해야할 로직들이 많음. (ex: List 갱신)
  • 구글에서 비동기처리로 권장하는 RxJava가 MVVM과 호환가능. (AsyncTask는 Android 11부터 Deprecated로 지정)

 

이러한 상황들을 해결하기 위해 Room과 MVVM을 사용하기로 결심했습니다. 먼저, 예제 하나를 따라해보고 본 프로젝트에 맞게 적용할 계획입니다. 해당 예제는 아래 영상과 가이드를 참고하며 진행하였습니다.

 

https://developer.android.com/codelabs/android-room-with-a-view#7

https://www.youtube.com/watch?v=ARpn-1FPNE4&list=PLrnPJCHvNZuDihTpkRs6SpZhqgBqPU118&index=1 

 

 

로컬 데이터베이스 Room, 코드로 구현하기

 Room에 대한 기본적인 설명은 이전 포스팅에 작성하였습니다. 이번에는 실제 코드를 리뷰하겠습니다.

 

0. build.gradle

 가장 먼저, dependency를 추가합니다. Android에서 제공하는 Room과 Lifecycle을 구현하기위해 관련 라이브러리들을 build.gradle의 dependencies에 추가하겠습니다. (이외에도 CardView를 추가했습니다.)

dependencies {
    def lifecycle_version = "2.2.0"
    def room_version = "2.3.0"

    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"

    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
    
    implementation 'androidx.cardview:cardview:1.0.0'
}

 

 

1. Note Class

 첫번째로 데이터 통신의 '틀' 역할을 하는 Dto를 구현합니다. @Entity를 통해 실제 DB에서 사용할 Table의 이름을 "note_table"로 지정하였고, 각 멤버변수들이 실제 컬럼이 됩니다. 이때 변수 id를 @PrimrayKey로 지정하여 PK역할을 부여할 수 있습니다.

package com.heon9u.aacproject;

import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity(tableName = "note_table")
public class Note {

    @PrimaryKey(autoGenerate = true)
    private int id;

    private String title;

    private String description;

    private int priority;

    public Note(String title, String description, int priority) {
        this.title = title;
        this.description = description;
        this.priority = priority;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public String getDescription() {
        return description;
    }

    public int getPriority() {
        return priority;
    }
}

 

2. NoteDao Interface

 Java코드로 DB에 접근하기 위한 Dao를 생성합니다. 이때, 로컬 DB의 추상 액세스를 제공하는 메소드가 포함되므로 Interface로 구현합니다. Dao에서 각 메서드에는 Annotation을 통해 Insert/update/delete 구현이 가능하며, @Query로 직접 쿼리문 작성이 가능합니다.

 

package com.heon9u.aacproject;

import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;

import java.util.List;

@Dao
public interface NoteDao {

    @Insert
    void insert(Note note);

    @Update
    void update(Note note);

    @Delete
    void delete(Note note);

    @Query("DELETE FROM note_table")
    void deleteAllNotes();

    @Query("SELECT * FROM note_table ORDER BY priority DESC")
    LiveData<List<Note>> getAllNotes();
}

 

3. NoteRepository AsyncTask

 로컬 DB인 Room과 ViewModel간의 브릿지 역할을 하는 Repository입니다. DB의 상태 변화를 담당하는 Dao의 메서드 호출은 AsyncTask로 진행합니다. 

 

package com.heon9u.aacproject;

import android.app.Application;
import android.os.AsyncTask;

import androidx.lifecycle.LiveData;

import java.util.List;

public class NoteRepository {
    private NoteDao noteDao;
    private LiveData<List<Note>> allNotes;

    public NoteRepository(Application application) {
        NoteDatabase database = NoteDatabase.getInstance(application);
        noteDao = database.noteDao();
        allNotes = noteDao.getAllNotes();
    }

    public void insert(Note note) {
        new InsertNoteAsyncTask(noteDao).execute(note);
    }

    public void update(Note note) {
        new UpdateNoteAsyncTask(noteDao).execute(note);
    }

    public void delete(Note note) {
        new DeleteNoteAsyncTask(noteDao).execute(note);
    }

    public void deleteAllNotes() {
        new DeleteAllNotesAsyncTask(noteDao).execute();
    }

    public LiveData<List<Note>> getAllNotes() {
        return allNotes;
    }

    private static class InsertNoteAsyncTask extends AsyncTask<Note, Void, Void> {
        private NoteDao noteDao;

        private InsertNoteAsyncTask(NoteDao noteDao) {
            this.noteDao = noteDao;
        }

        @Override
        protected Void doInBackground(Note... notes) {
            noteDao.insert(notes[0]);
            return null;
        }
    }

    private static class UpdateNoteAsyncTask extends AsyncTask<Note, Void, Void> {
        private NoteDao noteDao;

        private UpdateNoteAsyncTask(NoteDao noteDao) {
            this.noteDao = noteDao;
        }

        @Override
        protected Void doInBackground(Note... notes) {
            noteDao.update(notes[0]);
            return null;
        }
    }

    private static class DeleteNoteAsyncTask extends AsyncTask<Note, Void, Void> {
        private NoteDao noteDao;

        private DeleteNoteAsyncTask(NoteDao noteDao) {
            this.noteDao = noteDao;
        }

        @Override
        protected Void doInBackground(Note... notes) {
            noteDao.delete(notes[0]);
            return null;
        }
    }

    private static class DeleteAllNotesAsyncTask extends AsyncTask<Note, Void, Void> {
        private NoteDao noteDao;

        private DeleteAllNotesAsyncTask(NoteDao noteDao) {
            this.noteDao = noteDao;
        }

        @Override
        protected Void doInBackground(Note... notes) {
            noteDao.deleteAllNotes();
            return null;
        }
    }
}

 

4. NoteDatabase abstract

 마지막으로 실제 로컬 DB인 Room을 생성합니다. DB를 처음 생성할 때는 해당 작업이 먼저 이뤄져야하므로 synchronized 키워드를 통해 동기화시킵니다.

 DB 생성 시, Callback 메서드에서 PopulateDbAsyncTask 객체를 생성하여 초기값을 설정해줍니다.

 

package com.heon9u.aacproject;

import android.content.Context;
import android.os.AsyncTask;

import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase;

@Database(entities = {Note.class}, version = 1)
public abstract class NoteDatabase extends RoomDatabase {

    private static NoteDatabase instance;

    public abstract NoteDao noteDao();

    public static synchronized NoteDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context.getApplicationContext(),
                    NoteDatabase.class, "note_database")
                    .fallbackToDestructiveMigration()
                    .addCallback(roomCallback)
                    .build();
        }

        return instance;
    }

    private static RoomDatabase.Callback roomCallback = new RoomDatabase.Callback() {
        @Override
        public void onCreate(@NonNull SupportSQLiteDatabase db) {
            super.onCreate(db);
            new PopulateDbAsyncTask(instance).execute();
        }
    };

    private static class PopulateDbAsyncTask extends AsyncTask<Void, Void, Void> {
        private NoteDao noteDao;

        private PopulateDbAsyncTask(NoteDatabase db) {
            noteDao = db.noteDao();
        }

        @Override
        protected Void doInBackground(Void... voids) {
            noteDao.insert(new Note("Title1", "Description 1", 1));
            noteDao.insert(new Note("Title2", "Description 2", 2));
            noteDao.insert(new Note("Title3", "Description 3", 3));
            return null;
        }
    }
}

 

 

5. NoteViewModel

 마지막으로 UI에 매칭시킬 데이터를 저장 및 처리하는 ViewModel입니다. UI와 Repository의 중간 단계에 있으며, 뒤에서 언급할 Observe를 통해 데이터 셋의 변화를 모니터링하며 UI에 적용하는 역할을 합니다.

 

package com.heon9u.aacproject;

import android.app.Application;

import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;

import java.util.List;

public class NoteViewModel extends AndroidViewModel {
    private NoteRepository repository;
    private LiveData<List<Note>> allNotes;

    public NoteViewModel(@NonNull Application application) {
        super(application);
        repository = new NoteRepository(application);
        allNotes = repository.getAllNotes();
    }

    public void insert(Note note) {
        repository.insert(note);
    }

    public  void update(Note note) {
        repository.update(note);
    }

    public void delete(Note note) {
        repository.delete(note);
    }

    public void deleteAllNotes() {
        repository.deleteAllNotes();
    }

    public LiveData<List<Note>> getAllNotes() {
        return allNotes;
    }
}

 

 

 

지금까지 작성한 코드들 중, ViewModel을 MainActivity에서 어떻게 활용하는지 코드로 확인하겠습니다. ViewModel의 객체를 생성하여 List에 포함된 객체들을 모니터링합니다. 이때, Observer 객체 생성 및 onChanged()를 오버라이딩하여 구현합니다.

 실제로 List의 데이터 셋이 바뀔 경우, Toast 메세지가 생성됩니다.

 

package com.heon9u.aacproject;

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;

import android.os.Bundle;
import android.widget.Toast;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    private NoteViewModel noteViewModel;

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

        noteViewModel = new ViewModelProvider(this).get(NoteViewModel.class);
        noteViewModel.getAllNotes().observe(this, new Observer<List<Note>>() {
            @Override
            public void onChanged(List<Note> notes) {
                // update RecyclerView
                Toast.makeText(MainActivity.this, "onChanged", Toast.LENGTH_SHORT).show();
            }
        });
    }
} 

 

 

 여기까지 Android의 로컬 DB인 Room을 구현하는 과정입니다. 각 코드들이 무엇을 의미하는지 보다, 각 Class들이 어떤 역할을 하는지, 메서드의 호출 흐름을 파악하면 이해가 빠를 것입니다. 다음 포스팅에서는 layout구성과 RecylcerView를 구현해보도록 하겠습니다.

 

 

 

728x90
반응형