정상에서 IT를 외치다

[Android, MVP, Room] MVP 기본 예제 - Room 활용 본문

안드로이드

[Android, MVP, Room] MVP 기본 예제 - Room 활용

Black-Jin 2019. 2. 7. 15:48
반응형


안녕하세요. 블랙진입니다.


지난시간 Android MVP 기본예제에 대한 포스팅을 진행했습니다. 이번에는 기본 예제에 Room을 사용하여 최근 보았던 유저 목록을 저장해보겠습니다.

현재 MVP 를 활용한 토이프로젝트를 총 3단계에 걸쳐서 리뷰를 진행하고 있습니다.


1. MVP 기본 예제

2. Room 사용하기

3. Dagger 사용하기


<작업>


기존 작업물에서 MainActivity 하단에 Recent User 버튼이 생겼습니다. 클릭했던 유저의 데이터를 Room 데이터베이스에 저장하여 RecentActivity 화면에 보여줍니다. 또한 Clear All 버튼을 클릭하여 모든 데이터를 삭제할 수 있습니다.



1. Room 설정하기

Room을 설정할 수 있는 방법은 포스팅을 확인해주세요.

'


2. 엔티티를 만들어 줍니다.

데이터베이스에 저장할 데이터 형식으로 User 객체를 아래와 같이 수정해 줍니다.

//테이블 이를을 설정합니다.
//데이터베이스에 저장할 데이터의 형식을 정합니다.
@Entity(tableName = "userTable")
public class User implements Serializable {

// Entity 클래스의 field 가 object 인 경우 @Embeded 를 설정해 줍니다.
@Embedded
@SerializedName("gender") public String gender;

@Embedded
@SerializedName("name") public Name name;

@Embedded
@SerializedName("location") public Location location;

@Embedded
@SerializedName("login") public Login login;

@Embedded
@SerializedName("picture") public Picture picture;

// email 값을 Primary Key 롤 설정하였습니다.
// Primary Key 는 @NonNull 을 필수로 설정해 주어야 합니다.
@SerializedName("email")
@PrimaryKey
@NonNull
public String email;

@SerializedName("phone") public String phone;

@SerializedName("cell") public String cell;

public int likeCnt = 0;

public String getFullName() { return name.title + "." + name.first + " " + name.last; }

public String getLikeCnt() { return "Like : " + likeCnt; }

@Override
public String toString() {
return getFullName();
}
}

유저 객체는 서버와의 통신을 통해 랜덤유저 정보를 받아오기 위한 모델입니다. 유저 객체에 Entity 를 추가해 주고 테이블 이름을 설정해 줌으로써 Room의 데이터 베이스로도 사용할 수 있게 됩니다.



3. 데이터 접근 객체를 생성합니다.

// 데이터 접근 객체를 생성합니다.
@Dao
public interface UserDao {

// 데이터베이스에 저장소를 추가합니다.
// 이미 저장된 항목이 있을 경우 데이터를 덮어씁니다.
@Insert(onConflict = OnConflictStrategy.REPLACE)
void add(User user);

// 해당 데이터를 업데이트 합니다.
@Update
void update(User user);

//해당 데이터를 삭제합니다.
@Delete
void delete(User user);

// 저장되어 있는 저장소 목록을 반환합니다.
// Flowable 형태의 자료를 반환하므로, 데이터베이스가 변경되면 알림을 받아 새로운 자료를 가져옵니다.
// 따라서 항상 최신 자료를 유지합니다.
@Query("SELECT * FROM userTable")
Flowable<List<User>> getUser();

// repositories 테이블의 모든 데이터를 삭제합니다.
@Query("DELETE FROM userTable")
void clearAll();
}



4. 룸 데이터 베이스를 생성합니다.

// 데이터베이스에서 사용하는 엔티티와 버전을 지정합니다.
@Database(entities = {User.class}, version = 1)
public abstract class UserDatabase extends RoomDatabase {

// 데이터베이스와 연결할 데이터 접근 객체를 정의합니다.
public abstract UserDao getUserDao();
}



