From 27f151b7be96a78aae4988ddda63a8a03b5e38aa Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Thu, 11 Sep 2025 21:47:10 +0300 Subject: [PATCH 1/8] Add startup and onboarding flow --- app/build.gradle | 18 +++ app/src/main/AndroidManifest.xml | 14 ++- .../data/repository/OnboardingRepository.java | 9 ++ .../androidtutorials/java/di/AppModule.java | 8 ++ .../screens/onboarding/OnboardingActivity.kt | 24 ++++ .../ui/screens/onboarding/OnboardingScreen.kt | 106 ++++++++++++++++++ .../onboarding/OnboardingViewModel.java | 35 ++++++ .../DefaultOnboardingRepository.java | 57 ++++++++++ .../ui/screens/startup/StartupActivity.java | 25 ++++- app/src/main/res/values/keys.xml | 1 + app/src/main/res/values/strings.xml | 3 + build.gradle | 1 + gradle/libs.versions.toml | 7 ++ 13 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/data/repository/OnboardingRepository.java create mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingActivity.kt create mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingScreen.kt create mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java create mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/repository/DefaultOnboardingRepository.java diff --git a/app/build.gradle b/app/build.gradle index a48b0ad7..639d4ec0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,8 @@ plugins { id 'com.google.firebase.crashlytics' id 'com.mikepenz.aboutlibraries.plugin' id 'com.google.dagger.hilt.android' + id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.kapt' } android { @@ -40,9 +42,18 @@ android { targetCompatibility JavaVersion.VERSION_21 } + kotlinOptions { + jvmTarget = '21' + } + buildFeatures { viewBinding true buildConfig true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion '1.5.11' } bundle { @@ -97,12 +108,19 @@ dependencies { implementation libs.codeview implementation libs.hilt.android annotationProcessor libs.hilt.compiler + kapt libs.hilt.compiler implementation libs.androidx.room.runtime annotationProcessor libs.androidx.room.compiler annotationProcessor libs.glide.compiler implementation libs.retrofit2 implementation libs.retrofit2.converter.gson + implementation platform(libs.compose.bom) + implementation libs.compose.ui + implementation libs.compose.material3 + implementation libs.compose.foundation + implementation libs.compose.activity + // Testing testImplementation libs.junit testImplementation libs.androidx.core.testing diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9adf030c..0a8fd440 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,20 +38,28 @@ tools:targetApi="33"> - - + + + + + Unit) { + val pagerState = rememberPagerState(pageCount = {3}) + val scope = rememberCoroutineScope() + Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + HorizontalPager(state = pagerState, modifier = Modifier.weight(1f)) { page -> + when (page) { + 0 -> ThemePage(viewModel) + 1 -> DefaultTabPage(viewModel) + else -> BottomBarPage(viewModel, onFinish) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (pagerState.currentPage > 0) { + TextButton(onClick = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) } }) { + Text(stringResource(R.string.back)) + } + } + if (pagerState.currentPage < 2) { + TextButton(onClick = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } }) { + Text(stringResource(R.string.next)) + } + } + } + } +} + +@Composable +private fun ThemePage(viewModel: OnboardingViewModel) { + val entries = stringArrayResource(R.array.preference_theme_entries) + val values = stringArrayResource(R.array.preference_theme_values) + var selected by remember { mutableStateOf(values[0]) } + Column { + Text(stringResource(R.string.dark_mode), style = MaterialTheme.typography.titleMedium) + entries.forEachIndexed { index, title -> + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton(selected = selected == values[index], onClick = { + selected = values[index] + viewModel.saveTheme(values[index]) + }) + Text(title) + } + } + } +} + +@Composable +private fun DefaultTabPage(viewModel: OnboardingViewModel) { + val entries = stringArrayResource(R.array.preference_default_tab_entries) + val values = stringArrayResource(R.array.preference_default_tab_values) + var selected by remember { mutableStateOf(values[0]) } + Column { + Text(stringResource(R.string.default_tab), style = MaterialTheme.typography.titleMedium) + entries.forEachIndexed { index, title -> + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton(selected = selected == values[index], onClick = { + selected = values[index] + viewModel.saveDefaultTab(values[index]) + }) + Text(title) + } + } + } +} + +@Composable +private fun BottomBarPage(viewModel: OnboardingViewModel, onFinish: () -> Unit) { + val entries = stringArrayResource(R.array.preference_bottom_navigation_bar_labels_entries) + val values = stringArrayResource(R.array.preference_bottom_navigation_bar_labels_values) + var selected by remember { mutableStateOf(values[0]) } + Column { + Text(stringResource(R.string.bottom_navigation_bar_labels), style = MaterialTheme.typography.titleMedium) + entries.forEachIndexed { index, title -> + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton(selected = selected == values[index], onClick = { + selected = values[index] + viewModel.saveBottomBarLabels(values[index]) + }) + Text(title) + } + } + Button(onClick = onFinish, modifier = Modifier.padding(top = 24.dp)) { + Text(stringResource(R.string.finish)) + } + } +} diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java new file mode 100644 index 00000000..9df30ef3 --- /dev/null +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java @@ -0,0 +1,35 @@ +package com.d4rk.androidtutorials.java.ui.screens.onboarding; + +import androidx.lifecycle.ViewModel; + +import com.d4rk.androidtutorials.java.data.repository.OnboardingRepository; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import javax.inject.Inject; + +@HiltViewModel +public class OnboardingViewModel extends ViewModel { + + private final OnboardingRepository repository; + + @Inject + public OnboardingViewModel(OnboardingRepository repository) { + this.repository = repository; + } + + public void saveTheme(String value) { + repository.setTheme(value); + } + + public void saveDefaultTab(String value) { + repository.setDefaultTab(value); + } + + public void saveBottomBarLabels(String value) { + repository.setBottomBarLabels(value); + } + + public void completeOnboarding() { + repository.setOnboardingComplete(); + } +} diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/repository/DefaultOnboardingRepository.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/repository/DefaultOnboardingRepository.java new file mode 100644 index 00000000..e6c47a84 --- /dev/null +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/repository/DefaultOnboardingRepository.java @@ -0,0 +1,57 @@ +package com.d4rk.androidtutorials.java.ui.screens.onboarding.repository; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.appcompat.app.AppCompatDelegate; +import androidx.preference.PreferenceManager; + +import com.d4rk.androidtutorials.java.R; + +public class DefaultOnboardingRepository implements com.d4rk.androidtutorials.java.data.repository.OnboardingRepository { + + private final Context context; + private final SharedPreferences prefs; + + public DefaultOnboardingRepository(Context context) { + this.context = context.getApplicationContext(); + this.prefs = PreferenceManager.getDefaultSharedPreferences(this.context); + } + + @Override + public boolean isOnboardingComplete() { + return prefs.getBoolean(context.getString(R.string.key_onboarding_complete), false); + } + + @Override + public void setOnboardingComplete() { + prefs.edit().putBoolean(context.getString(R.string.key_onboarding_complete), true).apply(); + context.getSharedPreferences("startup", Context.MODE_PRIVATE) + .edit().putBoolean("value", false).apply(); + } + + @Override + public void setTheme(String value) { + prefs.edit().putString(context.getString(R.string.key_theme), value).apply(); + String[] values = context.getResources().getStringArray(R.array.preference_theme_values); + int mode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + if (value.equals(values[1])) { + mode = AppCompatDelegate.MODE_NIGHT_NO; + } else if (value.equals(values[2])) { + mode = AppCompatDelegate.MODE_NIGHT_YES; + } else if (value.equals(values[3])) { + mode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY; + } + AppCompatDelegate.setDefaultNightMode(mode); + } + + @Override + public void setDefaultTab(String value) { + prefs.edit().putString(context.getString(R.string.key_default_tab), value).apply(); + } + + @Override + public void setBottomBarLabels(String value) { + prefs.edit().putString(context.getString(R.string.key_bottom_navigation_bar_labels), value).apply(); + } +} diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java index ae967d52..9b4c9686 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java @@ -5,9 +5,16 @@ import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProvider; import com.d4rk.androidtutorials.java.databinding.ActivityStartupBinding; import com.d4rk.androidtutorials.java.ui.screens.main.MainActivity; +import com.d4rk.androidtutorials.java.ui.screens.onboarding.OnboardingActivity; +import com.d4rk.androidtutorials.java.data.repository.OnboardingRepository; +import com.google.android.ump.ConsentRequestParameters; +import com.d4rk.androidtutorials.java.ui.screens.startup.StartupViewModel; + +import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; import me.zhanghai.android.fastscroll.FastScrollerBuilder; @@ -15,12 +22,28 @@ @AndroidEntryPoint public class StartupActivity extends AppCompatActivity { + @Inject + OnboardingRepository onboardingRepository; + + private StartupViewModel viewModel; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (onboardingRepository.isOnboardingComplete()) { + startActivity(new Intent(this, MainActivity.class)); + finish(); + return; + } + ActivityStartupBinding binding = ActivityStartupBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + viewModel = new ViewModelProvider(this).get(StartupViewModel.class); + ConsentRequestParameters params = new ConsentRequestParameters.Builder().build(); + viewModel.requestConsentInfoUpdate(this, params, + () -> viewModel.loadConsentForm(this, null), null); + new FastScrollerBuilder(binding.scrollView) .useMd2Style() .build(); @@ -31,7 +54,7 @@ protected void onCreate(Bundle savedInstanceState) { ); binding.floatingButtonAgree.setOnClickListener(v -> { - startActivity(new Intent(this, MainActivity.class)); + startActivity(new Intent(this, OnboardingActivity.class)); finish(); }); } diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 279a1f5b..2ad5e922 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -20,4 +20,5 @@ consent_ad_storage consent_ad_user_data consent_ad_personalization + onboarding_complete \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec36d720..bd434b74 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -472,4 +472,7 @@ Fetch data Make HTTP requests with Retrofit. Retrofit is a type-safe HTTP client for Android and Java. This example fetches data from a web service. + Next + Back + Finish diff --git a/build.gradle b/build.gradle index 4d91a56a..57c04684 100644 --- a/build.gradle +++ b/build.gradle @@ -5,4 +5,5 @@ plugins { //noinspection NewerVersionAvailable id 'com.mikepenz.aboutlibraries.plugin' version '11.3.0' apply true id 'com.google.dagger.hilt.android' version '2.57.1' apply false + id 'org.jetbrains.kotlin.android' version '1.9.24' apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aed72c36..74893ff2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,8 @@ hilt = "2.57.1" room = "2.8.0" glide = "5.0.4" retrofit = "3.0.0" +composeBom = "2024.02.01" +activityCompose = "1.9.2" [libraries] aboutlibraries = { module = "com.mikepenz:aboutlibraries", version.ref = "aboutlibraries" } @@ -75,3 +77,8 @@ glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } retrofit2 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +compose-ui = { module = "androidx.compose.ui:ui" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-foundation = { module = "androidx.compose.foundation:foundation" } +compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } From c11b33e576212e476a70a59a8422eb1d06c824a4 Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Thu, 11 Sep 2025 22:07:18 +0300 Subject: [PATCH 2/8] Update Play Services Ads dependency --- .idea/kotlinc.xml | 6 - .idea/workspace.xml | 1187 ----------------- app/build.gradle | 20 - app/src/main/AndroidManifest.xml | 4 - .../data/repository/OnboardingRepository.java | 9 - .../androidtutorials/java/di/AppModule.java | 8 - .../screens/onboarding/OnboardingActivity.kt | 24 - .../ui/screens/onboarding/OnboardingScreen.kt | 106 -- .../onboarding/OnboardingViewModel.java | 35 - .../DefaultOnboardingRepository.java | 57 - .../ui/screens/startup/StartupActivity.java | 15 +- app/src/main/res/values/keys.xml | 1 - app/src/main/res/values/strings.xml | 3 - build.gradle | 3 +- gradle/libs.versions.toml | 9 - 15 files changed, 2 insertions(+), 1485 deletions(-) delete mode 100644 .idea/kotlinc.xml delete mode 100644 .idea/workspace.xml delete mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/data/repository/OnboardingRepository.java delete mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingActivity.kt delete mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingScreen.kt delete mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java delete mode 100644 app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/repository/DefaultOnboardingRepository.java diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index fdf8d994..00000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 26f483aa..00000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,1187 +0,0 @@ - - - - - - - - - - false - android-35 - - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - night - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppTheme - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - Portrait - night - @style/AppThemeActionBar - - - - - @style/AppThemeActionBar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -