fix gitignore

This commit is contained in:
2026-02-26 21:32:08 -05:00
parent a6a76c90b9
commit 06873ca27b
83 changed files with 2675 additions and 2 deletions
+2 -2
View File
@@ -13,8 +13,6 @@ npm-debug.*
# Expo / React Native
.expo/
app/
ios/
web-build/
# Shadow-cljs / Clojure
@@ -47,6 +45,8 @@ test/cljd-out/
local.properties
gradle/wrapper/gradle-wrapper.jar
captures/
*.keystore
*.jks
# Kotlin
*.class
+182
View File
@@ -0,0 +1,182 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
hermesCommand = new File(["node", "--print", "require.resolve('hermes-compiler/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
// Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects.
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
}
/**
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
*/
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace 'com.anonymous.todoexpojs'
defaultConfig {
applicationId 'com.anonymous.todoexpojs'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
shrinkResources enableShrinkResources.toBoolean()
minifyEnabled enableMinifyInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
crunchPngs enablePngCrunchInRelease.toBoolean()
}
}
packagingOptions {
jniLibs {
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
useLegacyPackaging enableLegacyPackaging.toBoolean()
}
}
androidResources {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
// Split option: 'foo,bar' -> ['foo', 'bar']
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
// Trim all elements in place.
for (i in 0..<options.size()) options[i] = options[i].trim();
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
options -= ""
if (options.length > 0) {
println "android.packagingOptions.$prop += $options ($options.length)"
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
options.each {
android.packagingOptions[prop] += it
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
}
if (isWebpEnabled) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
if (isWebpAnimatedEnabled) {
// Animated webp support
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
}
}
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
+14
View File
@@ -0,0 +1,14 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>
@@ -0,0 +1,25 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|smallestScreenSize" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,61 @@
package com.anonymous.todoexpojs
import android.os.Build
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
setTheme(R.style.AppTheme);
super.onCreate(null)
}
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "main"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
object : DefaultReactActivityDelegate(
this,
mainComponentName,
fabricEnabled
){})
}
/**
* Align the back button behavior with Android S
* where moving root activities to background instead of finishing activities.
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
*/
override fun invokeDefaultOnBackPressed() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (!moveTaskToBack(false)) {
// For non-root activities, use the default implementation to finish them.
super.invokeDefaultOnBackPressed()
}
return
}
// Use the default back button implementation on Android S
// because it's doing more than [Activity.moveTaskToBack] in fact.
super.invokeDefaultOnBackPressed()
}
}
@@ -0,0 +1,45 @@
package com.anonymous.todoexpojs
import android.app.Application
import android.content.res.Configuration
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.common.ReleaseLevel
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ExpoReactHostFactory
class MainApplication : Application(), ReactApplication {
override val reactHost: ReactHost by lazy {
ExpoReactHostFactory.getDefaultReactHost(
context = applicationContext,
packageList =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}
)
}
override fun onCreate() {
super.onCreate()
DefaultNewArchitectureEntryPoint.releaseLevel = try {
ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
} catch (e: IllegalArgumentException) {
ReleaseLevel.STABLE
}
loadReactNative(this)
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@@ -0,0 +1,6 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
<item>
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
</item>
</layer-list>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
>
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1 @@
<resources/>
@@ -0,0 +1,4 @@
<resources>
<color name="splashscreen_background">#FFFFFF</color>
<color name="colorPrimary">#023c69</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Todo App</string>
</resources>
@@ -0,0 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
</style>
</resources>
+44
View File
@@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.anonymous.todo_flutter"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.anonymous.todo_flutter"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="todo_flutter"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.anonymous.todo_flutter
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+42
View File
@@ -0,0 +1,42 @@
plugins {
id("com.android.application")
}
android {
namespace = "com.example.todojava"
compileSdk = 35
defaultConfig {
applicationId = "com.example.todojava"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(libs.material)
implementation(libs.room.runtime)
annotationProcessor(libs.room.compiler)
implementation(libs.lifecycle.viewmodel)
implementation(libs.lifecycle.livedata)
implementation(libs.recyclerview)
implementation(libs.appcompat)
implementation(libs.constraintlayout)
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.TodoJava">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,127 @@
package com.example.todojava;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.todojava.data.Todo;
import com.example.todojava.databinding.ActivityMainBinding;
import com.google.android.material.snackbar.Snackbar;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private TodoViewModel viewModel;
private TodoAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
viewModel = new ViewModelProvider(this).get(TodoViewModel.class);
setupRecyclerView();
setupFilterChips();
setupFab();
observeData();
}
private void setupRecyclerView() {
adapter = new TodoAdapter();
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerView.setAdapter(adapter);
adapter.setOnTodoClickListener(todo -> {
TodoFormDialogFragment dialog = TodoFormDialogFragment.newInstance(todo);
dialog.show(getSupportFragmentManager(), "edit_todo");
});
adapter.setOnTodoCheckedListener(todo -> viewModel.toggleCompleted(todo));
// Swipe to delete
SwipeToDeleteCallback swipeCallback = new SwipeToDeleteCallback(position -> {
Todo todo = adapter.getCurrentList().get(position);
viewModel.delete(todo);
Snackbar.make(binding.getRoot(), R.string.todo_deleted, Snackbar.LENGTH_LONG)
.setAction(R.string.undo, v -> viewModel.insert(todo))
.show();
});
new ItemTouchHelper(swipeCallback).attachToRecyclerView(binding.recyclerView);
}
private void setupFilterChips() {
binding.chipAll.setOnClickListener(v -> viewModel.setFilter(TodoViewModel.Filter.ALL));
binding.chipActive.setOnClickListener(v -> viewModel.setFilter(TodoViewModel.Filter.ACTIVE));
binding.chipDone.setOnClickListener(v -> viewModel.setFilter(TodoViewModel.Filter.DONE));
}
private void setupFab() {
binding.fab.setOnClickListener(v -> {
TodoFormDialogFragment dialog = TodoFormDialogFragment.newInstance();
dialog.show(getSupportFragmentManager(), "add_todo");
});
}
private void observeData() {
viewModel.getFilteredTodos().observe(this, todos -> {
adapter.submitList(todos);
updateEmptyState(todos);
});
// Update chip text with counts
viewModel.getAllCount().observe(this, count -> {
String text = getString(R.string.filter_all) + " (" + count + ")";
binding.chipAll.setText(text);
});
viewModel.getActiveCount().observe(this, count -> {
String text = getString(R.string.filter_active) + " (" + count + ")";
binding.chipActive.setText(text);
});
viewModel.getCompletedCount().observe(this, count -> {
String text = getString(R.string.filter_done) + " (" + count + ")";
binding.chipDone.setText(text);
});
}
private void updateEmptyState(List<Todo> todos) {
boolean isEmpty = todos == null || todos.isEmpty();
binding.emptyState.getRoot().setVisibility(isEmpty ? View.VISIBLE : View.GONE);
binding.recyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
if (isEmpty) {
TodoViewModel.Filter filter = viewModel.getCurrentFilter().getValue();
if (filter == null) filter = TodoViewModel.Filter.ALL;
View emptyRoot = binding.emptyState.getRoot();
android.widget.TextView titleView = emptyRoot.findViewById(R.id.emptyTitle);
android.widget.TextView subtitleView = emptyRoot.findViewById(R.id.emptySubtitle);
switch (filter) {
case ACTIVE:
titleView.setText(R.string.empty_active_title);
subtitleView.setText(R.string.empty_active_subtitle);
break;
case DONE:
titleView.setText(R.string.empty_done_title);
subtitleView.setText(R.string.empty_done_subtitle);
break;
default:
titleView.setText(R.string.empty_state_title);
subtitleView.setText(R.string.empty_state_subtitle);
break;
}
}
}
}
@@ -0,0 +1,84 @@
package com.example.todojava;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.ColorDrawable;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
public class SwipeToDeleteCallback extends ItemTouchHelper.SimpleCallback {
public interface OnSwipeListener {
void onSwiped(int position);
}
private final OnSwipeListener listener;
private final ColorDrawable background;
private final Paint textPaint;
public SwipeToDeleteCallback(OnSwipeListener listener) {
super(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
this.listener = listener;
this.background = new ColorDrawable(Color.parseColor("#E53935"));
this.textPaint = new Paint();
this.textPaint.setColor(Color.WHITE);
this.textPaint.setTextSize(40f);
this.textPaint.setAntiAlias(true);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
listener.onSwiped(viewHolder.getAdapterPosition());
}
@Override
public void onChildDraw(@NonNull Canvas c,
@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
float dX, float dY,
int actionState, boolean isCurrentlyActive) {
View itemView = viewHolder.itemView;
if (dX > 0) {
// Swiping right
background.setBounds(itemView.getLeft(), itemView.getTop(),
itemView.getLeft() + (int) dX, itemView.getBottom());
} else if (dX < 0) {
// Swiping left
background.setBounds(itemView.getRight() + (int) dX, itemView.getTop(),
itemView.getRight(), itemView.getBottom());
} else {
background.setBounds(0, 0, 0, 0);
}
background.draw(c);
// Draw "Delete" text
String deleteText = "Delete";
float textWidth = textPaint.measureText(deleteText);
float textY = itemView.getTop() + (itemView.getHeight() + textPaint.getTextSize()) / 2f - 4f;
if (dX > 0) {
float textX = itemView.getLeft() + 32f;
c.drawText(deleteText, textX, textY, textPaint);
} else if (dX < 0) {
float textX = itemView.getRight() - textWidth - 32f;
c.drawText(deleteText, textX, textY, textPaint);
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
@@ -0,0 +1,131 @@
package com.example.todojava;
import android.graphics.Paint;
import android.graphics.drawable.GradientDrawable;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.example.todojava.data.Todo;
import com.example.todojava.databinding.ItemTodoBinding;
public class TodoAdapter extends ListAdapter<Todo, TodoAdapter.TodoViewHolder> {
public interface OnTodoClickListener {
void onTodoClick(Todo todo);
}
public interface OnTodoCheckedListener {
void onTodoChecked(Todo todo);
}
private OnTodoClickListener clickListener;
private OnTodoCheckedListener checkedListener;
public TodoAdapter() {
super(DIFF_CALLBACK);
}
public void setOnTodoClickListener(OnTodoClickListener listener) {
this.clickListener = listener;
}
public void setOnTodoCheckedListener(OnTodoCheckedListener listener) {
this.checkedListener = listener;
}
private static final DiffUtil.ItemCallback<Todo> DIFF_CALLBACK =
new DiffUtil.ItemCallback<Todo>() {
@Override
public boolean areItemsTheSame(@NonNull Todo oldItem, @NonNull Todo newItem) {
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(@NonNull Todo oldItem, @NonNull Todo newItem) {
return oldItem.getTitle().equals(newItem.getTitle())
&& oldItem.isCompleted() == newItem.isCompleted()
&& oldItem.getCategory().equals(newItem.getCategory())
&& oldItem.getPriority().equals(newItem.getPriority());
}
};
@NonNull
@Override
public TodoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemTodoBinding binding = ItemTodoBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
return new TodoViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull TodoViewHolder holder, int position) {
Todo todo = getItem(position);
holder.bind(todo);
}
class TodoViewHolder extends RecyclerView.ViewHolder {
private final ItemTodoBinding binding;
TodoViewHolder(ItemTodoBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
void bind(Todo todo) {
binding.textTitle.setText(todo.getTitle());
binding.chipCategory.setText(todo.getCategory());
// Handle completed state
binding.checkBox.setOnCheckedChangeListener(null);
binding.checkBox.setChecked(todo.isCompleted());
if (todo.isCompleted()) {
binding.textTitle.setPaintFlags(
binding.textTitle.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
binding.textTitle.setTextColor(
ContextCompat.getColor(itemView.getContext(), R.color.completed_text));
} else {
binding.textTitle.setPaintFlags(
binding.textTitle.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
binding.textTitle.setTextColor(
ContextCompat.getColor(itemView.getContext(), R.color.on_surface));
}
// Priority dot color
int priorityColor;
switch (todo.getPriority()) {
case "High":
priorityColor = ContextCompat.getColor(itemView.getContext(), R.color.priority_high);
break;
case "Medium":
priorityColor = ContextCompat.getColor(itemView.getContext(), R.color.priority_medium);
break;
default:
priorityColor = ContextCompat.getColor(itemView.getContext(), R.color.priority_low);
break;
}
GradientDrawable dot = (GradientDrawable) binding.priorityDot.getBackground().mutate();
dot.setColor(priorityColor);
// Click listener for editing
itemView.setOnClickListener(v -> {
if (clickListener != null) {
clickListener.onTodoClick(todo);
}
});
// Checkbox listener
binding.checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (checkedListener != null) {
checkedListener.onTodoChecked(todo);
}
});
}
}
}
@@ -0,0 +1,156 @@
package com.example.todojava;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import com.example.todojava.data.Todo;
import com.example.todojava.databinding.DialogTodoFormBinding;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class TodoFormDialogFragment extends DialogFragment {
private static final String ARG_TODO_ID = "todo_id";
private static final String ARG_TODO_TITLE = "todo_title";
private static final String ARG_TODO_CATEGORY = "todo_category";
private static final String ARG_TODO_PRIORITY = "todo_priority";
private static final String ARG_TODO_COMPLETED = "todo_completed";
private static final String ARG_TODO_CREATED_AT = "todo_created_at";
private DialogTodoFormBinding binding;
private TodoViewModel viewModel;
public static TodoFormDialogFragment newInstance() {
return new TodoFormDialogFragment();
}
public static TodoFormDialogFragment newInstance(Todo todo) {
TodoFormDialogFragment fragment = new TodoFormDialogFragment();
Bundle args = new Bundle();
args.putLong(ARG_TODO_ID, todo.getId());
args.putString(ARG_TODO_TITLE, todo.getTitle());
args.putString(ARG_TODO_CATEGORY, todo.getCategory());
args.putString(ARG_TODO_PRIORITY, todo.getPriority());
args.putBoolean(ARG_TODO_COMPLETED, todo.isCompleted());
args.putLong(ARG_TODO_CREATED_AT, todo.getCreatedAt());
fragment.setArguments(args);
return fragment;
}
private boolean isEditing() {
return getArguments() != null && getArguments().containsKey(ARG_TODO_ID);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
binding = DialogTodoFormBinding.inflate(LayoutInflater.from(getContext()));
viewModel = new ViewModelProvider(requireActivity()).get(TodoViewModel.class);
if (isEditing()) {
prefillForm();
}
String title = isEditing() ? getString(R.string.edit_todo) : getString(R.string.add_todo);
return new MaterialAlertDialogBuilder(requireContext())
.setTitle(title)
.setView(binding.getRoot())
.setPositiveButton(R.string.save, (dialog, which) -> saveTodo())
.setNegativeButton(R.string.cancel, null)
.create();
}
private void prefillForm() {
Bundle args = getArguments();
if (args == null) return;
binding.editTitle.setText(args.getString(ARG_TODO_TITLE, ""));
String category = args.getString(ARG_TODO_CATEGORY, "Personal");
switch (category) {
case "Personal":
binding.chipPersonal.setChecked(true);
break;
case "Work":
binding.chipWork.setChecked(true);
break;
case "Shopping":
binding.chipShopping.setChecked(true);
break;
case "Health":
binding.chipHealth.setChecked(true);
break;
case "Learning":
binding.chipLearning.setChecked(true);
break;
}
String priority = args.getString(ARG_TODO_PRIORITY, "Medium");
switch (priority) {
case "Low":
binding.chipLow.setChecked(true);
break;
case "Medium":
binding.chipMedium.setChecked(true);
break;
case "High":
binding.chipHigh.setChecked(true);
break;
}
}
private String getSelectedCategory() {
int checkedId = binding.categoryChipGroup.getCheckedChipId();
if (checkedId == R.id.chipPersonal) return "Personal";
if (checkedId == R.id.chipWork) return "Work";
if (checkedId == R.id.chipShopping) return "Shopping";
if (checkedId == R.id.chipHealth) return "Health";
if (checkedId == R.id.chipLearning) return "Learning";
return "Personal";
}
private String getSelectedPriority() {
int checkedId = binding.priorityChipGroup.getCheckedChipId();
if (checkedId == R.id.chipLow) return "Low";
if (checkedId == R.id.chipMedium) return "Medium";
if (checkedId == R.id.chipHigh) return "High";
return "Medium";
}
private void saveTodo() {
String title = binding.editTitle.getText() != null
? binding.editTitle.getText().toString().trim() : "";
if (title.isEmpty()) {
return;
}
String category = getSelectedCategory();
String priority = getSelectedPriority();
if (isEditing()) {
Bundle args = getArguments();
Todo todo = new Todo(title, category, priority);
todo.setId(args.getLong(ARG_TODO_ID));
todo.setCompleted(args.getBoolean(ARG_TODO_COMPLETED, false));
todo.setCreatedAt(args.getLong(ARG_TODO_CREATED_AT, System.currentTimeMillis()));
viewModel.update(todo);
} else {
Todo todo = new Todo(title, category, priority);
viewModel.insert(todo);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
@@ -0,0 +1,123 @@
package com.example.todojava;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import com.example.todojava.data.Todo;
import com.example.todojava.data.TodoDao;
import com.example.todojava.data.TodoDatabase;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TodoViewModel extends AndroidViewModel {
public enum Filter {
ALL, ACTIVE, DONE
}
private final TodoDao todoDao;
private final ExecutorService executor;
private final LiveData<List<Todo>> allTodos;
private final LiveData<List<Todo>> activeTodos;
private final LiveData<List<Todo>> completedTodos;
private final LiveData<Integer> allCount;
private final LiveData<Integer> activeCount;
private final LiveData<Integer> completedCount;
private final MutableLiveData<Filter> currentFilter = new MutableLiveData<>(Filter.ALL);
private final MediatorLiveData<List<Todo>> filteredTodos = new MediatorLiveData<>();
public TodoViewModel(@NonNull Application application) {
super(application);
TodoDatabase db = TodoDatabase.getInstance(application);
todoDao = db.todoDao();
executor = Executors.newSingleThreadExecutor();
allTodos = todoDao.getAllTodos();
activeTodos = todoDao.getActiveTodos();
completedTodos = todoDao.getCompletedTodos();
allCount = todoDao.getAllCount();
activeCount = todoDao.getActiveCount();
completedCount = todoDao.getCompletedCount();
filteredTodos.addSource(allTodos, todos -> {
if (currentFilter.getValue() == Filter.ALL) {
filteredTodos.setValue(todos);
}
});
filteredTodos.addSource(activeTodos, todos -> {
if (currentFilter.getValue() == Filter.ACTIVE) {
filteredTodos.setValue(todos);
}
});
filteredTodos.addSource(completedTodos, todos -> {
if (currentFilter.getValue() == Filter.DONE) {
filteredTodos.setValue(todos);
}
});
filteredTodos.addSource(currentFilter, filter -> {
switch (filter) {
case ALL:
filteredTodos.setValue(allTodos.getValue());
break;
case ACTIVE:
filteredTodos.setValue(activeTodos.getValue());
break;
case DONE:
filteredTodos.setValue(completedTodos.getValue());
break;
}
});
}
public LiveData<List<Todo>> getFilteredTodos() {
return filteredTodos;
}
public LiveData<Integer> getAllCount() {
return allCount;
}
public LiveData<Integer> getActiveCount() {
return activeCount;
}
public LiveData<Integer> getCompletedCount() {
return completedCount;
}
public MutableLiveData<Filter> getCurrentFilter() {
return currentFilter;
}
public void setFilter(Filter filter) {
currentFilter.setValue(filter);
}
public void insert(Todo todo) {
executor.execute(() -> todoDao.insert(todo));
}
public void update(Todo todo) {
executor.execute(() -> todoDao.update(todo));
}
public void delete(Todo todo) {
executor.execute(() -> todoDao.delete(todo));
}
public void toggleCompleted(Todo todo) {
todo.setCompleted(!todo.isCompleted());
executor.execute(() -> todoDao.update(todo));
}
}
@@ -0,0 +1,88 @@
package com.example.todojava.data;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
@Entity(tableName = "todos")
public class Todo {
@PrimaryKey(autoGenerate = true)
public long id;
@ColumnInfo(name = "title")
public String title;
@ColumnInfo(name = "completed")
public boolean completed;
@ColumnInfo(name = "category")
public String category;
@ColumnInfo(name = "priority")
public String priority;
@ColumnInfo(name = "created_at")
public long createdAt;
public Todo() {
}
@Ignore
public Todo(String title, String category, String priority) {
this.title = title;
this.completed = false;
this.category = category;
this.priority = priority;
this.createdAt = System.currentTimeMillis();
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getPriority() {
return priority;
}
public void setPriority(String priority) {
this.priority = priority;
}
public long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(long createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,44 @@
package com.example.todojava.data;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface TodoDao {
@Query("SELECT * FROM todos ORDER BY created_at DESC")
LiveData<List<Todo>> getAllTodos();
@Query("SELECT * FROM todos WHERE completed = 0 ORDER BY created_at DESC")
LiveData<List<Todo>> getActiveTodos();
@Query("SELECT * FROM todos WHERE completed = 1 ORDER BY created_at DESC")
LiveData<List<Todo>> getCompletedTodos();
@Query("SELECT COUNT(*) FROM todos")
LiveData<Integer> getAllCount();
@Query("SELECT COUNT(*) FROM todos WHERE completed = 0")
LiveData<Integer> getActiveCount();
@Query("SELECT COUNT(*) FROM todos WHERE completed = 1")
LiveData<Integer> getCompletedCount();
@Insert
void insert(Todo todo);
@Update
void update(Todo todo);
@Delete
void delete(Todo todo);
@Query("SELECT * FROM todos WHERE id = :id")
Todo getTodoById(long id);
}
@@ -0,0 +1,30 @@
package com.example.todojava.data;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
@Database(entities = {Todo.class}, version = 1, exportSchema = false)
public abstract class TodoDatabase extends RoomDatabase {
public abstract TodoDao todoDao();
private static volatile TodoDatabase INSTANCE;
public static TodoDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (TodoDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
TodoDatabase.class,
"todo_database"
).build();
}
}
}
return INSTANCE;
}
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#43A047" />
<size
android:width="12dp"
android:height="12dp" />
</shape>
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/primary"
app:title="@string/toolbar_title"
app:titleTextColor="@color/on_primary" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.chip.ChipGroup
android:id="@+id/filterChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
app:singleSelection="true"
app:selectionRequired="true"
app:chipSpacingHorizontal="8dp">
<com.google.android.material.chip.Chip
android:id="@+id/chipAll"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filter_all"
android:checked="true" />
<com.google.android.material.chip.Chip
android:id="@+id/chipActive"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filter_active" />
<com.google.android.material.chip.Chip
android:id="@+id/chipDone"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filter_done" />
</com.google.android.material.chip.ChipGroup>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp"
tools:listitem="@layout/item_todo" />
<include
android:id="@+id/emptyState"
layout="@layout/empty_state"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/add_todo"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@color/on_primary"
app:backgroundTint="@color/primary" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/titleInputLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences"
android:maxLines="2" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/category_label"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/on_surface"
android:layout_marginTop="20dp"
android:layout_marginBottom="8dp" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/categoryChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:singleSelection="true"
app:selectionRequired="true"
app:chipSpacingHorizontal="8dp">
<com.google.android.material.chip.Chip
android:id="@+id/chipPersonal"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/category_personal"
android:checked="true" />
<com.google.android.material.chip.Chip
android:id="@+id/chipWork"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/category_work" />
<com.google.android.material.chip.Chip
android:id="@+id/chipShopping"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/category_shopping" />
<com.google.android.material.chip.Chip
android:id="@+id/chipHealth"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/category_health" />
<com.google.android.material.chip.Chip
android:id="@+id/chipLearning"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/category_learning" />
</com.google.android.material.chip.ChipGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/priority_label"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/on_surface"
android:layout_marginTop="20dp"
android:layout_marginBottom="8dp" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/priorityChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:singleSelection="true"
app:selectionRequired="true"
app:chipSpacingHorizontal="8dp">
<com.google.android.material.chip.Chip
android:id="@+id/chipLow"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/priority_low" />
<com.google.android.material.chip.Chip
android:id="@+id/chipMedium"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/priority_medium"
android:checked="true" />
<com.google.android.material.chip.Chip
android:id="@+id/chipHigh"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/priority_high" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</ScrollView>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@android:drawable/ic_menu_agenda"
android:alpha="0.4"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/emptyTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/empty_state_title"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/on_surface"
android:layout_marginTop="16dp" />
<TextView
android:id="@+id/emptySubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/empty_state_subtitle"
android:textSize="14sp"
android:textColor="@color/on_surface_variant"
android:layout_marginTop="8dp"
android:gravity="center" />
</LinearLayout>
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:layout_marginVertical="4dp"
app:cardElevation="2dp"
app:cardCornerRadius="12dp"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="12dp">
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:minHeight="0dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:id="@+id/textTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/on_surface"
tools:text="Buy groceries" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="4dp">
<com.google.android.material.chip.Chip
android:id="@+id/chipCategory"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
app:chipMinHeight="24dp"
app:chipMinTouchTargetSize="0dp"
tools:text="Shopping" />
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/priorityDot"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginStart="8dp"
android:background="@drawable/priority_dot" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#6200EE</color>
<color name="primary_variant">#3700B3</color>
<color name="on_primary">#FFFFFF</color>
<color name="secondary">#03DAC6</color>
<color name="on_secondary">#000000</color>
<color name="background">#FAFAFA</color>
<color name="surface">#FFFFFF</color>
<color name="on_surface">#1C1B1F</color>
<color name="on_surface_variant">#49454F</color>
<color name="priority_low">#43A047</color>
<color name="priority_medium">#FB8C00</color>
<color name="priority_high">#E53935</color>
<color name="completed_text">#9E9E9E</color>
<color name="divider">#E0E0E0</color>
</resources>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Todo Java</string>
<string name="toolbar_title">Todos \u2013 Java Android</string>
<string name="filter_all">All</string>
<string name="filter_active">Active</string>
<string name="filter_done">Done</string>
<string name="add_todo">Add Todo</string>
<string name="edit_todo">Edit Todo</string>
<string name="title_hint">What needs to be done?</string>
<string name="category_label">Category</string>
<string name="priority_label">Priority</string>
<string name="save">Save</string>
<string name="cancel">Cancel</string>
<string name="empty_state_title">No todos yet</string>
<string name="empty_state_subtitle">Tap the + button to add your first todo</string>
<string name="empty_active_title">All done!</string>
<string name="empty_active_subtitle">No active todos remaining</string>
<string name="empty_done_title">Nothing completed</string>
<string name="empty_done_subtitle">Complete a todo to see it here</string>
<string name="category_personal">Personal</string>
<string name="category_work">Work</string>
<string name="category_shopping">Shopping</string>
<string name="category_health">Health</string>
<string name="category_learning">Learning</string>
<string name="priority_low">Low</string>
<string name="priority_medium">Medium</string>
<string name="priority_high">High</string>
<string name="delete">Delete</string>
<string name="todo_deleted">Todo deleted</string>
<string name="undo">Undo</string>
</resources>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.TodoJava" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary_variant</item>
<item name="colorOnPrimary">@color/on_primary</item>
<item name="colorSecondary">@color/secondary</item>
<item name="colorOnSecondary">@color/on_secondary</item>
<item name="android:colorBackground">@color/background</item>
<item name="colorSurface">@color/surface</item>
<item name="colorOnSurface">@color/on_surface</item>
<item name="colorOnSurfaceVariant">@color/on_surface_variant</item>
</style>
</resources>
+61
View File
@@ -0,0 +1,61 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
}
android {
namespace = "com.example.todokotlin"
compileSdk = 35
defaultConfig {
applicationId = "com.example.todokotlin"
minSdk = 26
targetSdk = 35
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
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="Todo Kotlin"
android:supportsRtl="true"
android:theme="@style/Theme.TodoKotlin">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.TodoKotlin">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,22 @@
package com.example.todokotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.todokotlin.ui.TodoApp
import com.example.todokotlin.ui.theme.TodoKotlinTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TodoKotlinTheme {
val viewModel: TodoViewModel = viewModel()
TodoApp(viewModel = viewModel)
}
}
}
}
@@ -0,0 +1,84 @@
package com.example.todokotlin
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.todokotlin.data.Todo
import com.example.todokotlin.data.TodoDatabase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
enum class TodoFilter { All, Active, Done }
data class TodoUiState(
val todos: List<Todo> = emptyList(),
val filter: TodoFilter = TodoFilter.All,
val allCount: Int = 0,
val activeCount: Int = 0,
val doneCount: Int = 0
)
class TodoViewModel(application: Application) : AndroidViewModel(application) {
private val dao = TodoDatabase.getDatabase(application).todoDao()
private val _filter = MutableStateFlow(TodoFilter.All)
val uiState: StateFlow<TodoUiState> = combine(
dao.getAllTodos(),
_filter
) { todos, filter ->
val filtered = when (filter) {
TodoFilter.All -> todos
TodoFilter.Active -> todos.filter { !it.completed }
TodoFilter.Done -> todos.filter { it.completed }
}
TodoUiState(
todos = filtered,
filter = filter,
allCount = todos.size,
activeCount = todos.count { !it.completed },
doneCount = todos.count { it.completed }
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = TodoUiState()
)
fun setFilter(filter: TodoFilter) {
_filter.value = filter
}
fun addTodo(title: String, category: String, priority: String) {
viewModelScope.launch {
dao.insert(
Todo(
title = title,
category = category,
priority = priority
)
)
}
}
fun updateTodo(todo: Todo) {
viewModelScope.launch {
dao.update(todo)
}
}
fun toggleCompleted(todo: Todo) {
viewModelScope.launch {
dao.update(todo.copy(completed = !todo.completed))
}
}
fun deleteTodo(todo: Todo) {
viewModelScope.launch {
dao.delete(todo)
}
}
}
@@ -0,0 +1,15 @@
package com.example.todokotlin.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "todos")
data class Todo(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val completed: Boolean = false,
val category: String = "Personal",
val priority: String = "Medium",
val createdAt: Long = System.currentTimeMillis()
)
@@ -0,0 +1,26 @@
package com.example.todokotlin.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface TodoDao {
@Query("SELECT * FROM todos ORDER BY createdAt DESC")
fun getAllTodos(): Flow<List<Todo>>
@Insert
suspend fun insert(todo: Todo)
@Update
suspend fun update(todo: Todo)
@Delete
suspend fun delete(todo: Todo)
@Query("DELETE FROM todos WHERE id = :id")
suspend fun deleteById(id: Long)
}
@@ -0,0 +1,28 @@
package com.example.todokotlin.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Todo::class], version = 1, exportSchema = false)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
companion object {
@Volatile
private var INSTANCE: TodoDatabase? = null
fun getDatabase(context: Context): TodoDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TodoDatabase::class.java,
"todo_database"
).build()
INSTANCE = instance
instance
}
}
}
}
@@ -0,0 +1,42 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
val categories = listOf("Personal", "Work", "Shopping", "Health", "Learning")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CategoryPicker(
selected: String,
onSelect: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = "Category",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
categories.forEach { category ->
FilterChip(
selected = selected == category,
onClick = { onSelect(category) },
label = { Text(category) }
)
}
}
}
}
@@ -0,0 +1,39 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun EmptyState(modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "No todos yet",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Tap the + button to add your first todo",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
textAlign = TextAlign.Center
)
}
}
@@ -0,0 +1,45 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.todokotlin.TodoFilter
@Composable
fun FilterBar(
currentFilter: TodoFilter,
allCount: Int,
activeCount: Int,
doneCount: Int,
onFilterChange: (TodoFilter) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = currentFilter == TodoFilter.All,
onClick = { onFilterChange(TodoFilter.All) },
label = { Text("All ($allCount)") }
)
FilterChip(
selected = currentFilter == TodoFilter.Active,
onClick = { onFilterChange(TodoFilter.Active) },
label = { Text("Active ($activeCount)") }
)
FilterChip(
selected = currentFilter == TodoFilter.Done,
onClick = { onFilterChange(TodoFilter.Done) },
label = { Text("Done ($doneCount)") }
)
}
}
@@ -0,0 +1,56 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
data class PriorityOption(val name: String, val color: Color)
val priorities = listOf(
PriorityOption("Low", Color(0xFF43A047)),
PriorityOption("Medium", Color(0xFFFB8C00)),
PriorityOption("High", Color(0xFFE53935))
)
fun priorityColor(priority: String): Color {
return priorities.find { it.name == priority }?.color ?: Color(0xFFFB8C00)
}
@Composable
fun PriorityPicker(
selected: String,
onSelect: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = "Priority",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
priorities.forEach { priority ->
FilterChip(
selected = selected == priority.name,
onClick = { onSelect(priority.name) },
label = { Text(priority.name) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = priority.color.copy(alpha = 0.2f),
selectedLabelColor = priority.color
)
)
}
}
}
}
@@ -0,0 +1,106 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.todokotlin.TodoViewModel
import com.example.todokotlin.data.Todo
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TodoApp(viewModel: TodoViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var showAddDialog by remember { mutableStateOf(false) }
var editingTodo by remember { mutableStateOf<Todo?>(null) }
Scaffold(
topBar = {
TopAppBar(
title = {
Text("Todos \u2013 Kotlin Android")
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary
)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { showAddDialog = true },
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add Todo",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
FilterBar(
currentFilter = uiState.filter,
allCount = uiState.allCount,
activeCount = uiState.activeCount,
doneCount = uiState.doneCount,
onFilterChange = { viewModel.setFilter(it) }
)
if (uiState.todos.isEmpty()) {
EmptyState()
} else {
TodoList(
todos = uiState.todos,
onToggle = { viewModel.toggleCompleted(it) },
onTap = { editingTodo = it },
onDelete = { viewModel.deleteTodo(it) }
)
}
}
}
if (showAddDialog) {
TodoFormDialog(
onDismiss = { showAddDialog = false },
onSave = { title, category, priority ->
viewModel.addTodo(title, category, priority)
showAddDialog = false
}
)
}
editingTodo?.let { todo ->
TodoFormDialog(
todo = todo,
onDismiss = { editingTodo = null },
onSave = { title, category, priority ->
viewModel.updateTodo(
todo.copy(
title = title,
category = category,
priority = priority
)
)
editingTodo = null
}
)
}
}
@@ -0,0 +1,75 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.todokotlin.data.Todo
@Composable
fun TodoFormDialog(
todo: Todo? = null,
onDismiss: () -> Unit,
onSave: (title: String, category: String, priority: String) -> Unit
) {
var title by remember { mutableStateOf(todo?.title ?: "") }
var category by remember { mutableStateOf(todo?.category ?: "Personal") }
var priority by remember { mutableStateOf(todo?.priority ?: "Medium") }
val isEditing = todo != null
val dialogTitle = if (isEditing) "Edit Todo" else "Add Todo"
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(dialogTitle) },
text = {
Column {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Title") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
CategoryPicker(
selected = category,
onSelect = { category = it }
)
Spacer(modifier = Modifier.height(12.dp))
PriorityPicker(
selected = priority,
onSelect = { priority = it }
)
}
},
confirmButton = {
TextButton(
onClick = {
if (title.isNotBlank()) {
onSave(title.trim(), category, priority)
}
},
enabled = title.isNotBlank()
) {
Text("Save")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
@@ -0,0 +1,102 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.example.todokotlin.data.Todo
@Composable
fun TodoItem(
todo: Todo,
onToggle: () -> Unit,
onTap: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.clickable { onTap() },
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.completed,
onCheckedChange = { onToggle() }
)
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = todo.title,
style = MaterialTheme.typography.bodyLarge,
textDecoration = if (todo.completed) TextDecoration.LineThrough else TextDecoration.None,
color = if (todo.completed)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
SuggestionChip(
onClick = { },
label = {
Text(
text = todo.category,
style = MaterialTheme.typography.labelSmall
)
}
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(priorityColor(todo.priority))
)
Text(
text = todo.priority,
style = MaterialTheme.typography.labelSmall,
color = priorityColor(todo.priority)
)
}
}
}
}
}
}
@@ -0,0 +1,85 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.example.todokotlin.data.Todo
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TodoList(
todos: List<Todo>,
onToggle: (Todo) -> Unit,
onTap: (Todo) -> Unit,
onDelete: (Todo) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = todos,
key = { it.id }
) { todo ->
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
onDelete(todo)
true
} else {
false
}
}
)
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFE53935))
.padding(horizontal = 20.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White
)
}
},
enableDismissFromStartToEnd = false,
enableDismissFromEndToStart = true
) {
TodoItem(
todo = todo,
onToggle = { onToggle(todo) },
onTap = { onTap(todo) }
)
}
}
}
}
@@ -0,0 +1,29 @@
package com.example.todokotlin.ui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
primaryContainer = Color(0xFFE8DEF8),
onPrimaryContainer = Color(0xFF21005D),
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
background = Color(0xFFFFFBFE),
onBackground = Color(0xFF1C1B1F),
surface = Color(0xFFFFFBFE),
onSurface = Color(0xFF1C1B1F),
surfaceVariant = Color(0xFFE7E0EC),
onSurfaceVariant = Color(0xFF49454F)
)
@Composable
fun TodoKotlinTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = LightColorScheme,
content = content
)
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.TodoKotlin" parent="android:Theme.Material.Light.NoActionBar" />
</resources>