Dagger 2 с ViewModel, репозиторием, комнатой и сопрограммами

Я пытаюсь использовать Dagger 2 в проекте ViewModel + Respository + Room + Retrofit + Coroutines, написанном на Kotlin.

В настоящее время каждая ViewModel инициализирует необходимые репозитории и их зависимости сама по себе, вот так

class HomeViewModel(
    application: Application
) : AndroidViewModel(application) {
    private val repository: UserRepository =
        UserRepository(
            Webservice.create(),
            AppDatabase.getDatabase(application, viewModelScope).userDao()
        )

Я хотел бы упростить это до этого

class HomeViewModel @Inject constructor(
    private val repository: UserRepository
): ViewModel()

Чего я добился до сих пор

Создан компонент и модули кинжала

@Singleton
@Component(modules = [
    AppModule::class,
    NetworkModule::class,
    DataModule::class,
    RepositoryModule::class
])
interface AppComponent {
    val webservice: Webservice
    val userRepository: UserRepository
}

@Module
class AppModule(private val app: Application) {
    @Provides
    @Singleton
    fun provideApplication(): Application = app
}

@Module
class DataModule {
    @Provides
    @Singleton
    fun provideApplicationDatabase(app: Application, scope: CoroutineScope) =
        AppDatabase.getDatabase(app, scope)

    @Provides
    @Singleton
    fun provideUserDao(db: AppDatabase) = db.userDao()
}

@Module
class NetworkModule {
    @Provides
    @Singleton
    fun provideWebservice() = Webservice.create()
}

@Module
class RepositoryModule {
    @Provides
    @Singleton
    fun provideUserRepository(webservice: Webservice, userDao: UserDao) =
        UserRepository(webservice, userDao)
}

Получил инициализацию AppComponent в классе приложения

class App : Application() {
    companion object {
        lateinit var appComponent: AppComponent
    }
    override fun onCreate() {
        super.onCreate()
        appComponent = initDagger(this)
    }
    private fun initDagger(app: App): AppComponent =
        DaggerAppComponent.builder()
            .appModule(AppModule(app))
            .build()
}

А теперь я застрял.

Первый вопрос: как заставить работать конструктор вставки ViewModel?

Первоначально он был инициализирован из HomeFragment вот так

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java)

Как мне теперь вызвать инициализатор?

Второй вопрос немного сложнее.

Конструктору базы данных требуется область Coroutine, чтобы предварительно заполнить ее в фоновом потоке во время создания. Как мне перейти в прицел сейчас?

Вот определение базы данных и обратного вызова

@Database(
    entities = [User::class],
    version = 1, exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context, scope: CoroutineScope): AppDatabase {
            val tempInstance =
                INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "database"
                )
                    .fallbackToDestructiveMigration()
                    .addCallback(AppDatabaseCallback(scope))
                    .build()
                INSTANCE = instance
                return instance
            }
        }
    }

    private class AppDatabaseCallback(
        private val scope: CoroutineScope
    ) : RoomDatabase.Callback() {RoomDatabase.Callback() {
        override fun onCreate(db: SupportSQLiteDatabase) {
            super.onCreate(db)
            INSTANCE?.let { database ->
                scope.launch(Dispatchers.IO) {
                    //inserts
                }
            }
        }
    }
}

person yaugenka    schedule 20.01.2020    source источник


Ответы (2)


Второй вопрос посложнее.

Конструктору базы данных требуется область Coroutine, чтобы предварительно заполнить ее в фоновом потоке во время создания. Как мне перейти в прицел сейчас?

На самом деле это проще, не вводите CoroutineScope, используйте GlobalScope для этой операции.

Первый вопрос: как заставить работать конструктор Inject ViewModel?

Вам нужно получить Provider<HomeViewModel> от Dagger, затем вызвать его внутри ViewModelProvider.Factory, чтобы создать экземпляр HomeViewModel через провайдера, зарегистрированного в компоненте Dagger.

В качестве альтернативы, если Activity имеет свой собственный подкомпонент, вы можете использовать @BindsInstance, чтобы поместить экземпляр Activity в график, а затем переместить ViewModelProviders.of(activity).get(HomeViewModel::class.java, object: ViewModelProvider.Factory { ... return homeViewModelProvider.get() as T ... }) в модуль этого подкомпонента. Затем из этого подкомпонента можно было бы получить фактический экземпляр HomeViewModel и при этом получить правильную область действия + обратный вызов onCleared().

person EpicPandaForce    schedule 20.01.2020
comment
Благодарю за ответ. Действительно ли полезно использовать GlobalScope в производственных сборках, особенно в этом случае? Не могли бы вы предоставить пример кода того, как obtain the Provider<HomeViewModel> from Dagger, then invoke it inside a ViewModelProvider.Factory? Действие, содержащее HomeFragment, не имеет собственных подкомпонентов. - person yaugenka; 20.01.2020
comment
Вы получаете Provider точно так же, как получили бы экземпляр ViewModel из Dagger, за исключением того, что вы указываете его как Provider<MyViewModel> вместо ViewModel. Если бы GlobalScope был обязательством во всех случаях, его бы не существовало. - person EpicPandaForce; 20.01.2020
comment
Это тот же подход, что и этот ответ? stackoverflow.com/a/48837581/5360898 - person yaugenka; 21.01.2020
comment
Просто хотел упомянуть, что GlobalScope здесь безусловно разрешен, но есть более эффективные способы использовать GlobalScope, включая обработчик ошибок, в противном случае вам нужно будет предоставить уловки попыток для всех ваших блоков сопрограмм GlobalScope, если они могут выйти из строя. proandroiddev.com/coroutines-snags-6bf6fb53a3d1 Переход к нижним исключениям журналирования - person Aaron Smentkowski; 29.05.2020

Вам не нужно передавать область сопрограммы, просто запустите сопрограмму в диспетчере ввода-вывода, например:

@Database(
entities = [
    Login::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun loginDao(): LoginDao

companion object {

    @Volatile private var INSTANCE: AppDatabase? = null

    fun getInstance(app: Application): AppDatabase = INSTANCE ?: synchronized(this) {
            INSTANCE ?: buildDatabase(app).also { INSTANCE = it }
        }

    private fun buildDatabase(app: Application) =
        Room.databaseBuilder(app,
                AppDatabase::class.java,
                "daily_accountant")
            // prepopulate the database after onCreate was called
            .addCallback(object : Callback() {
                override fun onCreate(db: SupportSQLiteDatabase) {
                    super.onCreate(db)
                    // Do database operations through coroutine or any background thread
                    val handler = CoroutineExceptionHandler { _, exception ->
                        println("Caught during database creation --> $exception")
                    }

                    CoroutineScope(Dispatchers.IO).launch(handler) {
                        // Pre-populate database operations
                    }
                }
            })
            .build()
    }
}

И удалите coroutineScope из параметра функции.

person Md. Yamin Mollah    schedule 30.03.2020