Skip to content

Commit

Permalink
Improvements for ViewModels using Koin
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitriy-chernysh committed Feb 17, 2024
1 parent c117923 commit f1f1f60
Show file tree
Hide file tree
Showing 18 changed files with 197 additions and 192 deletions.
7 changes: 1 addition & 6 deletions build-logic/src/main/kotlin/feature-module.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,11 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = libs.versionStr("compose.compiler")
}

// It solve the issue with testing Coroutines Flow / ViewModel state
// and getting the error "app.cash.turbine.TurbineAssertionError: No value produced in 3s"
testOptions {
unitTests.isReturnDefaultValues = true
}
}

dependencies {
implementation(project(":core:ui"))
implementation(project(":core:di"))
implementation(project(":core:domain"))
implementation(project(":core:coroutines"))
}
2 changes: 1 addition & 1 deletion core/di/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("core-module")
id("core-compose-module")
}

dependencies {
Expand Down
23 changes: 22 additions & 1 deletion core/di/src/main/kotlin/com/mobiledevpro/di/KoinExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,37 @@
*/
package com.mobiledevpro.di

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.remember
import org.koin.compose.module.rememberKoinModules
import org.koin.compose.scope.rememberKoinScope
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.module.Module
import org.koin.core.qualifier.TypeQualifier
import org.koin.core.scope.Scope
import org.koin.ext.getFullName
import org.koin.java.KoinJavaComponent


inline fun <reified T : Any> koinScope(): Scope {
inline fun <reified T> koinScope(): Scope {

val scopeId = T::class.getFullName() + "@" + T::class.hashCode()
val qualifier = TypeQualifier(T::class)

return KoinJavaComponent.getKoin().getOrCreateScope(scopeId, qualifier)
}

@OptIn(KoinExperimentalAPI::class)
@Composable
inline fun <reified T> rememberViewModel(
crossinline modules: @DisallowComposableCalls () -> List<Module>
): T {
rememberKoinModules(
modules = modules
)

val scope = rememberKoinScope(scope = koinScope<T>())

return remember { scope.get() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.mobiledevpro.chatlist.di.featureChatListModule
import com.mobiledevpro.chatlist.view.ChatListScreen
import com.mobiledevpro.chatlist.view.ChatListViewModel
import com.mobiledevpro.di.koinScope
import com.mobiledevpro.di.rememberViewModel
import com.mobiledevpro.home.view.HomeScreen
import com.mobiledevpro.home.view.HomeViewModel
import com.mobiledevpro.navigation.ext.navigateTo
Expand All @@ -45,13 +46,13 @@ import com.mobiledevpro.people.profile.view.PeopleProfileScreen
import com.mobiledevpro.people.profile.view.PeopleProfileViewModel
import com.mobiledevpro.people.profile.view.args.PeopleProfileArgs
import com.mobiledevpro.people.view.PeopleScreen
import com.mobiledevpro.peoplelist.di.featurePeopleListModule
import com.mobiledevpro.peoplelist.view.PeopleListScreen
import com.mobiledevpro.peoplelist.view.PeopleListViewModel
import com.mobiledevpro.subscription.SubscriptionScreen
import com.mobiledevpro.user.profile.di.featureUserProfileModule
import com.mobiledevpro.user.profile.view.ProfileScreen
import com.mobiledevpro.user.profile.view.vm.ProfileViewModel
import org.koin.core.context.loadKoinModules


fun NavGraphBuilder.homeNavGraph(onNavigateToRoot: (Screen) -> Unit) {
Expand Down Expand Up @@ -179,7 +180,9 @@ fun NavGraphBuilder.chatListScreen() {
route = Screen.ChatList.route
) {

val viewModel: ChatListViewModel = viewModel()
val viewModel = rememberViewModel<ChatListViewModel>(
modules = { listOf(featureChatListModule) }
)

ChatListScreen(
state = viewModel.uiState,
Expand All @@ -195,7 +198,9 @@ fun NavGraphBuilder.peopleListScreen(onNavigateTo: (Screen) -> Unit) {
route = Screen.PeopleList.route
) {

val viewModel: PeopleListViewModel = viewModel()
val viewModel = rememberViewModel<PeopleListViewModel>(
modules = { listOf(featurePeopleListModule) }
)

PeopleListScreen(
viewModel.uiState,
Expand Down Expand Up @@ -237,8 +242,11 @@ fun NavGraphBuilder.profileScreen(onNavigateTo: (Screen) -> Unit) {
route = Screen.Profile.route
) {

loadKoinModules(featureUserProfileModule)
val viewModel: ProfileViewModel by koinScope<ProfileViewModel>().inject()
val viewModel = rememberViewModel<ProfileViewModel>(
modules = {
listOf(featureUserProfileModule)
}
)

ProfileScreen(
state = viewModel.uiState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,25 @@
* limitations under the License.
*
*/
package com.mobiledevpro.user.profile.data.local
package com.mobiledevpro.chatlist.di

import com.mobiledevpro.chatlist.domain.usecase.GetChatListUseCase
import com.mobiledevpro.chatlist.view.ChatListViewModel
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.scopedOf
import org.koin.dsl.module

/**
* For User Profile screen
* User Profile screen module
*
* Created on Jul 22, 2023.
*
*/
class ImplUserProfileLocalSource(

) : UserProfileLocalSource {
val featureChatListModule = module {

scope<ChatListViewModel> {
viewModelOf(::ChatListViewModel)
scopedOf(::GetChatListUseCase)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@
* limitations under the License.
*
*/
package com.mobiledevpro.user.profile.data.repository
package com.mobiledevpro.chatlist.domain.usecase

import com.mobiledevpro.user.profile.data.local.UserProfileLocalSource
import com.mobiledevpro.coroutines.BaseCoroutinesFLowUseCase
import com.mobiledevpro.coroutines.None
import com.mobiledevpro.domain.model.Chat
import com.mobiledevpro.domain.model.fakeChatList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

/**
* For User Profile screen
*
* Created on Jul 22, 2023.
*
*/
class ImplUserProfileRepository(
private val localSource: UserProfileLocalSource
) : UserProfileRepository {

class GetChatListUseCase(

) : BaseCoroutinesFLowUseCase<List<Chat>, None>(Dispatchers.IO) {

override fun buildUseCaseFlow(params: None?): Flow<List<Chat>> =
flowOf(fakeChatList)

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@
package com.mobiledevpro.chatlist.view

import com.mobiledevpro.domain.model.Chat
import com.mobiledevpro.ui.state.UIState

/**
* UI state for [com.mobiledevpro.chatlist.view.ChatListScreen]
*
* Created on Feb 04, 2023.
*
*/
sealed interface ChatListUIState {
sealed interface ChatListUIState : UIState {

object Empty : ChatListUIState
data object Empty : ChatListUIState

object Loading : ChatListUIState
data object Loading : ChatListUIState

class Success(val profileList : List<Chat>) : ChatListUIState

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,34 @@
*/
package com.mobiledevpro.chatlist.view

import androidx.lifecycle.ViewModel
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.mobiledevpro.domain.model.fakeChatList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import com.mobiledevpro.chatlist.domain.usecase.GetChatListUseCase
import com.mobiledevpro.ui.vm.BaseViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach


class ChatListViewModel : ViewModel() {
private val _uiState: MutableStateFlow<ChatListUIState> =
MutableStateFlow(ChatListUIState.Empty)
val uiState: StateFlow<ChatListUIState> = _uiState.asStateFlow()
class ChatListViewModel(
private val getChatListUseCase: GetChatListUseCase
) : BaseViewModel<ChatListUIState>() {

override fun initUIState(): ChatListUIState = ChatListUIState.Empty

init {
Log.d("UI", "ChatListViewModel init")
observeChatList()
}

private fun observeChatList() {
viewModelScope.launch {

_uiState.update { ChatListUIState.Loading }

// delay(1000)

/*
_uiState.update { PeopleProfileUIState.Empty }
_uiState.update { PeopleProfileUIState.Fail(Throwable("Test error")) }
*/
_uiState.update {
ChatListUIState.Success(fakeChatList)
}
}
getChatListUseCase.execute()
.onEach { result ->
result.onSuccess { list ->
ChatListUIState.Success(list)
.also { _uiState.value = it }
}.onFailure {
//TODO: show an error
}
}.launchIn(viewModelScope)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,25 @@
* limitations under the License.
*
*/
package com.mobiledevpro.user.profile.domain.interactor
package com.mobiledevpro.peoplelist.di

import com.mobiledevpro.user.profile.data.repository.UserProfileRepository
import com.mobiledevpro.peoplelist.domain.usecase.GetPeopleListUseCase
import com.mobiledevpro.peoplelist.view.PeopleListViewModel
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.scopedOf
import org.koin.dsl.module

/**
* For User Profile screen
* User Profile screen module
*
* Created on Jul 22, 2023.
*
*/
class ImplUserProfileInteractor(
val repository: UserProfileRepository
) : UserProfileInteractor {

val featurePeopleListModule = module {

scope<PeopleListViewModel> {
viewModelOf(::PeopleListViewModel)
scopedOf(::GetPeopleListUseCase)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2023 | Dmitri Chernysh | https://mobile-dev.pro
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.mobiledevpro.peoplelist.domain.usecase

import com.mobiledevpro.coroutines.BaseCoroutinesFLowUseCase
import com.mobiledevpro.coroutines.None
import com.mobiledevpro.domain.model.PeopleProfile
import com.mobiledevpro.domain.model.fakePeopleProfileList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf


class GetPeopleListUseCase(

) : BaseCoroutinesFLowUseCase<List<PeopleProfile>, None>(Dispatchers.IO) {

override fun buildUseCaseFlow(params: None?): Flow<List<PeopleProfile>> =
flowOf(fakePeopleProfileList)
}
Loading

0 comments on commit f1f1f60

Please sign in to comment.