├── resources
├── src
│ └── commonMain
│ │ ├── resources
│ │ └── MR
│ │ │ ├── data.txt
│ │ │ ├── fonts
│ │ │ ├── PTSans-Bold.ttf
│ │ │ ├── PTSans-Italic.ttf
│ │ │ └── PTSans-Regular.ttf
│ │ │ └── images
│ │ │ ├── arrow_back_ios.svg
│ │ │ ├── icon_bookmark_fill.svg
│ │ │ ├── icon_chevron_right.svg
│ │ │ ├── arrow_back_default.svg
│ │ │ ├── icon_adidas.svg
│ │ │ ├── icon_bookmark.svg
│ │ │ ├── icon_code_fill.svg
│ │ │ ├── icon_code.svg
│ │ │ ├── icon_plant.svg
│ │ │ ├── icon_pin_location.svg
│ │ │ ├── icon_search.svg
│ │ │ ├── icon_home_fill.svg
│ │ │ ├── icon_explore_fill.svg
│ │ │ ├── icon_home.svg
│ │ │ ├── icon_emoji_error.svg
│ │ │ ├── icon_shipping.svg
│ │ │ ├── icon_explore.svg
│ │ │ ├── icon_cart.svg
│ │ │ ├── icon_about_fill.svg
│ │ │ └── icon_about.svg
│ │ └── kotlin
│ │ └── com
│ │ └── utsman
│ │ └── tokobola
│ │ └── resources
│ │ └── Typealiases.kt
├── resources.podspec
└── build.gradle.kts
├── libraries
├── common
│ ├── src
│ │ ├── iosMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── utsman
│ │ │ │ └── tokobola
│ │ │ │ └── common
│ │ │ │ ├── root
│ │ │ │ └── component
│ │ │ │ ├── ProductTopBar.ios.kt
│ │ │ │ └── NativeView.kt
│ │ ├── androidMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── utsman
│ │ │ │ └── tokobola
│ │ │ │ └── common
│ │ │ │ ├── root
│ │ │ │ └── component
│ │ │ │ ├── ProductTopBar.android.kt
│ │ │ │ └── NativeView.kt
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── common
│ │ │ ├── entity
│ │ │ ├── Cart.kt
│ │ │ ├── Category.kt
│ │ │ ├── CartProduct.kt
│ │ │ ├── Brand.kt
│ │ │ ├── LocationPlace.kt
│ │ │ ├── HomeBanner.kt
│ │ │ ├── Product.kt
│ │ │ └── ThumbnailProduct.kt
│ │ │ ├── component
│ │ │ ├── NativeView.kt
│ │ │ ├── Dimens.kt
│ │ │ ├── ProductPullRefreshIndicator.kt
│ │ │ ├── Visibility.kt
│ │ │ ├── Shimmer.kt
│ │ │ ├── ScaffoldPullRefresh.kt
│ │ │ ├── MapView.kt
│ │ │ ├── TopBar.kt
│ │ │ └── ErrorScreen.kt
│ │ │ └── theme
│ │ │ ├── Colors.kt
│ │ │ ├── Typography.kt
│ │ │ └── Theme.kt
│ └── build.gradle.kts
├── core
│ ├── src
│ │ ├── androidMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── utsman
│ │ │ │ └── tokobola
│ │ │ │ └── core
│ │ │ │ ├── utils
│ │ │ │ ├── CoroutineExtensions.kt
│ │ │ │ ├── AndroidContextProvider.kt
│ │ │ │ └── PlatformUtils.android.kt
│ │ │ │ ├── SynchronizObject.kt
│ │ │ │ ├── Platform.android.kt
│ │ │ │ ├── ViewModel.kt
│ │ │ │ ├── RememberViewModel.android.kt
│ │ │ │ └── ImageLoader.android.kt
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── utsman
│ │ │ │ └── tokobola
│ │ │ │ └── core
│ │ │ │ ├── Platform.kt
│ │ │ │ ├── SynchronizObject.kt
│ │ │ │ ├── utils
│ │ │ │ ├── CoroutineExtensions.kt
│ │ │ │ ├── PlatformUtils.kt
│ │ │ │ ├── StateComposable.kt
│ │ │ │ ├── StateExtensions.kt
│ │ │ │ ├── LocationUtils.kt
│ │ │ │ └── MainExtensions.kt
│ │ │ │ ├── RememberViewModel.kt
│ │ │ │ ├── data
│ │ │ │ ├── ElvisExtensions.kt
│ │ │ │ ├── Paged.kt
│ │ │ │ └── LatLon.kt
│ │ │ │ ├── ImageLoader.kt
│ │ │ │ ├── Greeting.kt
│ │ │ │ ├── ViewModel.kt
│ │ │ │ ├── State.kt
│ │ │ │ ├── SingletonCreator.kt
│ │ │ │ └── navigation
│ │ │ │ ├── Navigation.kt
│ │ │ │ └── ScreenContainer.kt
│ │ └── iosMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── core
│ │ │ ├── utils
│ │ │ ├── CoroutineExtensions.kt
│ │ │ └── PlatformUtils.ios.kt
│ │ │ ├── Platform.ios.kt
│ │ │ ├── SynchronizObject.kt
│ │ │ ├── RememberViewModel.ios.kt
│ │ │ ├── ViewModel.kt
│ │ │ └── ImageLoader.ios.kt
│ └── build.gradle.kts
├── api
│ ├── src
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── api
│ │ │ ├── WebDataSource.kt
│ │ │ ├── LazyWebApi.kt
│ │ │ ├── response
│ │ │ ├── CategoryResponse.kt
│ │ │ ├── BrandResponse.kt
│ │ │ ├── HomeBannerResponse.kt
│ │ │ ├── ThumbnailProductResponse.kt
│ │ │ ├── ProductResponse.kt
│ │ │ ├── MapboxSearchResponse.kt
│ │ │ └── MapboxReverseResponse.kt
│ │ │ ├── WebEndPoint.kt
│ │ │ ├── MapboxWebApi.kt
│ │ │ └── ProductWebApi.kt
│ └── build.gradle.kts
├── database
│ ├── src
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── database
│ │ │ ├── LazyDatabaseRepository.kt
│ │ │ └── data
│ │ │ ├── WishlistRealm.kt
│ │ │ ├── RecentlyViewedRealm.kt
│ │ │ ├── LocationPlaceRealm.kt
│ │ │ └── CartProductRealm.kt
│ └── build.gradle.kts
├── location
│ ├── src
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── utsman
│ │ │ │ └── tokobola
│ │ │ │ └── location
│ │ │ │ ├── LocationExtensions.kt
│ │ │ │ ├── LocationInstanceProvider.kt
│ │ │ │ └── LocationTracker.kt
│ │ ├── iosMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── utsman
│ │ │ │ └── tokobola
│ │ │ │ └── location
│ │ │ │ └── LocationTracker.ios.kt
│ │ └── androidMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── location
│ │ │ └── LocationTracker.android.kt
│ └── build.gradle.kts
└── network
│ ├── src
│ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── utsman
│ │ └── tokobola
│ │ └── network
│ │ ├── response
│ │ ├── BaseResponse.kt
│ │ └── BasePagedResponse.kt
│ │ ├── ApiReducer.kt
│ │ ├── DynamicLookupSerializer.kt
│ │ ├── NetworkSources.kt
│ │ ├── ClientProvider.kt
│ │ ├── AutoPagingAdapter.kt
│ │ └── StateTransformation.kt
│ └── build.gradle.kts
├── doc
├── img.png
├── ss1.png
├── ss2.png
├── ss3.png
├── ss4.png
├── ss5.png
└── ss6.png
├── shared
├── src
│ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ │ └── main.android.kt
│ ├── commonMain
│ │ └── kotlin
│ │ │ ├── tab
│ │ │ ├── CustomTab.kt
│ │ │ ├── WishlistTab.kt
│ │ │ ├── HomeTab.kt
│ │ │ ├── ExploreTab.kt
│ │ │ └── AboutTab.kt
│ │ │ ├── ScreenContainerProvider.kt
│ │ │ └── NavigationProvider.kt
│ └── iosMain
│ │ └── kotlin
│ │ └── main.ios.kt
└── build.gradle.kts
├── readme_images
├── banner.png
├── run_on_android.png
├── target_device.png
├── edit_run_config.png
├── hello_world_ios.png
├── open_project_view.png
├── text_field_added.png
└── android_app_running.png
├── iosApp
├── Configuration
│ └── Config.xcconfig
├── iosApp
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── app-icon-1024.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── iOSApp.swift
│ ├── ContentView.swift
│ └── Info.plist
└── Podfile
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── androidApp
├── src
│ └── androidMain
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.webp
│ │ │ ├── ic_launcher_round.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.webp
│ │ │ ├── ic_launcher_round.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ ├── ic_launcher_round.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ ├── ic_launcher_round.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ ├── ic_launcher_round.webp
│ │ │ └── ic_launcher_foreground.webp
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ └── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── kotlin
│ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ ├── MainApplication.kt
│ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
└── build.gradle.kts
├── cleanup.sh
├── close.sh
├── .gitignore
├── features
├── details
│ ├── src
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── details
│ │ │ ├── ui
│ │ │ ├── product
│ │ │ │ ├── ProductDetailUiConfig.kt
│ │ │ │ └── ProductDetailViewModel.kt
│ │ │ ├── brand
│ │ │ │ └── BrandDetailViewModel.kt
│ │ │ └── category
│ │ │ │ └── CategoryDetailViewModel.kt
│ │ │ ├── domain
│ │ │ ├── BrandDetailUseCase.kt
│ │ │ ├── CategoryDetailUseCase.kt
│ │ │ ├── DetailRepository.kt
│ │ │ └── ProductDetailUseCase.kt
│ │ │ └── DetailInstanceProvider.kt
│ └── build.gradle.kts
├── wishlist
│ ├── src
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── wishlist
│ │ │ ├── ui
│ │ │ ├── WishlistViewModel.kt
│ │ │ └── WishlishCompose.kt
│ │ │ ├── domain
│ │ │ ├── WishlistRepository.kt
│ │ │ └── WishlistUseCase.kt
│ │ │ └── WishlistInstanceProvider.kt
│ └── build.gradle.kts
├── cart
│ ├── src
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── cart
│ │ │ ├── ui
│ │ │ ├── CartUiConfig.kt
│ │ │ ├── LocationPickerViewModel.kt
│ │ │ └── CartViewModel.kt
│ │ │ ├── CartInstanceProvider.kt
│ │ │ └── domain
│ │ │ ├── CartRepository.kt
│ │ │ └── LocationPickerUseCase.kt
│ └── build.gradle.kts
├── explore
│ ├── src
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── utsman
│ │ │ └── tokobola
│ │ │ └── explore
│ │ │ ├── ui
│ │ │ ├── ExploreUiConfig.kt
│ │ │ ├── search
│ │ │ │ └── SearchViewModel.kt
│ │ │ └── explore
│ │ │ │ └── ExploreViewModel.kt
│ │ │ ├── domain
│ │ │ ├── ExploreRepository.kt
│ │ │ └── search
│ │ │ │ └── SearchUseCase.kt
│ │ │ └── ExploreInstanceProvider.kt
│ └── build.gradle.kts
└── home
│ ├── src
│ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── utsman
│ │ └── tokobola
│ │ └── home
│ │ ├── HomeInstanceProvider.kt
│ │ ├── ui
│ │ ├── HomeBrand.kt
│ │ └── HomeViewModel.kt
│ │ └── domain
│ │ ├── HomeRepository.kt
│ │ └── HomeUseCase.kt
│ └── build.gradle.kts
├── cleanup-open.sh
├── gradle.properties
├── README.md
├── settings.gradle.kts
└── gradlew.bat
/resources/src/commonMain/resources/MR/data.txt:
--------------------------------------------------------------------------------
1 | anu
--------------------------------------------------------------------------------
/libraries/common/src/iosMain/kotlin/com/utsman/tokobola/common/root:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/common/src/androidMain/kotlin/com/utsman/tokobola/common/root:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/doc/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/img.png
--------------------------------------------------------------------------------
/doc/ss1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss1.png
--------------------------------------------------------------------------------
/doc/ss2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss2.png
--------------------------------------------------------------------------------
/doc/ss3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss3.png
--------------------------------------------------------------------------------
/doc/ss4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss4.png
--------------------------------------------------------------------------------
/doc/ss5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss5.png
--------------------------------------------------------------------------------
/doc/ss6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss6.png
--------------------------------------------------------------------------------
/shared/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/readme_images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/banner.png
--------------------------------------------------------------------------------
/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=com.utsman.tokobola
3 | APP_NAME=TokoBola
4 |
--------------------------------------------------------------------------------
/readme_images/run_on_android.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/run_on_android.png
--------------------------------------------------------------------------------
/readme_images/target_device.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/target_device.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/readme_images/edit_run_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/edit_run_config.png
--------------------------------------------------------------------------------
/readme_images/hello_world_ios.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/hello_world_ios.png
--------------------------------------------------------------------------------
/readme_images/open_project_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/open_project_view.png
--------------------------------------------------------------------------------
/readme_images/text_field_added.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/text_field_added.png
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | TokoBola
3 |
--------------------------------------------------------------------------------
/readme_images/android_app_running.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/android_app_running.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/utils/CoroutineExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/androidApp/src/androidMain/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/iosApp/Podfile:
--------------------------------------------------------------------------------
1 | target 'iosApp' do
2 | use_frameworks!
3 | platform :ios, '14.1'
4 | pod 'shared', :path => '../shared'
5 | pod 'MapboxMaps', '10.15.0'
6 | end
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/fonts/PTSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/resources/src/commonMain/resources/MR/fonts/PTSans-Bold.ttf
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/fonts/PTSans-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/resources/src/commonMain/resources/MR/fonts/PTSans-Italic.ttf
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/fonts/PTSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/resources/src/commonMain/resources/MR/fonts/PTSans-Regular.ttf
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/main.android.kt:
--------------------------------------------------------------------------------
1 | import androidx.activity.compose.BackHandler
2 | import androidx.compose.runtime.Composable
3 |
4 | @Composable fun MainView() = App()
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/resources/src/commonMain/kotlin/com/utsman/tokobola/resources/Typealiases.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.resources
2 |
3 | import dev.icerock.moko.graphics.Color
4 |
5 | val MokoColor = Color
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #4285F4
4 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/Platform.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | interface Platform {
4 | val name: String
5 | }
6 |
7 | expect fun getPlatform(): Platform
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/arrow_back_ios.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/SynchronizObject.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | expect open class SynchronizObject()
4 |
5 | expect inline fun synchroniz(lock: Any, block: () -> T): T
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/CoroutineExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/libraries/common/src/iosMain/kotlin/com/utsman/tokobola/common/component/ProductTopBar.ios.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 |
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/WebDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api
2 |
3 | import com.utsman.tokobola.network.NetworkSources
4 |
5 | abstract class WebDataSource : NetworkSources(BuildKonfig.BASE_URL)
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/RememberViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | expect fun rememberViewModel(viewModel: () -> T): T
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/data/ElvisExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.data
2 |
3 | fun Int?.orNol(): Int = this ?: 0
4 | fun Double?.orNol(): Double = this ?: 0.0
5 | fun Boolean?.orFalse(): Boolean = this ?: false
--------------------------------------------------------------------------------
/libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/utils/CoroutineExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.IO
6 |
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/Cart.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.entity
2 |
3 | data class Cart(
4 | val product: ThumbnailProduct = ThumbnailProduct(),
5 | var quantity: Int = 0,
6 | var millis: Long
7 | )
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/ImageLoader.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.seiko.imageloader.ImageLoader
5 |
6 | @Composable
7 | expect fun rememberImageLoader(): ImageLoader
--------------------------------------------------------------------------------
/libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/SynchronizObject.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | actual open class SynchronizObject
4 |
5 | actual inline fun synchroniz(lock: Any, block: () -> T): T {
6 | return synchronized(lock, block)
7 | }
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/Greeting.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | class Greeting {
4 | private val platform: Platform = getPlatform()
5 |
6 | fun greet(): String {
7 | return "Hello, ${platform.name}!"
8 | }
9 | }
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_bookmark_fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/data/Paged.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.data
2 |
3 | data class Paged(
4 | var data: List = emptyList(),
5 | var hasNextPage: Boolean = false,
6 | var page: Int = 1,
7 | var perPage: Int = 10
8 | )
--------------------------------------------------------------------------------
/cleanup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | rm -rf .idea
3 | ./gradlew clean
4 | rm -rf .gradle
5 | rm -rf build
6 | rm -rf */build
7 | rm -rf iosApp/iosApp.xcworkspace
8 | rm -rf iosApp/Pods
9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace
10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata
11 |
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/Category.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.entity
2 |
3 |
4 | data class Category(
5 | val id: Int = 0,
6 | val name: String = "",
7 | val description: String = "",
8 | val image: String = ""
9 | )
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/CartProduct.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.entity
2 |
3 | data class CartProduct(
4 | val productId: Int = 0,
5 | val quantity: Int = 0
6 | ) {
7 |
8 | fun isEmpty() = quantity == 0
9 | }
10 |
--------------------------------------------------------------------------------
/close.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | current_dir=$(pwd)
4 |
5 | osascript -e 'tell application "Android Studio" to if it is running then quit'
6 | osascript -e "tell app \"Terminal\"
7 | do script \"cd $current_dir && ./cleanup-open.sh\"
8 | end tell"
9 |
10 | osascript -e 'tell app "Android Studio"'
--------------------------------------------------------------------------------
/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iOSApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ZStack {
8 | Color.white.ignoresSafeArea(.all) // status bar color
9 | ContentView()
10 | }.preferredColorScheme(.light)
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/Brand.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.entity
2 |
3 | data class Brand(
4 | val id: Int = 0,
5 | val name: String = "",
6 | val description: String = "",
7 | val image: String = "",
8 | val logo: String = ""
9 | )
--------------------------------------------------------------------------------
/libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/Platform.android.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | class AndroidPlatform : Platform {
4 | override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
5 | }
6 |
7 | actual fun getPlatform(): Platform = AndroidPlatform()
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_chevron_right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/ViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 |
5 | expect abstract class ViewModel() {
6 | val viewModelScope: CoroutineScope
7 |
8 | protected open fun onCleared()
9 | fun clear()
10 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | build/
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | iosApp/Podfile.lock
11 | iosApp/Pods/*
12 | iosApp/iosApp.xcworkspace/*
13 | iosApp/iosApp.xcodeproj/*
14 | !iosApp/iosApp.xcodeproj/project.pbxproj
15 | shared/shared.podspec
16 |
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/NativeView.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 |
6 | @Composable
7 | expect fun NativeView(modifier: Modifier = Modifier, content: @Composable () -> Unit)
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/LocationPlace.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.entity
2 |
3 | import com.utsman.tokobola.core.data.LatLon
4 |
5 | data class LocationPlace(
6 | val name: String = "",
7 | val latLon: LatLon = LatLon(),
8 | val bbox: String = ""
9 | )
10 |
--------------------------------------------------------------------------------
/libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/LazyDatabaseRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.database
2 |
3 | fun localRepository(): Lazy {
4 | return lazy(
5 | mode = LazyThreadSafetyMode.SYNCHRONIZED
6 | ) {
7 | LocalRepository.providedLocalRepository()
8 | }
9 | }
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/arrow_back_default.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/PlatformUtils.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.unit.Dp
5 |
6 | @Composable
7 | expect fun rememberStatusBarHeightDp(): Dp
8 |
9 | @Composable
10 | expect fun rememberNavigationBarHeightDp(): Dp
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_adidas.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_bookmark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_code_fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/Platform.ios.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import platform.UIKit.UIDevice
4 |
5 | class IOSPlatform: Platform {
6 | override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
7 | }
8 |
9 | actual fun getPlatform(): Platform = IOSPlatform()
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/tab/CustomTab.kt:
--------------------------------------------------------------------------------
1 | package tab
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.graphics.painter.Painter
5 | import cafe.adriel.voyager.navigator.tab.Tab
6 |
7 | internal interface CustomTab : Tab {
8 |
9 | val iconSelected: Painter?
10 | @Composable
11 | get() = options.icon
12 | }
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/ui/product/ProductDetailUiConfig.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details.ui.product
2 |
3 | import androidx.compose.ui.unit.IntSize
4 |
5 | data class ProductDetailUiConfig(
6 | var selectedImageIndex: Int = 0,
7 | var isShowImageDialog: Boolean = false,
8 | var globalSize: IntSize = IntSize.Zero
9 | )
--------------------------------------------------------------------------------
/libraries/location/src/commonMain/kotlin/com/utsman/tokobola/location/LocationExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.location
2 |
3 | import com.utsman.tokobola.core.data.LatLon
4 | import dev.icerock.moko.geo.LatLng
5 |
6 | fun LatLon.isNear(other: LatLon): Boolean {
7 | return LatLng(latitude, longitude).distanceTo(LatLng(other.latitude, other.longitude)) < 30.0
8 | }
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/HomeBanner.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.entity
2 |
3 | data class HomeBanner(
4 | val id: Int = 0,
5 | val productId: Int = 0,
6 | val colorPrimary: String = "#FFFFFF",
7 | val colorAccent: String = "#000000",
8 | val productImage: String = "",
9 | val description: String = ""
10 | )
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_plant.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/State.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | sealed class State {
4 | class Idle : State()
5 | class Loading : State()
6 | data class Success(val data: T) : State()
7 | data class Failure(val exception: Throwable) : State()
8 |
9 | override fun toString(): String {
10 | return "${this::class.simpleName}"
11 | }
12 | }
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_pin_location.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/theme/Colors.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import com.utsman.tokobola.core.utils.parseString
5 |
6 | val ColorPrimaryDark = Color.parseString("#616161")
7 | val ColorPrimaryLight = Color.parseString("#607D8B")
8 |
9 | val ColorSecondaryDark = Color.parseString("#616161")
10 | val ColorSecondaryLight = Color.parseString("#607D8B")
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_home_fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/Product.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.entity
2 |
3 | data class Product(
4 | val category: String = "",
5 | val description: String = "",
6 | val id: Int = 0,
7 | val images: List = emptyList(),
8 | val name: String = "",
9 | val price: Double = 0.0,
10 | val isPromoted: Boolean = false,
11 | val brand: ThumbnailProduct.ThumbnailBrand = ThumbnailProduct.ThumbnailBrand()
12 | )
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_explore_fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/LazyWebApi.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api
2 |
3 | fun productWebApi(): Lazy {
4 | return lazy(
5 | mode = LazyThreadSafetyMode.SYNCHRONIZED
6 | ) {
7 | ProductWebApi.create { ProductWebApi() }
8 | }
9 | }
10 |
11 | fun mapboxWebApi(): Lazy {
12 | return lazy(
13 | mode = LazyThreadSafetyMode.SYNCHRONIZED
14 | ) {
15 | MapboxWebApi.create { MapboxWebApi() }
16 | }
17 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/CategoryResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api.response
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class CategoryResponse(
9 | @SerialName("id")
10 | val id: Int?,
11 | @SerialName("name")
12 | val name: String?,
13 | @SerialName("description")
14 | val description: String?,
15 | @SerialName("image")
16 | val image: String?
17 | )
--------------------------------------------------------------------------------
/libraries/location/src/commonMain/kotlin/com/utsman/tokobola/location/LocationInstanceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.location
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 |
5 | object LocationInstanceProvider {
6 |
7 | fun providedLocationTrackerProvider(): LocationTrackerProvider {
8 | return LocationTrackerProvider.create { LocationTrackerProvider() }
9 | }
10 | }
11 |
12 | val LocalLocationTrackerProvider = compositionLocalOf { error("Not provider") }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/Dimens.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.ui.unit.dp
4 |
5 | object Dimens {
6 |
7 | val PaddingGeneral = 12.dp
8 | val PaddingItem = 6.dp
9 | val HeightTopBarSearch = 112.dp
10 | val HeightTopBarSearchWithTitle = 142.dp
11 | val HeightProductItemGrid = 280.dp
12 | val HeightProductItemGridRectangle = 156.dp
13 | val CornerSize = 12.dp
14 | val ShadowElevation = 42.dp
15 | }
--------------------------------------------------------------------------------
/libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/ViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import androidx.lifecycle.ViewModel as LifecycleViewModel
4 | import androidx.lifecycle.viewModelScope as androidViewModelScope
5 |
6 | actual abstract class ViewModel : LifecycleViewModel() {
7 | actual val viewModelScope = androidViewModelScope
8 |
9 | actual override fun onCleared() {
10 | super.onCleared()
11 | }
12 |
13 | actual fun clear() {
14 | onCleared()
15 | }
16 | }
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/utils/AndroidContextProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 | import android.content.Context
4 | import com.utsman.tokobola.core.SingletonCreator
5 |
6 | interface AndroidContextProvider {
7 |
8 | val context: Context
9 |
10 | companion object : SingletonCreator() {
11 | fun getInstance(): AndroidContextProvider {
12 | return synchronized(this) { instance as AndroidContextProvider }
13 | }
14 |
15 | }
16 | }
--------------------------------------------------------------------------------
/libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/data/WishlistRealm.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.database.data
2 |
3 | import com.utsman.tokobola.core.utils.nowMillis
4 | import io.realm.kotlin.types.RealmObject
5 | import io.realm.kotlin.types.annotations.PrimaryKey
6 | import org.mongodb.kbson.BsonObjectId
7 | import org.mongodb.kbson.ObjectId
8 |
9 | class WishlistRealm : RealmObject {
10 | @PrimaryKey
11 | var _id: ObjectId = BsonObjectId()
12 | var productId: Int = 0
13 | var date: Long = nowMillis()
14 | }
--------------------------------------------------------------------------------
/libraries/location/src/iosMain/kotlin/com/utsman/tokobola/location/LocationTracker.ios.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.location
2 |
3 | import dev.icerock.moko.geo.LocationTracker
4 | import dev.icerock.moko.permissions.ios.PermissionsController
5 | import platform.CoreLocation.kCLLocationAccuracyBest
6 |
7 | internal actual val locationTracker: LocationTracker
8 | get() {
9 | return LocationTracker(
10 | permissionsController = PermissionsController(),
11 | accuracy = kCLLocationAccuracyBest
12 | )
13 | }
--------------------------------------------------------------------------------
/libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/data/RecentlyViewedRealm.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.database.data
2 |
3 | import com.utsman.tokobola.core.utils.nowMillis
4 | import io.realm.kotlin.types.RealmObject
5 | import io.realm.kotlin.types.annotations.PrimaryKey
6 | import org.mongodb.kbson.BsonObjectId
7 | import org.mongodb.kbson.ObjectId
8 |
9 | class RecentlyViewedRealm : RealmObject {
10 | @PrimaryKey
11 | var _id: ObjectId = BsonObjectId()
12 | var productId: Int = 0
13 | var date: Long = nowMillis()
14 | }
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_emoji_error.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/BrandResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api.response
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class BrandResponse(
9 | @SerialName("id")
10 | val id: Int?,
11 | @SerialName("name")
12 | val name: String?,
13 | @SerialName("description")
14 | val description: String?,
15 | @SerialName("image")
16 | val image: String?,
17 | @SerialName("logo")
18 | val logo: String?
19 | )
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_shipping.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/SingletonCreator.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import kotlin.jvm.Volatile
4 |
5 | open class SingletonCreator : SynchronizObject() {
6 |
7 | @Volatile
8 | var instance: T? = null
9 |
10 | fun create(creator: () -> T): T {
11 | synchroniz(this) {
12 | if (instance == null) {
13 | instance = creator()
14 | }
15 | return instance as T
16 | }
17 | }
18 |
19 | fun getInstanceOrThrow(): T = checkNotNull(instance)
20 | }
--------------------------------------------------------------------------------
/libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/SynchronizObject.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import kotlinx.atomicfu.locks.synchronized
4 | import kotlinx.atomicfu.locks.SynchronizedObject as AtomicSynchronizedObject
5 |
6 | actual open class SynchronizObject : AtomicSynchronizedObject()
7 |
8 | actual inline fun synchroniz(lock: Any, block: () -> T): T {
9 | return if (lock is AtomicSynchronizedObject) {
10 | synchronized(lock, block)
11 | } else {
12 | throw IllegalArgumentException("Lock param invalid!")
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/response/BaseResponse.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalSerializationApi::class)
2 |
3 | package com.utsman.tokobola.network.response
4 |
5 |
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.SerialName
8 | import kotlinx.serialization.Serializable
9 |
10 | @Serializable
11 | data class BaseResponse(
12 | @SerialName("data")
13 | var `data`: T?,
14 | @SerialName("message")
15 | var message: String?,
16 | @SerialName("status")
17 | var status: Boolean?
18 | )
--------------------------------------------------------------------------------
/libraries/common/src/iosMain/kotlin/com/utsman/tokobola/common/component/NativeView.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.interop.UIKitView
6 | import androidx.compose.ui.window.ComposeUIViewController
7 |
8 | @Composable
9 | actual fun NativeView(modifier: Modifier, content: @Composable () -> Unit) {
10 | UIKitView(
11 | factory = {
12 | ComposeUIViewController(content).view()
13 | },
14 | modifier = modifier
15 | )
16 | }
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_explore.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/data/LocationPlaceRealm.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.database.data
2 |
3 | import io.realm.kotlin.types.RealmObject
4 | import io.realm.kotlin.types.annotations.PrimaryKey
5 | import org.mongodb.kbson.BsonObjectId
6 | import org.mongodb.kbson.ObjectId
7 |
8 | class LocationPlaceRealm : RealmObject {
9 | @PrimaryKey
10 | var _id: ObjectId = BsonObjectId()
11 | var key: String = ""
12 | var name: String = ""
13 | var latitude: Double = 0.0
14 | var longitude: Double = 0.0
15 | var bbox: String = ""
16 | }
--------------------------------------------------------------------------------
/libraries/location/src/androidMain/kotlin/com/utsman/tokobola/location/LocationTracker.android.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.location
2 |
3 | import com.utsman.tokobola.core.utils.AndroidContextProvider
4 | import dev.icerock.moko.geo.LocationTracker
5 | import dev.icerock.moko.permissions.PermissionsController
6 |
7 | internal actual val locationTracker: LocationTracker
8 | get() {
9 | val contextProvider = AndroidContextProvider.getInstance()
10 | return LocationTracker(
11 | PermissionsController(applicationContext = contextProvider.context)
12 | )
13 | }
--------------------------------------------------------------------------------
/libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/RememberViewModel.ios.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNodeLifecycleCallback
5 | import androidx.compose.runtime.DisposableEffect
6 | import androidx.compose.runtime.remember
7 |
8 | @Composable
9 | actual fun rememberViewModel(viewModel: () -> T): T {
10 | val vm = remember { viewModel.invoke() }
11 | DisposableEffect(vm) {
12 | onDispose {
13 | vm.clear()
14 | }
15 | }
16 |
17 | return vm
18 | }
--------------------------------------------------------------------------------
/features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/ui/WishlistViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.wishlist.ui
2 |
3 | import com.utsman.tokobola.core.ViewModel
4 | import com.utsman.tokobola.wishlist.domain.WishlistUseCase
5 | import kotlinx.coroutines.flow.asStateFlow
6 | import kotlinx.coroutines.launch
7 |
8 | class WishlistViewModel(private val useCase: WishlistUseCase) : ViewModel() {
9 |
10 | val productWishlistState get() = useCase.productWishlistReducer.dataFlow.asStateFlow()
11 |
12 | fun listenProductWishlist() = viewModelScope.launch {
13 | useCase.getWishlist()
14 | }
15 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import shared
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | Main_iosKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea(edges: .bottom) // Compose has own keyboard handler
17 | .ignoresSafeArea()
18 | }
19 | }
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/kotlin/com/utsman/tokobola/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import com.utsman.tokobola.core.utils.AndroidContextProvider
6 |
7 | class MainApplication : Application() {
8 |
9 | override fun onCreate() {
10 | super.onCreate()
11 |
12 | val androidContextProvider = object : AndroidContextProvider {
13 | override val context: Context
14 | get() = this@MainApplication
15 | }
16 |
17 | AndroidContextProvider.create { androidContextProvider }
18 | }
19 | }
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/navigation/Navigation.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.navigation
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import com.utsman.tokobola.core.data.LatLon
5 |
6 | interface Navigation {
7 | fun back(): Boolean
8 |
9 | fun goToDetailProduct(id: Int): Boolean
10 | fun goToDetailCategory(categoryId: Int): Boolean
11 | fun goToDetailBrand(brandId: Int): Boolean
12 | fun goToSearch(): Boolean
13 | fun goToCart(): Boolean
14 | fun goToLocationPicker(latLon: LatLon): Boolean
15 | }
16 |
17 | val LocalNavigation = compositionLocalOf { error("navigation failure") }
--------------------------------------------------------------------------------
/libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/RememberViewModel.android.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.platform.LocalLifecycleOwner
7 |
8 | @Composable
9 | actual fun rememberViewModel(viewModel: () -> T): T {
10 | val lifecycle = LocalLifecycleOwner.current.lifecycle
11 | val vm = remember { viewModel.invoke() }
12 | DisposableEffect(lifecycle) {
13 | onDispose {
14 | vm.clear()
15 | }
16 | }
17 |
18 | return vm
19 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/HomeBannerResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api.response
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class HomeBannerResponse(
9 | @SerialName("product_id")
10 | val productId: Int?,
11 | @SerialName("color_primary")
12 | val colorPrimary: String?,
13 | @SerialName("color_accent")
14 | val colorAccent: String?,
15 | @SerialName("id")
16 | val id: Int?,
17 | @SerialName("product_image")
18 | val productImage: String?,
19 | @SerialName("description")
20 | val description: String?
21 | )
--------------------------------------------------------------------------------
/libraries/common/src/androidMain/kotlin/com/utsman/tokobola/common/component/ProductTopBar.android.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.rounded.ArrowBack
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.ColorFilter
13 | import androidx.compose.ui.unit.dp
14 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/main.ios.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.foundation.layout.fillMaxSize
2 | import androidx.compose.runtime.Composable
3 | import androidx.compose.runtime.DisposableEffect
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.interop.UIKitView
7 | import androidx.compose.ui.window.ComposeUIViewController
8 | import kotlinx.cinterop.useContents
9 | import platform.CoreLocation.CLLocationCoordinate2DMake
10 | import platform.MapKit.MKCoordinateRegionMakeWithDistance
11 | import platform.MapKit.MKMapView
12 | import platform.MapKit.MKPointAnnotation
13 |
14 | fun MainViewController() = ComposeUIViewController { App() }
--------------------------------------------------------------------------------
/features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/domain/WishlistRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.wishlist.domain
2 |
3 | import com.utsman.tokobola.api.productWebApi
4 | import com.utsman.tokobola.core.SingletonCreator
5 | import com.utsman.tokobola.database.localRepository
6 |
7 | class WishlistRepository {
8 |
9 | private val productWebApi by productWebApi()
10 | private val localRepository by localRepository()
11 |
12 | suspend fun getThumbnailByIds(ids: List) = productWebApi.getThumbnailByIds(ids)
13 |
14 | suspend fun getAllWishlist() = localRepository.selectAllWishlist()
15 |
16 | companion object : SingletonCreator()
17 | }
--------------------------------------------------------------------------------
/features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/ui/CartUiConfig.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.cart.ui
2 |
3 | import com.utsman.tokobola.common.entity.Cart
4 | import com.utsman.tokobola.core.utils.nowMillis
5 |
6 | data class CartUiConfig(
7 | var carts: List = emptyList(),
8 | val time: Long = nowMillis()
9 | ) {
10 | fun amount(): Double {
11 | return carts.sumOf { it.product.price * it.quantity }
12 | }
13 |
14 | fun isCartEmpty(): Boolean {
15 | return amount() <= 0.0
16 | }
17 |
18 | companion object {
19 | const val KEY_LOCATION_CURRENT = "current_location"
20 | const val KEY_LOCATION_SHIPPING = "shipping_location"
21 | }
22 | }
--------------------------------------------------------------------------------
/libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/data/CartProductRealm.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.database.data
2 |
3 | import com.utsman.tokobola.core.utils.nowMillis
4 | import io.realm.kotlin.types.RealmObject
5 | import io.realm.kotlin.types.annotations.PrimaryKey
6 | import org.mongodb.kbson.BsonObjectId
7 | import org.mongodb.kbson.ObjectId
8 |
9 | class CartProductRealm : RealmObject {
10 | @PrimaryKey
11 | var _id: ObjectId = BsonObjectId()
12 | var productId: Int = 0
13 | var quantity: Int = 0
14 |
15 | var millis = nowMillis()
16 |
17 | override fun toString(): String {
18 | return "[_id: $_id, productId: $productId, quantity: $quantity]"
19 | }
20 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/ThumbnailProduct.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.entity
2 |
3 | data class ThumbnailProduct(
4 | val id: Int = 0,
5 | val name: String = "",
6 | val price: Double = 0.0,
7 | val category: ThumbnailCategory = ThumbnailCategory(),
8 | val brand: ThumbnailBrand = ThumbnailBrand(),
9 | val image: String = "",
10 | val promoted: Boolean = false
11 | ) {
12 | data class ThumbnailBrand(
13 | val id: Int = 0,
14 | val name: String = "",
15 | val logo: String = ""
16 | )
17 |
18 | data class ThumbnailCategory(
19 | val id: Int = 0,
20 | val name: String = "",
21 | )
22 |
23 | }
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/StateComposable.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.utsman.tokobola.core.State
5 |
6 | @Composable
7 | fun State.onLoadingComposed(content: @Composable () -> Unit) {
8 | if (this is State.Loading) { content.invoke() }
9 | }
10 |
11 | @Composable
12 | fun State.onSuccessComposed(content: @Composable (T) -> Unit) {
13 | if (this is State.Success) { content.invoke(this.data) }
14 | }
15 |
16 | @Composable
17 | fun State.onFailureComposed(content: @Composable (Throwable) -> Unit) {
18 | if (this is State.Failure) { content.invoke(this.exception) }
19 | }
--------------------------------------------------------------------------------
/features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/ui/ExploreUiConfig.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.explore.ui
2 |
3 | import com.utsman.tokobola.common.entity.Brand
4 | import com.utsman.tokobola.common.entity.Category
5 | import com.utsman.tokobola.common.entity.ThumbnailProduct
6 | import com.utsman.tokobola.core.State
7 |
8 | data class ExploreUiConfig(
9 | val offsetTabCategory: Float = 0f,
10 | val heightTabCategory: Int = 0,
11 | val selectedTabCategoryIndex: Int = 0,
12 | val selectedTabBrandIndex: Int = 0,
13 | val selectedCategory: Category = Category(id = 1),
14 | val selectedBrand: Brand = Brand(id = 1),
15 | val selectedProductCategory: State> = State.Loading()
16 | )
--------------------------------------------------------------------------------
/cleanup-open.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | rm -rf .idea
3 | ./gradlew clean
4 | rm -rf .gradle
5 | rm -rf build
6 | rm -rf */build
7 | rm -rf iosApp/iosApp.xcworkspace
8 | rm -rf iosApp/Pods
9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace
10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata
11 |
12 | echo -en "> Reopen Android Studio when not running on 5 second.."
13 | sleep 2
14 | echo -en "\r> Reopen Android Studio when not running on 4 second.."
15 | sleep 2
16 | echo -en "\r> Reopen Android Studio when not running on 3 second.."
17 | sleep 2
18 | echo -en "\r> Reopen Android Studio when not running on 2 second.."
19 | sleep 2
20 | echo -en "\r> Reopen Android Studio when not running on 1 second.."
21 | sleep 2
22 |
23 | open -a /Applications/Android\ Studio.app
--------------------------------------------------------------------------------
/libraries/common/src/androidMain/kotlin/com/utsman/tokobola/common/component/NativeView.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.platform.ComposeView
6 | import androidx.compose.ui.platform.LocalContext
7 | import androidx.compose.ui.viewinterop.AndroidView
8 |
9 | @Composable
10 | actual fun NativeView(modifier: Modifier, content: @Composable () -> Unit) {
11 | val context = LocalContext.current
12 | AndroidView(
13 | factory = {
14 | ComposeView(context).apply {
15 | setContent { content.invoke() }
16 | }
17 | },
18 | modifier = modifier
19 | )
20 | }
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/data/LatLon.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.data
2 |
3 | import kotlinx.serialization.json.Json
4 | import kotlinx.serialization.json.decodeFromJsonElement
5 |
6 | data class LatLon(
7 | val latitude: Double = 0.0,
8 | val longitude: Double = 0.0
9 | ) {
10 | fun json(): String {
11 | return "{\"latitude\": \"$latitude\", \"longitude\":\"$longitude\"}"
12 | }
13 |
14 | fun isBlank(): Boolean {
15 | return latitude == 0.0
16 | }
17 |
18 | companion object {
19 | fun fromJson(json: String): LatLon {
20 | val element = Json.parseToJsonElement(json)
21 | return Json.decodeFromJsonElement(element)
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/ViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.IO
6 | import kotlinx.coroutines.SupervisorJob
7 | import kotlinx.coroutines.cancel
8 | import kotlin.coroutines.CoroutineContext
9 |
10 | actual abstract class ViewModel {
11 | actual val viewModelScope: CoroutineScope = object : CoroutineScope {
12 | override val coroutineContext: CoroutineContext
13 | get() = SupervisorJob() + Dispatchers.IO
14 | }
15 |
16 | protected actual open fun onCleared() {
17 | viewModelScope.cancel()
18 | }
19 |
20 | actual fun clear() {
21 | onCleared()
22 | }
23 | }
--------------------------------------------------------------------------------
/features/home/src/commonMain/kotlin/com/utsman/tokobola/home/HomeInstanceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.home
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import com.utsman.tokobola.core.SynchronizObject
5 | import com.utsman.tokobola.core.synchroniz
6 | import com.utsman.tokobola.home.domain.HomeRepository
7 | import com.utsman.tokobola.home.domain.HomeUseCase
8 | import kotlin.jvm.Volatile
9 | import kotlin.native.concurrent.ThreadLocal
10 |
11 | object HomeInstanceProvider {
12 |
13 | fun providedUseCase(): HomeUseCase {
14 | val repo = HomeRepository.create { HomeRepository() }
15 | return HomeUseCase.create { HomeUseCase(repo) }
16 | }
17 | }
18 |
19 | val LocalHomeUseCase = compositionLocalOf { error("Not Provided") }
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_cart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_about_fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Gradle
2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
3 |
4 | #Kotlin
5 | kotlin.code.style=official
6 |
7 | #MPP
8 | kotlin.mpp.stability.nowarn=true
9 | kotlin.mpp.enableCInteropCommonization=true
10 | kotlin.mpp.androidSourceSetLayoutVersion=2
11 |
12 | #Compose
13 | org.jetbrains.compose.experimental.uikit.enabled=true
14 | kotlin.native.cacheKind=none
15 |
16 | #Android
17 | android.useAndroidX=true
18 | android.compileSdk=33
19 | android.targetSdk=33
20 | android.minSdk=24
21 |
22 | #Versions
23 | kotlin.version=1.8.20
24 | agp.version=7.4.2
25 | compose.version=1.4.1
26 |
27 | #public properties
28 | base.url=https://footballstore.fly.dev/api
29 | mapbox.base.url=https://api.mapbox.com
30 | mapbox.token=sk.eyJ1Ijoia3VjaW5nYXBlcyIsImEiOiJjbGxmdnVtbGIwemdqM2txaHZtam5teGxiIn0.4AlEjvqBWJgaYOElmi5bxA
--------------------------------------------------------------------------------
/libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/response/BasePagedResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.network.response
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class BasePagedResponse(
8 | @SerialName("data")
9 | var `data`: DataResponse?,
10 | @SerialName("message")
11 | var message: String?,
12 | @SerialName("status")
13 | var status: Boolean?
14 | ) {
15 | @Serializable
16 | data class DataResponse(
17 | @SerialName("data")
18 | var `data`: List = emptyList(),
19 | @SerialName("has_next_page")
20 | var hasNextPage: Boolean = false,
21 | @SerialName("page")
22 | var page: Int = 1,
23 | @SerialName("per_page")
24 | var perPage: Int = 10
25 | )
26 | }
--------------------------------------------------------------------------------
/resources/src/commonMain/resources/MR/images/icon_about.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/ProductPullRefreshIndicator.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.foundation.layout.offset
4 | import androidx.compose.material.ExperimentalMaterialApi
5 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
6 | import androidx.compose.material.pullrefresh.PullRefreshState
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import com.utsman.tokobola.core.utils.rememberStatusBarHeightDp
10 |
11 | @OptIn(ExperimentalMaterialApi::class)
12 | @Composable
13 | fun PullRefreshIndicatorOffset(
14 | refreshing: Boolean,
15 | state: PullRefreshState,
16 | modifier: Modifier = Modifier
17 | ) {
18 | val offset = rememberStatusBarHeightDp()
19 | PullRefreshIndicator(
20 | refreshing, state, modifier.offset(y = offset)
21 | )
22 | }
--------------------------------------------------------------------------------
/features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/WishlistInstanceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.wishlist
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import com.utsman.tokobola.core.SynchronizObject
5 | import com.utsman.tokobola.core.synchroniz
6 | import com.utsman.tokobola.wishlist.domain.WishlistRepository
7 | import com.utsman.tokobola.wishlist.domain.WishlistUseCase
8 | import kotlin.jvm.Volatile
9 | import kotlin.native.concurrent.ThreadLocal
10 |
11 | object WishlistInstanceProvider {
12 | private fun getRepository(): WishlistRepository {
13 | return WishlistRepository.create { WishlistRepository() }
14 | }
15 |
16 | fun providedUseCase(): WishlistUseCase {
17 | return WishlistUseCase.create { WishlistUseCase(getRepository()) }
18 | }
19 | }
20 |
21 | val LocalWishlistUseCase = compositionLocalOf { error("Not provided") }
--------------------------------------------------------------------------------
/androidApp/src/androidMain/kotlin/com/utsman/tokobola/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola
2 |
3 | import MainView
4 | import android.os.Bundle
5 | import androidx.activity.compose.setContent
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.core.view.WindowCompat
9 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
10 |
11 | class MainActivity : AppCompatActivity() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | val currentWindow = window
15 | WindowCompat.setDecorFitsSystemWindows(currentWindow, false)
16 |
17 | setContent {
18 | val systemUiController = rememberSystemUiController()
19 | systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true)
20 | MainView()
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/ApiReducer.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.network
2 |
3 | import com.utsman.tokobola.core.State
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 |
6 | open class ApiReducer {
7 |
8 | val dataFlow: MutableStateFlow> = MutableStateFlow(State.Idle())
9 |
10 | suspend inline fun transform(
11 | transformation: StateTransformation = StateTransformation.DefaultResponseTransform(),
12 | noinline call: suspend () -> U,
13 | noinline mapper: (U) -> T
14 | ) {
15 | dataFlow.value = State.Loading()
16 | val result = transformation.transform(call, mapper)
17 | dataFlow.value = result
18 | }
19 |
20 | fun forcePushState(state: State) {
21 | dataFlow.value = state
22 | }
23 |
24 | fun clear() {
25 | dataFlow.value = State.Idle()
26 | }
27 | }
--------------------------------------------------------------------------------
/libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/utils/PlatformUtils.ios.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.unit.Dp
6 | import androidx.compose.ui.unit.dp
7 | import kotlinx.cinterop.useContents
8 | import platform.UIKit.UIApplication
9 |
10 | @Composable
11 | actual fun rememberStatusBarHeightDp(): Dp {
12 | return remember {
13 | val currentHeight = UIApplication.sharedApplication.statusBarFrame.useContents {
14 | this.size.height
15 | }
16 | val statusBarHeight = if (currentHeight > 0) {
17 | currentHeight.toInt()
18 | } else {
19 | 0
20 | }
21 | statusBarHeight
22 | }.dp
23 | }
24 |
25 |
26 | @Composable
27 | actual fun rememberNavigationBarHeightDp(): Dp {
28 | return remember {
29 | 30
30 | }.dp
31 | }
--------------------------------------------------------------------------------
/features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/domain/ExploreRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.explore.domain
2 |
3 | import com.utsman.tokobola.api.productWebApi
4 | import com.utsman.tokobola.core.SingletonCreator
5 |
6 | class ExploreRepository {
7 | private val productApi by productWebApi()
8 |
9 | suspend fun getBrand() = productApi.getBrand()
10 | suspend fun getCategory() = productApi.getCategory()
11 | suspend fun getProductBrand(brandId: Int, page: Int) = productApi.getByBrandPaged(brandId, page)
12 | suspend fun getProductCategory(categoryId: Int, page: Int) = productApi.getByCategoryPaged(categoryId, page)
13 | suspend fun getProductTop() = productApi.getTop()
14 | suspend fun getProductCurated() = productApi.getCurated()
15 |
16 | suspend fun getProductBySearch(page: Int, query: String) = productApi.getBySearch(page, query)
17 |
18 | companion object : SingletonCreator()
19 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/Visibility.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.AnimatedVisibilityScope
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 |
11 | @Composable
12 | fun DefaultAnimatedVisibility(
13 | isVisible: Boolean,
14 | modifier: Modifier = Modifier,
15 | duration: Int = 200,
16 | content: @Composable AnimatedVisibilityScope.() -> Unit
17 | ) {
18 | AnimatedVisibility(
19 | visible = isVisible,
20 | enter = fadeIn(animationSpec = tween(duration)),
21 | exit = fadeOut(animationSpec = tween(duration)),
22 | modifier = modifier,
23 | content = content
24 | )
25 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/Shimmer.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.RectangleShape
12 | import androidx.compose.ui.unit.Dp
13 | import androidx.compose.ui.unit.dp
14 |
15 | @Composable
16 | fun Shimmer(modifier: Modifier = Modifier.height(320.dp)) {
17 | Box(
18 | modifier = modifier
19 | .fillMaxWidth()
20 | .padding(6.dp)
21 | .shimmerBackground(shape = RoundedCornerShape(Dimens.CornerSize))
22 | )
23 | }
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/StateExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 | import com.utsman.tokobola.core.State
4 |
5 | fun State.onLoading(content: () -> Unit) {
6 | if (this is State.Loading) {
7 | content.invoke()
8 | }
9 | }
10 |
11 | fun State.onSuccess(content: (T) -> Unit) {
12 | if (this is State.Success) {
13 | content.invoke(this.data)
14 | }
15 | }
16 |
17 | fun State.onFailure(content: (Throwable) -> Unit) {
18 | if (this is State.Failure) {
19 | content.invoke(this.exception)
20 | }
21 | }
22 |
23 | fun State.onIdle(content: () -> Unit) {
24 | if (this is State.Idle) {
25 | content.invoke()
26 | }
27 | }
28 |
29 | fun State.getOrNull(): T? {
30 | return when (this) {
31 | is State.Success -> {
32 | data
33 | }
34 |
35 | else -> {
36 | null
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/theme/Typography.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.text.font.FontFamily
6 | import com.utsman.tokobola.resources.SharedRes
7 | import dev.icerock.moko.resources.compose.asFont
8 |
9 | @Composable
10 | fun Type(): Typography {
11 | val regularRes = SharedRes.fonts.PTSans.regular
12 | val italicRes = SharedRes.fonts.PTSans.italic
13 | val boldRes = SharedRes.fonts.PTSans.bold
14 |
15 | val regularFont = regularRes.asFont()
16 | val italicFont = italicRes.asFont()
17 | val boldFont = boldRes.asFont()
18 |
19 | val defaultFontFamily = if (regularFont != null && italicFont != null && boldFont != null) {
20 | FontFamily(regularFont, italicFont, boldFont)
21 | } else {
22 | FontFamily.Default
23 | }
24 |
25 | return Typography(
26 | defaultFontFamily = defaultFontFamily
27 | )
28 | }
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/ui/brand/BrandDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details.ui.brand
2 |
3 | import com.utsman.tokobola.common.entity.ThumbnailProduct
4 | import com.utsman.tokobola.core.ViewModel
5 | import com.utsman.tokobola.details.domain.BrandDetailUseCase
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.launch
8 |
9 | class BrandDetailViewModel(private val useCase: BrandDetailUseCase) : ViewModel() {
10 |
11 | val productListState get() = useCase.productPagedReducer.dataFlow
12 | val productListFlow = MutableStateFlow(emptyList())
13 | val brandTitle = MutableStateFlow("")
14 |
15 | fun getProduct(brandId: Int) = viewModelScope.launch {
16 | useCase.getProduct(brandId)
17 | }
18 |
19 | fun pushProductList(productList: List) = viewModelScope.launch {
20 | productListFlow.value = productList
21 | brandTitle.value = productList.firstOrNull()?.brand?.name.orEmpty()
22 | }
23 |
24 | fun clearData() {
25 | useCase.clear()
26 | }
27 | }
--------------------------------------------------------------------------------
/libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/utils/PlatformUtils.android.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 | import androidx.compose.foundation.layout.WindowInsets
3 | import androidx.compose.foundation.layout.asPaddingValues
4 | import androidx.compose.foundation.layout.navigationBars
5 | import androidx.compose.foundation.layout.statusBars
6 | import androidx.compose.foundation.layout.systemBars
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.platform.LocalContext
10 | import androidx.compose.ui.unit.Density
11 | import androidx.compose.ui.unit.Dp
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | actual fun rememberStatusBarHeightDp(): Dp {
16 | val statusBarPadding = WindowInsets.statusBars.asPaddingValues()
17 | return statusBarPadding.calculateTopPadding().value.toInt().dp
18 | }
19 |
20 | @Composable
21 | actual fun rememberNavigationBarHeightDp(): Dp {
22 | val navBarPadding = WindowInsets.navigationBars.asPaddingValues()
23 | return navBarPadding.calculateBottomPadding().value.toInt().dp
24 | }
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/LocationUtils.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.utils
2 |
3 | import com.utsman.tokobola.core.data.LatLon
4 | import kotlin.math.cos
5 |
6 | fun calculateBboxMapbox(center: LatLon, distanceKm: Double): String {
7 | val latRadian = center.latitude.radian()
8 | val lonRadian = center.longitude.radian()
9 | val latOffset = distanceKm / EARTH_RADIUS
10 |
11 | // Calculate latitude range
12 | val minLat =(latRadian - latOffset).degrees()
13 | val maxLat = (latRadian + latOffset).degrees()
14 |
15 | // Calculate longitude range
16 | val lonOffset = distanceKm / (EARTH_RADIUS * cos(latRadian))
17 | val minLon = (lonRadian - lonOffset).degrees()
18 | val maxLon = (lonRadian + lonOffset).degrees()
19 |
20 | return "$minLon,$minLat,$maxLon,$maxLat"
21 | }
22 |
23 | private fun Double.radian(): Double {
24 | return this * PI / 180.0
25 | }
26 |
27 | private fun Double.degrees(): Double {
28 | return this * 180.0 / PI
29 | }
30 |
31 | private const val PI = 3.14159265358979323846
32 | private const val EARTH_RADIUS = 6371.0
33 |
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/ui/category/CategoryDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details.ui.category
2 |
3 | import com.utsman.tokobola.common.entity.ThumbnailProduct
4 | import com.utsman.tokobola.core.ViewModel
5 | import com.utsman.tokobola.details.domain.CategoryDetailUseCase
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.launch
8 |
9 | class CategoryDetailViewModel(private val useCase: CategoryDetailUseCase) : ViewModel() {
10 |
11 | val productListState get() = useCase.productPagedReducer.dataFlow
12 | val productListFlow = MutableStateFlow(emptyList())
13 | val categoryTitle = MutableStateFlow("")
14 |
15 | fun getProduct(categoryId: Int) = viewModelScope.launch {
16 | useCase.getProduct(categoryId)
17 | }
18 |
19 | fun pushProductList(productList: List) = viewModelScope.launch {
20 | productListFlow.value = productList
21 | categoryTitle.value = productList.firstOrNull()?.category?.name.orEmpty()
22 | }
23 |
24 | fun clearData() {
25 | useCase.clear()
26 | }
27 | }
--------------------------------------------------------------------------------
/features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/CartInstanceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.cart
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import com.utsman.tokobola.cart.domain.CartRepository
5 | import com.utsman.tokobola.cart.domain.CartUseCase
6 | import com.utsman.tokobola.cart.domain.LocationPickerUseCase
7 | import com.utsman.tokobola.location.LocationTrackerProvider
8 |
9 | object CartInstanceProvider {
10 |
11 | private fun getRepository(): CartRepository {
12 | return CartRepository.create { CartRepository() }
13 | }
14 |
15 | fun providedCartUseCase(locationTrackerProvider: LocationTrackerProvider): CartUseCase {
16 | return CartUseCase.create { CartUseCase(getRepository(), locationTrackerProvider) }
17 | }
18 |
19 | fun providedLocationPickerUseCase(): LocationPickerUseCase {
20 | return LocationPickerUseCase.create { LocationPickerUseCase(getRepository()) }
21 | }
22 | }
23 |
24 | val LocalCartUseCase = compositionLocalOf { error("Not provided") }
25 | val LocalLocationPickerUseCase = compositionLocalOf { error("Not provided") }
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/domain/BrandDetailUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details.domain
2 |
3 | import com.utsman.tokobola.common.entity.ThumbnailProduct
4 | import com.utsman.tokobola.common.toThumbnailProduct
5 | import com.utsman.tokobola.core.SingletonCreator
6 | import com.utsman.tokobola.core.data.Paged
7 | import com.utsman.tokobola.network.ApiReducer
8 | import com.utsman.tokobola.network.AutoPagingAdapter
9 |
10 | class BrandDetailUseCase(private val detailRepository: DetailRepository) {
11 |
12 | val productPagedReducer = ApiReducer>()
13 | private val productPagedAdapter = AutoPagingAdapter(productPagedReducer)
14 |
15 | suspend fun getProduct(brandId: Int) {
16 | productPagedAdapter.executeResponse(
17 | call = { page ->
18 | detailRepository.getProductBrand(brandId, page)
19 | },
20 | mapper = {
21 | it.toThumbnailProduct()
22 | }
23 | )
24 | }
25 |
26 | fun clear() = productPagedAdapter.clear()
27 |
28 | companion object : SingletonCreator()
29 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/ThumbnailProductResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api.response
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class ThumbnailProductResponse(
8 | @SerialName("id")
9 | val id: Int?,
10 | @SerialName("name")
11 | val name: String?,
12 | @SerialName("price")
13 | val price: Double?,
14 | @SerialName("brand")
15 | val brand: BrandResponse?,
16 | @SerialName("image")
17 | val image: String?,
18 | @SerialName("category")
19 | val category: CategoryResponse?,
20 | @SerialName("promoted")
21 | val promoted: Boolean?
22 | ) {
23 | @Serializable
24 | data class BrandResponse(
25 | @SerialName("id")
26 | val id: Int?,
27 | @SerialName("name")
28 | val name: String?,
29 | @SerialName("logo")
30 | val logo: String?
31 | )
32 |
33 | @Serializable
34 | data class CategoryResponse(
35 | @SerialName("id")
36 | val id: Int?,
37 | @SerialName("name")
38 | val name: String?
39 | )
40 |
41 | }
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/domain/CategoryDetailUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details.domain
2 |
3 | import com.utsman.tokobola.common.entity.ThumbnailProduct
4 | import com.utsman.tokobola.common.toThumbnailProduct
5 | import com.utsman.tokobola.core.SingletonCreator
6 | import com.utsman.tokobola.core.data.Paged
7 | import com.utsman.tokobola.network.ApiReducer
8 | import com.utsman.tokobola.network.AutoPagingAdapter
9 |
10 | class CategoryDetailUseCase(private val detailRepository: DetailRepository) {
11 |
12 | val productPagedReducer = ApiReducer>()
13 | private val productPagedAdapter = AutoPagingAdapter(productPagedReducer)
14 |
15 | suspend fun getProduct(categoryId: Int) {
16 | productPagedAdapter.executeResponse(
17 | call = { page ->
18 | detailRepository.getProductCategory(categoryId, page)
19 | },
20 | mapper = {
21 | it.toThumbnailProduct()
22 | }
23 | )
24 | }
25 |
26 | fun clear() = productPagedAdapter.clear()
27 |
28 | companion object : SingletonCreator()
29 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/WebEndPoint.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api
2 |
3 | object WebEndPoint {
4 |
5 | const val PRODUCT_FEATURED = "/v2/product/featured?page={page}"
6 | const val PRODUCT_BRAND = "/v2/product/brand/{brand_id}?page={page}"
7 | const val PRODUCT_CATEGORY = "/v2/product/category/{category_id}?page={page}"
8 | const val PRODUCT_SEARCH = "/v2/product/search?q={query}&page={page}"
9 | const val PRODUCT_THUMBNAIL = "/v2/product/thumbnail?id={id}"
10 | const val PRODUCT_TOP = "/v2/product/top"
11 | const val PRODUCT_CURATED = "/v2/product/curated"
12 | const val PRODUCT_DETAIL = "/v2/product/{product_id}"
13 | const val BANNER = "/v2/product/banner"
14 | const val BRAND = "/brand"
15 | const val CATEGORY = "/category"
16 |
17 | const val MAPBOX_GEOCODING = "/geocoding/v5/mapbox.places/{lon},{lat}.json?access_token={mapbox_access_token}"
18 | const val MAPBOX_SEARCH = "/geocoding/v5/mapbox.places/{q}.json?access_token={mapbox_access_token}&proximity={proximity}"
19 | }
20 |
21 | internal fun String.withParam(key: String, value: Any): String {
22 | return replace("{$key}", "$value")
23 | }
--------------------------------------------------------------------------------
/features/home/src/commonMain/kotlin/com/utsman/tokobola/home/ui/HomeBrand.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.home.ui
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.MaterialTheme
13 | import androidx.compose.material.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.clip
18 | import androidx.compose.ui.text.style.TextAlign
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 | import com.seiko.imageloader.rememberImagePainter
22 | import com.utsman.tokobola.common.component.Dimens
23 | import com.utsman.tokobola.common.component.shimmerBackground
24 | import com.utsman.tokobola.common.entity.Brand
25 |
26 |
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/navigation/ScreenContainer.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.compositionLocalOf
5 | import cafe.adriel.voyager.core.screen.Screen
6 | import cafe.adriel.voyager.core.screen.ScreenKey
7 | import com.utsman.tokobola.core.data.LatLon
8 |
9 | interface ScreenContainer {
10 |
11 | fun home(): Screen
12 | fun detailProduct(productId: Int): Screen
13 | fun detailCategory(categoryId: Int): Screen
14 | fun detailBrand(brandId: Int): Screen
15 | fun explore(): Screen
16 | fun wishlist(): Screen
17 | fun search(): Screen
18 | fun cart(): Screen
19 | fun locationPicker(latLon: LatLon): Screen
20 |
21 | }
22 |
23 | val LocalScreenContainer = compositionLocalOf { error("screen container not found") }
24 |
25 | fun screenContentOf(key: String? = null, content: @Composable () -> Unit): Screen {
26 | return object : Screen {
27 | override val key: ScreenKey = key ?: super.key
28 |
29 | @Composable
30 | override fun Content() {
31 | content.invoke()
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/libraries/database/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | kotlin("plugin.serialization") version "1.8.21"
6 | id("io.realm.kotlin") version "1.10.0"
7 | }
8 |
9 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
10 | kotlin {
11 | targetHierarchy.default()
12 |
13 | android {
14 | compilations.all {
15 | kotlinOptions {
16 | jvmTarget = "1.8"
17 | }
18 | }
19 | }
20 |
21 | listOf(
22 | iosX64(),
23 | iosArm64(),
24 | iosSimulatorArm64()
25 | ).forEach {
26 | it.binaries.framework {
27 | baseName = "database"
28 | }
29 | }
30 |
31 | sourceSets {
32 | val commonMain by getting {
33 | dependencies {
34 | implementation(project(":libraries:core"))
35 | api("io.realm.kotlin:library-base:1.10.0")
36 | }
37 | }
38 | }
39 | }
40 |
41 | android {
42 | namespace = "com.utsman.tokobola.database"
43 | compileSdk = 33
44 | defaultConfig {
45 | minSdk = 24
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/DynamicLookupSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.network
2 |
3 | import kotlinx.serialization.ContextualSerializer
4 | import kotlinx.serialization.ExperimentalSerializationApi
5 | import kotlinx.serialization.InternalSerializationApi
6 | import kotlinx.serialization.KSerializer
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 | import kotlinx.serialization.serializer
11 |
12 | @ExperimentalSerializationApi
13 | class DynamicLookupSerializer: KSerializer {
14 | override val descriptor: SerialDescriptor = ContextualSerializer(Any::class, null, emptyArray()).descriptor
15 |
16 | @OptIn(InternalSerializationApi::class)
17 | override fun serialize(encoder: Encoder, value: Any) {
18 | val actualSerializer = encoder.serializersModule.getContextual(value::class) ?: value::class.serializer()
19 | encoder.encodeSerializableValue(actualSerializer as KSerializer, value)
20 | }
21 |
22 | override fun deserialize(decoder: Decoder): Any {
23 | return decoder.decodeSerializableValue(this)
24 | }
25 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/ProductResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api.response
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class ProductResponse(
8 | @SerialName("brand")
9 | var brand: BrandResponse?,
10 | @SerialName("category")
11 | var category: CategoryResponse?,
12 | @SerialName("description")
13 | var description: String?,
14 | @SerialName("id")
15 | var id: Int?,
16 | @SerialName("images")
17 | var images: List?,
18 | @SerialName("name")
19 | var name: String?,
20 | @SerialName("price")
21 | var price: Double?,
22 | @SerialName("promoted")
23 | var promoted: Boolean?
24 | ) {
25 | @Serializable
26 | data class BrandResponse(
27 | @SerialName("id")
28 | var id: Int?,
29 | @SerialName("name")
30 | var name: String?,
31 | @SerialName("logo")
32 | var logo: String?
33 | )
34 |
35 | @Serializable
36 | data class CategoryResponse(
37 | @SerialName("id")
38 | var id: Int?,
39 | @SerialName("name")
40 | var name: String?
41 | )
42 | }
--------------------------------------------------------------------------------
/features/explore/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | kotlin("plugin.serialization") version "1.8.21"
6 | }
7 |
8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
9 | kotlin {
10 | targetHierarchy.default()
11 |
12 | android {
13 | compilations.all {
14 | kotlinOptions {
15 | jvmTarget = "1.8"
16 | }
17 | }
18 | }
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach {
25 | it.binaries.framework {
26 | baseName = "explore"
27 | }
28 | }
29 |
30 | sourceSets {
31 | val commonMain by getting {
32 | dependencies {
33 | implementation(project(":libraries:core"))
34 | implementation(project(":libraries:network"))
35 |
36 | implementation(project(":libraries:common"))
37 | }
38 | }
39 | }
40 | }
41 |
42 | android {
43 | namespace = "com.utsman.tokobola.explore"
44 | compileSdk = 33
45 | defaultConfig {
46 | minSdk = 24
47 | }
48 | }
--------------------------------------------------------------------------------
/features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/ExploreInstanceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.explore
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import com.utsman.tokobola.core.SynchronizObject
5 | import com.utsman.tokobola.core.synchroniz
6 | import com.utsman.tokobola.explore.domain.ExploreRepository
7 | import com.utsman.tokobola.explore.domain.explore.ExploreUseCase
8 | import com.utsman.tokobola.explore.domain.search.SearchUseCase
9 | import kotlin.jvm.Volatile
10 | import kotlin.native.concurrent.ThreadLocal
11 |
12 | object ExploreInstanceProvider {
13 |
14 | private fun getRepository(): ExploreRepository {
15 | return ExploreRepository.create { ExploreRepository() }
16 | }
17 |
18 | fun providedExploreUseCase(): ExploreUseCase {
19 | return ExploreUseCase.create { ExploreUseCase(getRepository()) }
20 | }
21 |
22 | fun providedSearchUseCase(): SearchUseCase {
23 | return SearchUseCase.create { SearchUseCase(getRepository()) }
24 | }
25 | }
26 |
27 | val LocalExploreUseCase = compositionLocalOf { error("Explore UseCase not provided") }
28 | val LocalSearchUseCase = compositionLocalOf { error("Search UseCase not provided") }
--------------------------------------------------------------------------------
/features/home/src/commonMain/kotlin/com/utsman/tokobola/home/domain/HomeRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.home.domain
2 |
3 | import com.utsman.tokobola.api.productWebApi
4 | import com.utsman.tokobola.core.SingletonCreator
5 | import com.utsman.tokobola.database.data.RecentlyViewedRealm
6 | import com.utsman.tokobola.database.localRepository
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | class HomeRepository {
10 |
11 | private val productApi by productWebApi()
12 | private val localRepository by localRepository()
13 |
14 | suspend fun getFeaturedProductPaged(page: Int) = productApi.getByFeaturedPaged(page)
15 | suspend fun getBanner() = productApi.getHomeBanner()
16 | suspend fun getBrand() = productApi.getBrand()
17 |
18 | suspend fun getThumbnailByIds(ids: List) = productApi.getThumbnailByIds(ids)
19 |
20 | suspend fun markAsViewed(productId: Int) {
21 | localRepository.insertRecentlyViewed(RecentlyViewedRealm().apply {
22 | this.productId = productId
23 | })
24 | }
25 |
26 | suspend fun getAllRecentlyViewedFlow(): Flow> {
27 | return localRepository.selectAllRecentlyViewed()
28 | }
29 |
30 | companion object : SingletonCreator()
31 | }
--------------------------------------------------------------------------------
/features/home/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | kotlin("plugin.serialization") version "1.8.21"
6 | }
7 |
8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
9 | kotlin {
10 | targetHierarchy.default()
11 |
12 | android {
13 | compilations.all {
14 | kotlinOptions {
15 | jvmTarget = "1.8"
16 | }
17 | }
18 | }
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach {
25 | it.binaries.framework {
26 | baseName = "home"
27 | }
28 | }
29 |
30 | sourceSets {
31 | val commonMain by getting {
32 | dependencies {
33 | implementation(project(":libraries:core"))
34 | implementation(project(":libraries:network"))
35 | implementation(project(":libraries:database"))
36 |
37 | implementation(project(":libraries:common"))
38 | }
39 | }
40 | }
41 | }
42 |
43 | android {
44 | namespace = "com.utsman.tokobola.home"
45 | compileSdk = 33
46 | defaultConfig {
47 | minSdk = 24
48 | }
49 | }
--------------------------------------------------------------------------------
/features/details/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | kotlin("plugin.serialization") version "1.8.21"
6 | }
7 |
8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
9 | kotlin {
10 | targetHierarchy.default()
11 |
12 | android {
13 | compilations.all {
14 | kotlinOptions {
15 | jvmTarget = "1.8"
16 | }
17 | }
18 | }
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach {
25 | it.binaries.framework {
26 | baseName = "details"
27 | }
28 | }
29 |
30 | sourceSets {
31 | val commonMain by getting {
32 | dependencies {
33 | implementation(project(":libraries:core"))
34 | implementation(project(":libraries:network"))
35 | implementation(project(":libraries:database"))
36 |
37 | implementation(project(":libraries:common"))
38 | }
39 | }
40 | }
41 | }
42 |
43 | android {
44 | namespace = "com.utsman.tokobola.details"
45 | compileSdk = 33
46 | defaultConfig {
47 | minSdk = 24
48 | }
49 | }
--------------------------------------------------------------------------------
/features/wishlist/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | kotlin("plugin.serialization") version "1.8.21"
6 | }
7 |
8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
9 | kotlin {
10 | targetHierarchy.default()
11 |
12 | android {
13 | compilations.all {
14 | kotlinOptions {
15 | jvmTarget = "1.8"
16 | }
17 | }
18 | }
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach {
25 | it.binaries.framework {
26 | baseName = "wishlist"
27 | }
28 | }
29 |
30 | sourceSets {
31 | val commonMain by getting {
32 | dependencies {
33 | implementation(project(":libraries:core"))
34 | implementation(project(":libraries:network"))
35 | implementation(project(":libraries:database"))
36 |
37 | implementation(project(":libraries:common"))
38 | }
39 | }
40 | }
41 | }
42 |
43 | android {
44 | namespace = "com.utsman.tokobola.wishlist"
45 | compileSdk = 33
46 | defaultConfig {
47 | minSdk = 24
48 | }
49 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/tab/WishlistTab.kt:
--------------------------------------------------------------------------------
1 | package tab
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.graphics.painter.Painter
6 | import cafe.adriel.voyager.navigator.tab.TabOptions
7 | import com.utsman.tokobola.core.navigation.LocalScreenContainer
8 | import com.utsman.tokobola.resources.SharedRes
9 | import dev.icerock.moko.resources.compose.painterResource
10 |
11 | internal object WishlistTab : CustomTab {
12 | @Composable
13 | override fun Content() {
14 | val screenContainer = LocalScreenContainer.current
15 | screenContainer.wishlist().Content()
16 | }
17 |
18 | override val options: TabOptions
19 | @Composable
20 | get() {
21 | val title = "Wishlist"
22 | val painter = painterResource(SharedRes.images.icon_bookmark)
23 | return remember {
24 | TabOptions(
25 | index = 2u,
26 | title = title,
27 | icon = painter
28 | )
29 | }
30 | }
31 |
32 | override val iconSelected: Painter?
33 | @Composable
34 | get() {
35 | return painterResource(SharedRes.images.icon_bookmark_fill)
36 | }
37 | }
--------------------------------------------------------------------------------
/androidApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.application")
4 | id("org.jetbrains.compose")
5 | }
6 |
7 | kotlin {
8 | android()
9 | sourceSets {
10 | val androidMain by getting {
11 | dependencies {
12 | implementation("com.google.accompanist:accompanist-systemuicontroller:0.30.1")
13 | implementation(compose.ui)
14 |
15 | implementation(project(":shared"))
16 | implementation(project(":libraries:core"))
17 | }
18 | }
19 | }
20 | }
21 |
22 | android {
23 | compileSdk = (findProperty("android.compileSdk") as String).toInt()
24 | namespace = "com.utsman.tokobola"
25 |
26 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
27 |
28 | defaultConfig {
29 | applicationId = "com.utsman.tokobola"
30 | minSdk = (findProperty("android.minSdk") as String).toInt()
31 | targetSdk = (findProperty("android.targetSdk") as String).toInt()
32 | versionCode = 1
33 | versionName = "1.0"
34 | }
35 | compileOptions {
36 | sourceCompatibility = JavaVersion.VERSION_11
37 | targetCompatibility = JavaVersion.VERSION_11
38 | }
39 | kotlin {
40 | jvmToolchain(11)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/tab/HomeTab.kt:
--------------------------------------------------------------------------------
1 | package tab
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.graphics.painter.Painter
6 | import cafe.adriel.voyager.navigator.tab.Tab
7 | import cafe.adriel.voyager.navigator.tab.TabOptions
8 | import com.utsman.tokobola.core.navigation.LocalScreenContainer
9 | import com.utsman.tokobola.resources.SharedRes
10 | import dev.icerock.moko.resources.compose.painterResource
11 |
12 | internal object HomeTab : CustomTab {
13 |
14 | @Composable
15 | override fun Content() {
16 | val screenContainer = LocalScreenContainer.current
17 | screenContainer.home().Content()
18 | }
19 |
20 | override val options: TabOptions
21 | @Composable
22 | get() {
23 | val title = "Home"
24 | val painter = painterResource(SharedRes.images.icon_home)
25 |
26 | return remember {
27 | TabOptions(
28 | index = 0u,
29 | title = title,
30 | icon = painter
31 | )
32 | }
33 | }
34 |
35 | override val iconSelected: Painter?
36 | @Composable
37 | get() {
38 | return painterResource(SharedRes.images.icon_home_fill)
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/ui/search/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.explore.ui.search
2 |
3 | import com.utsman.tokobola.common.entity.ThumbnailProduct
4 | import com.utsman.tokobola.core.ViewModel
5 | import com.utsman.tokobola.explore.domain.search.SearchUseCase
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.collectLatest
8 | import kotlinx.coroutines.flow.debounce
9 | import kotlinx.coroutines.flow.distinctUntilChanged
10 | import kotlinx.coroutines.launch
11 |
12 | class SearchViewModel(private val useCase: SearchUseCase) : ViewModel() {
13 |
14 | val query get() = useCase.query
15 |
16 | val productSearchState get() = useCase.productSearchReducer.dataFlow
17 | val productSearchFlow = MutableStateFlow(emptyList())
18 |
19 | fun postResultSearch(list: List) {
20 | productSearchFlow.value = list
21 | }
22 |
23 | fun listenQuery() = viewModelScope.launch {
24 | query.debounce(2000)
25 | .distinctUntilChanged()
26 | .collectLatest {
27 | useCase.getBySearch(it)
28 | }
29 | }
30 |
31 | fun getNextSearch() = viewModelScope.launch {
32 | useCase.getBySearch(query.value)
33 | }
34 |
35 | fun clearData() {
36 | useCase.clearData()
37 | }
38 | }
--------------------------------------------------------------------------------
/features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/domain/WishlistUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.wishlist.domain
2 |
3 | import com.utsman.tokobola.common.entity.ThumbnailProduct
4 | import com.utsman.tokobola.common.toThumbnailProduct
5 | import com.utsman.tokobola.core.SingletonCreator
6 | import com.utsman.tokobola.network.ApiReducer
7 | import com.utsman.tokobola.network.StateTransformation
8 |
9 | class WishlistUseCase(private val repository: WishlistRepository) {
10 |
11 | val productWishlistReducer = ApiReducer>()
12 |
13 | suspend fun getWishlist() {
14 | repository.getAllWishlist()
15 | .collect { realms ->
16 | productWishlistReducer.transform(
17 | transformation = StateTransformation.SimpleTransform(),
18 | call = {
19 | val ids = realms.map { it.productId }
20 | repository.getThumbnailByIds(ids)
21 | },
22 | mapper = { thumbnailProductResponse ->
23 | thumbnailProductResponse.data
24 | ?.map { it.toThumbnailProduct() }
25 | .orEmpty()
26 | }
27 | )
28 | }
29 | }
30 |
31 | companion object : SingletonCreator()
32 | }
--------------------------------------------------------------------------------
/androidApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/ImageLoader.ios.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import com.seiko.imageloader.ImageLoader
6 | import com.seiko.imageloader.component.ComponentRegistryBuilder
7 | import com.seiko.imageloader.component.setupDefaultComponents
8 | import okio.Path
9 | import okio.Path.Companion.toPath
10 | import platform.Foundation.NSCachesDirectory
11 | import platform.Foundation.NSSearchPathForDirectoriesInDomains
12 | import platform.Foundation.NSUserDomainMask
13 |
14 | @Composable
15 | actual fun rememberImageLoader(): ImageLoader {
16 | return remember {
17 | val memCacheSize = 32 * 1024 * 1024
18 | val diskCacheSize = 512 * 1024 * 1024
19 |
20 | val cacheDir = NSSearchPathForDirectoriesInDomains(
21 | NSCachesDirectory, NSUserDomainMask, true
22 | ).first().toString()
23 |
24 | val cachePath = ("$cacheDir/media").toPath()
25 |
26 | ImageLoader {
27 | interceptor {
28 | memoryCacheConfig { maxSizeBytes(memCacheSize) }
29 | diskCacheConfig {
30 | directory(cachePath)
31 | maxSizeBytes(diskCacheSize.toLong())
32 | }
33 | }
34 |
35 | components { setupDefaultComponents() }
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/MapboxWebApi.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api
2 |
3 | import com.utsman.tokobola.api.response.MapboxReverseResponse
4 | import com.utsman.tokobola.api.response.MapboxSearchResponse
5 | import com.utsman.tokobola.core.SingletonCreator
6 | import com.utsman.tokobola.core.data.LatLon
7 | import com.utsman.tokobola.core.utils.calculateBboxMapbox
8 | import com.utsman.tokobola.network.NetworkSources
9 |
10 | class MapboxWebApi : NetworkSources(BuildKonfig.MAPBOX_BASE_URL) {
11 |
12 |
13 | suspend fun getReverseGeocoding(latLon: LatLon, mapboxAccessToken: String): MapboxReverseResponse {
14 | return getRaw(
15 | endPoint = WebEndPoint.MAPBOX_GEOCODING
16 | .withParam("lat", latLon.latitude)
17 | .withParam("lon", latLon.longitude)
18 | .withParam("mapbox_access_token", mapboxAccessToken)
19 | )
20 | }
21 |
22 | suspend fun getSearchGeocoding(query: String, latLon: LatLon, mapboxAccessToken: String): MapboxSearchResponse {
23 | return getRaw(
24 | endPoint = WebEndPoint.MAPBOX_SEARCH
25 | .withParam("proximity", "${latLon.longitude},${latLon.latitude}")
26 | .withParam("q", query)
27 | .withParam("mapbox_access_token", mapboxAccessToken)
28 | )
29 | }
30 |
31 | companion object : SingletonCreator()
32 | }
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/DetailInstanceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import com.utsman.tokobola.details.domain.BrandDetailUseCase
5 | import com.utsman.tokobola.details.domain.CategoryDetailUseCase
6 | import com.utsman.tokobola.details.domain.DetailRepository
7 | import com.utsman.tokobola.details.domain.ProductDetailUseCase
8 |
9 | object DetailInstanceProvider {
10 |
11 | private fun getRepository(): DetailRepository {
12 | return DetailRepository.create { DetailRepository() }
13 | }
14 |
15 | fun providedProductDetailUseCase(): ProductDetailUseCase {
16 | return ProductDetailUseCase.create { ProductDetailUseCase(getRepository()) }
17 | }
18 |
19 | fun providedCategoryDetailUseCase(): CategoryDetailUseCase {
20 | return CategoryDetailUseCase.create { CategoryDetailUseCase(getRepository()) }
21 | }
22 |
23 | fun providedBrandDetailUseCase(): BrandDetailUseCase {
24 | return BrandDetailUseCase.create { BrandDetailUseCase(getRepository()) }
25 | }
26 | }
27 |
28 | val LocalProductDetailUseCase = compositionLocalOf { error("Not provided") }
29 | val LocalCategoryDetailUseCase = compositionLocalOf { error("Not provided") }
30 | val LocalBrandDetailUseCase = compositionLocalOf { error("Not provided") }
--------------------------------------------------------------------------------
/libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/NetworkSources.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.network
2 |
3 | import com.utsman.tokobola.network.response.BasePagedResponse
4 | import com.utsman.tokobola.network.response.BaseResponse
5 | import io.ktor.client.call.body
6 | import io.ktor.client.request.get
7 | import io.ktor.http.ContentType
8 | import io.ktor.http.contentType
9 |
10 | abstract class NetworkSources(protected val baseUrl: String) {
11 |
12 | protected fun client() = ClientProvider.client()
13 |
14 | protected suspend inline fun getRaw(
15 | endPoint: String,
16 | contentType: ContentType = ContentType.Application.Json
17 | ): T {
18 | return client().get("$baseUrl$endPoint") {
19 | contentType(contentType)
20 | }.body()
21 | }
22 |
23 | protected suspend inline fun get(
24 | endPoint: String,
25 | contentType: ContentType = ContentType.Application.Json
26 | ): BaseResponse {
27 | return client().get("$baseUrl$endPoint") {
28 | contentType(contentType)
29 | }.body()
30 | }
31 |
32 | protected suspend inline fun getPaged(
33 | endPoint: String,
34 | contentType: ContentType = ContentType.Application.Json
35 | ): BasePagedResponse {
36 | return client().get("$baseUrl$endPoint") {
37 | contentType(contentType)
38 | }.body()
39 | }
40 | }
--------------------------------------------------------------------------------
/libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/ImageLoader.android.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.core
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.platform.LocalContext
7 | import com.seiko.imageloader.ImageLoader
8 | import com.seiko.imageloader.component.ComponentRegistryBuilder
9 | import com.seiko.imageloader.component.setupDefaultComponents
10 | import com.utsman.tokobola.core.utils.AndroidContextProvider
11 | import okio.Path
12 | import okio.Path.Companion.toPath
13 |
14 | private fun getImageCacheDirectoryPath(context: Context): Path {
15 | return context.cacheDir.absolutePath.toPath()
16 | }
17 |
18 | @Composable
19 | actual fun rememberImageLoader(): ImageLoader {
20 | return remember {
21 | val context = AndroidContextProvider.getInstance().context
22 | val memCacheSize = 32 * 1024 * 1024
23 | val diskCacheSize = 512 * 1024 * 1024
24 |
25 | ImageLoader {
26 | interceptor {
27 | memoryCacheConfig { maxSizeBytes(memCacheSize) }
28 | diskCacheConfig {
29 | directory(getImageCacheDirectoryPath(context))
30 | maxSizeBytes(diskCacheSize.toLong())
31 | }
32 | }
33 |
34 | components {
35 | setupDefaultComponents(context)
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/ClientProvider.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.network
2 |
3 | import com.utsman.tokobola.core.SynchronizObject
4 | import com.utsman.tokobola.core.synchroniz
5 | import io.ktor.client.HttpClient
6 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
7 | import io.ktor.client.plugins.logging.LogLevel
8 | import io.ktor.client.plugins.logging.Logger
9 | import io.ktor.client.plugins.logging.Logging
10 | import io.ktor.client.plugins.logging.SIMPLE
11 | import io.ktor.serialization.kotlinx.json.json
12 | import kotlinx.serialization.json.Json
13 | import kotlin.native.concurrent.ThreadLocal
14 |
15 |
16 | @ThreadLocal
17 | internal object ClientProvider : SynchronizObject() {
18 |
19 | private var _client: HttpClient? = null
20 |
21 | fun client(): HttpClient {
22 | if (_client == null) {
23 | _client = HttpClient {
24 | install(ContentNegotiation) {
25 | json(Json {
26 | prettyPrint = true
27 | isLenient = true
28 | ignoreUnknownKeys = true
29 | explicitNulls = false
30 | })
31 | }
32 | install(Logging) {
33 | logger = Logger.SIMPLE
34 | level = LogLevel.INFO
35 | }
36 | }
37 | }
38 | return synchroniz(this) { _client!! }
39 | }
40 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.graphics.Color
9 | import com.utsman.tokobola.core.utils.parseString
10 |
11 | private val DarkColorScheme = darkColors(
12 | primary = ColorPrimaryDark,
13 | secondary = ColorSecondaryDark,
14 | secondaryVariant = Color.White
15 | )
16 |
17 | private val LightColorScheme = lightColors(
18 | primary = ColorPrimaryLight,
19 | secondary = ColorSecondaryLight,
20 | secondaryVariant = Color.White
21 |
22 | /* Other default colors to override
23 | background = Color(0xFFFFFBFE),
24 | surface = Color(0xFFFFFBFE),
25 | onPrimary = Color.White,
26 | onSecondary = Color.White,
27 | onTertiary = Color.White,
28 | onBackground = Color(0xFF1C1B1F),
29 | onSurface = Color(0xFF1C1B1F),
30 | */
31 | )
32 |
33 | @Composable
34 | fun CommonTheme(
35 | darkTheme: Boolean = isSystemInDarkTheme(),
36 | content: @Composable () -> Unit
37 | ) {
38 | val colorSchema = if (darkTheme) {
39 | DarkColorScheme
40 | } else {
41 | LightColorScheme
42 | }
43 |
44 | MaterialTheme(
45 | colors = colorSchema,
46 | typography = Type(),
47 | content = content
48 | )
49 | }
--------------------------------------------------------------------------------
/libraries/location/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | kotlin("plugin.serialization") version "1.8.21"
6 | }
7 |
8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
9 | kotlin {
10 | targetHierarchy.default()
11 |
12 | android {
13 | compilations.all {
14 | kotlinOptions {
15 | jvmTarget = "1.8"
16 | }
17 | }
18 | }
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach {
25 | it.binaries.framework {
26 | baseName = "location"
27 | }
28 | }
29 |
30 | sourceSets {
31 | val commonMain by getting {
32 | dependencies {
33 | implementation(project(":libraries:core"))
34 | implementation(project(":libraries:network"))
35 | implementation(project(":libraries:database"))
36 |
37 | api("dev.icerock.moko:permissions-compose:0.16.0")
38 | api("dev.icerock.moko:geo-compose:0.6.0")
39 | }
40 | }
41 |
42 | getByName("androidMain") {
43 | dependencies {
44 | api("com.google.android.gms:play-services-location:21.0.1")
45 | }
46 | }
47 | }
48 | }
49 |
50 | android {
51 | namespace = "com.utsman.tokobola.location"
52 | compileSdk = 33
53 | defaultConfig {
54 | minSdk = 24
55 | }
56 | }
--------------------------------------------------------------------------------
/libraries/api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
2 |
3 | plugins {
4 | kotlin("multiplatform")
5 | id("com.android.library")
6 | kotlin("plugin.serialization") version "1.8.21"
7 | id("com.codingfeline.buildkonfig")
8 | }
9 |
10 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
11 | kotlin {
12 | targetHierarchy.default()
13 |
14 | android {
15 | compilations.all {
16 | kotlinOptions {
17 | jvmTarget = "1.8"
18 | }
19 | }
20 | }
21 |
22 | listOf(
23 | iosX64(),
24 | iosArm64(),
25 | iosSimulatorArm64()
26 | ).forEach {
27 | it.binaries.framework {
28 | baseName = "api"
29 | }
30 | }
31 |
32 | sourceSets {
33 |
34 | val commonMain by getting {
35 | dependencies {
36 | implementation(project(":libraries:core"))
37 | implementation(project(":libraries:network"))
38 | }
39 | }
40 | }
41 | }
42 |
43 | android {
44 | namespace = "com.utsman.tokobola.api"
45 | compileSdk = 33
46 | defaultConfig {
47 | minSdk = 24
48 | }
49 | }
50 |
51 | buildkonfig {
52 | packageName = "com.utsman.tokobola.api"
53 |
54 | // default config is required
55 | defaultConfigs {
56 | buildConfigField(STRING, "BASE_URL", project.properties.get("base.url").toString())
57 | buildConfigField(STRING, "MAPBOX_BASE_URL", project.properties.get("mapbox.base.url").toString())
58 | }
59 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/tab/ExploreTab.kt:
--------------------------------------------------------------------------------
1 | package tab
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.painter.Painter
10 | import androidx.compose.ui.unit.dp
11 | import cafe.adriel.voyager.navigator.tab.Tab
12 | import cafe.adriel.voyager.navigator.tab.TabOptions
13 | import com.utsman.tokobola.core.navigation.LocalNavigation
14 | import com.utsman.tokobola.core.navigation.LocalScreenContainer
15 | import com.utsman.tokobola.resources.SharedRes
16 | import dev.icerock.moko.resources.compose.painterResource
17 |
18 | internal object ExploreTab : CustomTab {
19 |
20 | @Composable
21 | override fun Content() {
22 | val screenContainer = LocalScreenContainer.current
23 | screenContainer.explore().Content()
24 | }
25 |
26 | override val options: TabOptions
27 | @Composable
28 | get() {
29 | val title = "Explorer"
30 | val painter = painterResource(SharedRes.images.icon_explore)
31 | return remember {
32 | TabOptions(
33 | index = 1u,
34 | title = title,
35 | icon = painter
36 | )
37 | }
38 | }
39 |
40 | override val iconSelected: Painter?
41 | @Composable
42 | get() {
43 | return painterResource(SharedRes.images.icon_explore_fill)
44 | }
45 | }
--------------------------------------------------------------------------------
/features/cart/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
2 |
3 | plugins {
4 | kotlin("multiplatform")
5 | id("com.android.library")
6 | id("org.jetbrains.compose")
7 | kotlin("plugin.serialization") version "1.8.21"
8 | id("com.codingfeline.buildkonfig")
9 | }
10 |
11 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
12 | kotlin {
13 | targetHierarchy.default()
14 |
15 | android {
16 | compilations.all {
17 | kotlinOptions {
18 | jvmTarget = "1.8"
19 | }
20 | }
21 | }
22 |
23 | listOf(
24 | iosX64(),
25 | iosArm64(),
26 | iosSimulatorArm64()
27 | ).forEach {
28 | it.binaries.framework {
29 | baseName = "cart"
30 | }
31 | }
32 |
33 | sourceSets {
34 | val commonMain by getting {
35 | dependencies {
36 | implementation(project(":libraries:core"))
37 | implementation(project(":libraries:network"))
38 | implementation(project(":libraries:database"))
39 | implementation(project(":libraries:location"))
40 |
41 | implementation(project(":libraries:common"))
42 | }
43 | }
44 | }
45 | }
46 |
47 | android {
48 | namespace = "com.utsman.tokobola.cart"
49 | compileSdk = 33
50 | defaultConfig {
51 | minSdk = 24
52 | }
53 | }
54 |
55 | buildkonfig {
56 | packageName = "com.utsman.tokobola.cart"
57 |
58 | // default config is required
59 | defaultConfigs {
60 | buildConfigField(STRING, "MAPBOX_TOKEN", project.properties.get("mapbox.token").toString())
61 | }
62 | }
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/domain/DetailRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details.domain
2 |
3 | import com.utsman.tokobola.api.productWebApi
4 | import com.utsman.tokobola.core.SingletonCreator
5 | import com.utsman.tokobola.database.data.RecentlyViewedRealm
6 | import com.utsman.tokobola.database.data.WishlistRealm
7 | import com.utsman.tokobola.database.localRepository
8 |
9 | class DetailRepository {
10 |
11 | private val productApi by productWebApi()
12 | private val localRepository by localRepository()
13 |
14 | suspend fun getDetailProduct(productId: Int) = productApi.getDetail(productId)
15 |
16 | suspend fun markAsViewed(productId: Int) {
17 | localRepository.insertRecentlyViewed(RecentlyViewedRealm().apply {
18 | this.productId = productId
19 | })
20 | }
21 |
22 | suspend fun getCartProduct(productId: Int) = localRepository.getProductCart(productId)
23 |
24 | suspend fun insertOrUpdateCart(productId: Int, operationQuantity: (Int) -> Int) {
25 | localRepository.insertOrUpdateProductCart(productId, operationQuantity)
26 | }
27 |
28 | suspend fun getProductCategory(categoryId: Int, page: Int) =
29 | productApi.getByCategoryPaged(categoryId, page)
30 |
31 | suspend fun getProductBrand(brandId: Int, page: Int) = productApi.getByBrandPaged(brandId, page)
32 |
33 | suspend fun toggleWishlist(productId: Int) {
34 | localRepository.toggleWishlist(WishlistRealm().apply {
35 | this.productId = productId
36 | })
37 | }
38 | suspend fun isWishlistExist(productId: Int) = localRepository.checkWishlistIsExist(productId)
39 |
40 | companion object : SingletonCreator()
41 | }
--------------------------------------------------------------------------------
/features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/ui/LocationPickerViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.cart.ui
2 |
3 | import com.utsman.tokobola.cart.domain.LocationPickerUseCase
4 | import com.utsman.tokobola.common.entity.LocationPlace
5 | import com.utsman.tokobola.core.ViewModel
6 | import com.utsman.tokobola.core.data.LatLon
7 | import kotlinx.coroutines.flow.collectLatest
8 | import kotlinx.coroutines.flow.debounce
9 | import kotlinx.coroutines.flow.distinctUntilChanged
10 | import kotlinx.coroutines.launch
11 |
12 | class LocationPickerViewModel(private val useCase: LocationPickerUseCase) : ViewModel() {
13 |
14 | val query get() = useCase.query
15 | val locationResultState = useCase.locationSearchReducer.dataFlow
16 | val locationReverseState = useCase.locationReverseReducer.dataFlow
17 |
18 | private var proximityLatLon: LatLon? = null
19 |
20 | fun listenQuery() = viewModelScope.launch {
21 | query.debounce(2000)
22 | .distinctUntilChanged()
23 | .collectLatest {
24 | proximityLatLon?.let { latLon ->
25 | useCase.searchLocationPlace(it, latLon)
26 | }
27 | }
28 | }
29 |
30 | fun updateProximityLatLon(latLon: LatLon) {
31 | proximityLatLon = latLon
32 | }
33 |
34 | fun getLocationReverse(latLon: LatLon) = viewModelScope.launch {
35 | useCase.getLocationReverse(latLon)
36 | }
37 |
38 | fun insertShippingAddress(locationPlace: LocationPlace) = viewModelScope.launch {
39 | useCase.saveShippingAddress(locationPlace)
40 | }
41 |
42 | fun clearData() {
43 | useCase.clearData()
44 | }
45 |
46 | override fun onCleared() {
47 | clearData()
48 | super.onCleared()
49 | }
50 | }
--------------------------------------------------------------------------------
/libraries/network/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | kotlin("plugin.serialization") version "1.8.21"
5 | }
6 |
7 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
8 | kotlin {
9 | targetHierarchy.default()
10 |
11 | android {
12 | compilations.all {
13 | kotlinOptions {
14 | jvmTarget = "1.8"
15 | }
16 | }
17 | }
18 |
19 | listOf(
20 | iosX64(),
21 | iosArm64(),
22 | iosSimulatorArm64()
23 | ).forEach {
24 | it.binaries.framework {
25 | baseName = "network"
26 | }
27 | }
28 |
29 | sourceSets {
30 | val ktorVersion = "2.3.2"
31 |
32 | val commonMain by getting {
33 | dependencies {
34 | implementation("io.ktor:ktor-client-core:$ktorVersion")
35 | implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
36 | implementation("io.ktor:ktor-client-logging:$ktorVersion")
37 | api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
38 |
39 | implementation(project(":libraries:core"))
40 | }
41 | }
42 | getByName("androidMain").dependencies {
43 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
44 | implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
45 | }
46 |
47 | getByName("iosMain").dependencies {
48 | implementation("io.ktor:ktor-client-darwin:$ktorVersion")
49 | }
50 | }
51 | }
52 |
53 | android {
54 | namespace = "com.utsman.tokobola.network"
55 | compileSdk = 33
56 | defaultConfig {
57 | minSdk = 24
58 | }
59 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TokoBola
2 |
3 | An online football store simulation application for Android and iOS, developed using the latest
4 | technology from Kotlin Multiplatform and Compose Multiplatform as the UI framework.
5 |
6 | This is a playground project and not serious. This project is being worked on periodically, with the
7 | goal of learning Kotlin Multiplatform, Compose for Multiplatform for Android and iOS, and many other
8 | things. I chose the marketplace theme because by developing this themed application, there's so much
9 | that can be learned, starting from architecture, local database, up to maps.
10 |
11 |
12 | |  |  |
13 | |--------------------|--------------------|
14 | |  |  |
15 | |  |  |
16 |
17 |
18 | ## Architecture
19 |
20 | This main architecture combines MVVM with state management, and I call it MVVM + State. This state
21 | is not like MVI, which has multiple states for each flow. I only create 4 conditions, namely Idle,
22 | Loading, Success, and Failure. These states are propagated from data sources to nodes to be rendered
23 | according to their respective needs.
24 |
25 |
26 |
27 |
28 |
29 | ## Third party library
30 |
31 | - [Ktor client](https://ktor.io/docs/getting-started-ktor-client-multiplatform-mobile.html)
32 | - [moko-resources](https://github.com/icerockdev/moko-resources)
33 | - [moko-geo](https://github.com/icerockdev/moko-geo)
34 | - [Voyager](https://voyager.adriel.cafe/)
35 | - [Compose ImageLoader](https://github.com/qdsfdhvh/compose-imageloader)
36 | - [Realm Kotlin](https://realm.io/realm-kotlin/)
37 | - [BuildKonfig](https://github.com/yshrsmz/BuildKonfig)
38 | - [Mapbox Android SDK](https://docs.mapbox.com/android/maps/guides/)
39 |
40 | ---
--------------------------------------------------------------------------------
/features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/domain/search/SearchUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.explore.domain.search
2 |
3 | import com.utsman.tokobola.common.entity.ThumbnailProduct
4 | import com.utsman.tokobola.common.toThumbnailProduct
5 | import com.utsman.tokobola.core.SingletonCreator
6 | import com.utsman.tokobola.core.data.Paged
7 | import com.utsman.tokobola.explore.domain.ExploreRepository
8 | import com.utsman.tokobola.network.ApiReducer
9 | import com.utsman.tokobola.network.AutoPagingAdapter
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 |
12 |
13 | class SearchUseCase(private val repository: ExploreRepository) {
14 |
15 | val productSearchReducer = ApiReducer>()
16 |
17 | private val searchPagingAdapter = AutoPagingAdapter(productSearchReducer)
18 |
19 | val query = MutableStateFlow("")
20 | private var currentQuery = ""
21 |
22 | suspend fun getBySearch(query: String) {
23 | if (currentQuery != query) {
24 | currentQuery = query
25 | searchPagingAdapter.clear()
26 | }
27 |
28 | when {
29 | (query.count() < 3) -> {
30 | currentQuery = ""
31 | searchPagingAdapter.clear()
32 | }
33 | else -> {
34 | searchPagingAdapter.executeResponse(
35 | call = {
36 | repository.getProductBySearch(it, query)
37 | },
38 | mapper = {
39 | it.toThumbnailProduct()
40 | }
41 | )
42 | }
43 | }
44 | }
45 |
46 | fun clearData() {
47 | productSearchReducer.clear()
48 | query.value = ""
49 | }
50 |
51 | companion object : SingletonCreator()
52 | }
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/tab/AboutTab.kt:
--------------------------------------------------------------------------------
1 | package tab
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.painter.Painter
10 | import androidx.compose.ui.unit.dp
11 | import cafe.adriel.voyager.navigator.tab.TabOptions
12 | import com.utsman.tokobola.core.navigation.LocalNavigation
13 | import com.utsman.tokobola.core.navigation.LocalScreenContainer
14 | import com.utsman.tokobola.resources.SharedRes
15 | import dev.icerock.moko.resources.compose.painterResource
16 |
17 |
18 | /**
19 | * must be internal
20 | * https://github.com/JetBrains/compose-multiplatform/issues/3175#issuecomment-1564546150
21 | * */
22 | internal object AboutTab : CustomTab {
23 |
24 | @Composable
25 | override fun Content() {
26 | val screenContainer = LocalScreenContainer.current
27 | val navigation = LocalNavigation.current
28 | Text("about", modifier = Modifier.padding(100.dp).clickable {
29 | navigation.goToDetailProduct(3)
30 | })
31 | }
32 |
33 | override val options: TabOptions
34 | @Composable
35 | get() {
36 | val title = "About"
37 | val painter = painterResource(SharedRes.images.icon_about)
38 | return remember {
39 | TabOptions(
40 | index = 1u,
41 | title = title,
42 | icon = painter
43 | )
44 | }
45 | }
46 |
47 | override val iconSelected: Painter?
48 | @Composable
49 | get() {
50 | return painterResource(SharedRes.images.icon_about_fill)
51 | }
52 | }
--------------------------------------------------------------------------------
/libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/MainExtensions.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalTime::class)
2 |
3 | package com.utsman.tokobola.core.utils
4 |
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.platform.LocalDensity
8 | import com.utsman.tokobola.resources.MokoColor
9 | import dev.icerock.moko.graphics.parseColor
10 | import io.ktor.util.date.getTimeMillis
11 | import kotlinx.coroutines.CoroutineScope
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.IO
14 | import kotlinx.coroutines.SupervisorJob
15 | import kotlinx.coroutines.async
16 | import kotlinx.coroutines.awaitAll
17 | import kotlinx.coroutines.withContext
18 | import kotlin.coroutines.CoroutineContext
19 | import kotlin.time.ExperimentalTime
20 |
21 | fun Double.currency(): String {
22 | return "$$this"
23 | }
24 |
25 | fun Color.Companion.parseString(hex: String): Color {
26 | return Color(MokoColor.parseColor(hex).argb)
27 | }
28 |
29 | fun DefaultScope(): CoroutineScope = object : CoroutineScope {
30 | override val coroutineContext: CoroutineContext
31 | get() = SupervisorJob() + Dispatchers.Default
32 | }
33 |
34 | fun IoScope(): CoroutineScope = object : CoroutineScope {
35 | override val coroutineContext: CoroutineContext
36 | get() = SupervisorJob() + Dispatchers.IO
37 | }
38 |
39 | suspend fun Iterable.pmap(mapper: suspend (T) -> U): List = withContext(Dispatchers.IO) {
40 | map { async { mapper.invoke(it) } }.awaitAll()
41 | }
42 |
43 | fun nowMillis(): Long {
44 | return getTimeMillis()
45 | }
46 |
47 | suspend fun asyncAwait(action: suspend () -> T): T {
48 | return withContext(Dispatchers.IO) {
49 | action.invoke()
50 | }
51 | }
52 |
53 | @Composable
54 | fun Int.pxToDp() = with(LocalDensity.current) {
55 | this@pxToDp.toDp()
56 | }
--------------------------------------------------------------------------------
/libraries/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
2 |
3 | plugins {
4 | kotlin("multiplatform")
5 | id("com.android.library")
6 | id("org.jetbrains.compose")
7 | kotlin("plugin.serialization") version "1.8.21"
8 | id("com.codingfeline.buildkonfig")
9 | }
10 |
11 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
12 | kotlin {
13 | targetHierarchy.default()
14 |
15 | android {
16 | compilations.all {
17 | kotlinOptions {
18 | jvmTarget = "1.8"
19 | }
20 | }
21 | }
22 |
23 | listOf(
24 | iosX64(),
25 | iosArm64(),
26 | iosSimulatorArm64()
27 | ).forEach {
28 | it.binaries.framework {
29 | baseName = "common"
30 | }
31 | }
32 |
33 | sourceSets {
34 | val commonMain by getting {
35 | dependencies {
36 | implementation(project(":libraries:core"))
37 | implementation(project(":libraries:network"))
38 | implementation(project(":libraries:database"))
39 | implementation(project(":libraries:location"))
40 | api(project(":libraries:api"))
41 | }
42 | }
43 |
44 | getByName("androidMain") {
45 | dependencies {
46 | implementation("com.mapbox.maps:android:10.15.0")
47 | }
48 | }
49 | }
50 | }
51 |
52 | android {
53 | namespace = "com.utsman.tokobola.feature.common"
54 | compileSdk = 33
55 | defaultConfig {
56 | minSdk = 24
57 | }
58 | }
59 |
60 | buildkonfig {
61 | packageName = "com.utsman.tokobola.common"
62 |
63 | // default config is required
64 | defaultConfigs {
65 | buildConfigField(STRING, "MAPBOX_TOKEN", project.properties.get("mapbox.token").toString())
66 | }
67 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/ScreenContainerProvider.kt:
--------------------------------------------------------------------------------
1 | import cafe.adriel.voyager.core.screen.Screen
2 | import com.utsman.tokobola.cart.ui.Cart
3 | import com.utsman.tokobola.cart.ui.LocationPicker
4 | import com.utsman.tokobola.core.data.LatLon
5 | import com.utsman.tokobola.core.navigation.ScreenContainer
6 | import com.utsman.tokobola.core.navigation.screenContentOf
7 | import com.utsman.tokobola.details.ui.product.ProductDetail
8 | import com.utsman.tokobola.details.ui.brand.BrandDetail
9 | import com.utsman.tokobola.details.ui.category.CategoryDetail
10 | import com.utsman.tokobola.explore.ui.explore.Explore
11 | import com.utsman.tokobola.explore.ui.search.Search
12 | import com.utsman.tokobola.home.ui.Home
13 | import com.utsman.tokobola.wishlist.ui.Wishlist
14 |
15 | class ScreenContainerProvider : ScreenContainer {
16 |
17 | override fun home(): Screen = screenContentOf("home") {
18 | Home()
19 | }
20 |
21 | override fun detailProduct(productId: Int): Screen = screenContentOf("detail") {
22 | ProductDetail(productId)
23 | }
24 |
25 | override fun detailCategory(categoryId: Int) = screenContentOf("category_detail") {
26 | CategoryDetail(categoryId)
27 | }
28 |
29 | override fun detailBrand(brandId: Int) = screenContentOf("brand_detail") {
30 | BrandDetail(brandId)
31 | }
32 |
33 | override fun explore(): Screen = screenContentOf("explore") {
34 | Explore()
35 | }
36 |
37 | override fun wishlist(): Screen = screenContentOf("wishlist") {
38 | Wishlist()
39 | }
40 |
41 | override fun search(): Screen = screenContentOf("search") {
42 | Search()
43 | }
44 |
45 | override fun cart(): Screen = screenContentOf("cart") {
46 | Cart()
47 | }
48 |
49 | override fun locationPicker(latLon: LatLon) = screenContentOf("location_picker") {
50 | LocationPicker(latLon)
51 | }
52 | }
--------------------------------------------------------------------------------
/resources/resources.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'resources'
3 | spec.version = '1.0.0'
4 | spec.homepage = 'Link to the Shared Module homepage'
5 | spec.source = { :http=> ''}
6 | spec.authors = ''
7 | spec.license = ''
8 | spec.summary = 'Some description for the Shared Module'
9 | spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework'
10 | spec.libraries = 'c++'
11 | spec.ios.deployment_target = '14.1'
12 |
13 |
14 | spec.pod_target_xcconfig = {
15 | 'KOTLIN_PROJECT_PATH' => ':resources',
16 | 'PRODUCT_MODULE_NAME' => 'shared',
17 | }
18 |
19 | spec.script_phases = [
20 | {
21 | :name => 'Build resources',
22 | :execution_position => :before_compile,
23 | :shell_path => '/bin/sh',
24 | :script => <<-SCRIPT
25 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
26 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
27 | exit 0
28 | fi
29 | set -ev
30 | REPO_ROOT="$PODS_TARGET_SRCROOT"
31 | "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
32 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
33 | -Pkotlin.native.cocoapods.archs="$ARCHS" \
34 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
35 | SCRIPT
36 | }
37 | ]
38 | spec.resources = ['src/commonMain/resources/**', 'src/iosMain/resources/**']
39 | spec.exclude_files = ['src/commonMain/resources/MR/**']
40 | end
--------------------------------------------------------------------------------
/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIViewControllerBasedStatusBarAppearance
6 |
7 | NSLocationWhenInUseUsageDescription
8 |
9 | CFBundleDevelopmentRegion
10 | $(DEVELOPMENT_LANGUAGE)
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
21 | CFBundleShortVersionString
22 | 1.0
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | CADisableMinimumFrameDurationOnPhone
28 |
29 | UIApplicationSceneManifest
30 |
31 | UIApplicationSupportsMultipleScenes
32 |
33 |
34 | UILaunchScreen
35 |
36 | UIRequiredDeviceCapabilities
37 |
38 | armv7
39 |
40 | UISupportedInterfaceOrientations
41 |
42 | UIInterfaceOrientationPortrait
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 | UISupportedInterfaceOrientations~ipad
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationPortraitUpsideDown
50 | UIInterfaceOrientationLandscapeLeft
51 | UIInterfaceOrientationLandscapeRight
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/ui/explore/ExploreViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.explore.ui.explore
2 |
3 | import com.utsman.tokobola.core.ViewModel
4 | import com.utsman.tokobola.explore.domain.explore.ExploreUseCase
5 | import com.utsman.tokobola.explore.ui.ExploreUiConfig
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.launch
8 |
9 | class ExploreViewModel(private val useCase: ExploreUseCase) : ViewModel() {
10 |
11 | val brandState get() = useCase.brandReducer.dataFlow
12 | val categoryState get() = useCase.categoryReducer.dataFlow
13 |
14 | val productBrandState get() = useCase.productBrandReducer.dataFlow
15 | val productCategoryState get() = useCase.productCategoryReducer.dataFlow
16 |
17 | val topProductState get() = useCase.topProductReducer.dataFlow
18 | val curatedProductState get() = useCase.curatedProductReducer.dataFlow
19 |
20 | val uiConfig = MutableStateFlow(ExploreUiConfig())
21 |
22 | fun getBrand() = viewModelScope.launch {
23 | useCase.getBrand()
24 | }
25 |
26 | fun getCategory() = viewModelScope.launch {
27 | useCase.getCategory()
28 | }
29 |
30 | fun getProductBrand(brandId: Int) = viewModelScope.launch {
31 | useCase.getProductBrand(brandId)
32 | }
33 | fun getProductCategory(categoryId: Int) = viewModelScope.launch {
34 | useCase.getProductCategory(categoryId)
35 | }
36 |
37 | fun getTopProduct() = viewModelScope.launch {
38 | useCase.getTopProduct()
39 | }
40 |
41 | fun getCuratedProduct() = viewModelScope.launch {
42 | useCase.getCuratedProduct()
43 | }
44 |
45 | fun updateUiConfig(uiConfig: () -> ExploreUiConfig) {
46 | val newUiConfig = uiConfig.invoke()
47 | this.uiConfig.value = newUiConfig
48 | }
49 |
50 | fun restartData() {
51 | useCase.clearData()
52 | getBrand()
53 | getCategory()
54 | }
55 | }
--------------------------------------------------------------------------------
/libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/AutoPagingAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.network
2 |
3 | import com.utsman.tokobola.core.data.Paged
4 | import com.utsman.tokobola.core.data.orFalse
5 | import com.utsman.tokobola.core.data.orNol
6 | import com.utsman.tokobola.network.response.BasePagedResponse
7 |
8 |
9 | class AutoPagingAdapter(val pagingReducer: ApiReducer>) {
10 |
11 | val listPaged: MutableList = mutableListOf()
12 | var currentPage: Int = 1
13 | var prevPage: Int = 1
14 | var hasNextPage = true
15 |
16 | suspend inline fun executeResponse(crossinline call: suspend (Int) -> BasePagedResponse, crossinline mapper: (T) -> U) {
17 | if (hasNextPage) {
18 | pagingReducer.transform(
19 | call = {
20 | val pagingResponse = call.invoke(currentPage).also {
21 | hasNextPage = it.data?.hasNextPage.orFalse()
22 | prevPage = currentPage
23 | }
24 | pagingResponse
25 | },
26 | mapper = { pagedResponse ->
27 | val dataPaged = pagedResponse.data
28 | val dataList = dataPaged?.data.orEmpty()
29 | .map(mapper)
30 |
31 | currentPage = dataPaged?.page.orNol() + 1
32 |
33 | listPaged.addAll(dataList)
34 | Paged(
35 | data = listPaged,
36 | hasNextPage = dataPaged?.hasNextPage.orFalse(),
37 | page = dataPaged?.page ?: 1,
38 | perPage = dataPaged?.perPage ?: 10
39 | )
40 | }
41 | )
42 | } else {
43 | println("End of reach paging!")
44 | }
45 | }
46 |
47 | fun clear() {
48 | currentPage = 1
49 | prevPage = 0
50 | hasNextPage = true
51 | listPaged.clear()
52 | pagingReducer.clear()
53 | }
54 | }
--------------------------------------------------------------------------------
/features/home/src/commonMain/kotlin/com/utsman/tokobola/home/ui/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.home.ui
2 |
3 | import com.utsman.tokobola.common.entity.Brand
4 | import com.utsman.tokobola.common.entity.ThumbnailProduct
5 | import com.utsman.tokobola.core.ViewModel
6 | import com.utsman.tokobola.home.domain.HomeUseCase
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.launch
9 |
10 | class HomeViewModel(private val homeUseCase: HomeUseCase) : ViewModel() {
11 |
12 | val productsFeaturedState get() = homeUseCase.productsFeaturedReducer.dataFlow
13 | val homeBannerState get() = homeUseCase.productBannerReducer.dataFlow
14 | val brandState get() = homeUseCase.brandReducer.dataFlow
15 |
16 | val productViewed get() = homeUseCase.productViewedReducer.dataFlow
17 |
18 | val productsFeaturedFlow = MutableStateFlow(emptyList())
19 | val brandListFlow = MutableStateFlow(emptyList())
20 |
21 | val isRestart = MutableStateFlow(false)
22 |
23 | fun getHomeProduct() = viewModelScope.launch {
24 | isRestart.value = true
25 | homeUseCase.getProduct()
26 | }
27 |
28 | fun getHomeBanner() = viewModelScope.launch {
29 | homeUseCase.getBanner()
30 | }
31 |
32 | fun getProductViewed() = viewModelScope.launch {
33 | homeUseCase.getAllProductViewed()
34 | }
35 |
36 | fun getBrand() = viewModelScope.launch {
37 | homeUseCase.getBrand()
38 | }
39 |
40 | fun restartData() {
41 | isRestart.value = true
42 | productsFeaturedFlow.value = emptyList()
43 | brandListFlow.value = emptyList()
44 | homeUseCase.clearProductPage()
45 | getHomeBanner()
46 | getHomeProduct()
47 | getBrand()
48 | getProductViewed()
49 | }
50 |
51 | fun postProduct(list: List) {
52 | isRestart.value = false
53 | productsFeaturedFlow.value = list
54 | }
55 |
56 | fun postBrandList(list: List) {
57 | brandListFlow.value = list
58 | }
59 | }
--------------------------------------------------------------------------------
/resources/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | kotlin("native.cocoapods")
4 | id("com.android.library")
5 | id("org.jetbrains.compose")
6 | id("dev.icerock.mobile.multiplatform-resources")
7 | }
8 |
9 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
10 | kotlin {
11 | targetHierarchy.default()
12 |
13 | android {
14 | compilations.all {
15 | kotlinOptions {
16 | jvmTarget = "1.8"
17 | }
18 | }
19 | }
20 |
21 | cocoapods {
22 | version = "1.0.0"
23 | summary = "Some description for the Shared Module"
24 | homepage = "Link to the Shared Module homepage"
25 | ios.deploymentTarget = "14.1"
26 | podfile = project.file("../iosApp/Podfile")
27 | framework {
28 | baseName = "shared"
29 | isStatic = true
30 | export("dev.icerock.moko:resources:0.23.0")
31 | export("dev.icerock.moko:graphics:0.9.0") // toUIColor here
32 | }
33 | extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
34 | extraSpecAttributes["exclude_files"] = "['src/commonMain/resources/MR/**']"
35 | }
36 |
37 | listOf(
38 | iosX64(),
39 | iosArm64(),
40 | iosSimulatorArm64()
41 | ).forEach {
42 | it.binaries.framework {
43 | baseName = "resources"
44 | }
45 | }
46 |
47 | sourceSets {
48 | val commonMain by getting {
49 | dependencies {
50 | api("dev.icerock.moko:resources-compose:0.23.0")
51 | }
52 | }
53 | }
54 | }
55 |
56 | multiplatformResources {
57 | multiplatformResourcesPackage = "com.utsman.tokobola.resources"
58 | multiplatformResourcesClassName = "SharedRes"
59 |
60 | }
61 |
62 | android {
63 | namespace = "com.utsman.tokobola.resources"
64 | sourceSets["main"].resources.srcDirs("src/commonMain/resources/MR/**")
65 |
66 | compileSdk = 33
67 | defaultConfig {
68 | minSdk = 24
69 | }
70 | }
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/ui/product/ProductDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details.ui.product
2 |
3 | import com.utsman.tokobola.core.ViewModel
4 | import com.utsman.tokobola.details.domain.ProductDetailUseCase
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.asStateFlow
7 | import kotlinx.coroutines.launch
8 |
9 | class ProductDetailViewModel(private val productDetailUseCase: ProductDetailUseCase) : ViewModel() {
10 |
11 | val detailState get() = productDetailUseCase.productDetailReducer.dataFlow.asStateFlow()
12 | val wishlistState get() = productDetailUseCase.isProductExist.asStateFlow()
13 |
14 | val uiConfig = MutableStateFlow(ProductDetailUiConfig())
15 |
16 | val productCart get() = productDetailUseCase.productCart.asStateFlow()
17 |
18 | fun getDetail(productId: Int) = viewModelScope.launch {
19 | productDetailUseCase.getDetail(productId)
20 | }
21 |
22 | fun postProductViewed(productId: Int) = viewModelScope.launch {
23 | productDetailUseCase.markProductViewed(productId)
24 | }
25 |
26 | fun updateUiConfig(uiConfig: () -> ProductDetailUiConfig) {
27 | val newUiConfig = uiConfig.invoke()
28 | this.uiConfig.value = newUiConfig
29 | }
30 |
31 | fun getCart(productId: Int) = viewModelScope.launch {
32 | productDetailUseCase.getProductCart(productId)
33 | }
34 |
35 | fun incrementCart(productId: Int) = viewModelScope.launch {
36 | productDetailUseCase.incrementCart(productId)
37 | }
38 |
39 | fun decrementCart(productId: Int) = viewModelScope.launch {
40 | productDetailUseCase.decrementCart(productId)
41 | }
42 |
43 | fun listenWishlist(productId: Int) = viewModelScope.launch {
44 | productDetailUseCase.listenIsWishlistExist(productId)
45 | }
46 |
47 | fun toggleWishlist(productId: Int) = viewModelScope.launch {
48 | productDetailUseCase.toggleWishlist(productId)
49 | }
50 |
51 | override fun onCleared() {
52 | viewModelScope.launch {
53 | productDetailUseCase.clearDetail()
54 | }
55 | super.onCleared()
56 | }
57 | }
--------------------------------------------------------------------------------
/features/details/src/commonMain/kotlin/com/utsman/tokobola/details/domain/ProductDetailUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.details.domain
2 |
3 | import com.utsman.tokobola.common.entity.CartProduct
4 | import com.utsman.tokobola.common.mapToProduct
5 | import com.utsman.tokobola.common.entity.Product
6 | import com.utsman.tokobola.common.toEntity
7 | import com.utsman.tokobola.core.SingletonCreator
8 | import com.utsman.tokobola.network.ApiReducer
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.map
11 |
12 | class ProductDetailUseCase(private val repository: DetailRepository) {
13 |
14 | val productDetailReducer = ApiReducer()
15 |
16 | val productCart = MutableStateFlow(CartProduct())
17 |
18 | val isProductExist = MutableStateFlow(false)
19 |
20 | suspend fun getDetail(productId: Int) {
21 | productDetailReducer.transform(
22 | call = { repository.getDetailProduct(productId) },
23 | mapper = {
24 | it.data?.mapToProduct() ?: Product()
25 | }
26 | )
27 | }
28 |
29 | suspend fun markProductViewed(productId: Int) {
30 | repository.markAsViewed(productId)
31 | }
32 |
33 | suspend fun incrementCart(productId: Int) {
34 | repository.insertOrUpdateCart(productId) {
35 | it + 1
36 | }
37 | }
38 |
39 | suspend fun decrementCart(productId: Int) {
40 | repository.insertOrUpdateCart(productId) {
41 | it - 1
42 | }
43 | }
44 |
45 | suspend fun getProductCart(productId: Int) {
46 | repository.getCartProduct(productId)
47 | .map { it?.toEntity() ?: CartProduct() }
48 | .collect {
49 | productCart.value = it
50 | }
51 | }
52 |
53 | suspend fun listenIsWishlistExist(productId: Int) {
54 | repository.isWishlistExist(productId)
55 | .collect { isExist ->
56 | isProductExist.value = isExist
57 | }
58 | }
59 |
60 | suspend fun toggleWishlist(productId: Int) {
61 | repository.toggleWishlist(productId)
62 | }
63 |
64 | suspend fun clearDetail() {
65 | productDetailReducer.clear()
66 | }
67 |
68 | companion object : SingletonCreator()
69 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.gradle.internal.impldep.org.jsoup.safety.Safelist.basic
2 | rootProject.name = "TokoBola"
3 |
4 | pluginManagement {
5 | repositories {
6 | gradlePluginPortal()
7 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
8 | google()
9 | }
10 |
11 | plugins {
12 | val kotlinVersion = extra["kotlin.version"] as String
13 | val agpVersion = extra["agp.version"] as String
14 | val composeVersion = extra["compose.version"] as String
15 |
16 | kotlin("jvm").version(kotlinVersion)
17 | kotlin("multiplatform").version(kotlinVersion)
18 | kotlin("android").version(kotlinVersion)
19 |
20 | id("com.android.application").version(agpVersion)
21 | id("com.android.library").version(agpVersion)
22 |
23 | id("org.jetbrains.compose").version(composeVersion)
24 | id("dev.icerock.mobile.multiplatform-resources").version("0.23.0")
25 | id("com.codingfeline.buildkonfig").version("0.13.3")
26 | }
27 | }
28 |
29 | dependencyResolutionManagement {
30 | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
31 | repositories {
32 | google()
33 | mavenCentral()
34 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
35 | maven {
36 | url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
37 | authentication {
38 | create("basic")
39 | }
40 | credentials {
41 | // Do not change the username below.
42 | // This should always be `mapbox` (not your username).
43 | username = "mapbox"
44 | // Use the secret token you stored in gradle.properties as the password
45 | password = "sk.eyJ1Ijoia3VjaW5nYXBlcyIsImEiOiJjbGxmdnVtbGIwemdqM2txaHZtam5teGxiIn0.4AlEjvqBWJgaYOElmi5bxA"
46 | }
47 | }
48 | }
49 | }
50 |
51 | include(":androidApp")
52 | include(":shared")
53 | include(":resources")
54 | include(":libraries:core")
55 | include(":libraries:common")
56 | include(":libraries:network")
57 | include(":libraries:api")
58 | include(":libraries:database")
59 | include(":libraries:location")
60 | include(":features:home")
61 | include(":features:details")
62 | include(":features:explore")
63 | include(":features:wishlist")
64 | include(":features:cart")
65 |
--------------------------------------------------------------------------------
/features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/domain/CartRepository.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.cart.domain
2 |
3 | import com.utsman.tokobola.api.mapboxWebApi
4 | import com.utsman.tokobola.api.productWebApi
5 | import com.utsman.tokobola.cart.BuildKonfig
6 | import com.utsman.tokobola.cart.ui.CartUiConfig
7 | import com.utsman.tokobola.common.entity.Cart
8 | import com.utsman.tokobola.common.entity.LocationPlace
9 | import com.utsman.tokobola.common.toRealm
10 | import com.utsman.tokobola.core.SingletonCreator
11 | import com.utsman.tokobola.core.data.LatLon
12 | import com.utsman.tokobola.database.data.CartProductRealm
13 | import com.utsman.tokobola.database.localRepository
14 |
15 | class CartRepository {
16 | private val productWebApi by productWebApi()
17 | private val mapboxWebApi by mapboxWebApi()
18 | private val localRepository by localRepository()
19 |
20 | suspend fun getThumbnailByIds(ids: List) = productWebApi.getThumbnailByIds(ids)
21 |
22 | suspend fun getAllCart() = localRepository.selectAllCart()
23 |
24 | suspend fun replaceCart(list: List) {
25 | val cartProductRealm = list.map {
26 | CartProductRealm().apply {
27 | productId = it.product.id
28 | quantity = it.quantity
29 | millis = it.millis
30 | }
31 | }
32 |
33 | localRepository.replaceAllCart(cartProductRealm)
34 | }
35 |
36 | suspend fun getLocationPlace(latLon: LatLon) = mapboxWebApi.getReverseGeocoding(latLon, BuildKonfig.MAPBOX_TOKEN)
37 | suspend fun searchLocationPlace(query: String, latLon: LatLon) = mapboxWebApi.getSearchGeocoding(query, latLon, BuildKonfig.MAPBOX_TOKEN)
38 |
39 | suspend fun getLocalCurrentLocationPlace() = localRepository.getLocationPlace(CartUiConfig.KEY_LOCATION_CURRENT)
40 | suspend fun getShippingLocationPlace() = localRepository.getLocationPlace(CartUiConfig.KEY_LOCATION_SHIPPING)
41 | suspend fun insertLocalCurrentLocationPlace(locationPlace: LocationPlace) {
42 | localRepository.insertOrUpdateLocationPlace(locationPlace.toRealm(CartUiConfig.KEY_LOCATION_CURRENT))
43 | }
44 |
45 | suspend fun insertShippingLocationPlace(locationPlace: LocationPlace) {
46 | localRepository.insertOrUpdateLocationPlace(locationPlace.toRealm(CartUiConfig.KEY_LOCATION_SHIPPING))
47 | }
48 |
49 | companion object : SingletonCreator()
50 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/MapboxSearchResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api.response
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class MapboxSearchResponse(
9 | @SerialName("type")
10 | val type: String?,
11 | @SerialName("query")
12 | val query: List?,
13 | @SerialName("features")
14 | val features: List?,
15 | @SerialName("attribution")
16 | val attribution: String?
17 | ) {
18 | @Serializable
19 | data class FeatureResponse(
20 | @SerialName("id")
21 | val id: String?,
22 | @SerialName("type")
23 | val type: String?,
24 | @SerialName("place_type")
25 | val placeType: List?,
26 | @SerialName("relevance")
27 | val relevance: Double?,
28 | @SerialName("properties")
29 | val properties: PropertiesResponse?,
30 | @SerialName("text")
31 | val text: String?,
32 | @SerialName("place_name")
33 | val placeName: String?,
34 | @SerialName("center")
35 | val center: List?,
36 | @SerialName("geometry")
37 | val geometry: GeometryResponse?,
38 | @SerialName("context")
39 | val context: List?
40 | ) {
41 | @Serializable
42 | data class PropertiesResponse(
43 | @SerialName("foursquare")
44 | val foursquare: String?,
45 | @SerialName("landmark")
46 | val landmark: Boolean?,
47 | @SerialName("address")
48 | val address: String?,
49 | @SerialName("category")
50 | val category: String?
51 | )
52 |
53 | @Serializable
54 | data class GeometryResponse(
55 | @SerialName("coordinates")
56 | val coordinates: List?,
57 | @SerialName("type")
58 | val type: String?
59 | )
60 |
61 | @Serializable
62 | data class ContextResponse(
63 | @SerialName("id")
64 | val id: String?,
65 | @SerialName("mapbox_id")
66 | val mapboxId: String?,
67 | @SerialName("text")
68 | val text: String?,
69 | @SerialName("wikidata")
70 | val wikidata: String?,
71 | @SerialName("short_code")
72 | val shortCode: String?
73 | )
74 | }
75 | }
--------------------------------------------------------------------------------
/features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/ui/CartViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.cart.ui
2 |
3 | import com.utsman.tokobola.cart.domain.CartUseCase
4 | import com.utsman.tokobola.common.entity.Cart
5 | import com.utsman.tokobola.core.ViewModel
6 | import com.utsman.tokobola.location.LocationTrackerProvider
7 | import dev.icerock.moko.geo.LatLng
8 | import io.ktor.util.date.getTimeMillis
9 | import kotlinx.coroutines.CoroutineExceptionHandler
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.asStateFlow
12 | import kotlinx.coroutines.launch
13 |
14 | class CartViewModel(private val useCase: CartUseCase) : ViewModel() {
15 |
16 | val cartState get() = useCase.cartReducer.dataFlow.asStateFlow()
17 |
18 | val cartUiConfig: MutableStateFlow = MutableStateFlow(CartUiConfig())
19 |
20 | val shippingLocationState = useCase.locationShippingReducer.dataFlow
21 |
22 | fun listenCart() = viewModelScope.launch {
23 | useCase.getCart()
24 | }
25 |
26 | fun pushCart(list: List) {
27 | cartUiConfig.value = CartUiConfig(list)
28 | }
29 |
30 | fun incrementCart(productId: Int) {
31 | val currentCart = cartUiConfig.value
32 | val newCart = updateQuantityInCart(currentCart.carts, productId) {
33 | it+1
34 | }
35 |
36 | cartUiConfig.value = currentCart.copy(carts = newCart, time = getTimeMillis())
37 | }
38 |
39 | fun decrementCart(productId: Int) {
40 | val currentCart = cartUiConfig.value
41 | val newCart = updateQuantityInCart(currentCart.carts, productId) {
42 | it-1
43 | }
44 | cartUiConfig.value = currentCart.copy(carts = newCart, time = getTimeMillis())
45 | }
46 |
47 | private fun updateQuantityInCart(cart: List, productIdToUpdate: Int, operation: (Int) -> Int): List {
48 | val newCart = cart.map {
49 | if (it.product.id == productIdToUpdate) {
50 | it.quantity = operation.invoke(it.quantity)
51 | }
52 | it
53 | }
54 |
55 | return newCart
56 | }
57 |
58 | fun getShippingLocation() = viewModelScope.launch {
59 | useCase.getShippingLocation()
60 | }
61 |
62 | override fun onCleared() {
63 | useCase.stopLocationPlace()
64 | viewModelScope.launch {
65 | useCase.updateCart(cartUiConfig.value.carts)
66 | }
67 | super.onCleared()
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/NavigationProvider.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.Composable
2 | import cafe.adriel.voyager.navigator.LocalNavigator
3 | import cafe.adriel.voyager.navigator.Navigator
4 | import com.utsman.tokobola.core.data.LatLon
5 | import com.utsman.tokobola.core.navigation.LocalScreenContainer
6 | import com.utsman.tokobola.core.navigation.Navigation
7 | import com.utsman.tokobola.core.navigation.ScreenContainer
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 |
10 | class NavigationProvider : Navigation {
11 |
12 | private val navigatorStack = MutableStateFlow(null)
13 | private val screenContainer = MutableStateFlow(null)
14 |
15 | @Composable
16 | fun initialize() {
17 | navigatorStack.value = LocalNavigator.current
18 | screenContainer.value = LocalScreenContainer.current
19 | }
20 |
21 | override fun back(): Boolean {
22 | return tryAction { it.pop() }
23 | }
24 |
25 | override fun goToDetailProduct(id: Int): Boolean {
26 | return tryAction { nav ->
27 | screenContainer.value?.detailProduct(id)?.let { nav.push(it) }
28 | }
29 | }
30 |
31 | override fun goToDetailCategory(categoryId: Int): Boolean {
32 | return tryAction { nav ->
33 | screenContainer.value?.detailCategory(categoryId)?.let { nav.push(it) }
34 | }
35 | }
36 |
37 | override fun goToDetailBrand(brandId: Int): Boolean {
38 | return tryAction { nav ->
39 | screenContainer.value?.detailBrand(brandId)?.let { nav.push(it) }
40 | }
41 | }
42 |
43 | override fun goToSearch(): Boolean {
44 | return tryAction { nav ->
45 | screenContainer.value?.search()?.let { nav.push(it) }
46 | }
47 | }
48 |
49 | override fun goToCart(): Boolean {
50 | return tryAction { nav ->
51 | screenContainer.value?.cart()?.let { nav.push(it) }
52 | }
53 | }
54 |
55 | override fun goToLocationPicker(latLon: LatLon): Boolean {
56 | return tryAction { nav ->
57 | screenContainer.value?.locationPicker(latLon)?.let { nav.push(it) }
58 | }
59 | }
60 |
61 | private fun tryAction(action: (Navigator) -> Unit): Boolean {
62 | return try {
63 | navigatorStack.value?.let(action)
64 | true
65 | } catch (e: IllegalArgumentException) {
66 | e.printStackTrace()
67 | false
68 | } catch (e: Exception) {
69 | e.printStackTrace()
70 | false
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/libraries/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | kotlin("plugin.serialization") version "1.8.21"
6 | }
7 |
8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
9 | kotlin {
10 | targetHierarchy.default()
11 |
12 | android {
13 | compilations.all {
14 | kotlinOptions {
15 | jvmTarget = "1.8"
16 | }
17 | }
18 | }
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach {
25 | it.binaries.framework {
26 | baseName = "core"
27 | }
28 | }
29 |
30 | sourceSets {
31 | val lifecycleVersion = "2.6.1"
32 | val voyagerVersion = "1.0.0-rc05"
33 |
34 | val commonMain by getting {
35 | dependencies {
36 | api(compose.runtime)
37 | api(compose.foundation)
38 | api(compose.material)
39 | @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
40 | api(compose.components.resources)
41 | api(compose.animation)
42 |
43 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
44 |
45 | api(project(":resources"))
46 |
47 | api("io.github.qdsfdhvh:image-loader:1.6.0")
48 | api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
49 |
50 | api("cafe.adriel.voyager:voyager-navigator:$voyagerVersion")
51 | api("cafe.adriel.voyager:voyager-transitions:$voyagerVersion")
52 | api("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion")
53 |
54 | api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
55 |
56 | }
57 | }
58 |
59 | getByName("androidMain") {
60 | dependencies {
61 | api("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
62 | api("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion")
63 |
64 | api("androidx.activity:activity-compose:1.7.2")
65 | api("androidx.appcompat:appcompat:1.6.1")
66 | api("androidx.core:core-ktx:1.10.1")
67 | }
68 | }
69 | }
70 | }
71 |
72 | android {
73 | namespace = "com.utsman.tokobola.core"
74 | compileSdk = 33
75 |
76 | sourceSets["main"].resources.srcDirs("src/commonMain/resources")
77 |
78 | defaultConfig {
79 | minSdk = 24
80 | }
81 | }
--------------------------------------------------------------------------------
/libraries/location/src/commonMain/kotlin/com/utsman/tokobola/location/LocationTracker.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.location
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.utsman.tokobola.core.SingletonCreator
5 | import com.utsman.tokobola.core.State
6 | import com.utsman.tokobola.core.data.LatLon
7 | import com.utsman.tokobola.core.utils.DefaultScope
8 | import dev.icerock.moko.geo.LocationTracker
9 | import dev.icerock.moko.geo.compose.BindLocationTrackerEffect
10 | import dev.icerock.moko.permissions.compose.BindEffect
11 | import kotlinx.coroutines.flow.SharingStarted
12 | import kotlinx.coroutines.flow.firstOrNull
13 | import kotlinx.coroutines.flow.map
14 | import kotlinx.coroutines.flow.onEach
15 | import kotlinx.coroutines.flow.stateIn
16 | import kotlinx.coroutines.flow.transform
17 |
18 | internal expect val locationTracker: LocationTracker
19 |
20 | private var lastKnowLocations: MutableList = mutableListOf()
21 |
22 | class LocationTrackerProvider {
23 | private val tracker = locationTracker
24 |
25 | var isHasStart: Boolean = false
26 | private set
27 |
28 | val locationFlow = tracker.getLocationsFlow()
29 | .map { LatLon(it.latitude, it.longitude) }
30 | .onEach {
31 | if (lastKnowLocations.isEmpty()) {
32 | lastKnowLocations.add(it)
33 | }
34 | }
35 | .stateIn(
36 | DefaultScope(),
37 | SharingStarted.Eagerly,
38 | null
39 | )
40 |
41 | val locationStateFlow = locationFlow
42 | .transform { value ->
43 | if (value == null) {
44 | emit(State.Loading())
45 | } else {
46 | emit(State.Success(value))
47 | }
48 | }
49 | .stateIn(
50 | DefaultScope(),
51 | SharingStarted.Eagerly,
52 | State.Idle()
53 | )
54 |
55 | @Composable
56 | fun bindComposable() {
57 | BindLocationTrackerEffect(tracker)
58 | BindEffect(tracker.permissionsController)
59 | }
60 |
61 | fun getLastKnownLocation(): LatLon? {
62 | return lastKnowLocations.firstOrNull()
63 | }
64 |
65 | suspend fun startTracking() {
66 | if (!isHasStart) {
67 | tracker.startTracking()
68 | isHasStart = true
69 | }
70 | }
71 |
72 | fun stopTracking() {
73 | tracker.stopTracking()
74 | isHasStart = false
75 | }
76 |
77 | suspend fun isEmptyQueue(): Boolean {
78 | return tracker.getLocationsFlow().firstOrNull() == null
79 | }
80 |
81 | companion object : SingletonCreator()
82 | }
--------------------------------------------------------------------------------
/libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/StateTransformation.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.network
2 |
3 | import com.utsman.tokobola.core.State
4 | import io.ktor.client.plugins.ClientRequestException
5 | import kotlinx.serialization.encodeToString
6 | import kotlinx.serialization.json.Json
7 | import kotlinx.serialization.json.jsonObject
8 |
9 | interface StateTransformation {
10 | suspend fun transform(call: suspend () -> U, mapper: (U) -> T): State
11 |
12 | companion object {
13 | @Suppress("FunctionName")
14 | inline fun DefaultResponseTransform(): StateTransformation {
15 | val json = Json {
16 | ignoreUnknownKeys = true
17 | }
18 |
19 | return object : StateTransformation {
20 | override suspend fun transform(call: suspend () -> U, mapper: (U) -> T): State {
21 | val resultEvent: State = try {
22 | val dataSuccess = call.invoke()
23 | val jsonString = Json.encodeToString(dataSuccess)
24 |
25 | val jsonData = json.parseToJsonElement(jsonString)
26 | .jsonObject
27 |
28 | val status = jsonData["status"].toString().toBooleanStrict()
29 | val message = jsonData["message"].toString()
30 |
31 | if (status) {
32 | val dataResult = mapper.invoke(dataSuccess)
33 | State.Success(dataResult)
34 | } else {
35 | State.Failure(Throwable(message))
36 | }
37 | } catch (e: ClientRequestException) {
38 | e.printStackTrace()
39 | State.Failure(e)
40 | } catch (e: IllegalArgumentException) {
41 | e.printStackTrace()
42 | State.Failure(e)
43 | } catch (e: Exception) {
44 | State.Failure(e)
45 | }
46 |
47 | return resultEvent
48 | }
49 |
50 | }
51 | }
52 |
53 | @Suppress("FunctionName")
54 | inline fun SimpleTransform(): StateTransformation {
55 | return object : StateTransformation {
56 | override suspend fun transform(call: suspend () -> U, mapper: (U) -> T): State {
57 | val data = call.invoke()
58 | return State.Success(mapper.invoke(data))
59 | }
60 |
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/MapboxReverseResponse.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api.response
2 |
3 |
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class MapboxReverseResponse(
9 | @SerialName("type")
10 | val type: String?,
11 | @SerialName("features")
12 | val features: List?,
13 | @SerialName("attribution")
14 | val attribution: String?
15 | ) {
16 | @Serializable
17 | data class FeatureResponse(
18 | @SerialName("id")
19 | val id: String?,
20 | @SerialName("type")
21 | val type: String?,
22 | @SerialName("place_type")
23 | val placeType: List?,
24 | @SerialName("relevance")
25 | val relevance: Int?,
26 | @SerialName("properties")
27 | val properties: PropertiesResponse?,
28 | @SerialName("text")
29 | val text: String?,
30 | @SerialName("place_name")
31 | val placeName: String?,
32 | @SerialName("center")
33 | val center: List?,
34 | @SerialName("geometry")
35 | val geometry: GeometryResponse?,
36 | @SerialName("context")
37 | val context: List?,
38 | @SerialName("bbox")
39 | val bbox: List?
40 | ) {
41 | @Serializable
42 | data class PropertiesResponse(
43 | @SerialName("foursquare")
44 | val foursquare: String?,
45 | @SerialName("landmark")
46 | val landmark: Boolean?,
47 | @SerialName("address")
48 | val address: String?,
49 | @SerialName("category")
50 | val category: String?,
51 | @SerialName("mapbox_id")
52 | val mapboxId: String?,
53 | @SerialName("wikidata")
54 | val wikidata: String?,
55 | @SerialName("short_code")
56 | val shortCode: String?
57 | )
58 |
59 | @Serializable
60 | data class GeometryResponse(
61 | @SerialName("coordinates")
62 | val coordinates: List?,
63 | @SerialName("type")
64 | val type: String?
65 | )
66 |
67 | @Serializable
68 | data class ContextResponse(
69 | @SerialName("id")
70 | val id: String?,
71 | @SerialName("mapbox_id")
72 | val mapboxId: String?,
73 | @SerialName("text")
74 | val text: String?,
75 | @SerialName("wikidata")
76 | val wikidata: String?,
77 | @SerialName("short_code")
78 | val shortCode: String?
79 | )
80 | }
81 | }
--------------------------------------------------------------------------------
/libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/ProductWebApi.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.api
2 |
3 | import com.utsman.tokobola.network.response.BasePagedResponse
4 | import com.utsman.tokobola.network.response.BaseResponse
5 | import com.utsman.tokobola.api.response.BrandResponse
6 | import com.utsman.tokobola.api.response.CategoryResponse
7 | import com.utsman.tokobola.api.response.HomeBannerResponse
8 | import com.utsman.tokobola.api.response.ProductResponse
9 | import com.utsman.tokobola.api.response.ThumbnailProductResponse
10 | import com.utsman.tokobola.core.SingletonCreator
11 |
12 | class ProductWebApi : WebDataSource() {
13 |
14 | suspend fun getByFeaturedPaged(page: Int): BasePagedResponse {
15 | return getPaged(WebEndPoint.PRODUCT_FEATURED.withParam("page", page))
16 | }
17 |
18 | suspend fun getByBrandPaged(brandId: Int, page: Int): BasePagedResponse {
19 | return getPaged(WebEndPoint.PRODUCT_BRAND.withParam("page", page).withParam("brand_id", brandId))
20 | }
21 |
22 | suspend fun getByCategoryPaged(categoryId: Int, page: Int): BasePagedResponse {
23 | return getPaged(WebEndPoint.PRODUCT_CATEGORY.withParam("page", page).withParam("category_id", categoryId))
24 | }
25 |
26 | suspend fun getBySearch(page: Int, query: String): BasePagedResponse {
27 | return getPaged(WebEndPoint.PRODUCT_SEARCH.withParam("page", page).withParam("query", query))
28 | }
29 |
30 | suspend fun getThumbnailByIds(ids: List): BaseResponse> {
31 | val newIds = ids.toString().replace("[", "").replace("]", "")
32 | .replace(" ", "")
33 | return get(WebEndPoint.PRODUCT_THUMBNAIL.withParam("id", newIds))
34 | }
35 |
36 | suspend fun getTop(): BaseResponse> {
37 | return get(WebEndPoint.PRODUCT_TOP)
38 | }
39 |
40 | suspend fun getCurated(): BaseResponse> {
41 | return get(WebEndPoint.PRODUCT_CURATED)
42 | }
43 |
44 | suspend fun getDetail(productId: Int): BaseResponse {
45 | return get(WebEndPoint.PRODUCT_DETAIL.withParam("product_id", productId))
46 | }
47 |
48 | suspend fun getHomeBanner(): BaseResponse> {
49 | return get(WebEndPoint.BANNER)
50 | }
51 |
52 | suspend fun getBrand(): BaseResponse> {
53 | return get(WebEndPoint.BRAND)
54 | }
55 |
56 | suspend fun getCategory() : BaseResponse> {
57 | return get(WebEndPoint.CATEGORY)
58 | }
59 |
60 | companion object : SingletonCreator()
61 | }
--------------------------------------------------------------------------------
/features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/domain/LocationPickerUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.cart.domain
2 |
3 | import androidx.compose.runtime.derivedStateOf
4 | import com.utsman.tokobola.common.entity.LocationPlace
5 | import com.utsman.tokobola.common.toLocationPlace
6 | import com.utsman.tokobola.common.toThumbnailProduct
7 | import com.utsman.tokobola.core.SingletonCreator
8 | import com.utsman.tokobola.core.State
9 | import com.utsman.tokobola.core.data.LatLon
10 | import com.utsman.tokobola.location.LocationTrackerProvider
11 | import com.utsman.tokobola.network.ApiReducer
12 | import com.utsman.tokobola.network.StateTransformation
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.flow.filterNotNull
15 | import kotlinx.coroutines.flow.firstOrNull
16 | import kotlinx.coroutines.flow.onStart
17 | import kotlinx.coroutines.flow.takeWhile
18 |
19 | class LocationPickerUseCase(private val repository: CartRepository) {
20 |
21 | val query = MutableStateFlow("")
22 |
23 | val locationSearchReducer = ApiReducer>()
24 | val locationReverseReducer = ApiReducer()
25 |
26 | suspend fun searchLocationPlace(query: String, latLon: LatLon) {
27 | if (this.query.value != query) {
28 | this.query.value = query
29 | }
30 |
31 | when {
32 | (query.count() < 3) -> {
33 | this.query.value = ""
34 | }
35 | else -> {
36 | locationSearchReducer
37 | .transform(
38 | transformation = StateTransformation.SimpleTransform(),
39 | call = {
40 | repository.searchLocationPlace(query, latLon)
41 | },
42 | mapper = { response ->
43 | response.features.orEmpty()
44 | .mapNotNull { it?.toLocationPlace() }
45 | }
46 | )
47 | }
48 | }
49 | }
50 |
51 | suspend fun getLocationReverse(latLon: LatLon) {
52 | locationReverseReducer.transform(
53 | transformation = StateTransformation.SimpleTransform(),
54 | call = {
55 | repository.getLocationPlace(latLon)
56 | },
57 | mapper = {
58 | it.toLocationPlace()
59 | }
60 | )
61 | }
62 |
63 | suspend fun saveShippingAddress(locationPlace: LocationPlace) {
64 | repository.insertShippingLocationPlace(locationPlace)
65 | }
66 |
67 | fun clearData() {
68 | locationSearchReducer.clear()
69 | locationReverseReducer.clear()
70 | query.value = ""
71 | }
72 |
73 | companion object : SingletonCreator()
74 | }
--------------------------------------------------------------------------------
/shared/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | kotlin("native.cocoapods")
4 | id("com.android.library")
5 | id("org.jetbrains.compose")
6 | }
7 |
8 | repositories {
9 | google()
10 | mavenCentral()
11 | }
12 |
13 | kotlin {
14 | android()
15 |
16 | iosX64()
17 | iosArm64()
18 | iosSimulatorArm64()
19 |
20 | cocoapods {
21 | version = "1.0.0"
22 | summary = "Some description for the Shared Module"
23 | homepage = "Link to the Shared Module homepage"
24 | ios.deploymentTarget = "14.1"
25 | podfile = project.file("../iosApp/Podfile")
26 | framework {
27 | baseName = "shared"
28 | isStatic = true
29 | }
30 | extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
31 | pod("MapboxMaps") {
32 | version = "10.15.0"
33 | moduleName = "shared"
34 | }
35 | }
36 |
37 | sourceSets {
38 | val commonMain by getting {
39 | dependencies {
40 | implementation(project(":libraries:core"))
41 | implementation(project(":libraries:database"))
42 | implementation(project(":libraries:location"))
43 |
44 | implementation(project(":libraries:common"))
45 | implementation(project(":features:home"))
46 | implementation(project(":features:explore"))
47 | implementation(project(":features:wishlist"))
48 | implementation(project(":features:details"))
49 | implementation(project(":features:cart"))
50 |
51 | }
52 | }
53 | val androidMain by getting {
54 | dependencies {
55 | api("com.google.android.gms:play-services-location:21.0.1")
56 | }
57 | }
58 | val iosX64Main by getting
59 | val iosArm64Main by getting
60 | val iosSimulatorArm64Main by getting
61 | val iosMain by creating {
62 | dependsOn(commonMain)
63 | iosX64Main.dependsOn(this)
64 | iosArm64Main.dependsOn(this)
65 | iosSimulatorArm64Main.dependsOn(this)
66 | }
67 | }
68 | }
69 |
70 | android {
71 | compileSdk = (findProperty("android.compileSdk") as String).toInt()
72 | namespace = "com.utsman.tokobola.common"
73 |
74 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
75 | sourceSets["main"].res.srcDirs("src/androidMain/res")
76 | sourceSets["main"].resources.srcDirs("src/commonMain/resources")
77 |
78 | defaultConfig {
79 | minSdk = (findProperty("android.minSdk") as String).toInt()
80 | targetSdk = (findProperty("android.targetSdk") as String).toInt()
81 | }
82 | compileOptions {
83 | sourceCompatibility = JavaVersion.VERSION_11
84 | targetCompatibility = JavaVersion.VERSION_11
85 | }
86 | kotlin {
87 | jvmToolchain(11)
88 | }
89 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/ScaffoldPullRefresh.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.animation.core.animateDpAsState
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.BoxScope
6 | import androidx.compose.foundation.layout.BoxWithConstraints
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.lazy.grid.GridCells
10 | import androidx.compose.foundation.lazy.grid.LazyGridScope
11 | import androidx.compose.foundation.lazy.grid.LazyGridState
12 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
13 | import androidx.compose.material.Scaffold
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.derivedStateOf
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.mutableStateOf
18 | import androidx.compose.runtime.rememberCoroutineScope
19 | import androidx.compose.runtime.setValue
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.shadow
22 | import androidx.compose.ui.layout.onGloballyPositioned
23 | import androidx.compose.ui.layout.onPlaced
24 | import androidx.compose.ui.unit.Dp
25 | import androidx.compose.ui.unit.IntSize
26 | import androidx.compose.ui.unit.dp
27 | import com.utsman.tokobola.core.utils.pxToDp
28 | import com.utsman.tokobola.core.utils.rememberNavigationBarHeightDp
29 | import kotlinx.coroutines.delay
30 | import kotlinx.coroutines.launch
31 |
32 | @Composable
33 | fun ScaffoldGridState(
34 | modifier: Modifier = Modifier.fillMaxSize(),
35 | lazyGridState: LazyGridState,
36 | fixColumn: Int = 2,
37 | topBarPadding: Dp = Dimens.HeightTopBarSearch,
38 | bottomBarPadding: Dp = (6 + (rememberNavigationBarHeightDp().value * 2)).dp,
39 | topBar: @Composable BoxScope.() -> Unit = {},
40 | pullRefresh: @Composable BoxScope.() -> Unit = {},
41 | content: LazyGridScope.() -> Unit
42 | ) {
43 |
44 | val isNeedLift by derivedStateOf {
45 | lazyGridState.canScrollBackward
46 | }
47 |
48 | val elevation by animateDpAsState(
49 | if (isNeedLift) 12.dp else 0.dp
50 | )
51 |
52 | Scaffold {
53 | BoxWithConstraints {
54 |
55 | }
56 | Box(modifier = modifier) {
57 | LazyVerticalGrid(
58 | columns = GridCells.Fixed(fixColumn),
59 | contentPadding = PaddingValues(
60 | top = topBarPadding + 12.dp,
61 | bottom = bottomBarPadding,
62 | start = 6.dp,
63 | end = 6.dp
64 | ),
65 | state = lazyGridState,
66 | content = content,
67 | )
68 |
69 | Box(
70 | modifier = Modifier
71 | .shadow(elevation)
72 | ) {
73 | topBar()
74 | }
75 | pullRefresh()
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/ui/WishlishCompose.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.wishlist.ui
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.foundation.lazy.grid.GridItemSpan
5 | import androidx.compose.foundation.lazy.grid.items
6 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.collectAsState
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.unit.dp
12 | import com.utsman.tokobola.common.component.Dimens
13 | import com.utsman.tokobola.common.component.EmptyScreen
14 | import com.utsman.tokobola.common.component.ErrorScreen
15 | import com.utsman.tokobola.common.component.ProductItemGrid
16 | import com.utsman.tokobola.common.component.ScaffoldGridState
17 | import com.utsman.tokobola.common.component.SearchBarStaticWithTitle
18 | import com.utsman.tokobola.common.component.Shimmer
19 | import com.utsman.tokobola.core.rememberViewModel
20 | import com.utsman.tokobola.core.utils.onFailure
21 | import com.utsman.tokobola.core.utils.onIdle
22 | import com.utsman.tokobola.core.utils.onLoading
23 | import com.utsman.tokobola.core.utils.onSuccess
24 | import com.utsman.tokobola.wishlist.LocalWishlistUseCase
25 |
26 | @Composable
27 | fun Wishlist() {
28 | val useCase = LocalWishlistUseCase.current
29 | val viewModel = rememberViewModel { WishlistViewModel(useCase) }
30 |
31 | val productState by viewModel.productWishlistState.collectAsState()
32 |
33 | val lazyGridState = rememberLazyGridState()
34 |
35 | ScaffoldGridState(
36 | topBar = {
37 | SearchBarStaticWithTitle(
38 | lazyGridState = lazyGridState,
39 | title = "Your Football Wishlist"
40 | )
41 | },
42 | topBarPadding = Dimens.HeightTopBarSearchWithTitle,
43 | lazyGridState = lazyGridState,
44 | modifier = Modifier.fillMaxSize()
45 | ) {
46 | with(productState) {
47 | onIdle {
48 | viewModel.listenProductWishlist()
49 | }
50 | onLoading {
51 | items(
52 | items = listOf(1, 2)
53 | ) {
54 | Shimmer()
55 | }
56 | }
57 | onSuccess { products ->
58 | if (products.isEmpty()) {
59 | item(
60 | span = { GridItemSpan(maxLineSpan) }
61 | ) {
62 | EmptyScreen()
63 | }
64 | } else {
65 | items(products) { product ->
66 | ProductItemGrid(product)
67 | }
68 | }
69 | }
70 | onFailure {
71 | item(
72 | span = { GridItemSpan(maxLineSpan) }
73 | ) {
74 | ErrorScreen(it)
75 | }
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if %ERRORLEVEL% equ 0 goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if %ERRORLEVEL% equ 0 goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | set EXIT_CODE=%ERRORLEVEL%
84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
86 | exit /b %EXIT_CODE%
87 |
88 | :mainEnd
89 | if "%OS%"=="Windows_NT" endlocal
90 |
91 | :omega
92 |
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/MapView.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.derivedStateOf
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.saveable.Saver
10 | import androidx.compose.runtime.saveable.rememberSaveable
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Modifier
13 | import com.utsman.tokobola.core.data.LatLon
14 | import com.utsman.tokobola.core.utils.getOrNull
15 | import com.utsman.tokobola.location.LocalLocationTrackerProvider
16 | import dev.icerock.moko.geo.LatLng
17 | import kotlinx.coroutines.MainScope
18 | import kotlinx.coroutines.delay
19 | import kotlinx.coroutines.flow.firstOrNull
20 | import kotlinx.coroutines.launch
21 |
22 | @Composable
23 | expect fun MapView(
24 | mapConfigState: MapConfigState = rememberMapConfigState(),
25 | modifier: Modifier = Modifier
26 | )
27 |
28 | data class MapConfig(
29 | val currentLatLon: LatLon = LatLon()
30 | )
31 |
32 | internal interface MapAction {
33 | fun getCenterLatLon(): LatLon
34 | fun zoomIn()
35 | fun zoomOut()
36 | fun setLocation(latLon: LatLon)
37 | fun addAnnotation(latLon: LatLon, title: String? = null)
38 | }
39 |
40 | fun LatLng.toLatLon(): LatLon {
41 | return LatLon(latitude, longitude)
42 | }
43 |
44 | class MapConfigState(currentLatLon: LatLon) {
45 | var currentLatLon: LatLon by mutableStateOf(currentLatLon)
46 |
47 | internal var mapAction: MapAction? = null
48 |
49 | fun getCenterLocation(): LatLon {
50 | return mapAction?.getCenterLatLon() ?: LatLon()
51 | }
52 |
53 | fun zoomIn() = mapAction?.zoomIn()
54 | fun zoomOut() = mapAction?.zoomOut()
55 |
56 | fun setLocation(latLon: LatLon) {
57 | MainScope().launch {
58 | delay(100)
59 | mapAction?.setLocation(latLon)
60 | }
61 | }
62 | fun addAnnotation(latLon: LatLon, title: String? = null) = mapAction?.addAnnotation(latLon, title)
63 |
64 | companion object {
65 | val Saver: Saver = Saver(
66 | save = {
67 | it.currentLatLon.json()
68 | },
69 | restore = {
70 | MapConfigState(LatLon.fromJson(it))
71 | }
72 | )
73 | }
74 | }
75 |
76 | @Composable
77 | fun rememberMapConfigState(latLon: LatLon? = null): MapConfigState {
78 | val state = if (latLon == null || latLon.isBlank()) {
79 | val trackerProvider = LocalLocationTrackerProvider.current
80 | val location = trackerProvider.getLastKnownLocation()
81 |
82 | LaunchedEffect(Unit) {
83 | trackerProvider.startTracking()
84 | }
85 |
86 | val newLatLon by derivedStateOf {
87 | location
88 | }
89 |
90 | MapConfigState(newLatLon ?: LatLon())
91 | } else {
92 | MapConfigState(latLon)
93 | }
94 |
95 |
96 | return rememberSaveable(saver = MapConfigState.Saver) { state }
97 | }
--------------------------------------------------------------------------------
/features/home/src/commonMain/kotlin/com/utsman/tokobola/home/domain/HomeUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.home.domain
2 |
3 | import com.utsman.tokobola.common.entity.Brand
4 | import com.utsman.tokobola.common.entity.HomeBanner
5 | import com.utsman.tokobola.common.entity.ThumbnailProduct
6 | import com.utsman.tokobola.common.toBrand
7 | import com.utsman.tokobola.common.toHomeBanner
8 | import com.utsman.tokobola.common.toThumbnailProduct
9 | import com.utsman.tokobola.core.SingletonCreator
10 | import com.utsman.tokobola.core.data.Paged
11 | import com.utsman.tokobola.network.ApiReducer
12 | import com.utsman.tokobola.network.AutoPagingAdapter
13 | import com.utsman.tokobola.network.StateTransformation
14 |
15 | class HomeUseCase(private val homeRepository: HomeRepository) {
16 |
17 | val productsFeaturedReducer = ApiReducer>()
18 | val productBannerReducer = ApiReducer>()
19 | val brandReducer = ApiReducer>()
20 |
21 | val productViewedReducer = ApiReducer>()
22 |
23 | private val featurePagingAdapter = AutoPagingAdapter(productsFeaturedReducer)
24 |
25 | suspend fun getProduct() {
26 | featurePagingAdapter.executeResponse(
27 | call = {
28 | homeRepository.getFeaturedProductPaged(it)
29 | },
30 | mapper = {
31 | it.toThumbnailProduct()
32 | }
33 | )
34 | }
35 |
36 | suspend fun getBanner() {
37 | productBannerReducer.transform(
38 | call = {
39 | homeRepository.getBanner()
40 | },
41 | mapper = { bannerResponse ->
42 | bannerResponse.data?.map { it.toHomeBanner() }.orEmpty()
43 | }
44 | )
45 | }
46 |
47 | suspend fun getBrand() {
48 | brandReducer.transform(
49 | call = {
50 | homeRepository.getBrand()
51 | },
52 | mapper = { brandResponse ->
53 | brandResponse.data?.map { it.toBrand() }.orEmpty()
54 | .filter {
55 | // filter "other" brand
56 | it.id != 40
57 | }
58 | }
59 | )
60 | }
61 |
62 | suspend fun getAllProductViewed() {
63 | homeRepository.getAllRecentlyViewedFlow()
64 | .collect { realms ->
65 | productViewedReducer.transform(
66 | transformation = StateTransformation.SimpleTransform(),
67 | call = {
68 | val ids = realms.map { it.productId }
69 | homeRepository.getThumbnailByIds(ids)
70 | },
71 | mapper = { thumbnailProductResponse ->
72 | thumbnailProductResponse.data
73 | ?.map { it.toThumbnailProduct() }
74 | .orEmpty()
75 | }
76 | )
77 | }
78 | }
79 |
80 | fun clearProductPage() {
81 | featurePagingAdapter.clear()
82 | brandReducer.clear()
83 | productViewedReducer.clear()
84 | }
85 |
86 | companion object : SingletonCreator()
87 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/TopBar.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.interaction.MutableInteractionSource
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.foundation.layout.wrapContentHeight
12 | import androidx.compose.foundation.lazy.grid.LazyGridState
13 | import androidx.compose.foundation.shape.CircleShape
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.material.Text
16 | import androidx.compose.material.ripple.rememberRipple
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.draw.clip
23 | import androidx.compose.ui.graphics.Color
24 | import androidx.compose.ui.graphics.ColorFilter
25 | import androidx.compose.ui.text.font.FontWeight
26 | import androidx.compose.ui.unit.dp
27 | import com.utsman.tokobola.core.navigation.LocalNavigation
28 | import com.utsman.tokobola.core.utils.rememberStatusBarHeightDp
29 | import com.utsman.tokobola.resources.SharedRes
30 | import dev.icerock.moko.resources.compose.painterResource
31 |
32 | @Composable
33 | fun TopBar(text: String, modifier: Modifier = Modifier, lazyGridState: LazyGridState) {
34 | val navigation = LocalNavigation.current
35 | val titleColor by lazyGridState.animatedColor(
36 | from = Color.White,
37 | to = MaterialTheme.colors.primary
38 | )
39 |
40 | val topBarColor by lazyGridState.animatedTopBarColor
41 |
42 | Row(
43 | modifier = modifier
44 | .background(color = topBarColor)
45 | .wrapContentHeight()
46 | .padding(
47 | top = 12.dp + rememberStatusBarHeightDp(),
48 | start = 12.dp,
49 | end = 12.dp,
50 | bottom = 12.dp
51 | ),
52 | horizontalArrangement = Arrangement.Center,
53 | verticalAlignment = Alignment.CenterVertically
54 | ) {
55 | Image(
56 | modifier = Modifier.size(34.dp)
57 | .clickable(
58 | interactionSource = remember { MutableInteractionSource() },
59 | indication = rememberRipple(false),
60 | onClick = {
61 | navigation.back()
62 | })
63 | .clip(CircleShape)
64 | .background(
65 | color = MaterialTheme.colors.secondary.copy(alpha = 0.6f)
66 | )
67 | .padding(6.dp),
68 | painter = painterResource(SharedRes.images.arrow_back_default),
69 | contentDescription = "",
70 | colorFilter = ColorFilter.tint(Color.White)
71 | )
72 |
73 | Text(
74 | text = text,
75 | fontWeight = FontWeight.Bold,
76 | modifier = Modifier.weight(1f).padding(12.dp),
77 | color = titleColor
78 | )
79 | }
80 | }
--------------------------------------------------------------------------------
/libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/ErrorScreen.kt:
--------------------------------------------------------------------------------
1 | package com.utsman.tokobola.common.component
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.size
11 | import androidx.compose.material.MaterialTheme
12 | import androidx.compose.material.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.text.style.TextAlign
19 | import androidx.compose.ui.unit.dp
20 | import com.utsman.tokobola.resources.SharedRes
21 | import dev.icerock.moko.resources.compose.painterResource
22 |
23 | @Composable
24 | fun ErrorScreen(throwable: Throwable) {
25 | Column(
26 | modifier = Modifier.fillMaxSize(),
27 | horizontalAlignment = Alignment.CenterHorizontally,
28 | verticalArrangement = Arrangement.Center
29 | ) {
30 | val painter = painterResource(SharedRes.images.image_error)
31 | Image(
32 | painter = painter,
33 | contentDescription = "",
34 | modifier = Modifier.size(100.dp)
35 | )
36 | Text(
37 | text = throwable.message.orEmpty(),
38 | color = Color.Red.copy(alpha = 0.7f),
39 | fontWeight = FontWeight.Black,
40 | textAlign = TextAlign.Center,
41 | modifier = Modifier.fillMaxWidth()
42 | .padding(22.dp)
43 | )
44 | }
45 | }
46 |
47 | @Composable
48 | fun SimpleErrorScreen(throwable: Throwable) {
49 | Row(
50 | modifier = Modifier.fillMaxSize()
51 | .padding(12.dp),
52 | verticalAlignment = Alignment.CenterVertically
53 | ) {
54 | val painter = painterResource(SharedRes.images.icon_emoji_error)
55 | Image(
56 | painter = painter,
57 | contentDescription = "",
58 | modifier = Modifier.size(24.dp)
59 | )
60 | Text(
61 | text = throwable.message.orEmpty(),
62 | color = Color.Red.copy(alpha = 0.7f),
63 | fontWeight = FontWeight.Black,
64 | textAlign = TextAlign.Center,
65 | modifier = Modifier.padding(22.dp)
66 | )
67 | }
68 | }
69 |
70 | @Composable
71 | fun EmptyScreen(message: String = "Nothing") {
72 | Column(
73 | modifier = Modifier.fillMaxSize(),
74 | horizontalAlignment = Alignment.CenterHorizontally,
75 | verticalArrangement = Arrangement.Center
76 | ) {
77 | val painter = painterResource(SharedRes.images.image_empty)
78 | Image(
79 | painter = painter,
80 | contentDescription = "",
81 | modifier = Modifier.size(180.dp)
82 | )
83 | Text(
84 | text = message,
85 | color = MaterialTheme.colors.primary,
86 | fontWeight = FontWeight.Black,
87 | textAlign = TextAlign.Center,
88 | modifier = Modifier.fillMaxWidth()
89 | .padding(22.dp)
90 | )
91 | }
92 | }
--------------------------------------------------------------------------------