Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4c015d0
feat: core-compose module
TomTruyen Oct 20, 2023
0de1092
fix: package name
TomTruyen Oct 23, 2023
15910d7
Merge branch 'convert-gradle-scripts-to-use-kotlin-dsl' into core-v2
DanneD-Wisemen Oct 29, 2023
895530e
Merge branch 'convert-gradle-scripts-to-use-kotlin-dsl' into core-v2
DanneD-Wisemen Oct 29, 2023
cfd809c
Merge branch 'develop' into core-v2
DanneD-Wisemen Feb 6, 2025
9641bc1
refactor: loading livedata to flow
TomTruyenWisemen Oct 20, 2023
ed4fc19
feat: BaseComposeViewModel
TomTruyen Oct 20, 2023
c5ca5ca
refactor: code convention
TomTruyen Oct 20, 2023
dc2c327
refactor: package name
TomTruyen Oct 23, 2023
607e77e
feat: add missing action typing
TomTruyen Oct 27, 2023
7d48c95
refactor: change order of generics
TomTruyen Oct 27, 2023
0b20a7f
feat: initial state value
TomTruyen Oct 27, 2023
4950a84
fix: statusbar color option
TomTruyen Nov 10, 2023
e334577
refactor: remove unused protected val
TomTruyen Nov 10, 2023
3362e2b
feat: improved basecompose viewmodels + snackbarhost
TomTruyen Dec 12, 2023
700e2b2
feat: BaseBindingActivity + BaseBindingFragment
TomTruyen Dec 13, 2023
a8cb8da
feat: baseapp
TomTruyen Jan 8, 2024
bd07ffd
feat: BaseDialogFragments
TomTruyen Jan 8, 2024
76a999d
feat: option to add custom snackbar composable
TomTruyen Jan 8, 2024
076ac75
build: update material version
TomTruyen Jan 8, 2024
b41a777
build: fix build errors
TomTruyen Jan 8, 2024
58a44d5
fix: createSnackbarHost not accepting custom snackbar
TomTruyen Jan 8, 2024
fa6df2e
Refactor: Update SDK versions and dependencies, remove unused modules
DanneD-Wisemen Feb 6, 2025
3a2dd08
Upgrade SDK versions and dependencies
DanneD-Wisemen Feb 6, 2025
7360592
Merge pull request #338 from wisemen-digital/feature/base-classes-v2
DanneD-Wisemen Feb 7, 2025
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
15 changes: 1 addition & 14 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
dependencies {
classpath(libs.gradle)
classpath(kotlin("gradle-plugin", version = libs.versions.kotlin.get()))

classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${libs.versions.navigation.get()}")

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.kotlin.compose) apply false
}

project.tasks.register("clean", Delete::class) {
Expand Down
1 change: 1 addition & 0 deletions core-compose/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
55 changes: 55 additions & 0 deletions core-compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
alias(libs.plugins.kotlin.compose)
}

android {
namespace = "be.appwise.compose.core"
compileSdk = 35

defaultConfig {
minSdk = 26

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
}

dependencies {
implementation(project(":core"))

implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)

implementation(libs.material3)

implementation(platform(libs.compose.bom))
implementation(libs.ui)
implementation(libs.ui.graphics)
implementation(libs.ui.tooling)
implementation(libs.ui.tooling.preview)
debugImplementation(libs.ui.test.manifest)
implementation(libs.ui.test.junit4)
}
Empty file added core-compose/consumer-rules.pro
Empty file.
21 changes: 21 additions & 0 deletions core-compose/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
4 changes: 4 additions & 0 deletions core-compose/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package be.appwise.compose.core.ui.base

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow

