A production-ready Android app that displays a draggable floating bubble (chat-head style) reminding you to commit to GitHub every day.
| Main Interface | Floating Bubble | Settings |
|---|---|---|
![]() |
![]() |
![]() |
Maintaining a daily GitHub streak is hard.
This app solves that by:
- Keeping a floating reminder always visible
- Tracking real commit activity via GitHub API
- Encouraging consistency through smart nudges
Perfect for developers preparing for placements or building habits.
| Feature | Implementation |
|---|---|
| Floating overlay bubble | WindowManager + SYSTEM_ALERT_WINDOW |
| Draggable + snap-to-edge | Custom OnTouchListener with ValueAnimator |
| Daily reminders | WorkManager PeriodicWorkRequest |
| Missed-commit follow-up | WorkManager OneTimeWorkRequest |
| GitHub API integration | Retrofit 2 + OkHttp |
| Commit streak calc | Event history analysis |
| Offline-first caching | Room database |
| Preferences | Jetpack DataStore |
| MVVM architecture | ViewModel + Repository |
| Dependency injection | Hilt |
| Dark mode | Material 3 DayNight |
| Bubble color picker | 5 color swatches |
GitCommitBuddy/
├── app/
│ └── src/main/
│ ├── kotlin/com/gitcommitbuddy/
│ │ ├── GitCommitBuddyApp.kt # Application + Hilt entry point
│ │ ├── data/
│ │ │ ├── api/
│ │ │ │ ├── ApiResult.kt # Sealed result wrapper
│ │ │ │ ├── GitHubApiService.kt # Retrofit interface
│ │ │ │ └── GitHubModels.kt # API data classes
│ │ │ ├── db/
│ │ │ │ └── Database.kt # Room DB, DAOs, Entities
│ │ │ ├── repository/
│ │ │ │ └── GitHubRepository.kt # Single source of truth
│ │ │ └── PreferencesManager.kt # DataStore preferences
│ │ ├── di/
│ │ │ └── AppModule.kt # Hilt DI module
│ │ ├── service/
│ │ │ ├── FloatingWidgetService.kt # Overlay bubble service
│ │ │ ├── ReminderWorker.kt # WorkManager workers
│ │ │ └── BootReceiver.kt # Boot + notification receivers
│ │ ├── ui/
│ │ │ ├── main/
│ │ │ │ └── MainActivity.kt
│ │ │ └── settings/
│ │ │ └── SettingsActivity.kt
│ │ ├── util/
│ │ │ ├── NotificationHelper.kt # Channels + builders
│ │ │ ├── PermissionHelper.kt # Permission checks
│ │ │ ├── TimeFormatter.kt # ISO-8601 → human readable
│ │ │ └── MotivationalMessages.kt # Random messages
│ │ └── viewmodel/
│ │ ├── MainViewModel.kt
│ │ └── SettingsViewModel.kt
│ ├── res/
│ │ ├── drawable/ # Icons + shape backgrounds
│ │ ├── layout/
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_settings.xml
│ │ │ ├── layout_floating_bubble.xml
│ │ │ └── layout_floating_panel.xml
│ │ ├── menu/main_menu.xml
│ │ ├── values/
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ └── values-night/themes.xml
│ └── AndroidManifest.xml
├── build.gradle
├── settings.gradle
└── gradle.properties
| Tool | Version |
|---|---|
| Android Studio | Hedgehog (2023.1.1) or newer |
| JDK | 17 |
| Android SDK | API 34 (target), API 26 (min) |
| Gradle | 8.4 |
| Kotlin | 1.9.22 |
- Open Android Studio
- File → Open → select the
GitCommitBuddyfolder - Wait for Gradle sync to complete (~2–3 minutes first time)
⚠️ TOKEN REQUIRED — Without a token the app still works but is limited to 60 API calls/hour.
With a token: 5,000 calls/hour.
- Go to github.com → Settings → Developer settings
- Personal access tokens → Fine-grained tokens → Generate new token
- Set expiry (e.g. 1 year)
- Under Repository access:
Public Repositories (read-only) - Under Permissions → Account permissions:
Events: Read-only
- Click Generate token
- Copy the token — you won't see it again!
- Connect a physical Android device (API 26+) or start an emulator
- Click Run ▶ (or
Shift+F10) - The app installs and opens
- Tap Open Settings on the main screen (or the ⋮ menu → Settings)
- Enter your GitHub username (e.g.
octocat) - Paste your Personal Access Token
- Tap Save Credentials
- Back on the main screen, toggle Floating Widget ON
- A system dialog appears — tap Allow to grant overlay permission
- The green bubble appears over all your apps! 🟢
Your PAT is stored only on your device using Jetpack DataStore (encrypted Android storage). It is:
- ❌ Never sent anywhere except
api.github.com - ❌ Never logged
- ❌ Never backed up to cloud
- ✅ Transmitted over HTTPS only
// In Android Studio's Terminal:
adb shell am broadcast -a com.gitcommitbuddy.OPEN_GITHUBOpen App Inspection in Android Studio → Background Task Inspector → select daily_reminder → click Run Now
Settings → Apps → GitCommit Buddy → Display over other apps → Allow
UI Layer ViewModel Layer Data Layer
───────── ─────────────── ──────────
MainActivity ←→ MainViewModel ←→ GitHubRepository
Settings ←→ SettingsViewModel ├── GitHubApiService (Retrofit)
FloatingWidget (StateFlow/LiveData) ├── CommitCacheDao (Room)
└── PreferencesManager (DataStore)
Data flow:
- UI calls
viewModel.refresh() - ViewModel calls
repository.refreshCommitStatus() - Repository hits GitHub API via Retrofit
- Response is parsed →
CommitStatusdomain object - Results saved to Room cache
- Room emits via
Flow→ ViewModel → UI updates
| Permission | Why needed |
|---|---|
SYSTEM_ALERT_WINDOW |
Draw the floating bubble over other apps |
INTERNET |
Fetch GitHub commit data |
POST_NOTIFICATIONS |
Daily reminder notifications (Android 13+) |
FOREGROUND_SERVICE |
Keep the floating widget service alive |
VIBRATE |
Haptic feedback on reminders |
RECEIVE_BOOT_COMPLETED |
Reschedule WorkManager after reboot |
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS |
Ensure reliable background work |
SCHEDULE_EXACT_ALARM |
Precise reminder timing (Android 12+) |
In ReminderWorker.kt, change the default in scheduleDailyReminder():
// Default: 9 PM (hour=21, minute=0)
ReminderWorker.scheduleDailyReminder(context, reminderHour = 21, reminderMinute = 0)In SettingsActivity.kt, add a new swatch:
binding.colorTeal.setOnClickListener { selectColor("#00796B") }In GitHubRepository.kt, filter for additional event types:
val prEvents = events.filter { it.type == "PullRequestEvent" }| Problem | Solution |
|---|---|
| Bubble doesn't appear | Grant overlay permission: Settings → Apps → GitCommit Buddy → Display over other apps |
| "User not found" error | Double-check username spelling (case-sensitive) |
| "Invalid token" error | Token may have expired — generate a new one |
| No notifications | Check notification permission + battery optimisation exemption |
| App crashes on launch | Ensure minSdk 26+ device/emulator |
| WorkManager not firing | Disable battery optimisation for the app |
| Gradle sync fails | File → Invalidate Caches → Restart |
// UI
com.google.android.material:material:1.11.0 // Material 3
androidx.constraintlayout:constraintlayout:2.1.4
// Architecture
androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0
androidx.lifecycle:lifecycle-livedata-ktx:2.7.0
// Background work
androidx.work:work-runtime-ktx:2.9.0 // WorkManager
// Networking
com.squareup.retrofit2:retrofit:2.9.0
com.squareup.okhttp3:logging-interceptor:4.12.0
// Local storage
androidx.room:room-runtime:2.6.1 // SQLite ORM
androidx.datastore:datastore-preferences:1.0.0 // Key-value prefs
// DI
com.google.dagger:hilt-android:2.50 // Hilt
// Image loading
com.github.bumptech.glide:glide:4.16.0- Generate a keystore:
keytool -genkey -v -keystore gcb-release.jks \
-alias gcb -keyalg RSA -keysize 2048 -validity 10000- Add to
app/build.gradle:
android {
signingConfigs {
release {
storeFile file('../gcb-release.jks')
storePassword 'YOUR_STORE_PASSWORD'
keyAlias 'gcb'
keyPassword 'YOUR_KEY_PASSWORD'
}
}
buildTypes {
release { signingConfig signingConfigs.release }
}
}- Build:
Build → Generate Signed Bundle/APK → APK → release
MIT License — free to use, modify, and distribute.
Built with ❤️ and Kotlin. Happy committing! 🔥



