ditch your ViewModels
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.
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.
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.
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
novm supports apps with multiple modules. Library modules must declare themselves as dependencies in their build.gradle.kts file:
ksp {
arg("novm.isDependency", "true")
}| 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> | ✅ |
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 { *; }
To report a bug, enable debug logging and open a GitHub issue with a trace
ksp {
arg("novm.debugLogging", "true")
}