diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..38f9561 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,84 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.example.todo" + compileSdk = 36 + defaultConfig { + applicationId = "com.example.todo" + minSdk = 36 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + buildFeatures { + compose = true + aidl = false + buildConfig = false + shaders = false + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +kotlin { + jvmToolchain(17) +} + +dependencies { + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + androidTestImplementation(composeBom) + + // Core Android dependencies + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + + // Arch Components + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + + // Compose + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + // Tooling + debugImplementation(libs.androidx.compose.ui.tooling) + // Instrumented tests + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.test.manifest) + + // Local tests: jUnit, coroutines, Android runner + testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) + + // Instrumented tests: jUnit rules and runners + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.espresso.core) + + // Navigation + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) +} diff --git a/android/app/src/androidTest/java/com/example/todo/ui/main/MainScreenTest.kt b/android/app/src/androidTest/java/com/example/todo/ui/main/MainScreenTest.kt new file mode 100644 index 0000000..75da26c --- /dev/null +++ b/android/app/src/androidTest/java/com/example/todo/ui/main/MainScreenTest.kt @@ -0,0 +1,26 @@ +package com.example.todo.ui.main + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +/** UI tests for [com.example.todo.ui.main.MainScreen]. */ +class MainScreenTest { + + @get:Rule val composeTestRule = createAndroidComposeRule() + + @Before + fun setup() { + composeTestRule.setContent { MainScreen(FAKE_DATA) } + } + + @Test + fun firstItem_exists() { + FAKE_DATA.forEach { composeTestRule.onNodeWithText("Hello $it!").assertExists() } + } +} + +private val FAKE_DATA = listOf("Sample1", "Sample2", "Sample3") diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f4ac2e4 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/example/todo/MainActivity.kt b/android/app/src/main/java/com/example/todo/MainActivity.kt new file mode 100644 index 0000000..3221d2b --- /dev/null +++ b/android/app/src/main/java/com/example/todo/MainActivity.kt @@ -0,0 +1,22 @@ +package com.example.todo + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import com.example.todo.theme.TodoTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + enableEdgeToEdge() + setContent { + TodoTheme { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { MainNavigation() } } + } + } +} diff --git a/android/app/src/main/java/com/example/todo/Navigation.kt b/android/app/src/main/java/com/example/todo/Navigation.kt new file mode 100644 index 0000000..42fbea5 --- /dev/null +++ b/android/app/src/main/java/com/example/todo/Navigation.kt @@ -0,0 +1,27 @@ +package com.example.todo + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay +import com.example.todo.ui.main.MainScreen + +@Composable +fun MainNavigation() { + val backStack = rememberNavBackStack(Main) + + NavDisplay( + backStack = backStack, + onBack = { backStack.removeLastOrNull() }, + entryProvider = + entryProvider { + entry
{ + MainScreen(onItemClick = { navKey -> backStack.add(navKey) }, modifier = Modifier.safeDrawingPadding().padding(16.dp)) + } + }, + ) +} diff --git a/android/app/src/main/java/com/example/todo/NavigationKeys.kt b/android/app/src/main/java/com/example/todo/NavigationKeys.kt new file mode 100644 index 0000000..8639513 --- /dev/null +++ b/android/app/src/main/java/com/example/todo/NavigationKeys.kt @@ -0,0 +1,6 @@ +package com.example.todo + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable data object Main : NavKey diff --git a/android/app/src/main/java/com/example/todo/data/DataRepository.kt b/android/app/src/main/java/com/example/todo/data/DataRepository.kt new file mode 100644 index 0000000..d897ff9 --- /dev/null +++ b/android/app/src/main/java/com/example/todo/data/DataRepository.kt @@ -0,0 +1,12 @@ +package com.example.todo.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +interface DataRepository { + val data: Flow> +} + +class DefaultDataRepository : DataRepository { + override val data: Flow> = flow { emit(listOf("Android")) } +} diff --git a/android/app/src/main/java/com/example/todo/theme/Color.kt b/android/app/src/main/java/com/example/todo/theme/Color.kt new file mode 100644 index 0000000..21fcb37 --- /dev/null +++ b/android/app/src/main/java/com/example/todo/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.todo.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) diff --git a/android/app/src/main/java/com/example/todo/theme/Theme.kt b/android/app/src/main/java/com/example/todo/theme/Theme.kt new file mode 100644 index 0000000..003617d --- /dev/null +++ b/android/app/src/main/java/com/example/todo/theme/Theme.kt @@ -0,0 +1,50 @@ +package com.example.todo.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80) + +private val LightColorScheme = + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ + ) + +@Composable +fun TodoTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val colorScheme = + when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content) +} diff --git a/android/app/src/main/java/com/example/todo/theme/Type.kt b/android/app/src/main/java/com/example/todo/theme/Type.kt new file mode 100644 index 0000000..5f3c153 --- /dev/null +++ b/android/app/src/main/java/com/example/todo/theme/Type.kt @@ -0,0 +1,36 @@ +package com.example.todo.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ + ) diff --git a/android/app/src/main/java/com/example/todo/ui/main/MainScreen.kt b/android/app/src/main/java/com/example/todo/ui/main/MainScreen.kt new file mode 100644 index 0000000..d1fa484 --- /dev/null +++ b/android/app/src/main/java/com/example/todo/ui/main/MainScreen.kt @@ -0,0 +1,55 @@ +package com.example.todo.ui.main + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation3.runtime.NavKey +import com.example.todo.data.DefaultDataRepository +import com.example.todo.theme.TodoTheme + +@Composable +fun MainScreen( + onItemClick: (NavKey) -> Unit, + modifier: Modifier = Modifier, + viewModel: MainScreenViewModel = viewModel { MainScreenViewModel(DefaultDataRepository()) }, +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + when (state) { + MainScreenUiState.Loading -> { + // Blank + } + is MainScreenUiState.Success -> { + MainScreen(data = (state as MainScreenUiState.Success).data, modifier = modifier) + } + is MainScreenUiState.Error -> { + Text("Error loading data: ${(state as MainScreenUiState.Error).throwable.message}") + } + } +} + +@Composable +internal fun MainScreen(data: List, modifier: Modifier = Modifier) { + Column(modifier) { data.forEach { Greeting(it) } } +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + Text(text = "Hello $name!", modifier = modifier) +} + +@Preview(showBackground = true) +@Composable +fun MainScreenPreview() { + TodoTheme { MainScreen(listOf("Android")) } +} + +@Preview(showBackground = true, widthDp = 340) +@Composable +fun MainScreenPortraitPreview() { + TodoTheme { MainScreen(listOf("Android")) } +} diff --git a/android/app/src/main/java/com/example/todo/ui/main/MainScreenViewModel.kt b/android/app/src/main/java/com/example/todo/ui/main/MainScreenViewModel.kt new file mode 100644 index 0000000..a03664c --- /dev/null +++ b/android/app/src/main/java/com/example/todo/ui/main/MainScreenViewModel.kt @@ -0,0 +1,27 @@ +package com.example.todo.ui.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.todo.data.DataRepository +import com.example.todo.ui.main.MainScreenUiState.Success +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class MainScreenViewModel(dataRepository: DataRepository) : ViewModel() { + val uiState: StateFlow = + dataRepository.data + .map, MainScreenUiState>(::Success) + .catch { emit(MainScreenUiState.Error(it)) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainScreenUiState.Loading) +} + +sealed interface MainScreenUiState { + object Loading : MainScreenUiState + + data class Error(val throwable: Throwable) : MainScreenUiState + + data class Success(val data: List) : MainScreenUiState +} diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..5996891 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Todo + \ No newline at end of file diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..9af0748 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +