Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
![](media/math_alarm_github.png)
# Math Alarm :alarm_clock:
# Math Alarm

![Android Build](https://github.com/t-regbs/MathAlarm/workflows/Android%20Build/badge.svg) ![My twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Ftimiaregbs) ![Shield](https://img.shields.io/badge/contributions-welcome-brightgreen) [![Made in Nigeria](https://img.shields.io/badge/made%20in-nigeria-008751.svg?style=flat-square)](https://github.com/acekyd/made-in-nigeria)

A **Kotlin Multiplatform** alarm app for Android and iOS where you solve math problems of varying difficulty to dismiss the alarm. Built with Clean Architecture and shared Kotlin ViewModels, Android Compose, and a native SwiftUI development UI.
Math Alarm is an Android and iOS app that asks you to solve math problems to dismiss an alarm. It shares Kotlin alarm logic and ViewModels, with Compose screens on Android and SwiftUI screens on iOS.

<a href='https://play.google.com/store/apps/details?id=com.timilehinaregbesola.mathalarm'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' width="280"/></a>

## Architecture

The project follows **Clean Architecture** with the **MVVM** pattern:
The project has four modules:

- **`:androidApp`** - All Compose screens/navigation/theme/localization/resources, application entry points and Android platform wiring
- **`:shared`** - UI-free feature ViewModels, application coordination, data and platform service adapters
Expand All @@ -18,30 +18,30 @@ The project follows **Clean Architecture** with the **MVVM** pattern:

The native migration uses shared Kotlin ViewModels and alarm logic with Compose on Android and SwiftUI on iOS. All native parity screens are implemented. Permanent XCTest/UI targets, native client flows, renderer/resource gates and archive checks are documented in [architecture and build](docs/native-architecture.md) and [testing](docs/testing.md). [Migration progress](docs/native-ui-migration-progress.md) separates implementation evidence from physical AlarmKit, acoustic, minimum-runtime and release acceptance gates. This development build is not yet release ready.

## Technologies Used
## Libraries

### Kotlin Multiplatform
* [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) - Share code between Android and iOS
* [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) - Android declarative UI renderer

### UI & Navigation
* [Material 3](https://m3.material.io/) - Modern Material Design components
### UI and navigation
* [Material 3](https://m3.material.io/) - Material Design components
* [Navigation 3](https://developer.android.com/guide/navigation) - Android Compose navigation
* [Compottie](https://github.com/alexzhirkevich/compottie) - Android Compose Lottie animations

### Data & Storage
### Data and storage
* [Room KMP](https://developer.android.com/kotlin/multiplatform/room) - Multiplatform database with SQLite
* [Multiplatform Settings](https://github.com/russhwolf/multiplatform-settings) - Key-value storage across platforms
* [Kotlinx Serialization](https://github.com/Kotlin/kotlinx.serialization) - JSON serialization

### Dependency Injection
* [Koin](https://insert-koin.io/) - Lightweight dependency injection framework for KMP
### Dependency injection
* [Koin](https://insert-koin.io/) - Dependency injection for KMP

### Async & Reactive
### Asynchronous code
* [Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) - Asynchronous programming
* [Kotlinx DateTime](https://github.com/Kotlin/kotlinx-datetime) - Multiplatform date/time library

### Logging & Analytics (Android)
### Logging and analytics on Android
* [Kermit](https://github.com/touchlab/Kermit) - Multiplatform logging library
* [Firebase Analytics](https://firebase.google.com/docs/analytics) - App analytics
* [Firebase Crashlytics](https://firebase.google.com/docs/crashlytics) - Crash reporting
Expand Down Expand Up @@ -82,9 +82,9 @@ xcodebuild -project iosApp/iosApp.xcodeproj -scheme MathAlarmNative -configurati
The PR workflow runs permanent native tests, client flows, nine-locale screenshots and archive packaging on the GitHub-hosted `xcode-27` Apple Silicon image, explicitly selecting Xcode 27.0 (27A266a). Use iPad simulators for layout/navigation checks and a physical iPhone for AlarmKit delivery and audio validation. Signed device builds require your Apple development team and provisioning profile; a simulator build does not establish alarm delivery reliability.

## Contribution
All contributions are welcome. Simply make a PR!
Open a pull request with a description of the change and how you tested it.

## LICENSE
## License
```
MIT License

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import cafe.adriel.lyricist.LyricistStrings
val EnMathAlarmStrings = Strings(
selectAlarmPrompt = "Select an alarm or create a new one",
closeAlarmEditor = "Close this editor?",
closeAlarmEditorMessage = "Any unsaved changes to this alarm will be discarded.",
closeAlarmEditorMessage = "You will lose unsaved changes to this alarm.",
discardChanges = "Discard changes",

mathDifficultyNames = listOf("Easy", "Medium", "Hard", "Custom"),
Expand All @@ -29,18 +29,18 @@ val EnMathAlarmStrings = Strings(
chooseRange = "Choose range",
checkAnswer = "Check answer",
nextFeature = "Next",
snoozeAnnouncementTitle = "Snooze on your terms",
snoozeAnnouncementTitle = "Snooze limits and duration",
snoozeAnnouncementDescription = "Set a snooze limit, choose a duration from 1–30 minutes, or turn snooze off. New alarms start with 3 snoozes of 5 minutes. Existing alarms keep their snooze limits.",
snoozeAnnouncementInstructions = "Use the Snooze checkbox for a quick on/off toggle. Tap Edit beside it to change the limit and duration.",
whatsNew = "What’s new",
latestFeatures = "Explore the latest features",
snoozeAnnouncementInstructions = "Turn Snooze on or off with the checkbox. Tap Edit beside it to change the limit and duration.",
whatsNew = "What's new",
latestFeatures = "New alarm settings",
gotIt = "Got it",
tryFeature = "Try it",
challengeAnnouncementTitle = "More ways to wake up",
challengeAnnouncementTitle = "Custom math challenges",
challengeAnnouncementInstructions = "Add or edit an alarm, then tap Edit next to Math challenge.",
exampleChallenge = "Example challenge",
exampleDifficultyMix = { easy, medium -> "Easy: $easy · Medium: $medium" },
challengeAnnouncementDescription = { maximum -> "Solve up to $maximum questions. Mix difficulties or build a custom challenge that works for you." },
challengeAnnouncementDescription = { maximum -> "Solve up to $maximum questions. Mix difficulties or create a custom challenge." },
questionCount = ::questionCountEn,
challengeSummary = { difficulty, count -> "$difficulty · ${questionCountEn(count)}" },
mixedQuestionTotal = { count, maximum -> "${questionCountEn(count)} total · Maximum $maximum" },
Expand All @@ -49,27 +49,27 @@ val EnMathAlarmStrings = Strings(
difficultyQuestionLabel = { difficulty -> "Questions: $difficulty" },
decreaseQuestionCount = { label -> "Decrease count: $label" },
increaseQuestionCount = { label -> "Increase count: $label" },
alarmScheduleFailed = "Couldn’t schedule alarm. Save it again.",
alarmSaveFailed = "Couldn’t save alarm. Try again.",
alarmUpdateFailed = "Couldn’t update alarm. Try again.",
preferenceUpdateFailed = "Couldn’t update settings. Try again.",
alarmScheduleFailed = "Couldn't schedule alarm. Save it again.",
alarmSaveFailed = "Couldn't save alarm. Try again.",
alarmUpdateFailed = "Couldn't update alarm. Try again.",
preferenceUpdateFailed = "Couldn't update settings. Try again.",
retry = "Retry",
alarmDismissFailed = "Couldn’t dismiss alarm. Try again.",
alarmSnoozeFailed = "Couldn’t snooze alarm. Try again.",
alarmDismissFailed = "Couldn't dismiss alarm. Try again.",
alarmSnoozeFailed = "Couldn't snooze alarm. Try again.",
incorrectAnswer = "Incorrect answer. Try again.",
multipleDays = "Multiple Days",
multipleDays = "Multiple days",
greeting = { hour ->
when {
hour < 12 -> "Good Morning"
hour < 12 -> "Good morning"
hour in 12..17 -> "Afternoon"
else -> "Good Evening"
else -> "Good evening"
}
},
expand = "Expand",
delete = "Delete",
edit = "Edit",
dateLocale = "en",
skipAnnouncementDescription = "Take a break from your next alarm without changing the rest of your schedule.",
skipAnnouncementDescription = "Skip the next alarm time and keep the rest of your schedule.",
skipAnnouncementInstructions = "Expand an enabled alarm and tap Skip next. Available for weekly alarms and alarms with multiple selected days.",
skipAnnouncementUndo = "Only the next occurrence is skipped; other selected days stay scheduled. That weekday returns the following week for repeating alarms. Non-repeating alarms keep only their remaining dates. Tap Undo before the skipped time to bring it back. Snoozes and other alarms stay scheduled.",
viewAlarms = "View alarms",
Expand All @@ -79,7 +79,7 @@ val EnMathAlarmStrings = Strings(
undo = "Undo",
undoSkip = "Undo skip",
collapse = "Collapse",
noUpcomingAlarms = "No Upcoming Alarms",
noUpcomingAlarms = "No upcoming alarms",
nextAlarmText = "Next alarm:",
nextOccurrenceOn = { date -> "Next: $date" },
nextAlarmIn = { hours, minutes ->
Expand All @@ -89,21 +89,21 @@ val EnMathAlarmStrings = Strings(
if (minutes > 0) "$minutes ${if (minutes == 1) "minute" else "minutes"}" else null,
).joinToString(" ")
},
alarmSet = "Alarm Set for",
alarmSet = "Alarm set for",
alarmPermissionDialogConfirm = "Confirm",
alarmPermissionDialogCancel = "Cancel",
alarmPermissionDialogText = "This app requires permission to set alarms.",
alarmPermissionDialogTitle = "Permission Required",
alarmPermissionDialogTitle = "Permission required",
clearAlarmDialogConfirm = "Confirm",
clearAlarmDialogCancel = "Cancel",
clearAlarmDialogText = "Are you sure you want to clear the alarms?",
clearAlarmDialogTitle = "Clear Alarms",
emptyAlarmList = "Empty Alarm List",
nothingToSee = "Nothing to see here",
clearAlarmDialogText = "Delete all alarms?",
clearAlarmDialogTitle = "Clear alarms",
emptyAlarmList = "No alarms",
nothingToSee = "Add an alarm to get started.",
alarms = "Alarms",
deleteAll = "Delete All",
deleteAll = "Delete all",
settings = "Settings",
toneUnavailable = "Tone Unavailable",
toneUnavailable = "Tone unavailable",
clear = "Clear",
maximumSnoozes = "Maximum snoozes",
unlimitedSnoozes = "Unlimited",
Expand All @@ -119,50 +119,50 @@ val EnMathAlarmStrings = Strings(
snoozeSettingsSummary = { minutes, limit -> "$minutes min · $limit" },
snooze = "Snooze",
enter = "Enter",
noRingtonePicker = "No Ringtone Picker",
alarmTitle = "Alarm Title",
noRingtonePicker = "No ringtone picker",
alarmTitle = "Alarm title",
goodDay = "Good day",
defaultAlarmTone = "Alarm Tone (Default)",
defaultAlarmTone = "Default alarm tone",
disabledNotificationMessage = "Notifications are disabled for this app.",
disabledNotificationMessageExtended = "Notifications of this application are disabled. Please go to your device settings and enable them.",
disabledNotificationMessageExtended = "Turn on notifications for Math Alarm in your device settings.",
ok = "OK",
notificationPermissionDialogMessage = "This app requires permission to show notifications.",
repeatWeekly = "Repeat Weekly",
repeatWeekly = "Repeat weekly",
vibrate = "Vibrate",
testAlarm = "Test Alarm",
testAlarm = "Test alarm",
save = "Save",
easyMath = "Easy Math",
mediumMath = "Medium Math",
hardMath = "Hard Math",
selectHour = "Select Hour",
easyMath = "Easy math",
mediumMath = "Medium math",
hardMath = "Hard math",
selectHour = "Select hour",
cancel = "Cancel",
input = "Input",
picker = "Picker",
system = "System",
dark = "Dark",
light = "Light",
appSettings = "App Settings",
appSettings = "App settings",
back = "Back",
colorTheme = "Color Theme",
alarmOrder = "Alarm Order",
alarmOrderDescription = "Choose whether alarms appear by when they were created or by the time they go off.",
creationOrder = "Creation Order",
colorTheme = "Color theme",
alarmOrder = "Alarm order",
alarmOrderDescription = "Sort alarms by creation time or alarm time.",
creationOrder = "Creation order",
timeOrder = "Time",
help = "Help",
sendFeedback = "Send Feedback",
sendFeedback = "Send feedback",
sendFeedbackMessage = "Send feedback to the developer",
supportEmail = "[email protected]",
emailChooserTitle = "Send Email",
defaultSendText = "Default Alarm",
emailChooserTitle = "Send email",
defaultSendText = "Default alarm",
shareMathAlarm = "Share Math Alarm",
shareWithOthers = "Check out this cool alarm app",
shareWithOthers = "Try Math Alarm. Solve math problems to dismiss your alarm.",
share = "Share",
taskAlarmPermissionDialogText = "In order for alarms to work, Math alarm needs the permission to set alarms. If this permission is not granted, the existing alarms will not work as well.",
taskAlarmPermissionDialogText = "Allow Math Alarm to set alarms. Without this permission, new and existing alarms cannot ring.",
taskAlarmPermissionDialogCancel = "Not now",
taskAlarmPermissionDialogConfirm = "Grant",
notification = "Notification",
alert = "Ringtone",
grantPermission = "Grant Permission",
grantPermission = "Grant permission",
alarmSound = "Alarm sound",
done = "Done",
daybreakDescription = "Soft mallets · gentle",
Expand All @@ -175,7 +175,7 @@ val EnMathAlarmStrings = Strings(
previewSound = "Preview",
stopSoundPreview = "Stop preview",
permissionsExternalStorageText = { tone ->
"It seems that we cannot play $tone, probably because a permission is required. If you want, you can grant the permission. Alternatively, select a different sound. This decision can be changed in System Settings."
"Cannot play $tone. Allow access to the sound or choose another one. You can change permissions in your device settings."
}
)

Expand Down
38 changes: 19 additions & 19 deletions androidApp/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<resources>
<string name="time_picker_title">Set Alarm Time:</string>
<string name="time_picker_title">Set alarm time</string>

<string name="settings_time_title">Set Alarm Time</string>
<string name="settings_time_title">Set alarm time</string>

<string name="settings_day_title">Set On</string>
<string name="settings_day_title">Alarm days</string>

<string name="settings_repeat_title">Repeat</string>
<string name="settings_repeat_text">Repeat Weekly</string>
<string name="settings_setting_title">Alarm Settings</string>
<string name="settings_repeat_text">Repeat weekly</string>
<string name="settings_setting_title">Alarm settings</string>
<string name="settings_vibrate_text">Vibrate</string>
<string name="settings_tone_text">Alarm Tone</string>
<string name="settings_tone_text">Alarm tone</string>
<string name="settings_snooze_text">Snooze (0 = OFF)</string>
<string name="minutes">minute(s)</string>
<string name="settings_math_difficulty_text">Math Difficulty</string>
<string name="settings_test">Test Alarm</string>
<string name="settings_math_difficulty_text">Math difficulty</string>
<string name="settings_test">Test alarm</string>

<string name="menu_add_alarm">Add Alarm</string>
<string name="menu_add_alarm">Add alarm</string>
<string name="menu_settings_done">Set</string>
<string name="menu_settings_delete">Delete</string>

Expand Down Expand Up @@ -44,11 +44,11 @@

<string name="snooze_off">No snooze allowed</string>

<string name="empty">Empty!</string>
<string name="empty">No alarms</string>
<string name="add_alarm">\nTap to\n add an\n alarm</string>

<string name="android_cd">Android</string>
<string name="bubble_cd">Speech Bubble</string>
<string name="bubble_cd">Speech bubble</string>
<string name="message_cd">Message</string>

<string name="alarm_duplicate_toast_text">Duplicate alarm detected</string>
Expand All @@ -62,22 +62,22 @@
<string name="alarm_days">days,</string>
<string name="tone_not_available">Alarm tone not available.</string>
<string name="ok">OK</string>
<string name="clear_alarms">Clear Alarms</string>
<string name="clear_alarms">Clear alarms</string>
<string name="alarm_notification_channel_id">alarm_channel</string>
<string name="notification_title">Math Alarm</string>
<string name="math_alarm">Math Alarm!!</string>
<string name="math_alarm">Math Alarm</string>
<string name="details_no_ringtone_picker">No ringtone picker</string>
<string name="default_alarm_tone">Alarm Tone (Default)</string>
<string name="default_alarm_tone">Default alarm tone</string>
<string name="alert">Ringtone</string>
<string name="permissions_external_storage_text">It seems that we cannot play %s, probably because a permission is required. If you want, you can grant the permission. Alternatively, select a different sound. This decision can be changed in System Settings.</string>
<string name="permissions_external_storage_text">Cannot play %s. Allow access to the sound or choose another one. You can change permissions in your device settings.</string>

<string name="channel_alarm_name">Math alarms</string>
<string name="channel_alarm_description">Notifications for alarms</string>
<string name="channel_update_name">App Updates</string>
<string name="channel_update_name">App updates</string>
<string name="channel_update_description">Notifications about new app versions</string>

<string name="notification_action_completed">Done</string>
<string name="notification_action_snooze">In a few mins</string>
<string name="notification_action_snooze">Snooze</string>

<string name="pref_theme_light">Light</string>
<string name="pref_theme_dark">Dark</string>
Expand All @@ -87,10 +87,10 @@
<string name="pref_theme_dark_value" translatable="false">dark</string>
<string name="pref_theme_system_value" translatable="false">system</string>

<!-- We default using the system default -->
<!-- Follow the device theme until the user chooses another theme. -->
<string name="pref_theme_default_value" translatable="false">@string/pref_theme_system_value</string>
<string name="task_alarm_permission_dialog_title">Alarms</string>
<string name="task_alarm_permission_dialog_text">" In order for alarms to work, Math alarm needs the permission to set alarms. If this permission is not granted, the existing alarms will not work as well."</string>
<string name="task_alarm_permission_dialog_text">Allow Math Alarm to set alarms. Without this permission, new and existing alarms cannot ring.</string>
<string name="task_alarm_permission_dialog_confirm">Grant</string>
<string name="task_alarm_permission_dialog_cancel">Not now</string>
</resources>
Loading
Loading