Skip to content

foooorsyth/novm

Repository files navigation

novm build novm-core novm-runtime novm-compiler novm-compose Follow @foooorsyth

ditch your ViewModels

Quick Start

First, add the ksp plugin to your project. Then, add the novm runtime and compiler to your module's build.gradle.kts file. Optionally, add novm-compose for Compose support:

dependencies {
    val novm_version = "1.4.1"
    implementation("com.forsyth.novm:novm-core:$novm_version")
    implementation("com.forsyth.novm:novm-runtime:$novm_version")
    ksp("com.forsyth.novm:novm-compiler:$novm_version")
    // optional, for compose support
    implementation("com.forsyth.novm:novm-compose:$novm_version")
}

Extend StateSavingActivity, declare state directly in your Activity, and annotate it with @Retain

import com.forsyth.novm.Retain
import com.forsyth.novm.StateDestroyingEvent
import com.forsyth.novm.StateSavingActivity
class MainActivity : StateSavingActivity() {
    // largeImage will survive configuration change
    // see: https://developer.android.com/guide/topics/resources/runtime-changes
    @Retain(across = StateDestroyingEvent.CONFIG_CHANGE)
    lateinit var largeImage: Bitmap 

    // computedHash will survive system-initiated process death
    // see: https://developer.android.com/guide/components/activities/activity-lifecycle#asem
    @Retain(across = StateDestroyingEvent.PROCESS_DEATH)
    lateinit var computedHash: String
    // ...
}

State that is designated to be retained across process death must be of a type supported by Bundle. See the chart below for supported types. See the docs for more guidance on state retention in the event of process death.

All variables annotated with @Retain must be have public visibility.

Compose support

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.forsyth.novm.StateSavingActivity
import com.forsyth.novm.compose.setContent
import com.forsyth.novm.compose.retainAcrossRecomposition
import com.forsyth.novm.compose.retainAcrossConfigChange
import com.forsyth.novm.compose.retainAcrossProcessDeath

// must extend StateSavingActivity
class ComposeActivity : StateSavingActivity() { 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // new entry point! this is not `androidx.activity.compose.setContent`
        setContent {
            // survives recomposition -- equivalent to `remember`
            var foo by retainAcrossRecomposition { mutableIntStateOf(1) }
            // survives config change (can be `Any?`) without a ViewModel
            var bar by retainAcrossConfigChange { mutableStateOf(NonSerializableClass()) }
            // survives process death (must be Bundle type) -- equivalent to `rememberSaveable`
            var baz by retainAcrossProcessDeath { mutableStateOf(SerializableClass()) }
            // ... 
        }
        // ...
    }
    // ...
}

Additionally, MutableState can always be declared in your component scope (Activities & Fragments), annotated with @Retain, and passed into your Compose composition.

Coroutine support

StateSavingActivity offers a retainedScope field, which is a CoroutineScope that will survive configuration change. Computation spanning multiple configuration changes can be safely executed here. By default, execution occurs on Dispatchers.Main -- to execute in the background, you can use withContext(Dispatchers.IO) { ... }. retainedScope's lifetime is effectively the same as a viewModelScope from a ViewModel in StateSavingActivity's ViewModelStore.

Fragment support

import com.forsyth.novm.Retain
import com.forsyth.novm.StateDestroyingEvent
import com.forsyth.novm.StateSavingFragment
class SomeFragment : StateSavingFragment() {
    @Retain(across = StateDestroyingEvent.CONFIG_CHANGE)
    lateinit var largeImage: Bitmap

    @Retain(across = StateDestroyingEvent.PROCESS_DEATH)
    lateinit var computedHash: String

    // Optional override, see below
    override var identificationStrategy = FragmentIdentificationStrategy.BY_ID
    // ...
}

Fragments are identified after recreation based on their identificationStrategy:

FragmentIdentificationStrategy.BY_TAG (default): Fragments are identified by their unique tag (you must give each of your Fragments using @Retain a unique tag using this setting)

FragmentIdentificationStrategy.BY_ID: Fragments are identified by their id

FragmentIdentificationStrategy.BY_CLASS: Fragments are identified by their class

Multi-module support

novm supports apps with multiple modules. Library modules must declare themselves as dependencies in their build.gradle.kts file:

ksp {
    arg("novm.isDependency", "true")
}

Supported types for retention across process death

Bundle Entry Type Supported by novm?
IBinder
Bundle
Byte
ByteArray
Char
CharArray
CharSequence
Array<(out) CharSequence>
ArrayList<CharSequence>
Float
FloatArray
Int
IntArray
ArrayList<Int>
Parcelable
Array<(out) Parcelable>
ArrayList<(out) Parcelable>
SparseArray<(out) Parcelable>
PersistableBundle (via #putAll())
Serializable
Short
ShortArray
Size
SizeF
String
Array<(out) String>
ArrayList<String>

R8 / ProGuard

novm should work out of the box with code minification and shrinking enabled. novm does not use runtime reflection to save and restore state, but does need to identify certain classes by name at build time. The following rules in :novm-runtime/consumer-rules.pro will be inherited by consumers:

-keep class com.forsyth.novm.** { *; }
-keep class androidx.activity.ComponentActivity { *; }
-keep class androidx.fragment.app.Fragment { *; }

Reporting bugs

To report a bug, enable debug logging and open a GitHub issue with a trace

ksp {
    arg("novm.debugLogging", "true")
}

About

ditch your ViewModels

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors