Android Dev/NowInAndroid / / 2022. 11. 1. 17:19

NowInAndroid(3) - (Hilt) Viewmodel + DI overall flow, 재구성 예제

1.  ViewModel with Compose data flow

 

1> Viewmodel을 hiltViewmodel()을 통해 instance 하면 DI 를 통해 필요한 정보들을 다 끌어다와 필요한 repository와 usecases를 가져다 올 수 있다.

2> ViewModel은 repository에서 stateFlow로 가져온 데이터를 Usecase와 조합하여 ViewModel내의 stateFlow로 조합하고, call back method를 관리하기 위해 메소드를 생성한다.

3> Compose 내에서는 instance된 ViewModel의 stateFlows에 대해 collectAsStateWithLifeCycle로 ViewModelScope내에 데이터를 수집할 수 있으며 수집된 데이터를 기반으로 한 View단에서의 구현을 해주면된다.

 

 

아래 플로우를 참고하라.

ViewModel 의 구성과 compose를 통해 사용하는 방법에 대한 플로우

 

 

 

2.  ViewModel with Compose data flow

 

1> Hilt-Dagger injection으로 Viewmodel에 주입할 Repository를 구성하는 방법은 아래 플로우와 같이 디자인을 참고하라.

2> Viewmodel은 interface Repository를 DI에서 implicitly하게 가져온다.

3> Repository는 주로 DAO interface와 Datasource interface 부분을 가지고 있으며 이를 ViewModel에서 호출하게 된다.

4> DAO interface와 연결되는 Room DB의 경우 또한 implicitly 하게 가져와야하므로, DI쪽에 Room DB instance 부분을 구성해야한다. 따라서 아래 그림과 같이 a) RoomDatabase를 만들고 DAO에 대한 메소드 선언을 하기위한 abstract class for ROOM. b)  DAO를 provide할 DI, c) DataBase를 providing하는 singleton Database로 구성한다.

5> 4>에서 만든 DAO는 Repository별로 사용할 부분들을 interface DAO를 가져와 implicitly call할 수 있으며, 이렇게 구성된 Repository는 이를 선언한 ViewModel에 의해 최종적으로 Consumed된다.

ViewModel을 위한 DI 구성 플로우

 

 

3.  Revision to ForYouViewModel

 

위를 참고하여 이전에 NowInAndroid(2) - ForYouViewModel 에서 리뷰했던 ViewModel의 구조를 다시 한번 보면 다음과 같다.

ForYouViewModel의 DI flow

 

 

 

4.  (Revisited) ViewModel 재구성(2022-11-17)

1. 위의 나우안의 구조를 참고하여 VM을 아래 순서와 같이 재구성하였다.

Firebase + Syncable을 통한 network / room 동기화

 

