diff --git a/sample-compose/app/build.gradle.kts b/sample-compose/app/build.gradle.kts index 45707ceaf..e801346a3 100644 --- a/sample-compose/app/build.gradle.kts +++ b/sample-compose/app/build.gradle.kts @@ -6,7 +6,6 @@ plugins { id("kotlin-parcelize") id("dagger.hilt.android.plugin") - id("androidx.navigation.safeargs.kotlin") id("kover") } @@ -58,7 +57,7 @@ android { } } - flavorDimensions(Flavor.DIMENSIONS) + flavorDimensions += Flavor.DIMENSION_VERSION productFlavors { create(Flavor.STAGING) { applicationIdSuffix = ".staging" @@ -81,27 +80,20 @@ android { } composeOptions { - kotlinCompilerVersion = Versions.KOTLIN_VERSION kotlinCompilerExtensionVersion = Versions.COMPOSE_COMPILER_VERSION } buildFeatures { - viewBinding = true compose = true } - lintOptions { - isCheckDependencies = true + lint { + checkDependencies = true xmlReport = true xmlOutput = file("build/reports/lint/lint-result.xml") } testOptions { - unitTests { - // Robolectric resource processing/loading - isIncludeAndroidResources = true - isReturnDefaultValues = true - } unitTests.all { if (it.name != "testStagingDebugUnitTest") { it.extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) { @@ -122,8 +114,6 @@ dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) - implementation("androidx.activity:activity-ktx:${Versions.ANDROIDX_ACTIVITY_KTX_VERSION}") - implementation("androidx.appcompat:appcompat:${Versions.ANDROIDX_SUPPORT_VERSION}") implementation("androidx.core:core-ktx:${Versions.ANDROIDX_CORE_KTX_VERSION}") implementation("androidx.lifecycle:lifecycle-runtime-ktx:${Versions.ANDROIDX_LIFECYCLE_VERSION}") @@ -131,13 +121,10 @@ dependencies { implementation("androidx.compose.ui:ui-tooling:${Versions.COMPOSE_VERSION}") implementation("androidx.compose.foundation:foundation:${Versions.COMPOSE_VERSION}") implementation("androidx.compose.material:material:${Versions.COMPOSE_VERSION}") - - implementation("androidx.fragment:fragment-ktx:${Versions.ANDROIDX_FRAGMENT_KTX_VERSION}") - implementation("androidx.navigation:navigation-fragment-ktx:${Versions.ANDROIDX_NAVIGATION_VERSION}") - implementation("androidx.navigation:navigation-runtime-ktx:${Versions.ANDROIDX_NAVIGATION_VERSION}") - implementation("androidx.navigation:navigation-ui-ktx:${Versions.ANDROIDX_NAVIGATION_VERSION}") + implementation("androidx.navigation:navigation-compose:${Versions.COMPOSE_NAVIGATION_VERSION}") implementation("com.google.dagger:hilt-android:${Versions.HILT_VERSION}") + implementation("androidx.hilt:hilt-navigation-compose:${Versions.HILT_NAVIGATION_COMPOSE_VERSION}") implementation("com.jakewharton.timber:timber:${Versions.TIMBER_LOG_VERSION}") @@ -148,16 +135,15 @@ dependencies { implementation("com.markodevcic:peko:${Versions.PEKO_VERSION}") + kapt("com.google.dagger:hilt-compiler:${Versions.HILT_VERSION}") + debugImplementation("com.github.chuckerteam.chucker:library:${Versions.CHUCKER_VERSION}") releaseImplementation("com.github.chuckerteam.chucker:library-no-op:${Versions.CHUCKER_VERSION}") - kapt("com.google.dagger:hilt-compiler:${Versions.HILT_VERSION}") - // Testing testImplementation("io.kotest:kotest-assertions-core:${Versions.TEST_KOTEST_VERSION}") testImplementation("junit:junit:${Versions.TEST_JUNIT_VERSION}") - testImplementation("org.robolectric:robolectric:${Versions.TEST_ROBOLECTRIC_VERSION}") - testImplementation("androidx.test:core:${Versions.ANDROIDX_TEST_CORE_VERSION}") + testImplementation("androidx.test:core:${Versions.TEST_ANDROIDX_CORE_VERSION}") testImplementation("androidx.test:runner:${Versions.TEST_RUNNER_VERSION}") testImplementation("androidx.test:rules:${Versions.TEST_RUNNER_VERSION}") testImplementation("androidx.test.ext:junit-ktx:${Versions.TEST_JUNIT_ANDROIDX_EXT_VERSION}") diff --git a/sample-compose/app/src/debug/AndroidManifest.xml b/sample-compose/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 26d32e7e1..000000000 --- a/sample-compose/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/sample-compose/app/src/debug/java/co/nimblehq/sample/compose/EmptyHiltActivity.kt b/sample-compose/app/src/debug/java/co/nimblehq/sample/compose/EmptyHiltActivity.kt deleted file mode 100644 index dadc84d94..000000000 --- a/sample-compose/app/src/debug/java/co/nimblehq/sample/compose/EmptyHiltActivity.kt +++ /dev/null @@ -1,7 +0,0 @@ -package co.nimblehq.sample.compose - -import androidx.appcompat.app.AppCompatActivity -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class EmptyHiltActivity : AppCompatActivity() diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/di/modules/NavigatorModule.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/di/modules/NavigatorModule.kt deleted file mode 100644 index aaf696817..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/di/modules/NavigatorModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package co.nimblehq.sample.compose.di.modules - -import co.nimblehq.sample.compose.ui.screens.MainNavigator -import co.nimblehq.sample.compose.ui.screens.MainNavigatorImpl -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.FragmentComponent - -@Module -@InstallIn(FragmentComponent::class) -abstract class NavigatorModule { - - @Binds - abstract fun mainNavigator(mainNavigator: MainNavigatorImpl): MainNavigator -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/extension/ViewModelExt.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/extension/ViewModelExt.kt deleted file mode 100644 index 1f5f9c11a..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/extension/ViewModelExt.kt +++ /dev/null @@ -1,44 +0,0 @@ -package co.nimblehq.sample.compose.extension - -import androidx.activity.ComponentActivity -import androidx.activity.viewModels -import androidx.annotation.MainThread -import androidx.fragment.app.* -import androidx.lifecycle.* -import androidx.lifecycle.viewmodel.CreationExtras - -/** - * PLEASE READ THIS BEFORE IMPLEMENT: - * Right now, there is no easy way to mock/fake the viewModel inside the Fragment when applying - * the 'by viewModels()' Kotlin property delegate from the activity-ktx/fragment-ktx artifact. - * After finding many ways to handle this issue, I end up with this solution that is to override the - * loading mechanism of the delegate. - * There is another way to resolve the issue as well and it is mentioned in the reference link. - * Reference: https://proandroiddev.com/testing-the-untestable-the-case-of-the-viewmodel-delegate-975c09160993 - */ -@MainThread -inline fun Fragment.provideActivityViewModels( - noinline extrasProducer: (() -> CreationExtras)? = null, - noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null -): Lazy = OverridableLazy(activityViewModels(extrasProducer, factoryProducer)) - -@MainThread -inline fun Fragment.provideViewModels( - noinline ownerProducer: () -> ViewModelStoreOwner = { this }, - noinline extrasProducer: (() -> CreationExtras)? = null, - noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null -): Lazy = OverridableLazy(viewModels(ownerProducer, extrasProducer, factoryProducer)) - -@MainThread -inline fun ComponentActivity.provideViewModels( - noinline extrasProducer: (() -> CreationExtras)? = null, - noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null -): Lazy = OverridableLazy(viewModels(extrasProducer, factoryProducer)) - -class OverridableLazy(var implementation: Lazy) : Lazy { - - override val value - get() = implementation.value - - override fun isInitialized() = implementation.isInitialized() -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/AppDestination.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/AppDestination.kt new file mode 100644 index 000000000..adeaa35c5 --- /dev/null +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/AppDestination.kt @@ -0,0 +1,27 @@ +package co.nimblehq.sample.compose.ui + +import androidx.navigation.* + +const val KeyId = "id" + +sealed class AppDestination(val route: String = "") { + + open val arguments: List = emptyList() + + open var destination: String = route + + object Up : AppDestination() + + object Home : AppDestination("home") + + object Second : AppDestination("second/{$KeyId}") { + + override val arguments = listOf( + navArgument(KeyId) { type = NavType.StringType } + ) + + fun buildDestination(id: String) = apply { + destination = "second/$id" + } + } +} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/AppNavigation.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/AppNavigation.kt new file mode 100644 index 000000000..2a7af52d5 --- /dev/null +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/AppNavigation.kt @@ -0,0 +1,51 @@ +package co.nimblehq.sample.compose.ui + +import androidx.compose.runtime.Composable +import androidx.navigation.* +import androidx.navigation.compose.* +import co.nimblehq.sample.compose.ui.screens.home.HomeComposeScreen +import co.nimblehq.sample.compose.ui.screens.second.SecondScreen + +@Composable +fun AppNavigation( + navController: NavHostController = rememberNavController(), + startDestination: String = AppDestination.Home.destination +) { + NavHost( + navController = navController, + startDestination = startDestination + ) { + composable(AppDestination.Home) { + HomeComposeScreen( + navigator = { destination -> navController.navigate(destination) } + ) + } + + composable(AppDestination.Second) { backStackEntry -> + SecondScreen( + navigator = { destination -> navController.navigate(destination) }, + id = backStackEntry.arguments?.getString(KeyId).orEmpty() + ) + } + } +} + +private fun NavGraphBuilder.composable( + destination: AppDestination, + deepLinks: List = emptyList(), + content: @Composable (NavBackStackEntry) -> Unit +) { + composable( + route = destination.route, + arguments = destination.arguments, + deepLinks = deepLinks, + content = content + ) +} + +private fun NavHostController.navigate(appDestination: AppDestination) { + when (appDestination) { + is AppDestination.Up -> navigateUp() + else -> navigate(route = appDestination.destination) + } +} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseActivity.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseActivity.kt deleted file mode 100644 index 354ba31fc..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseActivity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package co.nimblehq.sample.compose.ui.base - -import android.os.Bundle -import android.view.LayoutInflater -import androidx.appcompat.app.AppCompatActivity -import androidx.viewbinding.ViewBinding - -abstract class BaseActivity : AppCompatActivity() { - - protected abstract val viewModel: BaseViewModel - - protected abstract val bindingInflater: (LayoutInflater) -> VB - - private var _binding: ViewBinding? = null - - @Suppress("UNCHECKED_CAST") - protected val binding: VB - get() = _binding as VB - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(bindingInflater.invoke(layoutInflater).apply { - _binding = this - }.root) - } - - override fun onDestroy() { - super.onDestroy() - _binding = null - } -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseComposeFragment.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseComposeFragment.kt deleted file mode 100644 index 5d3ae71c8..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseComposeFragment.kt +++ /dev/null @@ -1,65 +0,0 @@ -package co.nimblehq.sample.compose.ui.base - -import android.os.Bundle -import android.view.* -import androidx.annotation.CallSuper -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.lifecycle.* -import co.nimblehq.sample.compose.ui.common.Toaster -import co.nimblehq.sample.compose.ui.theme.ComposeSampleTheme -import co.nimblehq.sample.compose.ui.userReadableMessage -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import javax.inject.Inject - -abstract class BaseComposeFragment : Fragment(), BaseComposeFragmentCallbacks { - - @Inject - lateinit var toaster: Toaster - - protected abstract val composeScreen: @Composable () -> Unit - - @CallSuper - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - (this as? BaseComposeFragmentCallbacks)?.let { initViewModel() } - } - - override fun initViewModel() {} - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return ComposeView(requireContext()).apply { - setContent { ComposeSampleTheme { composeScreen.invoke() } } - } - } - - @CallSuper - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - (this as? BaseComposeFragmentCallbacks)?.let { - bindViewModel() - } - } - - open fun displayError(error: Throwable) { - val message = error.userReadableMessage(requireContext()) - toaster.display(message) - } - - protected inline infix fun Flow.bindTo(crossinline action: (T) -> Unit) { - with(viewLifecycleOwner) { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - collect { action(it) } - } - } - } - } -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseComposeFragmentCallbacks.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseComposeFragmentCallbacks.kt deleted file mode 100644 index 474b60144..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseComposeFragmentCallbacks.kt +++ /dev/null @@ -1,36 +0,0 @@ -package co.nimblehq.sample.compose.ui.base - -/** - * An interface provide abstract commitments for the implemented class - * from [BaseComposeFragment], with the [BaseViewModel] - * - * These methods are set to go well with the Lifecycle of the [BaseComposeFragment], - * so developers don't have to worry about the basic setups, - * which could produce conflicts with the fragment's lifecycle - * - * See more detail in each function. - */ -interface BaseComposeFragmentCallbacks { - - /** - * The initial callback where you want to place your initialize functions that trigger - * the setup block for the ViewModel. - * - * This method usually get called only ONCE during the time the Fragment is created. - * Ideally, you would want to place the network calls, api requests in here. - * - * This is called right after [BaseComposeFragment.onCreate] so we should NOT implement or place - * view events functions here. - */ - fun initViewModel() - - /** - * The initial callback where you want to place your view events functions. - * - * This method usually get called multiple times, whenever the Fragment view is being created/re-created. - * Ideally, you would want to setup the data binding from ViewModel to View here. - * - * This is called right after [BaseComposeFragment.onViewCreated] - */ - fun bindViewModel() -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseNavigator.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseNavigator.kt deleted file mode 100644 index 56f616f0d..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseNavigator.kt +++ /dev/null @@ -1,86 +0,0 @@ -package co.nimblehq.sample.compose.ui.base - -import android.os.Bundle -import android.os.Parcelable -import androidx.annotation.IdRes -import androidx.fragment.app.Fragment -import androidx.navigation.NavController -import androidx.navigation.fragment.findNavController -import co.nimblehq.common.extensions.getResourceName -import timber.log.Timber - -interface BaseNavigator { - - val navHostFragmentId: Int - - fun findNavController(): NavController? - - fun navigate(event: NavigationEvent) - - fun navigateUp() -} - -abstract class BaseNavigatorImpl( - protected val fragment: Fragment -) : BaseNavigator { - - private var navController: NavController? = null - - override fun findNavController(): NavController? { - return navController ?: try { - fragment.findNavController().apply { - navController = this - } - } catch (e: IllegalStateException) { - // Log Crashlytics as non-fatal for monitoring - Timber.e(e) - null - } - } - - override fun navigateUp() { - findNavController()?.navigateUp() - } - - protected fun popBackTo(@IdRes destinationId: Int, inclusive: Boolean = false) { - findNavController()?.popBackStack(destinationId, inclusive) - } - - protected fun unsupportedNavigation() { - val navController = findNavController() - val currentGraph = fragment.requireActivity().getResourceName(navController?.graph?.id) - val currentDestination = - fragment.requireActivity().getResourceName(navController?.currentDestination?.id) - handleError( - NavigationException.UnsupportedNavigationException( - currentGraph, - currentDestination - ) - ) - } - - protected fun NavController.navigateToDestinationByDeepLink( - destinationId: Int, - bundle: Parcelable? = null - ) { - createDeepLink() - .setDestination(destinationId).apply { - bundle?.let { - setArguments(Bundle().apply { - putParcelable("bundle", bundle) - }) - } - } - .createPendingIntent() - .send() - } - - private fun handleError(error: Throwable) { - if (fragment is BaseComposeFragment) { - Timber.e(error) - fragment.displayError(error) - } else { - throw error - } - } -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt index e1f17a38b..1950f9a32 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt @@ -3,6 +3,7 @@ package co.nimblehq.sample.compose.ui.base import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.nimblehq.sample.compose.lib.IsLoading +import co.nimblehq.sample.compose.ui.AppDestination import co.nimblehq.sample.compose.util.DispatchersProvider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.* @@ -21,8 +22,8 @@ abstract class BaseViewModel(private val dispatchersProvider: DispatchersProvide val error: SharedFlow get() = _error - protected val _navigator = MutableSharedFlow() - val navigator: SharedFlow + protected val _navigator = MutableSharedFlow() + val navigator: SharedFlow get() = _navigator /** diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/NavigationEvent.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/NavigationEvent.kt deleted file mode 100644 index 0022ca004..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/NavigationEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package co.nimblehq.sample.compose.ui.base - -import co.nimblehq.sample.compose.model.UiModel - -sealed class NavigationEvent { - data class Second(val uiModel: UiModel) : NavigationEvent() -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/NavigationException.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/NavigationException.kt deleted file mode 100644 index d5b5e4ca8..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/NavigationException.kt +++ /dev/null @@ -1,11 +0,0 @@ -package co.nimblehq.sample.compose.ui.base - -sealed class NavigationException( - cause: Throwable? -) : Throwable(cause) { - - class UnsupportedNavigationException( - currentGraph: String?, - currentDestination: String? - ) : NavigationException(RuntimeException("Unsupported navigation on $currentGraph at $currentDestination")) -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/AppBar.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/AppBar.kt index aaa39f481..f139a631f 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/AppBar.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/AppBar.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import co.nimblehq.sample.compose.R -import co.nimblehq.sample.compose.ui.theme.ComposeSampleTheme +import co.nimblehq.sample.compose.ui.theme.ComposeTheme @Composable fun AppBar(@StringRes title: Int) { @@ -19,5 +19,5 @@ fun AppBar(@StringRes title: Int) { @Preview(showBackground = true) @Composable private fun AppBarPreview() { - ComposeSampleTheme { AppBar(R.string.home_title_bar) } + ComposeTheme { AppBar(R.string.home_title_bar) } } diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainActivity.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainActivity.kt index f9ff11ba9..85e28ba2a 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainActivity.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainActivity.kt @@ -1,16 +1,21 @@ package co.nimblehq.sample.compose.ui.screens -import android.view.LayoutInflater -import androidx.activity.viewModels -import co.nimblehq.sample.compose.databinding.ActivityMainBinding -import co.nimblehq.sample.compose.ui.base.BaseActivity +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import co.nimblehq.sample.compose.ui.AppNavigation +import co.nimblehq.sample.compose.ui.theme.ComposeTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MainActivity : BaseActivity() { +class MainActivity : ComponentActivity() { - override val bindingInflater: (LayoutInflater) -> ActivityMainBinding - get() = { inflater -> ActivityMainBinding.inflate(inflater) } - - override val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + ComposeTheme { + AppNavigation() + } + } + } } diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainNavigator.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainNavigator.kt deleted file mode 100644 index e1a9b06e9..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainNavigator.kt +++ /dev/null @@ -1,33 +0,0 @@ -package co.nimblehq.sample.compose.ui.screens - -import androidx.fragment.app.Fragment -import co.nimblehq.sample.compose.R -import co.nimblehq.sample.compose.model.UiModel -import co.nimblehq.sample.compose.ui.base.* -import co.nimblehq.sample.compose.ui.screens.home.HomeComposeFragmentDirections.Companion.actionHomeComposeFragmentToSecondFragment -import javax.inject.Inject - -interface MainNavigator : BaseNavigator - -class MainNavigatorImpl @Inject constructor( - fragment: Fragment -) : BaseNavigatorImpl(fragment), MainNavigator { - - override val navHostFragmentId = R.id.navHostFragment - - override fun navigate(event: NavigationEvent) { - when (event) { - is NavigationEvent.Second -> navigateToSecond(event.uiModel) - } - } - - private fun navigateToSecond(uiModel: UiModel) { - val navController = findNavController() - when (navController?.currentDestination?.id) { - R.id.homeComposeFragment -> navController.navigate( - actionHomeComposeFragmentToSecondFragment(uiModel) - ) - else -> unsupportedNavigation() - } - } -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeFragment.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeFragment.kt deleted file mode 100644 index c37636ed7..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeFragment.kt +++ /dev/null @@ -1,63 +0,0 @@ -package co.nimblehq.sample.compose.ui.screens.home - -import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.Manifest.permission.CAMERA -import android.os.Bundle -import android.view.View -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.lifecycle.lifecycleScope -import co.nimblehq.sample.compose.extension.provideViewModels -import co.nimblehq.sample.compose.ui.base.BaseComposeFragment -import co.nimblehq.sample.compose.ui.screens.MainNavigator -import com.markodevcic.peko.PermissionResult -import com.markodevcic.peko.PermissionResult.Denied -import com.markodevcic.peko.PermissionResult.Granted -import com.markodevcic.peko.requestPermissionsAsync -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@AndroidEntryPoint -class HomeComposeFragment : BaseComposeFragment() { - - @Inject - lateinit var navigator: MainNavigator - - private val viewModel: HomeComposeViewModel by provideViewModels() - - override val composeScreen: @Composable () -> Unit - get() = { - HomeComposeScreen( - uiModels = viewModel.uiModels.collectAsState().value, - showLoading = viewModel.showLoading.collectAsState().value, - onItemClick = { uiModel -> viewModel.navigateToSecond(uiModel) } - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - requestPermissions(ACCESS_FINE_LOCATION, CAMERA) - } - - override fun bindViewModel() { - viewModel.error bindTo ::displayError - viewModel.navigator bindTo navigator::navigate - } - - private fun requestPermissions(vararg permissions: String) { - lifecycleScope.launch { - val result = requestPermissionsAsync(*permissions) - handlePermissionResult(result) - } - } - - private fun handlePermissionResult(permissionResult: PermissionResult) { - when (permissionResult) { - is Granted -> Timber.d("${permissionResult.grantedPermissions} granted") - is Denied.NeedsRationale -> Timber.d("${permissionResult.deniedPermissions} needs rationale") - else -> Timber.d("Request cancelled, missing permissions in manifest or denied permanently") - } - } -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeScreen.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeScreen.kt index d1390073b..39b7a29b7 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeScreen.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeScreen.kt @@ -1,29 +1,64 @@ package co.nimblehq.sample.compose.ui.screens.home -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import android.widget.Toast +import androidx.compose.foundation.layout.* import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Scaffold -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel import co.nimblehq.sample.compose.R +import co.nimblehq.sample.compose.lib.IsLoading import co.nimblehq.sample.compose.model.UiModel +import co.nimblehq.sample.compose.ui.AppDestination import co.nimblehq.sample.compose.ui.screens.AppBar -import co.nimblehq.sample.compose.ui.theme.ComposeSampleTheme +import co.nimblehq.sample.compose.ui.theme.ComposeTheme +import co.nimblehq.sample.compose.ui.userReadableMessage // TODO: Rename to 'HomeScreen' @Composable fun HomeComposeScreen( + viewModel: HomeComposeViewModel = hiltViewModel(), + navigator: (destination: AppDestination) -> Unit +) { + val context = LocalContext.current + LaunchedEffect(viewModel.error) { + viewModel.error.collect { error -> + val message = error.userReadableMessage(context) + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } + LaunchedEffect(viewModel.navigator) { + viewModel.navigator.collect { destination -> navigator(destination) } + } + + val showLoading: IsLoading by viewModel.showLoading.collectAsState() + val uiModels: List by viewModel.uiModels.collectAsState() + + HomeScreenContent( + uiModels = uiModels, + showLoading = showLoading, + onItemClick = viewModel::navigateToSecond + ) +} + +@Composable +private fun HomeScreenContent( uiModels: List, - showLoading: Boolean, + showLoading: IsLoading, onItemClick: (UiModel) -> Unit ) { Scaffold(topBar = { AppBar(R.string.home_title_bar) - }) { - Box(modifier = Modifier.fillMaxSize()) { + }) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { if (showLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } else { @@ -39,8 +74,8 @@ fun HomeComposeScreen( @Preview(showBackground = true) @Composable private fun HomeComposeScreenPreview() { - ComposeSampleTheme { - HomeComposeScreen( + ComposeTheme { + HomeScreenContent( uiModels = listOf(UiModel("1"), UiModel("2"), UiModel("3")), showLoading = false, onItemClick = {} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeViewModel.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeViewModel.kt index 7138725dc..ec0a8ffd7 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeViewModel.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeComposeViewModel.kt @@ -3,8 +3,8 @@ package co.nimblehq.sample.compose.ui.screens.home import co.nimblehq.sample.compose.domain.usecase.UseCase import co.nimblehq.sample.compose.model.UiModel import co.nimblehq.sample.compose.model.toUiModels +import co.nimblehq.sample.compose.ui.AppDestination import co.nimblehq.sample.compose.ui.base.BaseViewModel -import co.nimblehq.sample.compose.ui.base.NavigationEvent import co.nimblehq.sample.compose.util.DispatchersProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* @@ -37,6 +37,6 @@ class HomeComposeViewModel @Inject constructor( } fun navigateToSecond(uiModel: UiModel) { - execute { _navigator.emit(NavigationEvent.Second(uiModel)) } + execute { _navigator.emit(AppDestination.Second.buildDestination(uiModel.id)) } } } diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/Item.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/Item.kt index 9456a46c2..753ca5019 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/Item.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/Item.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import co.nimblehq.sample.compose.model.UiModel -import co.nimblehq.sample.compose.ui.theme.ComposeSampleTheme +import co.nimblehq.sample.compose.ui.theme.ComposeTheme import co.nimblehq.sample.compose.ui.theme.SpacingNormal @Composable @@ -30,7 +30,7 @@ fun Item( @Preview(showBackground = true) @Composable private fun ItemPreview() { - ComposeSampleTheme { + ComposeTheme { Item( uiModel = UiModel("1"), onClick = {} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/ItemList.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/ItemList.kt index 4a0945696..cfaa079d1 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/ItemList.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/ItemList.kt @@ -6,7 +6,7 @@ import androidx.compose.material.Divider import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import co.nimblehq.sample.compose.model.UiModel -import co.nimblehq.sample.compose.ui.theme.ComposeSampleTheme +import co.nimblehq.sample.compose.ui.theme.ComposeTheme @Composable fun ItemList( @@ -27,7 +27,7 @@ fun ItemList( @Preview(showBackground = true) @Composable private fun ItemListPreview() { - ComposeSampleTheme { + ComposeTheme { ItemList( uiModels = listOf(UiModel("1"), UiModel("2"), UiModel("3")), onItemClick = {} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondFragment.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondFragment.kt deleted file mode 100644 index e02dff3e2..000000000 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondFragment.kt +++ /dev/null @@ -1,19 +0,0 @@ -package co.nimblehq.sample.compose.ui.screens.second - -import androidx.compose.runtime.Composable -import androidx.navigation.fragment.navArgs -import co.nimblehq.sample.compose.ui.base.BaseComposeFragment -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class SecondFragment : BaseComposeFragment() { - - private val args by navArgs() - - override val composeScreen: @Composable () -> Unit - get() = { SecondScreen(args.uiModel) } - - override fun bindViewModel() { - // Do nothing - } -} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondScreen.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondScreen.kt index bc223fcd2..582467c37 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondScreen.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondScreen.kt @@ -1,7 +1,6 @@ package co.nimblehq.sample.compose.ui.screens.second -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.* import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -9,19 +8,33 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel import co.nimblehq.sample.compose.R -import co.nimblehq.sample.compose.model.UiModel +import co.nimblehq.sample.compose.ui.AppDestination import co.nimblehq.sample.compose.ui.screens.AppBar -import co.nimblehq.sample.compose.ui.theme.ComposeSampleTheme +import co.nimblehq.sample.compose.ui.theme.ComposeTheme @Composable -fun SecondScreen(uiModel: UiModel) { +fun SecondScreen( + viewModel: SecondViewModel = hiltViewModel(), + navigator: (destination: AppDestination) -> Unit, + id: String, +) { + SecondScreenContent(id) +} + +@Composable +private fun SecondScreenContent(id: String) { Scaffold(topBar = { AppBar(R.string.second_title_bar) - }) { - Box(modifier = Modifier.fillMaxSize()) { + }) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { Text( - text = stringResource(R.string.second_id_title, uiModel.id), + text = stringResource(R.string.second_id_title, id), modifier = Modifier.align(Alignment.Center) ) } @@ -31,7 +44,7 @@ fun SecondScreen(uiModel: UiModel) { @Preview(showBackground = true) @Composable private fun SecondScreenPreview() { - ComposeSampleTheme { - SecondScreen(UiModel("1")) + ComposeTheme { + SecondScreenContent("1") } } diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainViewModel.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondViewModel.kt similarity index 74% rename from sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainViewModel.kt rename to sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondViewModel.kt index d03159024..91d82320e 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainViewModel.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/second/SecondViewModel.kt @@ -1,4 +1,4 @@ -package co.nimblehq.sample.compose.ui.screens +package co.nimblehq.sample.compose.ui.screens.second import co.nimblehq.sample.compose.ui.base.BaseViewModel import co.nimblehq.sample.compose.util.DispatchersProvider @@ -6,6 +6,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor( +class SecondViewModel @Inject constructor( dispatchers: DispatchersProvider ) : BaseViewModel(dispatchers) diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/theme/Theme.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/theme/Theme.kt index 9acb4487a..5339c6fbb 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/theme/Theme.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/theme/Theme.kt @@ -5,7 +5,7 @@ import androidx.compose.material.lightColors import androidx.compose.runtime.Composable @Composable -fun ComposeSampleTheme( +fun ComposeTheme( content: @Composable () -> Unit ) { MaterialTheme( diff --git a/sample-compose/app/src/main/res/layout/activity_main.xml b/sample-compose/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index b64385852..000000000 --- a/sample-compose/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - diff --git a/sample-compose/app/src/main/res/navigation/nav_graph_main.xml b/sample-compose/app/src/main/res/navigation/nav_graph_main.xml deleted file mode 100644 index a4ac97962..000000000 --- a/sample-compose/app/src/main/res/navigation/nav_graph_main.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - diff --git a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/TestModules.kt b/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/TestModules.kt deleted file mode 100644 index f821e7e5e..000000000 --- a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/TestModules.kt +++ /dev/null @@ -1,21 +0,0 @@ -package co.nimblehq.sample.compose.test - -import co.nimblehq.sample.compose.di.modules.NavigatorModule -import co.nimblehq.sample.compose.ui.screens.MainNavigator -import dagger.Module -import dagger.Provides -import dagger.hilt.android.components.FragmentComponent -import dagger.hilt.testing.TestInstallIn -import io.mockk.mockk - -@Module -@TestInstallIn( - components = [FragmentComponent::class], - replaces = [NavigatorModule::class] -) -object TestNavigatorModule { - val mockMainNavigator = mockk() - - @Provides - fun provideMainNavigator() = mockMainNavigator -} diff --git a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/ViewModelExt.kt b/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/ViewModelExt.kt deleted file mode 100644 index adea24a64..000000000 --- a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/ViewModelExt.kt +++ /dev/null @@ -1,21 +0,0 @@ -package co.nimblehq.sample.compose.test - -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModel -import co.nimblehq.sample.compose.extension.OverridableLazy -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.isAccessible - -fun T.replace( - viewModelDelegate: KProperty1, - viewModel: VM -) { - viewModelDelegate.isAccessible = true - (viewModelDelegate.getDelegate(this) as OverridableLazy).implementation = lazy { viewModel } -} - -inline fun T.getPrivateProperty(name: String): KProperty1 = - T::class - .memberProperties - .first { it.name == name } as KProperty1 diff --git a/sample-compose/app/src/test/resources/robolectric.properties b/sample-compose/app/src/test/resources/robolectric.properties deleted file mode 100644 index 1a7a2e6ec..000000000 --- a/sample-compose/app/src/test/resources/robolectric.properties +++ /dev/null @@ -1,2 +0,0 @@ -sdk=28 -application=dagger.hilt.android.testing.HiltTestApplication diff --git a/sample-compose/build.gradle.kts b/sample-compose/build.gradle.kts index 1ad6c1e53..97eea8d7a 100644 --- a/sample-compose/build.gradle.kts +++ b/sample-compose/build.gradle.kts @@ -6,7 +6,6 @@ buildscript { } dependencies { - classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.ANDROIDX_NAVIGATION_VERSION}") classpath("com.android.tools.build:gradle:${Versions.BUILD_GRADLE_VERSION}") classpath("com.google.dagger:hilt-android-gradle-plugin:${Versions.HILT_VERSION}") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN_VERSION}") diff --git a/sample-compose/buildSrc/src/main/java/Configurations.kt b/sample-compose/buildSrc/src/main/java/Configurations.kt index 51b3dee4b..175d0c1ba 100644 --- a/sample-compose/buildSrc/src/main/java/Configurations.kt +++ b/sample-compose/buildSrc/src/main/java/Configurations.kt @@ -1,7 +1,7 @@ object Flavor { const val PRODUCTION = "production" const val STAGING = "staging" - const val DIMENSIONS = "stage" + const val DIMENSION_VERSION = "version" } object BuildType { diff --git a/sample-compose/buildSrc/src/main/java/Versions.kt b/sample-compose/buildSrc/src/main/java/Versions.kt index d87323f42..76cebebc0 100644 --- a/sample-compose/buildSrc/src/main/java/Versions.kt +++ b/sample-compose/buildSrc/src/main/java/Versions.kt @@ -11,20 +11,16 @@ object Versions { // Dependencies (Alphabet sorted) const val ANDROID_COMMON_KTX_VERSION = "0.1.1" const val ANDROID_CRYPTO_VERSION = "1.0.0" - const val ANDROIDX_ACTIVITY_KTX_VERSION = "1.6.0" const val ANDROIDX_CORE_KTX_VERSION = "1.9.0" - const val ANDROIDX_TEST_CORE_VERSION = "1.4.0" - const val ANDROIDX_FRAGMENT_VERSION = "1.4.0" - const val ANDROIDX_FRAGMENT_KTX_VERSION = "1.5.3" const val ANDROIDX_LIFECYCLE_VERSION = "2.5.1" - const val ANDROIDX_NAVIGATION_VERSION = "2.3.4" - const val ANDROIDX_SUPPORT_VERSION = "1.5.1" const val CHUCKER_VERSION = "3.5.2" const val COMPOSE_VERSION = "1.2.1" const val COMPOSE_COMPILER_VERSION = "1.3.2" + const val COMPOSE_NAVIGATION_VERSION = "2.5.1" const val HILT_VERSION = "2.44" + const val HILT_NAVIGATION_COMPOSE_VERSION = "1.0.0" const val JAVAX_INJECT_VERSION = "1" @@ -47,10 +43,10 @@ object Versions { const val DETEKT_VERSION = "1.21.0" // Testing libraries + const val TEST_ANDROIDX_CORE_VERSION = "1.4.0" const val TEST_JUNIT_ANDROIDX_EXT_VERSION = "1.1.2" const val TEST_JUNIT_VERSION = "4.13.2" const val TEST_KOTEST_VERSION = "4.6.3" const val TEST_MOCKK_VERSION = "1.10.6" - const val TEST_ROBOLECTRIC_VERSION = "4.8.2" const val TEST_RUNNER_VERSION = "1.4.0" } diff --git a/sample-compose/detekt-config.yml b/sample-compose/detekt-config.yml index 2ae416fa9..a6d1e241c 100644 --- a/sample-compose/detekt-config.yml +++ b/sample-compose/detekt-config.yml @@ -67,7 +67,7 @@ complexity: threshold: 150 LongMethod: active: true - threshold: 20 + threshold: 60 LongParameterList: active: true functionThreshold: 5 diff --git a/template-compose/detekt-config.yml b/template-compose/detekt-config.yml index 5c1ce0739..a6d1e241c 100644 --- a/template-compose/detekt-config.yml +++ b/template-compose/detekt-config.yml @@ -67,7 +67,7 @@ complexity: threshold: 150 LongMethod: active: true - threshold: 20 + threshold: 60 LongParameterList: active: true functionThreshold: 5 @@ -127,7 +127,7 @@ exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: false - methodNames: [ 'toString', 'hashCode', 'equals' , 'finalize' ] + methodNames: [ 'toString', 'hashCode', 'equals', 'finalize' ] InstanceOfCheckForException: active: false NotImplementedDeclaration: