├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── navigation
│ │ │ │ └── app_main_navigation.xml
│ │ │ ├── values-night
│ │ │ │ └── themes.xml
│ │ │ ├── layout
│ │ │ │ └── activity_main.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── moove
│ │ │ │ └── app
│ │ │ │ ├── feature
│ │ │ │ ├── home
│ │ │ │ │ ├── HomeState.kt
│ │ │ │ │ ├── HomeNavigator.kt
│ │ │ │ │ ├── HomeViewModel.kt
│ │ │ │ │ ├── HomeFragment.kt
│ │ │ │ │ ├── HomeRoute.kt
│ │ │ │ │ └── HomeScreen.kt
│ │ │ │ └── deeplink
│ │ │ │ │ ├── data
│ │ │ │ │ ├── DynamicLinkDataRepository.kt
│ │ │ │ │ ├── DeeplinkDataRepository.kt
│ │ │ │ │ ├── remote
│ │ │ │ │ │ └── FirebaseDynamicLinkDataSource.kt
│ │ │ │ │ └── local
│ │ │ │ │ │ └── AppDeepLinkLocalDataSource.kt
│ │ │ │ │ ├── domain
│ │ │ │ │ └── AppDeepLink.kt
│ │ │ │ │ └── presentation
│ │ │ │ │ └── DeepLinkAppNavigator.kt
│ │ │ │ ├── di
│ │ │ │ ├── ExceptionsModule.kt
│ │ │ │ ├── CoroutineModule.kt
│ │ │ │ ├── NetModule.kt
│ │ │ │ ├── MainModule.kt
│ │ │ │ └── DeepLinkModule.kt
│ │ │ │ ├── main
│ │ │ │ ├── MainNavigator.kt
│ │ │ │ ├── MainActivityState.kt
│ │ │ │ ├── MainActivityViewModel.kt
│ │ │ │ └── MainActivity.kt
│ │ │ │ ├── shared
│ │ │ │ └── exception_handler
│ │ │ │ │ └── main
│ │ │ │ │ └── MainExceptionHandler.kt
│ │ │ │ ├── MooveApp.kt
│ │ │ │ └── navigation
│ │ │ │ └── AppNavigator.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── moove
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── moove
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── core
├── .gitignore
├── consumer-rules.pro
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── moove
│ │ └── core
│ │ ├── okhttp
│ │ ├── NoConnectionException.kt
│ │ └── ConnectionErrorInterceptor.kt
│ │ ├── exception
│ │ ├── ExceptionTransformer.kt
│ │ └── ExceptionHandler.kt
│ │ ├── kotlin
│ │ ├── text
│ │ │ └── StringExtensions.kt
│ │ ├── ResultExtensions.kt
│ │ └── coroutines
│ │ │ ├── CoroutineScopeExtensions.kt
│ │ │ └── AppCoroutineScope.kt
│ │ └── compose
│ │ └── AnnotatedString.kt
├── build.gradle
└── proguard-rules.pro
├── shared
├── .gitignore
├── consumer-rules.pro
├── src
│ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── moove
│ │ │ └── shared
│ │ │ ├── feature
│ │ │ └── deeplink
│ │ │ │ ├── domain
│ │ │ │ ├── DeepLink.kt
│ │ │ │ ├── DeeplinkRepository.kt
│ │ │ │ ├── DynamicLinkRepository.kt
│ │ │ │ ├── exceptions
│ │ │ │ │ └── DynamicLinkParseException.kt
│ │ │ │ ├── GetDynamicLinkUseCase.kt
│ │ │ │ └── GetDeeplinkUseCase.kt
│ │ │ │ └── presentation
│ │ │ │ └── DeepLinkNavigator.kt
│ │ │ ├── navigation
│ │ │ ├── ScreenNavigator.kt
│ │ │ ├── GlobalAppNavigator.kt
│ │ │ ├── TicketsNavigator.kt
│ │ │ └── NavControllerExtension.kt
│ │ │ ├── presentation
│ │ │ ├── viewmodel
│ │ │ │ ├── ViewModelExtensions.kt
│ │ │ │ └── OrbitExtensions.kt
│ │ │ ├── fragment
│ │ │ │ └── delegate
│ │ │ │ │ ├── FragmentViewLifecycleAwareDelegateExtensions.kt
│ │ │ │ │ ├── FragmentViewLifecycleAwareDelegate.kt
│ │ │ │ │ └── FragmentLifecycleAwareExtensions.kt
│ │ │ └── compose
│ │ │ │ ├── platform
│ │ │ │ ├── ComposeViewExtensions.kt
│ │ │ │ └── FragmentExtensions.kt
│ │ │ │ └── component
│ │ │ │ ├── ScreenContent.kt
│ │ │ │ ├── BlockingBox.kt
│ │ │ │ └── GenericError.kt
│ │ │ └── ModelMother.kt
│ │ ├── res
│ │ ├── values
│ │ │ ├── navigation_ids.xml
│ │ │ └── strings.xml
│ │ ├── anim
│ │ │ ├── no_animation.xml
│ │ │ ├── slide_in_up.xml
│ │ │ └── slide_out_bottom.xml
│ │ └── layout
│ │ │ └── compose_fragment.xml
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── tickets
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── moove
│ │ │ │ └── tickets
│ │ │ │ ├── domain
│ │ │ │ ├── model
│ │ │ │ │ ├── Fare.kt
│ │ │ │ │ └── Ryder.kt
│ │ │ │ ├── use_cases
│ │ │ │ │ ├── GetRydersUseCase.kt
│ │ │ │ │ ├── GetFaresByIdUseCase.kt
│ │ │ │ │ └── BuyTicketUseCase.kt
│ │ │ │ └── TicketsRepository.kt
│ │ │ │ ├── presentation
│ │ │ │ ├── confirmation
│ │ │ │ │ ├── ConfirmationNavigator.kt
│ │ │ │ │ ├── ConfirmationState.kt
│ │ │ │ │ ├── ConfirmationFragment.kt
│ │ │ │ │ ├── ConfirmationRoute.kt
│ │ │ │ │ ├── component
│ │ │ │ │ │ └── ConfirmationItem.kt
│ │ │ │ │ ├── ConfirmationViewModel.kt
│ │ │ │ │ └── ConfirmationScreen.kt
│ │ │ │ ├── fare
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── FareModelFake.kt
│ │ │ │ │ │ └── FareModel.kt
│ │ │ │ │ ├── FareListState.kt
│ │ │ │ │ ├── FareListNavigator.kt
│ │ │ │ │ ├── component
│ │ │ │ │ │ ├── FareList.kt
│ │ │ │ │ │ └── FareItem.kt
│ │ │ │ │ ├── FareListFragment.kt
│ │ │ │ │ ├── FareListRoute.kt
│ │ │ │ │ ├── FareListViewModel.kt
│ │ │ │ │ └── FareListScreen.kt
│ │ │ │ └── list
│ │ │ │ │ ├── RyderListNavigator.kt
│ │ │ │ │ ├── RyderListState.kt
│ │ │ │ │ ├── model
│ │ │ │ │ ├── RyderModelFake.kt
│ │ │ │ │ └── RyderModel.kt
│ │ │ │ │ ├── component
│ │ │ │ │ ├── RyderList.kt
│ │ │ │ │ └── RyderItem.kt
│ │ │ │ │ ├── RyderListFragment.kt
│ │ │ │ │ ├── RyderListRoute.kt
│ │ │ │ │ ├── RyderListViewModel.kt
│ │ │ │ │ └── RyderListScreen.kt
│ │ │ │ ├── data
│ │ │ │ ├── local
│ │ │ │ │ ├── dto
│ │ │ │ │ │ ├── TicketsDataDTO.kt
│ │ │ │ │ │ ├── FareDTO.kt
│ │ │ │ │ │ └── RyderDTO.kt
│ │ │ │ │ └── TicketsLocalDataSource.kt
│ │ │ │ └── TicketsDataRepository.kt
│ │ │ │ └── di
│ │ │ │ └── TicketsModule.kt
│ │ └── res
│ │ │ └── navigation
│ │ │ └── tickets_navigation.xml
│ └── test
│ │ └── java
│ │ └── com
│ │ └── moove
│ │ └── tickets
│ │ ├── data
│ │ ├── dto
│ │ │ └── RyderDTOMother.kt
│ │ └── TicketsDataRepositoryTest.kt
│ │ ├── domain
│ │ ├── model
│ │ │ └── RyderMother.kt
│ │ └── use_cases
│ │ │ ├── BuyTicketUseCaseTest.kt
│ │ │ ├── GetRydersUseCaseTest.kt
│ │ │ └── GetFaresByIdUseCaseTest.kt
│ │ └── presentation
│ │ ├── list
│ │ └── RyderListViewModelTest.kt
│ │ ├── fare
│ │ └── FareListViewModelTest.kt
│ │ └── confirmation
│ │ └── ConfirmationViewModelTest.kt
├── build.gradle
└── proguard-rules.pro
├── design-system
├── .gitignore
├── consumer-rules.pro
├── src
│ └── main
│ │ ├── res
│ │ └── font
│ │ │ ├── quicksand_bold.ttf
│ │ │ ├── source_sans_pro.ttf
│ │ │ └── source_sans_pro_semibold.ttf
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── moove
│ │ └── design_system
│ │ └── compose
│ │ ├── TopAppBar.kt
│ │ ├── moove
│ │ ├── NeutralColors.kt
│ │ ├── Shapes.kt
│ │ ├── Button.kt
│ │ ├── Theme.kt
│ │ ├── Colors.kt
│ │ └── Typography.kt
│ │ ├── Shapes.kt
│ │ ├── Dimensions.kt
│ │ ├── NeutralColors.kt
│ │ ├── Typography.kt
│ │ ├── Colors.kt
│ │ ├── Theme.kt
│ │ ├── Scaffold.kt
│ │ └── ProgressIndicator.kt
├── build.gradle
└── proguard-rules.pro
├── android-docs
├── Dockerfile
├── mkdocs.yml
└── docs
│ └── index.md
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── compose-android-config.gradle
└── base-android-config.gradle
├── .gitignore
├── .github
├── actions
│ └── job-set-up
│ │ └── action.yml
└── workflows
│ ├── generate_docs.sh
│ ├── code_review.yml
│ └── docs.yml
├── settings.gradle
├── gradle.properties
├── README.md
└── gradlew.bat
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/shared/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/shared/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tickets/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/tickets/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/design-system/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/design-system/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/android-docs/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:latest
2 | COPY android-docs/html /usr/share/nginx/html
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Moove
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/feature/deeplink/domain/DeepLink.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.feature.deeplink.domain
2 |
3 | interface DeepLink
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/design-system/src/main/res/font/quicksand_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/design-system/src/main/res/font/quicksand_bold.ttf
--------------------------------------------------------------------------------
/design-system/src/main/res/font/source_sans_pro.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/design-system/src/main/res/font/source_sans_pro.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/shared/src/main/res/values/navigation_ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/navigation/ScreenNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.navigation
2 |
3 | interface ScreenNavigator {
4 | fun goBack()
5 | }
6 |
--------------------------------------------------------------------------------
/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/design-system/src/main/res/font/source_sans_pro_semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MkhytarMkhoian/Moove/HEAD/design-system/src/main/res/font/source_sans_pro_semibold.ttf
--------------------------------------------------------------------------------
/shared/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/tickets/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/design-system/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/domain/model/Fare.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.model
2 |
3 | data class Fare(
4 | val description: String,
5 | val price: Float,
6 | )
7 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/navigation/GlobalAppNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.navigation
2 |
3 | interface GlobalAppNavigator : ScreenNavigator, TicketsNavigator {
4 |
5 | fun goHome()
6 | }
7 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/domain/model/Ryder.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.model
2 |
3 | data class Ryder(
4 | val id: String,
5 | val fares: List,
6 | val subtext: String?,
7 | )
--------------------------------------------------------------------------------
/gradle/compose-android-config.gradle:
--------------------------------------------------------------------------------
1 | android {
2 | buildFeatures {
3 | compose true
4 | }
5 | composeOptions {
6 | kotlinCompilerExtensionVersion libs.versions.composeCompiler.get()
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/feature/deeplink/domain/DeeplinkRepository.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.feature.deeplink.domain
2 |
3 | interface DeeplinkRepository {
4 |
5 | suspend fun getDeepLink(uri: String): DeepLink
6 | }
7 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/feature/deeplink/domain/DynamicLinkRepository.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.feature.deeplink.domain
2 |
3 | interface DynamicLinkRepository {
4 |
5 | suspend fun parseLink(uri: String): String?
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .DS_Store
5 | /build
6 | /captures
7 | .externalNativeBuild
8 | .cxx
9 | local.properties
10 | /android-docs/venv/
11 | /.idea/
12 | /android-docs/docs/api/
13 | /android-docs/html/
14 |
--------------------------------------------------------------------------------
/core/src/main/java/com/moove/core/okhttp/NoConnectionException.kt:
--------------------------------------------------------------------------------
1 | package com.moove.core.okhttp
2 |
3 | import java.io.IOException
4 |
5 | class NoConnectionException(
6 | message: String? = null,
7 | cause: Throwable? = null
8 | ) : IOException(message, cause)
9 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/navigation/TicketsNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.navigation
2 |
3 | interface TicketsNavigator {
4 |
5 | fun goFares(ryderId: String)
6 | fun goToConfirmation(ryderId: String, fareDescription: String, farePrice: Float)
7 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Feb 18 14:32:38 EET 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/feature/deeplink/presentation/DeepLinkNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.feature.deeplink.presentation
2 |
3 | import com.moove.shared.feature.deeplink.domain.DeepLink
4 |
5 | interface DeepLinkNavigator {
6 | fun navigateTo(link: DeepLink)
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/feature/deeplink/domain/exceptions/DynamicLinkParseException.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.feature.deeplink.domain.exceptions
2 |
3 | class DynamicLinkParseException(
4 | message: String? = null,
5 | cause: Throwable? = null,
6 | ) : Exception(message, cause)
7 |
--------------------------------------------------------------------------------
/core/src/main/java/com/moove/core/exception/ExceptionTransformer.kt:
--------------------------------------------------------------------------------
1 | package com.moove.core.exception
2 |
3 | fun interface ExceptionTransformer {
4 | fun transform(input: Throwable): Throwable
5 | }
6 |
7 | fun ExceptionTransformer.decorate(block: (ExceptionTransformer) -> ExceptionTransformer) = block(this)
8 |
--------------------------------------------------------------------------------
/shared/src/main/res/anim/no_animation.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/home/HomeState.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.home
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | class HomeState : Parcelable
8 |
9 | sealed class HomeEffect {
10 | data object GoToRyderList : HomeEffect()
11 | }
12 |
--------------------------------------------------------------------------------
/shared/src/main/res/anim/slide_in_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/core/src/main/java/com/moove/core/kotlin/text/StringExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.core.kotlin.text
2 |
3 | import java.util.regex.Pattern
4 |
5 | fun String.matchesPattern(pattern: String): Boolean {
6 | return Pattern
7 | .compile("($pattern)")
8 | .matcher(this)
9 | .find()
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/main/res/anim/slide_out_bottom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/shared/src/main/res/layout/compose_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/core/src/main/java/com/moove/core/exception/ExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.moove.core.exception
2 |
3 | import kotlinx.coroutines.CoroutineExceptionHandler
4 |
5 | fun interface ExceptionHandler {
6 | fun handle(error: Throwable)
7 | }
8 |
9 | fun ExceptionHandler.asCoroutineExceptionHandler() =
10 | CoroutineExceptionHandler { _, throwable -> this.handle(throwable) }
11 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/TopAppBar.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.ui.text.TextStyle
5 |
6 | @Immutable
7 | data class TopAppBarTypography(
8 | val title: TextStyle = TextStyle.Default,
9 | val titleExpanded: TextStyle = TextStyle.Default,
10 | )
11 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/feature/deeplink/domain/GetDynamicLinkUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.feature.deeplink.domain
2 |
3 | class GetDynamicLinkUseCase(
4 | private val dynamicLinkRepository: DynamicLinkRepository,
5 | ) {
6 |
7 | suspend operator fun invoke(uri: String): String? {
8 | return dynamicLinkRepository.parseLink(uri)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/di/ExceptionsModule.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.di
2 |
3 | import com.moove.app.shared.exception_handler.main.MainExceptionHandler
4 | import com.moove.core.exception.ExceptionHandler
5 | import org.koin.dsl.module
6 |
7 | val exceptionsModule = module {
8 | single { listOf() }
9 | single { MainExceptionHandler(get()) }
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/presentation/viewmodel/ViewModelExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.presentation.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.moove.core.kotlin.coroutines.executeUseCase
6 |
7 | suspend inline fun ViewModel.executeUseCase(block: () -> R): Result =
8 | viewModelScope.executeUseCase(block)
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/confirmation/ConfirmationNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.confirmation
2 |
3 | import com.moove.shared.navigation.ScreenNavigator
4 |
5 | class ConfirmationNavigator(
6 | private val screenNavigator: ScreenNavigator,
7 | ) : ScreenNavigator {
8 |
9 | override fun goBack() {
10 | screenNavigator.goBack()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/domain/use_cases/GetRydersUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.use_cases
2 |
3 | import com.moove.tickets.domain.TicketsRepository
4 | import com.moove.tickets.domain.model.Ryder
5 |
6 | class GetRydersUseCase(
7 | private val ticketsRepository: TicketsRepository,
8 | ) {
9 | suspend operator fun invoke(): List = ticketsRepository.getRyders()
10 | }
11 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/domain/TicketsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain
2 |
3 | import com.moove.tickets.domain.model.Fare
4 | import com.moove.tickets.domain.model.Ryder
5 |
6 | interface TicketsRepository {
7 | suspend fun getRyders(): List
8 | suspend fun getFares(ryderId: String): List
9 | suspend fun buyTicket(ryderId: String, fare: Fare, totalCount: Int)
10 | }
11 |
--------------------------------------------------------------------------------
/.github/actions/job-set-up/action.yml:
--------------------------------------------------------------------------------
1 | name: Job set up
2 | description: Sets up the Java and Gradle
3 | runs:
4 | using: "composite"
5 | steps:
6 | - uses: actions/setup-java@v4
7 | with:
8 | distribution: "zulu"
9 | java-version: "17"
10 |
11 | - name: Setup Gradle
12 | uses: gradle/gradle-build-action@v2
13 |
14 | - name: Setup Android SDK
15 | uses: android-actions/setup-android@v3
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/main/MainNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.main
2 |
3 | import com.moove.shared.feature.deeplink.domain.DeepLink
4 | import com.moove.shared.feature.deeplink.presentation.DeepLinkNavigator
5 |
6 | class MainNavigator(
7 | private val deepLinkNavigator: DeepLinkNavigator,
8 | ) {
9 |
10 | fun navigateDeepLink(link: DeepLink) {
11 | deepLinkNavigator.navigateTo(link)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/presentation/fragment/delegate/FragmentViewLifecycleAwareDelegateExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.presentation.fragment.delegate
2 |
3 | import android.view.View
4 | import androidx.fragment.app.Fragment
5 | import androidx.viewbinding.ViewBinding
6 |
7 | fun Fragment.viewBinding(viewBindingFactory: (View) -> T) =
8 | FragmentViewLifecycleAwareDelegate(this, viewBindingFactory)
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/moove/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.moove
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/moove/NeutralColors.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose.moove
2 |
3 | import com.moove.design_system.compose.NeutralColors
4 |
5 | internal val mooveNeutralColors: NeutralColors
6 | get() = NeutralColors(
7 | MooveColors.Grey01,
8 | MooveColors.Grey02,
9 | MooveColors.Grey03,
10 | MooveColors.Grey04,
11 | MooveColors.Grey05,
12 | )
13 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/data/local/dto/TicketsDataDTO.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.data.local.dto
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class TicketsDataDTO(
8 | @Json(name = "Adult")
9 | val adult: RyderDTO,
10 | @Json(name = "Child")
11 | val child: RyderDTO,
12 | @Json(name = "Senior")
13 | val senior: RyderDTO,
14 | )
--------------------------------------------------------------------------------
/core/src/main/java/com/moove/core/kotlin/ResultExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.core.kotlin
2 |
3 | import java.util.concurrent.CancellationException
4 |
5 | inline fun runSuspendCatching(block: () -> R): Result {
6 | return try {
7 | Result.success(block())
8 | } catch (cancellationException: CancellationException) {
9 | throw cancellationException
10 | } catch (e: Throwable) {
11 | Result.failure(e)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/domain/use_cases/GetFaresByIdUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.use_cases
2 |
3 | import com.moove.tickets.domain.TicketsRepository
4 | import com.moove.tickets.domain.model.Fare
5 |
6 | class GetFaresByIdUseCase(
7 | private val ticketsRepository: TicketsRepository,
8 | ) {
9 | suspend operator fun invoke(ryderId: String): List {
10 | return ticketsRepository.getFares(ryderId)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/di/CoroutineModule.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.di
2 |
3 | import com.moove.core.exception.ExceptionHandler
4 | import com.moove.core.exception.asCoroutineExceptionHandler
5 | import com.moove.core.kotlin.coroutines.AppCoroutineScope
6 | import org.koin.dsl.module
7 |
8 | val coroutineModule = module {
9 | single { AppCoroutineScope(exceptionHandler = get()) }
10 | single { get().asCoroutineExceptionHandler() }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/main/MainActivityState.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.main
2 |
3 | import android.os.Parcelable
4 | import com.moove.shared.feature.deeplink.domain.DeepLink
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @Parcelize
8 | class MainActivityState : Parcelable
9 |
10 | sealed class MainActivityEffect {
11 | data object ShowGenericError : MainActivityEffect()
12 | data class NavigateDeepLink(val deepLink: DeepLink) : MainActivityEffect()
13 | }
14 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/Shapes.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose
2 |
3 | import androidx.compose.material.Shapes
4 | import androidx.compose.runtime.Immutable
5 | import androidx.compose.runtime.staticCompositionLocalOf
6 |
7 | @Immutable
8 | data class AppShapes(
9 | val material: Shapes,
10 | )
11 |
12 | val LocalAppShapes = staticCompositionLocalOf {
13 | AppShapes(
14 | material = Shapes(),
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/di/NetModule.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.di
2 |
3 | import com.squareup.moshi.Moshi
4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
5 | import dev.zacsweers.moshix.adapters.AdaptedBy
6 | import org.koin.dsl.module
7 |
8 | val netModule = module {
9 |
10 | single {
11 | Moshi.Builder()
12 | .add(KotlinJsonAdapterFactory())
13 | .add(AdaptedBy.Factory())
14 | .build()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/Dimensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose
2 |
3 | import androidx.compose.ui.unit.dp
4 |
5 | object Spacing {
6 | val XXS = 2.dp
7 | val XS = 4.dp
8 | val S = 8.dp
9 | val SM = 12.dp
10 | val M = 16.dp
11 | val L = 24.dp
12 | val XL = 32.dp
13 | val XXL = 48.dp
14 | }
15 |
16 | object Elevation {
17 | val XS = 2.dp
18 | val S = 4.dp
19 | val M = 8.dp
20 | val LM = 12.dp
21 | }
22 |
--------------------------------------------------------------------------------
/core/src/main/java/com/moove/core/okhttp/ConnectionErrorInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.moove.core.okhttp
2 |
3 | import java.io.IOException
4 |
5 | class ConnectionErrorInterceptor : okhttp3.Interceptor {
6 |
7 | override fun intercept(chain: okhttp3.Interceptor.Chain): okhttp3.Response {
8 | try {
9 | return chain.proceed(chain.request())
10 | } catch (exception: IOException) {
11 | throw NoConnectionException(cause = exception)
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/NeutralColors.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.ui.graphics.Color
5 |
6 | @Immutable
7 | data class NeutralColors(
8 | val grey01: Color = Color.Unspecified,
9 | val grey02: Color = Color.Unspecified,
10 | val grey03: Color = Color.Unspecified,
11 | val grey04: Color = Color.Unspecified,
12 | val grey05: Color = Color.Unspecified,
13 | )
14 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "Moove"
17 | include ':app'
18 | include ':shared'
19 | include ':design-system'
20 | include ':tickets'
21 | include ':core'
22 |
--------------------------------------------------------------------------------
/shared/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Select Ryder
4 | Select Fare
5 | Confirm Selection
6 | An error has occurred.
7 | Buy %s Tickets - %s
8 | Success
9 |
10 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/feature/deeplink/domain/GetDeeplinkUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.feature.deeplink.domain
2 |
3 | class GetDeeplinkUseCase(
4 | private val deeplinkRepository: DeeplinkRepository,
5 | private val getDynamicLinkUseCase: GetDynamicLinkUseCase,
6 | ) {
7 | suspend operator fun invoke(uri: String): DeepLink {
8 | val parsedUri = getDynamicLinkUseCase(uri) ?: uri
9 | val deepLink = deeplinkRepository.getDeepLink(parsedUri)
10 | return deepLink
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/deeplink/data/DynamicLinkDataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.deeplink.data
2 |
3 | import com.moove.app.feature.deeplink.data.remote.FirebaseDynamicLinkDataSource
4 | import com.moove.shared.feature.deeplink.domain.DynamicLinkRepository
5 |
6 | class DynamicLinkDataRepository(
7 | private val dataSource: FirebaseDynamicLinkDataSource,
8 | ) : DynamicLinkRepository {
9 |
10 | override suspend fun parseLink(uri: String): String? {
11 | return dataSource.parseLink(uri)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tickets/src/test/java/com/moove/tickets/data/dto/RyderDTOMother.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.data.dto
2 |
3 | import com.moove.shared.faker
4 | import com.moove.tickets.data.local.dto.FareDTO
5 | import com.moove.tickets.data.local.dto.RyderDTO
6 |
7 | fun randomFareDTO() = FareDTO(
8 | description = faker.name().fullName(),
9 | price = faker.number().randomDigitNotZero().toFloat(),
10 | )
11 |
12 | fun randomRyderDTO() = RyderDTO(
13 | fares = listOf(randomFareDTO(), randomFareDTO()),
14 | subtext = faker.name().fullName(),
15 | )
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/deeplink/domain/AppDeepLink.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.deeplink.domain
2 |
3 | import com.moove.shared.feature.deeplink.domain.DeepLink
4 | import com.moove.tickets.domain.model.Fare
5 |
6 | sealed class AppDeepLink: DeepLink {
7 | data object Unknown : AppDeepLink()
8 | data object Home : AppDeepLink()
9 | data class FareList(val ryderId: String) : AppDeepLink()
10 |
11 | data class Confirmation(
12 | val ryderId: String,
13 | val fare: Fare,
14 | ) : AppDeepLink()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/fare/model/FareModelFake.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.fare.model
2 |
3 | internal val fakeFareModels: List
4 | get() = listOf(
5 | FareModel(
6 | description = "2.5 Hour Ticket",
7 | price = 2.5f,
8 | ),
9 | FareModel(
10 | description = "1 Day Pass",
11 | price = 5.0f,
12 | ),
13 | FareModel(
14 | description = "30 Day Pass",
15 | price = 100f,
16 | )
17 | )
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/domain/use_cases/BuyTicketUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.use_cases
2 |
3 | import com.moove.tickets.domain.TicketsRepository
4 | import com.moove.tickets.domain.model.Fare
5 | import com.moove.tickets.domain.model.Ryder
6 |
7 | class BuyTicketUseCase(
8 | private val ticketsRepository: TicketsRepository,
9 | ) {
10 | suspend operator fun invoke(ryderId: String, fare: Fare, totalCount: Int) {
11 | ticketsRepository.buyTicket(ryderId = ryderId, fare = fare, totalCount = totalCount)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/Typography.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.runtime.Immutable
5 | import androidx.compose.runtime.staticCompositionLocalOf
6 |
7 | @Immutable
8 | data class AppTypography(
9 | val material: Typography,
10 | val appBar: TopAppBarTypography,
11 | )
12 |
13 | val LocalAppTypography = staticCompositionLocalOf {
14 | AppTypography(
15 | material = Typography(),
16 | appBar = TopAppBarTypography()
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/shared/exception_handler/main/MainExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.shared.exception_handler.main
2 |
3 | import com.moove.core.exception.ExceptionHandler
4 | import com.moove.core.okhttp.NoConnectionException
5 |
6 | class MainExceptionHandler(
7 | private val exceptionHandlers: List
8 | ) : ExceptionHandler {
9 |
10 | override fun handle(error: Throwable) {
11 | if (error is NoConnectionException) {
12 | return
13 | }
14 | exceptionHandlers.forEach { it.handle(error) }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/list/RyderListNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.list
2 |
3 | import com.moove.shared.navigation.ScreenNavigator
4 | import com.moove.shared.navigation.TicketsNavigator
5 |
6 | class RyderListNavigator(
7 | private val ticketsNavigator: TicketsNavigator,
8 | private val screenNavigator: ScreenNavigator,
9 | ) : ScreenNavigator {
10 |
11 | fun goFares(id: String) {
12 | ticketsNavigator.goFares(id)
13 | }
14 |
15 | override fun goBack() {
16 | screenNavigator.goBack()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/data/local/dto/FareDTO.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.data.local.dto
2 |
3 | import com.moove.tickets.domain.model.Fare
4 | import com.squareup.moshi.Json
5 | import com.squareup.moshi.JsonClass
6 |
7 | @JsonClass(generateAdapter = true)
8 | data class FareDTO(
9 | @Json(name = "description")
10 | val description: String,
11 | @Json(name = "price")
12 | val price: Float,
13 | )
14 |
15 | fun FareDTO.asDomain() = Fare(
16 | description = description,
17 | price = price,
18 | )
19 |
20 | fun List.asDomain() = map { it.asDomain() }
21 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/data/local/dto/RyderDTO.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.data.local.dto
2 |
3 | import com.moove.tickets.domain.model.Ryder
4 | import com.squareup.moshi.Json
5 | import com.squareup.moshi.JsonClass
6 |
7 | @JsonClass(generateAdapter = true)
8 | data class RyderDTO(
9 | @Json(name = "fares")
10 | val fares: List,
11 | @Json(name = "subtext")
12 | val subtext: String?,
13 | )
14 |
15 | // TODO add extra tests
16 | fun RyderDTO.asDomain(id: String) = Ryder(
17 | id = id,
18 | fares = fares.asDomain(),
19 | subtext = subtext,
20 | )
--------------------------------------------------------------------------------
/tickets/src/test/java/com/moove/tickets/domain/model/RyderMother.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.model
2 |
3 | import com.moove.shared.faker
4 | import com.moove.tickets.presentation.fare.model.FareModel
5 | import com.moove.tickets.presentation.list.model.RyderModel
6 |
7 | fun randomFare() = Fare(
8 | description = faker.name().fullName(),
9 | price = faker.number().randomDigitNotZero().toFloat(),
10 | )
11 |
12 | fun randomRyder() = Ryder(
13 | id = faker.idNumber().valid(),
14 | subtext = faker.name().fullName(),
15 | fares = listOf(randomFare(), randomFare()),
16 | )
17 |
--------------------------------------------------------------------------------
/.github/workflows/generate_docs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # The website is built using MkDocs with the Material theme.
4 | # https://squidfunk.github.io/mkdocs-material/
5 | # It requires Python to run.
6 | # Install the packages with the following command:
7 | # pip install mkdocs mkdocs-material mkdocs-redirects
8 |
9 | set -ex
10 |
11 | # Generate the API docs
12 | ./gradlew dokkaHtmlMultiModule
13 | #mv ./build/dokka/api android-docs/docs
14 |
15 | # Build the site locally
16 | cd ./android-docs
17 | python3 -m venv venv
18 | source venv/bin/activate
19 | pip3 install mkdocs-material
20 | mkdocs build
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/deeplink/data/DeeplinkDataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.deeplink.data
2 |
3 | import com.moove.app.feature.deeplink.data.local.AppDeepLinkLocalDataSource
4 | import com.moove.shared.feature.deeplink.domain.DeepLink
5 | import com.moove.shared.feature.deeplink.domain.DeeplinkRepository
6 |
7 | class DeeplinkDataRepository(
8 | private val localDataSource: AppDeepLinkLocalDataSource,
9 | ) : DeeplinkRepository {
10 |
11 | override suspend fun getDeepLink(uri: String): DeepLink {
12 | return localDataSource.getDeepLinkData(uri)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/presentation/compose/platform/ComposeViewExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.presentation.compose.platform
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.platform.ComposeView
5 | import androidx.compose.ui.platform.ViewCompositionStrategy
6 | import com.moove.design_system.compose.AppTheme
7 |
8 | fun ComposeView.setAppComposeContent(strategy: ViewCompositionStrategy? = null, content: @Composable () -> Unit) {
9 | strategy?.also(::setViewCompositionStrategy)
10 | setContent {
11 | AppTheme(content = content)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/home/HomeNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.home
2 |
3 | import androidx.navigation.NavController
4 | import com.moove.shared.navigation.ScreenNavigator
5 | import com.moove.shared.navigation.navigateSafely
6 |
7 | class HomeNavigator(
8 | private val navController: NavController,
9 | private val screenNavigator: ScreenNavigator,
10 | ) : ScreenNavigator {
11 |
12 | fun goRyderList() {
13 | navController.navigateSafely(HomeFragmentDirections.actionHomeFragmentToTicketsFlow())
14 | }
15 |
16 | override fun goBack() {
17 | screenNavigator.goBack()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/moove/Shapes.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose.moove
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.unit.dp
7 | import com.moove.design_system.compose.AppShapes
8 |
9 | internal val mooveShapes: AppShapes
10 | @Composable
11 | get() = AppShapes(
12 | material = Shapes(
13 | small = RoundedCornerShape(8.dp),
14 | medium = RoundedCornerShape(8.dp),
15 | large = RoundedCornerShape(8.dp)
16 | ),
17 | )
18 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/Colors.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose
2 |
3 | import androidx.compose.material.Colors
4 | import androidx.compose.material.lightColors
5 | import androidx.compose.runtime.Immutable
6 | import androidx.compose.runtime.staticCompositionLocalOf
7 |
8 | @Immutable
9 | data class AppColors(
10 | val material: Colors,
11 | val neutral: NeutralColors,
12 | val buttons: ButtonsColors,
13 | )
14 |
15 | val LocalAppColors = staticCompositionLocalOf {
16 | AppColors(
17 | material = lightColors(),
18 | neutral = NeutralColors(),
19 | buttons = ButtonsColors(),
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/fare/model/FareModel.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.fare.model
2 |
3 | import android.os.Parcelable
4 | import com.moove.tickets.domain.model.Fare
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @Parcelize
8 | data class FareModel(
9 | val description: String,
10 | val price: Float,
11 | ) : Parcelable
12 |
13 | fun Fare.asPresentation() = FareModel(
14 | description = description,
15 | price = price,
16 | )
17 |
18 | fun List.asPresentation(): List = map { it.asPresentation() }
19 |
20 | fun FareModel.asDomain() = Fare(
21 | description = description,
22 | price = price,
23 | )
24 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/list/RyderListState.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.list
2 |
3 | import android.os.Parcelable
4 | import com.moove.shared.presentation.compose.component.ScreenContentStatus
5 | import com.moove.tickets.presentation.list.model.RyderModel
6 | import kotlinx.parcelize.Parcelize
7 |
8 | @Parcelize
9 | data class RyderListState(
10 | val status: ScreenContentStatus = ScreenContentStatus.Idle,
11 | val ryders: List = emptyList(),
12 | ) : Parcelable
13 |
14 | sealed class RyderListEffect {
15 | data class GoToFares(val id: String) : RyderListEffect()
16 | object ShowGenericError : RyderListEffect()
17 | }
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/fare/FareListState.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.fare
2 |
3 | import android.os.Parcelable
4 | import com.moove.shared.presentation.compose.component.ScreenContentStatus
5 | import com.moove.tickets.presentation.fare.model.FareModel
6 | import kotlinx.parcelize.Parcelize
7 |
8 | @Parcelize
9 | data class FareListState(
10 | val status: ScreenContentStatus = ScreenContentStatus.Idle,
11 | val fares: List = emptyList(),
12 | ) : Parcelable
13 |
14 | sealed class FareListEffect {
15 | data class GoToConfirmation(val ryderId: String, val fare: FareModel) : FareListEffect()
16 | object ShowGenericError : FareListEffect()
17 | }
--------------------------------------------------------------------------------
/design-system/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | apply from: "${rootProject.projectDir}/gradle/base-android-config.gradle"
7 | apply from: "${rootProject.projectDir}/gradle/compose-android-config.gradle"
8 |
9 | android {
10 | namespace 'com.moove.design_system'
11 | }
12 |
13 | dependencies {
14 |
15 | // region Google
16 | implementation libs.google.material
17 | implementation libs.bundles.google.accompanist
18 | // endregion
19 |
20 | // region AndroidX
21 | implementation libs.androidx.appcompat
22 | implementation libs.androidx.core
23 | implementation libs.bundles.androidx.compose
24 | // endregion
25 | }
--------------------------------------------------------------------------------
/tickets/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'kotlin-parcelize'
5 | id 'androidx.navigation.safeargs.kotlin'
6 | id 'com.google.devtools.ksp'
7 | }
8 |
9 | apply from: "${rootProject.projectDir}/gradle/base-android-config.gradle"
10 | apply from: "${rootProject.projectDir}/gradle/compose-android-config.gradle"
11 |
12 | android {
13 | namespace 'com.moove.tickets'
14 | }
15 |
16 | dependencies {
17 |
18 | implementation project(':shared')
19 |
20 | ksp libs.moshi.compiler
21 |
22 | testImplementation libs.bundles.tests.unit
23 | testImplementation libs.bundles.tests.viewmodel
24 | androidTestImplementation libs.bundles.tests.android
25 | }
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/fare/FareListNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.fare
2 |
3 | import com.moove.shared.navigation.ScreenNavigator
4 | import com.moove.shared.navigation.TicketsNavigator
5 | import com.moove.tickets.presentation.fare.model.FareModel
6 |
7 | class FareListNavigator(
8 | private val ticketsNavigator: TicketsNavigator,
9 | private val screenNavigator: ScreenNavigator,
10 | ) : ScreenNavigator {
11 |
12 | fun goToConfirmation(ryderId: String, fare: FareModel) {
13 | ticketsNavigator.goToConfirmation(ryderId, fare.description, fare.price)
14 | }
15 |
16 | override fun goBack() {
17 | screenNavigator.goBack()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/list/model/RyderModelFake.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.list.model
2 |
3 | import com.moove.tickets.presentation.fare.model.fakeFareModels
4 |
5 | internal val fakeRyderModels: List
6 | get() = listOf(
7 | RyderModel(
8 | id = "Adult",
9 | fares = fakeFareModels,
10 | subtext = null,
11 | ),
12 | RyderModel(
13 | id = "Child",
14 | fares = fakeFareModels,
15 | subtext = "Ages 8-17",
16 | ),
17 | RyderModel(
18 | id = "Senior",
19 | fares = fakeFareModels,
20 | subtext = "Ages 60+",
21 | ),
22 | )
--------------------------------------------------------------------------------
/core/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | }
5 | apply from: "${rootProject.projectDir}/gradle/base-android-config.gradle"
6 |
7 | android {
8 | namespace 'com.moove.core'
9 | }
10 |
11 | dependencies {
12 | // region kotlin
13 | api libs.kotlin.coroutines.core
14 | api libs.kotlin.coroutines.android
15 | // endregion
16 |
17 | // region ui
18 | api libs.androidx.core
19 | api libs.androidx.compose.ui.text
20 | // endregion
21 |
22 | // region io
23 | api platform(libs.okhttp.platform)
24 | api libs.okhttp.client
25 | api libs.okhttp.logging
26 | // endregion
27 |
28 | // region Tests
29 | testImplementation libs.bundles.tests.unit
30 | // endregion
31 | }
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/confirmation/ConfirmationState.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.confirmation
2 |
3 | import android.os.Parcelable
4 | import com.moove.shared.presentation.compose.component.ScreenContentStatus
5 | import com.moove.tickets.presentation.fare.model.FareModel
6 | import kotlinx.parcelize.Parcelize
7 |
8 | @Parcelize
9 | data class ConfirmationState(
10 | val status: ScreenContentStatus = ScreenContentStatus.Idle,
11 | val ryderId: String,
12 | val fare: FareModel,
13 | val ticketCount: Int,
14 | val totalPrice: Float,
15 | ) : Parcelable
16 |
17 | sealed class ConfirmationEffect {
18 | object ShowGenericError : ConfirmationEffect()
19 | object ShowSuccessMessage : ConfirmationEffect()
20 | }
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/list/model/RyderModel.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.list.model
2 |
3 | import android.os.Parcelable
4 | import com.moove.tickets.domain.model.Ryder
5 | import com.moove.tickets.presentation.fare.model.FareModel
6 | import com.moove.tickets.presentation.fare.model.asPresentation
7 | import kotlinx.parcelize.Parcelize
8 |
9 | @Parcelize
10 | data class RyderModel(
11 | val id: String,
12 | val fares: List,
13 | val subtext: String?,
14 | ) : Parcelable
15 |
16 | fun Ryder.asPresentation() = RyderModel(
17 | id = id,
18 | fares = fares.asPresentation(),
19 | subtext = subtext,
20 | )
21 |
22 | fun List.asPresentation(): List = map { it.asPresentation() }
23 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/ModelMother.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared
2 |
3 | import com.github.javafaker.Faker
4 |
5 | val faker = object : Faker() {}
6 |
7 | fun randomListOf(
8 | minListElements: Int = 2,
9 | maxListElements: Int = 20,
10 | elementGenerationBlock: () -> T
11 | ): List {
12 | val numberOfElements = faker.number().numberBetween(minListElements, maxListElements)
13 | return (0..numberOfElements).map { elementGenerationBlock() }
14 | }
15 |
16 | fun Array.random() = this[faker.number().numberBetween(0, this.size)]
17 |
18 | fun randomNullableOf(nullableValueGenerationBlock: () -> T): T? {
19 | if (faker.bool().bool()) {
20 | return null
21 | }
22 | return nullableValueGenerationBlock()
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/java/com/moove/core/kotlin/coroutines/CoroutineScopeExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.core.kotlin.coroutines
2 |
3 | import com.moove.core.kotlin.runSuspendCatching
4 | import kotlinx.coroutines.CoroutineExceptionHandler
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.coroutineScope
7 |
8 | @Suppress("RedundantSuspendModifier", "SuspendFunctionOnCoroutineScope")
9 | suspend inline fun CoroutineScope.executeUseCase(block: () -> R): Result {
10 | val currentExecContext = coroutineScope { coroutineContext }
11 | val exceptionHandler = currentExecContext[CoroutineExceptionHandler]
12 | return runSuspendCatching(block)
13 | .onFailure {
14 | exceptionHandler?.handleException(currentExecContext, it)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/moove/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.moove
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.moove", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/shared/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/tickets/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/design-system/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/res/navigation/app_main_navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/core/src/main/java/com/moove/core/kotlin/coroutines/AppCoroutineScope.kt:
--------------------------------------------------------------------------------
1 | package com.moove.core.kotlin.coroutines
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.CoroutineExceptionHandler
5 | import kotlinx.coroutines.CoroutineName
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.SupervisorJob
9 |
10 | /**
11 | * App-wide coroutine scope intended to be a singleton.
12 | * It is supervisor scope, meaning any unhandled exception wont affect siblings.
13 | * Also, it requires exception handler to exist meaning we want all un-handled exceptions to be caught.
14 | */
15 | class AppCoroutineScope(
16 | dispatcher: CoroutineDispatcher = Dispatchers.Default,
17 | exceptionHandler: CoroutineExceptionHandler,
18 | ) : CoroutineScope by CoroutineScope(
19 | SupervisorJob() + dispatcher + exceptionHandler + CoroutineName("AppScope")
20 | )
21 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/moove/Button.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose.moove
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.moove.design_system.compose.ButtonsColors
5 | import com.moove.design_system.compose.PrimaryButtonColors
6 |
7 | private object MooveButtonsDefaults {
8 | val mainColor = MooveColors.FloytGreen
9 | val contentColor = MooveColors.White
10 | val disabledColor = MooveColors.Grey05
11 | val disabledContentColor = MooveColors.Grey03
12 | }
13 |
14 | internal val mooveButtonsColors: ButtonsColors
15 | @Composable
16 | get() = ButtonsColors(
17 | primary = PrimaryButtonColors(
18 | background = MooveButtonsDefaults.mainColor,
19 | content = MooveButtonsDefaults.contentColor,
20 | disabledBackground = MooveButtonsDefaults.disabledColor,
21 | disabledContent = MooveButtonsDefaults.disabledContentColor,
22 | ),
23 | )
24 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/presentation/compose/platform/FragmentExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.presentation.compose.platform
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.platform.ComposeView
6 | import androidx.compose.ui.platform.LocalContext
7 | import androidx.compose.ui.platform.ViewCompositionStrategy
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.FragmentActivity
10 | import androidx.fragment.app.FragmentManager
11 |
12 | fun Fragment.setAppComposeContent(view: ComposeView, content: @Composable () -> Unit) {
13 | view.setAppComposeContent(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, content)
14 | }
15 |
16 | @Composable
17 | fun rememberFragmentManager(): FragmentManager? {
18 | val context = LocalContext.current
19 | return remember(context) {
20 | (context as? FragmentActivity)?.supportFragmentManager
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tickets/src/test/java/com/moove/tickets/domain/use_cases/BuyTicketUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.use_cases
2 |
3 | import com.moove.shared.faker
4 | import com.moove.tickets.domain.TicketsRepository
5 | import com.moove.tickets.domain.model.Fare
6 | import io.mockk.coVerify
7 | import io.mockk.mockk
8 | import kotlinx.coroutines.test.runTest
9 | import org.junit.Test
10 |
11 | class BuyTicketUseCaseTest {
12 |
13 | private val ticketsRepository: TicketsRepository = mockk(relaxed = true)
14 |
15 | private val buyTicketUseCase = BuyTicketUseCase(ticketsRepository)
16 |
17 | @Test
18 | fun `On invoke should call correct method on repository`() = runTest {
19 | val ryderId = faker.idNumber().valid()
20 | val fare: Fare = mockk()
21 | val totalCount: Int = faker.number().randomDigitNotZero()
22 |
23 | buyTicketUseCase(ryderId, fare, totalCount)
24 |
25 | coVerify { ticketsRepository.buyTicket(ryderId, fare, totalCount) }
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/home/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.home
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.moove.core.exception.ExceptionHandler
5 | import com.moove.core.exception.asCoroutineExceptionHandler
6 | import org.orbitmvi.orbit.Container
7 | import org.orbitmvi.orbit.ContainerHost
8 | import org.orbitmvi.orbit.syntax.simple.intent
9 | import org.orbitmvi.orbit.syntax.simple.postSideEffect
10 | import org.orbitmvi.orbit.viewmodel.container
11 |
12 | class HomeViewModel(
13 | exceptionHandler: ExceptionHandler,
14 | ) : ViewModel(), ContainerHost {
15 |
16 | override val container: Container = container(
17 | initialState = HomeState(),
18 | buildSettings = {
19 | this.exceptionHandler = exceptionHandler.asCoroutineExceptionHandler()
20 | },
21 | )
22 |
23 | fun onRyderClick() = intent {
24 | postSideEffect(HomeEffect.GoToRyderList)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/fare/component/FareList.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.fare.component
2 |
3 | import androidx.compose.foundation.lazy.LazyColumn
4 | import androidx.compose.foundation.lazy.items
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.tooling.preview.Preview
7 | import com.moove.design_system.compose.AppTheme
8 | import com.moove.tickets.presentation.fare.model.FareModel
9 | import com.moove.tickets.presentation.fare.model.fakeFareModels
10 |
11 | @Composable
12 | fun FareList(
13 | fares: List,
14 | onClick: (FareModel) -> Unit,
15 | ) {
16 | LazyColumn {
17 | items(fares) { item ->
18 | FareItem(fare = item, onClick = onClick)
19 | }
20 | }
21 | }
22 |
23 | @Preview(name = "Fare List", showBackground = true)
24 | @Composable
25 | private fun PreviewRyderList() {
26 | AppTheme {
27 | FareList(
28 | fares = fakeFareModels,
29 | onClick = {}
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/list/component/RyderList.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.list.component
2 |
3 | import androidx.compose.foundation.lazy.LazyColumn
4 | import androidx.compose.foundation.lazy.items
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.tooling.preview.Preview
7 | import com.moove.design_system.compose.AppTheme
8 | import com.moove.tickets.presentation.list.model.RyderModel
9 | import com.moove.tickets.presentation.list.model.fakeRyderModels
10 |
11 | @Composable
12 | fun RyderList(
13 | ryders: List,
14 | onClick: (RyderModel) -> Unit,
15 | ) {
16 | LazyColumn {
17 | items(ryders) { item ->
18 | RyderItem(ryder = item, onClick = onClick)
19 | }
20 | }
21 | }
22 |
23 | @Preview(name = "Ryder List", showBackground = true)
24 | @Composable
25 | private fun PreviewRyderList() {
26 | AppTheme {
27 | RyderList(
28 | ryders = fakeRyderModels,
29 | onClick = {}
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tickets/src/test/java/com/moove/tickets/domain/use_cases/GetRydersUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.use_cases
2 |
3 | import com.moove.tickets.domain.TicketsRepository
4 | import com.moove.tickets.domain.model.Ryder
5 | import io.mockk.coEvery
6 | import io.mockk.coVerify
7 | import io.mockk.mockk
8 | import kotlinx.coroutines.test.runTest
9 | import org.junit.Test
10 | import kotlin.test.assertEquals
11 |
12 | class GetRydersUseCaseTest {
13 |
14 | private val ticketsRepository: TicketsRepository = mockk(relaxed = true)
15 |
16 | private val getRydersUseCase = GetRydersUseCase(ticketsRepository)
17 |
18 | @Test
19 | fun `On invoke should return correct result`() = runTest {
20 | val ryders: List = listOf(
21 | mockk(),
22 | mockk(),
23 | )
24 |
25 | coEvery { ticketsRepository.getRyders() } returns ryders
26 |
27 | val result = getRydersUseCase()
28 |
29 | assertEquals(ryders, result)
30 |
31 | coVerify { ticketsRepository.getRyders() }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/moove/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose.moove
2 |
3 | import androidx.compose.material.LocalContentColor
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.CompositionLocalProvider
7 | import com.moove.design_system.compose.LocalAppColors
8 | import com.moove.design_system.compose.LocalAppShapes
9 | import com.moove.design_system.compose.LocalAppTypography
10 |
11 | @Composable
12 | internal fun MooveTheme(content: @Composable () -> Unit) {
13 | MaterialTheme(
14 | colors = mooveColors.material,
15 | typography = mooveTypography.material,
16 | shapes = mooveShapes.material,
17 | ) {
18 | CompositionLocalProvider(
19 | LocalContentColor provides MooveColors.Text,
20 | LocalAppTypography provides mooveTypography,
21 | LocalAppColors provides mooveColors,
22 | LocalAppShapes provides mooveShapes,
23 | content = content
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tickets/src/test/java/com/moove/tickets/domain/use_cases/GetFaresByIdUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.domain.use_cases
2 |
3 | import com.moove.shared.faker
4 | import com.moove.tickets.domain.TicketsRepository
5 | import com.moove.tickets.domain.model.Fare
6 | import io.mockk.coEvery
7 | import io.mockk.coVerify
8 | import io.mockk.mockk
9 | import kotlinx.coroutines.test.runTest
10 | import org.junit.Test
11 | import kotlin.test.assertEquals
12 |
13 | class GetFaresByIdUseCaseTest {
14 |
15 | private val ticketsRepository: TicketsRepository = mockk(relaxed = true)
16 |
17 | private val getFaresByIdUseCase = GetFaresByIdUseCase(ticketsRepository)
18 |
19 | @Test
20 | fun `On invoke should return correct result`() = runTest {
21 | val ryderId = faker.idNumber().valid()
22 |
23 | val fares: List = listOf(
24 | mockk(),
25 | mockk(),
26 | )
27 |
28 | coEvery { ticketsRepository.getFares(ryderId) } returns fares
29 |
30 | val result = getFaresByIdUseCase(ryderId)
31 |
32 | assertEquals(fares, result)
33 |
34 | coVerify { ticketsRepository.getFares(ryderId) }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/deeplink/data/remote/FirebaseDynamicLinkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.deeplink.data.remote
2 |
3 | import android.net.Uri
4 | import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
5 | import com.moove.core.kotlin.text.matchesPattern
6 | import com.moove.shared.feature.deeplink.domain.exceptions.DynamicLinkParseException
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.tasks.await
10 | import kotlinx.coroutines.withContext
11 |
12 | class FirebaseDynamicLinkDataSource(
13 | private val host: String,
14 | private val firebaseDynamicLinks: FirebaseDynamicLinks,
15 | private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
16 | ) {
17 |
18 | suspend fun parseLink(uri: String): String? = withContext(backgroundDispatcher) {
19 | if (uri.matchesPattern(host).not()) return@withContext null
20 | try {
21 | firebaseDynamicLinks.getDynamicLink(Uri.parse(uri)).await().link?.toString()
22 | } catch (e: Exception) {
23 | throw DynamicLinkParseException(cause = e)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/MooveApp.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app
2 |
3 | import android.app.Application
4 | import com.moove.app.di.coroutineModule
5 | import com.moove.app.di.deepLinkModule
6 | import com.moove.app.di.exceptionsModule
7 | import com.moove.app.di.mainModule
8 | import com.moove.app.di.netModule
9 | import com.moove.tickets.di.ticketsModule
10 | import org.koin.android.ext.koin.androidContext
11 | import org.koin.android.ext.koin.androidLogger
12 | import org.koin.core.context.startKoin
13 | import org.koin.core.logger.Level
14 |
15 | open class MooveApp : Application() {
16 |
17 | override fun onCreate() {
18 | super.onCreate()
19 | setupDependencyInjection()
20 | }
21 |
22 | private fun setupDependencyInjection() {
23 | startKoin {
24 | androidContext(this@MooveApp)
25 | // androidLogger(if (BuildConfig.DEBUG) Level.INFO else Level.NONE)
26 | modules(
27 | mainModule,
28 | coroutineModule,
29 | exceptionsModule,
30 | ticketsModule,
31 | netModule,
32 | deepLinkModule,
33 | )
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/di/MainModule.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.di
2 |
3 | import com.moove.app.feature.home.HomeNavigator
4 | import com.moove.app.feature.home.HomeViewModel
5 | import com.moove.app.main.MainActivityViewModel
6 | import com.moove.app.main.MainNavigator
7 | import com.moove.app.navigation.AppNavigator
8 | import com.moove.shared.navigation.GlobalAppNavigator
9 | import com.moove.shared.navigation.ScreenNavigator
10 | import com.moove.shared.navigation.TicketsNavigator
11 | import org.koin.androidx.viewmodel.dsl.viewModelOf
12 | import org.koin.core.module.dsl.factoryOf
13 | import org.koin.dsl.binds
14 | import org.koin.dsl.module
15 |
16 | val mainModule = module {
17 |
18 | factory {
19 | AppNavigator(navController = get())
20 | } binds arrayOf(
21 | ScreenNavigator::class,
22 | GlobalAppNavigator::class,
23 | TicketsNavigator::class,
24 | )
25 |
26 | factory {
27 | HomeNavigator(
28 | navController = get(),
29 | screenNavigator = get(),
30 | )
31 | }
32 | viewModelOf(::HomeViewModel)
33 |
34 | viewModelOf(::MainActivityViewModel)
35 | factoryOf(::MainNavigator)
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/home/HomeFragment.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.home
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.fragment.app.Fragment
6 | import androidx.lifecycle.lifecycleScope
7 | import androidx.navigation.fragment.findNavController
8 | import com.moove.shared.R
9 | import com.moove.shared.databinding.ComposeFragmentBinding
10 | import com.moove.shared.presentation.compose.platform.setAppComposeContent
11 | import com.moove.shared.presentation.fragment.delegate.viewBinding
12 | import org.koin.android.ext.android.inject
13 | import org.koin.core.parameter.parametersOf
14 |
15 | class HomeFragment : Fragment(R.layout.compose_fragment) {
16 |
17 | private val viewBinding by viewBinding(ComposeFragmentBinding::bind)
18 | private val navigator: HomeNavigator by inject {
19 | parametersOf(findNavController(), lifecycleScope, requireContext())
20 | }
21 |
22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
23 | super.onViewCreated(view, savedInstanceState)
24 | setAppComposeContent(viewBinding.compose) {
25 | HomeRoute(navigator = navigator)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.staticCompositionLocalOf
6 | import com.moove.design_system.compose.moove.MooveTheme
7 |
8 | /** Shared application theme */
9 | @Composable
10 | fun AppTheme(theme: Theme = Theme.MOOVE, content: @Composable () -> Unit) {
11 | CompositionLocalProvider(
12 | LocalTheme provides theme,
13 | ) {
14 | when (AppTheme.theme) {
15 | Theme.MOOVE -> MooveTheme(content)
16 | }
17 | }
18 | }
19 |
20 | enum class Theme {
21 | MOOVE
22 | }
23 |
24 | val LocalTheme = staticCompositionLocalOf { Theme.MOOVE }
25 |
26 | object AppTheme {
27 | val theme: Theme
28 | @Composable
29 | get() = LocalTheme.current
30 | val typography: AppTypography
31 | @Composable
32 | get() = LocalAppTypography.current
33 | val colors: AppColors
34 | @Composable
35 | get() = LocalAppColors.current
36 | val shapes: AppShapes
37 | @Composable
38 | get() = LocalAppShapes.current
39 | }
40 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/list/RyderListFragment.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.list
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.fragment.app.Fragment
6 | import androidx.lifecycle.lifecycleScope
7 | import androidx.navigation.fragment.findNavController
8 | import com.moove.shared.R
9 | import com.moove.shared.databinding.ComposeFragmentBinding
10 | import com.moove.shared.presentation.compose.platform.setAppComposeContent
11 | import com.moove.shared.presentation.fragment.delegate.viewBinding
12 | import org.koin.android.ext.android.inject
13 | import org.koin.core.parameter.parametersOf
14 |
15 | class RyderListFragment : Fragment(R.layout.compose_fragment) {
16 |
17 | private val viewBinding by viewBinding(ComposeFragmentBinding::bind)
18 | private val navigator: RyderListNavigator by inject {
19 | parametersOf(findNavController(), lifecycleScope, requireContext())
20 | }
21 |
22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
23 | super.onViewCreated(view, savedInstanceState)
24 | setAppComposeContent(viewBinding.compose) {
25 | RyderListRoute(navigator = navigator)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/home/HomeRoute.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.home
2 |
3 | import androidx.compose.material.ScaffoldState
4 | import androidx.compose.material.rememberScaffoldState
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import com.moove.shared.presentation.viewmodel.composableEffect
8 | import com.moove.shared.presentation.viewmodel.composableState
9 | import org.koin.androidx.compose.koinViewModel
10 |
11 | @Composable
12 | fun HomeRoute(
13 | navigator: HomeNavigator,
14 | viewModel: HomeViewModel = koinViewModel(),
15 | scaffoldState: ScaffoldState = rememberScaffoldState(),
16 | ) {
17 | val state by viewModel.composableState()
18 |
19 | HomeScreen(
20 | uiState = state,
21 | scaffoldState = scaffoldState,
22 | onRyderClick = viewModel::onRyderClick,
23 | )
24 |
25 | viewModel.RenderEffect(navigator = navigator)
26 | }
27 |
28 | @Composable
29 | private fun HomeViewModel.RenderEffect(
30 | navigator: HomeNavigator,
31 | ) {
32 | composableEffect { effect ->
33 | when (effect) {
34 | is HomeEffect.GoToRyderList -> {
35 | navigator.goRyderList()
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/deeplink/presentation/DeepLinkAppNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.deeplink.presentation
2 |
3 | import com.moove.app.feature.deeplink.domain.AppDeepLink
4 | import com.moove.shared.feature.deeplink.domain.DeepLink
5 | import com.moove.shared.feature.deeplink.presentation.DeepLinkNavigator
6 | import com.moove.shared.navigation.GlobalAppNavigator
7 | import com.moove.shared.navigation.TicketsNavigator
8 |
9 | class DeepLinkAppNavigator(
10 | private val globalAppNavigator: GlobalAppNavigator,
11 | private val ticketsNavigator: TicketsNavigator,
12 | ) : DeepLinkNavigator {
13 | override fun navigateTo(link: DeepLink) {
14 | when (link) {
15 | is AppDeepLink.FareList -> ticketsNavigator.goFares(link.ryderId)
16 | is AppDeepLink.Confirmation -> {
17 | ticketsNavigator.goFares(link.ryderId)
18 | ticketsNavigator.goToConfirmation(
19 | ryderId = link.ryderId,
20 | fareDescription = link.fare.description,
21 | farePrice = link.fare.price
22 | )
23 | }
24 |
25 | is AppDeepLink.Home, AppDeepLink.Unknown -> globalAppNavigator.goHome()
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
21 |
22 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/data/TicketsDataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.data
2 |
3 | import com.moove.tickets.data.local.TicketsLocalDataSource
4 | import com.moove.tickets.data.local.dto.asDomain
5 | import com.moove.tickets.domain.TicketsRepository
6 | import com.moove.tickets.domain.model.Fare
7 | import com.moove.tickets.domain.model.Ryder
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.withContext
11 |
12 | class TicketsDataRepository(
13 | private val ticketsLocalDataSource: TicketsLocalDataSource,
14 | private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
15 | ) : TicketsRepository {
16 |
17 | override suspend fun getRyders(): List = withContext(backgroundDispatcher) {
18 | val data = ticketsLocalDataSource.getData()
19 | data.map { it.value.asDomain(it.key) }
20 | }
21 |
22 | override suspend fun getFares(ryderId: String): List = withContext(backgroundDispatcher) {
23 | val data = ticketsLocalDataSource.getData()
24 | data[ryderId]?.fares?.asDomain() ?: emptyList()
25 | }
26 |
27 | override suspend fun buyTicket(ryderId: String, fare: Fare, totalCount: Int) {
28 | // Implement request to the server
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/fare/FareListFragment.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.fare
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.fragment.app.Fragment
6 | import androidx.lifecycle.lifecycleScope
7 | import androidx.navigation.fragment.findNavController
8 | import androidx.navigation.fragment.navArgs
9 | import com.moove.shared.R
10 | import com.moove.shared.databinding.ComposeFragmentBinding
11 | import com.moove.shared.presentation.compose.platform.setAppComposeContent
12 | import com.moove.shared.presentation.fragment.delegate.viewBinding
13 | import org.koin.android.ext.android.inject
14 | import org.koin.core.parameter.parametersOf
15 |
16 | class FareListFragment : Fragment(R.layout.compose_fragment) {
17 |
18 | private val arguments: FareListFragmentArgs by navArgs()
19 | private val viewBinding by viewBinding(ComposeFragmentBinding::bind)
20 | private val navigator: FareListNavigator by inject {
21 | parametersOf(findNavController(), lifecycleScope, requireContext())
22 | }
23 |
24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
25 | super.onViewCreated(view, savedInstanceState)
26 | setAppComposeContent(viewBinding.compose) {
27 | FareListRoute(navigator = navigator, ryderId = arguments.ryderId)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/confirmation/ConfirmationFragment.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.confirmation
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.fragment.app.Fragment
6 | import androidx.lifecycle.lifecycleScope
7 | import androidx.navigation.fragment.findNavController
8 | import androidx.navigation.fragment.navArgs
9 | import com.moove.shared.R
10 | import com.moove.shared.databinding.ComposeFragmentBinding
11 | import com.moove.shared.presentation.compose.platform.setAppComposeContent
12 | import com.moove.shared.presentation.fragment.delegate.viewBinding
13 | import org.koin.android.ext.android.inject
14 | import org.koin.core.parameter.parametersOf
15 |
16 | class ConfirmationFragment : Fragment(R.layout.compose_fragment) {
17 |
18 | private val arguments: ConfirmationFragmentArgs by navArgs()
19 | private val viewBinding by viewBinding(ComposeFragmentBinding::bind)
20 | private val navigator: ConfirmationNavigator by inject {
21 | parametersOf(findNavController(), lifecycleScope, requireContext())
22 | }
23 |
24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
25 | super.onViewCreated(view, savedInstanceState)
26 | setAppComposeContent(viewBinding.compose) {
27 | FareListRoute(
28 | navigator = navigator,
29 | ryderId = arguments.ryderId,
30 | fare = arguments.fare
31 | )
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/presentation/viewmodel/OrbitExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.presentation.viewmodel
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.lifecycle.Lifecycle
8 | import androidx.lifecycle.flowWithLifecycle
9 | import org.orbitmvi.orbit.ContainerHost
10 | import org.orbitmvi.orbit.compose.collectAsState
11 | import org.orbitmvi.orbit.compose.collectSideEffect
12 |
13 | val ContainerHost.currentState: STATE
14 | get() = container.stateFlow.value
15 |
16 | /**
17 | * Collect [ContainerHost.container]'s state via [collectAsState] but bounded with the [flowWithLifecycle]
18 | */
19 | @Composable
20 | fun ContainerHost.composableState(
21 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED
22 | ): State {
23 | return this.collectAsState(minActiveState)
24 | }
25 |
26 | /**
27 | * Collect [ContainerHost.container]'s effect bounded with the [flowWithLifecycle]
28 | */
29 | @SuppressLint("ComposableNaming")
30 | @Composable
31 | fun ContainerHost.composableEffect(
32 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
33 | action: (suspend (sideEffect: SIDE_EFFECT) -> Unit),
34 | ) {
35 | this.collectSideEffect(sideEffect = action, lifecycleState = minActiveState)
36 | }
37 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/list/RyderListRoute.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.list
2 |
3 | import androidx.compose.material.ScaffoldState
4 | import androidx.compose.material.rememberScaffoldState
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.ui.platform.LocalContext
8 | import com.moove.shared.presentation.compose.component.showGenericError
9 | import com.moove.shared.presentation.viewmodel.composableEffect
10 | import com.moove.shared.presentation.viewmodel.composableState
11 | import org.koin.androidx.compose.getViewModel
12 |
13 | @Composable
14 | fun RyderListRoute(
15 | navigator: RyderListNavigator,
16 | viewModel: RyderListViewModel = getViewModel(),
17 | scaffoldState: ScaffoldState = rememberScaffoldState(),
18 | ) {
19 | val state by viewModel.composableState()
20 |
21 | RyderListScreen(
22 | uiState = state,
23 | scaffoldState = scaffoldState,
24 | onRyderClick = viewModel::onRyderClick,
25 | )
26 |
27 | viewModel.RenderEffect(scaffoldState = scaffoldState, navigator = navigator)
28 | }
29 |
30 | @Composable
31 | private fun RyderListViewModel.RenderEffect(
32 | scaffoldState: ScaffoldState,
33 | navigator: RyderListNavigator,
34 | ) {
35 | val context = LocalContext.current
36 | composableEffect { effect ->
37 | when (effect) {
38 | is RyderListEffect.GoToFares -> {
39 | navigator.goFares(effect.id)
40 | }
41 |
42 | RyderListEffect.ShowGenericError -> scaffoldState.showGenericError(context)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/navigation/AppNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.navigation
2 |
3 | import androidx.navigation.NavController
4 | import com.moove.MobileNavigationDirections
5 | import com.moove.app.feature.home.HomeFragmentDirections
6 | import com.moove.shared.navigation.GlobalAppNavigator
7 | import com.moove.shared.navigation.navigateSafely
8 | import com.moove.tickets.presentation.fare.FareListFragmentDirections
9 | import com.moove.tickets.presentation.fare.model.FareModel
10 | import com.moove.tickets.presentation.list.RyderListFragmentDirections
11 |
12 | class AppNavigator(
13 | private val navController: NavController,
14 | ) : GlobalAppNavigator {
15 |
16 | override fun goFares(ryderId: String) {
17 | navController.navigateSafely(HomeFragmentDirections.actionHomeFragmentToTicketsFlow())
18 | navController.navigateSafely(
19 | RyderListFragmentDirections.actionRydersFragmentToFareListFragment(ryderId = ryderId)
20 | )
21 | }
22 |
23 | override fun goToConfirmation(ryderId: String, fareDescription: String, farePrice: Float) {
24 | navController.navigateSafely(
25 | FareListFragmentDirections.actionFareListFragmentToConfirmationFragment(
26 | ryderId = ryderId,
27 | fare = FareModel(
28 | description = fareDescription,
29 | price = farePrice
30 | )
31 | )
32 | )
33 | }
34 |
35 | override fun goBack() {
36 | navController.navigateUp()
37 | }
38 |
39 | override fun goHome() {
40 | navController.navigateSafely(
41 | MobileNavigationDirections.actionGlobalGoHome()
42 | )
43 | }
44 | }
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/fare/component/FareItem.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.fare.component
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import com.moove.design_system.compose.AppTheme
12 | import com.moove.design_system.compose.Spacing
13 | import com.moove.tickets.presentation.fare.model.FareModel
14 | import com.moove.tickets.presentation.fare.model.fakeFareModels
15 |
16 | @Composable
17 | fun FareItem(
18 | modifier: Modifier = Modifier,
19 | fare: FareModel,
20 | onClick: (FareModel) -> Unit
21 | ) {
22 | Column(
23 | modifier = modifier
24 | .fillMaxWidth()
25 | .clickable(onClick = { onClick(fare) })
26 | .padding(horizontal = Spacing.S, vertical = Spacing.S)
27 | ) {
28 | Text(
29 | text = fare.description,
30 | style = AppTheme.typography.material.h1,
31 | maxLines = 1,
32 | )
33 | Text(
34 | text = "$".plus(String.format("%.2f", fare.price)),
35 | style = AppTheme.typography.material.subtitle1,
36 | maxLines = 1,
37 | )
38 | }
39 | }
40 |
41 | @Preview(name = "Fare Item", showBackground = true)
42 | @Composable
43 | private fun PreviewFareItem() {
44 | AppTheme {
45 | FareItem(
46 | fare = fakeFareModels.first(),
47 | onClick = {}
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/list/component/RyderItem.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.list.component
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import com.moove.design_system.compose.AppTheme
12 | import com.moove.design_system.compose.Spacing
13 | import com.moove.tickets.presentation.list.model.RyderModel
14 | import com.moove.tickets.presentation.list.model.fakeRyderModels
15 |
16 | @Composable
17 | fun RyderItem(
18 | modifier: Modifier = Modifier,
19 | ryder: RyderModel,
20 | onClick: (RyderModel) -> Unit
21 | ) {
22 | Column(
23 | modifier = modifier
24 | .fillMaxWidth()
25 | .clickable(onClick = { onClick(ryder) })
26 | .padding(horizontal = Spacing.S, vertical = Spacing.S)
27 | ) {
28 | Text(
29 | text = ryder.id,
30 | style = AppTheme.typography.material.h1,
31 | maxLines = 1,
32 | )
33 | ryder.subtext?.let {
34 | Text(
35 | text = ryder.subtext,
36 | style = AppTheme.typography.material.subtitle1,
37 | maxLines = 1,
38 | )
39 | }
40 | }
41 | }
42 |
43 | @Preview(name = "Ryder Item", showBackground = true)
44 | @Composable
45 | private fun PreviewRyderItem() {
46 | AppTheme {
47 | RyderItem(
48 | ryder = fakeRyderModels[1],
49 | onClick = {}
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/android-docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Moove
2 | site_description: "Moove - Documentation"
3 | site_author: Mkhytar Mkhoian.
4 | site_dir: html
5 |
6 | nav:
7 | - 'Getting Started': index.md
8 | - 'API': api/index.html
9 |
10 | theme:
11 | name: material
12 | features:
13 | - navigation.sections
14 | - navigation.top
15 | - toc.integrate
16 | - search.suggest
17 | - search.highlight
18 | - content.tabs.link
19 | - content.code.annotation
20 | - content.code.copy
21 | language: en
22 | palette:
23 | # Palette toggle for light mode
24 | - scheme: default
25 | media: "(prefers-color-scheme: light)"
26 | primary: custom
27 | accent: 'dark-blue'
28 | toggle:
29 | icon: material/brightness-7
30 | name: Switch to dark mode
31 |
32 | # Palette toggle for dark mode
33 | - scheme: slate
34 | media: "(prefers-color-scheme: dark)"
35 | primary: custom
36 | accent: 'dark-blue'
37 | toggle:
38 | icon: material/brightness-4
39 | name: Switch to light mode
40 |
41 | plugins:
42 | - search:
43 | lang: en
44 | - offline:
45 | enabled: true
46 |
47 | markdown_extensions:
48 | - pymdownx.inlinehilite
49 | - pymdownx.highlight:
50 | use_pygments: true
51 | anchor_linenums: true
52 | pygments_lang_class: true
53 | auto_title: true
54 | - pymdownx.superfences
55 | - codehilite:
56 | guess_lang: false
57 | - pymdownx.tabbed:
58 | alternate_style: true
59 | - pymdownx.critic
60 | - pymdownx.critic
61 | - admonition
62 | - pymdownx.details
63 | - toc:
64 | permalink: true
65 | - md_in_html
66 | - attr_list
67 | - pymdownx.emoji:
68 | emoji_index: !!python/name:materialx.emoji.twemoji
69 | emoji_generator: !!python/name:materialx.emoji.to_svg
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/di/DeepLinkModule.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.di
2 |
3 | import com.google.firebase.Firebase
4 | import com.google.firebase.dynamiclinks.dynamicLinks
5 | import com.moove.BuildConfig
6 | import com.moove.app.feature.deeplink.data.DeeplinkDataRepository
7 | import com.moove.app.feature.deeplink.data.DynamicLinkDataRepository
8 | import com.moove.app.feature.deeplink.data.local.AppDeepLinkLocalDataSource
9 | import com.moove.app.feature.deeplink.data.remote.FirebaseDynamicLinkDataSource
10 | import com.moove.app.feature.deeplink.presentation.DeepLinkAppNavigator
11 | import com.moove.shared.feature.deeplink.domain.DeeplinkRepository
12 | import com.moove.shared.feature.deeplink.domain.DynamicLinkRepository
13 | import com.moove.shared.feature.deeplink.domain.GetDeeplinkUseCase
14 | import com.moove.shared.feature.deeplink.domain.GetDynamicLinkUseCase
15 | import com.moove.shared.feature.deeplink.presentation.DeepLinkNavigator
16 | import org.koin.dsl.module
17 |
18 | val deepLinkModule = module {
19 |
20 | factory { DeeplinkDataRepository(get()) }
21 | factory { DynamicLinkDataRepository(get()) }
22 | factory {
23 | DeepLinkAppNavigator(
24 | ticketsNavigator = get(),
25 | globalAppNavigator = get(),
26 | )
27 | }
28 | factory {
29 | FirebaseDynamicLinkDataSource(
30 | host = BuildConfig.FIREBASE_DYNAMIC_LINK_HOST,
31 | // firebaseDynamicLinks = Firebase.dynamicLinks
32 | )
33 | }
34 | factory { AppDeepLinkLocalDataSource() }
35 | factory {
36 | GetDeeplinkUseCase(
37 | deeplinkRepository = get(),
38 | getDynamicLinkUseCase = get(),
39 | )
40 | }
41 | factory { GetDynamicLinkUseCase(get()) }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
16 |
19 |
22 |
23 |
24 |
25 |
31 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/presentation/fare/FareListRoute.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.presentation.fare
2 |
3 | import androidx.compose.material.ScaffoldState
4 | import androidx.compose.material.rememberScaffoldState
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.ui.platform.LocalContext
8 | import com.moove.shared.presentation.compose.component.showGenericError
9 | import com.moove.shared.presentation.viewmodel.composableEffect
10 | import com.moove.shared.presentation.viewmodel.composableState
11 | import org.koin.androidx.compose.getViewModel
12 | import org.koin.androidx.compose.koinViewModel
13 | import org.koin.core.parameter.parametersOf
14 |
15 | @Composable
16 | fun FareListRoute(
17 | navigator: FareListNavigator,
18 | ryderId: String,
19 | viewModel: FareListViewModel = koinViewModel { parametersOf(ryderId) },
20 | scaffoldState: ScaffoldState = rememberScaffoldState(),
21 | ) {
22 | val state by viewModel.composableState()
23 |
24 | FareListScreen(
25 | uiState = state,
26 | scaffoldState = scaffoldState,
27 | onFareClick = viewModel::onFareClick,
28 | )
29 |
30 | viewModel.RenderEffect(scaffoldState = scaffoldState, navigator = navigator)
31 | }
32 |
33 | @Composable
34 | private fun FareListViewModel.RenderEffect(
35 | scaffoldState: ScaffoldState,
36 | navigator: FareListNavigator,
37 | ) {
38 | val context = LocalContext.current
39 | composableEffect { effect ->
40 | when (effect) {
41 | is FareListEffect.GoToConfirmation -> {
42 | navigator.goToConfirmation(ryderId = effect.ryderId, fare = effect.fare)
43 | }
44 |
45 | FareListEffect.ShowGenericError -> scaffoldState.showGenericError(context)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/main/MainActivityViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.main
2 |
3 | import android.content.Intent
4 | import androidx.lifecycle.ViewModel
5 | import com.moove.core.exception.ExceptionHandler
6 | import com.moove.core.exception.asCoroutineExceptionHandler
7 | import com.moove.shared.feature.deeplink.domain.GetDeeplinkUseCase
8 | import com.moove.shared.presentation.viewmodel.executeUseCase
9 | import org.orbitmvi.orbit.Container
10 | import org.orbitmvi.orbit.ContainerHost
11 | import org.orbitmvi.orbit.syntax.simple.intent
12 | import org.orbitmvi.orbit.syntax.simple.postSideEffect
13 | import org.orbitmvi.orbit.viewmodel.container
14 |
15 | class MainActivityViewModel(
16 | exceptionHandler: ExceptionHandler,
17 | private val getDeeplinkUseCase: GetDeeplinkUseCase,
18 | ) : ViewModel(), ContainerHost {
19 |
20 | override val container: Container = container(
21 | initialState = MainActivityState(),
22 | buildSettings = {
23 | this.exceptionHandler = exceptionHandler.asCoroutineExceptionHandler()
24 | },
25 | )
26 |
27 | fun handleIntent(intent: Intent?) = intent {
28 | if (intent == null) return@intent
29 |
30 | val uri = getDeepLinkFromIntent(intent)
31 | if (uri.isNullOrEmpty()) return@intent
32 |
33 | executeUseCase { getDeeplinkUseCase(uri) }
34 | .onSuccess { deeplink -> postSideEffect(MainActivityEffect.NavigateDeepLink(deeplink)) }
35 | .onFailure { postSideEffect(MainActivityEffect.ShowGenericError) }
36 | }
37 |
38 | private fun getDeepLinkFromIntent(intent: Intent): String? {
39 | return intent.takeIf {
40 | Intent.ACTION_VIEW == it.action || Intent.ACTION_MAIN == it.action
41 | }?.dataString
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Moove Android App
2 |
3 | ## 🏛 Architecture overview
4 |
5 | ### Modules 🐘
6 |
7 | The app consists of several gradle modules:
8 | - `app` – the main module which setups other modules together, along with the top-level navigation and DI.
9 |
10 | It also contains legacy features implementation that would be extracted in the future;
11 |
12 | - `tickets/featureX` - concrete standalone feature implementation, e.g. search, bookings, etc.;
13 | - `core` - project-agnostic language tools & extensions shared with other modules;
14 | - `shared` - basic components and business rules to re-use across the app and feature modules:
15 | - `features` - shared features which could be reused in the app directly
16 | as the business rules (use cases): _GetSessionUseCase_ or _SubscribeToAccountUseCase_, etc;
17 | - `presentation` - basic presentation-level reusable components;
18 | - `design-system` - common resources: styles, themes, drawables, strings;
19 |
20 | ### Components ❖
21 |
22 | Check all the [dependencies](../gradle/libs.versions.toml)
23 |
24 | #### Core/Language
25 | - the app is written in `Kotlin`;
26 | - `Coroutines` is the only lib used for async operations;
27 | - Java17+ APIs are available via [desugaring](https://developer.android.com/studio/write/java8-support-table);
28 |
29 | #### Domain/Data
30 | Per Context (in terms of DDD) _UseCase->Repository->DataSource_ pattern is implemented with non-base own interfaces.
31 |
32 | #### Presentation
33 | The app is built mainly with `AndroidX` components: Navigation, Fragment and ViewModel.
34 | The ViewModel itself is often set up in a MVI manner with the help of [Orbit-MVI](https://github.com/orbit-mvi/orbit-mvi)
35 |
36 | #### DI
37 | The DI is built with [Koin](https://insert-koin.io/) and fully belongs to the _App_ module.
38 |
39 | #### Testing
40 | We do unit test only for new: JUnit4 is used with the help of `mockk`.
--------------------------------------------------------------------------------
/.github/workflows/code_review.yml:
--------------------------------------------------------------------------------
1 | name: Code review
2 |
3 | concurrency:
4 | group: ${{ github.workflow }}-${{ github.ref }}
5 | cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
6 |
7 | on:
8 | workflow_dispatch:
9 | pull_request:
10 | types: [opened, ready_for_review, synchronize, labeled]
11 | branches:
12 | - master
13 |
14 | jobs:
15 | SetUp:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - id: setVariables
19 | name: Set variables
20 | run: |
21 | isFromMaster=${{ github.ref == 'refs/heads/master' }}
22 | isManual=${{ github.event_name == 'workflow_dispatch' }}
23 |
24 | if [ ${{ github.event_name }} == workflow_dispatch ] || [ ${{ github.event_name }} == push ] || ([ ${{ github.event_name }} == pull_request ] && [ ${{ github.event.pull_request.draft }} == false ]); then
25 | exit 0
26 | else
27 | exit 1
28 | fi
29 |
30 | BuildAndRunTests:
31 | needs: SetUp
32 | runs-on: ubuntu-latest
33 | timeout-minutes: 30
34 | steps:
35 | - uses: actions/checkout@v4
36 | - name: Job set up
37 | uses: ./.github/actions/job-set-up
38 |
39 | - name: Build
40 | run: ./gradlew assembleDebug
41 |
42 | - name: Run unit tests
43 | run: ./gradlew clean testDebugUnitTest
44 |
45 | AllowMerge:
46 | if: always()
47 | runs-on: ubuntu-latest
48 | needs: [ SetUp, BuildAndRunTests ]
49 | steps:
50 | - run: |
51 | if [ ${{ github.event_name }} == pull_request ] && [ ${{ join(github.event.pull_request.labels.*.name) == '' }} == true ]; then
52 | exit 1
53 | elif [ ${{ (contains(needs.Build.result, 'failure')) }} == true ] || [ ${{ (contains(needs.BuildAndRunTests.result, 'failure')) }} ]; then
54 | exit 1
55 | else
56 | exit 0
57 | fi
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Docs
2 |
3 | concurrency:
4 | group: ${{ github.workflow }}-${{ github.ref }}
5 | cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
6 |
7 | on:
8 | workflow_dispatch:
9 | push:
10 | branches:
11 | - master
12 |
13 | permissions:
14 | contents: read
15 | packages: write
16 | pages: write
17 | id-token: write
18 |
19 | jobs:
20 | Deploy:
21 | environment:
22 | name: github-pages
23 | url: ${{ steps.deployment.outputs.page_url }}
24 |
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | - name: Checkout
29 | uses: actions/checkout@v4
30 |
31 | - name: Job set up
32 | uses: ./.github/actions/job-set-up
33 |
34 | - uses: actions/setup-python@v5
35 |
36 | - name: Generate Docs
37 | run: ./.github/workflows/generate_docs.sh
38 |
39 | - name: Upload documentation zip archive
40 | uses: actions/upload-artifact@v4
41 | with:
42 | name: moove-android-docs
43 | path: android-docs/html
44 |
45 | - name: Setup Pages
46 | uses: actions/configure-pages@v4
47 | - name: Upload artifact
48 | uses: actions/upload-pages-artifact@v3
49 | with:
50 | path: './android-docs/html'
51 | - name: Deploy to GitHub Pages
52 | id: deployment
53 | uses: actions/deploy-pages@v4
54 |
55 | - name: Build Docker image
56 | run: docker build . --file android-docs/Dockerfile --tag moove-android-docs:lattest
57 |
58 | - name: Tag image
59 | run: docker tag moove-android-docs:lattest ghcr.io/mkhytarmkhoian/moove/android-docs:latest
60 |
61 | - name: Log in to registry
62 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
63 |
64 | - name: Push image
65 | run: docker push ghcr.io/mkhytarmkhoian/moove/android-docs:latest
--------------------------------------------------------------------------------
/android-docs/docs/index.md:
--------------------------------------------------------------------------------
1 | # Moove Android App
2 |
3 | ## 🏛 Architecture overview
4 |
5 | ### Modules 🐘
6 |
7 | The app consists of several gradle modules:
8 | - `app` – the main module which setups other modules together, along with the top-level navigation and DI.
9 |
10 | It also contains legacy features implementation that would be extracted in the future;
11 |
12 | - `tickets/featureX` - concrete standalone feature implementation, e.g. search, bookings, etc.;
13 | - `core` - project-agnostic language tools & extensions shared with other modules;
14 | - `shared` - basic components and business rules to re-use across the app and feature modules:
15 | - `features` - shared features which could be reused in the app directly
16 | as the business rules (use cases): _GetSessionUseCase_ or _SubscribeToAccountUseCase_, etc;
17 | - `presentation` - basic presentation-level reusable components;
18 | - `design-system` - common resources: styles, themes, drawables, strings;
19 |
20 | ### Components ❖
21 |
22 | Check all the [dependencies](../gradle/libs.versions.toml)
23 |
24 | #### Core/Language
25 | - the app is written in `Kotlin`;
26 | - `Coroutines` is the only lib used for async operations;
27 | - Java17+ APIs are available via [desugaring](https://developer.android.com/studio/write/java8-support-table);
28 |
29 | #### Domain/Data
30 | Per Context (in terms of DDD) _UseCase->Repository->DataSource_ pattern is implemented with non-base own interfaces.
31 |
32 | #### Presentation
33 | The app is built mainly with `AndroidX` components: Navigation, Fragment and ViewModel.
34 | The ViewModel itself is often set up in a MVI manner with the help of [Orbit-MVI](https://github.com/orbit-mvi/orbit-mvi)
35 |
36 | #### DI
37 | The DI is built with [Koin](https://insert-koin.io/) and fully belongs to the _App_ module.
38 |
39 | #### Testing
40 | We do unit test only for new: JUnit4 is used with the help of `mockk`.
--------------------------------------------------------------------------------
/design-system/src/main/java/com/moove/design_system/compose/moove/Colors.kt:
--------------------------------------------------------------------------------
1 | package com.moove.design_system.compose.moove
2 |
3 | import androidx.compose.material.lightColors
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 | import com.moove.design_system.compose.AppColors
7 |
8 | internal val mooveColors: AppColors
9 | @Composable
10 | get() {
11 | val material = lightColors(
12 | primary = MooveColors.FloytGreen,
13 | primaryVariant = MooveColors.Mint,
14 | onPrimary = MooveColors.White,
15 | secondary = MooveColors.FloytGreen,
16 | onSecondary = MooveColors.White,
17 | surface = MooveColors.White,
18 | onSurface = MooveColors.Text,
19 | background = MooveColors.White,
20 | onBackground = MooveColors.Text,
21 | error = MooveColors.AccentRed,
22 | onError = MooveColors.White,
23 | )
24 | return AppColors(
25 | material = material,
26 | neutral = mooveNeutralColors,
27 | buttons = mooveButtonsColors,
28 | )
29 | }
30 |
31 | internal object MooveColors {
32 | val FloytGreen = Color(0xFF00977D)
33 | val Mint = Color(0xFF6EC2B7)
34 | val Mint30 = Color(0xFFD4EDE9)
35 | val Rose = Color(0xFFE4A5A3)
36 | val Orange = Color(0xFFFF4B19)
37 | val AccentGreen = Color(0xFF3C9307)
38 | val AccentYellow = Color(0xFFFFA800)
39 | val AccentRed = Color(0xFFD63F24)
40 | val AccentLightGreen = Color(0xFF75B21B)
41 | val AccentBlue = Color(0xFF3694BA)
42 | val Grey01 = Color(0xFF5A5A5A)
43 | val Grey02 = Color(0xFF909090)
44 | val Grey03 = Color(0xFFC8C8C8)
45 | val Grey04 = Color(0xFFE8E8E8)
46 | val Grey05 = Color(0xFFF3F3F3)
47 | val Black = Color(0xFF000000)
48 | val White = Color(0xFFFFFFFF)
49 | val Text = Color(0xFF222222)
50 | }
51 |
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/di/TicketsModule.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.di
2 |
3 | import com.moove.tickets.data.TicketsDataRepository
4 | import com.moove.tickets.data.local.TicketsLocalDataSource
5 | import com.moove.tickets.domain.TicketsRepository
6 | import com.moove.tickets.domain.use_cases.BuyTicketUseCase
7 | import com.moove.tickets.domain.use_cases.GetFaresByIdUseCase
8 | import com.moove.tickets.domain.use_cases.GetRydersUseCase
9 | import com.moove.tickets.presentation.confirmation.ConfirmationNavigator
10 | import com.moove.tickets.presentation.confirmation.ConfirmationViewModel
11 | import com.moove.tickets.presentation.fare.FareListNavigator
12 | import com.moove.tickets.presentation.fare.FareListViewModel
13 | import com.moove.tickets.presentation.list.RyderListNavigator
14 | import com.moove.tickets.presentation.list.RyderListViewModel
15 | import org.koin.androidx.viewmodel.dsl.viewModel
16 | import org.koin.dsl.module
17 |
18 | val ticketsModule = module {
19 |
20 | // region domain
21 | factory { GetRydersUseCase(get()) }
22 | factory { GetFaresByIdUseCase(get()) }
23 | factory { BuyTicketUseCase(get()) }
24 | // endregion
25 |
26 | // region data
27 | single { TicketsLocalDataSource(get()) }
28 | single { TicketsDataRepository(get()) }
29 | // endregion
30 |
31 | factory { RyderListNavigator(get(), get()) }
32 | viewModel {
33 | RyderListViewModel(
34 | exceptionHandler = get(),
35 | getRydersUseCase = get(),
36 | )
37 | }
38 |
39 | factory { FareListNavigator(get(), get()) }
40 | viewModel {
41 | FareListViewModel(
42 | exceptionHandler = get(),
43 | ryderId = get(),
44 | getFaresByIdUseCase = get(),
45 | )
46 | }
47 |
48 | factory { ConfirmationNavigator(get()) }
49 | viewModel {
50 | ConfirmationViewModel(
51 | exceptionHandler = get(),
52 | ryderId = get(),
53 | fare = get(),
54 | buyTicketUseCase = get(),
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-parcelize'
5 | id 'com.google.devtools.ksp'
6 | id 'androidx.navigation.safeargs.kotlin'
7 | }
8 | apply from: "${rootProject.projectDir}/gradle/base-android-config.gradle"
9 | apply from: "${rootProject.projectDir}/gradle/compose-android-config.gradle"
10 |
11 | android {
12 | namespace 'com.moove'
13 |
14 | defaultConfig {
15 | buildConfigField "String", "FIREBASE_DYNAMIC_LINK_HOST", "\"$firebase_dynamic_link_host\""
16 | manifestPlaceholders = [
17 | firebaseDynamicLinkHost: "\"$firebase_dynamic_link_host\""
18 | ]
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
24 |
25 | // region Project
26 | implementation project(':shared')
27 | implementation project(':tickets')
28 | // endregion
29 |
30 | // region AndroidX
31 | implementation libs.androidx.appcompat
32 | implementation libs.androidx.core
33 | implementation libs.androidx.fragment
34 | implementation libs.bundles.androidx.navigation
35 | implementation libs.bundles.androidx.lifecycle
36 | implementation libs.bundles.androidx.compose
37 | // endregion
38 |
39 | // region Kotlin
40 | implementation libs.kotlin.coroutines.core
41 | implementation libs.kotlin.coroutines.android
42 | implementation libs.kotlin.reflect
43 | // endregion
44 |
45 | // region di
46 | implementation libs.bundles.koin
47 | // endregion
48 |
49 | // region presentation
50 | implementation libs.orbit.viewmodel
51 | testImplementation libs.orbit.test
52 | // endregion
53 |
54 | // region UI
55 | implementation libs.google.material
56 | implementation libs.androidx.splashscreen
57 | implementation libs.androidx.constraintlayout
58 | implementation libs.androidx.swiperefreshlayout
59 | // endregion
60 |
61 | // region Tests
62 | testImplementation libs.bundles.tests.unit
63 | testImplementation libs.bundles.tests.viewmodel
64 | androidTestImplementation libs.bundles.tests.android
65 | // endregion
66 | }
--------------------------------------------------------------------------------
/tickets/src/main/res/navigation/tickets_navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
17 |
20 |
21 |
22 |
23 |
28 |
29 |
32 |
33 |
36 |
39 |
40 |
43 |
44 |
45 |
46 |
51 |
52 |
55 |
56 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/moove/shared/navigation/NavControllerExtension.kt:
--------------------------------------------------------------------------------
1 | package com.moove.shared.navigation
2 |
3 | import android.app.Activity
4 | import androidx.navigation.NavController
5 | import androidx.navigation.NavDirections
6 | import androidx.navigation.NavOptions
7 | import androidx.navigation.navOptions
8 |
9 | fun NavController.navigateSafely(direction: NavDirections, navOptions: NavOptions? = null) {
10 | currentDestination?.getAction(direction.actionId)?.run {
11 | val navOptionsWithDefaults = (navOptions ?: this.navOptions ?: navOptions {}).merge(
12 | navOptions {
13 | anim {
14 | enter = androidx.navigation.ui.R.anim.nav_default_enter_anim
15 | exit = androidx.navigation.ui.R.anim.nav_default_exit_anim
16 | popEnter = androidx.navigation.ui.R.anim.nav_default_pop_enter_anim
17 | popExit = androidx.navigation.ui.R.anim.nav_default_pop_exit_anim
18 | }
19 | }
20 | )
21 | navigate(direction, navOptionsWithDefaults)
22 | }
23 | }
24 |
25 | /** Override only default (non-defined) parameters with new ones */
26 | private fun NavOptions.merge(new: NavOptions) = navOptions {
27 | anim {
28 | enter = if (enterAnim == -1) new.enterAnim else enterAnim
29 | exit = if (exitAnim == -1) new.exitAnim else exitAnim
30 | popEnter = if (popEnterAnim == -1) new.popEnterAnim else popEnter
31 | popExit = if (popExitAnim == -1) new.popExitAnim else popExit
32 | }
33 | this.launchSingleTop = shouldLaunchSingleTop() || new.shouldLaunchSingleTop()
34 | restoreState = shouldRestoreState() || new.shouldRestoreState()
35 | if (this@merge.popUpToId != -1) popUpTo(this@merge.popUpToId) {
36 | this.inclusive = isPopUpToInclusive()
37 | this.saveState = shouldPopUpToSaveState()
38 | } else popUpTo(new.popUpToId) {
39 | this.inclusive = new.isPopUpToInclusive()
40 | this.saveState = new.shouldPopUpToSaveState()
41 | }
42 | }
43 |
44 | fun NavController.goBack(activity: Activity) {
45 | if (popBackStack()) {
46 | return
47 | }
48 | activity.finish()
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/moove/app/feature/home/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.moove.app.feature.home
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.statusBarsPadding
8 | import androidx.compose.material.ScaffoldState
9 | import androidx.compose.material.Text
10 | import androidx.compose.material.rememberScaffoldState
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.tooling.preview.Preview
15 | import com.moove.design_system.compose.AppTheme
16 | import com.moove.design_system.compose.Button
17 | import com.moove.design_system.compose.Scaffold
18 | import com.moove.design_system.compose.Spacing
19 |
20 | @Composable
21 | fun HomeScreen(
22 | uiState: HomeState,
23 | scaffoldState: ScaffoldState = rememberScaffoldState(),
24 | onRyderClick: () -> Unit,
25 | ) {
26 | Scaffold(
27 | modifier = Modifier.statusBarsPadding(),
28 | scaffoldState = scaffoldState,
29 | content = {
30 | Box(
31 | modifier = Modifier.fillMaxSize(),
32 | contentAlignment = Alignment.Center
33 | ) {
34 | Button(
35 | modifier = Modifier
36 | .clickable(onClick = { onRyderClick() })
37 | .padding(horizontal = Spacing.S, vertical = Spacing.S),
38 | onClick = onRyderClick
39 | ) {
40 | Text(
41 | text = "Go to Ryders list",
42 | style = AppTheme.typography.material.h1,
43 | maxLines = 1,
44 | )
45 | }
46 | }
47 | }
48 | )
49 | }
50 |
51 | @Preview(name = "Home Content", showBackground = true)
52 | @Composable
53 | fun PreviewHomeContent() {
54 | AppTheme {
55 | HomeScreen(
56 | uiState = HomeState(),
57 | onRyderClick = {}
58 | )
59 | }
60 | }
--------------------------------------------------------------------------------
/tickets/src/main/java/com/moove/tickets/data/local/TicketsLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.moove.tickets.data.local
2 |
3 | import com.moove.tickets.data.local.dto.RyderDTO
4 | import com.squareup.moshi.JsonAdapter
5 | import com.squareup.moshi.Moshi
6 | import com.squareup.moshi.Types
7 |
8 | class TicketsLocalDataSource(
9 | private val moshi: Moshi
10 | ) {
11 |
12 | // TODO json structure is bad
13 | private val data = "{\n" +
14 | " \"Adult\": {\n" +
15 | " \"fares\": [\n" +
16 | " { \"description\": \"2.5 Hour Ticket\", \"price\": 2.5 },\n" +
17 | " { \"description\": \"1 Day Pass\", \"price\": 5.0 },\n" +
18 | " { \"description\": \"30 Day Pass\", \"price\": 100 }\n" +
19 | " ],\n" +
20 | " \"subtext\": null\n" +
21 | " },\n" +
22 | " \"Child\": {\n" +
23 | " \"fares\": [\n" +
24 | " { \"description\": \"2.5 Hour Ticket\", \"price\": 1.5 },\n" +
25 | " { \"description\": \"1 Day Pass\", \"price\": 2.0 },\n" +
26 | " { \"description\": \"30 Day Pass\", \"price\": 40.0 }\n" +
27 | " ],\n" +
28 | " \"subtext\": \"Ages 8-17\"\n" +
29 | " },\n" +
30 | " \"Senior\": {\n" +
31 | " \"fares\": [\n" +
32 | " { \"description\": \"2.5 Hour Ticket\", \"price\": 1.0 },\n" +
33 | " { \"description\": \"1 Day Pass\", \"price\": 2.0 },\n" +
34 | " { \"description\": \"30 Day Pass\", \"price\": 40.0 }\n" +
35 | " ],\n" +
36 | " \"subtext\": \"Ages 60+\"\n" +
37 | " }\n" +
38 | "}\n"
39 |
40 | fun getData(): Map {
41 | val type = Types.newParameterizedType(
42 | Map::class.java,
43 | String::class.java,
44 | RyderDTO::class.java
45 | )
46 | val adapter: JsonAdapter