├── .github
├── CODEOWNERS
└── workflows
│ ├── build.yml
│ ├── detekt.yml
│ └── module-graphs.yml
├── .gitignore
├── README.md
├── app
├── .gitignore
├── README.md
├── build.gradle.kts
├── config
│ └── detekt
│ │ └── detekt.yml
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── ibrahimkurt
│ │ └── multimodreelcompose
│ │ ├── MainActivity.kt
│ │ └── MultiModReelApp.kt
│ └── res
│ ├── drawable
│ ├── ic_launcher_background.xml
│ └── ic_launcher_foreground.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build-logic
├── convention
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ ├── AndroidComposePlugin.kt
│ │ ├── AndroidHiltPlugin.kt
│ │ ├── AndroidLibraryPlugin.kt
│ │ ├── ApplicationComposePlugin.kt
│ │ ├── ApplicationPlugin.kt
│ │ ├── DetektPlugin.kt
│ │ ├── JvmLibraryPlugin.kt
│ │ ├── RetrofitSerializationPlugin.kt
│ │ ├── UISetupPlugin.kt
│ │ └── com
│ │ └── ibrahimkurt
│ │ └── convention
│ │ └── LibExt.kt
├── gradle.properties
└── settings.gradle.kts
├── build.gradle.kts
├── core
├── common
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── ibrahimkurt
│ │ └── core
│ │ └── common
│ │ └── util
│ │ ├── APIConst.kt
│ │ ├── Constants.kt
│ │ ├── PagingException.kt
│ │ ├── Resource.kt
│ │ └── TokenConst.kt
├── component
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── kotlin
│ │ └── com
│ │ │ └── ibrahimkurt
│ │ │ └── core
│ │ │ └── component
│ │ │ ├── CoilImage.kt
│ │ │ ├── ReelIconButtonType.kt
│ │ │ ├── ReelPreview.kt
│ │ │ ├── ReelScaffold.kt
│ │ │ ├── ReelTopAppBarType.kt
│ │ │ ├── extensions
│ │ │ └── Surface.kt
│ │ │ ├── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ │ └── util
│ │ │ └── Constants.kt
│ │ └── res
│ │ └── font
│ │ ├── montserrat_light.ttf
│ │ ├── montserrat_medium.ttf
│ │ ├── montserrat_regular.ttf
│ │ ├── montserrat_thin.ttf
│ │ └── worksans_bold.ttf
├── data
│ ├── .gitignore
│ ├── README.md
│ └── build.gradle.kts
├── domain
│ ├── .gitignore
│ ├── README.md
│ └── build.gradle.kts
├── network
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── ibrahimkurt
│ │ │ └── core
│ │ │ └── network
│ │ │ ├── calladapter
│ │ │ ├── NetworkResult.kt
│ │ │ ├── NetworkResultCall.kt
│ │ │ ├── NetworkResultCallAdapter.kt
│ │ │ └── NetworkResultCallAdapterFactory.kt
│ │ │ ├── di
│ │ │ └── NetworkModule.kt
│ │ │ ├── extensions
│ │ │ └── NetworkExt.kt
│ │ │ ├── intercapter
│ │ │ ├── AuthTokenInterceptor.kt
│ │ │ └── NetworkInterceptor.kt
│ │ │ └── util
│ │ │ ├── ErrorCategory.kt
│ │ │ └── NetworkUnavailableException.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── pagination
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── pagination
│ │ └── BasePagingSource.kt
└── ui
│ ├── .gitignore
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ └── main
│ ├── kotlin
│ └── com
│ │ └── ibrahimkurt
│ │ └── core
│ │ └── ui
│ │ ├── extensions
│ │ └── NavExt.kt
│ │ └── widgets
│ │ └── FilmCard.kt
│ └── res
│ └── drawable
│ └── examplefastx.jpg
├── docs
└── images
│ ├── graphs
│ ├── 1__ZrkCb8QE0nK4FApKzKgmA.webp
│ ├── dep_graph_app.svg
│ ├── dep_graph_core_common.svg
│ ├── dep_graph_core_component.svg
│ ├── dep_graph_core_data.svg
│ ├── dep_graph_core_domain.svg
│ ├── dep_graph_core_network.svg
│ ├── dep_graph_core_pagination.svg
│ ├── dep_graph_core_ui.svg
│ ├── dep_graph_features_detail_data.svg
│ ├── dep_graph_features_detail_domain.svg
│ ├── dep_graph_features_detail_ui.svg
│ ├── dep_graph_features_home_data.svg
│ ├── dep_graph_features_home_domain.svg
│ ├── dep_graph_features_home_ui.svg
│ ├── dep_graph_features_template_data.svg
│ ├── dep_graph_features_template_domain.svg
│ ├── dep_graph_features_template_ui.svg
│ └── dep_graph_navigation.svg
│ └── screenshot
│ ├── detail.png
│ └── home.png
├── features
├── detail
│ ├── data
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ └── main
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── features
│ │ │ └── detail
│ │ │ └── data
│ │ │ ├── di
│ │ │ ├── DetailBindsModule.kt
│ │ │ └── DetailProvidesModule.kt
│ │ │ ├── dto
│ │ │ ├── CreatedBy.kt
│ │ │ ├── Genre.kt
│ │ │ ├── LastEpisodeToAir.kt
│ │ │ ├── Network.kt
│ │ │ ├── NextEpisodeToAir.kt
│ │ │ ├── ProductionCompany.kt
│ │ │ ├── ProductionCountry.kt
│ │ │ ├── Season.kt
│ │ │ ├── SpokenLanguage.kt
│ │ │ └── TvSeriesDetailsResponse.kt
│ │ │ ├── mapper
│ │ │ └── DetailMapper.kt
│ │ │ ├── repository
│ │ │ └── DetailRepositoryImpl.kt
│ │ │ └── source
│ │ │ └── DetailApiService.kt
│ ├── domain
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ └── main
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── features
│ │ │ └── detail
│ │ │ └── domain
│ │ │ ├── model
│ │ │ └── TvSeriesDetail.kt
│ │ │ ├── repository
│ │ │ └── DetailRepository.kt
│ │ │ └── usecase
│ │ │ └── GetDetailUseCase.kt
│ └── ui
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── com
│ │ └── features
│ │ └── detail
│ │ └── ui
│ │ ├── DetailNavigation.kt
│ │ ├── DetailScreen.kt
│ │ └── DetailViewModel.kt
├── home
│ ├── data
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ └── main
│ │ │ └── java
│ │ │ └── com
│ │ │ └── ibrahimkurt
│ │ │ └── features
│ │ │ └── home
│ │ │ └── data
│ │ │ ├── di
│ │ │ ├── HomeBindsModule.kt
│ │ │ └── HomeProvidesModule.kt
│ │ │ ├── dto
│ │ │ ├── ResultDto.kt
│ │ │ └── TvShowResponseDto.kt
│ │ │ ├── mapper
│ │ │ └── HomeMapper.kt
│ │ │ ├── repository
│ │ │ └── HomeRepositoryImpl.kt
│ │ │ └── source
│ │ │ └── HomeApiService.kt
│ ├── domain
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ └── main
│ │ │ └── java
│ │ │ └── com
│ │ │ └── ibrahimkurt
│ │ │ └── features
│ │ │ └── home
│ │ │ └── domain
│ │ │ ├── model
│ │ │ ├── TvShow.kt
│ │ │ └── TvShowList.kt
│ │ │ ├── repository
│ │ │ └── HomeRepository.kt
│ │ │ └── usecase
│ │ │ └── GetTvUseCase.kt
│ └── ui
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ └── main
│ │ └── java
│ │ └── com
│ │ └── ibrahimkurt
│ │ └── features
│ │ └── home
│ │ └── ui
│ │ ├── HomeNavigation.kt
│ │ ├── HomeScreen.kt
│ │ └── HomeViewModel.kt
└── template
│ ├── data
│ ├── .gitignore
│ ├── README.md
│ └── build.gradle.kts
│ ├── domain
│ ├── .gitignore
│ ├── README.md
│ └── build.gradle.kts
│ └── ui
│ ├── .gitignore
│ ├── README.md
│ └── build.gradle.kts
├── generateModuleGraphs.sh
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── navigation
├── .gitignore
├── README.md
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── com
│ └── ibrahimkurt
│ └── navigation
│ └── AppNavHost.kt
└── settings.gradle.kts
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | /core/ @ubuntuyiw
2 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ "master" ]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout Repository
12 | uses: actions/checkout@v3
13 |
14 | - name: Set up JDK 17
15 | uses: actions/setup-java@v3
16 | with:
17 | java-version: '17'
18 | distribution: 'temurin'
19 |
20 | - name: Cache Gradle Wrapper and Caches
21 | uses: actions/cache@v3
22 | with:
23 | path: |
24 | ~/.gradle/caches
25 | ~/.gradle/wrapper
26 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
27 | restore-keys: |
28 | ${{ runner.os }}-gradle-
29 |
30 | - name: Set up environment variables
31 | run: |
32 | echo "SDK_DIR=${{ secrets.SDK_DIR }}" >> $GITHUB_ENV
33 | echo "API_KEY=${{ secrets.API_KEY }}" >> $GITHUB_ENV
34 |
35 | - name: Grant execute permission for gradlew
36 | run: chmod +x gradlew
37 |
38 | - name: Build with Gradle
39 | run: ./gradlew build --parallel
--------------------------------------------------------------------------------
/.github/workflows/detekt.yml:
--------------------------------------------------------------------------------
1 | name: Detekt Static Code Analysis
2 |
3 | on:
4 | pull_request:
5 | branches: [ "master" ]
6 |
7 | jobs:
8 | detekt:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 |
13 | - name: Set up JDK 17
14 | uses: actions/setup-java@v3
15 | with:
16 | java-version: '17'
17 | distribution: 'temurin'
18 |
19 | - name: Run Detekt and generate reports
20 | run: |
21 | chmod +x gradlew
22 | ./gradlew detekt -Pconfig-file=path/to/your/detekt.yml -Poutput-format=xml,html -Poutput-dir=path/to/your/reports
23 |
24 | - name: Upload Detekt reports
25 | uses: actions/upload-artifact@v3
26 | with:
27 | name: detekt-reports
28 | path: |
29 | path/to/your/reports/*.xml
30 | path/to/your/reports/*.html
31 |
--------------------------------------------------------------------------------
/.github/workflows/module-graphs.yml:
--------------------------------------------------------------------------------
1 | name: Generate Module Graphs and Commit
2 |
3 | on:
4 | pull_request:
5 | branches: [ "github/module-graph" ]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | with:
13 | token: ${{ secrets.PERSONAL_TOKEN }}
14 | persist-credentials: true
15 | fetch-depth: 0
16 | ref: ${{ github.head_ref }}
17 |
18 | - name: Set up JDK 17
19 | uses: actions/setup-java@v3
20 | with:
21 | java-version: '17'
22 | distribution: 'temurin'
23 |
24 | - name: Install Graphviz
25 | run: sudo apt-get install graphviz
26 |
27 | - name: Grant execute permission for gradlew
28 | run: chmod +x gradlew
29 |
30 | - name: Generate Module Graphs
31 | run: |
32 | chmod +x ./generateModuleGraphs.sh
33 | ./generateModuleGraphs.sh --no-configure-on-demand
34 |
35 | - name: Setup Local Branch
36 | run: |
37 | git checkout -B github/module-graph
38 |
39 | - name: Check for Changes
40 | id: check_changes
41 | run: |
42 | git status --porcelain
43 | if [ -z "$(git status --porcelain)" ]; then
44 | echo "No changes detected."
45 | echo "::set-output name=changes_exist::false"
46 | else
47 | echo "Changes detected."
48 | echo "::set-output name=changes_exist::true"
49 | fi
50 |
51 | - name: Commit and Push Changes to github/module-graph
52 | if: steps.check_changes.outputs.changes_exist == 'true'
53 | run: |
54 | git config --global user.email "github@actions.com"
55 | git config --global user.name "GitHub Actions"
56 | git add .
57 | git commit -m "CI generated updates"
58 | git push --set-upstream origin github/module-graph
59 | env:
60 | GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .externalNativeBuild
3 | .cxx
4 |
5 | # Gradle files
6 | .gradle/
7 | build/
8 |
9 | # Local configuration file (sdk path, etc)
10 | local.properties
11 |
12 | # Log/OS Files
13 | *.log
14 |
15 | # Android Studio generated files and folders
16 | captures/
17 | .externalNativeBuild/
18 | *.apk
19 | output.json
20 | output-metadata.json
21 |
22 | # IntelliJ
23 | *.iml
24 | .idea/
25 | misc.xml
26 | deploymentTargetDropDown.xml
27 | render.experimental.xml
28 |
29 |
30 | # Keystore files
31 | *.jks
32 | *.keystore
33 | changes.patch
34 |
35 | # Google Services (e.g. APIs or Firebase)
36 | google-services.json
37 |
38 | # Android Profiling
39 | *.hprof
40 |
41 | bin/
42 | gen/
43 | out/
44 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 | # :app module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.application)
3 | alias(libs.plugins.ibrahimkurt.android.compose)
4 | alias(libs.plugins.ibrahimkurt.android.uiSetup)
5 |
6 | }
7 |
8 | android {
9 | namespace = "com.ibrahimkurt.multimodreelcompose"
10 |
11 | defaultConfig {
12 | applicationId = "com.ibrahimkurt.multimodreelcompose"
13 | versionCode = 1
14 | versionName = "1.0"
15 |
16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
17 | vectorDrawables {
18 | useSupportLibrary = true
19 | }
20 | }
21 |
22 | buildTypes {
23 | release {
24 | isMinifyEnabled = false
25 | proguardFiles(
26 | getDefaultProguardFile("proguard-android-optimize.txt"),
27 | "proguard-rules.pro"
28 | )
29 | signingConfig = signingConfigs.getByName("debug")
30 | }
31 | }
32 | packaging {
33 | resources {
34 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
35 | }
36 | }
37 | }
38 |
39 | dependencies {
40 | implementation(projects.core.component)
41 | implementation(projects.navigation)
42 | implementation(libs.androidx.core.ktx)
43 | implementation(libs.androidx.activity.compose)
44 |
45 | implementation(projects.features.home.data)
46 | implementation(projects.features.detail.data)
47 | implementation(libs.coil)
48 |
49 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibrahimkurt/multimodreelcompose/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.multimodreelcompose
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import com.ibrahimkurt.navigation.AppNavHost
8 | import dagger.hilt.android.AndroidEntryPoint
9 |
10 | @AndroidEntryPoint
11 | class MainActivity : ComponentActivity() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | enableEdgeToEdge()
15 | setContent {
16 | AppNavHost()
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ibrahimkurt/multimodreelcompose/MultiModReelApp.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.multimodreelcompose
2 |
3 | import android.app.Application
4 | import coil.ImageLoader
5 | import coil.ImageLoaderFactory
6 | import dagger.hilt.android.HiltAndroidApp
7 | import javax.inject.Inject
8 |
9 | @HiltAndroidApp
10 | class MultiModReelApp : Application(), ImageLoaderFactory {
11 |
12 | @Inject
13 | lateinit var imageLoader: dagger.Lazy
14 |
15 | override fun newImageLoader(): ImageLoader = imageLoader.get()
16 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MultiModReelCompose
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build-logic/convention/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/build-logic/convention/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | }
6 |
7 | group = "com.ibrahimkurt.convention.buildlogic"
8 |
9 | java {
10 | sourceCompatibility = JavaVersion.VERSION_17
11 | targetCompatibility = JavaVersion.VERSION_17
12 | }
13 |
14 | tasks.withType().configureEach {
15 | kotlinOptions {
16 | jvmTarget = JavaVersion.VERSION_17.toString()
17 | }
18 | }
19 |
20 | dependencies {
21 | compileOnly(libs.android.gradlePlugin)
22 | compileOnly(libs.kotlin.gradlePlugin)
23 | compileOnly(libs.detekt.gradlePlugin)
24 |
25 | }
26 |
27 | gradlePlugin {
28 | plugins {
29 | register("androidCompose") {
30 | id = "ibrahimkurt.android.library.compose"
31 | implementationClass = "AndroidComposePlugin"
32 | }
33 | register("androidHilt") {
34 | id = "ibrahimkurt.android.hilt"
35 | implementationClass = "AndroidHiltPlugin"
36 | }
37 | register("androidLibrary") {
38 | id = "ibrahimkurt.android.library"
39 | implementationClass = "AndroidLibraryPlugin"
40 | }
41 | register("applicationCompose") {
42 | id = "ibrahimkurt.android.compose"
43 | implementationClass = "ApplicationComposePlugin"
44 | }
45 | register("application") {
46 | id = "ibrahimkurt.android.application"
47 | implementationClass = "ApplicationPlugin"
48 | }
49 | register("detekt") {
50 | id = "ibrahimkurt.jvm.detekt"
51 | implementationClass = "DetektPlugin"
52 | }
53 | register("jvmLibrary") {
54 | id = "ibrahimkurt.jvm.library"
55 | implementationClass = "JvmLibraryPlugin"
56 | }
57 | register("retrofitSerialization") {
58 | id = "ibrahimkurt.android.retrofit-serialization"
59 | implementationClass = "RetrofitSerializationPlugin"
60 | }
61 | register("UISetup") {
62 | id = "ibrahimkurt.android.ui-setup"
63 | implementationClass = "UISetupPlugin"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/AndroidComposePlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.LibraryExtension
2 | import com.ibrahimkurt.convention.libs
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.dependencies
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | class AndroidComposePlugin: Plugin {
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("com.android.library")
13 | }
14 | val extension = extensions.getByType()
15 |
16 | with(extension) {
17 | buildFeatures {
18 | compose = true
19 | }
20 |
21 | composeOptions {
22 | kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString()
23 | }
24 |
25 | dependencies {
26 | val bom = libs.findLibrary("androidx-compose-bom").get()
27 | add("implementation", platform(bom))
28 | add("implementation", libs.findLibrary("androidx-ui-tooling-preview").get())
29 | add("debugImplementation", libs.findLibrary("androidx-ui-tooling").get())
30 | }
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/AndroidHiltPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.ibrahimkurt.convention.libs
2 | import org.gradle.api.Plugin
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.dependencies
5 |
6 | class AndroidHiltPlugin: Plugin {
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("com.google.devtools.ksp")
11 | apply("com.google.dagger.hilt.android")
12 | }
13 |
14 | dependencies {
15 | "implementation"(libs.findLibrary("hilt.android").get())
16 | "ksp"(libs.findLibrary("hilt.compiler").get())
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/AndroidLibraryPlugin.kt:
--------------------------------------------------------------------------------
1 |
2 | import com.android.build.api.dsl.LibraryExtension
3 | import org.gradle.api.JavaVersion
4 | import org.gradle.api.Plugin
5 | import org.gradle.api.Project
6 | import org.gradle.kotlin.dsl.configure
7 | import org.gradle.kotlin.dsl.withType
8 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
9 |
10 | class AndroidLibraryPlugin : Plugin {
11 | override fun apply(target: Project) {
12 | with(target) {
13 | with(pluginManager) {
14 | apply("com.android.library")
15 | apply("org.jetbrains.kotlin.android")
16 | apply("ibrahimkurt.jvm.detekt")
17 | }
18 |
19 | extensions.configure() {
20 | compileSdk = 34
21 | defaultConfig {
22 | minSdk = 23
23 | }
24 |
25 |
26 | compileOptions {
27 | sourceCompatibility = JavaVersion.VERSION_17
28 | targetCompatibility = JavaVersion.VERSION_17
29 | }
30 |
31 | tasks.withType().configureEach {
32 | kotlinOptions {
33 | jvmTarget = JavaVersion.VERSION_17.toString()
34 | }
35 | }
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/ApplicationComposePlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ApplicationExtension
2 | import com.ibrahimkurt.convention.libs
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.dependencies
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | class ApplicationComposePlugin: Plugin {
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("com.android.application")
13 | }
14 | val extension = extensions.getByType()
15 |
16 | with(extension) {
17 | buildFeatures {
18 | compose = true
19 | }
20 |
21 | composeOptions {
22 | kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString()
23 | }
24 |
25 | dependencies {
26 | val bom = libs.findLibrary("androidx-compose-bom").get()
27 | add("implementation", platform(bom))
28 | add("implementation", libs.findLibrary("androidx-ui-tooling-preview").get())
29 | add("debugImplementation", libs.findLibrary("androidx-ui-tooling").get())
30 | }
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/ApplicationPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.api.dsl.ApplicationExtension
2 | import org.gradle.api.JavaVersion
3 | import org.gradle.api.Plugin
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.configure
6 | import org.gradle.kotlin.dsl.withType
7 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
8 |
9 | class ApplicationPlugin : Plugin {
10 | override fun apply(target: Project) {
11 | with(target) {
12 | with(pluginManager) {
13 | apply("com.android.application")
14 | apply("org.jetbrains.kotlin.android")
15 | apply("ibrahimkurt.jvm.detekt")
16 | }
17 |
18 | extensions.configure {
19 | defaultConfig.targetSdk = 34
20 | apply {
21 | compileSdk = 34
22 |
23 | defaultConfig {
24 | minSdk = 23
25 | }
26 |
27 | compileOptions {
28 | sourceCompatibility = JavaVersion.VERSION_17
29 | targetCompatibility = JavaVersion.VERSION_17
30 | }
31 |
32 | tasks.withType().configureEach {
33 | kotlinOptions {
34 | jvmTarget = JavaVersion.VERSION_17.toString()
35 | }
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/DetektPlugin.kt:
--------------------------------------------------------------------------------
1 |
2 | import com.ibrahimkurt.convention.libs
3 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension
4 | import org.gradle.api.Plugin
5 | import org.gradle.api.Project
6 | import org.gradle.kotlin.dsl.configure
7 | import org.gradle.kotlin.dsl.dependencies
8 |
9 | class DetektPlugin : Plugin {
10 | override fun apply(project: Project) {
11 | with(project) {
12 | pluginManager.apply("io.gitlab.arturbosch.detekt")
13 | allprojects {
14 | configure {
15 | toolVersion = "1.23.6"
16 | autoCorrect = true
17 | buildUponDefaultConfig = true
18 | config.setFrom("${rootProject.projectDir}/app/config/detekt/detekt.yml")
19 | source.setFrom("src/main/java", "src/main/kotlin")
20 | parallel = false
21 | allRules = true
22 | }
23 | }
24 |
25 | dependencies {
26 | add("detektPlugins", libs.findLibrary("detekt").get())
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/JvmLibraryPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.JavaVersion
2 | import org.gradle.api.Plugin
3 | import org.gradle.api.Project
4 | import org.gradle.api.plugins.JavaPluginExtension
5 | import org.gradle.kotlin.dsl.configure
6 | import org.gradle.kotlin.dsl.withType
7 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
8 |
9 | class JvmLibraryPlugin : Plugin {
10 | override fun apply(target: Project) {
11 | with(target) {
12 | with(pluginManager) {
13 | apply("org.jetbrains.kotlin.jvm")
14 | }
15 |
16 | extensions.configure {
17 | sourceCompatibility = JavaVersion.VERSION_17
18 | targetCompatibility = JavaVersion.VERSION_17
19 |
20 | tasks.withType().configureEach {
21 | kotlinOptions {
22 | jvmTarget = JavaVersion.VERSION_17.toString()
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/RetrofitSerializationPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.ibrahimkurt.convention.libs
2 | import org.gradle.api.Plugin
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.dependencies
5 |
6 | class RetrofitSerializationPlugin: Plugin {
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("org.jetbrains.kotlin.plugin.serialization")
11 | apply("kotlinx-serialization")
12 | }
13 |
14 | dependencies {
15 | add("implementation", libs.findLibrary("retrofit.core").get())
16 | add("implementation", libs.findLibrary("kotlinx.serialization.json").get())
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/UISetupPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.ibrahimkurt.convention.libs
2 | import org.gradle.api.Plugin
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.dependencies
5 |
6 | class UISetupPlugin : Plugin {
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("ibrahimkurt.android.hilt")
11 | }
12 |
13 | dependencies {
14 | add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
15 | add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
16 | add("implementation", libs.findLibrary("androidx.lifecycle.runtime.ktx").get())
17 | add("implementation", libs.findLibrary("androidx.navigation.compose").get())
18 | add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get())
19 | }
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/java/com/ibrahimkurt/convention/LibExt.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.convention
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.VersionCatalog
5 | import org.gradle.api.artifacts.VersionCatalogsExtension
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | val Project.libs
9 | get(): VersionCatalog = extensions.getByType().named("libs")
--------------------------------------------------------------------------------
/build-logic/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.parallel=true
2 | org.gradle.caching=true
3 | org.gradle.configureondemand=true
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | versionCatalogs {
7 | create("libs") {
8 | from(files("../gradle/libs.versions.toml"))
9 | }
10 | }
11 | }
12 |
13 | rootProject.name = "build-logic"
14 | include(":convention")
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.jetbrains.kotlin.android) apply false
5 | alias(libs.plugins.jetbrains.kotlin.jvm) apply false
6 | alias(libs.plugins.hilt) apply false
7 | alias(libs.plugins.ksp) apply false
8 | alias(libs.plugins.android.library) apply false
9 | alias(libs.plugins.kotlin.serialization) apply false
10 | alias(libs.plugins.detekt) apply false
11 | alias(libs.plugins.module.graph) apply true
12 | }
13 |
14 | tasks.register("createFeatureModule") {
15 | doLast {
16 | val moduleName = project.findProperty("moduleName") as? String
17 | ?: throw IllegalArgumentException("Module name not provided")
18 | val moduleType = project.findProperty("moduleType") as? String
19 | ?: throw IllegalArgumentException("Module type not provided")
20 |
21 | val types = if (moduleType == "all") listOf("ui", "data", "domain") else listOf(moduleType)
22 |
23 | types.forEach { type ->
24 | val modulePath = "features:$moduleName:$type"
25 | val srcDir = file("features/template/$type")
26 | val destDir = file("features/$moduleName/$type")
27 |
28 | srcDir.copyRecursively(destDir, overwrite = true)
29 |
30 | val settingsFile = rootProject.file("settings.gradle.kts")
31 | settingsFile.appendText("\ninclude(\"$modulePath\")")
32 |
33 | fun updatePackagePaths(sourcePath: String) {
34 | val oldPackageDir = destDir.resolve(sourcePath)
35 | val newPackageDir = destDir.resolve(sourcePath.replace("template", moduleName))
36 | if (oldPackageDir.exists()) {
37 | oldPackageDir.renameTo(newPackageDir)
38 | }
39 |
40 | newPackageDir.walkTopDown().forEach { file ->
41 | if (file.extension in listOf("kt", "java")) {
42 | val content = file.readText().replace(
43 | "com.ibrahimkurt.features.template",
44 | "com.ibrahimkurt.features.$moduleName")
45 | file.writeText(content)
46 | }
47 | }
48 | }
49 |
50 | updatePackagePaths("src/main/java/com/ibrahimkurt/features/template")
51 | updatePackagePaths("src/androidTest/java/com/ibrahimkurt/features/template")
52 | updatePackagePaths("src/test/java/com/ibrahimkurt/features/template")
53 | }
54 | }
55 | }
56 |
57 | tasks.register("printModulePaths") {
58 | subprojects {
59 | if (subprojects.size == 0) {
60 | println(this.path)
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/core/common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/common/README.md:
--------------------------------------------------------------------------------
1 | # :core:common module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/core/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.jvm.library)
3 | }
4 |
5 | group = "com.ibrahimkurt.core.common"
6 |
7 |
8 | dependencies {
9 |
10 | }
--------------------------------------------------------------------------------
/core/common/src/main/java/com/ibrahimkurt/core/common/util/APIConst.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.common.util
2 |
3 | object APIConst {
4 | const val IMAGE_URL = "https://image.tmdb.org/t/p/w500"
5 | const val ORIGINAL_IMAGE_URL = "https://image.tmdb.org/t/p/original"
6 | }
--------------------------------------------------------------------------------
/core/common/src/main/java/com/ibrahimkurt/core/common/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.common.util
2 |
3 | import com.ibrahimkurt.core.common.util.Constants.ZERO_DOUBLE
4 | import com.ibrahimkurt.core.common.util.Constants.ZERO_FLOAT
5 | import com.ibrahimkurt.core.common.util.Constants.ZERO_INT
6 |
7 | object Constants {
8 | const val EMPTY_STRING = ""
9 | const val ZERO_DOUBLE = 0.0
10 | const val ZERO_INT = 0
11 | const val ZERO_FLOAT = 0f
12 | }
13 |
14 | fun Int?.orZero() = this ?: ZERO_INT
15 | fun Float?.orZero() = this ?: ZERO_FLOAT
16 | fun Double?.orZero() = this ?: ZERO_DOUBLE
17 | fun Boolean?.orFalse() = this ?: false
18 | fun Boolean?.orTrue() = this ?: true
--------------------------------------------------------------------------------
/core/common/src/main/java/com/ibrahimkurt/core/common/util/PagingException.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.common.util
2 |
3 |
4 | class PagingException(
5 | val error: String = Constants.EMPTY_STRING,
6 | val resError: Int = 0
7 | ) : Exception()
--------------------------------------------------------------------------------
/core/common/src/main/java/com/ibrahimkurt/core/common/util/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.common.util
2 |
3 | import com.ibrahimkurt.core.common.util.Constants.EMPTY_STRING
4 |
5 |
6 | interface ResourceProvider {
7 | fun getString(resourceId: Int): String
8 | }
9 |
10 | data class ErrorMessage(
11 | val message: String = EMPTY_STRING,
12 | val resourceId: Int? = null
13 | )
14 |
15 | sealed interface Resource {
16 | data class Success(val data: T) : Resource {
17 | suspend fun Resource.getDataOrNull(dataCallBack: suspend (T) -> N?): N? {
18 | return when (this) {
19 | is Success -> dataCallBack(this.data)
20 | is Error -> null
21 | }
22 | }
23 | }
24 |
25 | data class Error(val error: ErrorMessage) : Resource {
26 | fun getErrorMessage(resourceProvider: ResourceProvider): String {
27 | return error.message.ifBlank { error.resourceId?.let { resourceProvider.getString(it) } ?: "Unknown error" }
28 | }
29 |
30 | fun toPagingException(): PagingException {
31 | return PagingException(error.message, error.resourceId ?: 0)
32 | }
33 | }
34 | }
35 |
36 | suspend fun Resource.map(data: suspend (T) -> N): Resource {
37 | return when (this) {
38 | is Resource.Success -> Resource.Success(data(this.data))
39 | is Resource.Error -> Resource.Error(this.error)
40 | }
41 | }
42 |
43 | suspend fun Resource.onSuccess(data: suspend (T) -> Unit): Resource {
44 | when (this) {
45 | is Resource.Success -> data(this.data)
46 | is Resource.Error -> this.error
47 | }
48 | return this
49 | }
50 |
51 | suspend fun Resource.onFailure(failure: suspend (ErrorMessage) -> Unit): Resource {
52 | when (this) {
53 | is Resource.Success -> this.data
54 | is Resource.Error -> failure(this.error)
55 | }
56 | return this
57 | }
58 |
59 | suspend fun Resource.onCompletion(onCompletion: suspend (T?, ErrorMessage?) -> Unit): Resource {
60 | when (this) {
61 | is Resource.Success -> onCompletion(this.data, null)
62 | is Resource.Error -> onCompletion(null, this.error)
63 | }
64 | return this
65 | }
--------------------------------------------------------------------------------
/core/common/src/main/java/com/ibrahimkurt/core/common/util/TokenConst.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.common.util
2 |
3 | import com.ibrahimkurt.core.common.util.Constants.EMPTY_STRING
4 |
5 | object TokenConst {
6 | @Volatile
7 | var token: String = EMPTY_STRING
8 | }
--------------------------------------------------------------------------------
/core/component/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/component/README.md:
--------------------------------------------------------------------------------
1 | # :core:component module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/core/component/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.library.compose)
4 | }
5 |
6 | android {
7 | namespace = "com.ibrahimkurt.core.component"
8 |
9 | }
10 |
11 | dependencies {
12 | api(libs.androidx.ui)
13 | api(libs.androidx.ui.graphics)
14 | api(libs.androidx.material3)
15 | api(libs.androidx.compose.material.iconsExtended)
16 | api(libs.coil)
17 | api(libs.coilCompose)
18 | }
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/CoilImage.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Alignment
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.graphics.ColorFilter
7 | import androidx.compose.ui.graphics.DefaultAlpha
8 | import androidx.compose.ui.graphics.FilterQuality
9 | import androidx.compose.ui.graphics.drawscope.DrawScope
10 | import androidx.compose.ui.layout.ContentScale
11 | import androidx.compose.ui.platform.LocalContext
12 | import coil.ImageLoader
13 | import coil.compose.AsyncImage
14 | import coil.compose.AsyncImagePainter
15 | import coil.compose.DefaultModelEqualityDelegate
16 | import coil.compose.EqualityDelegate
17 |
18 | @Composable
19 | fun CoilImage(
20 | model: Any?,
21 | contentDescription: String?,
22 | imageLoader: ImageLoader,
23 | modifier: Modifier = Modifier,
24 | transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform,
25 | onState: ((AsyncImagePainter.State) -> Unit)? = null,
26 | alignment: Alignment = Alignment.Center,
27 | contentScale: ContentScale = ContentScale.Fit,
28 | alpha: Float = DefaultAlpha,
29 | colorFilter: ColorFilter? = null,
30 | filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
31 | clipToBounds: Boolean = true,
32 | modelEqualityDelegate: EqualityDelegate = DefaultModelEqualityDelegate,
33 | ) {
34 | AsyncImage(
35 | model = model,
36 | contentDescription = contentDescription,
37 | imageLoader = imageLoader,
38 | modifier = modifier,
39 | transform = transform,
40 | onState = onState,
41 | alignment = alignment,
42 | contentScale = contentScale,
43 | alpha = alpha,
44 | colorFilter = colorFilter,
45 | filterQuality = filterQuality,
46 | clipToBounds = clipToBounds,
47 | modelEqualityDelegate = modelEqualityDelegate,
48 | )
49 | }
50 |
51 | @ReelPreview
52 | @Composable
53 | private fun CoilImagePreview() {
54 | CoilImage(
55 | model = null,
56 | contentDescription = null,
57 | imageLoader = ImageLoader(LocalContext.current)
58 | )
59 | }
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/ReelIconButtonType.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component
2 |
3 | import androidx.compose.foundation.interaction.MutableInteractionSource
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
6 | import androidx.compose.material3.FilledIconButton
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.IconButtonColors
9 | import androidx.compose.material3.IconButtonDefaults
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.graphics.Shape
16 | import com.ibrahimkurt.core.component.theme.MultiModReelComposeTheme
17 | import com.ibrahimkurt.core.component.util.Constants.EMPTY_STRING
18 |
19 | object ReelIconButtonType {
20 | @Composable
21 | fun reelIconButtonColor(
22 | containerColor: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
23 | contentColor: Color = MaterialTheme.colorScheme.primary,
24 | ): IconButtonColors = IconButtonDefaults.filledIconButtonColors(
25 | containerColor = containerColor,
26 | contentColor = contentColor,
27 | disabledContainerColor = containerColor,
28 | disabledContentColor = contentColor
29 | )
30 |
31 | @Composable
32 | fun ReelIconButton(
33 | onClick: () -> Unit,
34 | modifier: Modifier = Modifier,
35 | enabled: Boolean = true,
36 | shape: Shape = IconButtonDefaults.filledShape,
37 | colors: IconButtonColors = reelIconButtonColor(),
38 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
39 | content: @Composable () -> Unit
40 | ) {
41 | FilledIconButton(
42 | onClick = onClick,
43 | modifier = modifier,
44 | enabled = enabled,
45 | colors = colors,
46 | interactionSource = interactionSource,
47 | content = content,
48 | shape = shape,
49 | )
50 | }
51 | }
52 |
53 | @ReelPreview
54 | @Composable
55 | fun SADecrementButtonIconButtonPreview() {
56 | MultiModReelComposeTheme {
57 | ReelIconButtonType.ReelIconButton(
58 | onClick = {
59 | }
60 | ) {
61 | Icon(
62 | imageVector = Icons.AutoMirrored.Default.ArrowBack,
63 | contentDescription = EMPTY_STRING,
64 | tint = MaterialTheme.colorScheme.primary
65 | )
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/ReelPreview.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component
2 |
3 | import androidx.compose.ui.tooling.preview.Preview
4 |
5 | @Preview(
6 | device = "spec:width=390dp,height=844dp,dpi=320",
7 | showBackground = true,
8 | backgroundColor = 0xFFFEFEFE
9 | )
10 | annotation class ReelPreview
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/ReelScaffold.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.WindowInsets
6 | import androidx.compose.foundation.layout.fillMaxHeight
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.material3.FabPosition
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Scaffold
11 | import androidx.compose.material3.ScaffoldDefaults
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.Color
16 | import com.ibrahimkurt.core.component.theme.MultiModReelComposeTheme
17 |
18 | object ReelScaffoldType {
19 |
20 | @Composable
21 | private fun SAScaffold(
22 | modifier: Modifier = Modifier,
23 | topBar: @Composable () -> Unit = {},
24 | bottomBar: @Composable () -> Unit = {},
25 | snackbarHost: @Composable () -> Unit = {},
26 | containerColor: Color = MaterialTheme.colorScheme.background,
27 | contentColor: Color = MaterialTheme.colorScheme.onBackground,
28 | floatingActionButton: @Composable () -> Unit = {},
29 | floatingActionButtonPosition: FabPosition = FabPosition.End,
30 | contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
31 | content: @Composable (PaddingValues) -> Unit
32 | ) {
33 | Scaffold(
34 | modifier = modifier.fillMaxSize(),
35 | topBar = topBar,
36 | bottomBar = bottomBar,
37 | snackbarHost = snackbarHost,
38 | floatingActionButton = floatingActionButton,
39 | floatingActionButtonPosition = floatingActionButtonPosition,
40 | containerColor = containerColor,
41 | contentColor = contentColor,
42 | contentWindowInsets = contentWindowInsets,
43 | content = {
44 | content(it)
45 | },
46 | )
47 | }
48 |
49 | @Composable
50 | fun PrimaryScaffold(
51 | modifier: Modifier = Modifier,
52 | topBar: @Composable () -> Unit = {},
53 | bottomBar: @Composable () -> Unit = {},
54 | snackbarHost: @Composable () -> Unit = {},
55 | containerColor: Color = MaterialTheme.colorScheme.background,
56 | contentColor: Color = MaterialTheme.colorScheme.onBackground,
57 | floatingActionButton: @Composable () -> Unit = {},
58 | floatingActionButtonPosition: FabPosition = FabPosition.End,
59 | contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
60 | content: @Composable (PaddingValues) -> Unit
61 | ) {
62 | SAScaffold(
63 | modifier = Modifier
64 | .fillMaxSize()
65 | .then(modifier),
66 | topBar = topBar,
67 | bottomBar = bottomBar,
68 | contentColor = contentColor,
69 | containerColor = containerColor,
70 | snackbarHost = snackbarHost,
71 | floatingActionButton = floatingActionButton,
72 | floatingActionButtonPosition = floatingActionButtonPosition,
73 | contentWindowInsets = contentWindowInsets,
74 | content = {
75 | Box(
76 | contentAlignment = Alignment.TopCenter,
77 | modifier = Modifier.fillMaxSize()
78 | ) {
79 | Box(
80 | contentAlignment = Alignment.TopCenter,
81 | modifier = Modifier.fillMaxHeight()
82 | ) {
83 | content(it)
84 | }
85 | }
86 | },
87 | )
88 | }
89 | }
90 |
91 | @ReelPreview
92 | @Composable
93 | fun SAPrimaryScaffoldPreview() {
94 | MultiModReelComposeTheme {
95 | ReelScaffoldType.PrimaryScaffold {}
96 | }
97 | }
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/ReelTopAppBarType.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.RowScope
5 | import androidx.compose.foundation.layout.WindowInsets
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.material3.TopAppBar
14 | import androidx.compose.material3.TopAppBarColors
15 | import androidx.compose.material3.TopAppBarDefaults
16 | import androidx.compose.material3.TopAppBarScrollBehavior
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.platform.LocalDensity
22 | import androidx.compose.ui.unit.LayoutDirection
23 | import androidx.compose.ui.unit.dp
24 | import com.ibrahimkurt.core.component.ReelTopAppBarType.ReelTopAppBar
25 | import com.ibrahimkurt.core.component.theme.MultiModReelComposeTheme
26 | import com.ibrahimkurt.core.component.util.Constants.EMPTY_STRING
27 |
28 | object ReelTopAppBarType {
29 | @OptIn(ExperimentalMaterial3Api::class)
30 | @Composable
31 | fun reelColor(
32 | containerColor: Color = MaterialTheme.colorScheme.surface,
33 | scrolledContainerColor: Color = MaterialTheme.colorScheme.primary,
34 | navigationIconContentColor: Color = MaterialTheme.colorScheme.secondary,
35 | titleContentColor: Color = MaterialTheme.colorScheme.onBackground,
36 | ): TopAppBarColors = TopAppBarDefaults.topAppBarColors(
37 | containerColor = containerColor,
38 | scrolledContainerColor = scrolledContainerColor,
39 | navigationIconContentColor = navigationIconContentColor,
40 | titleContentColor = titleContentColor,
41 | )
42 |
43 | @OptIn(ExperimentalMaterial3Api::class)
44 | @Composable
45 | fun ReelTopAppBar(
46 | modifier: Modifier = Modifier,
47 | title: @Composable () -> Unit = {},
48 | navigationIcon: @Composable () -> Unit = {},
49 | actions: @Composable RowScope.() -> Unit = {},
50 | colors: TopAppBarColors = reelColor(),
51 | scrollBehavior: TopAppBarScrollBehavior? = null
52 | ) {
53 | val localDensity = LocalDensity.current
54 | val left = TopAppBarDefaults.windowInsets.getLeft(localDensity, LayoutDirection.Ltr)
55 | val right = TopAppBarDefaults.windowInsets.getLeft(localDensity, LayoutDirection.Rtl)
56 | val top = TopAppBarDefaults.windowInsets.getTop(localDensity)
57 | val padding = WindowInsets(
58 | left = left,
59 | right = right,
60 | top = top,
61 | bottom = 0
62 | )
63 | TopAppBar(
64 | title = title,
65 | modifier = modifier,
66 | windowInsets = padding,
67 | navigationIcon = {
68 | Box(
69 | contentAlignment = Alignment.Center,
70 | modifier = Modifier.padding(start = 16.dp)
71 | ) {
72 | navigationIcon()
73 | }
74 | },
75 | actions = actions,
76 | colors = colors,
77 | scrollBehavior = scrollBehavior
78 | )
79 | }
80 | }
81 |
82 | @OptIn(ExperimentalMaterial3Api::class)
83 | @ReelPreview
84 | @Composable
85 | private fun ReelTopAppBarPreview() {
86 | MultiModReelComposeTheme {
87 | ReelTopAppBar(
88 | title = {
89 | Text(text = "MultiModReelCompose")
90 | },
91 | navigationIcon = {
92 | ReelIconButtonType.ReelIconButton(
93 | onClick = {},
94 | colors = ReelIconButtonType.reelIconButtonColor()
95 | ) {
96 | Icon(
97 | imageVector = Icons.AutoMirrored.Default.ArrowBack,
98 | contentDescription = EMPTY_STRING,
99 | )
100 | }
101 | }
102 | )
103 | }
104 | }
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/extensions/Surface.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component.extensions
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.border
6 | import androidx.compose.runtime.Stable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.draw.clip
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.Shape
11 | import androidx.compose.ui.graphics.graphicsLayer
12 |
13 | @Stable
14 | fun Modifier.surface(
15 | shape: Shape,
16 | backgroundColor: Color,
17 | border: BorderStroke?,
18 | shadowElevation: Float,
19 | spotShadowColor: Color = Color.Gray,
20 | ) = this
21 | .graphicsLayer(spotShadowColor = spotShadowColor, shadowElevation = shadowElevation, shape = shape, clip = true)
22 | .then(if (border != null) Modifier.border(border, shape) else Modifier)
23 | .background(color = backgroundColor, shape = shape)
24 | .clip(shape)
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val DarkBlue = Color(0xFF0d253f)
6 | val LightBlue = Color(0xFF01b4e4)
7 | val LightGreen = Color(0xFF90cea1)
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = DarkBlue,
16 | secondary = LightBlue,
17 | tertiary = LightGreen,
18 | background = Color.White,
19 | onBackground = Color.Black
20 | )
21 |
22 | private val LightColorScheme = lightColorScheme(
23 | primary = DarkBlue,
24 | secondary = LightBlue,
25 | tertiary = LightGreen,
26 | background = Color.White,
27 | onBackground = Color.Black
28 | )
29 |
30 | @Composable
31 | fun MultiModReelComposeTheme(
32 | darkTheme: Boolean = isSystemInDarkTheme(),
33 | dynamicColor: Boolean = false,
34 | content: @Composable () -> Unit
35 | ) {
36 | val colorScheme = when {
37 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
38 | val context = LocalContext.current
39 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
40 | }
41 |
42 | darkTheme -> DarkColorScheme
43 | else -> LightColorScheme
44 | }
45 |
46 | MaterialTheme(
47 | colorScheme = colorScheme,
48 | typography = Typography,
49 | content = content
50 | )
51 | }
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.unit.sp
8 | import com.ibrahimkurt.core.component.R
9 |
10 | val Typography = Typography(
11 | titleMedium = TextStyle(
12 | fontFamily = FontFamily(Font(R.font.worksans_bold)),
13 | fontSize = 16.sp,
14 | lineHeight = 24.sp,
15 | ),
16 | bodyMedium = TextStyle(
17 | fontFamily = FontFamily(Font(R.font.montserrat_regular)),
18 | fontSize = 16.sp,
19 | lineHeight = 24.sp
20 | ),
21 | labelMedium = TextStyle(
22 | fontFamily = FontFamily(Font(R.font.montserrat_thin)),
23 | fontSize = 16.sp,
24 | lineHeight = 24.sp
25 | )
26 | )
--------------------------------------------------------------------------------
/core/component/src/main/kotlin/com/ibrahimkurt/core/component/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.component.util
2 |
3 | object Constants {
4 | const val EMPTY_STRING = ""
5 | }
--------------------------------------------------------------------------------
/core/component/src/main/res/font/montserrat_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/core/component/src/main/res/font/montserrat_light.ttf
--------------------------------------------------------------------------------
/core/component/src/main/res/font/montserrat_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/core/component/src/main/res/font/montserrat_medium.ttf
--------------------------------------------------------------------------------
/core/component/src/main/res/font/montserrat_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/core/component/src/main/res/font/montserrat_regular.ttf
--------------------------------------------------------------------------------
/core/component/src/main/res/font/montserrat_thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/core/component/src/main/res/font/montserrat_thin.ttf
--------------------------------------------------------------------------------
/core/component/src/main/res/font/worksans_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/core/component/src/main/res/font/worksans_bold.ttf
--------------------------------------------------------------------------------
/core/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/data/README.md:
--------------------------------------------------------------------------------
1 | # :core:data module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/core/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.hilt)
4 | alias(libs.plugins.ibrahimkurt.android.retrofitSerialization)
5 | }
6 |
7 | android {
8 | namespace = "com.ibrahimkurt.core.data"
9 | }
10 |
11 | dependencies {
12 | implementation(projects.core.domain)
13 | api(projects.core.common)
14 | api(projects.core.network)
15 |
16 | }
--------------------------------------------------------------------------------
/core/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/domain/README.md:
--------------------------------------------------------------------------------
1 | # :core:domain module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/core/domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.jvm.library)
3 | }
4 |
5 | group = "com.ibrahimkurt.core.domain"
6 |
7 | dependencies {
8 | api(projects.core.common)
9 | implementation(libs.javax.inject)
10 | implementation(libs.kotlinx.coroutines.core)
11 |
12 |
13 | }
--------------------------------------------------------------------------------
/core/network/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/network/README.md:
--------------------------------------------------------------------------------
1 | # :core:network module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/core/network/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.konan.properties.Properties
2 | plugins {
3 | alias(libs.plugins.ibrahimkurt.android.library)
4 | alias(libs.plugins.ibrahimkurt.android.hilt)
5 | alias(libs.plugins.ibrahimkurt.android.retrofitSerialization)
6 | }
7 | val localProperties = Properties().apply {
8 | val localPropertiesFile = rootProject.file("local.properties")
9 | if (localPropertiesFile.exists()) {
10 | localPropertiesFile.reader().use { load(it) }
11 | }
12 | }
13 | val apiKey = localProperties.getProperty("API_KEY") ?: System.getenv("API_KEY")
14 | android {
15 | namespace = "com.ibrahimkurt.core.network"
16 |
17 | buildFeatures {
18 | buildConfig = true
19 |
20 | }
21 |
22 | buildTypes {
23 | getByName("debug") {
24 | buildConfigField("String", "BASE_URL", "\"https://api.themoviedb.org/3/\"")
25 | buildConfigField("String", "API_KEY", "\"$apiKey\"")
26 | }
27 | getByName("release") {
28 | buildConfigField("String", "BASE_URL", "\"https://api.themoviedb.org/3/\"")
29 | buildConfigField("String", "API_KEY", "\"$apiKey\"")
30 | }
31 | }
32 | }
33 |
34 | dependencies {
35 | api(projects.core.common)
36 | debugImplementation(libs.chucker)
37 | releaseImplementation(libs.chuckerNoOp)
38 | implementation(libs.retrofit2.kotlinx.serialization.converter)
39 | implementation(libs.coil)
40 | implementation(libs.coilSvg)
41 | }
--------------------------------------------------------------------------------
/core/network/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/calladapter/NetworkResult.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.calladapter
2 |
3 | import com.ibrahimkurt.core.common.util.Constants.EMPTY_STRING
4 | import com.ibrahimkurt.core.common.util.ErrorMessage
5 | import com.ibrahimkurt.core.common.util.Resource
6 |
7 | sealed interface NetworkResult {
8 | data class Success(val data: T) : NetworkResult
9 |
10 | data class Error(
11 | val error: String = EMPTY_STRING,
12 | val resError: Int? = null
13 | ) : NetworkResult
14 | }
15 |
16 | fun NetworkResult.toResource(): Resource {
17 | return when (this) {
18 | is NetworkResult.Success -> Resource.Success(data)
19 | is NetworkResult.Error -> Resource.Error(
20 | ErrorMessage(message = error, resourceId = resError)
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/calladapter/NetworkResultCall.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.calladapter
2 |
3 | import com.ibrahimkurt.core.network.util.ErrorCategory
4 | import com.ibrahimkurt.core.network.util.NetworkUnavailableException
5 | import okhttp3.Request
6 | import okio.Timeout
7 | import org.json.JSONObject
8 | import retrofit2.Call
9 | import retrofit2.Callback
10 | import retrofit2.Response
11 | import java.net.SocketTimeoutException
12 |
13 | class NetworkResultCall(
14 | private val proxy: Call
15 | ) : Call> {
16 |
17 | override fun enqueue(callback: Callback>) {
18 | proxy.enqueue(object : Callback {
19 | override fun onResponse(call: Call, response: Response) {
20 | val networkResult = handleApiNetworkResult(response)
21 |
22 | callback.onResponse(this@NetworkResultCall, Response.success(networkResult))
23 | }
24 |
25 | override fun onFailure(call: Call, t: Throwable) {
26 | val networkResult: NetworkResult =
27 | if (t is NetworkUnavailableException) {
28 | NetworkResult.Error(resError = ErrorCategory.NetworkUnavailable.message)
29 | } else {
30 | NetworkResult.Error(resError = ErrorCategory.UnknownError.message)
31 | }
32 | callback.onResponse(this@NetworkResultCall, Response.success(networkResult))
33 | }
34 | })
35 | }
36 |
37 | private fun handleApiNetworkResult(
38 | response: Response
39 | ): NetworkResult {
40 | return try {
41 | val body = response.body()
42 |
43 | if (response.isSuccessful) {
44 | body?.let {
45 | NetworkResult.Success(body)
46 | }
47 | ?: NetworkResult.Error(resError = ErrorCategory.NotFound.message)
48 | } else {
49 | if (response.code() >= 500) {
50 | NetworkResult.Error(resError = ErrorCategory.ServerError.message)
51 | } else {
52 | val errorBody = JSONObject(response.errorBody()?.string() ?: "{}")
53 | val message = errorBody.getString("status_message")
54 | NetworkResult.Error(error = message)
55 | }
56 | }
57 | } catch (e: SocketTimeoutException) {
58 | NetworkResult.Error(resError = ErrorCategory.TimeOut.message)
59 | } catch (e: Throwable) {
60 | NetworkResult.Error(resError = ErrorCategory.UnknownError.message)
61 | }
62 | }
63 |
64 | override fun execute(): Response> = throw NotImplementedError()
65 | override fun clone(): Call> = NetworkResultCall(proxy.clone())
66 | override fun request(): Request = proxy.request()
67 | override fun timeout(): Timeout = proxy.timeout()
68 | override fun isExecuted(): Boolean = proxy.isExecuted
69 | override fun isCanceled(): Boolean = proxy.isCanceled
70 | override fun cancel() {
71 | proxy.cancel()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/calladapter/NetworkResultCallAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.calladapter
2 |
3 | import retrofit2.Call
4 | import retrofit2.CallAdapter
5 | import java.lang.reflect.Type
6 |
7 | class NetworkResultCallAdapter(
8 | private val resultType: Type
9 | ) : CallAdapter>> {
10 |
11 | override fun responseType(): Type = resultType
12 |
13 | override fun adapt(call: Call): Call> {
14 | return NetworkResultCall(call)
15 | }
16 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/calladapter/NetworkResultCallAdapterFactory.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.calladapter
2 |
3 | import retrofit2.Call
4 | import retrofit2.CallAdapter
5 | import retrofit2.Retrofit
6 | import java.lang.reflect.ParameterizedType
7 | import java.lang.reflect.Type
8 |
9 | class NetworkResultCallAdapterFactory private constructor() : CallAdapter.Factory() {
10 | override fun get(
11 | returnType: Type,
12 | annotations: Array,
13 | retrofit: Retrofit
14 | ): CallAdapter<*, *>? {
15 | if (getRawType(returnType) != Call::class.java) {
16 | return null
17 | }
18 |
19 | val callType = getParameterUpperBound(0, returnType as ParameterizedType)
20 | if (getRawType(callType) != NetworkResult::class.java) {
21 | return null
22 | }
23 |
24 | val resultType = getParameterUpperBound(0, callType as ParameterizedType)
25 | return NetworkResultCallAdapter(resultType)
26 | }
27 |
28 | companion object {
29 | fun create(): NetworkResultCallAdapterFactory = NetworkResultCallAdapterFactory()
30 | }
31 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.di
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import coil.ImageLoader
6 | import coil.decode.SvgDecoder
7 | import coil.util.DebugLogger
8 | import com.chuckerteam.chucker.api.ChuckerInterceptor
9 | import com.ibrahimkurt.core.network.BuildConfig
10 | import com.ibrahimkurt.core.network.calladapter.NetworkResultCallAdapterFactory
11 | import com.ibrahimkurt.core.network.intercapter.AuthTokenInterceptor
12 | import com.ibrahimkurt.core.network.intercapter.NetworkInterceptor
13 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
14 | import dagger.Module
15 | import dagger.Provides
16 | import dagger.hilt.InstallIn
17 | import dagger.hilt.android.qualifiers.ApplicationContext
18 | import dagger.hilt.components.SingletonComponent
19 | import kotlinx.serialization.json.Json
20 | import okhttp3.Call
21 | import okhttp3.MediaType.Companion.toMediaType
22 | import okhttp3.OkHttpClient
23 | import retrofit2.Retrofit
24 | import java.util.concurrent.TimeUnit
25 | import javax.inject.Singleton
26 |
27 | @InstallIn(SingletonComponent::class)
28 | @Module
29 | internal object NetworkModule {
30 |
31 | @Provides
32 | @Singleton
33 | fun provideJson(): Json {
34 | return Json {
35 | ignoreUnknownKeys = true
36 | encodeDefaults = true
37 | useAlternativeNames = true
38 | isLenient = true
39 | allowSpecialFloatingPointValues = false
40 | allowStructuredMapKeys = false
41 | coerceInputValues = true
42 | }
43 | }
44 |
45 | @Provides
46 | @Singleton
47 | fun provideChuckInterceptor(application: Application): ChuckerInterceptor =
48 | ChuckerInterceptor.Builder(application).build()
49 |
50 | @Provides
51 | @Singleton
52 | fun provideNetworkInterceptor(@ApplicationContext context: Context): NetworkInterceptor =
53 | NetworkInterceptor(context)
54 |
55 | @Provides
56 | @Singleton
57 | fun provideOkHttpClient(
58 | chuckInterceptor: ChuckerInterceptor,
59 | authTokenInterceptor: AuthTokenInterceptor,
60 | networkInterceptor: NetworkInterceptor,
61 | ): OkHttpClient {
62 | return OkHttpClient.Builder().apply {
63 | if (BuildConfig.DEBUG) addInterceptor(chuckInterceptor)
64 | addInterceptor(networkInterceptor)
65 | addInterceptor(authTokenInterceptor)
66 | readTimeout(60L, TimeUnit.SECONDS)
67 | connectTimeout(60L, TimeUnit.SECONDS)
68 | writeTimeout(60L, TimeUnit.SECONDS)
69 | }.build()
70 | }
71 |
72 | @Provides
73 | @Singleton
74 | fun provideRetrofit(okHttpClient: OkHttpClient, json: Json): Retrofit = Retrofit.Builder()
75 | .baseUrl(BuildConfig.BASE_URL)
76 | .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
77 | .addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
78 | .client(okHttpClient)
79 | .build()
80 |
81 | @Provides
82 | @Singleton
83 | fun okHttpCallFactory(
84 | chuckInterceptor: ChuckerInterceptor
85 | ): Call.Factory {
86 | return OkHttpClient.Builder()
87 | .apply {
88 | if (BuildConfig.DEBUG) addInterceptor(chuckInterceptor)
89 | }
90 | .build()
91 | }
92 |
93 | @Provides
94 | @Singleton
95 | fun imageLoader(
96 | okHttpCallFactory: Call.Factory,
97 | @ApplicationContext application: Context,
98 | ): ImageLoader {
99 | return ImageLoader.Builder(application)
100 | .callFactory(okHttpCallFactory)
101 | .components { add(SvgDecoder.Factory()) }
102 | .respectCacheHeaders(false)
103 | .apply {
104 | if (BuildConfig.DEBUG) {
105 | logger(DebugLogger())
106 | }
107 | }
108 | .build()
109 | }
110 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/extensions/NetworkExt.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.extensions
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 |
7 | fun Context.isNetworkAvailable(): Boolean {
8 | val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
9 | val network = connectivityManager.activeNetwork ?: return false
10 | val actNw = connectivityManager.getNetworkCapabilities(network) ?: return false
11 | return when {
12 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
13 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
14 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
15 | else -> false
16 | }
17 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/intercapter/AuthTokenInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.intercapter
2 |
3 | import com.ibrahimkurt.core.common.util.TokenConst.token
4 | import com.ibrahimkurt.core.network.BuildConfig
5 | import okhttp3.Interceptor
6 | import okhttp3.Response
7 | import javax.inject.Inject
8 |
9 | class AuthTokenInterceptor @Inject constructor() : Interceptor {
10 | override fun intercept(chain: Interceptor.Chain): Response {
11 | token = BuildConfig.API_KEY
12 | val original = chain.request()
13 | val requestBuilder = original.newBuilder()
14 | .header("Authorization", "Bearer ${BuildConfig.API_KEY}")
15 | val request = requestBuilder.build()
16 | return chain.proceed(request)
17 | }
18 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/intercapter/NetworkInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.intercapter
2 |
3 | import android.content.Context
4 | import com.ibrahimkurt.core.network.extensions.isNetworkAvailable
5 | import com.ibrahimkurt.core.network.util.NetworkUnavailableException
6 | import okhttp3.Interceptor
7 | import okhttp3.Response
8 |
9 | class NetworkInterceptor(private val context: Context) : Interceptor {
10 |
11 | override fun intercept(chain: Interceptor.Chain): Response {
12 | if (context.isNetworkAvailable()) {
13 | val builder = chain.request().newBuilder()
14 | .addHeader("Content-Type", "application/json")
15 | return chain.proceed(builder.build())
16 | } else {
17 | throw NetworkUnavailableException()
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/util/ErrorCategory.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.util
2 |
3 | import androidx.annotation.StringRes
4 | import com.ibrahimkurt.core.network.R
5 |
6 | enum class ErrorCategory(@StringRes val message: Int) {
7 | UnknownError(R.string.error_unknown),
8 | ServerError(R.string.error_system),
9 | NetworkUnavailable(R.string.error_check_internet),
10 | NotFound(R.string.error_not_found),
11 | TimeOut(R.string.error_response_not_received),
12 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/ibrahimkurt/core/network/util/NetworkUnavailableException.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.network.util
2 |
3 | import java.io.IOException
4 |
5 | class NetworkUnavailableException : IOException()
--------------------------------------------------------------------------------
/core/network/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bilinmeyen bir hata oluştu, bir süre sonra tekrar deneyiniz.
4 | Şu an sistemlerimizde bir problem var, bir süre sonra tekrar deneyiniz.
5 | Lütfen internet bağlantınızı kontrol ediniz.
6 | Bulunamadı.
7 | Cevap alınamadı lütfen tekrar deneyin.
8 |
--------------------------------------------------------------------------------
/core/pagination/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/pagination/README.md:
--------------------------------------------------------------------------------
1 | # :core:pagination module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/core/pagination/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.jvm.library)
3 | }
4 |
5 | group = "com.ibrahimkurt.pagination"
6 |
7 | dependencies {
8 | api(libs.kotlinx.paging3Common)
9 | implementation(projects.core.common)
10 | }
--------------------------------------------------------------------------------
/core/pagination/src/main/kotlin/pagination/BasePagingSource.kt:
--------------------------------------------------------------------------------
1 | package pagination
2 |
3 | import androidx.paging.Pager
4 | import androidx.paging.PagingConfig
5 | import androidx.paging.PagingData
6 | import androidx.paging.PagingSource
7 | import androidx.paging.PagingState
8 | import com.ibrahimkurt.core.common.util.Resource
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | internal class BasePagingSource(
12 | val loadDataFromApi: suspend (page: Int, pageSize: Int) -> List?,
13 | ) : PagingSource() {
14 |
15 | override suspend fun load(params: LoadParams): LoadResult {
16 | val currentPage = params.key ?: STARTING_PAGE_INDEX
17 | return try {
18 | val data = loadDataFromApi(currentPage, params.loadSize)
19 | LoadResult.Page(
20 | data = data.orEmpty(),
21 | prevKey = if (currentPage == STARTING_PAGE_INDEX) null else currentPage - 1,
22 | nextKey = if (data.isNullOrEmpty()) null else currentPage + 1
23 | )
24 | } catch (e: Exception) {
25 | LoadResult.Error(e)
26 | }
27 | }
28 |
29 | override fun getRefreshKey(state: PagingState): Int? {
30 | return state.anchorPosition?.let { anchorPosition ->
31 | state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
32 | ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
33 | }
34 | }
35 |
36 | companion object {
37 | const val STARTING_PAGE_INDEX = 1
38 | const val LIMIT = 30
39 | }
40 | }
41 |
42 | fun safeApiCallPaging(
43 | loadDataFromApi: suspend (page: Int, pageSize: Int) -> Resource>
44 | ): Flow> {
45 | return setPager(
46 | pagingSourceFactory = {
47 | BasePagingSource { page, pageSize ->
48 | when (val result = loadDataFromApi(page, pageSize)) {
49 | is Resource.Success -> result.data
50 | is Resource.Error -> throw result.toPagingException()
51 | }
52 | }
53 | }
54 | ).flow
55 | }
56 |
57 | internal fun setPager(
58 | pageSize: Int = BasePagingSource.LIMIT,
59 | initialLoadSize: Int = BasePagingSource.LIMIT,
60 | enablePlaceholders: Boolean = false,
61 | pagingSourceFactory: () -> BasePagingSource,
62 | ): Pager {
63 | return Pager(
64 | config = PagingConfig(
65 | pageSize = pageSize,
66 | initialLoadSize = initialLoadSize,
67 | prefetchDistance = 40,
68 | enablePlaceholders = enablePlaceholders,
69 | ),
70 | pagingSourceFactory = pagingSourceFactory
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/core/ui/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/ui/README.md:
--------------------------------------------------------------------------------
1 | # :core:ui module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/core/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.library.compose)
4 | alias(libs.plugins.ibrahimkurt.android.uiSetup)
5 | }
6 |
7 | android {
8 | namespace = "com.ibrahimkurt.core.ui"
9 | }
10 |
11 | dependencies {
12 | implementation(projects.core.domain)
13 | api(projects.core.component)
14 | api(projects.core.common)
15 | }
--------------------------------------------------------------------------------
/core/ui/src/main/kotlin/com/ibrahimkurt/core/ui/extensions/NavExt.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.core.ui.extensions
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraph.Companion.findStartDestination
5 | import androidx.navigation.NavHostController
6 | import androidx.navigation.NavOptions
7 | import androidx.navigation.Navigator
8 |
9 | fun NavHostController.navigateReorderBackStack(route: String) {
10 | this@navigateReorderBackStack.navigate(route) {
11 | launchSingleTop = true
12 | popUpTo(this@navigateReorderBackStack.graph.findStartDestination().id) {
13 | saveState = true
14 | }
15 | restoreState = true
16 | }
17 | }
18 |
19 | fun NavController.navigateTopSingle(
20 | route: String,
21 | navOptions: NavOptions? = null,
22 | navigatorExtras: Navigator.Extras? = null
23 | ) {
24 | val defaultNavOptionsBuilder = NavOptions.Builder()
25 | .setLaunchSingleTop(true)
26 | navOptions?.let { userNavOptions ->
27 | defaultNavOptionsBuilder.apply {
28 | setEnterAnim(userNavOptions.enterAnim)
29 | setExitAnim(userNavOptions.exitAnim)
30 | setPopEnterAnim(userNavOptions.popEnterAnim)
31 | setPopExitAnim(userNavOptions.popExitAnim)
32 | setPopUpTo(userNavOptions.popUpToId, userNavOptions.isPopUpToInclusive())
33 | }
34 | }
35 | return navigate(route, defaultNavOptionsBuilder.build(), navigatorExtras)
36 | }
--------------------------------------------------------------------------------
/core/ui/src/main/res/drawable/examplefastx.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/core/ui/src/main/res/drawable/examplefastx.jpg
--------------------------------------------------------------------------------
/docs/images/graphs/1__ZrkCb8QE0nK4FApKzKgmA.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/docs/images/graphs/1__ZrkCb8QE0nK4FApKzKgmA.webp
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_core_common.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
20 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_core_component.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
20 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_core_data.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
68 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_core_domain.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
32 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_core_network.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
32 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_core_pagination.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
32 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_core_ui.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
62 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_detail_data.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
98 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_detail_domain.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_detail_ui.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
92 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_home_data.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
116 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_home_domain.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_home_ui.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
92 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_template_data.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
80 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_template_domain.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/docs/images/graphs/dep_graph_features_template_ui.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
74 |
--------------------------------------------------------------------------------
/docs/images/screenshot/detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/docs/images/screenshot/detail.png
--------------------------------------------------------------------------------
/docs/images/screenshot/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/docs/images/screenshot/home.png
--------------------------------------------------------------------------------
/features/detail/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/detail/data/README.md:
--------------------------------------------------------------------------------
1 | # :features:detail:data module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/detail/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.hilt)
4 | alias(libs.plugins.ibrahimkurt.android.retrofitSerialization)
5 | }
6 |
7 | android {
8 | namespace = "com.ibrahimkurt.features.detail.data"
9 | }
10 |
11 | dependencies {
12 | implementation(projects.core.data)
13 | implementation(projects.features.detail.domain)
14 | }
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/di/DetailBindsModule.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.di
2 |
3 | import com.features.detail.data.repository.DetailRepositoryImpl
4 | import com.features.detail.domain.repository.DetailRepository
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.components.ViewModelComponent
9 | import dagger.hilt.android.scopes.ViewModelScoped
10 |
11 | @Module
12 | @InstallIn(ViewModelComponent::class)
13 | internal abstract class DetailBindsModule {
14 |
15 | @Binds
16 | @ViewModelScoped
17 | abstract fun bindHomeRepository(impl: DetailRepositoryImpl): DetailRepository
18 | }
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/di/DetailProvidesModule.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.di
2 |
3 | import com.features.detail.data.source.DetailApiService
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.components.ViewModelComponent
8 | import dagger.hilt.android.scopes.ViewModelScoped
9 | import retrofit2.Retrofit
10 |
11 | @Module
12 | @InstallIn(ViewModelComponent::class)
13 | object DetailProvidesModule {
14 |
15 | @ViewModelScoped
16 | @Provides
17 | fun provideNetworkService(retrofit: Retrofit): DetailApiService {
18 | return retrofit.create(DetailApiService::class.java)
19 | }
20 | }
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/CreatedBy.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class CreatedBy(
8 | @SerialName("credit_id")
9 | val creditId: String? = null,
10 | val gender: Int? = null,
11 | val id: Int? = null,
12 | val name: String? = null,
13 | @SerialName("profile_path")
14 | val profilePath: String? = null,
15 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/Genre.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class Genre(
7 | val id: Int? = null,
8 | val name: String? = null,
9 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/LastEpisodeToAir.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class LastEpisodeToAir(
8 | @SerialName("air_date")
9 | val airDate: String? = null,
10 | @SerialName("episode_number")
11 | val episodeNumber: Int? = null,
12 | val id: Int? = null,
13 | val name: String? = null,
14 | val overview: String? = null,
15 | @SerialName("production_code")
16 | val productionCode: String? = null,
17 | val runtime: Int? = null,
18 | @SerialName("season_number")
19 | val seasonNumber: Int? = null,
20 | @SerialName("show_id")
21 | val showId: Int? = null,
22 | @SerialName("still_path")
23 | val stillPath: String? = null,
24 | @SerialName("vote_average")
25 | val voteAverage: Double? = null,
26 | @SerialName("vote_count")
27 | val voteCount: Int? = null,
28 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/Network.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class Network(
8 | val id: Int? = null,
9 | @SerialName("logo_path")
10 | val logoPath: String? = null,
11 | val name: String? = null,
12 | @SerialName("origin_country")
13 | val originCountry: String? = null,
14 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/NextEpisodeToAir.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class NextEpisodeToAir(
8 | @SerialName("air_date")
9 | val airDate: String? = null,
10 | @SerialName("episode_number")
11 | val episodeNumber: Int? = null,
12 | @SerialName("episode_type")
13 | val episodeType: String? = null,
14 | val id: Int? = null,
15 | val name: String? = null,
16 | val overview: String? = null,
17 | @SerialName("production_code")
18 | val productionCode: String? = null,
19 | val runtime: Int? = null,
20 | @SerialName("season_number")
21 | val seasonNumber: Int? = null,
22 | @SerialName("show_id")
23 | val showId: Int? = null,
24 | @SerialName("still_path")
25 | val stillPath: String? = null,
26 | @SerialName("vote_average")
27 | val voteAverage: Double? = null,
28 | @SerialName("vote_count")
29 | val voteCount: Int? = null,
30 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/ProductionCompany.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class ProductionCompany(
8 | val id: Int? = null,
9 | @SerialName("logo_path")
10 | val logoPath: String? = null,
11 | val name: String? = null,
12 | @SerialName("origin_country")
13 | val originCountry: String? = null,
14 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/ProductionCountry.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class ProductionCountry(
8 | @SerialName("iso_3166_1")
9 | val iso: String? = null,
10 | val name: String? = null,
11 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/Season.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class Season(
8 | @SerialName("air_date")
9 | val airDate: String? = null,
10 | @SerialName("episode_count")
11 | val episodeCount: Int? = null,
12 | val id: Int? = null,
13 | val name: String? = null,
14 | val overview: String? = null,
15 | @SerialName("poster_path")
16 | val posterPath: String? = null,
17 | @SerialName("season_number")
18 | val seasonNumber: Int? = null,
19 | @SerialName("vote_average")
20 | val voteAverage: Double? = null,
21 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/SpokenLanguage.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class SpokenLanguage(
8 | @SerialName("english_name")
9 | val englishName: String? = null,
10 | @SerialName("iso_639_1")
11 | val iso: String? = null,
12 | val name: String? = null,
13 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/dto/TvSeriesDetailsResponse.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class TvSeriesDetailsResponse(
8 | val adult: Boolean? = null,
9 | @SerialName("backdrop_path")
10 | val backdropPath: String? = null,
11 | @SerialName("created_by")
12 | val createdBy: List? = null,
13 | @SerialName("episode_run_time")
14 | val episodeRunTime: List? = null,
15 | @SerialName("first_air_date")
16 | val firstAirDate: String? = null,
17 | val genres: List? = null,
18 | val homepage: String? = null,
19 | val id: Int? = null,
20 | @SerialName("in_production")
21 | val inProduction: Boolean? = null,
22 | val languages: List? = null,
23 | @SerialName("last_air_date")
24 | val lastAirDate: String? = null,
25 | @SerialName("last_episode_to_air")
26 | val lastEpisodeToAir: LastEpisodeToAir? = null,
27 | val name: String? = null,
28 | val networks: List? = null,
29 | @SerialName("next_episode_to_air")
30 | val nextEpisodeToAir: NextEpisodeToAir? = null,
31 | @SerialName("number_of_episodes")
32 | val numberOfEpisodes: Int? = null,
33 | @SerialName("number_of_seasons")
34 | val numberOfSeasons: Int? = null,
35 | @SerialName("origin_country")
36 | val originCountry: List? = null,
37 | @SerialName("original_language")
38 | val originalLanguage: String? = null,
39 | @SerialName("original_name")
40 | val originalName: String? = null,
41 | val overview: String? = null,
42 | val popularity: Double? = null,
43 | @SerialName("poster_path")
44 | val posterPath: String? = null,
45 | @SerialName("production_companies")
46 | val productionCompanies: List? = null,
47 | @SerialName("production_countries")
48 | val productionCountries: List? = null,
49 | val seasons: List? = null,
50 | @SerialName("spoken_languages")
51 | val spokenLanguages: List? = null,
52 | val status: String? = null,
53 | val tagline: String? = null,
54 | val type: String? = null,
55 | @SerialName("vote_average")
56 | val voteAverage: Double? = null,
57 | @SerialName("vote_count")
58 | val voteCount: Int? = null
59 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/mapper/DetailMapper.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.mapper
2 |
3 | import com.features.detail.data.dto.TvSeriesDetailsResponse
4 | import com.features.detail.domain.model.TvSeriesDetail
5 | import com.ibrahimkurt.core.common.util.APIConst.IMAGE_URL
6 | import com.ibrahimkurt.core.common.util.APIConst.ORIGINAL_IMAGE_URL
7 | import com.ibrahimkurt.core.common.util.orZero
8 | import java.util.Locale
9 |
10 | fun TvSeriesDetailsResponse.toDomain() = TvSeriesDetail(
11 | backdropPath = ORIGINAL_IMAGE_URL + backdropPath.orEmpty(),
12 | id = id.orZero(),
13 | name = name.orEmpty(),
14 | numberOfEpisodes = numberOfEpisodes.orZero(),
15 | numberOfSeasons = numberOfSeasons.orZero(),
16 | originalLanguage = originalLanguage.orEmpty(),
17 | firstAirDate = firstAirDate.orEmpty(),
18 | originalName = originalName.orEmpty(),
19 | overview = overview.orEmpty(),
20 | popularity = popularity.orZero(),
21 | posterPath = IMAGE_URL + posterPath.orEmpty(),
22 | status = status.orEmpty(),
23 | voteAverage = voteAverage?.toFloat().orZero(),
24 | voteAverageFormat = String.format(Locale.US, "%.1f", voteAverage.orZero()),
25 | )
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/repository/DetailRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.repository
2 |
3 | import com.features.detail.data.mapper.toDomain
4 | import com.features.detail.data.source.DetailApiService
5 | import com.features.detail.domain.model.TvSeriesDetail
6 | import com.features.detail.domain.repository.DetailRepository
7 | import com.ibrahimkurt.core.common.util.Resource
8 | import com.ibrahimkurt.core.common.util.map
9 | import com.ibrahimkurt.core.network.calladapter.toResource
10 | import javax.inject.Inject
11 |
12 | internal class DetailRepositoryImpl @Inject constructor(
13 | private val detailDataSource: DetailApiService
14 | ) : DetailRepository {
15 |
16 | override suspend fun getDetail(id: Int): Resource {
17 | return detailDataSource.getDetailTvSeries(id).toResource().map {
18 | it.toDomain()
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/features/detail/data/src/main/kotlin/com/features/detail/data/source/DetailApiService.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.data.source
2 |
3 | import com.features.detail.data.dto.TvSeriesDetailsResponse
4 | import com.ibrahimkurt.core.network.calladapter.NetworkResult
5 | import retrofit2.http.GET
6 | import retrofit2.http.Path
7 | import retrofit2.http.Query
8 |
9 | interface DetailApiService {
10 | @GET("tv/{series_id}")
11 | suspend fun getDetailTvSeries(
12 | @Path("series_id") seriesId: Int,
13 | @Query("language") language: String = "en-US",
14 | ): NetworkResult
15 | }
--------------------------------------------------------------------------------
/features/detail/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/detail/domain/README.md:
--------------------------------------------------------------------------------
1 | # :features:detail:domain module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/detail/domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.jvm.library)
3 | }
4 |
5 | group = "com.ibrahimkurt.features.detail.domain"
6 |
7 | dependencies {
8 | implementation(projects.core.domain)
9 | implementation(libs.javax.inject)
10 | implementation(libs.kotlinx.coroutines.core)
11 | }
--------------------------------------------------------------------------------
/features/detail/domain/src/main/kotlin/com/features/detail/domain/model/TvSeriesDetail.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.domain.model
2 |
3 | import com.ibrahimkurt.core.common.util.Constants.EMPTY_STRING
4 | import com.ibrahimkurt.core.common.util.Constants.ZERO_DOUBLE
5 | import com.ibrahimkurt.core.common.util.Constants.ZERO_FLOAT
6 | import com.ibrahimkurt.core.common.util.Constants.ZERO_INT
7 |
8 | data class TvSeriesDetail(
9 | val backdropPath: String = EMPTY_STRING,
10 | val id: Int = ZERO_INT,
11 | val name: String = EMPTY_STRING,
12 | val numberOfEpisodes: Int = ZERO_INT,
13 | val numberOfSeasons: Int = ZERO_INT,
14 | val originalLanguage: String = EMPTY_STRING,
15 | val originalName: String = EMPTY_STRING,
16 | val firstAirDate: String = EMPTY_STRING,
17 | val overview: String = EMPTY_STRING,
18 | val popularity: Double = ZERO_DOUBLE,
19 | val posterPath: String = EMPTY_STRING,
20 | val status: String = EMPTY_STRING,
21 | val voteAverage: Float = ZERO_FLOAT,
22 | val voteAverageFormat: String = EMPTY_STRING
23 | )
--------------------------------------------------------------------------------
/features/detail/domain/src/main/kotlin/com/features/detail/domain/repository/DetailRepository.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.domain.repository
2 |
3 | import com.features.detail.domain.model.TvSeriesDetail
4 | import com.ibrahimkurt.core.common.util.Resource
5 |
6 | interface DetailRepository {
7 | suspend fun getDetail(id: Int): Resource
8 | }
--------------------------------------------------------------------------------
/features/detail/domain/src/main/kotlin/com/features/detail/domain/usecase/GetDetailUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.domain.usecase
2 |
3 | import com.features.detail.domain.repository.DetailRepository
4 | import kotlinx.coroutines.flow.flow
5 | import javax.inject.Inject
6 |
7 | class GetDetailUseCase @Inject constructor(
8 | private val detailRepository: DetailRepository
9 | ) {
10 | operator fun invoke(id: Int) = flow {
11 | emit(detailRepository.getDetail(id))
12 | }
13 | }
--------------------------------------------------------------------------------
/features/detail/ui/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/detail/ui/README.md:
--------------------------------------------------------------------------------
1 | # :features:detail:ui module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/detail/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.library.compose)
4 | alias(libs.plugins.ibrahimkurt.android.uiSetup)
5 | }
6 |
7 | android {
8 | namespace = "com.ibrahimkurt.features.detail.ui"
9 | }
10 |
11 | dependencies {
12 | implementation(projects.core.ui)
13 | implementation(projects.features.detail.domain)
14 | }
--------------------------------------------------------------------------------
/features/detail/ui/src/main/kotlin/com/features/detail/ui/DetailNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.ui
2 |
3 | import androidx.navigation.NavController
4 | import androidx.navigation.NavGraphBuilder
5 | import androidx.navigation.NavOptions
6 | import androidx.navigation.NavType
7 | import androidx.navigation.compose.composable
8 | import androidx.navigation.navArgument
9 | import com.ibrahimkurt.core.ui.extensions.navigateTopSingle
10 |
11 | data class DetailNavActions(
12 | val navigateToBack: () -> Unit
13 | )
14 |
15 | fun NavController.navigateToDetail(
16 | id: Int,
17 | navOptions: NavOptions? = null
18 | ) {
19 | navigateTopSingle("detail/$id", navOptions)
20 | }
21 |
22 | fun NavGraphBuilder.detail(detailNavActions: DetailNavActions) {
23 | composable(
24 | route = "detail/{id}",
25 | arguments = listOf(
26 | navArgument("id") {
27 | type = NavType.IntType
28 | }
29 | )
30 | ) {
31 | DetailRoute(detailNavActions = detailNavActions)
32 | }
33 | }
--------------------------------------------------------------------------------
/features/detail/ui/src/main/kotlin/com/features/detail/ui/DetailScreen.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.ui
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.offset
8 | import androidx.compose.foundation.rememberScrollState
9 | import androidx.compose.foundation.verticalScroll
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.LaunchedEffect
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.graphics.FilterQuality
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.platform.LocalContext
22 | import androidx.compose.ui.unit.dp
23 | import androidx.hilt.navigation.compose.hiltViewModel
24 | import coil.imageLoader
25 | import coil.request.ImageRequest
26 | import com.features.detail.domain.model.TvSeriesDetail
27 | import com.ibrahimkurt.core.component.CoilImage
28 | import com.ibrahimkurt.core.component.ReelIconButtonType
29 | import com.ibrahimkurt.core.component.ReelPreview
30 | import com.ibrahimkurt.core.component.ReelTopAppBarType
31 | import com.ibrahimkurt.core.component.util.Constants
32 | import com.ibrahimkurt.core.ui.widgets.FilmCard
33 |
34 | @Composable
35 | fun DetailRoute(
36 | viewModel: DetailViewModel = hiltViewModel(),
37 | detailNavActions: DetailNavActions
38 | ) {
39 | LaunchedEffect(key1 = Unit) {
40 | viewModel.snapshot.collect {
41 | when (it) {
42 | is DetailSnapshot.Error -> {
43 | }
44 | }
45 | }
46 | }
47 |
48 | DetailScreen(
49 | detailNavActions,
50 | viewModel.state
51 | )
52 | }
53 |
54 | @OptIn(ExperimentalMaterial3Api::class)
55 | @Composable
56 | fun DetailScreen(
57 | detailNavActions: DetailNavActions,
58 | state: DetailState
59 | ) {
60 | val context = LocalContext.current
61 | val imageLoader = context.imageLoader
62 | Column(
63 | modifier = Modifier
64 | .fillMaxSize()
65 | .verticalScroll(rememberScrollState())
66 | ) {
67 | Box {
68 | val request = ImageRequest.Builder(LocalContext.current)
69 | .data(state.tvSeriesDetail.backdropPath)
70 | .crossfade(true)
71 | .build()
72 |
73 | CoilImage(
74 | model = request,
75 | contentDescription = null,
76 | filterQuality = FilterQuality.High,
77 | imageLoader = imageLoader,
78 | contentScale = ContentScale.FillWidth,
79 | modifier = Modifier.fillMaxWidth()
80 | )
81 | Box(
82 | modifier = Modifier
83 | .align(Alignment.BottomStart)
84 | .offset(y = 75.dp)
85 | ) {
86 | FilmCard(
87 | model = state.tvSeriesDetail.posterPath,
88 | title = state.tvSeriesDetail.name,
89 | date = state.tvSeriesDetail.firstAirDate,
90 | voteAverage = state.tvSeriesDetail.voteAverage,
91 | voteAverageFormat = state.tvSeriesDetail.voteAverageFormat
92 | )
93 | }
94 |
95 | ReelTopAppBarType.ReelTopAppBar(
96 | navigationIcon = {
97 | ReelIconButtonType.ReelIconButton(
98 | onClick = { detailNavActions.navigateToBack() },
99 | colors = ReelIconButtonType.reelIconButtonColor()
100 | ) {
101 | Icon(
102 | imageVector = Icons.AutoMirrored.Default.ArrowBack,
103 | contentDescription = Constants.EMPTY_STRING,
104 | )
105 | }
106 | },
107 | colors = ReelTopAppBarType.reelColor(
108 | containerColor = Color.Transparent,
109 | )
110 | )
111 | }
112 | }
113 | }
114 |
115 | @ReelPreview
116 | @Composable
117 | private fun DetailPreview() {
118 | DetailScreen(
119 | detailNavActions = DetailNavActions(
120 | navigateToBack = {}
121 | ),
122 | state = DetailState(
123 | tvSeriesDetail = TvSeriesDetail(name = "Example")
124 | )
125 | )
126 | }
--------------------------------------------------------------------------------
/features/detail/ui/src/main/kotlin/com/features/detail/ui/DetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.features.detail.ui
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.SavedStateHandle
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.viewModelScope
9 | import com.features.detail.domain.model.TvSeriesDetail
10 | import com.features.detail.domain.usecase.GetDetailUseCase
11 | import com.ibrahimkurt.core.common.util.ErrorMessage
12 | import com.ibrahimkurt.core.common.util.onFailure
13 | import com.ibrahimkurt.core.common.util.onSuccess
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.channels.Channel
16 | import kotlinx.coroutines.flow.launchIn
17 | import kotlinx.coroutines.flow.onCompletion
18 | import kotlinx.coroutines.flow.onEach
19 | import kotlinx.coroutines.flow.onStart
20 | import kotlinx.coroutines.flow.receiveAsFlow
21 | import javax.inject.Inject
22 |
23 | @HiltViewModel
24 | class DetailViewModel @Inject constructor(
25 | private val getDetailUseCase: GetDetailUseCase,
26 | saveStateHandle: SavedStateHandle
27 | ) : ViewModel() {
28 |
29 | var state by mutableStateOf(DetailState())
30 | private set
31 |
32 | private val _snapshot = Channel()
33 | val snapshot = _snapshot.receiveAsFlow()
34 |
35 | init {
36 | saveStateHandle.get("id")?.let {
37 | getDetail(it)
38 | }
39 | }
40 |
41 | private fun getDetail(id: Int) {
42 | getDetailUseCase(id).onStart {
43 | state = state.copy(isLoading = true)
44 | }.onCompletion {
45 | state = state.copy(isLoading = false)
46 | }.onEach { resource ->
47 | resource.onSuccess {
48 | state = state.copy(tvSeriesDetail = it)
49 | }.onFailure {
50 | _snapshot.send(DetailSnapshot.Error(it))
51 | }
52 | }.launchIn(viewModelScope)
53 | }
54 | }
55 |
56 | data class DetailState(
57 | val isLoading: Boolean = false,
58 | val tvSeriesDetail: TvSeriesDetail = TvSeriesDetail(),
59 | )
60 |
61 | sealed interface DetailSnapshot {
62 | data class Error(val errorMessage: ErrorMessage) : DetailSnapshot
63 | }
--------------------------------------------------------------------------------
/features/home/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/home/data/README.md:
--------------------------------------------------------------------------------
1 | # :features:home:data module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/home/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.hilt)
4 | alias(libs.plugins.ibrahimkurt.android.retrofitSerialization)
5 | }
6 |
7 | android {
8 | namespace = "com.ibrahimkurt.features.home.data"
9 | }
10 |
11 | dependencies {
12 | implementation(projects.features.home.domain)
13 | implementation(projects.core.data)
14 | implementation(projects.core.pagination)
15 | }
--------------------------------------------------------------------------------
/features/home/data/src/main/java/com/ibrahimkurt/features/home/data/di/HomeBindsModule.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.data.di
2 |
3 | import com.ibrahimkurt.features.home.data.repository.HomeRepositoryImpl
4 | import com.ibrahimkurt.features.home.domain.repository.HomeRepository
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.components.ViewModelComponent
9 | import dagger.hilt.android.scopes.ViewModelScoped
10 |
11 | @Module
12 | @InstallIn(ViewModelComponent::class)
13 | abstract class HomeBindsModule {
14 |
15 | @Binds
16 | @ViewModelScoped
17 | abstract fun bindHomeRepository(impl: HomeRepositoryImpl): HomeRepository
18 | }
--------------------------------------------------------------------------------
/features/home/data/src/main/java/com/ibrahimkurt/features/home/data/di/HomeProvidesModule.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.data.di
2 |
3 | import com.ibrahimkurt.features.home.data.source.HomeApiService
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.components.ViewModelComponent
8 | import dagger.hilt.android.scopes.ViewModelScoped
9 | import retrofit2.Retrofit
10 |
11 | @Module
12 | @InstallIn(ViewModelComponent::class)
13 | object HomeProvidesModule {
14 |
15 | @ViewModelScoped
16 | @Provides
17 | fun provideNetworkService(retrofit: Retrofit): HomeApiService {
18 | return retrofit.create(HomeApiService::class.java)
19 | }
20 | }
--------------------------------------------------------------------------------
/features/home/data/src/main/java/com/ibrahimkurt/features/home/data/dto/ResultDto.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class ResultDto(
8 | val adult: Boolean? = null,
9 | @SerialName("backdrop_path")
10 | val backdropPath: String? = null,
11 | @SerialName("first_air_date")
12 | val firstAirDate: String? = null,
13 | @SerialName("genre_ids")
14 | val genreIds: List? = null,
15 | val id: Int? = null,
16 | val name: String? = null,
17 | @SerialName("origin_country")
18 | val originCountry: List? = null,
19 | @SerialName("original_language")
20 | val originalLanguage: String? = null,
21 | @SerialName("original_name")
22 | val originalName: String? = null,
23 | val overview: String? = null,
24 | val popularity: Double? = null,
25 | @SerialName("poster_path")
26 | val posterPath: String? = null,
27 | @SerialName("vote_average")
28 | val voteAverage: Double? = null,
29 | @SerialName("vote_count")
30 | val voteCount: Int? = null,
31 | )
--------------------------------------------------------------------------------
/features/home/data/src/main/java/com/ibrahimkurt/features/home/data/dto/TvShowResponseDto.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.data.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class TvShowResponseDto(
8 | val page: Int? = null,
9 | val results: List? = null,
10 | @SerialName("total_pages")
11 | val totalPages: Int? = null,
12 | @SerialName("total_results")
13 | val totalResults: Int? = null,
14 | )
--------------------------------------------------------------------------------
/features/home/data/src/main/java/com/ibrahimkurt/features/home/data/mapper/HomeMapper.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.data.mapper
2 |
3 | import com.ibrahimkurt.core.common.util.APIConst.IMAGE_URL
4 | import com.ibrahimkurt.core.common.util.orZero
5 | import com.ibrahimkurt.features.home.data.dto.ResultDto
6 | import com.ibrahimkurt.features.home.domain.model.TvShow
7 | import java.util.Locale
8 |
9 | fun List?.toDomain() = this?.map {
10 | TvShow(
11 | id = it.id.orZero(),
12 | name = it.name.orEmpty(),
13 | originalLanguage = it.originalLanguage.orEmpty(),
14 | originalName = it.originalName.orEmpty(),
15 | overview = it.overview.orEmpty(),
16 | popularity = it.popularity.orZero(),
17 | backdropPath = it.backdropPath.orEmpty(),
18 | firstAirDate = it.firstAirDate.orEmpty(),
19 | genreIds = it.genreIds.orEmpty(),
20 | originCountry = it.originCountry.orEmpty(),
21 | posterPath = IMAGE_URL + it.posterPath.orEmpty(),
22 | voteAverage = it.voteAverage?.toFloat().orZero(),
23 | voteAverageFormat = String.format(Locale.US, "%.1f", it.voteAverage.orZero()),
24 | voteCount = it.voteCount.orZero()
25 | )
26 | }.orEmpty()
--------------------------------------------------------------------------------
/features/home/data/src/main/java/com/ibrahimkurt/features/home/data/repository/HomeRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.data.repository
2 |
3 | import androidx.paging.PagingData
4 | import com.ibrahimkurt.core.common.util.map
5 | import com.ibrahimkurt.core.network.calladapter.toResource
6 | import com.ibrahimkurt.features.home.data.mapper.toDomain
7 | import com.ibrahimkurt.features.home.data.source.HomeApiService
8 | import com.ibrahimkurt.features.home.domain.model.TvShow
9 | import com.ibrahimkurt.features.home.domain.repository.HomeRepository
10 | import kotlinx.coroutines.flow.Flow
11 | import pagination.safeApiCallPaging
12 | import javax.inject.Inject
13 |
14 | class HomeRepositoryImpl @Inject constructor(
15 | private val homeApiService: HomeApiService
16 | ) : HomeRepository {
17 |
18 | override fun getHome(): Flow> {
19 | return safeApiCallPaging { page, _ ->
20 | homeApiService.getTvShows(page = page).toResource().map {
21 | it.results.toDomain()
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/features/home/data/src/main/java/com/ibrahimkurt/features/home/data/source/HomeApiService.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.data.source
2 |
3 | import com.ibrahimkurt.core.network.calladapter.NetworkResult
4 | import com.ibrahimkurt.features.home.data.dto.TvShowResponseDto
5 | import retrofit2.http.GET
6 | import retrofit2.http.Query
7 |
8 | interface HomeApiService {
9 | @GET("discover/tv")
10 | suspend fun getTvShows(
11 | @Query("include_adult") includeAdult: Boolean = false,
12 | @Query("include_null_first_air_dates") includeNullFirstAirDates: Boolean = false,
13 | @Query("language") language: String = "en-US",
14 | @Query("page") page: Int = 1,
15 | @Query("sort_by") sortBy: String = "popularity.desc"
16 | ): NetworkResult
17 | }
--------------------------------------------------------------------------------
/features/home/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/home/domain/README.md:
--------------------------------------------------------------------------------
1 | # :features:home:domain module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/home/domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.jvm.library)
3 |
4 | }
5 | group = "com.ibrahimkurt.features.home.domain"
6 |
7 |
8 | dependencies {
9 | implementation(projects.core.domain)
10 | implementation(libs.javax.inject)
11 | implementation(libs.kotlinx.coroutines.core)
12 | implementation(libs.kotlinx.paging3Common)
13 | }
--------------------------------------------------------------------------------
/features/home/domain/src/main/java/com/ibrahimkurt/features/home/domain/model/TvShow.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.domain.model
2 |
3 | import com.ibrahimkurt.core.common.util.Constants.EMPTY_STRING
4 | import com.ibrahimkurt.core.common.util.Constants.ZERO_DOUBLE
5 | import com.ibrahimkurt.core.common.util.Constants.ZERO_FLOAT
6 | import com.ibrahimkurt.core.common.util.Constants.ZERO_INT
7 |
8 | data class TvShow(
9 | val id: Int = ZERO_INT,
10 | val name: String = EMPTY_STRING,
11 | val originalLanguage: String = EMPTY_STRING,
12 | val originalName: String = EMPTY_STRING,
13 | val overview: String = EMPTY_STRING,
14 | val popularity: Double = ZERO_DOUBLE,
15 | val backdropPath: String = EMPTY_STRING,
16 | val firstAirDate: String = EMPTY_STRING,
17 | val genreIds: List = emptyList(),
18 | val originCountry: List = emptyList(),
19 | val posterPath: String = EMPTY_STRING,
20 | val voteAverage: Float = ZERO_FLOAT,
21 | val voteAverageFormat: String = EMPTY_STRING,
22 | val voteCount: Int = ZERO_INT
23 | )
--------------------------------------------------------------------------------
/features/home/domain/src/main/java/com/ibrahimkurt/features/home/domain/model/TvShowList.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.domain.model
2 |
3 | data class TvShowList(
4 | val page: Int,
5 | val results: List,
6 | val totalPages: Int,
7 | val totalResults: Int
8 | )
--------------------------------------------------------------------------------
/features/home/domain/src/main/java/com/ibrahimkurt/features/home/domain/repository/HomeRepository.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.domain.repository
2 |
3 | import androidx.paging.PagingData
4 | import com.ibrahimkurt.features.home.domain.model.TvShow
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface HomeRepository {
8 | fun getHome(): Flow>
9 | }
--------------------------------------------------------------------------------
/features/home/domain/src/main/java/com/ibrahimkurt/features/home/domain/usecase/GetTvUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.domain.usecase
2 |
3 | import androidx.paging.PagingData
4 | import com.ibrahimkurt.features.home.domain.model.TvShow
5 | import com.ibrahimkurt.features.home.domain.repository.HomeRepository
6 | import kotlinx.coroutines.flow.Flow
7 | import javax.inject.Inject
8 |
9 | class GetTvUseCase @Inject constructor(
10 | private val homeRepository: HomeRepository
11 | ) {
12 | fun execute(): Flow> {
13 | return homeRepository.getHome()
14 | }
15 | }
--------------------------------------------------------------------------------
/features/home/ui/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/home/ui/README.md:
--------------------------------------------------------------------------------
1 | # :features:home:ui module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/home/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.library.compose)
4 | alias(libs.plugins.ibrahimkurt.android.uiSetup)
5 | }
6 |
7 | android {
8 | namespace = "com.ibrahimkurt.features.home.ui"
9 | }
10 |
11 | dependencies {
12 | implementation(projects.core.ui)
13 | implementation(projects.features.home.domain)
14 | implementation(libs.androidx.paging3Compose)
15 | }
--------------------------------------------------------------------------------
/features/home/ui/src/main/java/com/ibrahimkurt/features/home/ui/HomeNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.ui
2 |
3 | import androidx.navigation.NavGraphBuilder
4 | import androidx.navigation.compose.composable
5 |
6 | data class HomeNavActions(
7 | val navigateToDetail: (id: Int) -> Unit
8 | )
9 |
10 | fun NavGraphBuilder.home(homeNavActions: HomeNavActions) {
11 | composable(
12 | route = "home"
13 | ) {
14 | HomeRoute(homeNavActions)
15 | }
16 | }
--------------------------------------------------------------------------------
/features/home/ui/src/main/java/com/ibrahimkurt/features/home/ui/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.ui
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.statusBarsPadding
8 | import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
9 | import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.dp
15 | import androidx.hilt.navigation.compose.hiltViewModel
16 | import androidx.paging.PagingData
17 | import androidx.paging.compose.LazyPagingItems
18 | import androidx.paging.compose.collectAsLazyPagingItems
19 | import com.ibrahimkurt.core.component.ReelPreview
20 | import com.ibrahimkurt.core.ui.widgets.FilmCard
21 | import com.ibrahimkurt.features.home.domain.model.TvShow
22 | import kotlinx.coroutines.flow.Flow
23 | import kotlinx.coroutines.flow.flowOf
24 |
25 | @Composable
26 | fun HomeRoute(homeNavActions: HomeNavActions) {
27 | val viewModel: HomeViewModel = hiltViewModel()
28 | val homePage = viewModel.homePaging.collectAsLazyPagingItems()
29 | HomeScreen(
30 | homeNavActions = homeNavActions,
31 | homePage = homePage
32 | )
33 | }
34 |
35 | @Composable
36 | fun HomeScreen(
37 | homeNavActions: HomeNavActions,
38 | homePage: LazyPagingItems
39 | ) {
40 | LazyVerticalStaggeredGrid(
41 | columns = StaggeredGridCells.Adaptive(160.dp),
42 | modifier = Modifier
43 | .statusBarsPadding()
44 | .fillMaxSize()
45 | .background(Color.White)
46 | ) {
47 | items(count = homePage.itemCount) { index ->
48 | val item = homePage[index] ?: TvShow()
49 |
50 | Box(
51 | contentAlignment = Alignment.Center,
52 | modifier = Modifier
53 | .padding(4.dp)
54 | ) {
55 | FilmCard(
56 | model = item.posterPath,
57 | title = item.name,
58 | date = item.firstAirDate,
59 | voteAverage = item.voteAverage,
60 | voteAverageFormat = item.voteAverageFormat
61 | ) {
62 | homeNavActions.navigateToDetail(item.id)
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
69 | @ReelPreview
70 | @Composable
71 | fun HomePreview() {
72 | val pagingDataFlow: Flow> = flowOf(
73 | PagingData.from(
74 | listOf(
75 | TvShow(name = "example"),
76 | TvShow(name = "example1"),
77 | TvShow(name = "example2"),
78 | )
79 | )
80 | )
81 | HomeScreen(
82 | homeNavActions = HomeNavActions(
83 | navigateToDetail = {}
84 | ),
85 | homePage = pagingDataFlow.collectAsLazyPagingItems()
86 | )
87 | }
--------------------------------------------------------------------------------
/features/home/ui/src/main/java/com/ibrahimkurt/features/home/ui/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.features.home.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import androidx.paging.PagingData
6 | import androidx.paging.cachedIn
7 | import com.ibrahimkurt.features.home.domain.model.TvShow
8 | import com.ibrahimkurt.features.home.domain.usecase.GetTvUseCase
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.launchIn
13 | import kotlinx.coroutines.flow.onEach
14 | import kotlinx.coroutines.flow.update
15 | import javax.inject.Inject
16 |
17 | @HiltViewModel
18 | class HomeViewModel @Inject constructor(
19 | private val getTvUseCase: GetTvUseCase
20 | ) : ViewModel() {
21 | private val _homePaging = MutableStateFlow(PagingData.empty())
22 | val homePaging: StateFlow> = _homePaging
23 |
24 | init {
25 | getHome()
26 | }
27 |
28 | private fun getHome() {
29 | getTvUseCase.execute().cachedIn(viewModelScope).onEach { pagingData ->
30 | _homePaging.update { pagingData }
31 | }.launchIn(viewModelScope)
32 | }
33 | }
--------------------------------------------------------------------------------
/features/template/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/template/data/README.md:
--------------------------------------------------------------------------------
1 | # :features:template:data module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/template/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.hilt)
4 | alias(libs.plugins.ibrahimkurt.android.retrofitSerialization)
5 | }
6 |
7 | android {
8 | namespace = "com.ibrahimkurt.features.template.data"
9 | }
10 |
11 | dependencies {
12 | implementation(projects.core.data)
13 | }
--------------------------------------------------------------------------------
/features/template/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/template/domain/README.md:
--------------------------------------------------------------------------------
1 | # :features:template:domain module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/template/domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.jvm.library)
3 | }
4 |
5 | group = "com.ibrahimkurt.features.template.domain"
6 |
7 | dependencies {
8 | implementation(projects.core.domain)
9 | implementation(libs.javax.inject)
10 | implementation(libs.kotlinx.coroutines.core)
11 | }
--------------------------------------------------------------------------------
/features/template/ui/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/template/ui/README.md:
--------------------------------------------------------------------------------
1 | # :features:template:ui module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/features/template/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.library.compose)
4 | alias(libs.plugins.ibrahimkurt.android.uiSetup)
5 | }
6 |
7 | android {
8 | namespace = "com.ibrahimkurt.features.template.ui"
9 | }
10 |
11 | dependencies {
12 | implementation(projects.core.ui)
13 | }
--------------------------------------------------------------------------------
/generateModuleGraphs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Copyright 2024 The Android Open Source Project
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # https://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 | #
18 | # Script to generate dependency graphs for each of the modules. The --exclude-module parameter can
19 | # be used to exclude modules which are not part of the root dependency graph (and which, if included
20 | # would cause the script to fail.
21 | #
22 | # Usage: generateModuleGraphs.sh --exclude-module :benchmarks --exclude-module :lint --exclude-module :ui-test-hilt-manifest
23 |
24 | # Check if the dot command is available
25 | if ! command -v dot &> /dev/null
26 | then
27 | echo "The 'dot' command is not found. This is required to generate SVGs from the Graphviz files."
28 | echo "Installation instructions:"
29 | echo " - On macOS: You can install Graphviz using Homebrew with the command: 'brew install graphviz'"
30 | echo " - On Ubuntu: You can install Graphviz using APT with the command: 'sudo apt-get install graphviz'"
31 | exit 1
32 | fi
33 |
34 | # Initialize an array to store excluded modules
35 | excluded_modules=()
36 |
37 | # Parse command-line arguments for excluded modules
38 | while [[ $# -gt 0 ]]; do
39 | case "$1" in
40 | --exclude-module)
41 | excluded_modules+=("$2")
42 | shift # Past argument
43 | shift # Past value
44 | ;;
45 | *)
46 | echo "Unknown parameter passed: $1"
47 | exit 1
48 | ;;
49 | esac
50 | done
51 |
52 | # Get the module paths
53 | module_paths=$(./gradlew -q printModulePaths --no-configuration-cache)
54 |
55 | # Ensure the output directory exists
56 | mkdir -p docs/images/graphs/
57 |
58 | # Function to check and create a README.md for modules which don't have one.
59 | check_and_create_readme() {
60 | local module_path="$1"
61 | local file_name="$2"
62 |
63 | local readme_path="${module_path:1}" # Remove leading colon
64 | readme_path=${readme_path//:/\/} # Replace colons with slashes
65 | readme_path="${readme_path}/README.md" #Append the filename
66 |
67 | # Check if README.md exists and create it if not
68 | if [[ ! -f "$readme_path" ]]; then
69 | echo "Creating README.md for ${module_path}"
70 |
71 | # Determine the depth of the module based on the number of colons
72 | local depth=$(awk -F: '{print NF-1}' <<< "${module_path}")
73 |
74 | # Construct the relative image path with the correct number of "../"
75 | local relative_image_path="../"
76 | for ((i=1; i<$depth; i++)); do
77 | relative_image_path+="../"
78 | done
79 | relative_image_path+="docs/images/graphs/${file_name}.svg"
80 |
81 | echo "# ${module_path} module" > "$readme_path"
82 | echo "## Dependency graph" >> "$readme_path"
83 | echo "" >> "$readme_path"
84 | fi
85 | }
86 |
87 | # Loop through each module path
88 | echo "$module_paths" | while read -r module_path; do
89 | # Check if the module is in the excluded list
90 | if [[ ! " ${excluded_modules[@]} " =~ " ${module_path} " ]]; then
91 | # Derive the filename from the module path
92 | file_name="dep_graph${module_path//:/_}" # Replace colons with underscores
93 | file_name="${file_name//-/_}" # Replace dashes with underscores
94 |
95 | check_and_create_readme "$module_path" "$file_name"
96 |
97 | # Generate the .gv file in a temporary location
98 | # "docs/images/graphs/${file_name}.svg"
105 | # Remove the temporary .gv file
106 | rm "/tmp/${file_name}.gv"
107 | fi
108 | done
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC
10 |
11 | # When configured, Gradle will run in incubating parallel mode.
12 | # This option should only be used with decoupled projects. More details, visit
13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
14 | org.gradle.parallel=true
15 |
16 | # Not encouraged by Gradle and can produce weird results. Wait for isolated projects instead.
17 | org.gradle.configureondemand=false
18 |
19 | # Enable caching between builds.
20 | org.gradle.caching=true
21 |
22 | # Enable configuration caching between builds.
23 | org.gradle.configuration-cache=false
24 | # This option is set because of https://github.com/google/play-services-plugins/issues/246
25 | # to generate the Configuration Cache regardless of incompatible tasks.
26 | # See https://github.com/android/nowinandroid/issues/1022 before using it.
27 | org.gradle.configuration-cache.problems=warn
28 | # When configured, Gradle will run in incubating parallel mode.
29 | # This option should only be used with decoupled projects. For more details, visit
30 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
31 | # org.gradle.parallel=true
32 | # AndroidX package structure to make it clearer which packages are bundled with the
33 | # Android operating system, and which are packaged with your app's APK
34 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
35 | android.useAndroidX=true
36 | # Kotlin code style for this project: "official" or "obsolete":
37 | kotlin.code.style=official
38 | # Enables namespacing of each library's R class so that its R class includes only the
39 | # resources declared in the library itself and none from the library's dependencies,
40 | # thereby reducing the size of the R class for that library
41 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.4.0-rc02"
3 | hiltNavigationCompose = "1.2.0"
4 | kotlin = "1.9.23"
5 | coreKtx = "1.13.0"
6 | kotlinxCoroutinesCore = "1.8.1-Beta"
7 | kotlinxSerializationCoreJvm = "1.6.3"
8 | ktorClientOkhttp = "2.3.5"
9 | library = "4.0.0"
10 | libraryNoOp = "4.0.0"
11 | activityCompose = "1.9.0"
12 | composeBom = "2024.04.01"
13 | jetbrainsKotlinJvm = "1.9.23"
14 | hilt = "2.51.1"
15 | ksp = "1.9.23-1.0.19"
16 | androidxComposeCompiler = "1.5.12"
17 | androidxLifecycle = "2.7.0"
18 | navigationCompose = "2.7.7"
19 | pagingRuntimeKtx = "3.2.1"
20 | pagingCompose = "3.3.0-beta01"
21 | retrofit = "2.11.0"
22 | retrofit2KotlinxSerializationConverter = "1.0.0"
23 | kotlinxSerializationJson = "1.6.3"
24 | detekt = "1.23.6"
25 | moduleGraph = "2.5.0"
26 | coil = "2.6.0"
27 |
28 |
29 | [libraries]
30 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
31 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
32 |
33 | #Compose UI
34 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
35 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
36 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
37 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
38 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
39 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
40 | androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" }
41 |
42 | #Lifecycle Scopes
43 | androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
44 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
45 | androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
46 | androidx-lifecycle-viewModel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
47 |
48 | #Hilt
49 | hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
50 | hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
51 | javax-inject = { group = "javax.inject", name = "javax.inject", version = "1" }
52 |
53 | #Navigation
54 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
55 | androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
56 |
57 | #Paging3
58 | androidx-paging3 = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "pagingRuntimeKtx" }
59 | kotlinx-paging3Common = { group = "androidx.paging", name = "paging-common", version.ref = "pagingRuntimeKtx" }
60 | androidx-paging3Compose = { group = "androidx.paging", name = "paging-compose", version.ref = "pagingCompose" }
61 |
62 | #Coroutine
63 | kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
64 |
65 |
66 | #Coil
67 | coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
68 | coilCompose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
69 | coilSvg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
70 |
71 | #Retrofit
72 | retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
73 | retrofit2-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" }
74 |
75 | #Serialization
76 | kotlinx-serialization-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core-jvm", version.ref = "kotlinxSerializationCoreJvm" }
77 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
78 |
79 |
80 | #Chucker
81 | chucker = { group = "com.github.chuckerteam.chucker", name = "library", version.ref = "library" }
82 | chuckerNoOp = { group = "com.github.chuckerteam.chucker", name = "library-no-op", version.ref = "libraryNoOp" }
83 |
84 | #Detekt
85 | detekt = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
86 |
87 | #build-logic
88 | android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
89 | kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
90 | detekt-gradlePlugin = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detekt" }
91 |
92 | [plugins]
93 | android-application = { id = "com.android.application", version.ref = "agp" }
94 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
95 | jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
96 | android-library = { id = "com.android.library", version.ref = "agp" }
97 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
98 | hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
99 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
100 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
101 | module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "moduleGraph" }
102 |
103 |
104 | # Plugins defined by this project
105 | ibrahimkurt-android-application = { id = "ibrahimkurt.android.application", version = "unspecified" }
106 | ibrahimkurt-android-hilt = { id = "ibrahimkurt.android.hilt", version = "unspecified" }
107 | ibrahimkurt-android-compose = { id = "ibrahimkurt.android.compose", version = "unspecified" }
108 | ibrahimkurt-jvm-library = { id = "ibrahimkurt.jvm.library", version = "unspecified" }
109 | ibrahimkurt-android-library = { id = "ibrahimkurt.android.library", version = "unspecified" }
110 | ibrahimkurt-android-library-compose = { id = "ibrahimkurt.android.library.compose", version = "unspecified" }
111 | ibrahimkurt-android-uiSetup = { id = "ibrahimkurt.android.ui-setup", version = "unspecified" }
112 | ibrahimkurt-android-retrofitSerialization = { id = "ibrahimkurt.android.retrofit-serialization", version = "unspecified" }
113 | ibrahimkurt-jvm-detekt = { id = "ibrahimkurt.jvm.detekt", version = "unspecified" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntuyiw/MultiModReelCompose/91dda3230cfc52f7882c9bd97b261976e2ae47f4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Apr 14 23:50:43 TRT 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/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%" == "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%"=="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 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/navigation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/navigation/README.md:
--------------------------------------------------------------------------------
1 | # :navigation module
2 | ## Dependency graph
3 | 
4 |
--------------------------------------------------------------------------------
/navigation/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.ibrahimkurt.android.library)
3 | alias(libs.plugins.ibrahimkurt.android.library.compose)
4 | alias(libs.plugins.ibrahimkurt.android.uiSetup)
5 |
6 | }
7 |
8 | android {
9 | namespace = "com.ibrahimkurt.navigation"
10 |
11 | }
12 |
13 | dependencies {
14 |
15 | implementation(libs.androidx.navigation.compose)
16 | implementation(projects.core.component)
17 |
18 | implementation(projects.features.home.ui)
19 | implementation(projects.features.detail.ui)
20 | }
--------------------------------------------------------------------------------
/navigation/src/main/java/com/ibrahimkurt/navigation/AppNavHost.kt:
--------------------------------------------------------------------------------
1 | package com.ibrahimkurt.navigation
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Surface
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.navigation.compose.NavHost
9 | import androidx.navigation.compose.rememberNavController
10 | import com.features.detail.ui.DetailNavActions
11 | import com.features.detail.ui.detail
12 | import com.features.detail.ui.navigateToDetail
13 | import com.ibrahimkurt.core.component.theme.MultiModReelComposeTheme
14 | import com.ibrahimkurt.features.home.ui.HomeNavActions
15 | import com.ibrahimkurt.features.home.ui.home
16 |
17 | @Composable
18 | fun AppNavHost() {
19 | val appNavHostController = rememberNavController()
20 | MultiModReelComposeTheme {
21 | Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
22 | NavHost(navController = appNavHostController, startDestination = "home") {
23 | home(
24 | homeNavActions = HomeNavActions(
25 | navigateToDetail = {
26 | appNavHostController.navigateToDetail(it)
27 | }
28 | )
29 | )
30 |
31 | detail(
32 | detailNavActions = DetailNavActions(
33 | navigateToBack = appNavHostController::navigateUp
34 | )
35 | )
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | includeBuild("build-logic")
3 | repositories {
4 | google {
5 | content {
6 | includeGroupByRegex("com\\.android.*")
7 | includeGroupByRegex("com\\.google.*")
8 | includeGroupByRegex("androidx.*")
9 | }
10 | }
11 | mavenCentral()
12 | gradlePluginPortal()
13 | }
14 | }
15 | dependencyResolutionManagement {
16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
17 | repositories {
18 | google()
19 | mavenCentral()
20 | }
21 | }
22 |
23 | rootProject.name = "MultiModReelCompose"
24 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
25 | include(":app")
26 | include(":navigation")
27 | include(":core:network")
28 | include(":core:component")
29 | include(":core:common")
30 | include(":core:ui")
31 | include(":core:domain")
32 | include(":core:data")
33 | include(":features:template:ui")
34 | include(":features:template:data")
35 | include(":features:template:domain")
36 | include(":features:home:ui")
37 | include(":features:home:data")
38 | include(":features:home:domain")
39 | include(":core:pagination")
40 |
41 | include("features:detail:ui")
42 | include("features:detail:data")
43 | include("features:detail:domain")
--------------------------------------------------------------------------------