Code Monkey home page Code Monkey logo

retained's Introduction

Retained Instance

A lightweight library built on top of Android Architecture Component ViewModel to simplify how UI Controllers (e.g., Activity, Fragment & NavBackStackEntry) retain instances on Android.

  • Eliminate ViewModel inheritance.
  • Eliminate ViewModelProvider.Factory need.
  • Easy access to ViewModel scoped properties: CoroutineScope (viewModelScope), SavedStateHandle, and many others.
  • Enable composition: callbacks can be listened with OnClearedListener.

Motivation: Retained was originally created to share a ViewModel in Kotlin Multiplatform projects between Android & iOS with ease.

Download

dependencies {
    // `Activity` support
    implementation 'dev.marcellogalhardo:retained-activity:{Tag}'

    // `Fragment` support, includes `Activity` support
    implementation 'dev.marcellogalhardo:retained-fragment:{Tag}'

    // Navigation support
    implementation 'dev.marcellogalhardo:retained-navigation:{Tag}'    

    // Navigation with Fragment support, includes `Navigation` support
    implementation 'dev.marcellogalhardo:retained-navigation-fragment:{Tag}'
    
    // Compose support
    implementation 'dev.marcellogalhardo:retained-compose:{Tag}'
    
    // View support (experimental)
    implementation 'dev.marcellogalhardo:retained-view:{Tag}'
    implementation 'dev.marcellogalhardo:retained-navigation-view:{Tag}'
}

(Please replace {Tag} with the latest version numbers)

Usage

The following sections demonstrate how to retain instances in activities and fragments. For simplicity, all examples will retain the following class:

class ViewModel(var counter: Int = 0)

Use Retained in Activities and Fragments

// retain an instance in an Activity:
class CounterActivity : AppCompatActivity() {
    private val viewModel: ViewModel by retain { ViewModel() }
}

// retain an instance in a Fragment:
class CounterFragment : Fragment() {
    private val viewModel: ViewModel by retain { ViewModel() }
}

// share an instance between Fragments scoped to the Activity
class CounterFragment : Fragment() {
    private val sharedViewModel: ViewModel by retainInActivity { ViewModel() }
}

// share an instance between Fragments scoped to the NavGraph
class CounterFragment : Fragment() {
    private val viewModel: ViewModel by retainInNavGraph(R.navigation.nav_graph) { ViewModel() }
}

Compose Support

@Composable
fun SampleView() {
    val viewModel = retain { ViewModel() }
    
    val activity: ComponentActivity // find Activity
    val viewModel = retain(owner = activity) { ViewModel() }
    
    val fragment: Fragment // find Fragment
    val viewModel = retain(owner = fragment) { ViewModel() }
    
    val navBackStackEntry: NavBackStackEntry // find NavBackStackEntry
    val viewModel = retain(owner = navBackStackEntry) { ViewModel() }
}

Advanced usage

Custom parameters from Jetpack's ViewModel

When retaining an instance, you have access to a RetainedEntry which contains all parameters you might need.

@Composable
fun SampleView() {
    val viewModel = retain { entry: RetainedEntry ->
        ViewModel()
    }
    // ...
}

The entry exposes a SavedStateHandle that can be used to work with the saved state, just like in a regular Android ViewModel.

class CounterFragment : Fragment() {
    private val viewModel: ViewModel by retain { entry -> 
        ViewModel(counter = entry.savedStateHandle.get<Int>("count"))
    }
    // ...
}

It also exposes a CoroutineScope that works just like viewModelScope from the Android ViewModel.

class Presenter(scope: CoroutineScope) { /* ... */ }

fun SampleFragment() {
    private val presenter: Presenter by retain { entry -> 
        Presenter(scope = entry.scope)
    }
    // ...
}

For more details, see RetainedEntry.

Listening onCleared calls

When retaining an instance, you can use the RetainedEntry to be notified when a retained instance is cleared (ViewModel.onCleared).

