Skip to content

RackaApps/Reluct

Repository files navigation

βŒ› Reluct

Code Checks Build Pipeline

Twitter: rackadev

An app to manage your screen time, assign Tasks and set personal goals. Works on Android with Desktop support in the works.

✨ Documentation

🀳 Screenshots

πŸ’» Install

Platform Download Status
Android Download Button πŸ§ͺ Alpha
Desktop - Windows Download Button πŸ§ͺ Not Released
Desktop - macOS Download Button πŸ§ͺ Not Released
Desktop - Linux Download Button πŸ§ͺ Not Released

ℹ️ Compose Debug apks are sometimes laggy as they contain a lot of debug code.

ℹ️ Download the app from Play Store and it will have the expected performance.

πŸ‹ Requirements

  • Java 17 or above
  • Android Studio Dolphin | 2021.3+

πŸ—οΈοΈ Built with

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

Issues

If you encounter any issues you simply file them with the relevant details here

πŸ“ƒ Important Analysis

1. The purpose of this project

I made this project solely as a means of learning about Kotlin Multiplatform, Jetpack Compose and multi module project structuring. I do not advocate for the use of this structure, especially for small projects. You can easily get similar results and re-usability with far less modules than this. I made this to test the boundaries of Jetpack Compose and see how tooling works with Kotlin Multiplatform. You can checkout a similarly structured project (with Compose Desktop already running) called Thinkrchive

2. Evaluating Jetpack Compose on Android

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;

i. Tooling

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.

ii. Going outside the box

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.

iii. 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;

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

iv. Animations

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.

v. Bugs, Experimental & Missing Features

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;

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

vi. Conclusions

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.

3. Evaluating Kotlin Multiplatform

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:

i. Tooling

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

ii. Multiplatform UI

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).

iii. Sharing business logic

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 in commonMain 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.

4. Broken tests

Some of the Unit tests in this project are broken. Why? This project first started as an Android only project with some unit tests already written. Upon migration migration to Kotlin Multiplatform I had to rewrite all tests so they can be platform agnostic. This meant replacing all Junit with Kotlin-test, removing Hilt in favor of Koin and avoiding the use of Mockk. During the migration refactoring became quite tricky and time consuming for something I was doing on in my spare time. Android studio and Intellij IDEs became quite unstable with bugs like KT48148 that made IDE assisted refactoring impossible I quickly gave up and just YOLOed into feature completion. Tooling is still not great and refactoring tests is still tricky but I am working on it in the repair-tests branch. Any help will be appreciated.

βœ… TODO

  • Fix broken tests
  • Add more Tests (UI Tests & Integration Tests)
  • Add more features
  • Support for more platforms

πŸ™‡ Credits

  • Special thanks to @theapache64 for readgen
  • Thanks to all amazing people at Twitter for inspiring me to continue the development of this project.

🀝 Contributing

❀ Show your support

Give a ⭐️ if this project helped you!

ko-fi

πŸ“ License

Reluct - Tasks, Goals and Digital Wellbeing.
Copyright (C) 2022  racka98

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 ❀