Skip to content

Commit 27f151b

Browse files
Add startup and onboarding flow
1 parent 3c172ba commit 27f151b

File tree

13 files changed

+304
-4
lines changed

13 files changed

+304
-4
lines changed

app/build.gradle

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ plugins {
44
id 'com.google.firebase.crashlytics'
55
id 'com.mikepenz.aboutlibraries.plugin'
66
id 'com.google.dagger.hilt.android'
7+
id 'org.jetbrains.kotlin.android'
8+
id 'org.jetbrains.kotlin.kapt'
79
}
810

911
android {
@@ -40,9 +42,18 @@ android {
4042
targetCompatibility JavaVersion.VERSION_21
4143
}
4244

45+
kotlinOptions {
46+
jvmTarget = '21'
47+
}
48+
4349
buildFeatures {
4450
viewBinding true
4551
buildConfig true
52+
compose true
53+
}
54+
55+
composeOptions {
56+
kotlinCompilerExtensionVersion '1.5.11'
4657
}
4758

4859
bundle {
@@ -97,12 +108,19 @@ dependencies {
97108
implementation libs.codeview
98109
implementation libs.hilt.android
99110
annotationProcessor libs.hilt.compiler
111+
kapt libs.hilt.compiler
100112
implementation libs.androidx.room.runtime
101113
annotationProcessor libs.androidx.room.compiler
102114
annotationProcessor libs.glide.compiler
103115
implementation libs.retrofit2
104116
implementation libs.retrofit2.converter.gson
105117

118+
implementation platform(libs.compose.bom)
119+
implementation libs.compose.ui
120+
implementation libs.compose.material3
121+
implementation libs.compose.foundation
122+
implementation libs.compose.activity
123+
106124
// Testing
107125
testImplementation libs.junit
108126
testImplementation libs.androidx.core.testing

app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,28 @@
3838
tools:targetApi="33">
3939

4040
<activity
41-
android:name=".ui.screens.main.MainActivity"
41+
android:name=".ui.screens.startup.StartupActivity"
4242
android:exported="true"
4343
android:theme="@style/SplashScreenTheme">
4444
<intent-filter>
45-
<action android:name="android.intent.action.VIEW" />
4645
<action android:name="android.intent.action.MAIN" />
47-
4846
<category android:name="android.intent.category.LAUNCHER" />
4947
</intent-filter>
48+
</activity>
49+
50+
<activity
51+
android:name=".ui.screens.main.MainActivity"
52+
android:exported="true"
53+
android:theme="@style/SplashScreenTheme">
5054
<meta-data
5155
android:name="android.app.shortcuts"
5256
android:resource="@xml/shortcuts" />
5357
</activity>
5458

59+
<activity
60+
android:name=".ui.screens.onboarding.OnboardingActivity"
61+
android:exported="false" />
62+
5563
<activity
5664
android:name="com.mikepenz.aboutlibraries.ui.LibsActivity"
5765
android:exported="false"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.d4rk.androidtutorials.java.data.repository;
2+
3+
public interface OnboardingRepository {
4+
boolean isOnboardingComplete();
5+
void setOnboardingComplete();
6+
void setTheme(String value);
7+
void setDefaultTab(String value);
8+
void setBottomBarLabels(String value);
9+
}

app/src/main/java/com/d4rk/androidtutorials/java/di/AppModule.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
import com.d4rk.androidtutorials.java.ui.screens.help.repository.HelpRepository;
5454
import com.d4rk.androidtutorials.java.ui.screens.settings.repository.SettingsRepository;
5555
import com.d4rk.androidtutorials.java.ui.screens.startup.repository.StartupRepository;
56+
import com.d4rk.androidtutorials.java.ui.screens.onboarding.repository.DefaultOnboardingRepository;
57+
import com.d4rk.androidtutorials.java.data.repository.OnboardingRepository;
5658

5759
import java.util.concurrent.ExecutorService;
5860
import java.util.concurrent.Executors;
@@ -262,6 +264,12 @@ public SupportRepository provideSupportRepository(Application application) {
262264
return new DefaultSupportRepository(application);
263265
}
264266

267+
@Provides
268+
@Singleton
269+
public OnboardingRepository provideOnboardingRepository(Application application) {
270+
return new DefaultOnboardingRepository(application);
271+
}
272+
265273
@Provides
266274
public InitBillingClientUseCase provideInitBillingClientUseCase(SupportRepository repository) {
267275
return new InitBillingClientUseCase(repository);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.d4rk.androidtutorials.java.ui.screens.onboarding
2+
3+
import android.content.Intent
4+
import android.os.Bundle
5+
import androidx.activity.ComponentActivity
6+
import androidx.activity.compose.setContent
7+
import dagger.hilt.android.AndroidEntryPoint
8+
import com.d4rk.androidtutorials.java.ui.screens.main.MainActivity
9+
import androidx.lifecycle.viewmodel.compose.viewModel
10+
11+
@AndroidEntryPoint
12+
class OnboardingActivity : ComponentActivity() {
13+
override fun onCreate(savedInstanceState: Bundle?) {
14+
super.onCreate(savedInstanceState)
15+
setContent {
16+
val viewModel: OnboardingViewModel = viewModel()
17+
OnboardingScreen(viewModel) {
18+
viewModel.completeOnboarding()
19+
startActivity(Intent(this, MainActivity::class.java))
20+
finish()
21+
}
22+
}
23+
}
24+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.d4rk.androidtutorials.java.ui.screens.onboarding
2+
3+
import androidx.compose.foundation.ExperimentalFoundationApi
4+
import androidx.compose.foundation.layout.*
5+
import androidx.compose.foundation.pager.HorizontalPager
6+
import androidx.compose.foundation.pager.rememberPagerState
7+
import androidx.compose.material3.*
8+
import androidx.compose.runtime.*
9+
import androidx.compose.ui.Alignment
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.res.stringArrayResource
12+
import androidx.compose.ui.res.stringResource
13+
import androidx.compose.ui.unit.dp
14+
import com.d4rk.androidtutorials.java.R
15+
import kotlinx.coroutines.launch
16+
17+
@OptIn(ExperimentalFoundationApi::class)
18+
@Composable
19+
fun OnboardingScreen(viewModel: OnboardingViewModel, onFinish: () -> Unit) {
20+
val pagerState = rememberPagerState(pageCount = {3})
21+
val scope = rememberCoroutineScope()
22+
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
23+
HorizontalPager(state = pagerState, modifier = Modifier.weight(1f)) { page ->
24+
when (page) {
25+
0 -> ThemePage(viewModel)
26+
1 -> DefaultTabPage(viewModel)
27+
else -> BottomBarPage(viewModel, onFinish)
28+
}
29+
}
30+
Row(
31+
modifier = Modifier.fillMaxWidth(),
32+
horizontalArrangement = Arrangement.SpaceBetween
33+
) {
34+
if (pagerState.currentPage > 0) {
35+
TextButton(onClick = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) } }) {
36+
Text(stringResource(R.string.back))
37+
}
38+
}
39+
if (pagerState.currentPage < 2) {
40+
TextButton(onClick = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } }) {
41+
Text(stringResource(R.string.next))
42+
}
43+
}
44+
}
45+
}
46+
}
47+
48+
@Composable
49+
private fun ThemePage(viewModel: OnboardingViewModel) {
50+
val entries = stringArrayResource(R.array.preference_theme_entries)
51+
val values = stringArrayResource(R.array.preference_theme_values)
52+
var selected by remember { mutableStateOf(values[0]) }
53+
Column {
54+
Text(stringResource(R.string.dark_mode), style = MaterialTheme.typography.titleMedium)
55+
entries.forEachIndexed { index, title ->
56+
Row(verticalAlignment = Alignment.CenterVertically) {
57+
RadioButton(selected = selected == values[index], onClick = {
58+
selected = values[index]
59+
viewModel.saveTheme(values[index])
60+
})
61+
Text(title)
62+
}
63+
}
64+
}
65+
}
66+
67+
@Composable
68+
private fun DefaultTabPage(viewModel: OnboardingViewModel) {
69+
val entries = stringArrayResource(R.array.preference_default_tab_entries)
70+
val values = stringArrayResource(R.array.preference_default_tab_values)
71+
var selected by remember { mutableStateOf(values[0]) }
72+
Column {
73+
Text(stringResource(R.string.default_tab), style = MaterialTheme.typography.titleMedium)
74+
entries.forEachIndexed { index, title ->
75+
Row(verticalAlignment = Alignment.CenterVertically) {
76+
RadioButton(selected = selected == values[index], onClick = {
77+
selected = values[index]
78+
viewModel.saveDefaultTab(values[index])
79+
})
80+
Text(title)
81+
}
82+
}
83+
}
84+
}
85+
86+
@Composable
87+
private fun BottomBarPage(viewModel: OnboardingViewModel, onFinish: () -> Unit) {
88+
val entries = stringArrayResource(R.array.preference_bottom_navigation_bar_labels_entries)
89+
val values = stringArrayResource(R.array.preference_bottom_navigation_bar_labels_values)
90+
var selected by remember { mutableStateOf(values[0]) }
91+
Column {
92+
Text(stringResource(R.string.bottom_navigation_bar_labels), style = MaterialTheme.typography.titleMedium)
93+
entries.forEachIndexed { index, title ->
94+
Row(verticalAlignment = Alignment.CenterVertically) {
95+
RadioButton(selected = selected == values[index], onClick = {
96+
selected = values[index]
97+
viewModel.saveBottomBarLabels(values[index])
98+
})
99+
Text(title)
100+
}
101+
}
102+
Button(onClick = onFinish, modifier = Modifier.padding(top = 24.dp)) {
103+
Text(stringResource(R.string.finish))
104+
}
105+
}
106+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.d4rk.androidtutorials.java.ui.screens.onboarding;
2+
3+
import androidx.lifecycle.ViewModel;
4+
5+
import com.d4rk.androidtutorials.java.data.repository.OnboardingRepository;
6+
7+
import dagger.hilt.android.lifecycle.HiltViewModel;
8+
import javax.inject.Inject;
9+
10+
@HiltViewModel
11+
public class OnboardingViewModel extends ViewModel {
12+
13+
private final OnboardingRepository repository;
14+
15+
@Inject
16+
public OnboardingViewModel(OnboardingRepository repository) {
17+
this.repository = repository;
18+
}
19+
20+
public void saveTheme(String value) {
21+
repository.setTheme(value);
22+
}
23+
24+
public void saveDefaultTab(String value) {
25+
repository.setDefaultTab(value);
26+
}
27+
28+
public void saveBottomBarLabels(String value) {
29+
repository.setBottomBarLabels(value);
30+
}
31+
32+
public void completeOnboarding() {
33+
repository.setOnboardingComplete();
34+
}
35+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.d4rk.androidtutorials.java.ui.screens.onboarding.repository;
2+
3+
import android.content.Context;
4+
import android.content.SharedPreferences;
5+
6+
import androidx.appcompat.app.AppCompatDelegate;
7+
import androidx.preference.PreferenceManager;
8+
9+
import com.d4rk.androidtutorials.java.R;
10+
11+
public class DefaultOnboardingRepository implements com.d4rk.androidtutorials.java.data.repository.OnboardingRepository {
12+
13+
private final Context context;
14+
private final SharedPreferences prefs;
15+
16+
public DefaultOnboardingRepository(Context context) {
17+
this.context = context.getApplicationContext();
18+
this.prefs = PreferenceManager.getDefaultSharedPreferences(this.context);
19+
}
20+
21+
@Override
22+
public boolean isOnboardingComplete() {
23+
return prefs.getBoolean(context.getString(R.string.key_onboarding_complete), false);
24+
}
25+
26+
@Override
27+
public void setOnboardingComplete() {
28+
prefs.edit().putBoolean(context.getString(R.string.key_onboarding_complete), true).apply();
29+
context.getSharedPreferences("startup", Context.MODE_PRIVATE)
30+
.edit().putBoolean("value", false).apply();
31+
}
32+
33+
@Override
34+
public void setTheme(String value) {
35+
prefs.edit().putString(context.getString(R.string.key_theme), value).apply();
36+
String[] values = context.getResources().getStringArray(R.array.preference_theme_values);
37+
int mode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
38+
if (value.equals(values[1])) {
39+
mode = AppCompatDelegate.MODE_NIGHT_NO;
40+
} else if (value.equals(values[2])) {
41+
mode = AppCompatDelegate.MODE_NIGHT_YES;
42+
} else if (value.equals(values[3])) {
43+
mode = AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY;
44+
}
45+
AppCompatDelegate.setDefaultNightMode(mode);
46+
}
47+
48+
@Override
49+
public void setDefaultTab(String value) {
50+
prefs.edit().putString(context.getString(R.string.key_default_tab), value).apply();
51+
}
52+
53+
@Override
54+
public void setBottomBarLabels(String value) {
55+
prefs.edit().putString(context.getString(R.string.key_bottom_navigation_bar_labels), value).apply();
56+
}
57+
}

app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,45 @@
55
import android.os.Bundle;
66

77
import androidx.appcompat.app.AppCompatActivity;
8+
import androidx.lifecycle.ViewModelProvider;
89

910
import com.d4rk.androidtutorials.java.databinding.ActivityStartupBinding;
1011
import com.d4rk.androidtutorials.java.ui.screens.main.MainActivity;
12+
import com.d4rk.androidtutorials.java.ui.screens.onboarding.OnboardingActivity;
13+
import com.d4rk.androidtutorials.java.data.repository.OnboardingRepository;
14+
import com.google.android.ump.ConsentRequestParameters;
15+
import com.d4rk.androidtutorials.java.ui.screens.startup.StartupViewModel;
16+
17+
import javax.inject.Inject;
1118

1219
import dagger.hilt.android.AndroidEntryPoint;
1320
import me.zhanghai.android.fastscroll.FastScrollerBuilder;
1421

1522
@AndroidEntryPoint
1623
public class StartupActivity extends AppCompatActivity {
1724

25+
@Inject
26+
OnboardingRepository onboardingRepository;
27+
28+
private StartupViewModel viewModel;
29+
1830
@Override
1931
protected void onCreate(Bundle savedInstanceState) {
2032
super.onCreate(savedInstanceState);
33+
if (onboardingRepository.isOnboardingComplete()) {
34+
startActivity(new Intent(this, MainActivity.class));
35+
finish();
36+
return;
37+
}
38+
2139
ActivityStartupBinding binding = ActivityStartupBinding.inflate(getLayoutInflater());
2240
setContentView(binding.getRoot());
2341

42+
viewModel = new ViewModelProvider(this).get(StartupViewModel.class);
43+
ConsentRequestParameters params = new ConsentRequestParameters.Builder().build();
44+
viewModel.requestConsentInfoUpdate(this, params,
45+
() -> viewModel.loadConsentForm(this, null), null);
46+
2447
new FastScrollerBuilder(binding.scrollView)
2548
.useMd2Style()
2649
.build();
@@ -31,7 +54,7 @@ protected void onCreate(Bundle savedInstanceState) {
3154
);
3255

3356
binding.floatingButtonAgree.setOnClickListener(v -> {
34-
startActivity(new Intent(this, MainActivity.class));
57+
startActivity(new Intent(this, OnboardingActivity.class));
3558
finish();
3659
});
3760
}

app/src/main/res/values/keys.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@
2020
<string name="key_consent_ad_storage" translatable="false">consent_ad_storage</string>
2121
<string name="key_consent_ad_user_data" translatable="false">consent_ad_user_data</string>
2222
<string name="key_consent_ad_personalization" translatable="false">consent_ad_personalization</string>
23+
<string name="key_onboarding_complete" translatable="false">onboarding_complete</string>
2324
</resources>

0 commit comments

Comments
 (0)