5. 데이터 베이스 객체를 받기위한 싱글톤 클래스를 생성합니다.

// 싱글톤 패턴을 사용하여 데이터베이스 객체를 제공합니다.
public class UserDatabaseProvider {

private volatile static UserDatabase INSTANCE = null;

private UserDatabaseProvider() {}

public static UserDatabase getInstance(Context context) {

if(INSTANCE == null) {
synchronized (UserDatabase.class) {
INSTANCE = Room.databaseBuilder(
context,
UserDatabase.class,
"black_jin.db")
.build();
}
}

return INSTANCE;
}
}


이렇게 1~5 의 순서를 통해 Room을 설정해 보았습니다. 



6. Main


MainContract

// 유저 추가를 해줍니다.
void addUser(UserDao userDao, User user);

MainContrat 에서 Room 을 위한 비즈니스 로직을 위해 위 두가지를 추가해 줍니다.



MainActivity

@Override
public void onClick(User user) {

// UserDatabase 에 저장합니다.
presenter.addUser(
UserDatabaseProvider.getInstance(this).getUserDao(),
user);

// DetailActivity 로 이동합니다.
Intent intent = new Intent(this, DetailActivity.class);
intent.putExtra(DetailActivity.KEY_USER, user);
startActivity(intent);
}

아이템 클릭시 상세 화면으로 넘어가는 부분에 addUser 을 추가해 줍니다.


MainPresenter

@Override
public void addUser(UserDao userDao, User user) {

disposable.add(
Observable.just(user)
.subscribeOn(Schedulers.io())
.subscribe(
item -> {
Log.d("MyTag","item : " + item + " 저장");
userDao.add(item);
},
error -> {
Log.d("MyTag","onError");
},
() -> {
Log.d("MyTag","onCompleted");
}
)
);

}

데이터 추가는 백그라운드 쓰레드에서 실행되어야 하므로 RxJava 를 사용했습니다.



7. Detail


DetailContract

public interface DetailContract {

interface View extends BaseContract.View {

void setText(String text);
}

interface Presenter extends BaseContract.Presenter<View> {

@Override
void setView(DetailContract.View view);

@Override
void releaseView();

// UserDao 값을 추가 인자로 보내줍니다.
void clickEvent(UserDao userDao, User user);
}
}

Like 클릭시 Room에 데이터의 변화를 알려줘야 하기 때문에 UserDao 값을 추가 인자로 보내줍니다.



DetailPresenter

private CompositeDisposable disposable;

DetailPresenter() {
disposable = new CompositeDisposable();
}

RxJava 를 사용하기 위해 disposable 객체를 초기화 해줍니다.

@Override
public void releaseView() {
disposable.clear();
}

Presenter 가 해제 되었을 때 disposable 을 clear 해주어야 합니다.

@Override
public void clickEvent(UserDao userDao, User user) {
user.likeCnt++;
view.setText(user.getLikeCnt());

RxEvent.getInstance().sendEvent(user);

// 유저 정보를 동기화 해줍니다.
disposable.add(
Observable.just(user)
.subscribeOn(Schedulers.io())
.subscribe(
item -> {
Log.d("MyTag","item : " + item + " 업데이트");
userDao.update(item);
},
error -> {
Log.d("MyTag","onError");
},
() -> {
Log.d("MyTag","onCompleted");
}
)
);
}

Like 클릭시 룸 데이터가 수정되었음을 알려줍니다.



7. Recent

MainAcivity 에서 보았던 유저들을 Room 에 저장하여 RecentActivity 에서 보고 삭제 할 수 있습니다.



RecentContract

public interface RecentContract {

interface View extends BaseContract.View {

void showProgress();

void hideProgress();

// Room 으로 부터 받은 데이터를 어댑터를 통해 보여줍니다.
void setItems(ArrayList<User> items);
}

interface Presenter extends BaseContract.Presenter<View> {

@Override
void setView(View view);

@Override
void releaseView();

// Room 으로 부터 데이터를 받아 옵니다.
void loadData(UserDao userDao);

// Room 데이터베이스에서 user 데이터 1개를 삭제합니다.
void deleteData(UserDao userDao, User user);

// Room 데이터를 전부 삭제 합니다.
void clearAll(UserDao userDao);
}
}


