Skip to content

Commit 7360592

Browse files
Merge pull request #338 from wisemen-digital/feature/base-classes-v2
Feature/base classes v2
2 parents cfd809c + 3a2dd08 commit 7360592

26 files changed

+433
-102
lines changed

build.gradle.kts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,8 @@
1-
// Top-level build file where you can add configuration options common to all sub-projects/modules.
2-
buildscript {
3-
dependencies {
4-
classpath(libs.gradle)
5-
classpath(kotlin("gradle-plugin", version = libs.versions.kotlin.get()))
6-
7-
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:${libs.versions.navigation.get()}")
8-
9-
// NOTE: Do not place your application dependencies here; they belong
10-
// in the individual module build.gradle files
11-
}
12-
}
13-
141
plugins {
15-
alias(libs.plugins.android.library) apply false
162
alias(libs.plugins.kotlin.android) apply false
173
alias(libs.plugins.android.application) apply false
184
alias(libs.plugins.ksp) apply false
5+
alias(libs.plugins.kotlin.compose) apply false
196
}
207

218
project.tasks.register("clean", Delete::class) {

core-compose/build.gradle.kts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
plugins {
22
id("com.android.library")
33
id("org.jetbrains.kotlin.android")
4+
alias(libs.plugins.kotlin.compose)
45
}
56

67
android {
78
namespace = "be.appwise.compose.core"
8-
compileSdk = 34
9+
compileSdk = 35
910

1011
defaultConfig {
11-
minSdk = 21
12+
minSdk = 26
1213

1314
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1415
consumerProguardFiles("consumer-rules.pro")
@@ -30,14 +31,25 @@ android {
3031
kotlinOptions {
3132
jvmTarget = "17"
3233
}
34+
buildFeatures {
35+
compose = true
36+
}
3337
}
3438

3539
dependencies {
40+
implementation(project(":core"))
41+
42+
implementation(libs.core.ktx)
43+
implementation(libs.appcompat)
44+
implementation(libs.material)
45+
46+
implementation(libs.material3)
3647

37-
implementation("androidx.core:core-ktx:1.12.0")
38-
implementation("androidx.appcompat:appcompat:1.6.1")
39-
implementation("com.google.android.material:material:1.10.0")
40-
testImplementation("junit:junit:4.13.2")
41-
androidTestImplementation("androidx.test.ext:junit:1.1.5")
42-
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
48+
implementation(platform(libs.compose.bom))
49+
implementation(libs.ui)
50+
implementation(libs.ui.graphics)
51+
implementation(libs.ui.tooling)
52+
implementation(libs.ui.tooling.preview)
53+
debugImplementation(libs.ui.test.manifest)
54+
implementation(libs.ui.test.junit4)
4355
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package be.appwise.compose.core.ui.base
2+
3+
import kotlinx.coroutines.channels.Channel
4+
import kotlinx.coroutines.flow.MutableStateFlow
5+
import kotlinx.coroutines.flow.asStateFlow
6+
import kotlinx.coroutines.flow.receiveAsFlow
7+
8+
open class BaseComposeStateViewModel<UIState, NavigationType>(initialState: UIState): BaseComposeViewModel() {
9+
private val _uiState = MutableStateFlow(initialState)
10+
val uiState = _uiState.asStateFlow()
11+
12+
private val _navigation = Channel<NavigationType>()
13+
val navigation = _navigation.receiveAsFlow()
14+
15+
fun updateUIState(state: UIState) {
16+
_uiState.tryEmit(state)
17+
}
18+
19+
fun navigate(navigation: NavigationType) {
20+
_navigation.trySend(navigation)
21+
}
22+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package be.appwise.compose.core.ui.base
2+
3+
import android.util.Log
4+
import androidx.compose.animation.AnimatedVisibility
5+
import androidx.compose.material3.SnackbarHost
6+
import androidx.compose.material3.SnackbarHostState
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.runtime.LaunchedEffect
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.mutableStateOf
11+
import androidx.compose.runtime.remember
12+
import androidx.compose.runtime.setValue
13+
import androidx.compose.ui.graphics.Color
14+
import androidx.compose.ui.graphics.vector.ImageVector
15+
import androidx.compose.ui.platform.LocalContext
16+
import be.appwise.compose.core.ui.base.snackbar.BaseSnackbar
17+
import be.appwise.compose.core.ui.base.snackbar.Empty
18+
import be.appwise.compose.core.ui.base.snackbar.Error
19+
import be.appwise.compose.core.ui.base.snackbar.SnackbarMessage
20+
import be.appwise.core.ui.base.BaseViewModel
21+
import kotlinx.coroutines.CoroutineScope
22+
import kotlinx.coroutines.launch
23+
24+
open class BaseComposeViewModel: BaseViewModel() {
25+
// Snackbar Handling
26+
private var snackbarMessageState by mutableStateOf<SnackbarMessage>(Empty)
27+
28+
override fun vmScopeWithCustomExceptionHandler(onError: (error: Throwable) -> Unit): CoroutineScope {
29+
return super.vmScopeWithCustomExceptionHandler {
30+
Log.w(this::class.simpleName, "Exception caught: ", it)
31+
snackbarMessageState = Error(it.message)
32+
}
33+
}
34+
35+
@Composable
36+
fun createSnackBarHost(
37+
snackBar: (@Composable (message: String, icon: ImageVector?, containerColor: Color) -> Unit)? = null
38+
) {
39+
val snackBarHostState = remember {
40+
SnackbarHostState()
41+
}
42+
43+
val context = LocalContext.current
44+
45+
LaunchedEffect(key1 = snackbarMessageState) {
46+
if (snackbarMessageState !is Empty) {
47+
snackBarHostState.showSnackbar(snackbarMessageState.getMessage(context) ?: "Unknown error")
48+
setCoroutineException(null)
49+
snackbarMessageState = Empty
50+
}
51+
}
52+
53+
SnackbarHost(hostState = snackBarHostState) { snackBarData ->
54+
AnimatedVisibility(visible = snackbarMessageState.isNotEmpty()) {
55+
val icon = snackbarMessageState.icon
56+
val message = snackBarData.visuals.message
57+
val containerColor = snackbarMessageState.containerColor ?: Color.Transparent
58+
59+
snackBar?.invoke(message, icon, containerColor)
60+
?: BaseSnackbar(
61+
icon = icon,
62+
message = message,
63+
containerColor = containerColor
64+
)
65+
}
66+
}
67+
}
68+
69+
70+
fun showSnackbar(snackbarMessage: SnackbarMessage) = vmScope.launch {
71+
snackbarMessageState = snackbarMessage
72+
}
73+
74+
fun showSnackbar(message: String?) = vmScope.launch {
75+
snackbarMessageState = Error(message = message ?: return@launch)
76+
}
77+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package be.appwise.compose.core.ui.base.snackbar
2+
3+
import androidx.compose.foundation.layout.Row
4+
import androidx.compose.foundation.layout.padding
5+
import androidx.compose.foundation.shape.RoundedCornerShape
6+
import androidx.compose.material3.Icon
7+
import androidx.compose.material3.Snackbar
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.Alignment
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.graphics.Color
13+
import androidx.compose.ui.graphics.vector.ImageVector
14+
import androidx.compose.ui.unit.dp
15+
import androidx.compose.ui.zIndex
16+
17+
@Composable
18+
fun BaseSnackbar(
19+
message: String,
20+
containerColor: Color,
21+
icon: ImageVector? = null,
22+
) {
23+
Snackbar(
24+
containerColor = containerColor,
25+
modifier = Modifier.padding(8.dp).zIndex(100f),
26+
shape = RoundedCornerShape(0.dp)
27+
) {
28+
Row(
29+
verticalAlignment = Alignment.CenterVertically
30+
) {
31+
if(icon != null) {
32+
Icon(
33+
icon,
34+
tint = Color.White,
35+
contentDescription = null
36+
)
37+
}
38+
Text(
39+
message,
40+
color = Color.White,
41+
modifier = Modifier
42+
.padding(start = 8.dp)
43+
.weight(1f)
44+
)
45+
}
46+
}
47+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package be.appwise.compose.core.ui.base.snackbar
2+
3+
import android.content.Context
4+
import androidx.compose.material.icons.Icons
5+
import androidx.compose.material.icons.filled.Check
6+
import androidx.compose.material.icons.filled.Warning
7+
import androidx.compose.ui.graphics.Color
8+
import androidx.compose.ui.graphics.vector.ImageVector
9+
import java.io.Serializable
10+
11+
12+
open class SnackbarMessage(
13+
open val message: String? = null,
14+
open val icon: ImageVector? = null,
15+
open val containerColor: Color? = Color.Transparent,
16+
open vararg val formatParams: Any?
17+
) : Serializable {
18+
19+
fun getMessage(context: Context) = when {
20+
message != null -> message
21+
else -> null
22+
}
23+
24+
fun isNotEmpty() = message != null
25+
}
26+
27+
data object Empty : SnackbarMessage()
28+
29+
class Success(
30+
override val message: String? = null,
31+
override val icon: ImageVector = Icons.Default.Check,
32+
override val containerColor: Color = Color.Green,
33+
override vararg val formatParams: Any?
34+
) : SnackbarMessage(
35+
message = message,
36+
icon = icon,
37+
containerColor = containerColor,
38+
*formatParams
39+
)
40+
41+
class Error(
42+
override val message: String? = null,
43+
override val icon: ImageVector = Icons.Default.Warning,
44+
override val containerColor: Color = Color.Red,
45+
override vararg val formatParams: Any?
46+
) : SnackbarMessage(
47+
message = message,
48+
icon = icon,
49+
containerColor = containerColor,
50+
*formatParams
51+
)

core/build.gradle.kts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ plugins {
88
}
99

1010
android {
11-
compileSdk = 34
11+
compileSdk = 35
1212

1313
defaultConfig {
14-
minSdk = 21
14+
minSdk = 26
1515

1616
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1717
consumerProguardFiles("consumer-rules.pro")
@@ -44,11 +44,9 @@ android {
4444
dependencies {
4545
implementation(libs.startup.runtime)
4646

47-
testImplementation(libs.mockk)
48-
testImplementation(libs.junit)
49-
androidTestImplementation(libs.junit.ext)
50-
androidTestImplementation(libs.test.runner)
51-
androidTestImplementation(libs.espresso.core)
47+
// https://github.com/Ereza/CustomActivityOnCrash
48+
implementation(libs.customactivityoncrash)
49+
5250
api(libs.core.ktx)
5351

5452
api(libs.multidex)
@@ -98,8 +96,11 @@ dependencies {
9896
// Time manipulation for Java 7
9997
api(libs.joda.time)
10098

101-
// https://github.com/Ereza/CustomActivityOnCrash
102-
implementation(libs.customactivityoncrash)
99+
testImplementation(libs.mockk)
100+
testImplementation(libs.junit)
101+
androidTestImplementation(libs.junit.ext)
102+
androidTestImplementation(libs.test.runner)
103+
androidTestImplementation(libs.espresso.core)
103104
}
104105

105106
val sourceJar: Task by tasks.creating(Jar::class) {

core/src/main/java/be/appwise/core/ui/base/BaseActivity.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package be.appwise.core.ui.base
22

3+
import androidx.annotation.ColorRes
34
import androidx.annotation.DrawableRes
45
import androidx.annotation.StringRes
56
import androidx.appcompat.app.AppCompatActivity
67
import androidx.appcompat.widget.Toolbar
78
import androidx.core.content.ContextCompat
9+
import androidx.core.view.WindowInsetsControllerCompat
810
import be.appwise.core.R
911
import be.appwise.core.extensions.activity.showSnackbar
1012
import com.orhanobut.logger.Logger
@@ -52,4 +54,9 @@ open class BaseActivity : AppCompatActivity() {
5254
showSnackbar(throwable.message ?: getString(R.string.error_default))
5355
Logger.t("BaseActivity").e(throwable, throwable.message ?: getString(R.string.error_default))
5456
}
55-
}
57+
58+
fun setStatusBarColor(@ColorRes statusBarColor: Int, isDarkMode: Boolean = false) {
59+
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = !isDarkMode
60+
window.statusBarColor = ContextCompat.getColor(this, statusBarColor)
61+
}
62+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package be.appwise.core.ui.base
2+
3+
import android.app.Application
4+
import be.appwise.core.BuildConfig
5+
import be.appwise.core.core.CoreApp
6+
import com.orhanobut.hawk.Hawk
7+
8+
class BaseApp: Application() {
9+
fun init(appName: String) {
10+
Hawk.init(this).build()
11+
12+
initCore(appName)
13+
}
14+
15+
private fun initCore(appName: String) {
16+
CoreApp.init {
17+
initializeLogger(appName, BuildConfig.DEBUG)
18+
19+
if(BuildConfig.DEBUG) {
20+
initializeErrorActivity(true)
21+
}
22+
}
23+
}
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package be.appwise.core.ui.base
2+
3+
import android.os.Bundle
4+
import androidx.annotation.LayoutRes
5+
import androidx.databinding.DataBindingUtil
6+
import androidx.databinding.ViewDataBinding
7+
8+
abstract class BaseBindingActivity<B : ViewDataBinding>: BaseActivity() {
9+
protected lateinit var mBinding: B
10+
private set
11+
12+
@LayoutRes
13+
protected abstract fun getLayout(): Int
14+
15+
override fun onCreate(savedInstanceState: Bundle?) {
16+
mBinding = DataBindingUtil.setContentView(this, getLayout())
17+
mBinding.lifecycleOwner = this
18+
super.onCreate(savedInstanceState)
19+
}
20+
}

0 commit comments

Comments
 (0)