@Composable
fun SampleView() {
    val viewModel = retain { entry ->
        entry.onClearedListeners += {
            println("Invoked when the host 'ViewModel.onCleared' is called")
        }
        // ...
    }
    // ...
}

As a convenience, if the retained instance implements the OnClearedListener interface, it will be automatically added to onClearedListeners and notified.

View support (experimental)

Besides Activities and Fragments, it's also possible to retain instances in a view. There are a couple of extra modules for that:

dependencies {
    implementation 'dev.marcellogalhardo:retained-view:{Tag}'
    implementation 'dev.marcellogalhardo:retained-navigation-view:{Tag}'
}

The retained-view module exposes retainInActivity and retain, which will scope the instance to the parent being it an activity or a fragment. The retained-view-navigation module exposes retainInNavGraph to retain instances scoped to the NavGraph.

License

Copyright 2019 Marcello Galhardo

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

retained's People

Contributors

grodin avatar marcellogalhardo avatar saket avatar tfcporciuncula avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

retained's Issues

Set `warningAsError=true`

To help keeping the project in good shape, we can set all warning as errors in all our modules.

android {
    kotlinOptions {
        kotlinOptions.allWarningsAsErrors = true
    }
}

Promote Retained API to 1.0.0

Retained has been stable for a long period of time, so we do not foresee any major API change. We already have GitHub Actions to ensure the quality of any future release while committing to the current API.

Note we will wait for #62 to be resolved before promoting the API officially to 1.0.0 stable. We want to be sure Compose support is included as a stable API. The view API will keep its status as an experimental API and will be revised after this release.

Support NavGraph

JetPack Navigation includes support for ViewModel (navGraphViewModels) which today is not supported by retained. I propose we include a retainInNavGraph that will cover this use case. We still need to study in which artefact it would be better to add this function (either create a new navigation artefact or include it as part of the main artefacts).

Expose `key` and `classRef` in `RetainedEntry`

To be able to create factories in a service locator fashion, we need to expose key: String and classRef: KClass<Any> in the RetainedEntry interface. This way, one can use this information to create their objects based in the classRef or key.

Add first class support to Compose

Version 2.5.0 of Lifecycle introduces a new CreationExtras parameter which would allow us to provide better support for SavedStateHandle on Compose.

What does that mean to Retained? In addition to the current APIs we currently support (ComponentActivity, Fragment and NavBackStackEntry) we could add a composable function that knows how to resolve the parent lifecycle owner, and connects to the right SavedStateProvider. Everything out of the box

The current API of Retained would remain the same, and you should be able to use any of the currently supported APIs with Compose, with the addition of a new function (to be defined):

@Composable
fun View() {
    val vm = retain { entry: RetainedEntry ->
        MyViewModel(entry.savedStateHandle)
    }
    // Other stuff.
}

Open questions:

  • Should we return a Retained delegate or follow viewModel and return the instance itself?

The new function would be provided by a new artifact: retained-compose.

Set `buildconfig=false` on library modules

Pull Request #67 failed a Binary Compatibility Validator check due to changes in the namespace. That happens because we are generating a BuildConfig in the library module. That is not necessary as we don't use that in the library at all.

We probably can set all the defaults to false and "play safe":

// gradle.properties
android.defaults.buildfeatures.aidl=false
android.defaults.buildfeatures.buildconfig=false
android.defaults.buildfeatures.compose=false
android.defaults.buildfeatures.dataBinding=false
android.defaults.buildfeatures.prefab=false
android.defaults.buildfeatures.renderscript=false
android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false
android.defaults.buildfeatures.viewBinding=false

Allow `ViewModel.onCleared` callback to be listened by any object

Only the object returned by retain method is capable to listen to ViewModel.onCleared implementing DisposableHandle interface from Kotlin library. Here we have two problems:

  1. Retained aims to allow composability. Unfortunately, this solution means that an object that implements DisposableHandle inside the retained object might not get the callback if not directly call it.
  2. We created a dependency to kotlin.coroutines.DisposableHandle. This is not a big problem because we already depend on Coroutines artefact to provide scope: CoroutineScope but it we don't want to create extra dependency that is not required, even by types.

One solution might be to modify RetainedEntry in the following way:

typealias OnClearedListener = () -> Unit

class RetainedEntry(
    val onClearedListeners: MutableList<OnClearedListener>
    // ...
)

Now, anyone with access to onClearedListeners can easily listen to the callback with onClearedListeners += {}.

Kotlin Multiplatform support

Use case

Creating a responsive application with Compose Multiplatform (Desktop, Android, ...) in which all screens can be in common code can force the consumer of this library to create manually expect/actual for any retain like function.

Thoughts

After talking with @marcellogalhardo, some conclusions are:

  • The core and compose modules can be converted to multiplatform
  • Android gives us "scopes": Activity, Fragment or NavBackStackEntry. We would need to handle those scopes in multiplatform too. That isn't a problem, as we can have a RetainedStore which on Android uses a ViewModelStore version of it but on other platforms is a simple list.
  • We will need to provide a SavedStateHandle which is multiplatform. Maybe support restorable objects on iOS

Better separate modules

Now that I have a better understanding on Compose implementation of ViewModels, to prepare for release 1.0.0 I propose a new module structure:

  • core: vanilla functionalities. E.g., retain(viewModelStoreOwner, ...) {}.
  • activity: activity extension functions, without Compose support. E.g., Activity.retain {}.
  • activity-compose: activity extension functions, with Compose support. E.g., @Composable retainInActivity {}.
  • fragment: fragment extension functions, without Compose support. E.g., Fragment.retain {}.
  • fragment-compose: fragment extension functions, with Compose support. E.g., @Composable retainInFragment {}.
  • navigation: navigation dedicated functionalities. E.g., retain(navBackStackEntry) {}.
  • navigation-compose: navigation extension functions, with Compose support. E.g., @Composable retainInNavGraph {}.
  • navigation-fragment: navigation extension functions, with Fragment support. E.g., Fragment.retainInNavGraph(navId) {}

This new structure will guarantee that new applications that only are built only with Compose do not require transitive dependency on fragments or other Android components.

`RetainedEntry` should not be exposed as an interface

There are no cases where someone would like to implement a RetainedEntry interface. Therefore, we should consider making it a class with internal constructor.

Open questions:

  • Should we provide a default constructor to RetainedEntry for tests purposes?

Spike: Drop Fragment and AppCompat support on 'retained-compose'

Since beginning, we support both AppCompatActivity and Fragment in 'retained-compose' artefact. However, with the new activity-compose and viewmodel-compose artefacts, it opens the possibility to completely drop the previous bloated classes and support only the bare minimum.

As a side effect, we would need to do a big compromise: Retained Compose will only be able to load arguments from ComponentActivity and/or NavBackStackEntry (navigation compose). In other words, Fragment users will require to load arguments from a Fragment one must set manually defaultArgs: Bundle = fragment?.arguments parameters.

Sample uses Closeable instead of OnClearedListener

This is incredibly minor, but the sample project has a retained instance which implements Closeable and I'm pretty certain it should implement OnClearedListener instead.

Thanks for this library by the way. The boilerplate associated with ViewModel is just irritating noise most of the time and this library eliminates it totally!

Unable to access functions in retained-navigation

Thanks for creating this library! I'm trying to use it in Compose UI, but I'm unable to access NavBackStackEntry.retain from my project. It's almost as if Gradle is unable to find any of its APIs. This can be reproduced in the sample project too!

- implementation projects.core
- implementation projects.activity
- implementation projects.fragment
- implementation projects.view

+ implementation 'dev.marcellogalhardo:retained-activity:0.15.0'
+ implementation 'dev.marcellogalhardo:retained-fragment:0.15.0'
+ implementation 'dev.marcellogalhardo:retained-view:0.15.0'
+ implementation 'dev.marcellogalhardo:retained-navigation:0.15.0'

Both SampleFragment and SampleView will fail to compile. Any ideas what might be happening?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.