RecentActivity

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recent);
setTitle("RECENT USER");

// recycler view 초기화
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
adapter.setClickListener(this);

// presenter 와 연결
presenter.setView(this);
presenter.loadData(UserDatabaseProvider.
getInstance(this).
getUserDao());
}

@Override
protected void onDestroy() {
super.onDestroy();
// presenter 와의 연결을 해제합니다.
presenter.releaseView();
}

@Override
// 단일 아이템 삭제
public void onClick(User user) {
presenter.deleteData(
UserDatabaseProvider.getInstance(this).getUserDao(),
user);
}

@Override
public void showProgress() {
progressBar.setVisibility(View.VISIBLE);
}

@Override
public void hideProgress() {
progressBar.setVisibility(View.GONE);
}

@Override
public void setItems(ArrayList<User> items) {
adapter.setItems(items);
}

@OnClick(R.id.btn_clear_all)
// 모든 아이템 삭제
void onClick() {
presenter.clearAll(
UserDatabaseProvider.getInstance(this).getUserDao());
}

비즈니스 로직은 모두 Presenter 에서 이뤄 지면 View 의 변화만 Activity 에서 처리 됩니다.


RecentPresenter

@Override
public void loadData(UserDao userDao) {
disposable.add(
userDao.getUser()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(__ -> {
view.showProgress();
})
.subscribe(
users -> {
view.setItems((ArrayList<User>) users);
view.hideProgress();
},
error -> {
Log.e("MyTag", error.getMessage());
})
);
}

Room 으로부터 모든 데이터를 가져옵니다.


@Override
public void deleteData(UserDao userDao, User user) {
disposable.add(
Observable.just(user)
.subscribeOn(Schedulers.io())
.subscribe(
item -> {
Log.d("MyTag", "item : " + item + " 삭제");
userDao.delete(item);
}
)
);
}

아이템 클릭 시 해당 아이템을 삭제합니다.


@Override
public void clearAll(UserDao userDao) {
disposable.add(
Observable.just("clear ALl")
.subscribeOn(Schedulers.io())
.subscribe(
item -> {
Log.d("MyTag", item);
userDao.clearAll();
}
)
);
}

모든 데이터를 삭제합니다.


<심화>

Room 에서의 Flowable 은 아래와 같은 특징이 있습니다.

/**
* The Flowable returned by Room never complete.
* It allows you to receive update upon database modifications.
*
* 1. When there is no user in the database and the query returns no rows,
* the Flowable will not emit, neither onNext, nor onError.
* 2. When there is a user in the database, the Flowable will trigger onNext.
* 3. Every time the user data is updated, the Flowable object will emit automatically,
* allowing you to update the UI based on the latest data.
*
* https://stackoverflow.com/questions/46541385/rxjava2-flowable-not-firing-oncomplete
* https://medium.com/androiddevelopers/room-rxjava-acb0cd4f3757
*/
// 저장되어 있는 저장소 목록을 반환합니다.
// Flowable 형태의 자료를 반환하므로, 데이터베이스가 변경되면 알림을 받아 새로운 자료를 가져옵니다.
// 따라서 항상 최신 자료를 유지합니다.
@Query("SELECT * FROM userTable")
Flowable<List<User>> getUser();


MVP 를 패턴에서의 Room 사용법에 대해 포스팅을 해봤습니다. View 와 Model 그리고 Presenter 을 분리하여 보다 자기 역활에 충실 할 수 있도록 짤 수 있게 노력해 보았습니다. 이제  Dagger 을 사용해 위 코드의 객체를 외부에서 주입할 수 있도록 코드를 변경하는 법에 대해 다음 포스팅을 진행해보겠습니다.


위 예제 코드는 링크 에서 확인하실 수 있습니다.


1. MVP 기본 예제

2. Room 사용하기

3. Dagger 사용하기

반응형
Comments