(interface syncable을 사용하여 network 동기화 하는 부분은 (https://witcheryoon.tistory.com/346)를 참고하라)

 

먼저 Room db를 설계하는 부분을 만든뒤 Testcode를 만든다. 그 뒤 Firebase을 통한 Network 모델을 만들 것이다.

 

 1>  core - data 부분에 대한 패키지 구성을 한다.

repository와 entity model
model 패키지 구성

설계 순서 1> database를 구성한다.

데이터는 간단한 형태의 bulletin board를 생각해서 아래의 데이터를 포함하게 될 것이다.

author / date / title / text / imageUrl

 

model - data

../core/model/data/Bulletin.kt

data class Bulletin(
    val wid: String, //date+author
    val title: String,
    val text: String,
    val imageUrl: String,
    val author: String
)

val previewBulletins = listOf(
    Bulletin(
        wid = "22",
        title = "https://twitter.com/alex_vanyo",
        text = "Alex joined Android DevRel in 2021, and has worked supporting form factors from small watches to large foldables and tablets. His special interests include insets, Compose, testing and state.",
        imageUrl = "https://pbs.twimg.com/profile_images/1431339735931305989/nOE2mmi2_400x400.jpg",
        author = "Alex Vanyo",
    ),
    Bulletin(
        wid = "312",
        title = "Simona Stojanovic",
        text = "Android Developer Relations Engineer @Google, working on the Compose team and taking care of Layouts & Navigation.",
        imageUrl = "https://twitter.com/anomisSi",
        author = "https://pbs.twimg.com/profile_images/1437506849016778756/pG0NZALw_400x400.jpg",
    )
)

com.vlmplayground.core.model.data/Bulletin.kt

database - model 

../core/database/model/BulletinEntity.kt

@Entity(
    tableName = "bulletinboard",
)
data class BulletinEntity(
    @PrimaryKey
    val wid: String, //date+author
    @ColumnInfo(defaultValue = "")
    val title: String,
    @ColumnInfo(defaultValue = "")
    val text: String,
    @ColumnInfo(name = "image_url")
    val imageUrl: String,
    @ColumnInfo(defaultValue = "")
    val author: String
)

fun BulletinEntity.asExternalModel() = Bulletin(
    wid = wid,
    title   = title,
    text    = text,
    imageUrl    = imageUrl,
    author  = author,
)

 

 

설계 순서 2> Dao를 설계한다.

.../core/database/dao/BulletinBoardDao.kt

@Dao
interface BulletinBoardDao {

    @Query(
        value = """
        SELECT * FROM bulletinboard
        """
    )
    fun getAllEntityStream(): Flow<BulletinEntity>

    @Query(
        value = """
            DELETE FROM bulletinboard
        """
    )
    suspend fun deleteAllEntityStream()

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertOrIgnoreBulletin(bulletinEntities: List<BulletinEntity>): List<Long>

    @Update
    suspend fun updateBulletin(entities: List<BulletinEntity>)

    @Upsert
    suspend fun upsertBulletin(entities: List<BulletinEntity>)

}

 

설계 순서 3> RoomDatabase 를 만든다.

../core/database/VlmDatabase.kt

@Database(
    entities = [
        BulletinEntity::class
    ],
    version = 11,
    autoMigrations = [
//        AutoMigration(from = 1, to = 2),
    ],
    exportSchema = true,
)
@TypeConverters(
    InstantConverter::class,
)
abstract class VlmDatabase : RoomDatabase() {
    abstract fun topicDao(): BulletinBoardDao
}

 

 

설계 순서 4> repository모델로 설계(core - data model)

위의 위치 repository class를 생성

모든 구성을 끝내고나서 di - DataModule에서 injecting을 하는 코드를 설계할 것임

먼저 model과 repository 설정은 아래와 같이 설계할 수 있다.

 

 

../core/data/model/Bulletin.kt

package com.vlmplayground.core.data.model

import com.vlmplayground.core.database.model.BulletinEntity
import com.vlmplayground.core.model.data.Bulletin

fun Bulletin.asEntity() = BulletinEntity(
    wid = wid,
    title   = title,
    text    = text,
    imageUrl    = imageUrl,
    author  = author,
)

 

../core/data/repository/BulletinRepository.kt

package com.vlmplayground.core.data.repository

import com.vlmplayground.core.model.data.Bulletin
import kotlinx.coroutines.flow.Flow

interface BulletinRepository /*: Syncable*/ {
    fun getAuthorsStream(): Flow<List<Bulletin>>
    fun getAuthorStream(id: String): Flow<Bulletin>
}

 

.../core/data/repository/OfflineFirstBulletinRepository.kt

import com.vlmplayground.core.database.dao.BulletinBoardDao
import com.vlmplayground.core.database.model.BulletinEntity
import com.vlmplayground.core.database.model.asExternalModel
import com.vlmplayground.core.model.data.Bulletin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class OfflineFirstBulletinRepository@Inject constructor(
    private val bulletinBoardDao: BulletinBoardDao,
//    private val network: NetworkDataSource,
) : BulletinRepository {

    
    override fun getBulletinStream(): Flow<List<Bulletin>> =
        bulletinBoardDao.getAllEntityStream().map { it.map(BulletinEntity::asExternalModel) }

//    override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
//        synchronizer.changeListSync(
//
//      )
}

위와 같이 러프하게 설계를 일단 해놓고 이를 di/DataModule.kt에서 아래와 같이 주입할 수 있다.

 

import com.vlmplayground.core.data.repository.BulletinRepository
import com.vlmplayground.core.data.repository.OfflineFirstBulletinRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
interface DataModule {
    @Binds
    fun bindsBulletinRepository(
        topicsRepository: OfflineFirstBulletinRepository
    ): BulletinRepository
}

 

위와같이 설계하면, repository에 대한 1차적인 설계가 끝나게 된다. 이를 Viewmodel에서 사용하면 된다.

 

테스트 모델 구성 : androidTest에 Firebase Networking을 하여 위의 모델을 syncable 하게 다시 구성하였다.

아래의 링크를 참고하라 : https://witcheryoon.tistory.com/359

 

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유