open class BaseComposeStateViewModel<UIState, NavigationType>(initialState: UIState): BaseComposeViewModel() {
private val _uiState = MutableStateFlow(initialState)
val uiState = _uiState.asStateFlow()

private val _navigation = Channel<NavigationType>()
val navigation = _navigation.receiveAsFlow()

fun updateUIState(state: UIState) {
_uiState.tryEmit(state)
}

fun navigate(navigation: NavigationType) {
_navigation.trySend(navigation)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package be.appwise.compose.core.ui.base

import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import be.appwise.compose.core.ui.base.snackbar.BaseSnackbar
import be.appwise.compose.core.ui.base.snackbar.Empty
import be.appwise.compose.core.ui.base.snackbar.Error
import be.appwise.compose.core.ui.base.snackbar.SnackbarMessage
import be.appwise.core.ui.base.BaseViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

open class BaseComposeViewModel: BaseViewModel() {
// Snackbar Handling
private var snackbarMessageState by mutableStateOf<SnackbarMessage>(Empty)

override fun vmScopeWithCustomExceptionHandler(onError: (error: Throwable) -> Unit): CoroutineScope {
return super.vmScopeWithCustomExceptionHandler {
Log.w(this::class.simpleName, "Exception caught: ", it)
snackbarMessageState = Error(it.message)
}
}

@Composable
fun createSnackBarHost(
snackBar: (@Composable (message: String, icon: ImageVector?, containerColor: Color) -> Unit)? = null
) {
val snackBarHostState = remember {
SnackbarHostState()
}

val context = LocalContext.current

LaunchedEffect(key1 = snackbarMessageState) {
if (snackbarMessageState !is Empty) {
snackBarHostState.showSnackbar(snackbarMessageState.getMessage(context) ?: "Unknown error")
setCoroutineException(null)
snackbarMessageState = Empty
}
}

SnackbarHost(hostState = snackBarHostState) { snackBarData ->
AnimatedVisibility(visible = snackbarMessageState.isNotEmpty()) {
val icon = snackbarMessageState.icon
val message = snackBarData.visuals.message
val containerColor = snackbarMessageState.containerColor ?: Color.Transparent

snackBar?.invoke(message, icon, containerColor)
?: BaseSnackbar(
icon = icon,
message = message,
containerColor = containerColor
)
}
}
}


fun showSnackbar(snackbarMessage: SnackbarMessage) = vmScope.launch {
snackbarMessageState = snackbarMessage
}

fun showSnackbar(message: String?) = vmScope.launch {
snackbarMessageState = Error(message = message ?: return@launch)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package be.appwise.compose.core.ui.base.snackbar

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Snackbar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex

@Composable
fun BaseSnackbar(
message: String,
containerColor: Color,
icon: ImageVector? = null,
) {
Snackbar(
containerColor = containerColor,
modifier = Modifier.padding(8.dp).zIndex(100f),
shape = RoundedCornerShape(0.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
if(icon != null) {
Icon(
icon,
tint = Color.White,
contentDescription = null
)
}
Text(
message,
color = Color.White,
modifier = Modifier
.padding(start = 8.dp)
.weight(1f)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package be.appwise.compose.core.ui.base.snackbar

import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Warning
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import java.io.Serializable


open class SnackbarMessage(
open val message: String? = null,
open val icon: ImageVector? = null,
open val containerColor: Color? = Color.Transparent,
open vararg val formatParams: Any?
) : Serializable {

fun getMessage(context: Context) = when {
message != null -> message
else -> null
}

fun isNotEmpty() = message != null
}

data object Empty : SnackbarMessage()

class Success(
override val message: String? = null,
override val icon: ImageVector = Icons.Default.Check,
override val containerColor: Color = Color.Green,
override vararg val formatParams: Any?
) : SnackbarMessage(
message = message,
icon = icon,
containerColor = containerColor,
*formatParams
)

class Error(
override val message: String? = null,
override val icon: ImageVector = Icons.Default.Warning,
override val containerColor: Color = Color.Red,
override vararg val formatParams: Any?
) : SnackbarMessage(
message = message,
icon = icon,
containerColor = containerColor,
*formatParams
)
19 changes: 10 additions & 9 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ plugins {
}

android {
compileSdk = 34
compileSdk = 35

defaultConfig {
minSdk = 21
minSdk = 26

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
Expand Down Expand Up @@ -44,11 +44,9 @@ android {
dependencies {
implementation(libs.startup.runtime)

testImplementation(libs.mockk)
testImplementation(libs.junit)
androidTestImplementation(libs.junit.ext)
androidTestImplementation(libs.test.runner)
androidTestImplementation(libs.espresso.core)
// https://github.com/Ereza/CustomActivityOnCrash
implementation(libs.customactivityoncrash)

api(libs.core.ktx)

api(libs.multidex)
Expand Down Expand Up @@ -98,8 +96,11 @@ dependencies {
// Time manipulation for Java 7
api(libs.joda.time)

// https://github.com/Ereza/CustomActivityOnCrash
implementation(libs.customactivityoncrash)
testImplementation(libs.mockk)
testImplementation(libs.junit)
androidTestImplementation(libs.junit.ext)
androidTestImplementation(libs.test.runner)
androidTestImplementation(libs.espresso.core)
}

val sourceJar: Task by tasks.creating(Jar::class) {
Expand Down
9 changes: 8 additions & 1 deletion core/src/main/java/be/appwise/core/ui/base/BaseActivity.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package be.appwise.core.ui.base

import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.WindowInsetsControllerCompat
import be.appwise.core.R
import be.appwise.core.extensions.activity.showSnackbar
import com.orhanobut.logger.Logger
Expand Down Expand Up @@ -52,4 +54,9 @@ open class BaseActivity : AppCompatActivity() {
showSnackbar(throwable.message ?: getString(R.string.error_default))
Logger.t("BaseActivity").e(throwable, throwable.message ?: getString(R.string.error_default))
}
}

fun setStatusBarColor(@ColorRes statusBarColor: Int, isDarkMode: Boolean = false) {
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = !isDarkMode
window.statusBarColor = ContextCompat.getColor(this, statusBarColor)
}
}
Loading