Skip to content

Commit

Permalink
Merge pull request #1 from Nek-12/ci
Browse files Browse the repository at this point in the history
Configured CI
  • Loading branch information
Nek-12 authored Jun 7, 2022
2 parents c650919 + d6d45b1 commit efeec99
Show file tree
Hide file tree
Showing 37 changed files with 283 additions and 171 deletions.
16 changes: 16 additions & 0 deletions .github/ci-gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 -XX:+UseParallelGC
android.useAndroidX=true
kotlin.code.style=official
org.gradle.caching=true
org.gradle.parallel=true
android.enableR8.fullMode=true
org.gradle.configureondemand=true
org.gradle.unsafe.configuration-cache=false
android.enableJetifier=false
kotlin.incremental.usePreciseJavaTracking=true
android.nonTransitiveRClass=true
warningsAsErrors=true
org.gradle.daemon=false
org.gradle.workers.max=2
#kotlin.incremental=false
#kotlin.compiler.execution.strategy=in-process
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
# push:
# branches: [ master ]
pull_request:
branches: [ master ]

workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties

- name: set up JDK
uses: actions/setup-java@v1
with:
java-version: 17

- name: Make files executable
run: chmod +x ./gradlew && chmod +x ./scripts/checksum.sh

- name: Generate cache key
run: ./scripts/checksum.sh checksum.txt

- name: Create local properties
env:
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
run: echo "$LOCAL_PROPERTIES" > local.properties

- uses: actions/cache@v2
with:
path: |
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
~/.gradle/caches/build-cache-*
key: gradle-${{ hashFiles('checksum.txt') }}

# - name: Lint
# run: ./gradlew lintDebug --stacktrace

- name: Run detekt
run: ./gradlew detektAll

- name: Build debug
run: ./gradlew assembleDebug --stacktrace

- name: Build release
run: ./gradlew assembleRelease --stacktrace

- name: Unit tests
run: ./gradlew test --stacktrace
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ hs_err_pid*
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
Expand Down
1 change: 0 additions & 1 deletion android-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ android {
}
}


