An app to manage your screen time, assign Tasks and set personal goals. Works on Android with Desktop support in the works.
Note: This is fully Multiplatform version of old Reluct app. It is still in migration phase
Platform | Download | Status |
---|---|---|
Android | π§ͺ Not Released | |
Desktop - Windows | π§ͺ Not Released | |
Desktop - macOS | π§ͺ Not Released | |
Desktop - Linux | π§ͺ Not Released |
βΉοΈ Compose Debug apks are sometimes laggy as they contain a lot of debug code.
βΉοΈ Download the app from releases page or build a release apk and it will have the expected performance.
- Java 17 or above
- Android Studio Dolphin | 2021.3+
Component | Libraries |
---|---|
π User Interface | Jetpack Compose + Compose Multiplatform |
π Architecture | MVVM |
π DI | Koin |
π£οΈ Navigation | Compose Navigation, Decompose |
π Async | Coroutines + Flow + StateFlow + SharedFlow |
π Networking | Ktor Client |
π΅ Billing | RevenueCat |
π JSON | Kotlin Serialization |
πΎ Persistence | SQLDelight, Multiplatform Settings |
β¨οΈ Logging | Timber - Android, slf4j + logback, Kermit |
πΈ Image Loading | Coil |
π§ͺ Testing | Mockk, JUnit, Turbine, Kotlin Test + Robolectric |
β Date and Time | Kotlinx DateTime |
π Immutability | Kotlinx Immutable Collections |
π§ Supplementary | Accompanist |
If you encounter any issues you simply file them with the relevant details here
-
/composeApp
contains the Compose Multiplatform code and is the starting point for targets. It contains several subfolders:commonMain
is for code thatβs common for all targets.- Other folders are for Kotlin code that will be compiled for only the platform indicated in the
folder name.
For example, if you want to use Android Context for the Android parts of your Kotlin app,
androidMain
would be the right folder for such calls.
-
/common
contains all the common code and is a starting template for any modules that you may add to this project. -
/tooling
contains all the necessary tools you would need during build It contains the following:/checks
contains detekt configuration files/desktop
contains the icons needed for building an installer for desktop targets/proguard-config
contains proguard configuration files for both desktop and android/plugins
contains custom gradle convention plugins that simplify module configuration
-
/iosApp
contains iOS applications. Even if youβre sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. It needs to be set up for use.
This contains all the code for the custom plugins used in this project.
These plugins help with build.gradle.kts
boilerplate code that can be annoying to configure
Their uses are as follows:
ComposeMultiplatformAppPlugin
used to configure the main app modulecomposeApp
. It contains code for android setup, Compose Multiplatform and the necessary libraries already configuredComposeMultiplatformLibPlugin
used to configure all library sub-modules that will also contain Compose code Necessary libraries already installed too.KotlinMultiplatformLibPlugin
used to configure all library sub-modules. Configured forkoin
and basic Android core librariesDetektConventionPlugin
used to configuredetekt
code analysis in the project levelbuild.gradle.kts
This project contains Github Actions set up in /.github/workflows
code-check-pipeline.yml
contains code for runningdetekt
for code analysis. You can also set up other linters and unit tests inside this fileci-cd-pipeline.yml
contains code for CI/CD integrations. You make sure you have provided the required secrets for the signing stage of the Android app. The secrets needed includeKEYSTORE_FILE
,KEYSTORE_PASSWORD
,KEY_ALIAS
andKEY_PASSWORD
. Learn more about adding these keys from here and here
This is a fully Compose Multiplatform version of the original Reluct App This will be the only updated version of the app. The older (original) version will no longer be maintained.
There has been a lot of talk and doubt on Jetpack Compose and how production ready is it. After dealing with it in this project (I've used it other projects too). I can say the following;
Tooling for Compose varies from easy to annoying.
- With Android Studio Dolphin we have full support for Layout Inspector and can see the number of recompositions happening in the app.
- We now have better support for Previews in Android Studio Electric Eel with a "hot reload like" feature upon code changes. The feature is called Live Edit. While its good it doesn't really compare to the convenience of previews in XML since there is no compilation involved there.
- Support for Animation Previews is a very big feature that I really enjoy using. But it faces the same issues of slow compilation on code changes as Previews
- No drag and drop like feature for UI components. It's hard to make such tool for Compose since it's Kotlin code that's adaptive. Some would argue that they never use drag and drop with XML but it's very much a deterring reason to some. At least we have Relay that can help you with some of this.
Most of the instability issues with the IDE have been fixed and it's very stable now but the slow Previews that can only be fixed by faster machines are still a very big issue to some developers.
Implementing designs that are different from the Material Design spec can vary from extremely easy to very hard.
Compose is very flexible and you will get more benefits if you are tasked with creating a design system. Compared to XML, custom designs are very easy in Compose and over great re-usability when done correctly.
I have a components
module that has all the common custom components use throughout the app for easy re-usability and consistent design.
But it's not all rainbows, when you start doing custom things it can become tricky pretty fast.
1. Making the bottom navigation bar collapsible on scroll or in some destinations was quite tricky
You need the bottom nav bar to be at the top most Scaffold
for the best effect, but hiding and showing it is based on the children screen below the top Scaffold
So what can you do to monitor the scroll of the different screens and decide when to hide or show the bar, while still maintaining readable code?
Well you need to create something custom to do that for you. So, I had to create BarsVisibility and ScrollContext
// BarsVisibility interface
@Stable
interface BarVisibilityState {
val isVisible: Boolean
fun hide()
fun show()
}
@Stable
interface BarsVisibility {
val topBar: BarVisibilityState
val bottomBar: BarVisibilityState
// StatusBar and NavBar items are useful for triggering immersive mode
val statusBar: BarVisibilityState
val navigationBar: BarVisibilityState
}
// ScrollContext implementation
private fun LazyListState.isLastItemVisible(): Boolean =
layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
private fun LazyListState.isFirstItemVisible(): Boolean =
firstVisibleItemIndex == 0
data class ScrollContext(
val isTop: Boolean,
val isBottom: Boolean,
)
@Composable
fun rememberScrollContext(listState: LazyListState): ScrollContext {
val scrollContext by remember {
derivedStateOf {
ScrollContext(
isTop = listState.isFirstItemVisible(),
isBottom = listState.isLastItemVisible()
)
}
}
return scrollContext
}
Simply observing layoutInfo.visibleItemsInfo
or similar from LazyListState
will cause random Recompositions. You need to get creative.
So, now you use them to hide or show the bottom nav bar without making it messy.
@Composable
fun MyApp() {
val barsVisibility = rememberBarsVisibility()
Scaffold(
bottomBar = { BottomNavBar(show = barsVisibility.bottomBar.isVisible) }
) {
AnimatedNavHost {
composable {
val listState = rememberLazyListState()
val scrollContext = rememberScrollContenxt(listState)
SideEffect {
if (scrollContext.isTop) {
barsVisibility.bottomBar.show()
} else {
barsVisibility.bottomBar.hide()
}
}
LazyList(listState) {
// Some Items Here
}
}
}
}
}
The issue with this is that hiding or showing the Bottom nav bar causes the whole screen to Recompose simply because that also changes the
size of the parent Scaffold
which causes re-calculation of its size. If most of Composables aren't skippable then you are out of luck.
This is something I'm still exploring to see how I can fix it.
2. Easily ending up with function having numerous parameters
See ScreenTimeStatisticsUI as an example.
If you really want to make sure you don't break Unidirectional Data Flow and don't pollute you all you child composables with ViewModel
parameter that will cause multiple recompositions you need to State Hoist and end up with this;
@Composable
internal fun ScreenTimeStatisticsUI(
modifier: Modifier = Modifier,
barsVisibility: BarsVisibility,
snackbarHostState: SnackbarHostState,
uiState: ScreenTimeStatsState,
getUsageData: (isGranted: Boolean) -> Unit,
onSelectDay: (dayIsoNumber: Int) -> Unit,
onUpdateWeekOffset: (weekOffsetValue: Int) -> Unit,
onAppUsageInfoClick: (app: AppUsageInfo) -> Unit,
onAppTimeLimitSettingsClicked: (packageName: String) -> Unit,
onSaveAppTimeLimitSettings: (hours: Int, minutes: Int) -> Unit
)
There are various other quirks like this that need you to come up with your own solution and make sure your solution doesn't drastically affect performance.
There are various articles that have discussed performance on Compose so I won't analyse much here. See this article to know the common downfalls.
I can say that ensuring you have great performance on low end devices can become very hard in Jetpack Compose. You need to know and use these;
- Using ImmutableList or ImmutableMap from kotlinx.immutable.collections
- LaunchedEffect, SideEffect, DisposableEffect, remember, rememberSaveable, deriveStateOf, produceState.
- State Hoisting
Thinking In Compose guide can help you adjust your mental model but some say that this is a lot of caveats just to write UI differently When using Compose you need to tread carefully or you might cause a significant performance problem. I even have some of these problems in this project. This app performs well on most devices. Anything with the performance of Xiaomi Redmi 10C (SD 680) or Google Pixel 3a(SD 670) and higher should face no major performance issues, but it will struggle on low end devices.
Performance is hard even with Views and XML but it's easier to mess up with Jetpack Compose
This is an area when I thing Compose excels imo. Most of the animations in this app are done with just;
AnimatedVisibility
AnimatedConten
animateFloatAsState
animatedColorAsState
Animatable
With no fancy or complicated code but you still get great results.
Mostly it has been smooth sailing with very little bugs caused by Compose itself.
But there are some major bugs that can make it not suitable for production completely. Some notable ones;
- Keyboard gets closed when Text Field is scrolled slightly off screen You can reproduce this in this app. Open Tasks, Add New Task, Click the title text field, scroll a bit and the keyboard closes.
- You can't request focus for a field not in Composition yet (related to the issue above)
- No full native auto fill support in TextFields
Then there's the issue of having a lot of @ExperimentalXX
API that may not be acceptable in some companies and this immediately signals that this code is already technical debt.
Even stable versions of Compose have this problem.
Some important components are missing, though they are easy to replicate or find in the Accompanist library;
- Date & Time Picker
- Basic Graphs & Charts
- Dynamic Horizontal and Vertical pages
- Built-in image loading from URL
- System UI controller
- Smart Text wrapping in TextField
- More transition Animations in the official Navigation library (plenty of replacements though)
- More items in Long-Tap ContextMenu
- Remove Animations for LazyColumn
- Material Motions
- And others that I've probably forgotten
While there might some issues in Jetpack Compose right now I think it's a great step toward native declarative UI in Android. I look forward to more features and critical bug fixes on Compose so it can be feature parity with Views/XML.
I will still keep using Compose for the right projects because it has made development and custom designs faster for me. You'll benefit more from the speed of development in Compose when you define you build block components and use them instead of writing everything over and over.
Kotlin Multiplatform is still in alpha stage. While Kotlin Multiplatform Mobile (Android + iOS) has been announced going beta core Kotlin Multiplatform (Mobile + JVM + Linux + mingw/Windows + macOS/iOS native) is still very much alpha. There are companies that already embrace this alpha product like Touchlab and Square/Block, adopting this will not be compelling to most companies.
I can say that there are some things worth noting before diving into it:
IDE support is great at the moment but there are some major bugs that creep their way into it from time to time. Take the KT48148 bug as an example. It pretty much breaks all the smart IDE features and makes doing anything in the common code a hassle. Right now (as of Nov-01/2022) we can't use Android Studio (Electric Eel and lower) without facing some variation of the bug mentioned above as it's only fixed in newer versions of the Intellij IDEA but at the same time Intellij IDEA does't support Android Gradle Plugin 7.3+. This makes it unsuitable for Android targets.
Tooling still has time to mature and I'd expect more collaboration between Jetbrains and Google as seen in this Slack thread
Kotlin Multiplatform is not aimed at being the next Flutter or React native and that means it does not force you to use a specific UI toolkit for your entire app.
It emphasizes more on sharing business logic to reduce replication on native code of the target platforms. However, there is support for having common UI with Compose Multiplatform where you share UI components. For now we can share some UI components with Android, Desktop (JVM) and macOS/iOS (experimental). There is support for Compose Web but that doesn't share any components with the other platforms since it's based on the Web DOM.
Personally, I think sticking to just sharing business logic and may presentation logic (ViewModels or Presenters) is the sweet spot. There's still great value in writing the UI using platform specific toolkits. You will be able to adhere to platform UI and UX guidelines and still be able to make the app belong to platform. Not everyone want an iOS that looks like an Android one (since Compose Multiplatform use Material design as a base).
The most compelling reason for Kotlin Multiplatform is the sharing of business logic. There are KMP ready libraries like SQLDelight, Multiplatform Settings, Ktor Networking, Analytics Kotlin and many more that let you write
everything in common code without having to make separate implementations. For things that are not supported you can easily make alternatives yourself with expect/actual
or Interfaces.
- I've experimented with this for things like having a view model that can be used in common code but still get backed by platform specific implementations for easy use like here.
// Define in commonMain
expect abstract class CommonViewModel() {
val vmScope: CoroutineScope
open fun onDestroy()
}
// Actual in jvmMain
actual abstract class CommonViewModel actual constructor() {
actual val vmScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
actual open fun destroy() {
vmScope.cancel()
}
}
// Actual in androidMain
actual abstract class CommonViewModel actual constructor() : ViewModel() {
actual val vmScope: CoroutineScope = viewModelScope
actual open fun onDestroy() {
vmScope.cancel()
}
// Cleared automatically by Android
override fun onCleared() {
super.onCleared()
onDestroy()
}
}
- Using this in
commonMain
class MyFeatureViewModel : CommonViewModel() {
fun someCall() {
vmScope.launch {
/** Do things here **/
}
}
}
- Or making an abstraction layer for things like Billing so you make using it in common code easier.
// Interface in commonMain
interface BillingApi {
fun getProducts(): Result
fun checkEntitlement(param: Type): Result?
fun purchase(product: Product): Result
}
// implementation in androidMain
class AndroidBilling {
/** Impl here **/
}
// implementation in jvmMain
class DesktopBilling {
/** Impl here **/
}
- You can then use
BillingApi
incommonMain
with no problem
// In commonMain
class ManageProducts(billing: BillingApi) {
/** Do what you want here **/
}
Writing business logic once can be very beneficial for products that have a lot of business logic and depend on native platform development.
- Fix broken tests
- Add more Tests (UI Tests & Integration Tests)
- Add more features
- Support for more platforms
- Special thanks to @theapache64 for readgen
- Thanks to all amazing people at Twitter for inspiring me to continue the development of this project.
- See CONTRIBUTING
Give a βοΈ if this project helped you!
Reluct - Tasks, Goals and Digital Wellbeing.
Copyright (C) 2024 rackadev
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Made With β€ From Tanzania πΉπΏ
This README was generated by readgen β€
Learn more about Kotlin Multiplatformβ¦