{"id":16251184,"url":"https://github.com/ramzijabali/core-android-features","last_synced_at":"2025-10-25T03:31:32.156Z","repository":{"id":239241826,"uuid":"774528564","full_name":"RamziJabali/Core-Android-Features","owner":"RamziJabali","description":"Important Android features that I have used consistently. They are documented in a way to allow anyone to be able to quickly implement that feature.","archived":true,"fork":false,"pushed_at":"2024-05-16T22:19:58.000Z","size":129,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-12-22T01:11:08.095Z","etag":null,"topics":["android","android-development","camerax","camerax-api","coroutines-android","coroutines-flow","coroutines-room","foreground-service-android","notification-android","room-database","room-database-kotlin-implementation"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RamziJabali.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-19T17:50:21.000Z","updated_at":"2024-06-01T06:18:20.000Z","dependencies_parsed_at":"2024-05-10T23:27:37.862Z","dependency_job_id":"e49ec83e-aec1-4989-8ec5-33f4718651d6","html_url":"https://github.com/RamziJabali/Core-Android-Features","commit_stats":null,"previous_names":["ramzijabali/random-android-features"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RamziJabali%2FCore-Android-Features","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RamziJabali%2FCore-Android-Features/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RamziJabali%2FCore-Android-Features/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RamziJabali%2FCore-Android-Features/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RamziJabali","download_url":"https://codeload.github.com/RamziJabali/Core-Android-Features/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238075320,"owners_count":19412311,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-development","camerax","camerax-api","coroutines-android","coroutines-flow","coroutines-room","foreground-service-android","notification-android","room-database","room-database-kotlin-implementation"],"created_at":"2024-10-10T15:09:14.250Z","updated_at":"2025-10-25T03:31:26.863Z","avatar_url":"https://github.com/RamziJabali.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Android Features (Kotlin + Compose)\n\n## Snack Bar \n\nThis example shows how to display a SnackBar from a floating action button\n\n```kotlin\n@Composable\nfun ScaffoldForSnackBar() {\n    val coroutineScope = rememberCoroutineScope() // To launch your corotines\n    val snackBarHostState = remember { SnackbarHostState() } // snack bar state\n    val textState = remember {  mutableStateOf(\"\") } // Text state\n    Log.i(\"Scaffold\", \"In Scaffold\") \n    Scaffold(\n        // Snack bar parameter \n        snackbarHost = {\n            SnackbarHost(hostState = snackBarHostState)\n        },\n        // floating action button parameter \n        floatingActionButton = {\n            //defining floating action button \n            ExtendedFloatingActionButton(\n                text = { Text(\"Show snackbar\") },\n                icon = { Icon(Icons.Filled.Add, contentDescription = \"\") },\n                // onClick listener\n                onClick = {\n                    coroutineScope.launch {\n                        val result = snackBarHostState\n                            .showSnackbar(\n                                message = \"Snackbar\",\n                                actionLabel = \"Action\",\n                                // Defaults to SnackbarDuration.Short\n                                duration = SnackbarDuration.Indefinite,\n                                withDismissAction = true\n                            )\n                        // Logs user clicks within snackbar\n                        when (result) {\n                            SnackbarResult.ActionPerformed -\u003e {\n                                Log.i(\"Scaffold\", \"In Action Performed\")\n                                textState.value = \"Showing Snackbar\"\n                                snackBarHostState.showSnackbar(result.name)\n                                /* Handle snackbar action performed */\n                            }\n\n                            SnackbarResult.Dismissed -\u003e {\n                                /* Handle snackbar dismissed */\n                                Log.i(\"Scaffold\", \"In Dismissed\")\n                                textState.value = \"Dismissed Snackbar\"\n                            }\n                        }\n                    }\n                }\n            )\n        }\n    ) { contentPadding -\u003e\n        Column(modifier = Modifier.padding(contentPadding)) {\n            Text(text = textState.value, modifier = Modifier.fillMaxSize())\n        }\n    }\n}\n```\n\nThis example is done showcasing the use of SnackBar action button when handling notifications\n\n```kotlin\n@Composable\nfun SimpleTextBoxView(\n    checkText: (text: String) -\u003e Unit,\n    userClickedActionSnackbar: () -\u003e Unit,\n    failureMessage: String,\n    showSnackBar: Boolean,\n    rationaleMessage: String\n) {\n    val coroutineScope = rememberCoroutineScope()\n    val snackBarHostState = remember { SnackbarHostState() }\n    var text by remember { mutableStateOf(\"\") }\n    Log.i(\"Scaffold\", \"In Scaffold\")\n    Scaffold(\n        snackbarHost = {\n            SnackbarHost(hostState = snackBarHostState)\n            if (showSnackBar) {\n                Runnable {\n                    coroutineScope.launch {\n                        Log.i(\"snackbar state\", \"Displaying SnackBar\")\n                        val result = snackBarHostState\n                            .showSnackbar(\n                                message = rationaleMessage,\n                                actionLabel = \"Accept\",\n                                // Defaults to SnackbarDuration.Short\n                                duration = SnackbarDuration.Indefinite,\n                                withDismissAction = true\n                            )\n                        when (result) {\n                            SnackbarResult.ActionPerformed -\u003e {\n                                Log.i(\"snackbar state\", \"In Action Performed\")\n                                userClickedActionSnackbar()\n\n                            }\n\n                            SnackbarResult.Dismissed -\u003e {\n                                /* Handle snackbar dismissed */\n                                Log.i(\"snackbar state\", \"In Dismissed\")\n                            }\n                        }\n                    }\n                }.run()\n            }\n        }\n    ) { contentPadding -\u003e\n        Column(modifier = Modifier.padding(contentPadding)) {\n            Column(horizontalAlignment = CenterHorizontally) {\n                Text(\"Enter Number 1234 for a Personal notification channel and 4321 for Work notification channel\")\n                TextField(value = text, onValueChange = { newText -\u003e\n                    text = newText\n                }, Modifier.fillMaxWidth())\n                Text(text = failureMessage)\n                Button(onClick = {\n                    checkText(text)\n                }, Modifier.fillMaxWidth()) {\n                    Text(text = \"Click Me To Check IF you were RIGHT\")\n                }\n            }\n        }\n    }\n}\n```\n\n## Foreground Service with persistant notification\n\n### how to start a foreground service\n\n1. Create Foreground Service class\n \n```kotlin\nclass ForegroundService : Service() { ... }\n```\n\n2. Add to manifest\n    - You will have to Define it in the manifest\n    - Your foreground service can be of a different type, mine is a location foreground service\n\n```manifest\n     \u003cservice\n            android:foregroundServiceType=\"location\"\n            android:exported=\"false\"\n            android:name=\".loactionservice.ForegroundService\"\u003e\n     \u003c/service\u003e\n```\n\n3. Override onStartCommand function\n    * You can define an expression to handle the different actions tht are permitted within your service\n    * In this case I have an actions enum defined with the service `enum class Actions { START, STOP}`\n\n```kotlin\noverride fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        when (intent?.action) {\n            Actions.START.name -\u003e {\n                Log.i(\"ForegroundService::Class\", \"Starting service\")\n                start()\n            }\n            Actions.STOP.name -\u003e {\n                Log.i(\"ForegroundService::Class\", \"Stopping service\")\n                stop()\n            }\n        }\n        return super.onStartCommand(intent, flags, startId)\n    }\n\n```\n\n4. Create notifications and notification channels\n    - Notification channels can be created in a seprate class and initialized on onCreate()\n    - My notification includes Pending Intents\n\n\n    * The first PendingIntent(Action that will take place at a later time) makes it so if the user clicks on the notification the MainActivity::class.java is started\n        * `PendingIntent.getActivity()` ~ It is used to create a PendingIntent that will start an activity when triggered.\n        * This happens because we pass it an Intent `Intent(this, MainActivity::class.java)` to start the MainActivity::class\n        * `PendingIntent.FLAG_IMMUTABLE` This flag indicates that the PendingIntent should be immutable, meaning that its configuration cannot be changed after it is created. \n\n```kotlin\nprivate val pendingIntent: PendingIntent by lazy {\n        PendingIntent.getActivity(\n            this, 0, Intent(this, MainActivity::class.java),\n            PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n```\n\n* The Second PendingIntent(Action that will take place at a later time) makes it so if the user clicks on the notification action button the service is stopped\n    * `PendingIntent.getService()` ~ Similar to getActivity(), it is used to create a PendingIntent, but instead of starting an activity, it starts a service when triggered. \n    * This happens because we pass it an Intent `Intent(this, ForegroundService::class.java).apply {action = Actions.STOP.name}` to start the ForegroundService::class.java\n    * `PendingIntent.FLAG_IMMUTABLE` This flag indicates that the PendingIntent should be immutable, meaning that its configuration cannot be changed after it is created. \n    * Recall `onStartCommand()` contains an expression that evaluates what is passed into the intent\n\n```kotlin\n    private val stopServicePendingIntent: PendingIntent by lazy {\n        PendingIntent.getService(\n            this, 0, Intent(this, ForegroundService::class.java).apply {\n                action = Actions.STOP.name\n            },\n            PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n```\n\n```kotlin\nprivate fun getNotification() =\n        NotificationCompat.Builder(applicationContext, CHANNEL_ID_1)\n            .setSmallIcon(R.mipmap.just_jog_icon_foreground)\n            .setContentTitle(ContextCompat.getString(applicationContext, R.string.just_jog))\n            .setContentText(ContextCompat.getString(applicationContext, R.string.notification_text))\n            .setPriority(NotificationCompat.PRIORITY_DEFAULT)\n            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)\n            .setOngoing(true)\n            .setAutoCancel(false)\n            .setStyle(NotificationCompat.BigTextStyle())\n            // Set the intent that fires when the user taps the notification.\n            .setContentIntent(pendingIntent)\n            .addAction(\n                R.mipmap.cross_monochrome,\n                getString(R.string.stop_jog),\n                stopServicePendingIntent\n            )\n            .build()\n```\n\n5. Define your logic that is used when your service is started\n    * Using a LocationManager and LocationListener\n      \n```kotlin\nprivate val locationManager by lazy {\n        ContextCompat.getSystemService(application, LocationManager::class.java) as LocationManager\n    }\n\n    private val locationListener: LocationListener by lazy {\n        LocationListener { location -\u003e\n            Log.d(\n                \"ForegroundService::Class\",\n                \"Time:${ZonedDateTime.now()}\\nLatitude: ${location.latitude}, Longitude:${location.longitude}\"\n            )\n        }\n    }\n```\n\n\n```kotlin\n private fun stop() {\n        locationManager.removeUpdates(locationListener)\n        stopSelf()\n    }\n\n    private fun start() {\n        Log.i(\"ForegroundService::Class\", \"Checking permissions\")\n        for (permission in permissions.list) {\n            val currentPermission = ContextCompat.checkSelfPermission(this, permission)\n            if (currentPermission == PackageManager.PERMISSION_DENIED) {\n                // Without these permissions the service cannot run in the foreground\n                // Consider informing user or updating your app UI if visible.\n                Log.d(\"ForegroundService::Class\", \"Permissions were not given, stopping service!\")\n                stopSelf()\n            }\n        }\n\n        try {\n            ServiceCompat.startForeground(\n                this,\n                NOTIFICATION_ID, // Cannot be 0\n                getNotification(),\n                if (Build.VERSION.SDK_INT \u003e= Build.VERSION_CODES.R) {\n                    ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION\n                } else {\n                    0\n                },\n            )\n            // getting user location\n            locationManager.requestLocationUpdates(\n                LocationManager.GPS_PROVIDER,\n                LOCATION_REQUEST_INTERVAL_MS,\n                0F,\n                locationListener\n            )\n        } catch (e: Exception) {\n            if (Build.VERSION.SDK_INT \u003e= Build.VERSION_CODES.S\n                \u0026\u0026 e is ForegroundServiceStartNotAllowedException\n            ) {\n                // App not in a valid state to start foreground service\n                // (e.g. started from bg)\n                Log.d(\"ForegroundService::Class\", e.message.toString())\n            }\n        }\n    }\n```\n7. You have now defined a general skeleton for your foreground service\n    * You created a means to handle intents to start your foreground service\n    * You created your own notification\n    * You created pending intents to go with your notification\n        * Starts MainActivity::Class.java and stops the foreground service    \n    * You created logic that is executed when the service is started\n\n  \n## Creating Room Database\n\n### Where to start?\n\nThere are three components to creating a Room database\n\n1. Database class: holds the database and contains refrences of all DAO's\n2. DAO(Data Access Object): Get's entites from the database and pushes changes back to the database\n3. Entites: Get/Set field values\n\n### Now that we have the basic understanding down, let's get started:\n\n1. Create an entity for your table:\n    * You have to annotate your data class with the `@Entity or @Entity(\"jog_entries\")`\n        * You are defining this data class as an entity for your DAO, and you can decide to label your table\n        * You have to define a primary key for your entity via the `@PrimaryKey()` annotation, you can also have it so the keys are autogenrated.\n        * You can optionally have different names for your columns by using `@ColumnInfo(name = \"id\")` which sets a custom names for a table and it's columns\n           \n```kotlin\n@Entity(\"jog_entries\")\ndata class JogEntry(\n    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = \"id\")\n    val id: Int,\n    @ColumnInfo(\"jog_id\")\n    val jogId: Int,\n    @ColumnInfo(\"date_time\")\n    val dateTime: String,\n    @ColumnInfo(\"latitude\")\n    val latitude: Double,\n    @ColumnInfo(\"longitude\")\n    val longitude: Double,\n)\n```\n\n2. Create a DAO for your table\n\n    * The DAO(data access object) provides methods that the rest of your application uses to interact with your table.\n        * So in this case my `JogEntryDAO` has multiple methods that will be used to interact with my `jog_entries` table previously defined.\n     \n```kotlin\n@Dao\ninterface JogEntryDAO {\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    fun addUpdateWorkout(jogEntries: JogEntry)\n\n    @Query(\"SELECT * FROM jog_entries\")\n    fun getAll(): Flow\u003cList\u003cJogEntry\u003e\u003e\n\n    @Query(\"SELECT * FROM jog_entries WHERE date_time LIKE (:stringDate)\")\n    fun getByDate(stringDate: String): Flow\u003cList\u003cJogEntry\u003e?\u003e\n\n    @Query(\"SELECT * FROM jog_entries WHERE date_time BETWEEN (:startDate) AND (:endDate)\")\n    fun getByRangeOfDates(startDate: String, endDate: String): Flow\u003cList\u003cJogEntry\u003e\u003e\n\n    @Query(\"SELECT * FROM jog_entries WHERE jog_summary_id IS :jogId\")\n    fun getByID(jogId: Int): Flow\u003cList\u003cJogEntry?\u003e\u003e\n\n    @Query(\"DELETE FROM jog_entries\")\n    fun deleteAll()\n}\n```\n\n* You could have a usecase class that transforms values returned from your DAO and can transorm values required for your DAO.\n\n3. Create your Database\n\n    1. Class creation\n        * Create an abstract class that extends `RoomDatabase`, ex: `abstract class AppDatabase : RoomDatabase()`\n        * Annotate your class with the `@Database()` annotation and provide it with all your entities and a * version number: `@Database(entities = [JogEntryDAO::class], version = 1)`\n    \n    2. Within the class\n        * Define abstract functions for all your available DAO's: `abstract fun jogEntryDao(): JogEntryDAO`\n    \n    3. Refrence your database for usage\n        * Use Rooms `databaseBuilder()` to create your database object\n        * ```\n          val db = Room.databaseBuilder(\n            applicationContext,\n            AppDatabase::class.java, \"just-jog-database\")\n          .build()\n          ```\n        * I prefer to do this with dependency injection framework like Koin.\n\n```kotlin\n@Database(entities = [JogEntryDAO::class], version = 1)\nabstract class AppDatabase : RoomDatabase() {\n    abstract fun jogEntryDao(): JogEntryDAO\n}\n```\n\n4. Use your database\n\n```kotlin\nval db = Room.databaseBuilder(\n            applicationContext,\n            AppDatabase::class.java, \"just-jog-database\")\n          .build()\n\nval jogEntryDao = db.jogEntryDao()\nval users: List\u003cUser\u003e = jogEntryDao.getAll()\n\n```\n\n5. Example:\n\n   * Mock View Model to simulate an actual application\n   * Pass it a dao refrence or a usecase refrence to be able to make calls to and from the database\n   * Using a `CompletableDeferred` makes it so I know that the process completed successfully or failed exceptionally\n\n```kotlin\n     class MockVM(private val dao: JogEntryDAO): ViewModel {\n   \n     fun addJog(jogEntry: JogEntry) {\n        val deferred = CompletableDeferred\u003cUnit\u003e()\n\n        val job = viewModelScope.launch(Dispatchers.IO)  {\n            try {\n                dao.addUpdateWorkout(jogEntry)\n                deferred.complete(Unit)\n                Log.d(\"MockVM::Class.java\", \"added Jog: $deferred\")\n            } catch (e: CancellationException) {\n                Log.d(\"MockVM::Class.java\", \"Coroutine canceled: ${e.message}\")\n                // Handle cancellation if needed\n            }\n        }\n\n        deferred.invokeOnCompletion { cause -\u003e\n            if (cause != null) {\n                if (cause is CancellationException) {\n                    Log.d(\"MockVM::Class.java\", \"Coroutine canceled: ${cause.message}\")\n                } else {\n                    Log.e(\"MockVM::Class.java\", \"Coroutine failed with: $cause\")\n                }\n                job.cancel() // Cancel the job explicitly\n                }\n            }\n         }\n\n      fun getAllJogs() {\n          viewModelScope.launch(Dispatchers.IO){ \n                val list = viewModelScope.async { dao.getAll() }.await()\n                list.collect {\n                    Log.d(\"MockVM::Class.java\", \"getAllJogs: $it\")\n                    }\n                }\n            }\n        }\n     }\n   \n```\n\n* Within your `MainActivity` you can have a refrence to your `MockVM` and call the `getAllJogs()` function\n   \n```kotlin\noverride fun onCreate(){\nvm.getAllJogs()\n     override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val db = Room.databaseBuilder(\n            applicationContext,\n            JustJogDataBase::class.java, \"just-jog-database\"\n        )\n            .build()\n        val vm = MockVM(db.jogEntryDao())\n        vm.addJog(\n            JogEntry(\n                id = 0, jogSummaryId = 0, dateTime = \"\", latitude = 0.0, longitude = 0.0\n            )\n        )\n\n        vm.getAllJogs()\n}\n```\n  \n\n### Result:\n\nLogCat\n```\n2024-05-13 13:42:03.554 17739-17763 MockVM::Class.java      ramzi.eljabali.justjog               D  addJog: true\n2024-05-13 13:42:03.850 17739-17763 MockVM::Class.java      ramzi.eljabali.justjog               D  getAllJogs: [JogEntry(id=1, jogSummaryId=0, dateTime=, latitude=0.0, longitude=0.0)]\n```\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Framzijabali%2Fcore-android-features","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Framzijabali%2Fcore-android-features","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Framzijabali%2Fcore-android-features/lists"}