Android

Room With Coroutine Flow

그란. 2022. 7. 30. 10:13

Room과 Flow을 결합하는 방법을 학습하여 정리함

 

샘플 기능 내용

데이터 구조 

data class User(
    val id : Long,
    val name :String,
    val favorites : List<Favorite>
){
    data class Favirite(
    	val id : Int,
        val name : String
    )
}

도메인 레이어에서는 User 모델을 사용하고 

Local 레이어에서는 UserEntity, FavoriteEntity로 분리하여 저장 

 

1 : N으로 정의된 Room 테이블에서 데이터가 업데이트 될때마다 Listen을 할 수 있게 만드는게 목적

 

1.  버튼을 누를때마다 현재 유저의 favoriteList에 추가된다.

2. 업데이트 될때마다 Flow형태로 구독할 수 있다.

3. Room Interface Dao를 정의할때 데이터를 가져오는 방법이 크게 두가지가 있다 

  • 중간 데이터 클래스를 만들어 관계를 지정하는 방법 
    • Query는 간단하지만 Entity 클래스를 별도로 정의해야함
  • 멀티 맵핑 ( Room 2.4 이상) 
    • Query는 복잡하지만 코드의 복잡도는 낮아짐 
    • 구글에서 추천하는 방법 

4. 기능 정의

  •   fetchUser() : Flow<User>    ->  현재 UserId로 User객체 ( List<Favorite>  포함 ) 를 가져오는 함수
    1. 중간 데이터 클래스로 가져옴
  •   fetchAll() : Flow<List<User>>   -> 저장되어 있는 모든 유저정보를 가져오는 함수
    1. 멀티 맵핑 형태로 가져옴
  •   updateFavorite(favorite: User.Favorite)

 

(가정)

  • 여러개의 계정을 로그인, 로그아웃 허용
  • 로그인 할때마다 저장되어 있는 유저정보가 있다면 가져와서 사용
  • 멀티모듈, Hilt 적용 
  • DI는 이전글의 DataStore와 비슷하기에 생략 

 

UserEntitiy

@Entity(tableName = "user")
internal data class UserEntity(
    @PrimaryKey
    @ColumnInfo(name = "id")
    val id: Long,

    @ColumnInfo(name = "name")
    val name: String

)

FavoriteEntity

@Entity(tableName = "favorite", primaryKeys = ["id", "userId"])
internal data class FavoriteEntity(
    @ColumnInfo(name = "id")
    val id: Int,

    @ColumnInfo(name = "userId")
    val userId: Long,

    @ColumnInfo(name = "name")
    val name: String,

    )

 

UserWithFavoritesEntity 

internal data class UserWithFavoriteEntity(
    @Embedded val user: UserEntity,

    @Relation(
        parentColumn = "id",
        entityColumn = "userId"
    )
    val favorites: List<FavoriteEntity>

)

 

 

UserDao

@Query("SELECT * FROM user WHERE user.id = :userId")
fun fetchUser(userId: Long): Flow<UserWithFavoriteEntity>  // 중간 데이터 클래스 정의

@Query("SELECT * FROM user JOIN favorite ON user.id = favorite.userId")
fun fetchAll(): Flow<Map<UserEntity, List<FavoriteEntity>>> // 멀티 맵핑 정의

 

FavoriteDao

@Insert(onConflict = REPLACE)
suspend fun insert(favorite: FavoriteEntity)

 

 

UserRepository 

internal class UserRepositoryImpl @Inject constructor(
    private val local: UserLocalDataSource,
    private val session: SessionLocalDataSource
) : UserRepository {

        override fun fetchUser(): Flow<User> {
            return session.fetchUserId()  // Flow<Long?> 형태
                .filterNotNull()
                .flatMapLatest {
                    local.fetchUser(it).map { user ->
                        User(
                            id = user.id,
                            name = user.name,
                            favorites = user.favorites.map { favorite ->
                                User.Favorite(
                                    id = favorite.id,
                                    name = favorite.name
                                )
                            }
                        )
                    }
                }
        }
    }
    
    override fun fetchAll() = local.fetchAll()
        .map {
            it.map { user ->
                User(
                    id = user.id,
                    name = user.name,
                    favorites = user.favorites.map { favorite ->
                        User.Favorite(
                            id = favorite.id,
                            name = favorite.name
                        )
                    }
                )
            }
        }
    
    //Favorite 테이블에 데이터를 추가하는 기능
    override suspend fun updateFavorite(favorite: User.Favorite) {
        val userId = session.fetchUserId().firstOrNull() ?: return
        local.updateFavorite(
            userId = userId,
            favorite = UserData.Favorite(
                id = favorite.id,
                name = favorite.name
            )
        )
    }
}

 

 

UseCase

@Singleton
class FetchUserUseCase @Inject constructor(
    private val repository: UserRepository
) {
    operator fun invoke(): Flow<User> = repository.fetchUser() ?: emptyFlow()
}


@Singleton
class FetchUserListUseCase @Inject constructor(
    private val repository: UserRepository
) {
    operator fun invoke(): Flow<List<User>> = repository.fetchAll()
}


@Singleton
class UpdateFavoriteUseCase @Inject constructor(
    private val repository: UserRepository
) {

    suspend operator fun invoke(
        id: Int,
        name: String
    ) = repository.updateFavorite(
        User.Favorite(
            id = id,
            name = name
        )
    )
}

 

//ViewModel

val user = fetchUserUseCase.invoke()

val userList = fetchUserListUseCase.invoke()

fun updateFavorite() = viewModelScope.launch {
        updateFavoriteUseCase.invoke(
            id = Random.nextInt(),
            name = UUID.randomUUID().toString()
        )
    }

 

//Activity
repeatOnStarted {
    viewModel.user.collect {
        Log.e("ERROR", "user: $it")
    }
}

repeatOnStarted {
    viewModel.userList.collect {
        Log.e("ERROR", "userList: ${it.joinToString("\n")}")
    }
}

btn.setOnClickListener {  // 버튼을 누를때마다 FavoriteTable에 데이터를 입력한다
        viewModel.updateFavorite()
}

 

 


 

정리 

유저정보를 가져오고 싶은데 단순히 유저정보만이 아닌 Relation 관계에 있는 정보들도 같이 가져오려고 한다. 

 

이때 유저정보를 업데이트했을때 Noti 받는건 당연하고 

Relation 정보를 업데이트 했을때도 Noti 를 받으려고 한다.

 

이런 상황에서 Room의 Interface가 잘 정의되어 있어 우리는 User 객체 하나로 추상화 할 수 있게 된다.

 

 

 

'Android' 카테고리의 다른 글

소셜 로그인 모듈화 과정 ( 추상화 및 액티비티 의존성 제거 )  (1) 2022.09.29
transition animation 적용  (0) 2022.08.14
DataStore 적용하기  (0) 2022.07.27
코루틴 공부  (0) 2022.07.18
Facebook Login Trouble Shooting  (0) 2022.07.18