From 48be99955939202bd6f1d3c2f575b986b9c04dce Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 11:10:19 +0300 Subject: [PATCH 01/26] update code highlights, remove code highlights fork --- sample/build.gradle.kts | 2 +- sample/libs.versions.toml | 5 +- .../dev/snipme/highlights/Highlights.kt | 91 ---------- .../highlights/internal/CodeAnalyzer.kt | 123 -------------- .../highlights/internal/CodeComparator.kt | 53 ------ .../snipme/highlights/internal/Extensions.kt | 59 ------- .../highlights/internal/SyntaxTokens.kt | 156 ------------------ .../internal/locator/AnnotationLocator.kt | 33 ---- .../internal/locator/CommentLocator.kt | 23 --- .../internal/locator/KeywordLocator.kt | 47 ------ .../internal/locator/LinkLocator.kt | 20 --- .../internal/locator/MarkLocator.kt | 21 --- .../locator/MultilineCommentLocator.kt | 35 ---- .../internal/locator/NumericLiteralLocator.kt | 115 ------------- .../internal/locator/PunctuationLocator.kt | 26 --- .../internal/locator/StringLocator.kt | 38 ----- .../internal/locator/TokenLocator.kt | 24 --- .../snipme/highlights/model/CodeHighlight.kt | 8 - .../snipme/highlights/model/CodeStructure.kt | 99 ----------- .../snipme/highlights/model/SyntaxLanguage.kt | 30 ---- .../snipme/highlights/model/SyntaxTheme.kt | 43 ----- .../snipme/highlights/model/SyntaxThemes.kt | 154 ----------------- .../flowmvi/sample/ui/widgets/CodeView.kt | 58 +++---- 23 files changed, 24 insertions(+), 1239 deletions(-) delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/CodeAnalyzer.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/CodeComparator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/SyntaxTokens.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/AnnotationLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/CommentLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/KeywordLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/LinkLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MarkLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MultilineCommentLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/NumericLiteralLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/PunctuationLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/StringLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/TokenLocator.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/model/CodeHighlight.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/model/CodeStructure.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxLanguage.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxTheme.kt delete mode 100644 sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxThemes.kt diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 9cc45dfd..5b20365a 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -88,7 +88,7 @@ kotlin { implementation(applibs.bundles.koin) implementation(applibs.apiresult) implementation(applibs.decompose.compose) - // implementation(applibs.compose.codehighlighting) + implementation(applibs.compose.codehighlighting) implementation(applibs.decompose) implementation(projects.core) diff --git a/sample/libs.versions.toml b/sample/libs.versions.toml index 99a27ab3..195ab610 100644 --- a/sample/libs.versions.toml +++ b/sample/libs.versions.toml @@ -8,7 +8,7 @@ activity = "1.9.0" okio = "3.9.0" splashscreen = "1.1.0-rc01" xml-constraintlayout = "2.2.0-alpha13" -#codehighlights = "0.8.0" +codehighlights = "0.9.0" [libraries] decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } @@ -31,8 +31,7 @@ androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = " androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" } view-material = { module = "com.google.android.material:material", version.ref = "material" } view-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "xml-constraintlayout" } - -#compose-codehighlighting = { module = "dev.snipme:highlights", version.ref = "codehighlights" } +compose-codehighlighting = { module = "dev.snipme:highlights", version.ref = "codehighlights" } [bundles] koin = [ diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt deleted file mode 100644 index 636c91ce..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt +++ /dev/null @@ -1,91 +0,0 @@ -package dev.snipme.highlights - -import dev.snipme.highlights.internal.CodeAnalyzer -import dev.snipme.highlights.internal.CodeSnapshot -import dev.snipme.highlights.model.BoldHighlight -import dev.snipme.highlights.model.CodeHighlight -import dev.snipme.highlights.model.CodeStructure -import dev.snipme.highlights.model.ColorHighlight -import dev.snipme.highlights.model.PhraseLocation -import dev.snipme.highlights.model.SyntaxLanguage -import dev.snipme.highlights.model.SyntaxTheme -import dev.snipme.highlights.model.SyntaxThemes - -class Highlights private constructor( - private var code: String, - private val language: SyntaxLanguage, - private val theme: SyntaxTheme, - private var emphasisLocations: List -) { - var snapshot: CodeSnapshot? = null - private set - - companion object { - fun default() = fromBuilder(Builder()) - - fun fromBuilder(builder: Builder) = builder.build() - - fun themes(darkMode: Boolean) = SyntaxThemes.themes(darkMode) - - fun languages() = SyntaxLanguage.entries - } - - @Suppress("DataClassShouldBeImmutable") - data class Builder( - var code: String = "", - var language: SyntaxLanguage = SyntaxLanguage.DEFAULT, - var theme: SyntaxTheme = SyntaxThemes.default(), - var emphasisLocations: List = emptyList(), - ) { - fun code(code: String) = apply { this.code = code } - fun language(language: SyntaxLanguage) = apply { this.language = language } - fun theme(theme: SyntaxTheme) = apply { this.theme = theme } - fun emphasis(vararg locations: PhraseLocation) = - apply { this.emphasisLocations = locations.toList() } - - fun build() = Highlights(code, language, theme, emphasisLocations) - } - - fun setCode(code: String) { - this.code = code - } - - fun setEmphasis(vararg locations: PhraseLocation) { - this.emphasisLocations = locations.toList() - } - - fun getCodeStructure(): CodeStructure { - val structure = CodeAnalyzer.analyze(code, language, snapshot) - snapshot = CodeSnapshot(code, structure, language) - return structure - } - - fun getHighlights(): List { - val highlights = mutableListOf() - val structure = getCodeStructure() - with(structure) { - marks.forEach { highlights.add(ColorHighlight(it, theme.mark)) } - punctuations.forEach { highlights.add(ColorHighlight(it, theme.punctuation)) } - keywords.forEach { highlights.add(ColorHighlight(it, theme.keyword)) } - strings.forEach { highlights.add(ColorHighlight(it, theme.string)) } - literals.forEach { highlights.add(ColorHighlight(it, theme.literal)) } - annotations.forEach { highlights.add(ColorHighlight(it, theme.metadata)) } - comments.forEach { highlights.add(ColorHighlight(it, theme.comment)) } - multilineComments.forEach { highlights.add(ColorHighlight(it, theme.multilineComment)) } - } - - emphasisLocations.forEach { highlights.add(BoldHighlight(it)) } - - return highlights - } - - fun getBuilder() = Builder(code, language, theme, emphasisLocations) - - fun getCode() = code - - fun getLanguage() = language - - fun getTheme() = theme - - fun getEmphasis() = emphasisLocations -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/CodeAnalyzer.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/CodeAnalyzer.kt deleted file mode 100644 index 66f0ef96..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/CodeAnalyzer.kt +++ /dev/null @@ -1,123 +0,0 @@ -package dev.snipme.highlights.internal - -import dev.snipme.highlights.internal.SyntaxTokens.ALL_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.ALL_MIXED_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.COFFEE_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.CPP_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.CSHARP_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.C_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.JAVA_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.JSCRIPT_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.KOTLIN_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.PERL_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.PYTHON_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.RUBY_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.RUST_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.SH_KEYWORDS -import dev.snipme.highlights.internal.SyntaxTokens.SWIFT_KEYWORDS -import dev.snipme.highlights.internal.locator.AnnotationLocator -import dev.snipme.highlights.internal.locator.CommentLocator -import dev.snipme.highlights.internal.locator.KeywordLocator -import dev.snipme.highlights.internal.locator.MarkLocator -import dev.snipme.highlights.internal.locator.MultilineCommentLocator -import dev.snipme.highlights.internal.locator.NumericLiteralLocator -import dev.snipme.highlights.internal.locator.PunctuationLocator -import dev.snipme.highlights.internal.locator.StringLocator -import dev.snipme.highlights.model.CodeStructure -import dev.snipme.highlights.model.SyntaxLanguage -import dev.snipme.highlights.model.SyntaxLanguage.C -import dev.snipme.highlights.model.SyntaxLanguage.COFFEESCRIPT -import dev.snipme.highlights.model.SyntaxLanguage.CPP -import dev.snipme.highlights.model.SyntaxLanguage.CSHARP -import dev.snipme.highlights.model.SyntaxLanguage.DEFAULT -import dev.snipme.highlights.model.SyntaxLanguage.JAVA -import dev.snipme.highlights.model.SyntaxLanguage.JAVASCRIPT -import dev.snipme.highlights.model.SyntaxLanguage.KOTLIN -import dev.snipme.highlights.model.SyntaxLanguage.MIXED -import dev.snipme.highlights.model.SyntaxLanguage.PERL -import dev.snipme.highlights.model.SyntaxLanguage.PYTHON -import dev.snipme.highlights.model.SyntaxLanguage.RUBY -import dev.snipme.highlights.model.SyntaxLanguage.RUST -import dev.snipme.highlights.model.SyntaxLanguage.SHELL -import dev.snipme.highlights.model.SyntaxLanguage.SWIFT - -data class CodeSnapshot( - val code: String, - val structure: CodeStructure, - val language: SyntaxLanguage, -) - -internal object CodeAnalyzer { - fun analyze( - code: String, - language: SyntaxLanguage = DEFAULT, - snapshot: CodeSnapshot? = null, - ): CodeStructure = - when { - snapshot == null -> analyzeFull(code, language) - language != snapshot.language -> analyzeFull(code, language) - code != snapshot.code -> analyzePartial(snapshot, code) - else -> snapshot.structure - } - - private fun analyzeFull(code: String, language: SyntaxLanguage) = analyzeForLanguage(code, language) - - private fun analyzePartial(codeSnapshot: CodeSnapshot, code: String): CodeStructure { - val difference = CodeComparator.difference(codeSnapshot.code, code) - val structure = when (difference) { - is CodeDifference.Increase -> { - val newStructure = analyzeForLanguage(difference.change, codeSnapshot.language) - codeSnapshot.structure + newStructure.move(codeSnapshot.code.length + 1) - } - - is CodeDifference.Decrease -> { - val newStructure = analyzeForLanguage(difference.change, codeSnapshot.language) - val lengthDifference = codeSnapshot.code.length - difference.change.length - codeSnapshot.structure - newStructure.move(lengthDifference) - } - - CodeDifference.None -> return codeSnapshot.structure - } - - return structure - } - - private fun analyzeForLanguage(code: String, language: SyntaxLanguage) = - when (language) { - DEFAULT -> analyzeCodeWithKeywords(code, ALL_KEYWORDS) - MIXED -> analyzeCodeWithKeywords(code, ALL_MIXED_KEYWORDS) - C -> analyzeCodeWithKeywords(code, C_KEYWORDS) - CPP -> analyzeCodeWithKeywords(code, CPP_KEYWORDS) - JAVA -> analyzeCodeWithKeywords(code, JAVA_KEYWORDS) - KOTLIN -> analyzeCodeWithKeywords(code, KOTLIN_KEYWORDS) - RUST -> analyzeCodeWithKeywords(code, RUST_KEYWORDS) - CSHARP -> analyzeCodeWithKeywords(code, CSHARP_KEYWORDS) - COFFEESCRIPT -> analyzeCodeWithKeywords(code, COFFEE_KEYWORDS) - JAVASCRIPT -> analyzeCodeWithKeywords(code, JSCRIPT_KEYWORDS) - PERL -> analyzeCodeWithKeywords(code, PERL_KEYWORDS) - PYTHON -> analyzeCodeWithKeywords(code, PYTHON_KEYWORDS) - RUBY -> analyzeCodeWithKeywords(code, RUBY_KEYWORDS) - SHELL -> analyzeCodeWithKeywords(code, SH_KEYWORDS) - SWIFT -> analyzeCodeWithKeywords(code, SWIFT_KEYWORDS) - } - - private fun analyzeCodeWithKeywords(code: String, keywords: List): CodeStructure { - val comments = CommentLocator.locate(code) - val multiLineComments = MultilineCommentLocator.locate(code) - val strings = StringLocator.locate(code) - - val plainTextRanges = comments + multiLineComments + strings - - return CodeStructure( - marks = MarkLocator.locate(code), - punctuations = PunctuationLocator.locate(code), - keywords = KeywordLocator.locate(code, keywords, plainTextRanges), - strings = strings, - literals = NumericLiteralLocator.locate(code), - comments = comments, - multilineComments = multiLineComments, - annotations = AnnotationLocator.locate(code), - incremental = false, - ) - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/CodeComparator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/CodeComparator.kt deleted file mode 100644 index 4e469792..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/CodeComparator.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.snipme.highlights.internal - -private const val WORDS_DELIMITER = " " - -internal sealed class CodeDifference { - data class Increase(val change: String) : CodeDifference() - data class Decrease(val change: String) : CodeDifference() - object None : CodeDifference() -} - -internal object CodeComparator { - fun difference(current: String, updated: String): CodeDifference { - val currentWords = current.tokenize() - val updatedWords = updated.tokenize() - - return when { - currentWords.size == updatedWords.size -> CodeDifference.None - - currentWords.size < updatedWords.size -> CodeDifference.Increase( - findDifference( - currentWords, - updatedWords, - isIncrease = true - ) - ) - - else -> CodeDifference.Decrease( - findDifference( - currentWords, - updatedWords, - isIncrease = false - ) - ) - } - } - - private fun findDifference( - current: List, - updated: List, - isIncrease: Boolean, - ): String { - val differentWords = if (isIncrease) { - updated - current - } else { - current - updated - } - - return differentWords.joinToString(WORDS_DELIMITER) - } - - private fun String.tokenize() = - this.split("\n").map { it.split(WORDS_DELIMITER) }.flatten() -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt deleted file mode 100644 index ab71067f..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt +++ /dev/null @@ -1,59 +0,0 @@ -package dev.snipme.highlights.internal - -fun String.indicesOf( - phrase: String, -): Set { - val indices = mutableSetOf() - - // No found - val startIndexOf = indexOf(phrase, 0) - if (startIndexOf < 0) { - return emptySet() - } - - indices.add(startIndexOf) - - // The found is the only one - if (startIndexOf == lastIndex - phrase.length) { - return indices - } - - var startingIndex = indexOf(phrase, startIndexOf + phrase.length) - - while (startingIndex > 0) { - indices.add(startingIndex) - startingIndex = indexOf(phrase, startingIndex + phrase.length) - } - - return indices -} - -fun Char.isNewLine(): Boolean { - val stringChar = this.toString() - return stringChar == "\n" || stringChar == "\r" || stringChar == "\r\n" -} - -fun String.lengthToEOF(start: Int = 0): Int { - if (all { it.isNewLine().not() }) return length - start - var endIndex = start - while (this.getOrNull(endIndex)?.isNewLine()?.not() == true) { - endIndex++ - } - return endIndex - start -} - -// TODO Create unit tests for this -// Sometimes keyword can be found in the middle of word. -// This returns information if index points only to the keyword -fun String.isIndependentPhrase( - code: String, - index: Int, -): Boolean { - if (index == 0) return true - if (index == code.lastIndex) return true - - val charBefore = code[maxOf(index - 1, 0)] - val charAfter = code[minOf(index + this.length, code.lastIndex)] - - return charBefore.isLetter().not() && charAfter.isDigit().not() -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/SyntaxTokens.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/SyntaxTokens.kt deleted file mode 100644 index 7726a83d..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/SyntaxTokens.kt +++ /dev/null @@ -1,156 +0,0 @@ -package dev.snipme.highlights.internal - -internal object SyntaxTokens { - val FLOW_CONTROL_KEYWORDS = "break,continue,do,else,for,if,return,while".split(",") - - val C_KEYWORDS = FLOW_CONTROL_KEYWORDS + ( - "auto,case,char,const,default," + - "double,enum,extern,float,goto,inline,int,long,register,short,signed," + - "sizeof,static,struct,switch,typedef,union,unsigned,void,volatile" - ).split(",") - - val COMMON_KEYWORDS = C_KEYWORDS + ( - "catch,class,delete,false,import," + - "new,operator,private,protected,public,this,throw,true,try,typeof" - ).split(",") - - val CPP_KEYWORDS = COMMON_KEYWORDS + ( - "alignof,align_union,asm,axiom,bool," + - "concept,concept_map,const_cast,constexpr,decltype,delegate," + - "dynamic_cast,explicit,export,friend,generic,late_check," + - "mutable,namespace,nullptr,property,reinterpret_cast,static_assert," + - "static_cast,template,typeid,typename,using,virtual,where" - ).split(",") - - val JAVA_KEYWORDS = COMMON_KEYWORDS + - ( - "abstract,assert,boolean,byte,extends,final,finally,implements,import," + - "instanceof,interface,null,native,package,strictfp,super,synchronized," + - "throws,transient" - ).split(",") - - val KOTLIN_KEYWORDS = JAVA_KEYWORDS + - ( - "as,as?,fun,in,!in,is,!is,object,typealias,val,var,when,by,constructor,delegate,dynamic," + - "file,get,init,set,value,where,actual,annotation,companion,crossinline,data,enum,expect," + - "external,field,infix,inline,inner,internal,lateinit,noinline,open,operator,out,override," + - "reified,sealed,suspend,tailrec,vararg" - ).split(",") - - val RUST_KEYWORDS = FLOW_CONTROL_KEYWORDS + ( - "as,assert,const,copy,drop," + - "enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv," + - "pub,pure,ref,self,static,struct,true,trait,type,unsafe,use" - ).split(",") - - val CSHARP_KEYWORDS = JAVA_KEYWORDS + - ( - "as,base,by,checked,decimal,delegate,descending,dynamic,event," + - "fixed,foreach,from,group,implicit,in,internal,into,is,let," + - "lock,object,out,override,orderby,params,partial,readonly,ref,sbyte," + - "sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort," + - "var,virtual,where" - ).split(",") - - val COFFEE_KEYWORDS = ( - "all,and,by,catch,class,else,extends,false,finally," + - "for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then," + - "throw,true,try,unless,until,when,while,yes" - ).split(",") - - val JSCRIPT_KEYWORDS = COMMON_KEYWORDS + - ( - "debugger,eval,export,function,get,null,set,undefined,var,with," + - "Infinity,NaN" - ).split(",") - - val PERL_KEYWORDS = ( - "caller,delete,die,do,dump,elsif,eval,exit,foreach,for," + - "goto,if,import,last,local,my,next,no,our,print,package,redo,require," + - "sub,undef,unless,until,use,wantarray,while,BEGIN,END" - ).split(",") - - val PYTHON_KEYWORDS = FLOW_CONTROL_KEYWORDS + - ( - "and,as,assert,class,def,del," + - "elif,except,exec,finally,from,global,import,in,is,lambda," + - "nonlocal,not,or,pass,print,raise,try,with,yield," + - "False,True,None" - ).split(",") - - val RUBY_KEYWORDS = FLOW_CONTROL_KEYWORDS + - ( - "alias,and,begin,case,class," + - "def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo," + - "rescue,retry,self,super,then,true,undef,unless,until,when,yield," + - "BEGIN,END" - ).split(",") - - val SH_KEYWORDS = FLOW_CONTROL_KEYWORDS + - ( - "case,done,elif,esac,eval,fi," + - "function,in,local,set,then,until" - ).split(",") - - val SWIFT_KEYWORDS = FLOW_CONTROL_KEYWORDS + "," + - "associatedtype,async,await,class,deinit,enum,extension,fileprivate," + - "func,import,init,inout,internal,let,open,operator,private,protocol,public,rethrows,static," + - "struct,subscript,typealias,andvar,case,default,defer,fallthrough," + - "guard,in,repeat,switch,where,as,Any,catch,false,is,nil,super,self,Self," + - "throw,throws,true,try,#available,#colorLiteral,#column,#else,#elseif,#endif,#error,#file," + - "#fileID,#fileLiteral,#filePath,#function,#if,#imageLiteral,#line,#selector,#sourceLocation," + - "#warning,associativity,convenience,dynamic,didSet,final,get,infix,indirect,lazy,left," + - "mutating,none,nonmutating,optional,override,postfix,precedence,prefix,Protocol,required," + - "right,set,Type,unowned,weak,willSet,var,_".split(",") - - val ALL_KEYWORDS = - CPP_KEYWORDS + KOTLIN_KEYWORDS + CSHARP_KEYWORDS + - RUST_KEYWORDS + COFFEE_KEYWORDS + - JSCRIPT_KEYWORDS + PERL_KEYWORDS + PYTHON_KEYWORDS + RUBY_KEYWORDS + - SH_KEYWORDS + SWIFT_KEYWORDS - - val ALL_MIXED_KEYWORDS: List = - """ - #available #column #define #defined #elif #else #else#elseif #endif #error #file #function - #if #ifdef #ifndef #include #line #pragma #selector #undef abstract add after alias - alignas alignof and and_eq andalso as ascending asm assert associatedtype associativity - async atomic_cancel atomic_commit atomic_noexcept auto await base become begin bitand - bitor bnot bor box break bsl bsr bxor case catch chan - checked class compl concept cond const const_cast constexpr continue convenience - covariant crate debugger decltype def default defer deferred defined? deinit - del delegate delete descending didset div do dynamic dynamic_cast dynamictype - elif else elseif elsif end ensure eval event except explicit export extends extension - extern external factory fallthrough false final finally fixed fn for foreach friend - from fun func function get global go goto group guard if impl implements implicit import - in indirect infix init inline inout instanceof interface internal into is join lambda - lazy left let library local lock long loop macro map match mod module move mut mutable - mutating namespace native new next nil noexcept none nonlocal nonmutating not not_eq - null nullptr object of offsetof operator optional or or_eq orderby orelse out override - package params part partial pass postfix precedence prefix priv private proc protected - protocol pub public pure raise range readonly receive redo ref register reinterpret_cast - rem remove repeat required requires rescue rethrow rethrows retry return right sbyte - sealed select self set short signed sizeof stackalloc static static_assert static_cast - strictfp struct subscript super switch sync synchronized template then this - thread_local throw throws trait transaction_safe transaction_safe_dynamic transient - true try type typealias typedef typeid typename typeof uint ulong unchecked undef - union unless unowned unsafe unsigned unsized until use ushort using value var virtual - void volatile wchar_t weak when where while willset with xor xor_eq xorauto yield - yieldabstract yieldarguments val list override get set as as? in !in !is is by - constructor delegate dynamic field file init param property receiver setparam data - data expect lateinit crossinline companion annotation actual noinline open reified - suspend tailrec vararg it constraint alter column table all any asc backup database - between check create index replace view procedure unique desc distinct drop exec - exists foreign key full outer having inner insert like limit order primary rownum - top truncate update values - """.trimIndent().split(" ") - - // TODO Migrate to list of chars - val TOKEN_DELIMITERS = listOf(" ", ",", ".", ":", ";", "(", ")", "=", "{", "}", "<", ">", "\r", "\n") - val STRING_DELIMITERS = listOf("\'", "\"", "\"\"\"") - val COMMENT_DELIMITERS = listOf("//", "#") - - // TODO Add support for other other languages like Dart or Python - val MULTILINE_COMMENT_DELIMITERS = listOf(Pair("/*", "*/")) - val PUNCTUATION_CHARACTERS = listOf(",", ".", ":", ";") - val MARK_CHARACTERS = listOf("(", ")", "=", "{", "}", "<", ">", "-", "+", "[", "]", "|", "&") -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/AnnotationLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/AnnotationLocator.kt deleted file mode 100644 index 72268340..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/AnnotationLocator.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.model.PhraseLocation - -internal object AnnotationLocator { - - fun locate(code: String): List { - val foundAnnotations = emptyList() - val locations = mutableSetOf() - code.split(delimiters = TOKEN_DELIMITERS.toTypedArray()) - .asSequence() - .filter { it.isNotEmpty() } - .filter { foundAnnotations.contains(it).not() } - .filter { it.contains('@') } - .forEach { annotation -> - code.indicesOf(annotation).forEach { phraseStartIndex -> - val symbolLocation = annotation.indexOf('@') - val startIndex = phraseStartIndex + symbolLocation - - locations.add( - PhraseLocation( - startIndex, - startIndex + annotation.length - symbolLocation - ) - ) - } - } - - return locations.toList() - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/CommentLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/CommentLocator.kt deleted file mode 100644 index 133d78a1..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/CommentLocator.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.COMMENT_DELIMITERS -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.internal.lengthToEOF -import dev.snipme.highlights.model.PhraseLocation - -internal object CommentLocator { - - fun locate(code: String): List { - val locations = mutableListOf() - val indices = mutableListOf() - COMMENT_DELIMITERS.forEach { delimiter -> - indices.addAll(code.indicesOf(delimiter)) - } - - indices.forEach { start -> - val end = start + code.lengthToEOF(start) - locations.add(PhraseLocation(start, end)) - } - return locations - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/KeywordLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/KeywordLocator.kt deleted file mode 100644 index 61cdf6ac..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/KeywordLocator.kt +++ /dev/null @@ -1,47 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.internal.isIndependentPhrase -import dev.snipme.highlights.model.PhraseLocation - -internal object KeywordLocator { - - fun locate( - code: String, - keywords: List, - ignoreRanges: List = emptyList(), - ): List { - val locations = mutableListOf() - val foundKeywords = findKeywords(code, keywords) - - val interpretedKeywords = foundKeywords.filterNot { keyword -> - val index = code.indexOf(keyword) - val length = keyword.length - ignoreRanges.any { it.start <= index && it.end >= index + length } - } - - interpretedKeywords.forEach { keyword -> - val indices = code - .indicesOf(keyword) - .filter { keyword.isIndependentPhrase(code, it) } - - indices.forEach { index -> - locations.add(PhraseLocation(index, index + keyword.length)) - } - } - - return locations.toList() - } - - private fun findKeywords(code: String, keywords: List): Set = - TOKEN_DELIMITERS.toTypedArray().let { delimiters -> - code.split(delimiters = delimiters, ignoreCase = true) // Split into words - .asSequence() // Reduce amount of operations - .filter { it.isNotBlank() } // Remove empty - .map { it.trim() } // Remove whitespaces from phrase - .map { it.lowercase() } // Standardize - .filter { it in keywords } // Get supported - .toSet() // Filter duplicates - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/LinkLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/LinkLocator.kt deleted file mode 100644 index bbe6724d..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/LinkLocator.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS -import dev.snipme.highlights.model.PhraseLocation - -private val EXCLUDED_URL_CHARACTERS = listOf(":", ".", "=") - -internal object LinkLocator { - fun locate(code: String): List = - code.split(delimiters = TOKEN_DELIMITERS.minus(EXCLUDED_URL_CHARACTERS).toTypedArray()) - .filter { isUrl(it) } - .map { - val start = code.indexOf(it) - val end = start + it.length - PhraseLocation(start, end) - } - - private fun isUrl(phrase: String): Boolean = - phrase.startsWith("http://") || phrase.startsWith("https://") -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MarkLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MarkLocator.kt deleted file mode 100644 index 1526f3bf..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MarkLocator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.MARK_CHARACTERS -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.model.PhraseLocation - -internal object MarkLocator { - fun locate(code: String): List { - val locations = mutableListOf() - code.asSequence() - .toSet() - .filter { it.toString() in MARK_CHARACTERS } - .forEach { - code.indicesOf(it.toString()).forEach { index -> - locations.add(PhraseLocation(index, index + 1)) - } - } - - return locations - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MultilineCommentLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MultilineCommentLocator.kt deleted file mode 100644 index c006d50e..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MultilineCommentLocator.kt +++ /dev/null @@ -1,35 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.MULTILINE_COMMENT_DELIMITERS -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.model.PhraseLocation - -private const val START_INDEX = 0 - -internal object MultilineCommentLocator { - - fun locate(code: String): List { - val locations = mutableListOf() - val comments = mutableListOf>() - val startIndices = mutableListOf() - val endIndices = mutableListOf() - - MULTILINE_COMMENT_DELIMITERS.forEach { commentBlock -> - val (prefix, postfix) = commentBlock - startIndices.addAll(code.indicesOf(prefix)) - endIndices.addAll(code.indicesOf(postfix).map { it + postfix.length }) - } - - val endIndex = minOf(startIndices.size, endIndices.size) - 1 - for (i in START_INDEX..endIndex) { - comments.add(Pair(startIndices[i], endIndices[i])) - } - - comments.forEach { - val (start, end) = it - locations.add(PhraseLocation(start, end)) - } - - return locations - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/NumericLiteralLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/NumericLiteralLocator.kt deleted file mode 100644 index 22bdd531..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/NumericLiteralLocator.kt +++ /dev/null @@ -1,115 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.model.PhraseLocation - -private val NUMBER_START_CHARACTERS = listOf('-', '.') -private val NUMBER_TYPE_CHARACTERS = listOf('e', 'u', 'f', 'l') -private val HEX_NUMBER_CHARACTERS = listOf('a', 'b', 'c', 'd', 'e', 'f') -private val NUMBER_SPECIAL_CHARACTERS = listOf('_') - -internal object NumericLiteralLocator { - - fun locate(code: String) = findDigitIndices(code) - - private fun findDigitIndices(code: String): List { - val foundPhrases = mutableSetOf() - val locations = mutableSetOf() - - val delimiters = TOKEN_DELIMITERS.filterNot { it == "." }.toTypedArray() - - code.split(delimiters = delimiters) // Separate words - .asSequence() // Manipulate on given word separately - .filterNot { foundPhrases.contains(it) } - .filter { it.isNotBlank() } // Filter spaces and others - .filter { - it.first().isDigit() || - NUMBER_START_CHARACTERS.contains(it.first()) && - it.getOrNull(1)?.isDigit() == true - } // Find start of literals - .forEach { number -> - // For given literal find all occurrences - val indices = code.indicesOf(number) - for (startIndex in indices) { - // TODO Correct this and publish - if (code.isFullNumber(number, startIndex).not()) return@forEach - // Omit in the middle of text, probably variable name (this100) - if (code.isNumberFirstIndex(startIndex).not()) return@forEach - // Add matching occurrence to the output locations - val length = calculateNumberLength(number.lowercase()) - locations.add(PhraseLocation(startIndex, startIndex + length)) - } - - foundPhrases.add(number) - } - - return locations.toList() - } - - // Returns if given index is the beginning of word (there is no letter before) - private fun String.isNumberFirstIndex(index: Int): Boolean { - if (index < 0) return false - if (index == 0) return true - val positionBefore = maxOf(index - 1, 0) - val charBefore = getOrNull(positionBefore) ?: return false - - return TOKEN_DELIMITERS.contains(charBefore.toString()) - } - - private fun String.isFullNumber(number: String, startIndex: Int): Boolean { - val numberEndingIndex = startIndex + number.length - if (numberEndingIndex >= lastIndex) return true - val numberEnding = getOrNull(numberEndingIndex) ?: return false - - return TOKEN_DELIMITERS.contains(numberEnding.toString()) - } - - private fun calculateNumberLength(number: String): Int { - val letters = number.filter { it.isLetter() } - - if (number.startsWith("0x")) { - return getLengthOfSubstringFor(number) { - it.isDigit() || HEX_NUMBER_CHARACTERS.contains(it) - } - } - - if (number.contains("0b")) { - return getLengthOfSubstringFor(number) { - it == '0' || it == '1' - } - } - - // Highlight only 4f when e.g. number is like 4fff - if (NUMBER_TYPE_CHARACTERS.any { letters.contains(it) }) { - var length = 1 // Single letter - length += number.count { it.isDigit() } - length += number.count { NUMBER_START_CHARACTERS.contains(it) } - length += number.count { NUMBER_SPECIAL_CHARACTERS.contains(it) } - if ("e+" in number) length++ - return length - } - - return number.filter { - it.isDigit() || - NUMBER_START_CHARACTERS.contains(it) || - NUMBER_TYPE_CHARACTERS.contains(it) || - NUMBER_SPECIAL_CHARACTERS.contains(it) - }.length - } - - private fun getLengthOfSubstringFor(number: String, condition: (Char) -> Boolean): Int { - var hexSequenceLength = 2 - run loop@{ - number.substring(startIndex = hexSequenceLength).forEach { - if (condition(it)) { - hexSequenceLength++ - } else { - return@loop - } - } - } - - return hexSequenceLength - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/PunctuationLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/PunctuationLocator.kt deleted file mode 100644 index ab4f9773..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/PunctuationLocator.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.PUNCTUATION_CHARACTERS -import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.model.PhraseLocation - -internal object PunctuationLocator { - fun locate(code: String): List { - val locations = mutableSetOf() - code.asSequence() - .map { it.toString().trim() } - .filter { it in TOKEN_DELIMITERS } - .filter { it.isNotBlank() } - .filter { it in PUNCTUATION_CHARACTERS } - .forEach { - val indices = code.indicesOf(it) - for (index in indices) { - if (code[index].isWhitespace()) return@forEach - locations.add(PhraseLocation(index, index + 1)) - } - } - - return locations.toList() - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/StringLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/StringLocator.kt deleted file mode 100644 index b5595e16..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/StringLocator.kt +++ /dev/null @@ -1,38 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens.STRING_DELIMITERS -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.model.PhraseLocation - -private const val START_INDEX = 0 -private const val TWO_ELEMENTS = 2 -private const val QUOTE_ENDING_POSITION = 1 - -internal object StringLocator { - - fun locate(code: String): List = findStrings(code) - - private fun findStrings(code: String): List { - val locations = mutableListOf() - - // Find index of each string delimiter like " or ' or """ - STRING_DELIMITERS.forEach { - val textIndices = mutableListOf() - textIndices += code.indicesOf(it) - - // For given indices find words between - for (i in START_INDEX..textIndices.lastIndex step TWO_ELEMENTS) { - if (textIndices.getOrNull(i + 1) != null) { - locations.add( - PhraseLocation( - textIndices[i], - textIndices[i + 1] + QUOTE_ENDING_POSITION - ) - ) - } - } - } - - return locations - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/TokenLocator.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/TokenLocator.kt deleted file mode 100644 index 9f68bbeb..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/internal/locator/TokenLocator.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.snipme.highlights.internal.locator - -import dev.snipme.highlights.internal.SyntaxTokens -import dev.snipme.highlights.internal.indicesOf -import dev.snipme.highlights.internal.isIndependentPhrase -import dev.snipme.highlights.model.PhraseLocation - -internal object TokenLocator { - fun locate(code: String): List { - val locations = mutableSetOf() - code.split(delimiters = SyntaxTokens.TOKEN_DELIMITERS.toTypedArray()) // Separate words - .asSequence() // Manipulate on given word separately - .filter { it.isNotBlank() } // Filter spaces and others - .forEach { token -> - code.indicesOf(token) - .filter { token.isIndependentPhrase(code, it) } - .forEach { startIndex -> - locations.add(PhraseLocation(startIndex, startIndex + token.length)) - } - } - - return locations.toList() - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/CodeHighlight.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/model/CodeHighlight.kt deleted file mode 100644 index 149d95b3..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/CodeHighlight.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.snipme.highlights.model - -sealed class CodeHighlight(open val location: PhraseLocation) -data class BoldHighlight(override val location: PhraseLocation) : CodeHighlight(location) -data class ColorHighlight( - override val location: PhraseLocation, - val rgb: Int -) : CodeHighlight(location) diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/CodeStructure.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/model/CodeStructure.kt deleted file mode 100644 index 05ce814c..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/CodeStructure.kt +++ /dev/null @@ -1,99 +0,0 @@ -package dev.snipme.highlights.model - -data class PhraseLocation(val start: Int, val end: Int) - -// TODO Migrate to set -data class CodeStructure( - val marks: List, - val punctuations: List, - val keywords: List, - val strings: List, - val literals: List, - val comments: List, - val multilineComments: List, - val annotations: List, - val incremental: Boolean, -) { - fun move(position: Int) = - CodeStructure( - marks = marks.map { it.copy(start = it.start + position, end = it.end + position) }, - punctuations = punctuations.map { - it.copy( - start = it.start + position, - end = it.end + position - ) - }, - keywords = keywords.map { - it.copy( - start = it.start + position, - end = it.end + position - ) - }, - strings = strings.map { it.copy(start = it.start + position, end = it.end + position) }, - literals = literals.map { - it.copy( - start = it.start + position, - end = it.end + position - ) - }, - comments = comments.map { - it.copy( - start = it.start + position, - end = it.end + position - ) - }, - multilineComments = multilineComments.map { - it.copy( - start = it.start + position, - end = it.end + position - ) - }, - annotations = annotations.map { - it.copy( - start = it.start + position, - end = it.end + position - ) - }, - incremental = true, - ) - - operator fun plus(new: CodeStructure): CodeStructure = - CodeStructure( - marks = marks + new.marks, - punctuations = punctuations + new.punctuations, - keywords = keywords + new.keywords, - strings = strings + new.strings, - literals = literals + new.literals, - comments = comments + new.comments, - multilineComments = multilineComments + new.multilineComments, - annotations = annotations + new.annotations, - incremental = true, - ) - - operator fun minus(new: CodeStructure): CodeStructure = - CodeStructure( - marks = marks - new.marks, - punctuations = punctuations - new.punctuations, - keywords = keywords - new.keywords, - strings = strings - new.strings, - literals = literals - new.literals, - comments = comments - new.comments, - multilineComments = multilineComments - new.multilineComments, - annotations = annotations - new.annotations, - incremental = true, - ) - - fun printPhrases(code: String) { - print("marks = ${marks.join(code)}") - print("punctuations = ${punctuations.join(code)}") - print("keywords = ${keywords.join(code)}") - print("strings = ${strings.join(code)}") - print("literals = ${literals.join(code)}") - print("comments = ${comments.join(code)}") - print("multilineComments = ${multilineComments.join(code)}") - print("annotations = ${annotations.join(code)}") - } - - private fun List.join(code: String) = - this.map { code.substring(it.start, it.end) }.joinToString(separator = " ") + "\n" -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxLanguage.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxLanguage.kt deleted file mode 100644 index 34b78b1e..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxLanguage.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.snipme.highlights.model - -enum class SyntaxLanguage { - DEFAULT, - MIXED, - C, - CPP, - JAVA, - KOTLIN, - RUST, - CSHARP, - COFFEESCRIPT, - JAVASCRIPT, - PERL, - PYTHON, - RUBY, - SHELL, - SWIFT; - - companion object { - fun getNames(): List = values().map { - it.name - .lowercase() - .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - } - - fun getByName(name: String): SyntaxLanguage? = - values().find { it.name.equals(name, ignoreCase = true) } - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxTheme.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxTheme.kt deleted file mode 100644 index 7c7cfbc7..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxTheme.kt +++ /dev/null @@ -1,43 +0,0 @@ -package dev.snipme.highlights.model - -data class SyntaxTheme( - val key: String, - val code: Int, - val keyword: Int, - val string: Int, - val literal: Int, - val comment: Int, - val metadata: Int, - val multilineComment: Int, - val punctuation: Int, - val mark: Int -) { - companion object { - fun simple(key: String, code: Int, string: Int, accent: Int, value: Int) = SyntaxTheme( - key = key, - code = code, - keyword = accent, - string = string, - literal = value, - comment = string, - metadata = value, - multilineComment = string, - punctuation = accent, - mark = code - ) - - fun basic(key: String, code: Int, string: Int, accent: Int, value: Int, comment: Int) = - SyntaxTheme( - key = key, - code = code, - keyword = accent, - string = string, - literal = value, - comment = comment, - metadata = code, - multilineComment = comment, - punctuation = accent, - mark = code - ) - } -} diff --git a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxThemes.kt b/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxThemes.kt deleted file mode 100644 index e2c703fa..00000000 --- a/sample/src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxThemes.kt +++ /dev/null @@ -1,154 +0,0 @@ -package dev.snipme.highlights.model - -private const val DARCULA_KEY = "darcula" -private const val MONOKAI_KEY = "monokai" -private const val NOTEPAD_KEY = "notepad" -private const val MATRIX_KEY = "matrix" -private const val PASTEL_KEY = "pastel" - -object SyntaxThemes { - - val dark = mapOf( - DARCULA_KEY to SyntaxTheme( - key = DARCULA_KEY, - code = 0xEDEDED, - keyword = 0xCC7832, - string = 0x6A8759, - literal = 0x6897BB, - comment = 0x909090, - metadata = 0xBBB529, - multilineComment = 0x629755, - punctuation = 0xCC7832, - mark = 0xEDEDED - ), - MONOKAI_KEY to SyntaxTheme( - key = MONOKAI_KEY, - code = 0xF8F8F2, - keyword = 0xF92672, - string = 0xE6DB74, - literal = 0xAE81FF, - comment = 0xFD971F, - metadata = 0xB8F4B8, - multilineComment = 0xFD971F, - punctuation = 0xF8F8F2, - mark = 0xF8F8F2 - ), - NOTEPAD_KEY to SyntaxTheme( - key = NOTEPAD_KEY, - code = 0x000080, - keyword = 0x0000FF, - string = 0x808080, - literal = 0xFF8000, - comment = 0x008000, - metadata = 0x000080, - multilineComment = 0x008000, - punctuation = 0xAA2C8C, - mark = 0xAA2C8C - ), - MATRIX_KEY to SyntaxTheme( - key = MATRIX_KEY, - code = 0x008500, - keyword = 0x008500, - string = 0x269926, - literal = 0x39E639, - comment = 0x67E667, - metadata = 0x008500, - multilineComment = 0x67E667, - punctuation = 0x008500, - mark = 0x008500 - ), - PASTEL_KEY to SyntaxTheme( - key = PASTEL_KEY, - code = 0xDFDEE0, - keyword = 0x729FCF, - string = 0x93CF55, - literal = 0x8AE234, - comment = 0x888A85, - metadata = 0x5DB895, - multilineComment = 0x888A85, - punctuation = 0xCB956D, - mark = 0xCB956D - ) - ) - - val light = mapOf( - DARCULA_KEY to SyntaxTheme( - key = DARCULA_KEY, - code = 0x121212, - keyword = 0xCC7832, - string = 0x6A8759, - literal = 0x6897BB, - comment = 0x909090, - metadata = 0xBBB529, - multilineComment = 0x629755, - punctuation = 0xCC7832, - mark = 0x121212 - ), - MONOKAI_KEY to SyntaxTheme( - key = MONOKAI_KEY, - code = 0x07070D, - keyword = 0xF92672, - string = 0xE6DB74, - literal = 0xAE81FF, - comment = 0xFD971F, - metadata = 0xB8F4B8, - multilineComment = 0xFD971F, - punctuation = 0x07070D, - mark = 0x07070D - ), - NOTEPAD_KEY to SyntaxTheme( - key = NOTEPAD_KEY, - code = 0x000080, - keyword = 0x0000FF, - string = 0x808080, - literal = 0xFF8000, - comment = 0x008000, - metadata = 0x000080, - multilineComment = 0x008000, - punctuation = 0xAA2C8C, - mark = 0xAA2C8C - ), - MATRIX_KEY to SyntaxTheme( - key = MATRIX_KEY, - code = 0x008500, - keyword = 0x008500, - string = 0x269926, - literal = 0x39E639, - comment = 0x67E667, - metadata = 0x008500, - multilineComment = 0x67E667, - punctuation = 0x008500, - mark = 0x008500 - ), - PASTEL_KEY to SyntaxTheme( - key = PASTEL_KEY, - code = 0x20211F, - keyword = 0x729FCF, - string = 0x93CF55, - literal = 0x8AE234, - comment = 0x888A85, - metadata = 0x5DB895, - multilineComment = 0x888A85, - punctuation = 0xCB956D, - mark = 0xCB956D - ) - ) - - fun themes(darkMode: Boolean = false) = if (darkMode) dark else light - - fun default(darkMode: Boolean = false) = themes(darkMode)[DARCULA_KEY]!! - - fun darcula(darkMode: Boolean = false) = themes(darkMode)[DARCULA_KEY]!! - fun monokai(darkMode: Boolean = false) = themes(darkMode)[MONOKAI_KEY]!! - fun notepad(darkMode: Boolean = false) = themes(darkMode)[NOTEPAD_KEY]!! - fun matrix(darkMode: Boolean = false) = themes(darkMode)[MATRIX_KEY]!! - fun pastel(darkMode: Boolean = false) = themes(darkMode)[PASTEL_KEY]!! - - fun getNames(): List = SyntaxThemes.light.map { - it.key - .lowercase() - .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - } - - fun SyntaxTheme.useDark(darkMode: Boolean) = if (darkMode) dark[key] else light[key] -} diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt index 3fdfd53f..4fc534e0 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt @@ -8,9 +8,15 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily @@ -23,35 +29,9 @@ import dev.snipme.highlights.model.BoldHighlight import dev.snipme.highlights.model.ColorHighlight import dev.snipme.highlights.model.PhraseLocation import dev.snipme.highlights.model.SyntaxLanguage -import dev.snipme.highlights.model.SyntaxTheme - -private const val ONE_KEY = "one" - -val OneDarkTheme = SyntaxTheme( - key = ONE_KEY, - code = 0xBBBBBB, - keyword = 0xD55FDE, - string = 0x89CA78, - literal = 0xD19A66, - comment = 0x5C6370, - metadata = 0xE5C07B, - multilineComment = 0x5C6370, - punctuation = 0xEF596F, - mark = 0x2BBAC5 -) - -val OneLightTheme = SyntaxTheme( - key = ONE_KEY, - code = 0x383A42, - keyword = 0xA626A4, - string = 0x50A14F, - literal = 0x986801, - comment = 0xA1A1A1, - metadata = 0xC18401, - multilineComment = 0xA1A1A1, - punctuation = 0xE45649, - mark = 0x526FFF, -) +import dev.snipme.highlights.model.SyntaxThemes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext val Highlights.annotatedString get() = buildAnnotatedString { @@ -81,14 +61,18 @@ fun CodeText( darkMode: Boolean = isSystemInDarkTheme(), language: SyntaxLanguage = SyntaxLanguage.KOTLIN, ) { - val string = remember(code, darkMode, language, emphasis) { - Highlights.Builder().run { - theme(if (darkMode) OneDarkTheme else OneLightTheme) - code(code) - emphasis(locations = emphasis) - language(language) - build() - }.annotatedString + var string by remember { mutableStateOf(AnnotatedString(code)) } + + LaunchedEffect(code, darkMode, language, emphasis) { + withContext(Dispatchers.Default) { + string = Highlights.Builder().run { + theme(SyntaxThemes.atom(darkMode)) + code(code) + emphasis(locations = emphasis) + language(language) + build() + }.annotatedString + } } Box(modifier = modifier.horizontalScroll(rememberScrollState())) { SelectionContainer { From 5af2ada01a2ea12eecb18ea34f02931db91e5e53 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 11:39:09 +0300 Subject: [PATCH 02/26] feat: update to kotlin 2.0 --- build.gradle.kts | 33 +++++++++---------- buildSrc/src/main/kotlin/ConfigureAndroid.kt | 12 ++----- compose/build.gradle.kts | 1 + debugger/app/build.gradle.kts | 4 +-- debugger/ideplugin/build.gradle.kts | 1 + debugger/server/build.gradle.kts | 4 +-- essenty/essenty-compose/build.gradle.kts | 1 + gradle.properties | 1 - gradle/libs.versions.toml | 26 +++++++-------- sample/build.gradle.kts | 9 ++--- sample/libs.versions.toml | 4 +-- .../navigation/component/RootComponent.kt | 13 +++++++- .../navigation/component/StackComponent.kt | 12 ------- savedstate/build.gradle.kts | 2 +- 14 files changed, 56 insertions(+), 67 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index bbab969d..b0411062 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,39 +20,38 @@ plugins { // alias(libs.plugins.androidApplication) apply false // alias(libs.plugins.androidLibrary) apply false // alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.compose.compiler) apply false } allprojects { group = Config.artifactId version = Config.versionName + // plugins.withType().configureEach { + // the().apply { + // enableIntrinsicRemember = true + // enableNonSkippingGroupOptimization = true + // enableStrongSkippingMode = true + // stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_definitions.txt") + // if (properties["enableComposeCompilerReports"] == "true") { + // val metricsDir = layout.buildDirectory.dir("compose_metrics") + // metricsDestination = metricsDir + // reportsDestination = metricsDir + // } + // } + // } tasks.withType().configureEach { compilerOptions { optIn.addAll(Config.optIns) jvmTarget = Config.jvmTarget languageVersion = Config.kotlinVersion - freeCompilerArgs.apply { - addAll(Config.jvmCompilerArgs) - addAll( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" + - "${rootProject.rootDir.absolutePath}/stability_definitions.txt", - ) - if (project.findProperty("enableComposeCompilerReports") == "true") { - addAll( - "-P", - "$PluginPrefix=${layout.buildDirectory.get()}/compose_metrics", - "-P", - "$PluginPrefix=${layout.buildDirectory.get()}/compose_metrics", - ) - } - } + freeCompilerArgs.apply { addAll(Config.jvmCompilerArgs) } } } } subprojects { // TODO: Migrate to applying dokka plugin per-project in conventions - if (name in setOf("app", "debugger", "server")) return@subprojects + if (name in setOf("sample", "debugger", "server")) return@subprojects apply(plugin = rootProject.libs.plugins.dokka.id) dependencies { diff --git a/buildSrc/src/main/kotlin/ConfigureAndroid.kt b/buildSrc/src/main/kotlin/ConfigureAndroid.kt index 1dfd4a76..f99ada62 100644 --- a/buildSrc/src/main/kotlin/ConfigureAndroid.kt +++ b/buildSrc/src/main/kotlin/ConfigureAndroid.kt @@ -4,11 +4,8 @@ import com.android.build.api.dsl.CommonExtension import com.android.build.gradle.LibraryExtension import org.gradle.api.Project -fun Project.configureAndroid( - commonExtension: CommonExtension<*, *, *, *, *, *>, -) = commonExtension.apply { +fun CommonExtension<*, *, *, *, *, *>.configureAndroid() = apply { compileSdk = Config.compileSdk - val libs by versionCatalog defaultConfig { minSdk = Config.minSdk @@ -66,15 +63,10 @@ fun Project.configureAndroid( } } } - - composeOptions { - kotlinCompilerExtensionVersion = libs.requireVersion("compose-compiler") - useLiveLiterals = true - } } fun Project.configureAndroidLibrary(variant: LibraryExtension) = variant.apply { - configureAndroid(this) + configureAndroid() testFixtures { enable = true diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 17627009..e3553170 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id(libs.plugins.kotlinMultiplatform.id) id(libs.plugins.androidLibrary.id) alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) id("maven-publish") signing } diff --git a/debugger/app/build.gradle.kts b/debugger/app/build.gradle.kts index 263ffc99..bc51f0db 100644 --- a/debugger/app/build.gradle.kts +++ b/debugger/app/build.gradle.kts @@ -3,14 +3,12 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { id(libs.plugins.kotlinMultiplatform.id) alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) alias(libs.plugins.serialization) } kotlin { jvm("desktop") { - compilations.all { - compilerOptions.configure { jvmToolchain(21) } - } } sourceSets { diff --git a/debugger/ideplugin/build.gradle.kts b/debugger/ideplugin/build.gradle.kts index a2c76fa2..18b8bce8 100644 --- a/debugger/ideplugin/build.gradle.kts +++ b/debugger/ideplugin/build.gradle.kts @@ -4,6 +4,7 @@ plugins { alias(libs.plugins.intellij.ide) kotlin("jvm") alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) } val props by localProperties diff --git a/debugger/server/build.gradle.kts b/debugger/server/build.gradle.kts index dcb8d672..73cd9031 100644 --- a/debugger/server/build.gradle.kts +++ b/debugger/server/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id(libs.plugins.kotlinMultiplatform.id) alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) alias(libs.plugins.serialization) } @@ -10,9 +11,6 @@ compose.resources { kotlin { jvm { - compilations.all { - compilerOptions.configure { jvmToolchain(21) } - } } sourceSets { diff --git a/essenty/essenty-compose/build.gradle.kts b/essenty/essenty-compose/build.gradle.kts index 90775115..4fc633e0 100644 --- a/essenty/essenty-compose/build.gradle.kts +++ b/essenty/essenty-compose/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id(libs.plugins.kotlinMultiplatform.id) id(libs.plugins.androidLibrary.id) alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) id("maven-publish") signing } diff --git a/gradle.properties b/gradle.properties index 9d0c0ae4..67e82948 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,6 @@ android.nonFinalResIds=true kotlin.native.ignoreIncorrectDependencies=true kotlinx.atomicfu.enableJvmIrTransformation=true org.jetbrains.compose.experimental.macos.enabled=true -kotlin.experimental.tryK2=false android.lint.useK2Uast=true nl.littlerobots.vcu.resolver=true org.jetbrains.compose.experimental.jscanvas.enabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00b3f3a0..8a9dd23a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,32 +1,31 @@ [versions] activity = "1.9.0" -androidx-lifecycle = "2.8.0-beta01" -compose = "1.6.10-beta02" -compose-compiler = "1.5.12" -compose-lifecycle = "2.8.0-beta02" +androidx-lifecycle = "2.8.0" +compose = "1.6.10" +compose-lifecycle = "2.8.0" composeDetektPlugin = "1.3.0" -core-ktx = "1.13.0" -coroutines = "1.8.1-Beta" -datetime = "0.6.0-RC.2" +core-ktx = "1.13.1" +coroutines = "1.8.1" +datetime = "0.6.0" dependencyAnalysisPlugin = "1.31.0" detekt = "1.23.6" detektFormattingPlugin = "1.23.6" dokka = "1.9.20" essenty = "2.0.0" -fragment = "1.7.0-rc02" -gradleAndroid = "8.4.0" +fragment = "1.8.0-beta01" +gradleAndroid = "8.6.0-alpha03" gradleDoctorPlugin = "0.9.2" intellij-ide-plugin = "2.0.0-beta1" junit = "4.13.2" -kotest = "5.8.1" +kotest = "5.9.0" kotest-plugin = "5.8.1" # @pin -kotlin = "1.9.23" +kotlin = "2.0.0" kotlin-collections = "0.3.7" -kotlin-io = "0.3.3" +kotlin-io = "0.3.5" kotlinx-atomicfu = "0.24.0" ktor = "3.0.0-beta-1" -serialization = "1.6.3" +serialization = "1.7.0-RC" turbine = "1.1.0" uuid = "0.8.4" versionCatalogUpdatePlugin = "0.8.4" @@ -140,3 +139,4 @@ kotest = { id = "io.kotest.multiplatform", version.ref = "kotest-plugin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdatePlugin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 5b20365a..67a73c29 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id(libs.plugins.kotlinMultiplatform.id) id(applibs.plugins.android.application.id) alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) alias(libs.plugins.serialization) } @@ -42,7 +43,6 @@ kotlin { } testTask { enabled = false } } - applyBinaryen() } jvm("desktop") @@ -126,7 +126,7 @@ kotlin { } android { namespace = Config.Sample.namespace - configureAndroid(this) + configureAndroid() buildFeatures { viewBinding = true buildConfig = true @@ -190,10 +190,11 @@ compose { publicResClass = false } - experimental { - web.application { } + web { + } + desktop { application { mainClass = "${Config.Sample.namespace}.MainKt" diff --git a/sample/libs.versions.toml b/sample/libs.versions.toml index 195ab610..1849cab1 100644 --- a/sample/libs.versions.toml +++ b/sample/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -decompose = "3.0.0-beta01" +decompose = "3.0.0" apiresult = "1.0.4" koin = "3.6.0-wasm-alpha2" kmputils = "1.3.2" -material = "1.12.0-rc01" +material = "1.12.0" activity = "1.9.0" okio = "3.9.0" splashscreen = "1.1.0-rc01" diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/navigation/component/RootComponent.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/navigation/component/RootComponent.kt index cb498954..80006f3a 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/navigation/component/RootComponent.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/navigation/component/RootComponent.kt @@ -4,14 +4,25 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.childContext import com.arkivanov.decompose.router.stack.webhistory.WebHistoryController +import pro.respawn.flowmvi.sample.navigation.destination.Destination import pro.respawn.flowmvi.sample.navigation.details.DetailsComponent @OptIn(ExperimentalDecomposeApi::class) class RootComponent( context: ComponentContext, controller: WebHistoryController? = null, -) : StackComponent(context.childContext("stack"), controller), +) : StackComponent(context.childContext("stack")), DestinationComponent by destinationComponent(null, context) { val details = DetailsComponent(childContext("detailPane")) + + init { + controller?.attach( + navigator = stackNav, + stack = stack, + getPath = { it.routes.first() }, + getConfiguration = { Destination.byRoute[it.removePrefix("/")] ?: Destination.Home }, + serializer = Destination.serializer(), + ) + } } diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/navigation/component/StackComponent.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/navigation/component/StackComponent.kt index b81557cb..067ddf2f 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/navigation/component/StackComponent.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/navigation/component/StackComponent.kt @@ -16,7 +16,6 @@ import com.arkivanov.decompose.router.stack.active import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.navigate import com.arkivanov.decompose.router.stack.pop -import com.arkivanov.decompose.router.stack.webhistory.WebHistoryController import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update @@ -29,7 +28,6 @@ import pro.respawn.flowmvi.sample.navigation.util.retained @Stable open class StackComponent( context: ComponentContext, - controller: WebHistoryController?, ) : Navigator { val results by context.retained { MutableStateFlow>>(emptySet()) } @@ -42,16 +40,6 @@ open class StackComponent( childFactory = ::destinationComponent, ) - init { - controller?.attach( - navigator = stackNav, - stack = stack, - getPath = { it.routes.first() }, - getConfiguration = { Destination.byRoute[it.removePrefix("/")] ?: Destination.Home }, - serializer = Destination.serializer(), - ) - } - fun navigate( destination: Destination, filter: (Destination) -> Boolean = { false }, diff --git a/savedstate/build.gradle.kts b/savedstate/build.gradle.kts index d6d12410..1b03602b 100644 --- a/savedstate/build.gradle.kts +++ b/savedstate/build.gradle.kts @@ -18,7 +18,7 @@ kotlin { withAndroidTarget() } group("browser") { - withWasm() + withWasmJs() withJs() } } From 635718415ed251c9b09c9dafe2fe738b3623c153 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 11:57:08 +0300 Subject: [PATCH 03/26] fix: bring back stateProvider under a deprecation message to reduce migration complexity --- compose/build.gradle.kts | 4 ++++ .../flowmvi/compose/preview/StateProvider.kt | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/preview/StateProvider.kt diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index e3553170..7e21104f 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -44,6 +44,10 @@ kotlin { jvmMain.dependencies { implementation(compose.desktop.common) } + androidMain.dependencies { + implementation(compose.preview) + implementation(compose.uiTooling) + } } } diff --git a/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/preview/StateProvider.kt b/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/preview/StateProvider.kt new file mode 100644 index 00000000..e5a75706 --- /dev/null +++ b/compose/src/androidMain/kotlin/pro/respawn/flowmvi/compose/preview/StateProvider.kt @@ -0,0 +1,18 @@ +package pro.respawn.flowmvi.compose.preview + +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider + +/** + * Preview provider that takes a vararg argument for convenience + */ +@Deprecated( + """ + FlowMVI will no longer provide preview functionality as it is platform-dependent and out of scope of the library. + Please copy and paste the code of the provider to your repository if you need it. +""", + ReplaceWith( + "CollectionPreviewParameterProvider(states.asList())", + "androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider", + ) +) +public open class StateProvider(vararg states: T) : CollectionPreviewParameterProvider(states.asList()) From c8d185ec78f5a1fd7174d65e9103fa51fd902f82 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 12:24:03 +0300 Subject: [PATCH 04/26] Revert "remove deprecated: consoleLogging, parentStore, savedState, nativeLogging, androidLogging" This reverts commit 741902310cc0dac6b1c9adeb5a3079c52bb79b42. --- .../flowmvi/plugins/Deprecated.android.kt | 46 +++ .../pro/respawn/flowmvi/plugins/Deprecated.kt | 265 ++++++++++++++++++ .../flowmvi/plugins/Deprecated.native.kt | 22 ++ 3 files changed, 333 insertions(+) create mode 100644 core/src/androidMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.android.kt create mode 100644 core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt create mode 100644 core/src/nativeMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.native.kt diff --git a/core/src/androidMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.android.kt b/core/src/androidMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.android.kt new file mode 100644 index 00000000..9e154f7d --- /dev/null +++ b/core/src/androidMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.android.kt @@ -0,0 +1,46 @@ +package pro.respawn.flowmvi.plugins + +import android.util.Log +import pro.respawn.flowmvi.api.FlowMVIDSL +import pro.respawn.flowmvi.api.LazyPlugin +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.dsl.StoreBuilder +import pro.respawn.flowmvi.logging.StoreLogLevel + +private val Int.asStoreLogLevel + get() = when (this) { + Log.VERBOSE -> StoreLogLevel.Trace + Log.DEBUG -> StoreLogLevel.Debug + Log.WARN -> StoreLogLevel.Warn + Log.INFO -> StoreLogLevel.Info + Log.ASSERT, Log.ERROR -> StoreLogLevel.Error + else -> error("Not an android Log level") + } + +/** + * Create a new [loggingPlugin] that prints using android's [Log]. + */ +@Deprecated( + "Just use logging plugin", + ReplaceWith("loggingPlugin(tag = tag, level = level)") +) +@FlowMVIDSL +public fun androidLoggingPlugin( + tag: String? = null, + level: Int? = null, +): LazyPlugin = loggingPlugin(tag = tag, level = level?.asStoreLogLevel) + +/** + * Create a new [loggingPlugin] that prints using android's [Log]. + */ +@Deprecated( + "Just use logging plugin", + ReplaceWith("logging(name = name, level = level)") +) +@FlowMVIDSL +public fun StoreBuilder.androidLoggingPlugin( + name: String? = null, + level: Int? = null, +): Unit = loggingPlugin(tag = name, level = level?.asStoreLogLevel).let(::install) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt new file mode 100644 index 00000000..c40f8ba3 --- /dev/null +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt @@ -0,0 +1,265 @@ +@file:Suppress("DEPRECATION") + +package pro.respawn.flowmvi.plugins + +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import pro.respawn.flowmvi.api.FlowMVIDSL +import pro.respawn.flowmvi.api.LazyPlugin +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.api.PipelineContext +import pro.respawn.flowmvi.api.Store +import pro.respawn.flowmvi.api.StorePlugin +import pro.respawn.flowmvi.dsl.StoreBuilder +import pro.respawn.flowmvi.dsl.plugin +import pro.respawn.flowmvi.dsl.subscribe +import pro.respawn.flowmvi.logging.ConsoleStoreLogger +import pro.respawn.flowmvi.logging.PlatformStoreLogger +import pro.respawn.flowmvi.logging.StoreLogLevel +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * A logging plugin that prints logs to the console using [println]. Tag is not used except for naming the plugin. + * @see loggingPlugin + */ +@FlowMVIDSL +@Deprecated( + "Just use logging plugin with ConsoleStoreLogger from now on", + ReplaceWith("loggingPlugin(ConsoleStoreLogger, tag, name, level)") +) +public fun consoleLoggingPlugin( + tag: String? = null, + level: StoreLogLevel? = null, +): LazyPlugin = loggingPlugin(tag, level) + +/** + * Creates a [loggingPlugin] that is suitable for each targeted platform. + * This plugin will log to: + * * Logcat on Android, + * * NSLog on apple targets, + * * console.log on JS, + * * stdout on mingw/native + * * System.out on JVM. + */ +@Deprecated( + "Just use logging plugin and a platform store logger from now on", + ReplaceWith("loggingPlugin()") +) +@FlowMVIDSL +public fun platformLoggingPlugin( + tag: String? = null, + level: StoreLogLevel? = null +): LazyPlugin = loggingPlugin(tag, level) + +/** + * A base class for creating custom [StorePlugin]s. + * + * It is preferred to use composition instead of inheriting this class. + * Prefer [pro.respawn.flowmvi.dsl.plugin] builder function instead of extending this class. + * For an example, see how a [jobManagerPlugin] ([JobManager]) is implemented. + * + * @see [StorePlugin] + * @see [pro.respawn.flowmvi.dsl.plugin] + */ +@Deprecated( + """ +Plugin builders provide sufficient functionality to use them instead of this class. +Extending this class limits your API and leaks lifecycle methods of a plugin to external code. +This class will become internal in future releases of the library. +""" +) +public abstract class AbstractStorePlugin( + final override val name: String? = null, +) : StorePlugin { + + final override fun toString(): String = "StorePlugin \"${name ?: super.toString()}\"" + final override fun hashCode(): Int = name?.hashCode() ?: super.hashCode() + final override fun equals(other: Any?): Boolean = when { + other !is StorePlugin<*, *, *> -> false + other.name == null && name == null -> this === other + else -> name == other.name + } +} + +/** + * An overload of the [parentStorePlugin] that also consumes its [MVIAction]s. + * Please see the other overload for more documentation. + * + * @see parentStorePlugin + * @see parentStore + */ +@Suppress("Indentation") // conflicts with IDE formatting +@FlowMVIDSL +public inline fun < + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + A2 : MVIAction + > parentStorePlugin( + parent: Store, + name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, + minExternalSubscriptions: Int = 1, + @BuilderInference crossinline consume: suspend PipelineContext.(action: A2) -> Unit, + @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, +): StorePlugin = whileSubscribedPlugin(name = name, minSubscriptions = minExternalSubscriptions) { + // do not use pipeline context to cancel subscription properly, suspend instead + coroutineScope { + subscribe(parent, { consume(it) }, { render(it) }).join() + } +} + +/** + * Creates and installs a new plugin that will subscribe to the [parent] store at the same time the current store is + * subscribed to and when [minExternalSubscriptions] are reached. + * + * This plugin will subscribe to the parent store and [render] its states while there are [minExternalSubscriptions] of + * the current store present. + * When the subscribers leave as in [whileSubscribedPlugin], this store will also unsubscribe from the parent store. + * + * Essentially, this store will be a subscriber of another store while this store is also subscribed to externally. + * For the behavior where the store will always be subscribed, please subscribe in the [initPlugin]. + * + * This function will not consume [MVIAction]s of the parent store. For that, please see the other overload of this + * function. + * + * The name of this plugin will be derived from the parent store's name, if present, otherwise `null`. + * + * @see parentStorePlugin + * @see parentStore + */ +@Suppress("Indentation") // conflicts with IDE formatting +@FlowMVIDSL +@Deprecated( + """ + Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. + Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() + """, + ReplaceWith( + "whileSubscribedPlugin { parent.collect { } }", + "pro.respawn.flowmvi.plugins.whileSubscribedPlugin", + "pro.respawn.flowmvi.dsl.collect" + ) +) +public inline fun parentStorePlugin( + parent: Store, + name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, + minExternalSubscriptions: Int = 1, + @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, +): StorePlugin = whileSubscribedPlugin(name = name, minSubscriptions = minExternalSubscriptions) { + coroutineScope { + subscribe(parent, render = { render(it) }).join() + } +} + +/** + * Install a new [parentStorePlugin]. This overload **does not** collect the parent store's actions. + * @see parentStorePlugin + */ +@Deprecated( + """ + Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. + Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() + """, + ReplaceWith( + "whileSubscribed { parent.collect { } }", + "pro.respawn.flowmvi.plugins.whileSubscribed", + "pro.respawn.flowmvi.dsl.collect" + ) +) +@Suppress("Indentation") // conflicts with IDE formatting +@FlowMVIDSL +public inline fun < + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + > StoreBuilder.parentStore( + parent: Store, + name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, + minExternalSubscriptions: Int = 1, + @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, +): Unit = install(parentStorePlugin(parent, name, minExternalSubscriptions, render = render)) + +/** + * Install a new [parentStorePlugin]. + * @see parentStorePlugin + */ +@Deprecated( + """ + Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. + Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() + """, + ReplaceWith( + "whileSubscribed { parent.collect { } }", + "pro.respawn.flowmvi.plugins.whileSubscribed", + "pro.respawn.flowmvi.dsl.collect" + ) +) +@Suppress("Indentation") // conflicts with IDE formatting +@FlowMVIDSL +public inline fun < + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + A2 : MVIAction, + > StoreBuilder.parentStore( + parent: Store, + name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, + minExternalSubscriptions: Int = 1, + @BuilderInference crossinline consume: suspend PipelineContext.(action: A2) -> Unit, + @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, +): Unit = install(parentStorePlugin(parent, name, minExternalSubscriptions, consume = consume, render = render)) + +/** + * Default name for the SavedStatePlugin + */ +public const val DefaultSavedStatePluginName: String = "SavedState" + +/** + * A plugin that restores the [pro.respawn.flowmvi.api.StateProvider.state] using [get] in [StorePlugin.onStart] + * and saves using [set] asynchronously in [StorePlugin.onState]. + * There are platform overloads for this function. + */ +@FlowMVIDSL +@Deprecated("If you want to save state, use the new `savedstate` module dependency") +public inline fun savedStatePlugin( + name: String = DefaultSavedStatePluginName, + context: CoroutineContext = EmptyCoroutineContext, + @BuilderInference crossinline get: suspend S.() -> S?, + @BuilderInference crossinline set: suspend (S) -> Unit, +): StorePlugin = plugin { + this.name = name + onState { _, new -> + launch(context) { set(new) } + new + } + onStart { + withContext(context) { + updateState { + get() ?: this + } + } + } +} + +/** + * Creates and installs a new [savedStatePlugin]. + */ +@FlowMVIDSL +@Suppress("DEPRECATION") +@Deprecated("If you want to save state, use the new `savedstate` module dependency") +public inline fun StoreBuilder.saveState( + name: String = DefaultSavedStatePluginName, + context: CoroutineContext = EmptyCoroutineContext, + @BuilderInference crossinline get: suspend S.() -> S?, + @BuilderInference crossinline set: suspend S.() -> Unit, +): Unit = install(savedStatePlugin(name, context, get, set)) diff --git a/core/src/nativeMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.native.kt b/core/src/nativeMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.native.kt new file mode 100644 index 00000000..41dd93ee --- /dev/null +++ b/core/src/nativeMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.native.kt @@ -0,0 +1,22 @@ +package pro.respawn.flowmvi.plugins + +import pro.respawn.flowmvi.api.FlowMVIDSL +import pro.respawn.flowmvi.api.LazyPlugin +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.api.StorePlugin +import pro.respawn.flowmvi.logging.PlatformStoreLogger + +/** + * Log store's events using platform logger. + * @see [loggingPlugin] + */ +@Deprecated( + "Just use logging plugin with PlatformLogger", + ReplaceWith("loggingPlugin(PlatformStoreLogger, name = name)") +) +@FlowMVIDSL +public fun nativeLoggingPlugin( + name: String? = null +): LazyPlugin = loggingPlugin(name) From cbbddd74af994bf6be2298890019c056175016fc Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 12:34:19 +0300 Subject: [PATCH 05/26] fix: bring back removed logging plugin as it was removed too early --- .../flowmvi/logging/AndroidLogPriority.kt | 10 + .../flowmvi/plugins/Deprecated.android.kt | 14 +- .../pro/respawn/flowmvi/plugins/Deprecated.kt | 232 +----------------- .../flowmvi/plugins/Deprecated.native.kt | 10 +- 4 files changed, 20 insertions(+), 246 deletions(-) diff --git a/core/src/androidMain/kotlin/pro/respawn/flowmvi/logging/AndroidLogPriority.kt b/core/src/androidMain/kotlin/pro/respawn/flowmvi/logging/AndroidLogPriority.kt index f56a6642..224450b1 100644 --- a/core/src/androidMain/kotlin/pro/respawn/flowmvi/logging/AndroidLogPriority.kt +++ b/core/src/androidMain/kotlin/pro/respawn/flowmvi/logging/AndroidLogPriority.kt @@ -13,3 +13,13 @@ public val StoreLogLevel.asLogPriority: Int StoreLogLevel.Warn -> Log.WARN StoreLogLevel.Error -> Log.ERROR } + +internal val Int.asStoreLogLevel + get() = when (this) { + Log.VERBOSE -> StoreLogLevel.Trace + Log.DEBUG -> StoreLogLevel.Debug + Log.WARN -> StoreLogLevel.Warn + Log.INFO -> StoreLogLevel.Info + Log.ASSERT, Log.ERROR -> StoreLogLevel.Error + else -> error("Not an android Log level") + } diff --git a/core/src/androidMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.android.kt b/core/src/androidMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.android.kt index 9e154f7d..f67e43e7 100644 --- a/core/src/androidMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.android.kt +++ b/core/src/androidMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.android.kt @@ -7,17 +7,7 @@ import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.dsl.StoreBuilder -import pro.respawn.flowmvi.logging.StoreLogLevel - -private val Int.asStoreLogLevel - get() = when (this) { - Log.VERBOSE -> StoreLogLevel.Trace - Log.DEBUG -> StoreLogLevel.Debug - Log.WARN -> StoreLogLevel.Warn - Log.INFO -> StoreLogLevel.Info - Log.ASSERT, Log.ERROR -> StoreLogLevel.Error - else -> error("Not an android Log level") - } +import pro.respawn.flowmvi.logging.asStoreLogLevel /** * Create a new [loggingPlugin] that prints using android's [Log]. @@ -37,7 +27,7 @@ public fun androidLoggingPlugin( */ @Deprecated( "Just use logging plugin", - ReplaceWith("logging(name = name, level = level)") + ReplaceWith("enableLogging(name = name, level = level)") ) @FlowMVIDSL public fun StoreBuilder.androidLoggingPlugin( diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt index c40f8ba3..0c3a671a 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt @@ -1,26 +1,11 @@ -@file:Suppress("DEPRECATION") - package pro.respawn.flowmvi.plugins -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import pro.respawn.flowmvi.api.FlowMVIDSL import pro.respawn.flowmvi.api.LazyPlugin import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState -import pro.respawn.flowmvi.api.PipelineContext -import pro.respawn.flowmvi.api.Store -import pro.respawn.flowmvi.api.StorePlugin -import pro.respawn.flowmvi.dsl.StoreBuilder -import pro.respawn.flowmvi.dsl.plugin -import pro.respawn.flowmvi.dsl.subscribe -import pro.respawn.flowmvi.logging.ConsoleStoreLogger -import pro.respawn.flowmvi.logging.PlatformStoreLogger import pro.respawn.flowmvi.logging.StoreLogLevel -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext /** * A logging plugin that prints logs to the console using [println]. Tag is not used except for naming the plugin. @@ -28,8 +13,8 @@ import kotlin.coroutines.EmptyCoroutineContext */ @FlowMVIDSL @Deprecated( - "Just use logging plugin with ConsoleStoreLogger from now on", - ReplaceWith("loggingPlugin(ConsoleStoreLogger, tag, name, level)") + "Just use logging plugin from now on", + ReplaceWith("loggingPlugin(tag, name, level)") ) public fun consoleLoggingPlugin( tag: String? = null, @@ -46,220 +31,11 @@ public fun consoleLoggingPlugin( * * System.out on JVM. */ @Deprecated( - "Just use logging plugin and a platform store logger from now on", - ReplaceWith("loggingPlugin()") + "Just use logging plugin from now on", + ReplaceWith("loggingPlugin(tag = tag, level = level)") ) @FlowMVIDSL public fun platformLoggingPlugin( tag: String? = null, level: StoreLogLevel? = null ): LazyPlugin = loggingPlugin(tag, level) - -/** - * A base class for creating custom [StorePlugin]s. - * - * It is preferred to use composition instead of inheriting this class. - * Prefer [pro.respawn.flowmvi.dsl.plugin] builder function instead of extending this class. - * For an example, see how a [jobManagerPlugin] ([JobManager]) is implemented. - * - * @see [StorePlugin] - * @see [pro.respawn.flowmvi.dsl.plugin] - */ -@Deprecated( - """ -Plugin builders provide sufficient functionality to use them instead of this class. -Extending this class limits your API and leaks lifecycle methods of a plugin to external code. -This class will become internal in future releases of the library. -""" -) -public abstract class AbstractStorePlugin( - final override val name: String? = null, -) : StorePlugin { - - final override fun toString(): String = "StorePlugin \"${name ?: super.toString()}\"" - final override fun hashCode(): Int = name?.hashCode() ?: super.hashCode() - final override fun equals(other: Any?): Boolean = when { - other !is StorePlugin<*, *, *> -> false - other.name == null && name == null -> this === other - else -> name == other.name - } -} - -/** - * An overload of the [parentStorePlugin] that also consumes its [MVIAction]s. - * Please see the other overload for more documentation. - * - * @see parentStorePlugin - * @see parentStore - */ -@Suppress("Indentation") // conflicts with IDE formatting -@FlowMVIDSL -public inline fun < - S : MVIState, - I : MVIIntent, - A : MVIAction, - S2 : MVIState, - I2 : MVIIntent, - A2 : MVIAction - > parentStorePlugin( - parent: Store, - name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, - minExternalSubscriptions: Int = 1, - @BuilderInference crossinline consume: suspend PipelineContext.(action: A2) -> Unit, - @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, -): StorePlugin = whileSubscribedPlugin(name = name, minSubscriptions = minExternalSubscriptions) { - // do not use pipeline context to cancel subscription properly, suspend instead - coroutineScope { - subscribe(parent, { consume(it) }, { render(it) }).join() - } -} - -/** - * Creates and installs a new plugin that will subscribe to the [parent] store at the same time the current store is - * subscribed to and when [minExternalSubscriptions] are reached. - * - * This plugin will subscribe to the parent store and [render] its states while there are [minExternalSubscriptions] of - * the current store present. - * When the subscribers leave as in [whileSubscribedPlugin], this store will also unsubscribe from the parent store. - * - * Essentially, this store will be a subscriber of another store while this store is also subscribed to externally. - * For the behavior where the store will always be subscribed, please subscribe in the [initPlugin]. - * - * This function will not consume [MVIAction]s of the parent store. For that, please see the other overload of this - * function. - * - * The name of this plugin will be derived from the parent store's name, if present, otherwise `null`. - * - * @see parentStorePlugin - * @see parentStore - */ -@Suppress("Indentation") // conflicts with IDE formatting -@FlowMVIDSL -@Deprecated( - """ - Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. - Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() - """, - ReplaceWith( - "whileSubscribedPlugin { parent.collect { } }", - "pro.respawn.flowmvi.plugins.whileSubscribedPlugin", - "pro.respawn.flowmvi.dsl.collect" - ) -) -public inline fun parentStorePlugin( - parent: Store, - name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, - minExternalSubscriptions: Int = 1, - @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, -): StorePlugin = whileSubscribedPlugin(name = name, minSubscriptions = minExternalSubscriptions) { - coroutineScope { - subscribe(parent, render = { render(it) }).join() - } -} - -/** - * Install a new [parentStorePlugin]. This overload **does not** collect the parent store's actions. - * @see parentStorePlugin - */ -@Deprecated( - """ - Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. - Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() - """, - ReplaceWith( - "whileSubscribed { parent.collect { } }", - "pro.respawn.flowmvi.plugins.whileSubscribed", - "pro.respawn.flowmvi.dsl.collect" - ) -) -@Suppress("Indentation") // conflicts with IDE formatting -@FlowMVIDSL -public inline fun < - S : MVIState, - I : MVIIntent, - A : MVIAction, - S2 : MVIState, - I2 : MVIIntent, - > StoreBuilder.parentStore( - parent: Store, - name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, - minExternalSubscriptions: Int = 1, - @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, -): Unit = install(parentStorePlugin(parent, name, minExternalSubscriptions, render = render)) - -/** - * Install a new [parentStorePlugin]. - * @see parentStorePlugin - */ -@Deprecated( - """ - Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. - Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() - """, - ReplaceWith( - "whileSubscribed { parent.collect { } }", - "pro.respawn.flowmvi.plugins.whileSubscribed", - "pro.respawn.flowmvi.dsl.collect" - ) -) -@Suppress("Indentation") // conflicts with IDE formatting -@FlowMVIDSL -public inline fun < - S : MVIState, - I : MVIIntent, - A : MVIAction, - S2 : MVIState, - I2 : MVIIntent, - A2 : MVIAction, - > StoreBuilder.parentStore( - parent: Store, - name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, - minExternalSubscriptions: Int = 1, - @BuilderInference crossinline consume: suspend PipelineContext.(action: A2) -> Unit, - @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, -): Unit = install(parentStorePlugin(parent, name, minExternalSubscriptions, consume = consume, render = render)) - -/** - * Default name for the SavedStatePlugin - */ -public const val DefaultSavedStatePluginName: String = "SavedState" - -/** - * A plugin that restores the [pro.respawn.flowmvi.api.StateProvider.state] using [get] in [StorePlugin.onStart] - * and saves using [set] asynchronously in [StorePlugin.onState]. - * There are platform overloads for this function. - */ -@FlowMVIDSL -@Deprecated("If you want to save state, use the new `savedstate` module dependency") -public inline fun savedStatePlugin( - name: String = DefaultSavedStatePluginName, - context: CoroutineContext = EmptyCoroutineContext, - @BuilderInference crossinline get: suspend S.() -> S?, - @BuilderInference crossinline set: suspend (S) -> Unit, -): StorePlugin = plugin { - this.name = name - onState { _, new -> - launch(context) { set(new) } - new - } - onStart { - withContext(context) { - updateState { - get() ?: this - } - } - } -} - -/** - * Creates and installs a new [savedStatePlugin]. - */ -@FlowMVIDSL -@Suppress("DEPRECATION") -@Deprecated("If you want to save state, use the new `savedstate` module dependency") -public inline fun StoreBuilder.saveState( - name: String = DefaultSavedStatePluginName, - context: CoroutineContext = EmptyCoroutineContext, - @BuilderInference crossinline get: suspend S.() -> S?, - @BuilderInference crossinline set: suspend S.() -> Unit, -): Unit = install(savedStatePlugin(name, context, get, set)) diff --git a/core/src/nativeMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.native.kt b/core/src/nativeMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.native.kt index 41dd93ee..8bbee30a 100644 --- a/core/src/nativeMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.native.kt +++ b/core/src/nativeMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.native.kt @@ -5,18 +5,16 @@ import pro.respawn.flowmvi.api.LazyPlugin import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState -import pro.respawn.flowmvi.api.StorePlugin -import pro.respawn.flowmvi.logging.PlatformStoreLogger /** * Log store's events using platform logger. * @see [loggingPlugin] */ @Deprecated( - "Just use logging plugin with PlatformLogger", - ReplaceWith("loggingPlugin(PlatformStoreLogger, name = name)") + "Just use logging plugin", + ReplaceWith("loggingPlugin(tag = tag, name = name)") ) @FlowMVIDSL public fun nativeLoggingPlugin( - name: String? = null -): LazyPlugin = loggingPlugin(name) + tag: String? = null, +): LazyPlugin = loggingPlugin(tag = tag) From 404b51628236465314f1bf513f369831074ea48b Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 12:34:40 +0300 Subject: [PATCH 06/26] feat: allow overriding name and logger for the logging plugin --- .../respawn/flowmvi/plugins/LoggingPlugin.kt | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/LoggingPlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/LoggingPlugin.kt index 1bebcc34..da7e8dc4 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/LoggingPlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/LoggingPlugin.kt @@ -15,6 +15,7 @@ import pro.respawn.flowmvi.logging.StoreLogLevel.Debug import pro.respawn.flowmvi.logging.StoreLogLevel.Error import pro.respawn.flowmvi.logging.StoreLogLevel.Info import pro.respawn.flowmvi.logging.StoreLogLevel.Trace +import pro.respawn.flowmvi.logging.StoreLogger import pro.respawn.flowmvi.logging.invoke import kotlin.math.log @@ -28,51 +29,55 @@ import kotlin.math.log public fun StoreBuilder.enableLogging( tag: String? = null, level: StoreLogLevel? = null, -): Unit = loggingPlugin(tag, level).let(::install) + name: String? = null, + logger: StoreLogger? = null, +): Unit = loggingPlugin(tag, level, name, logger).let(::install) /** * Create a new [StorePlugin] that prints messages using [log]. - * [tag] is used as a name for the plugin. - * Tag can be null, in which case, store's name will be used. Provide an empty string to remove the tag. - * [level] level override to print all messages. If null, a default level will be used (null by default) + * * [tag] is used as a name for the plugin, unless overridden by [name]. Tag can be null, in which case, store's name will be used. Provide an empty string to remove the tag. + * * [level] level override to print all messages. If null, a default level will be used (null by default) + * * [logger] Unless a non-null value is provided, a store logger will be used. */ @Suppress("CyclomaticComplexMethod") // false-positive based on ternary ops @FlowMVIDSL public fun loggingPlugin( tag: String? = null, level: StoreLogLevel? = null, + name: String? = null, + logger: StoreLogger? = null, ): LazyPlugin = lazyPlugin { val currentTag = tag ?: config.name - this.name = currentTag.let { "${it.orEmpty()}Logging" } - val logger = config.logger + this.name = name ?: currentTag.let { "${it.orEmpty()}Logging" } + val log = logger ?: config.logger onState { old, new -> if (old == new) return@onState new - new.also { logger(level ?: Trace, currentTag) { "State:\n--->\n$old\n<---\n$new" } } + new.also { log(level ?: Trace, currentTag) { "State:\n--->\n$old\n<---\n$new" } } } onIntent { - it.also { logger(level ?: Debug, currentTag) { "Intent -> $it" } } + it.also { log(level ?: Debug, currentTag) { "Intent -> $it" } } } onAction { - it.also { logger(level ?: Debug, currentTag) { "Action -> $it" } } + it.also { log(level ?: Debug, currentTag) { "Action -> $it" } } } onException { - it.also { logger(it, level ?: Error, currentTag) } + it.also { log(it, level ?: Error, currentTag) } } onStart { - logger(level ?: Info, currentTag) { "Started ${config.name ?: "Store"}" } + log(level ?: Info, currentTag) { "Started ${config.name ?: "Store"}" } } onSubscribe { - logger(level ?: Info, currentTag) { "New subscriber #${it + 1}" } + log(level ?: Info, currentTag) { "New subscriber #${it + 1}" } } onUnsubscribe { - logger(level ?: Info, currentTag) { "Subscriber #${it + 1} removed" } + log(level ?: Info, currentTag) { "Subscriber #${it + 1} removed" } } - onStop { - if (it == null) { - logger(level ?: Info, currentTag) { "Stopped ${config.name ?: "Store"}" } + onStop { e -> + if (e == null) { + log(level ?: Info, currentTag) { "Stopped ${config.name ?: "Store"}" } return@onStop } - logger(level ?: Error, currentTag) { "Stopped with exception: " } - logger(it, level ?: Error, currentTag) + log(level ?: Error, currentTag) { "Stopped with exception: " } + log(e, level ?: Error, currentTag) } } From b7b226fce03977f85e62b2657386155f9fdc6dc0 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 12:38:07 +0300 Subject: [PATCH 07/26] fix: bring back removed nameByType temporarily to reduce migration complexity --- .../kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt index 02cde423..17da4b4c 100644 --- a/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt +++ b/savedstate/src/commonMain/kotlin/pro/respawn/flowmvi/savedstate/util/Util.kt @@ -3,6 +3,7 @@ package pro.respawn.flowmvi.savedstate.util import kotlinx.coroutines.CancellationException import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.savedstate.api.Saver @PublishedApi @@ -42,3 +43,9 @@ internal val DefaultJson: Json = Json { allowTrailingComma = true useAlternativeNames = true } + +/** + * Get the name of the class, removing the "State" suffix, if present. + */ +@Deprecated("Usage of this function leads to some unintended consequences when enabling code obfuscation") +public inline fun nameByType(): String? = T::class.simpleName?.removeSuffix("State") From a0cd05c1616b8e12a49c0660ea53a8aa80ae5ecc Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 12:44:58 +0300 Subject: [PATCH 08/26] feat: undeprecate overloads of `subscribe` where the lifecycle is not provided explicitly --- .../respawn/flowmvi/compose/dsl/ComposeDsl.kt | 4 +- .../respawn/flowmvi/compose/dsl/Deprecated.kt | 64 ------------------- 2 files changed, 2 insertions(+), 66 deletions(-) delete mode 100644 compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/Deprecated.kt diff --git a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ComposeDsl.kt b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ComposeDsl.kt index 2a464ba0..725aa6e7 100644 --- a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ComposeDsl.kt +++ b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/ComposeDsl.kt @@ -44,7 +44,7 @@ import kotlin.jvm.JvmName @FlowMVIDSL @JvmName("subscribeConsume") public fun ImmutableStore.subscribe( - lifecycle: SubscriberLifecycle, + lifecycle: SubscriberLifecycle = DefaultLifecycle, mode: SubscriptionMode = SubscriptionMode.Started, consume: suspend CoroutineScope.(action: A) -> Unit, ): State { @@ -80,7 +80,7 @@ public fun ImmutableStore. @Composable @FlowMVIDSL public fun ImmutableStore.subscribe( - lifecycle: SubscriberLifecycle, + lifecycle: SubscriberLifecycle = DefaultLifecycle, mode: SubscriptionMode = SubscriptionMode.Started, ): State { val state = remember(this) { mutableStateOf(state) } diff --git a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/Deprecated.kt b/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/Deprecated.kt deleted file mode 100644 index 74ff79a6..00000000 --- a/compose/src/commonMain/kotlin/pro/respawn/flowmvi/compose/dsl/Deprecated.kt +++ /dev/null @@ -1,64 +0,0 @@ -package pro.respawn.flowmvi.compose.dsl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import kotlinx.coroutines.CoroutineScope -import pro.respawn.flowmvi.api.FlowMVIDSL -import pro.respawn.flowmvi.api.ImmutableStore -import pro.respawn.flowmvi.api.MVIAction -import pro.respawn.flowmvi.api.MVIIntent -import pro.respawn.flowmvi.api.MVIState -import pro.respawn.flowmvi.api.SubscriptionMode -import pro.respawn.flowmvi.dsl.subscribe - -private const val Message = """ -An overload without an explicit Lifecycle parameter is deprecated as it is error prone in environments where -custom lifecycle is provided by the system or a navigation library. Please pass one of: -- requireLifecycle() if you provide a lifecycle via a composition local -- DefaultLifecycle if you wish to use a platform lifecycle if no composition local is provided -- A custom component that implements Lifecycle owner if you are using an integration library -""" - -/** - * A function to subscribe to the store that follows the system lifecycle. - * - * * This function will assign the store a new subscriber when invoked, then populate the returned [State] with new states. - * * Provided [consume] parameter will be used to consume actions that come from the store. - * * Store's subscribers will **not** wait until the store is launched when they subscribe to the store. - * Such subscribers will not receive state updates or actions. Don't forget to launch the store. - * - * @param mode the subscription mode that should be reached in order to subscribe to the store. At specified moments - * in the UI lifecycle (Activity, Composable, Window etc), the store will subscribe and unsubscribe from the store. - * @param consume a lambda to consume actions with. - * @return the [State] that contains the current state. - * @see ImmutableStore.subscribe - * @see subscribe - */ -@Deprecated(Message, ReplaceWith("this.subscribe(DefaultLifecycle, mode, consume)")) -@Suppress("ComposableParametersOrdering") -@Composable -@FlowMVIDSL -public fun ImmutableStore.subscribe( - mode: SubscriptionMode = SubscriptionMode.Started, - consume: suspend CoroutineScope.(action: A) -> Unit, -): State = subscribe(DefaultLifecycle, mode, consume) - -/** - * A function to subscribe to the store that follows the system lifecycle. - * - * * This function will assign the store a new subscriber when invoked, then populate the returned [State] with new states. - * * Store's subscribers will **not** wait until the store is launched when they subscribe to the store. - * Such subscribers will not receive state updates or actions. Don't forget to launch the store. - * - * @param mode the subscription mode that should be reached in order to subscribe to the store. At specified moments - * in the UI lifecycle (Activity, Composable, Window etc), the store will subscribe and unsubscribe from the store. - * @return the [State] that contains the current state. - * @see ImmutableStore.subscribe - * @see subscribe - */ -@Deprecated(Message, ReplaceWith("this.subscribe(DefaultLifecycle, mode)")) -@Composable -@FlowMVIDSL -public fun ImmutableStore.subscribe( - mode: SubscriptionMode = SubscriptionMode.Started, -): State = subscribe(DefaultLifecycle, mode) From 3f91484e72b51755f44c9a96066e18e3d6afeac1 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 12:48:07 +0300 Subject: [PATCH 09/26] fixup! feat: update to kotlin 2.0 --- build.gradle.kts | 1 - buildSrc/src/main/kotlin/Config.kt | 1 - buildSrc/src/main/kotlin/ConfigureMultiplatform.kt | 1 - buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts | 1 - sample/build.gradle.kts | 1 - 5 files changed, 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b0411062..bb92d6be 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,7 +43,6 @@ allprojects { compilerOptions { optIn.addAll(Config.optIns) jvmTarget = Config.jvmTarget - languageVersion = Config.kotlinVersion freeCompilerArgs.apply { addAll(Config.jvmCompilerArgs) } } } diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 815600b5..6f393a1f 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -74,7 +74,6 @@ object Config { val jvmTarget = JvmTarget.JVM_11 val idePluginJvmTarget = JvmTarget.JVM_17 val javaVersion = JavaVersion.VERSION_11 - val kotlinVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 const val compileSdk = 34 const val targetSdk = compileSdk const val minSdk = 21 diff --git a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt index f354c667..89415d08 100644 --- a/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt +++ b/buildSrc/src/main/kotlin/ConfigureMultiplatform.kt @@ -91,7 +91,6 @@ fun Project.configureMultiplatform( all { languageSettings { progressiveMode = true - languageVersion = Config.kotlinVersion.version Config.optIns.forEach { optIn(it) } } } diff --git a/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts b/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts index 7d34e316..edce2041 100644 --- a/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts +++ b/buildSrc/src/main/kotlin/pro.respawn.android-library.gradle.kts @@ -15,6 +15,5 @@ android { kotlinOptions { jvmTarget = Config.jvmTarget.target - languageVersion = Config.kotlinVersion.version } } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 67a73c29..2bf83251 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -63,7 +63,6 @@ kotlin { all { languageSettings { progressiveMode = true - languageVersion = Config.kotlinVersion.version Config.optIns.forEach { optIn(it) } } } From 83162b0ec80d3c44e063f2adea9ce7fb48f4130f Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 12:49:19 +0300 Subject: [PATCH 10/26] remove redundant compose compiler flags --- build.gradle.kts | 28 +++++++++++++++------------- buildSrc/src/main/kotlin/Config.kt | 2 -- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index bb92d6be..a40369a3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ import nl.littlerobots.vcu.plugin.versionCatalogUpdate import nl.littlerobots.vcu.plugin.versionSelector +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradleSubplugin import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension @@ -26,19 +28,19 @@ plugins { allprojects { group = Config.artifactId version = Config.versionName - // plugins.withType().configureEach { - // the().apply { - // enableIntrinsicRemember = true - // enableNonSkippingGroupOptimization = true - // enableStrongSkippingMode = true - // stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_definitions.txt") - // if (properties["enableComposeCompilerReports"] == "true") { - // val metricsDir = layout.buildDirectory.dir("compose_metrics") - // metricsDestination = metricsDir - // reportsDestination = metricsDir - // } - // } - // } + plugins.withType().configureEach { + the().apply { + enableIntrinsicRemember = true + enableNonSkippingGroupOptimization = true + enableStrongSkippingMode = true + stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_definitions.txt") + if (properties["enableComposeCompilerReports"] == "true") { + val metricsDir = layout.buildDirectory.dir("compose_metrics") + metricsDestination = metricsDir + reportsDestination = metricsDir + } + } + } tasks.withType().configureEach { compilerOptions { optIn.addAll(Config.optIns) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 6f393a1f..ca38c5fd 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -60,8 +60,6 @@ object Config { val compilerArgs = listOf( "-Xbackend-threads=0", // parallel IR compilation "-Xexpect-actual-classes", - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true" ) val jvmCompilerArgs = buildList { addAll(compilerArgs) From d1f3d5c0b4c6e0f5d9b009ab4e4a245b42e5b797 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 12:59:08 +0300 Subject: [PATCH 11/26] update docs for the new compose compiler --- docs/compose.md | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/compose.md b/docs/compose.md index f3e44b0d..6c6bacd8 100644 --- a/docs/compose.md +++ b/docs/compose.md @@ -33,29 +33,22 @@ pro.respawn.flowmvi.api.IntentReceiver -Then configure compose compiler to account for the definitions in your root `build.gradle.kts`: +Then configure compose compiler to account for the definitions in your feature's `build.gradle.kts`:
-/build.gradle.kts +feature-module/build.gradle.kts ```kotlin -allprojects { - tasks.withType().configureEach { - compilerOptions { - freeCompilerArgs.addAll( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" + - "${rootProject.rootDir.absolutePath}/stability_definitions.txt" - ) - } - } +composeCompiler { + stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_definitions.txt") } ```
Now the states/intents you create will be stable in compose. Immutability of these classes is already required by the -library, so this will ensure you get the best performance. +library, so this will ensure you get the best performance. See the project's gradle configuration if you want to learn +how to set compose compiler configuration globally and/or in gradle conventions. ## Step 3: Subscribe to Stores @@ -71,7 +64,7 @@ fun CounterScreen( container: CounterContainer, ) = with(container.store) { - val state by subscribe(DefaultLifecycle) { action -> + val state by subscribe { action -> when (action) { is ShowMessage -> { /* ... */ @@ -83,10 +76,13 @@ fun CounterScreen( } ``` -Under the hood, the `subscribe` function will efficiently subscribe to the store (it is lifecycle-aware) and +Under the hood, the `subscribe` function will efficiently subscribe to the store and use the composition scope to process your events. Event processing will stop when the UI is no longer visible (by default). When the UI is visible again, the function will re-subscribe. Your composable will recompose when the state -changes. +changes. By default, the function will use your system's default lifecycle, provided by the +compose `LocalLifecycleOwner`. If you are using a custom lifecycle implementation e.g. provided by the navigation +library, you can use that lifecycle by providing it as a `LocalSubscriberLifecycle` composition local or passing it as +a parameter to the `subscribe` function. Use the lambda parameter of `subscribe` to subscribe to `MVIActions`. Those will be processed as they arrive and the `consume` lambda will **suspend** until an action is processed. Use a receiver coroutine scope to @@ -119,17 +115,12 @@ When you have defined your `*Content` function, you will get a composable that c That composable will not need DI, Local Providers from compose, or anything else for that matter, to draw itself. But there's a catch: It has an `IntentReceiver` as a parameter. To deal with this, there is an `EmptyReceiver` composable. EmptyReceiver does nothing when an intent is sent, which is exactly what we want for previews. We can now -define our `PreviewParameterProvider` and the Preview composable. +define our `PreviewParameterProvider` and the Preview composable. You won't need an `EmptyReceiver` if you pass the +`intent` callback manually. ```kotlin -// vararg preview provider for convenience -open class PreviewProvider( - vararg values: T, -) : CollectionPreviewParameterProvider(values.toList()) - -private class StateProvider : PreviewProvider( - DisplayingCounter(counter = 1), - Loading, +private class StateProvider : CollectionPreviewParameterProvider( + listOf(DisplayingCounter(counter = 1), Loading) ) @Composable @@ -137,6 +128,6 @@ private class StateProvider : PreviewProvider( private fun CounterScreenPreview( @PreviewParameter(StateProvider::class) state: CounterState, ) = EmptyReceiver { - ComposeScreenContent(state) + CounterScreenContent(state) } ``` From e54f390742a0dcfb0cb90f35499f3f556005eae6 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 13:31:18 +0300 Subject: [PATCH 12/26] feat: deprecate `useState` and rename to `updateStateImmediate` with auto-replace --- .../pro/respawn/flowmvi/api/StateReceiver.kt | 57 ++++++++++++------- .../pro/respawn/flowmvi/api/StorePlugin.kt | 2 +- .../pro/respawn/flowmvi/dsl/StateDsl.kt | 39 ++++++------- .../flowmvi/dsl/StoreConfigurationBuilder.kt | 2 +- .../respawn/flowmvi/modules/RecoverModule.kt | 1 - .../respawn/flowmvi/modules/StateModule.kt | 2 +- .../flowmvi/test/store/StoreStatesTest.kt | 4 +- .../debugger/server/DebugServerStore.kt | 4 +- docs/custom_plugins.md | 2 +- docs/faq.md | 3 +- docs/plugins.md | 4 +- docs/quickstart.md | 4 +- .../savedstate/SavedStateContainer.kt | 4 +- .../features/undoredo/UndoRedoContainer.kt | 6 +- .../features/undoredo/UndoRedoScreen.kt | 6 +- .../test/plugin/TestPipelineContext.kt | 2 +- 16 files changed, 77 insertions(+), 65 deletions(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StateReceiver.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StateReceiver.kt index dad672bc..5f7fbdb9 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StateReceiver.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StateReceiver.kt @@ -7,29 +7,30 @@ package pro.respawn.flowmvi.api public interface StateReceiver { /** - * Obtain the current [StateProvider.state] and update it with the result of [transform] - * atomically and in a suspending manner. + * Obtain the current [StateProvider.state] and update it with the result of [transform]. * - * **This function will suspend until all previous [withState] or [updateState] invocations are finished.** - * **This function is reentrant, for more info, see [withState].** - * - * If you want to operate on a state of particular subtype, use the typed version of this function. + * * **This function will suspend until all previous [withState] or [updateState] invocations are finished if + * [StoreConfiguration.atomicStateUpdates] is enabled** + * * **This function is reentrant, for more info, see [withState]** + * * If you want to operate on a state of particular subtype, use the typed version of this function. + * * If you wish to ignore plugins and thread-safety of state updates in favor of greater performance, + * see [updateStateImmediate] * * @see [withState] + * @see [updateStateImmediate] */ @FlowMVIDSL public suspend fun updateState(transform: suspend S.() -> S) /** - * Obtain the current state and operate on it, returning [S]. - * - * This function does NOT update the state, for that, use [updateState]. - * Store allows only one state update at a time, and because of that, - * **every coroutine that will invoke [withState] or [updateState] - * will be suspended until the previous state handler is finished.** + * Obtain the current state and operate on it without changing the state. * - * This function uses locks under the hood. - * For a version that runs when the state is of particular subtype, see other overloads of this function. + * * This function does NOT update the state, for that, use [updateState]. + * * **This function will suspend until all previous [withState] or [updateState] invocations are finished if + * [StoreConfiguration.atomicStateUpdates] is enabled** + * * If you want to operate on a state of particular subtype, use the typed version of this function. + * * If you wish to ignore plugins and thread-safety of state updates in favor of greater performance, + * see [updateStateImmediate] * * This function is reentrant, which means, if you call: * ```kotlin @@ -37,9 +38,7 @@ public interface StateReceiver { * withState { } * } * ``` - * you should not get a deadlock, but overriding coroutine contexts can still cause problems. - * This function has lower performance than [useState] and allows plugins to intercept the state change. - * If you really need the additional performance or wish to avoid plugins, use [useState]. + * you will not get a deadlock. * * @returns the value of [S], i.e. the result of the block. */ @@ -49,17 +48,33 @@ public interface StateReceiver { /** * A function that obtains current state and updates it atomically (in the thread context), and non-atomically in * the coroutine context, which means it can cause races when you want to update states in parallel. - * This function is performant, but **circumvents ALL plugins** and is **not thread-safe**. - * It should only be used for the most critical state updates happening very often. + * + * This function is performant, but **ignores ALL plugins** and + * **does not perform a serializable state transaction** + * + * It should only be used for the state updates that demand the highest performance and happen very often. + * If [StoreConfiguration.atomicStateUpdates] is `false`, then this function is the same as [updateState] + * + * @see updateState + * @see withState */ @FlowMVIDSL - public fun useState(block: S.() -> S) + public fun updateStateImmediate(block: S.() -> S) /** * Obtain the current value of state in an unsafe manner. - * It is recommended to always use [withState] or [updateState] always as obtaining this value can lead + * It is recommended to always use [withState] or [updateState] as obtaining this value can lead * to data races when the state transaction changes the value of the state previously obtained. */ @DelicateStoreApi public val state: S + + // region deprecated + + @FlowMVIDSL + @Suppress("UndocumentedPublicFunction") + @Deprecated("renamed to updateStateImmediate()", ReplaceWith("updateStateImmediate(block)")) + public fun useState(block: S.() -> S): Unit = updateStateImmediate(block) + + // endregion } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StorePlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StorePlugin.kt index bb11005b..d01f8660 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StorePlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StorePlugin.kt @@ -55,7 +55,7 @@ public interface StorePlugin : LazyP * or modify the state change. * * This callback is invoked **after** a [StateReceiver.updateState] call is finished. - * This callback is **not** invoked at all when state is changed through [StateReceiver.useState] + * This callback is **not** invoked at all when state is changed through [StateReceiver.updateStateImmediate] * or when [StateProvider.state] is obtained. * * * Return null to cancel the state change. All plugins registered later when building the store will not receive diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StateDsl.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StateDsl.kt index 476523b0..0ac1a806 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StateDsl.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StateDsl.kt @@ -1,3 +1,5 @@ +@file:Suppress("DEPRECATION") + package pro.respawn.flowmvi.dsl import pro.respawn.flowmvi.api.FlowMVIDSL @@ -10,12 +12,7 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** - * Run [block] if current [StateProvider.states] value is of type [T], otherwise do nothing. - * - * **This function will suspend until all previous [StateReceiver.withState] invocations are finished.** - * @see StateReceiver.withState - * @see StateReceiver.useState - * @see StateReceiver.updateState + * A typed overload of [StateReceiver.withState]. */ @FlowMVIDSL public suspend inline fun StateReceiver.withState( @@ -28,13 +25,7 @@ public suspend inline fun StateReceiver.withSta } /** - * Obtain the current [StateProvider.states] value and update it with - * the result of [transform] if it is of type [T], otherwise do nothing. - * - * **This function will suspend until all previous [StateReceiver.withState] invocations are finished.** - * @see StateReceiver.withState - * @see StateReceiver.useState - * @see StateReceiver.updateState + * A typed overload of [StateReceiver.updateState]. */ @FlowMVIDSL public suspend inline fun StateReceiver.updateState( @@ -47,18 +38,24 @@ public suspend inline fun StateReceiver.updateS } /** - * Obtain the current [StateProvider.states] value and update it with - * the result of [transform] if it is of type [T], otherwise do nothing. - * - * * **This function may be executed multiple times** - * * **This function will not trigger any plugins. It is intended for performance-critical operations only** - * * **This function does lock the state. Watch out for races** + * A typed overload of [StateReceiver.updateStateImmediate]. * * @see StateReceiver.withState - * @see StateReceiver.useState + * @see StateReceiver.updateStateImmediate * @see StateReceiver.updateState */ @FlowMVIDSL +public inline fun StateReceiver.updateStateImmediate( + @BuilderInference crossinline transform: T.() -> S +): Unit = updateStateImmediate { withType { transform() } } + +// region deprecated + +@FlowMVIDSL +@Suppress("UndocumentedPublicFunction") +@Deprecated("renamed to updateStateImmediate()", ReplaceWith("updateStateImmediate(block)")) public inline fun StateReceiver.useState( @BuilderInference crossinline transform: T.() -> S -): Unit = useState { withType { transform() } } +): Unit = updateStateImmediate { withType { transform() } } + +// endregion diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreConfigurationBuilder.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreConfigurationBuilder.kt index 559bb3ac..3cbbe283 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreConfigurationBuilder.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreConfigurationBuilder.kt @@ -104,7 +104,7 @@ public class StoreConfigurationBuilder @PublishedApi internal constructor() { * * Synchronizes state updates, allowing only **one** client to read and/or update the state at a time. * All other clients attempt to get the state will wait on a FIFO queue and suspend the parent coroutine. * * This property disables state transactions for the whole store. - * For one-time usage of non-atomic updates, see [useState]. + * For one-time usage of non-atomic updates, see [updateStateImmediate]. * * Has a small performance impact because of coroutine context switching and mutex usage. * * `true` by default diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/RecoverModule.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/RecoverModule.kt index d8f8e7e0..449d562d 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/RecoverModule.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/RecoverModule.kt @@ -28,7 +28,6 @@ internal fun recoverModule( /** * An entity that can [recover] from exceptions happening during its lifecycle. Most often, a [Store] */ -@Suppress("FUN_INTERFACE_WITH_SUSPEND_FUNCTION") // https://youtrack.jetbrains.com/issue/KTIJ-7642 internal fun interface RecoverModule : CoroutineContext.Element { override val key: CoroutineContext.Key<*> get() = RecoverModule diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/StateModule.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/StateModule.kt index 69c26e25..494d9fa4 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/StateModule.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/StateModule.kt @@ -32,7 +32,7 @@ private class StateModuleImpl( @DelicateStoreApi override val state: S by states::value - override inline fun useState(block: S.() -> S) = _states.update(block) + override inline fun updateStateImmediate(block: S.() -> S) = _states.update(block) override suspend inline fun withState( crossinline block: suspend S.() -> Unit diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreStatesTest.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreStatesTest.kt index cde1a7b1..beddd65a 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreStatesTest.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreStatesTest.kt @@ -61,13 +61,13 @@ class StoreStatesTest : FreeSpec({ } } } - "then useState overrides the state locks" { + "then updateStateImmediate overrides the state locks" { val newState = TestState.SomeData(1) store.subscribeAndTest { states.test { awaitItem() shouldBe TestState.Some intent(blockingIntent) - intent { useState { newState } } + intent { updateStateImmediate { newState } } awaitItem() shouldBe newState state shouldBe newState } diff --git a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/DebugServerStore.kt b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/DebugServerStore.kt index b29179c1..145eb1aa 100644 --- a/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/DebugServerStore.kt +++ b/debugger/server/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/server/DebugServerStore.kt @@ -43,8 +43,8 @@ internal fun debugServerStore() = lazyStore(Idle) { reduce { intent -> when (intent) { is RestoreRequested -> updateState { previous } - is StopRequested -> useState { Idle } // needs to be fast - is ServerStarted -> useState { Running() } + is StopRequested -> updateStateImmediate { Idle } // needs to be fast + is ServerStarted -> updateStateImmediate { Running() } is EventReceived -> state { when (val event = intent.event) { is StoreDisconnected -> { diff --git a/docs/custom_plugins.md b/docs/custom_plugins.md index 5a4f395d..3871565e 100644 --- a/docs/custom_plugins.md +++ b/docs/custom_plugins.md @@ -93,7 +93,7 @@ A callback to be invoked each time `updateState` is called. This callback is invoked **before** the state changes, but **after** the function's `block` is invoked. Any plugin can veto (forbid) or modify the state change. -This callback is **not** invoked at all when state is changed through `useState`, used in `withState` +This callback is **not** invoked at all when state is changed through `updateStateImmediate`, used in `withState` or when `state` is obtained directly. * Return null to cancel the state change. All plugins registered later when building the store will not receive diff --git a/docs/faq.md b/docs/faq.md index 32ba71ba..fa6a451c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -7,7 +7,8 @@ * Use nested class imports and import aliases to clean up your code, as contract class names can be long sometimes. * Use value classes to reduce object allocations if your Intents are being sent frequently, such as for text field value changes or scroll events. - * You can use the `useState` function to optimize the performance of the store by bypassing all checks and plugins. + * You can use the `updateStateImmediate` function to optimize the + performance of the store by bypassing all checks and plugins. * Overall, there are cases when changes are so frequent that you'll want to just leave some logic on the UI layer to avoid polluting the heap with garbage collected objects and keep the UI performant. * Avoid subscribing to a bunch of flows in your Store. The best way to implement a reactive UI pattern is to diff --git a/docs/plugins.md b/docs/plugins.md index a995d794..a0b99044 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -487,8 +487,8 @@ val store = store(Loading) { is ClickedRedo -> undoRedo.redo() is ClickedUndo -> undoRedo.undo() is ChangedInput -> undoRedo( - redo = { useState { copy(input = intent.current) } }, - undo = { useState { copy(input = intent.previous) } }, + redo = { updateState { copy(input = intent.current) } }, + undo = { updateState { copy(input = intent.previous) } }, ) } } diff --git a/docs/quickstart.md b/docs/quickstart.md index 1023c093..c440c990 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -253,8 +253,8 @@ val store = store(Loading) { // set * `atomicStateUpdates` - Enables transaction serialization for state updates, making state updates atomic and suspendable. Synchronizes state updates, allowing only **one** client to read and/or update the state at a time. All other clients that attempt to get the state will wait in a FIFO queue and suspend the parent coroutine. For one-time - usage of non-atomic updates, see `useState`. Has a small performance impact because of coroutine context switching and - mutex usage when enabled. + usage of non-atomic updates, see `updateStateImediate`. + Has a small performance impact because of coroutine context switching and mutex usage when enabled. * `allowIdleSubscriptions` - A flag to indicate that clients may subscribe to this store even while it is not started. If you intend to stop and restart your store while the subscribers are present, set this to `true`. By default, will use the opposite value of the `debuggable` parameter (`false` on production). diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt index fdc4e19f..9cba1d59 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/savedstate/SavedStateContainer.kt @@ -2,7 +2,7 @@ package pro.respawn.flowmvi.sample.features.savedstate import pro.respawn.flowmvi.api.Container import pro.respawn.flowmvi.dsl.store -import pro.respawn.flowmvi.dsl.useState +import pro.respawn.flowmvi.dsl.updateStateImmediate import pro.respawn.flowmvi.plugins.reduce import pro.respawn.flowmvi.sample.arch.configuration.ConfigurationFactory import pro.respawn.flowmvi.sample.arch.configuration.configure @@ -33,7 +33,7 @@ internal class SavedStateContainer( reduce { intent -> when (intent) { - is ChangedInput -> useState { + is ChangedInput -> updateStateImmediate { copy(input = input(intent.value)) } } diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/undoredo/UndoRedoContainer.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/undoredo/UndoRedoContainer.kt index cb3d1d6d..37abe3bf 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/undoredo/UndoRedoContainer.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/undoredo/UndoRedoContainer.kt @@ -39,8 +39,8 @@ internal class UndoRedoContainer( if (new.value == old) return@withState undoRedo( doImmediately = false, - redo = { useState { copy(input = new) } }, - undo = { useState { copy(input = old.input()) } }, + redo = { updateStateImmediate { copy(input = new) } }, + undo = { updateStateImmediate { copy(input = old.input()) } }, ) } }.launchIn(this) @@ -62,7 +62,7 @@ internal class UndoRedoContainer( when (intent) { is ClickedRedo -> undoRedo.redo() is ClickedUndo -> undoRedo.undo() - is ChangedInput -> useState { + is ChangedInput -> updateStateImmediate { lastInput.value = intent.value copy(input = input(intent.value)) } diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/undoredo/UndoRedoScreen.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/undoredo/UndoRedoScreen.kt index 30526e43..afaa5379 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/undoredo/UndoRedoScreen.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/features/undoredo/UndoRedoScreen.kt @@ -67,8 +67,8 @@ internal class UndoRedoContainer : Container undoRedo.redo() is ClickedUndo -> undoRedo.undo() - is ChangedInput -> useState { + is ChangedInput -> updateStateImmediate { lastInput.value = intent.value copy(input = input(intent.value)) } diff --git a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt index 9729ab5a..ddc35dab 100644 --- a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt +++ b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/TestPipelineContext.kt @@ -44,7 +44,7 @@ internal class TestPipelineContext @ override suspend fun withState(block: suspend S.() -> Unit): Unit = block(state) - override fun useState(block: S.() -> S) { + override fun updateStateImmediate(block: S.() -> S) { state = block(state) } } From cacc07cdcdccc829d999ecda3007487b46bace92 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 13:38:55 +0300 Subject: [PATCH 13/26] fixup! fix: bring back removed logging plugin as it was removed too early --- .../pro/respawn/flowmvi/plugins/Deprecated.kt | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt index 0c3a671a..4c8437d3 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt @@ -1,11 +1,22 @@ package pro.respawn.flowmvi.plugins +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import pro.respawn.flowmvi.api.FlowMVIDSL import pro.respawn.flowmvi.api.LazyPlugin import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.api.PipelineContext +import pro.respawn.flowmvi.api.Store +import pro.respawn.flowmvi.api.StorePlugin +import pro.respawn.flowmvi.dsl.StoreBuilder +import pro.respawn.flowmvi.dsl.plugin +import pro.respawn.flowmvi.dsl.subscribe import pro.respawn.flowmvi.logging.StoreLogLevel +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * A logging plugin that prints logs to the console using [println]. Tag is not used except for naming the plugin. @@ -39,3 +50,182 @@ public fun platformLoggingPlugin( tag: String? = null, level: StoreLogLevel? = null ): LazyPlugin = loggingPlugin(tag, level) + +/** + * An overload of the [parentStorePlugin] that also consumes its [MVIAction]s. + * Please see the other overload for more documentation. + * + * @see parentStorePlugin + * @see parentStore + */ +@Suppress("Indentation") // conflicts with IDE formatting +@FlowMVIDSL +public inline fun < + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + A2 : MVIAction + > parentStorePlugin( + parent: Store, + name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, + minExternalSubscriptions: Int = 1, + @BuilderInference crossinline consume: suspend PipelineContext.(action: A2) -> Unit, + @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, +): StorePlugin = whileSubscribedPlugin(name = name, minSubscriptions = minExternalSubscriptions) { + // do not use pipeline context to cancel subscription properly, suspend instead + coroutineScope { + subscribe(parent, { consume(it) }, { render(it) }).join() + } +} + +/** + * Creates and installs a new plugin that will subscribe to the [parent] store at the same time the current store is + * subscribed to and when [minExternalSubscriptions] are reached. + * + * This plugin will subscribe to the parent store and [render] its states while there are [minExternalSubscriptions] of + * the current store present. + * When the subscribers leave as in [whileSubscribedPlugin], this store will also unsubscribe from the parent store. + * + * Essentially, this store will be a subscriber of another store while this store is also subscribed to externally. + * For the behavior where the store will always be subscribed, please subscribe in the [initPlugin]. + * + * This function will not consume [MVIAction]s of the parent store. For that, please see the other overload of this + * function. + * + * The name of this plugin will be derived from the parent store's name, if present, otherwise `null`. + * + * @see parentStorePlugin + * @see parentStore + */ +@Suppress("Indentation") // conflicts with IDE formatting +@FlowMVIDSL +@Deprecated( + """ + Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. + Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() + """, + ReplaceWith( + "whileSubscribedPlugin { parent.collect { } }", + "pro.respawn.flowmvi.plugins.whileSubscribedPlugin", + "pro.respawn.flowmvi.dsl.collect" + ) +) +public inline fun parentStorePlugin( + parent: Store, + name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, + minExternalSubscriptions: Int = 1, + @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, +): StorePlugin = whileSubscribedPlugin(name = name, minSubscriptions = minExternalSubscriptions) { + coroutineScope { + subscribe(parent, render = { render(it) }).join() + } +} + +/** + * Install a new [parentStorePlugin]. This overload **does not** collect the parent store's actions. + * @see parentStorePlugin + */ +@Deprecated( + """ + Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. + Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() + """, + ReplaceWith( + "whileSubscribed { parent.collect { } }", + "pro.respawn.flowmvi.plugins.whileSubscribed", + "pro.respawn.flowmvi.dsl.collect" + ) +) +@Suppress("Indentation") // conflicts with IDE formatting +@FlowMVIDSL +public inline fun < + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + > StoreBuilder.parentStore( + parent: Store, + name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, + minExternalSubscriptions: Int = 1, + @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, +): Unit = install(parentStorePlugin(parent, name, minExternalSubscriptions, render = render)) + +/** + * Install a new [parentStorePlugin]. + * @see parentStorePlugin + */ +@Deprecated( + """ + Parent store plugin introduces unnecessary complexity to the behavior and has little flexibility. + Subscribe to the store in some other plugin instead, such as whileSubscribedPlugin using a suspending function collect() + """, + ReplaceWith( + "whileSubscribed { parent.collect { } }", + "pro.respawn.flowmvi.plugins.whileSubscribed", + "pro.respawn.flowmvi.dsl.collect" + ) +) +@Suppress("Indentation") // conflicts with IDE formatting +@FlowMVIDSL +public inline fun < + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + A2 : MVIAction, + > StoreBuilder.parentStore( + parent: Store, + name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, + minExternalSubscriptions: Int = 1, + @BuilderInference crossinline consume: suspend PipelineContext.(action: A2) -> Unit, + @BuilderInference crossinline render: suspend PipelineContext.(state: S2) -> Unit, +): Unit = install(parentStorePlugin(parent, name, minExternalSubscriptions, consume = consume, render = render)) + +/** + * Default name for the SavedStatePlugin + */ +public const val DefaultSavedStatePluginName: String = "SavedState" + +/** + * A plugin that restores the [pro.respawn.flowmvi.api.StateProvider.state] using [get] in [StorePlugin.onStart] + * and saves using [set] asynchronously in [StorePlugin.onState]. + * There are platform overloads for this function. + */ +@FlowMVIDSL +@Deprecated("If you want to save state, use the new `savedstate` module dependency") +public inline fun savedStatePlugin( + name: String = DefaultSavedStatePluginName, + context: CoroutineContext = EmptyCoroutineContext, + @BuilderInference crossinline get: suspend S.() -> S?, + @BuilderInference crossinline set: suspend (S) -> Unit, +): StorePlugin = plugin { + this.name = name + onState { _, new -> + launch(context) { set(new) } + new + } + onStart { + withContext(context) { + updateState { + get() ?: this + } + } + } +} + +/** + * Creates and installs a new [savedStatePlugin]. + */ +@FlowMVIDSL +@Suppress("DEPRECATION") +@Deprecated("If you want to save state, use the new `savedstate` module dependency") +public inline fun StoreBuilder.saveState( + name: String = DefaultSavedStatePluginName, + context: CoroutineContext = EmptyCoroutineContext, + @BuilderInference crossinline get: suspend S.() -> S?, + @BuilderInference crossinline set: suspend S.() -> Unit, +): Unit = install(savedStatePlugin(name, context, get, set)) From 73b2da96528668355c6cee0d49d651db71417358 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 14:28:52 +0300 Subject: [PATCH 14/26] optimize plugin iteration speed --- .../pro/respawn/flowmvi/dsl/StateDsl.kt | 10 ++++++---- .../pro/respawn/flowmvi/dsl/StoreBuilder.kt | 2 +- .../flowmvi/plugins/CompositePlugin.kt | 14 +++++++++----- .../respawn/flowmvi/util/FastCollectionOp.kt | 19 +++++++++++++++++++ .../flowmvi/debugger/client/DebuggerPlugin.kt | 2 +- 5 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 core/src/commonMain/kotlin/pro/respawn/flowmvi/util/FastCollectionOp.kt diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StateDsl.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StateDsl.kt index 0ac1a806..63f2fb85 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StateDsl.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StateDsl.kt @@ -1,10 +1,7 @@ -@file:Suppress("DEPRECATION") - package pro.respawn.flowmvi.dsl import pro.respawn.flowmvi.api.FlowMVIDSL import pro.respawn.flowmvi.api.MVIState -import pro.respawn.flowmvi.api.StateProvider import pro.respawn.flowmvi.api.StateReceiver import pro.respawn.flowmvi.util.typed import pro.respawn.flowmvi.util.withType @@ -47,7 +44,12 @@ public suspend inline fun StateReceiver.updateS @FlowMVIDSL public inline fun StateReceiver.updateStateImmediate( @BuilderInference crossinline transform: T.() -> S -): Unit = updateStateImmediate { withType { transform() } } +) { + contract { + callsInPlace(transform) + } + updateStateImmediate { withType { transform() } } +} // region deprecated diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreBuilder.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreBuilder.kt index 1cd69ab4..53a03a61 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreBuilder.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreBuilder.kt @@ -165,6 +165,6 @@ public class StoreBuilder @Published @PublishedApi @FlowMVIDSL internal operator fun invoke(): Store = config(initial).let { config -> - StoreImpl(config, compositePlugin(plugins.toSet().map { it(config) })) + StoreImpl(config, compositePlugin(plugins.map { it(config) })) } } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt index 718cbf2a..fb9ae563 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt @@ -6,6 +6,8 @@ import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.StorePlugin import pro.respawn.flowmvi.dsl.plugin +import pro.respawn.flowmvi.util.fastFold +import pro.respawn.flowmvi.util.fastForEach /** * A plugin that delegates to [plugins] in the iteration order. @@ -13,10 +15,12 @@ import pro.respawn.flowmvi.dsl.plugin * * This plugin is mostly not intended for usage in general code as there are no real use cases for it so far. * It can be useful in testing and custom store implementations. + * + * The [plugins] list must support random element access in order to be performant */ @FlowMVIDSL public fun compositePlugin( - plugins: Iterable>, + plugins: List>, name: String? = null, ): StorePlugin = plugin { this.name = name @@ -30,11 +34,11 @@ public fun compositePlugin( onStop { plugins.fold { onStop(it) } } } -private inline fun Iterable>.fold( +private inline fun List>.fold( block: StorePlugin.() -> Unit, -) = forEach(block) +) = fastForEach(block) -private inline fun Iterable>.fold( +private inline fun List>.fold( initial: R, block: StorePlugin.(R) -> R? -) = fold<_, R?>(initial) inner@{ acc, it -> it.block(acc ?: return@fold acc) } +) = fastFold<_, R?>(initial) inner@{ acc, it -> it.block(acc ?: return@fold acc) } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/FastCollectionOp.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/FastCollectionOp.kt new file mode 100644 index 00000000..87752803 --- /dev/null +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/FastCollectionOp.kt @@ -0,0 +1,19 @@ +package pro.respawn.flowmvi.util + +import kotlin.contracts.contract + +internal inline fun List.fastForEach(action: (T) -> Unit) { + contract { callsInPlace(action) } + for (index in indices) { + action(get(index)) + } +} + +internal inline fun List.fastFold(initial: R, operation: (acc: R, T) -> R): R { + contract { callsInPlace(operation) } + var accumulator = initial + fastForEach { e -> + accumulator = operation(accumulator, e) + } + return accumulator +} diff --git a/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt b/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt index 73b52434..377d04f1 100644 --- a/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt +++ b/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt @@ -117,7 +117,7 @@ public fun debuggerPlugin( val tt = TimeTravel(maxHistorySize = historySize) compositePlugin( name = "${config.name}DebuggerPlugin", - plugins = setOf( + plugins = listOf( timeTravelPlugin(timeTravel = tt, name = "${config.name}DebuggerTimeTravel"), debuggerPlugin( client = client, From f5266963120e1e99b84d89b6dce7a7909235c284 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 14:45:34 +0300 Subject: [PATCH 15/26] feat: change the return value of `onSubscribe` to return the new subscriber count --- README.md | 8 ++++---- .../kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt | 5 ++--- .../kotlin/pro/respawn/flowmvi/api/StorePlugin.kt | 12 +++++------- .../pro/respawn/flowmvi/dsl/StorePluginBuilder.kt | 10 +++++----- .../respawn/flowmvi/modules/SubscriptionModule.kt | 4 +--- .../flowmvi/plugins/AwaitSubscribersPlugin.kt | 5 ++--- .../kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt | 2 +- .../pro/respawn/flowmvi/plugins/LoggingPlugin.kt | 2 +- .../respawn/flowmvi/plugins/WhileSubscribedPlugin.kt | 3 +-- .../flowmvi/test/plugin/WhileSubscribedPluginTest.kt | 2 +- .../flowmvi/test/store/StoreSubscriptionsTest.kt | 2 +- .../flowmvi/debugger/client/DebuggerPlugin.kt | 1 - docs/custom_plugins.md | 3 +-- docs/plugins.md | 6 ++---- .../respawn/flowmvi/test/plugin/PluginTestScope.kt | 6 ++++-- 15 files changed, 31 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 6081770a..5940ed15 100644 --- a/README.md +++ b/README.md @@ -214,10 +214,7 @@ Powerful DSL allows to hook into store events and amend any store's logic with r ```kotlin val counterPlugin = lazyPlugin { - - // access the store configuration - if (config.debuggable) config.logger(Debug) { "Store is debuggable" } - + onStart { } onStop { } @@ -233,6 +230,9 @@ val counterPlugin = lazyPlugin { onUnsubscribe { subs -> } onException { e -> } + + // access the store configuration + if (config.debuggable) config.logger(Debug) { "Store is debuggable" } } ``` diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt index 0ebc1ab2..8e75b307 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/ImmutableStore.kt @@ -30,9 +30,8 @@ public interface ImmutableStore.() -> Unit): Job diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StorePlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StorePlugin.kt index d01f8660..02f4b033 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StorePlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/api/StorePlugin.kt @@ -117,8 +117,8 @@ public interface StorePlugin : LazyP /** * A callback to be executed each time [Store.subscribe] is called. * - * * This callback is executed **before** the [subscriberCount] is incremented. - * This means, for the first subscription, [subscriberCount] will be zero. + * * This callback is executed **after** the subscriber count is incremented, i.e. the value represents + * the **new** number of subscribers. * * There is no guarantee that the subscribers will not be able to subscribe when the store has not been started yet. * But this function will be invoked as soon as the store is started, with the most recent subscriber count. * * This function is invoked in the store's scope, not the subscriber's scope. @@ -130,15 +130,13 @@ public interface StorePlugin : LazyP * * Suspending in this function will prevent other plugins from receiving the subscription event (i.e. next plugins * that use [onSubscribe] will wait for this one to complete. */ - public suspend fun PipelineContext.onSubscribe( - subscriberCount: Int - ): Unit = Unit + public suspend fun PipelineContext.onSubscribe(newSubscriberCount: Int): Unit = Unit /** * A callback to be executed when the subscriber cancels its subscription job (unsubscribes). * * * This callback is executed **after** the subscriber has been removed and **after** [subscriberCount] is - * decremented. This means, for the last subscriber, the count will be 0. + * decremented, i.e. the value represents the **new** number of subscribers. * * There is no guarantee that this will be invoked exactly before a subscriber reappears. * It may be so that a second subscriber appears before the first one disappears (due to the parallel nature of * coroutines). In that case, [onSubscribe] will be invoked first as if it was a second subscriber, and then @@ -146,7 +144,7 @@ public interface StorePlugin : LazyP * * Suspending in this function will prevent other plugins from receiving the subscription event (i.e. next plugins * that use [onUnsubscribe] will wait for this one to complete. */ - public suspend fun PipelineContext.onUnsubscribe(subscriberCount: Int): Unit = Unit + public suspend fun PipelineContext.onUnsubscribe(newSubscriberCount: Int): Unit = Unit /** * Invoked when [Store.close] is invoked. This is called **after** the store is already closed, and you cannot diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StorePluginBuilder.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StorePluginBuilder.kt index 51abe963..49992b70 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StorePluginBuilder.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StorePluginBuilder.kt @@ -76,7 +76,7 @@ public open class StorePluginBuilder */ @FlowMVIDSL public fun onSubscribe( - block: suspend PipelineContext.(subscriberCount: Int) -> Unit + block: suspend PipelineContext.(newSubscriberCount: Int) -> Unit ): Unit = setOnce(::subscribe, block) /** @@ -115,12 +115,12 @@ public open class StorePluginBuilder return block(e) } - override suspend fun PipelineContext.onSubscribe(subscriberCount: Int) { - this@StorePluginBuilder.subscribe?.invoke(this, subscriberCount) + override suspend fun PipelineContext.onSubscribe(newSubscriberCount: Int) { + this@StorePluginBuilder.subscribe?.invoke(this, newSubscriberCount) } - override suspend fun PipelineContext.onUnsubscribe(subscriberCount: Int) { - this@StorePluginBuilder.unsubscribe?.invoke(this, subscriberCount) + override suspend fun PipelineContext.onUnsubscribe(newSubscriberCount: Int) { + this@StorePluginBuilder.unsubscribe?.invoke(this, newSubscriberCount) } override fun onStop(e: Exception?) { diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/SubscriptionModule.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/SubscriptionModule.kt index 20836a1f..df54f5df 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/SubscriptionModule.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/modules/SubscriptionModule.kt @@ -33,9 +33,7 @@ internal suspend inline fun SubscriptionModule.observeSubscribers( crossinline onUnsubscribe: suspend (count: Int) -> Unit, ) = subscribers.collect { (previous, new) -> when { - // although the implementation has changed, maintain previous behavior by passing previous sub value - // to the plugins - new > previous -> onSubscribe(new - 1) + new > previous -> onSubscribe(new) new < previous -> onUnsubscribe(new) } } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AwaitSubscribersPlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AwaitSubscribersPlugin.kt index b9413812..e98f7223 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AwaitSubscribersPlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/AwaitSubscribersPlugin.kt @@ -116,8 +116,7 @@ public fun awaitSubscribersPlugin( onStop { manager.complete() } - onSubscribe { previous -> - val currentSubs = previous + 1 - if (currentSubs >= minSubs) manager.completeAndWait() + onSubscribe { current -> + if (current >= minSubs) manager.completeAndWait() } } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt index 4c8437d3..f5fb0dd5 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt @@ -138,7 +138,7 @@ public inline fun loggingPlugin( log(level ?: Info, currentTag) { "Started ${config.name ?: "Store"}" } } onSubscribe { - log(level ?: Info, currentTag) { "New subscriber #${it + 1}" } + log(level ?: Info, currentTag) { "New subscriber #$it" } } onUnsubscribe { log(level ?: Info, currentTag) { "Subscriber #${it + 1} removed" } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/WhileSubscribedPlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/WhileSubscribedPlugin.kt index 7590b39d..b95af8e9 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/WhileSubscribedPlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/WhileSubscribedPlugin.kt @@ -67,8 +67,7 @@ public inline fun whileSubscribedPl require(minSubscriptions > 0) { "Minimum number of subscribers must be greater than 0" } this.name = name val job = SubscriptionHolder() - onSubscribe { previous -> - val current = previous + 1 + onSubscribe { current -> when { current < minSubscriptions -> job.cancelAndJoin() job.isActive -> Unit // condition was already satisfied diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/plugin/WhileSubscribedPluginTest.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/plugin/WhileSubscribedPluginTest.kt index 5ae92093..5677756c 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/plugin/WhileSubscribedPluginTest.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/plugin/WhileSubscribedPluginTest.kt @@ -49,7 +49,7 @@ class WhileSubscribedPluginTest : FreeSpec({ running.test { plugin().test(TestState.Some) { idle() - onSubscribe(minSubs) // previous value + onSubscribe(minSubs + 1) // previous value idle() awaitItem().shouldBeTrue() onStop(null) diff --git a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreSubscriptionsTest.kt b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreSubscriptionsTest.kt index 6ff1f359..8c2e28f2 100644 --- a/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreSubscriptionsTest.kt +++ b/core/src/jvmTest/kotlin/pro/respawn/flowmvi/test/store/StoreSubscriptionsTest.kt @@ -19,7 +19,7 @@ class StoreSubscriptionsTest : FreeSpec({ var subs = 0 val store = testStore(tt) { install { - onSubscribe { previous -> subs = previous + 1 } + onSubscribe { new -> subs = new } onUnsubscribe { subs = it } onStop { subs = 0 } } diff --git a/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt b/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt index 377d04f1..40e991f7 100644 --- a/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt +++ b/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt @@ -27,7 +27,6 @@ internal const val NonDebuggableStoreMessage: String = """ Store must be debuggable in order to use the debugger. Please set `debuggable = true` before installing the plugin. Don't include debug code in production builds. -Suppress this error by using install(debuggerPlugin) directly. """ @Suppress("UNUSED_PARAMETER") diff --git a/docs/custom_plugins.md b/docs/custom_plugins.md index 3871565e..3a719dfd 100644 --- a/docs/custom_plugins.md +++ b/docs/custom_plugins.md @@ -182,8 +182,7 @@ suspend fun PipelineContext.onSubscribe(subscriberCount: Int): Unit = U A callback to be executed **each time** `Store.subscribe` is called. -* This callback is executed **before** the `subscriberCount` is incremented. - This means, for the first subscription, `subscriberCount` will be zero. +* This callback is executed **after** the `subscriberCount` is incremented i.e. with the **new** count of subscribers. * There is no guarantee that the subscribers will not be able to subscribe when the store has not been started yet. But this function will be invoked as soon as the store is started, with the most recent subscriber count. * This function is invoked in the store's scope, not the subscriber's scope. diff --git a/docs/plugins.md b/docs/plugins.md index a0b99044..bce97d2b 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -215,8 +215,7 @@ fun whileSubscribedPlugin( ) = plugin { val job = SubscriptionHolder() - onSubscribe { previous -> - val current = previous + 1 + onSubscribe { current -> when { current < minSubscriptions -> job.cancelAndJoin() job.isActive -> Unit // condition was already satisfied @@ -432,8 +431,7 @@ fun awaitSubscribersPlugin( onStop { manager.complete() } - onSubscribe { previous -> - val currentSubs = previous + 1 + onSubscribe { currentSubs -> if (currentSubs >= minSubs) manager.completeAndWait() } } diff --git a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/PluginTestScope.kt b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/PluginTestScope.kt index 7d0d274b..58e6eb48 100644 --- a/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/PluginTestScope.kt +++ b/test/src/commonMain/kotlin/pro/respawn/flowmvi/test/plugin/PluginTestScope.kt @@ -36,11 +36,13 @@ public class PluginTestScope private ctx = TestPipelineContext( config = configuration, plugin = compositePlugin( - setOf( + sequenceOf( loggingPlugin(), timeTravelPlugin(timeTravel), plugin, - ).map { it.invoke(configuration) }, + ) + .map { it.invoke(configuration) } + .toList(), ) ) ) From 83ee7378d75f3e031cde5147fd9e57051d56d650 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 14:51:29 +0300 Subject: [PATCH 16/26] fix lint --- build.gradle.kts | 2 - .../flowmvi/logging/AndroidLogPriority.kt | 2 +- .../pro/respawn/flowmvi/plugins/Deprecated.kt | 40 +++++++++---------- sample/build.gradle.kts | 2 - .../flowmvi/sample/ui/widgets/CodeView.kt | 1 - 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a40369a3..9ee9f725 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,8 +7,6 @@ import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -private val PluginPrefix = "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination" - plugins { alias(libs.plugins.detekt) alias(libs.plugins.gradleDoctor) diff --git a/core/src/androidMain/kotlin/pro/respawn/flowmvi/logging/AndroidLogPriority.kt b/core/src/androidMain/kotlin/pro/respawn/flowmvi/logging/AndroidLogPriority.kt index 224450b1..fe3febfe 100644 --- a/core/src/androidMain/kotlin/pro/respawn/flowmvi/logging/AndroidLogPriority.kt +++ b/core/src/androidMain/kotlin/pro/respawn/flowmvi/logging/AndroidLogPriority.kt @@ -14,7 +14,7 @@ public val StoreLogLevel.asLogPriority: Int StoreLogLevel.Error -> Log.ERROR } -internal val Int.asStoreLogLevel +internal val Int.asStoreLogLevel get() = when (this) { Log.VERBOSE -> StoreLogLevel.Trace Log.DEBUG -> StoreLogLevel.Debug diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt index f5fb0dd5..220ff88e 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/Deprecated.kt @@ -61,13 +61,13 @@ public fun platformLoggingPlugin( @Suppress("Indentation") // conflicts with IDE formatting @FlowMVIDSL public inline fun < - S : MVIState, - I : MVIIntent, - A : MVIAction, - S2 : MVIState, - I2 : MVIIntent, - A2 : MVIAction - > parentStorePlugin( + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + A2 : MVIAction + > parentStorePlugin( parent: Store, name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, minExternalSubscriptions: Int = 1, @@ -141,12 +141,12 @@ public inline fun StoreBuilder.parentStore( + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + > StoreBuilder.parentStore( parent: Store, name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, minExternalSubscriptions: Int = 1, @@ -171,13 +171,13 @@ public inline fun < @Suppress("Indentation") // conflicts with IDE formatting @FlowMVIDSL public inline fun < - S : MVIState, - I : MVIIntent, - A : MVIAction, - S2 : MVIState, - I2 : MVIIntent, - A2 : MVIAction, - > StoreBuilder.parentStore( + S : MVIState, + I : MVIIntent, + A : MVIAction, + S2 : MVIState, + I2 : MVIIntent, + A2 : MVIAction, + > StoreBuilder.parentStore( parent: Store, name: String? = parent.name?.let { "ParentStorePlugin\$$it" }, minExternalSubscriptions: Int = 1, diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 2bf83251..74dcab08 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -190,10 +190,8 @@ compose { } web { - } - desktop { application { mainClass = "${Config.Sample.namespace}.MainKt" diff --git a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt index 4fc534e0..53f73b5c 100644 --- a/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt +++ b/sample/src/commonMain/kotlin/pro/respawn/flowmvi/sample/ui/widgets/CodeView.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue From e8879130017e276570e8e4c54386aca871ad44da Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 14:59:10 +0300 Subject: [PATCH 17/26] migrate wasm app to ComposeViewport --- sample/src/wasmJsMain/kotlin/main.kt | 3 ++- sample/src/wasmJsMain/resources/404.html | 26 ---------------------- sample/src/wasmJsMain/resources/index.html | 7 +++--- sample/src/wasmJsMain/resources/styles.css | 5 ----- 4 files changed, 5 insertions(+), 36 deletions(-) delete mode 100644 sample/src/wasmJsMain/resources/404.html diff --git a/sample/src/wasmJsMain/kotlin/main.kt b/sample/src/wasmJsMain/kotlin/main.kt index 7e32ea5a..e576cef2 100644 --- a/sample/src/wasmJsMain/kotlin/main.kt +++ b/sample/src/wasmJsMain/kotlin/main.kt @@ -2,6 +2,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.window.CanvasBasedWindow +import androidx.compose.ui.window.ComposeViewport import com.arkivanov.decompose.DefaultComponentContext import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.webhistory.DefaultWebHistoryController @@ -49,7 +50,7 @@ fun main() { localStorage.setItem(KEY_SAVED_STATE, stateKeeper.save().encodeToString()) null } - CanvasBasedWindow { AppContent(root) } + ComposeViewport(document.body!!) { AppContent(root) } } private fun LifecycleRegistry.attachToDocument() { diff --git a/sample/src/wasmJsMain/resources/404.html b/sample/src/wasmJsMain/resources/404.html deleted file mode 100644 index 77f69fa9..00000000 --- a/sample/src/wasmJsMain/resources/404.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - FlowMVI - - - - - - - - - - - - - - - - - - - diff --git a/sample/src/wasmJsMain/resources/index.html b/sample/src/wasmJsMain/resources/index.html index e04cb3af..cb543869 100644 --- a/sample/src/wasmJsMain/resources/index.html +++ b/sample/src/wasmJsMain/resources/index.html @@ -3,7 +3,7 @@ FlowMVI - + @@ -29,8 +29,7 @@ + - - - + diff --git a/sample/src/wasmJsMain/resources/styles.css b/sample/src/wasmJsMain/resources/styles.css index 7e3af0ac..8e94d43f 100644 --- a/sample/src/wasmJsMain/resources/styles.css +++ b/sample/src/wasmJsMain/resources/styles.css @@ -5,8 +5,3 @@ html, body { padding: 0; overflow: hidden; } - -#ComposeTarget { - width: 100%; - height: 100%; -} From 5bded70c203e38b63590ad6b614773fd6f7a1f94 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 15:19:21 +0300 Subject: [PATCH 18/26] bump version to 3.0 --- buildSrc/src/main/kotlin/Config.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index ca38c5fd..196c7582 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -15,11 +15,11 @@ object Config { const val artifactId = "$group.$artifact" - const val versionCode = 5 - const val majorRelease = 2 - const val minorRelease = 5 + const val versionCode = 7 + const val majorRelease = 3 + const val minorRelease = 0 const val patch = 0 - const val postfix = "-alpha12" // include dash (-) + const val postfix = "" // include dash (-) const val majorVersionName = "$majorRelease.$minorRelease.$patch" const val versionName = "$majorVersionName$postfix" const val url = "https://github.com/respawn-app/FlowMVI" From 089aa33c0810f24c744acc28a5ba167f0725374f Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 15:45:26 +0300 Subject: [PATCH 19/26] fix lint --- sample/src/wasmJsMain/kotlin/main.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/sample/src/wasmJsMain/kotlin/main.kt b/sample/src/wasmJsMain/kotlin/main.kt index e576cef2..9e0dd529 100644 --- a/sample/src/wasmJsMain/kotlin/main.kt +++ b/sample/src/wasmJsMain/kotlin/main.kt @@ -1,7 +1,6 @@ @file:Suppress("MissingPackageDeclaration", "Filename") import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.window.CanvasBasedWindow import androidx.compose.ui.window.ComposeViewport import com.arkivanov.decompose.DefaultComponentContext import com.arkivanov.decompose.ExperimentalDecomposeApi From 6cfb5f9645b316b2d65af6e156dfbd188af36889 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 16:02:46 +0300 Subject: [PATCH 20/26] update deps (2) --- .idea/inspectionProfiles/Project_Default.xml | 3 ++- gradle/libs.versions.toml | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 84353676..28f93916 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,7 @@ - \ No newline at end of file + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8a9dd23a..374b75df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,18 +7,17 @@ composeDetektPlugin = "1.3.0" core-ktx = "1.13.1" coroutines = "1.8.1" datetime = "0.6.0" -dependencyAnalysisPlugin = "1.31.0" +dependencyAnalysisPlugin = "1.32.0" detekt = "1.23.6" detektFormattingPlugin = "1.23.6" dokka = "1.9.20" essenty = "2.0.0" fragment = "1.8.0-beta01" gradleAndroid = "8.6.0-alpha03" -gradleDoctorPlugin = "0.9.2" +gradleDoctorPlugin = "0.10.0" intellij-ide-plugin = "2.0.0-beta1" junit = "4.13.2" kotest = "5.9.0" -kotest-plugin = "5.8.1" # @pin kotlin = "2.0.0" kotlin-collections = "0.3.7" @@ -135,7 +134,7 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } gradleDoctor = { id = "com.osacky.doctor", version.ref = "gradleDoctorPlugin" } intellij-ide = { id = "org.jetbrains.intellij.platform", version.ref = "intellij-ide-plugin" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" } -kotest = { id = "io.kotest.multiplatform", version.ref = "kotest-plugin" } +kotest = { id = "io.kotest.multiplatform", version.ref = "kotest" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdatePlugin" } From 3528f493e507980feb27ee2a4c0dd5308d12d989 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 16:05:14 +0300 Subject: [PATCH 21/26] remove pre-2.0 build workarounds --- settings.gradle.kts | 68 +++------------------------------------------ 1 file changed, 4 insertions(+), 64 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 3cfb7d88..7a3e1e5e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,6 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { - maven("https://oss.sonatype.org/content/repositories/snapshots/") google() gradlePluginPortal() mavenCentral() @@ -22,21 +21,17 @@ pluginManagement { } dependencyResolutionManagement { // REQUIRED for IDE module configuration to resolve IDE platform - repositoriesMode = RepositoriesMode.PREFER_PROJECT + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS repositories { mavenLocal() google() mavenCentral() - ivyNative() - node() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } - dependencyResolutionManagement { - versionCatalogs { - create("applibs") { - from(files("sample/libs.versions.toml")) - } + versionCatalogs { + create("applibs") { + from(files("sample/libs.versions.toml")) } } } @@ -59,58 +54,3 @@ include(":debugger:debugger-plugin") include(":debugger:server") include(":debugger:debugger-common") // include(":debugger:ideplugin") - -fun RepositoryHandler.node() { - exclusiveContent { - forRepository { - ivy("https://nodejs.org/dist/") { - name = "Node Distributions at $url" - patternLayout { artifact("v[revision]/[artifact](-v[revision]-[classifier]).[ext]") } - metadataSources { artifact() } - content { includeModule("org.nodejs", "node") } - } - } - filter { includeGroup("org.nodejs") } - } - - exclusiveContent { - forRepository { - ivy("https://github.com/yarnpkg/yarn/releases/download") { - name = "Yarn Distributions at $url" - patternLayout { artifact("v[revision]/[artifact](-v[revision]).[ext]") } - metadataSources { artifact() } - content { includeModule("com.yarnpkg", "yarn") } - } - } - filter { includeGroup("com.yarnpkg") } - } -} - -fun RepositoryHandler.ivyNative() { - ivy { url = uri("https://download.jetbrains.com") } - - // TODO: Maybe this is not needed anymore - exclusiveContent { - forRepository { - this@ivyNative.ivy("https://download.jetbrains.com/kotlin/native/builds") { - name = "Kotlin Native" - patternLayout { - listOf( - "macos-x86_64", - "macos-aarch64", - "osx-x86_64", - "osx-aarch64", - "linux-x86_64", - "windows-x86_64", - ).forEach { os -> - listOf("dev", "releases").forEach { stage -> - artifact("$stage/[revision]/$os/[artifact]-[revision].[ext]") - } - } - } - metadataSources { artifact() } - } - } - filter { includeModuleByRegex(".*", ".*kotlin-native-prebuilt.*") } - } -} From d32763249d986805ba3c7f5e8dc76f38e7ee46c8 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 16:16:36 +0300 Subject: [PATCH 22/26] rename some deps, workaround wasm dep download --- build.gradle.kts | 2 +- compose/build.gradle.kts | 2 +- debugger/app/build.gradle.kts | 2 +- debugger/ideplugin/build.gradle.kts | 2 +- debugger/server/build.gradle.kts | 2 +- essenty/essenty-compose/build.gradle.kts | 2 +- gradle/libs.versions.toml | 11 +++++------ sample/build.gradle.kts | 2 +- savedstate/build.gradle.kts | 2 +- settings.gradle.kts | 2 +- 10 files changed, 14 insertions(+), 15 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9ee9f725..1031bb9b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ plugins { alias(libs.plugins.atomicfu) alias(libs.plugins.dependencyAnalysis) alias(libs.plugins.serialization) apply false - alias(libs.plugins.jetbrainsCompose) apply false + alias(libs.plugins.compose) apply false // plugins already on a classpath (conventions) // alias(libs.plugins.androidApplication) apply false // alias(libs.plugins.androidLibrary) apply false diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 7e21104f..f974e176 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { id(libs.plugins.kotlinMultiplatform.id) id(libs.plugins.androidLibrary.id) - alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) id("maven-publish") signing diff --git a/debugger/app/build.gradle.kts b/debugger/app/build.gradle.kts index bc51f0db..d1f6056d 100644 --- a/debugger/app/build.gradle.kts +++ b/debugger/app/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { id(libs.plugins.kotlinMultiplatform.id) - alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) alias(libs.plugins.serialization) } diff --git a/debugger/ideplugin/build.gradle.kts b/debugger/ideplugin/build.gradle.kts index 18b8bce8..847eb25a 100644 --- a/debugger/ideplugin/build.gradle.kts +++ b/debugger/ideplugin/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.intellij.ide) kotlin("jvm") - alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) } val props by localProperties diff --git a/debugger/server/build.gradle.kts b/debugger/server/build.gradle.kts index 73cd9031..8c65cb3b 100644 --- a/debugger/server/build.gradle.kts +++ b/debugger/server/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id(libs.plugins.kotlinMultiplatform.id) - alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) alias(libs.plugins.serialization) } diff --git a/essenty/essenty-compose/build.gradle.kts b/essenty/essenty-compose/build.gradle.kts index 4fc633e0..d3304ac6 100644 --- a/essenty/essenty-compose/build.gradle.kts +++ b/essenty/essenty-compose/build.gradle.kts @@ -1,7 +1,7 @@ plugins { id(libs.plugins.kotlinMultiplatform.id) id(libs.plugins.androidLibrary.id) - alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) id("maven-publish") signing diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 374b75df..72f4464f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,7 @@ [versions] activity = "1.9.0" -androidx-lifecycle = "2.8.0" compose = "1.6.10" -compose-lifecycle = "2.8.0" +lifecycle = "2.8.0" composeDetektPlugin = "1.3.0" core-ktx = "1.13.1" coroutines = "1.8.1" @@ -34,7 +33,7 @@ android-gradle = { module = "com.android.tools.build:gradle", version.ref = "gra androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" } -androidx-lifecycle-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" } +lifecycle-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } detekt-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "composeDetektPlugin" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detektFormattingPlugin" } detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } @@ -82,8 +81,8 @@ ktor-server-engine = { module = "io.ktor:ktor-server-netty", version.ref = "ktor ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" } ktor-server-partialcontent = { module = "io.ktor:ktor-server-partial-content", version.ref = "ktor" } ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" } -lifecycle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime", version.ref = "compose-lifecycle" } -lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "compose-lifecycle" } +lifecycle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" } +lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } @@ -133,7 +132,7 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } gradleDoctor = { id = "com.osacky.doctor", version.ref = "gradleDoctorPlugin" } intellij-ide = { id = "org.jetbrains.intellij.platform", version.ref = "intellij-ide-plugin" } -jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" } +compose = { id = "org.jetbrains.compose", version.ref = "compose" } kotest = { id = "io.kotest.multiplatform", version.ref = "kotest" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 74dcab08..700a28db 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl plugins { id(libs.plugins.kotlinMultiplatform.id) id(applibs.plugins.android.application.id) - alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) alias(libs.plugins.serialization) } diff --git a/savedstate/build.gradle.kts b/savedstate/build.gradle.kts index 1b03602b..f6303cb0 100644 --- a/savedstate/build.gradle.kts +++ b/savedstate/build.gradle.kts @@ -30,7 +30,7 @@ kotlin { implementation(libs.kotlin.io) } androidMain.dependencies { - api(libs.androidx.lifecycle.savedstate) + api(libs.lifecycle.savedstate) } commonMain.dependencies { api(projects.core) diff --git a/settings.gradle.kts b/settings.gradle.kts index 7a3e1e5e..f84cb51b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,7 +21,7 @@ pluginManagement { } dependencyResolutionManagement { // REQUIRED for IDE module configuration to resolve IDE platform - repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS + repositoriesMode = RepositoriesMode.PREFER_PROJECT repositories { mavenLocal() google() From 2b93619a61f446927a5155db583d163a256b4857 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 16:34:03 +0300 Subject: [PATCH 23/26] update publication settings --- buildSrc/src/main/kotlin/Config.kt | 1 + buildSrc/src/main/kotlin/PublishingExt.kt | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43453 bytes 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 196c7582..02cbd7ea 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -23,6 +23,7 @@ object Config { const val majorVersionName = "$majorRelease.$minorRelease.$patch" const val versionName = "$majorVersionName$postfix" const val url = "https://github.com/respawn-app/FlowMVI" + const val developerUrl = "https://respawn.pro" const val licenseFile = "LICENSE.txt" const val licenseName = "The Apache Software License, Version 2.0" const val licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt" diff --git a/buildSrc/src/main/kotlin/PublishingExt.kt b/buildSrc/src/main/kotlin/PublishingExt.kt index 7dea1841..7d2ce277 100644 --- a/buildSrc/src/main/kotlin/PublishingExt.kt +++ b/buildSrc/src/main/kotlin/PublishingExt.kt @@ -35,7 +35,7 @@ internal fun MavenPublication.configurePom() = pom { id.set(Config.vendorId) name.set(Config.vendorName) email.set(Config.supportEmail) - url.set("https://opensource.respawn.pro") + url.set(Config.developerUrl) organization.set(Config.vendorName) organizationUrl.set(url) } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch delta 34118 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 delta 34176 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 SGW6ptSs!X7{rG*o_g?|vpSEZK From 2ada225a7a77a9053c55691730184f87d4aa9c68 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 18:29:36 +0300 Subject: [PATCH 24/26] try to fix wasm inline compiler bug --- .../flowmvi/plugins/CompositePlugin.kt | 22 +++++++++---------- .../flowmvi/debugger/client/DebuggerPlugin.kt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt index fb9ae563..24f35361 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt @@ -24,21 +24,21 @@ public fun compositePlugin( name: String? = null, ): StorePlugin = plugin { this.name = name - onState { old: S, new: S -> plugins.fold(new) { onState(old, it) } } - onIntent { intent: I -> plugins.fold(intent) { onIntent(it) } } - onAction { action: A -> plugins.fold(action) { onAction(it) } } - onException { e: Exception -> plugins.fold(e) { onException(it) } } - onUnsubscribe { subs: Int -> plugins.fold { onUnsubscribe(subs) } } - onSubscribe { subs: Int -> plugins.fold { onSubscribe(subs) } } - onStart { plugins.fold { onStart() } } - onStop { plugins.fold { onStop(it) } } + onState { old: S, new: S -> plugins.iterate(new) { onState(old, it) } } + onIntent { intent: I -> plugins.iterate(intent) { onIntent(it) } } + onAction { action: A -> plugins.iterate(action) { onAction(it) } } + onException { e: Exception -> plugins.iterate(e) { onException(it) } } + onSubscribe { subs: Int -> plugins.iterate { onSubscribe(subs) } } + onUnsubscribe { subs: Int -> plugins.iterate { onUnsubscribe(subs) } } + onStart { plugins.iterate { onStart() } } + onStop { plugins.iterate { onStop(it) } } } -private inline fun List>.fold( +private inline fun List>.iterate( block: StorePlugin.() -> Unit, ) = fastForEach(block) -private inline fun List>.fold( +private inline fun List>.iterate( initial: R, block: StorePlugin.(R) -> R? -) = fastFold<_, R?>(initial) inner@{ acc, it -> it.block(acc ?: return@fold acc) } +) = fastFold<_, R?>(initial) inner@{ acc, it -> block(it, acc ?: return@iterate acc) } diff --git a/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt b/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt index 40e991f7..84005f9d 100644 --- a/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt +++ b/debugger/debugger-client/src/commonMain/kotlin/pro/respawn/flowmvi/debugger/client/DebuggerPlugin.kt @@ -124,8 +124,8 @@ public fun debuggerPlugin( host = host, port = port, reconnectionDelay = reconnectionDelay - ).invoke(config) - ), + ) + ).map { it.invoke(config) }, ) } From 1afea48aca71652fe032844baa15d070b504f2d1 Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 21:17:33 +0300 Subject: [PATCH 25/26] reduce object allocation when creating plugins --- .../pro/respawn/flowmvi/dsl/PluginDsl.kt | 50 +++++++++++ .../respawn/flowmvi/dsl/StorePluginBuilder.kt | 83 ++++++------------- .../flowmvi/plugins/CompositePlugin.kt | 30 +++---- 3 files changed, 89 insertions(+), 74 deletions(-) create mode 100644 core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PluginDsl.kt diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PluginDsl.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PluginDsl.kt new file mode 100644 index 00000000..1b148fa8 --- /dev/null +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/PluginDsl.kt @@ -0,0 +1,50 @@ +package pro.respawn.flowmvi.dsl + +import pro.respawn.flowmvi.api.MVIAction +import pro.respawn.flowmvi.api.MVIIntent +import pro.respawn.flowmvi.api.MVIState +import pro.respawn.flowmvi.api.PipelineContext +import pro.respawn.flowmvi.api.StorePlugin + +/** + * Create a new instance of [StorePlugin] using provided callback parameters. + * + * See [plugin] for a DSL-like experience. + */ +internal inline fun StorePlugin( + @BuilderInference crossinline onState: suspend PipelineContext.(old: S, new: S) -> S? = { _, new -> new }, + @BuilderInference crossinline onIntent: suspend PipelineContext.(intent: I) -> I? = { it }, + @BuilderInference crossinline onAction: suspend PipelineContext.(action: A) -> A? = { it }, + @BuilderInference crossinline onException: suspend PipelineContext.(e: Exception) -> Exception? = { it }, + @BuilderInference crossinline onStart: suspend PipelineContext.() -> Unit = {}, + @BuilderInference crossinline onSubscribe: suspend PipelineContext.(subs: Int) -> Unit = {}, + @BuilderInference crossinline onUnsubscribe: suspend PipelineContext.(subs: Int) -> Unit = {}, + @BuilderInference crossinline onStop: (e: Exception?) -> Unit = {}, + name: String? = null, +): StorePlugin = object : StorePlugin { + + override val name = name + + override suspend fun PipelineContext.onStart() = onStart(this) + override suspend fun PipelineContext.onState(old: S, new: S) = onState(this, old, new) + override suspend fun PipelineContext.onIntent(intent: I) = onIntent(this, intent) + override suspend fun PipelineContext.onAction(action: A) = onAction(this, action) + override suspend fun PipelineContext.onException(e: Exception) = onException(this, e) + override fun onStop(e: Exception?) = onStop.invoke(e) + + override suspend fun PipelineContext.onSubscribe( + newSubscriberCount: Int + ) = onSubscribe(this, newSubscriberCount) + + override suspend fun PipelineContext.onUnsubscribe( + newSubscriberCount: Int + ) = onUnsubscribe(this, newSubscriberCount) + + override fun toString(): String = "StorePlugin${name?.let { " \"$it\"" }.orEmpty()}" + override fun hashCode(): Int = name?.hashCode() ?: super.hashCode() + override fun equals(other: Any?): Boolean = when { + other !is StorePlugin<*, *, *> -> false + other.name == null && name == null -> this === other + else -> name == other.name + } +} diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StorePluginBuilder.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StorePluginBuilder.kt index 49992b70..f135df2b 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StorePluginBuilder.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StorePluginBuilder.kt @@ -34,10 +34,10 @@ public open class StorePluginBuilder public var name: String? = null /** - * @see [StorePlugin.onIntent] + * @see [StorePlugin.onStart] */ @FlowMVIDSL - public fun onIntent(block: suspend PipelineContext.(intent: I) -> I?): Unit = setOnce(::intent, block) + public fun onStart(block: suspend PipelineContext.() -> Unit): Unit = setOnce(::start, block) /** * @see [StorePlugin.onState] @@ -46,16 +46,16 @@ public open class StorePluginBuilder public fun onState(block: suspend PipelineContext.(old: S, new: S) -> S?): Unit = setOnce(::state, block) /** - * @see [StorePlugin.onStart] + * @see [StorePlugin.onIntent] */ @FlowMVIDSL - public fun onStart(block: suspend PipelineContext.() -> Unit): Unit = setOnce(::start, block) + public fun onIntent(block: suspend PipelineContext.(intent: I) -> I?): Unit = setOnce(::intent, block) /** - * @see [StorePlugin.onStop] + * @see [StorePlugin.onAction] */ @FlowMVIDSL - public fun onStop(block: (e: Exception?) -> Unit): Unit = setOnce(::stop, block) + public fun onAction(block: suspend PipelineContext.(action: A) -> A?): Unit = setOnce(::action, block) /** * @see [StorePlugin.onException] @@ -65,12 +65,6 @@ public open class StorePluginBuilder block: suspend PipelineContext.(e: Exception) -> Exception? ): Unit = setOnce(::exception, block) - /** - * @see [StorePlugin.onAction] - */ - @FlowMVIDSL - public fun onAction(block: suspend PipelineContext.(action: A) -> A?): Unit = setOnce(::action, block) - /** * @see [StorePlugin.onSubscribe] */ @@ -87,55 +81,26 @@ public open class StorePluginBuilder block: suspend PipelineContext.(subscriberCount: Int) -> Unit ): Unit = setOnce(::unsubscribe, block) + /** + * @see [StorePlugin.onStop] + */ @FlowMVIDSL - @PublishedApi - internal fun build(): StorePlugin = object : StorePlugin { - override val name = this@StorePluginBuilder.name - override suspend fun PipelineContext.onStart() { - this@StorePluginBuilder.start?.invoke(this) - } - - override suspend fun PipelineContext.onState(old: S, new: S): S? { - val block = this@StorePluginBuilder.state ?: return new - return block(old, new) - } - - override suspend fun PipelineContext.onIntent(intent: I): I? { - val block = this@StorePluginBuilder.intent ?: return intent - return block(intent) - } - - override suspend fun PipelineContext.onAction(action: A): A? { - val block = this@StorePluginBuilder.action ?: return action - return block(action) - } - - override suspend fun PipelineContext.onException(e: Exception): Exception? { - val block = this@StorePluginBuilder.exception ?: return e - return block(e) - } - - override suspend fun PipelineContext.onSubscribe(newSubscriberCount: Int) { - this@StorePluginBuilder.subscribe?.invoke(this, newSubscriberCount) - } - - override suspend fun PipelineContext.onUnsubscribe(newSubscriberCount: Int) { - this@StorePluginBuilder.unsubscribe?.invoke(this, newSubscriberCount) - } - - override fun onStop(e: Exception?) { - stop?.invoke(e) - } - - override fun toString(): String = name?.let { "StorePlugin \"$it\"" } ?: super.toString() - - override fun hashCode(): Int = name?.hashCode() ?: super.hashCode() + public fun onStop(block: (e: Exception?) -> Unit): Unit = setOnce(::stop, block) - override fun equals(other: Any?): Boolean = when { - other !is StorePlugin<*, *, *> -> false - other.name == null && name == null -> this === other - else -> name == other.name - } + @PublishedApi + internal fun build(): StorePlugin { + val builder = this@StorePluginBuilder + return StorePlugin( + name = name, + onStart = { builder.start?.invoke(this) }, + onState = call@{ old, new -> builder.state?.let { return@call it(old, new) } ?: new }, + onIntent = call@{ intent -> builder.intent?.let { return@call it(intent) } ?: intent }, + onAction = call@{ action -> builder.action?.let { return@call it(action) } ?: action }, + onException = call@{ e -> builder.exception?.let { return@call it(e) } ?: e }, + onSubscribe = { builder.subscribe?.invoke(this, it) }, + onUnsubscribe = { builder.unsubscribe?.invoke(this, it) }, + onStop = { builder.stop?.invoke(it) }, + ) } } diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt index 24f35361..e9d3d213 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/plugins/CompositePlugin.kt @@ -5,7 +5,7 @@ import pro.respawn.flowmvi.api.MVIAction import pro.respawn.flowmvi.api.MVIIntent import pro.respawn.flowmvi.api.MVIState import pro.respawn.flowmvi.api.StorePlugin -import pro.respawn.flowmvi.dsl.plugin +import pro.respawn.flowmvi.dsl.StorePlugin import pro.respawn.flowmvi.util.fastFold import pro.respawn.flowmvi.util.fastForEach @@ -22,23 +22,23 @@ import pro.respawn.flowmvi.util.fastForEach public fun compositePlugin( plugins: List>, name: String? = null, -): StorePlugin = plugin { - this.name = name - onState { old: S, new: S -> plugins.iterate(new) { onState(old, it) } } - onIntent { intent: I -> plugins.iterate(intent) { onIntent(it) } } - onAction { action: A -> plugins.iterate(action) { onAction(it) } } - onException { e: Exception -> plugins.iterate(e) { onException(it) } } - onSubscribe { subs: Int -> plugins.iterate { onSubscribe(subs) } } - onUnsubscribe { subs: Int -> plugins.iterate { onUnsubscribe(subs) } } - onStart { plugins.iterate { onStart() } } - onStop { plugins.iterate { onStop(it) } } -} +): StorePlugin = StorePlugin( + name = name, + onState = { old: S, new: S -> plugins.fold(new) { next -> onState(old, next) } }, + onIntent = { intent: I -> plugins.fold(intent) { onIntent(it) } }, + onAction = { action: A -> plugins.fold(action) { onAction(it) } }, + onException = { e: Exception -> plugins.fold(e) { onException(it) } }, + onSubscribe = { subs: Int -> plugins.fold { onSubscribe(subs) } }, + onUnsubscribe = { subs: Int -> plugins.fold { onUnsubscribe(subs) } }, + onStart = { plugins.fold { onStart() } }, + onStop = { plugins.fold { onStop(it) } } +) -private inline fun List>.iterate( +private inline fun List>.fold( block: StorePlugin.() -> Unit, ) = fastForEach(block) -private inline fun List>.iterate( +private inline fun List>.fold( initial: R, block: StorePlugin.(R) -> R? -) = fastFold<_, R?>(initial) inner@{ acc, it -> block(it, acc ?: return@iterate acc) } +) = fastFold<_, R?>(initial) inner@{ acc, it -> block(it, acc ?: return@fold acc) } From 0606042ebca9ce2abd2c6d55fbf64a344730825f Mon Sep 17 00:00:00 2001 From: Nek-12 Date: Sat, 25 May 2024 21:18:23 +0300 Subject: [PATCH 26/26] disable wasmOptimize task to workaround compilation bug --- sample/build.gradle.kts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 700a28db..393ba43c 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -30,6 +30,11 @@ val generateBuildConfig by tasks.registering(Sync::class) { into(layout.buildDirectory.dir("generated/kotlin/src/commonMain")) } +// https://youtrack.jetbrains.com/issue/KT-68088 +tasks.matching { it.name.contains("compileProductionExecutableKotlinWasmJsOptimize") }.configureEach { + enabled = false +} + kotlin { applyDefaultHierarchyTemplate() @@ -182,16 +187,13 @@ dependencies { debugImplementation(projects.debugger.debuggerPlugin) } compose { - android { - } + android { } + web { } resources { packageOfResClass = Config.Sample.namespace publicResClass = false } - web { - } - desktop { application { mainClass = "${Config.Sample.namespace}.MainKt"