dependencies {
api(project(":android"))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.nek12.flowMVI.android.compose

import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
Expand All @@ -11,12 +10,14 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

//credits: https://proandroiddev.com/how-to-collect-flows-lifecycle-aware-in-jetpack-compose-babd53582d0b
// credits: https://proandroiddev.com/how-to-collect-flows-lifecycle-aware-in-jetpack-compose-babd53582d0b

@Composable
fun <T> rememberLifecycleFlow(
Expand All @@ -28,7 +29,7 @@ fun <T> rememberLifecycleFlow(
}

@Composable
fun <T: R, R> Flow<T>.collectAsStateOnLifecycle(
fun <T : R, R> Flow<T>.collectAsStateOnLifecycle(
initial: R,
context: CoroutineContext = EmptyCoroutineContext,
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
Expand All @@ -45,13 +46,18 @@ fun <T> StateFlow<T>.collectAsStateOnLifecycle(
): State<T> = collectAsStateOnLifecycle(value, context, lifecycleState)

@Composable
@SuppressLint("ComposableNaming")
@Suppress("ComposableEventParameterNaming")
fun <T> Flow<T>.collectOnLifecycle(
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
consumer: suspend CoroutineScope.(T) -> Unit,
) {
val lifecycleFlow = rememberLifecycleFlow(this, lifecycleState)
LaunchedEffect(lifecycleFlow) {
lifecycleFlow.collect { consumer(it) }
// see [LifecycleOwner.subscribe] in :android for reasoning
withContext(Dispatchers.Main.immediate) {
lifecycleFlow.collect {
consumer(it)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,46 @@
package com.nek12.flowMVI.android.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.lifecycle.Lifecycle
import com.nek12.flowMVI.MVIAction
import com.nek12.flowMVI.MVIIntent
import com.nek12.flowMVI.MVIProvider
import com.nek12.flowMVI.MVIState
import kotlinx.coroutines.Dispatchers

@Composable
/**
* A function that introduces MVIIntentScope to the content and ensures safe lifecycle-aware and efficient collection
* of states and actions.
* Usage:
* ```
* @Composable
* fun HomeScreen() = MVIComposable(getViewModel<HomeViewModel>()) { state ->
* consume { action ->
* when(action) {
* /*...*/
* }
* }
* when(state) {
* //use state to render content
* }
* }
* ```
* @param provider an MVIProvider (usually a viewModel) that handles this screen's logic
* @param lifecycleState the minimum lifecycle state, in which the activity must be to receive actions/states
* @param content the actual screen content. Will be recomposed each time you receive a new state
* @param content the actual screen content. Will be recomposed each time a new state is received.
*/
fun <S: MVIState, I: MVIIntent, A: MVIAction, VM: MVIProvider<S, I, A>> MVIComposable(
@Composable
fun <S : MVIState, I : MVIIntent, A : MVIAction, VM : MVIProvider<S, I, A>> MVIComposable(
provider: VM,
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
content: @Composable MVIIntentScope<I, A>.(state: S) -> Unit,
) {

val scope = rememberScope(provider, lifecycleState)

val state by provider.states.collectAsStateOnLifecycle(lifecycleState = lifecycleState)
// see [LifecycleOwner.subscribe] in :android for reasoning behind the dispatcher
val state by provider.states.collectAsStateOnLifecycle(Dispatchers.Main.immediate, lifecycleState)

content(scope, state)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
@file:SuppressLint("ComposableNaming")
@file:Suppress("ComposableNaming", "ComposableEventParameterNaming")

package com.nek12.flowMVI.android.compose

import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
Expand All @@ -17,7 +16,7 @@ import kotlinx.coroutines.CoroutineScope
* An interface for the scope that provides magic [send] and [consume] functions inside your composable
*/
@Stable
interface MVIIntentScope<in I: MVIIntent, out A: MVIAction> {
interface MVIIntentScope<in I : MVIIntent, out A : MVIAction> {

/**
* Send a new intent for the provider you used in [MVIComposable] {
Expand All @@ -32,15 +31,15 @@ interface MVIIntentScope<in I: MVIIntent, out A: MVIAction> {
}

@Composable
internal fun <S: MVIState, I: MVIIntent, A: MVIAction> rememberScope(
internal fun <S : MVIState, I : MVIIntent, A : MVIAction> rememberScope(
provider: MVIProvider<S, I, A>,
lifecycleState: Lifecycle.State,
): MVIIntentScope<I, A> = remember(provider, lifecycleState) { MVIIntentScopeImpl(provider, lifecycleState) }

private class MVIIntentScopeImpl<in I: MVIIntent, out A: MVIAction>(
private class MVIIntentScopeImpl<in I : MVIIntent, out A : MVIAction>(
private val provider: MVIProvider<*, I, A>,
private val lifecycleState: Lifecycle.State,
): MVIIntentScope<I, A> {
) : MVIIntentScope<I, A> {

override fun send(intent: I) = provider.send(intent)

Expand All @@ -51,7 +50,7 @@ private class MVIIntentScopeImpl<in I: MVIIntent, out A: MVIAction>(
}

@Composable
fun <A: MVIAction> MVIProvider<*, *, A>.consume(
fun <A : MVIAction> MVIProvider<*, *, A>.consume(
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
onAction: suspend CoroutineScope.(action: A) -> Unit,
) = actions.collectOnLifecycle(lifecycleState, onAction)
Expand All @@ -62,11 +61,11 @@ fun <A: MVIAction> MVIProvider<*, *, A>.consume(
*/
@Suppress("UNCHECKED_CAST")
@Composable
fun <T: MVIIntent, A: MVIAction> EmptyScope(
fun <T : MVIIntent, A : MVIAction> EmptyScope(
@BuilderInference call: @Composable MVIIntentScope<T, A>.() -> Unit,
) = call(EmptyScopeImpl as MVIIntentScope<T, A>)

private object EmptyScopeImpl: MVIIntentScope<MVIIntent, MVIAction> {
private object EmptyScopeImpl : MVIIntentScope<MVIIntent, MVIAction> {

override fun send(intent: MVIIntent) = Unit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import com.nek12.flowMVI.MVIState
import com.nek12.flowMVI.MVIView
import com.nek12.flowMVI.android.subscribe


/**
* Subscribe to the [provider] lifecycle-aware. Call this in [Fragment.onViewCreated]
* @param consume called on each new action. Implement action handling here.
* @param render called each time the state changes. Render state here.
* @param lifecycleState the minimum lifecycle state the [LifecycleOwner] must be in to receive updates.
* @see repeatOnLifecycle
*/
inline fun <S: MVIState, I: MVIIntent, A: MVIAction> Fragment.subscribe(
inline fun <S : MVIState, I : MVIIntent, A : MVIAction> Fragment.subscribe(
provider: MVIProvider<S, I, A>,
crossinline consume: (action: A) -> Unit,
crossinline render: (state: S) -> Unit,
Expand All @@ -31,6 +30,6 @@ inline fun <S: MVIState, I: MVIIntent, A: MVIAction> Fragment.subscribe(
* @param lifecycleState the minimum lifecycle state the [LifecycleOwner] must be in to receive updates.
* @see repeatOnLifecycle
*/
fun <S: MVIState, I: MVIIntent, A: MVIAction, T> T.subscribe(
fun <S : MVIState, I : MVIIntent, A : MVIAction, T> T.subscribe(
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
) where T: Fragment, T: MVIView<S, I, A> = viewLifecycleOwner.subscribe(provider, ::consume, ::render, lifecycleState)
) where T : Fragment, T : MVIView<S, I, A> = viewLifecycleOwner.subscribe(provider, ::consume, ::render, lifecycleState)
16 changes: 8 additions & 8 deletions android/src/main/kotlin/com/nek12/flowMVI/android/MVIExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ import kotlinx.coroutines.withContext
* @param lifecycleState the minimum lifecycle state the [LifecycleOwner] must be in to receive updates.
* @see repeatOnLifecycle
*/
inline fun <S: MVIState, I: MVIIntent, A: MVIAction> LifecycleOwner.subscribe(
inline fun <S : MVIState, I : MVIIntent, A : MVIAction> LifecycleOwner.subscribe(
provider: MVIProvider<S, I, A>,
crossinline consume: (action: A) -> Unit,
crossinline render: (state: S) -> Unit,
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
) = lifecycleScope.launch {

//using multiple repeatOnLifecycle instead of flowWithLifecycle to avoid creating hot flows
// using multiple repeatOnLifecycle instead of flowWithLifecycle to avoid creating hot flows

//https://github.com/Kotlin/kotlinx.coroutines/issues/2886
//TL;DR: uses immediate dispatcher to circumvent prompt cancellation fallacy (and missed events)
// https://github.com/Kotlin/kotlinx.coroutines/issues/2886
// TL;DR: uses immediate dispatcher to circumvent prompt cancellation fallacy (and missed events)
withContext(Dispatchers.Main.immediate) {
launch {
repeatOnLifecycle(lifecycleState) {
Expand All @@ -52,16 +52,16 @@ inline fun <S: MVIState, I: MVIIntent, A: MVIAction> LifecycleOwner.subscribe(
* @param lifecycleState the minimum lifecycle state the [LifecycleOwner] must be in to receive updates.
* @see repeatOnLifecycle
*/
fun <S: MVIState, I: MVIIntent, A: MVIAction, T> T.subscribe(
fun <S : MVIState, I : MVIIntent, A : MVIAction, T> T.subscribe(
provider: MVIProvider<S, I, A>,
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
) where T: LifecycleOwner, T: MVISubscriber<S, A> = subscribe(provider, ::consume, ::render, lifecycleState)
) where T : LifecycleOwner, T : MVISubscriber<S, A> = subscribe(provider, ::consume, ::render, lifecycleState)

/**
* Subscribe to the provider lifecycle-aware.
* @param lifecycleState the minimum lifecycle state the [LifecycleOwner] must be in to receive updates.
* @see repeatOnLifecycle
*/
fun <S: MVIState, I: MVIIntent, A: MVIAction, T> T.subscribe(
fun <S : MVIState, I : MVIIntent, A : MVIAction, T> T.subscribe(
lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
) where T: LifecycleOwner, T: MVIView<S, I, A> = subscribe(provider, lifecycleState)
) where T : LifecycleOwner, T : MVIView<S, I, A> = subscribe(provider, lifecycleState)
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ import kotlin.coroutines.EmptyCoroutineContext
* @See MVIView
* @See MVIProvider
*/
abstract class MVIViewModel<S: MVIState, I: MVIIntent, A: MVIAction>(
abstract class MVIViewModel<S : MVIState, I : MVIIntent, A : MVIAction>(
initialState: S,
): ViewModel(), MVIProvider<S, I, A> {
) : ViewModel(), MVIProvider<S, I, A> {

/**
* [reduce] will be launched sequentially, on main thread, for each intent that comes from the view.
Expand Down Expand Up @@ -96,7 +96,7 @@ abstract class MVIViewModel<S: MVIState, I: MVIIntent, A: MVIAction>(
/**
* Execute [block] if current state is [T] else just return [currentState].
*/
protected inline fun <reified T: S> withState(block: T.() -> S): S {
protected inline fun <reified T : S> withState(block: T.() -> S): S {
return (currentState as? T)?.let(block) ?: currentState
}
}
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ android {
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
// proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import android.app.Application
import com.nek12.flowMVI.sample.di.appModule
import org.koin.core.context.startKoin

class MVIApplication: Application() {
class MVIApplication : Application() {

override fun onCreate() {
super.onCreate()
Expand Down
Loading

0 comments on commit efeec99

Please sign in to comment.