1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 @file:OptIn(ExperimentalStdlibApi::class) 18 19 package androidx.lifecycle.viewmodel.internal 20 21 import androidx.annotation.MainThread 22 import androidx.lifecycle.ViewModel 23 import kotlin.concurrent.Volatile 24 import kotlinx.coroutines.CoroutineScope 25 26 /** 27 * Internal implementation of the multiplatform [ViewModel]. 28 * 29 * Kotlin Multiplatform does not support expect class with default implementation yet, so we 30 * extracted the common logic used by all platforms to this internal class. 31 * 32 * @see <a href="https://youtrack.jetbrains.com/issue/KT-20427">KT-20427</a> 33 */ 34 internal class ViewModelImpl { 35 36 private val lock = SynchronizedObject() 37 38 /** 39 * Holds a mapping between [String] keys and [AutoCloseable] resources that have been associated 40 * with this [ViewModel]. 41 * 42 * The associated resources will be [AutoCloseable.close] right before the [ViewModel.onCleared] 43 * is called. This provides automatic resource cleanup upon [ViewModel] release. 44 * 45 * For specifics about the clearing sequence, refer to the [ViewModel.clear] method. 46 * 47 * **Note:** Manually [SynchronizedObject] is necessary to prevent issues on Android API 21 48 * and 22. This avoids potential problems found in older versions of `ConcurrentHashMap`. 49 * 50 * @see <a href="https://issuetracker.google.com/37042460">b/37042460</a> 51 */ 52 private val keyToCloseables = mutableMapOf<String, AutoCloseable>() 53 54 /** @see [keyToCloseables] */ 55 private val closeables = mutableSetOf<AutoCloseable>() 56 57 @Volatile private var isCleared = false 58 59 constructor() 60 61 constructor(viewModelScope: CoroutineScope) { 62 addCloseable(VIEW_MODEL_SCOPE_KEY, viewModelScope.asCloseable()) 63 } 64 65 constructor(vararg closeables: AutoCloseable) { 66 this.closeables += closeables 67 } 68 69 constructor(viewModelScope: CoroutineScope, vararg closeables: AutoCloseable) { 70 addCloseable(VIEW_MODEL_SCOPE_KEY, viewModelScope.asCloseable()) 71 this.closeables += closeables 72 } 73 74 /** @see [ViewModel.clear] */ 75 @MainThread clearnull76 fun clear() { 77 if (isCleared) return 78 79 isCleared = true 80 synchronized(lock) { 81 for (closeable in keyToCloseables.values) { 82 closeWithRuntimeException(closeable) 83 } 84 for (closeable in closeables) { 85 closeWithRuntimeException(closeable) 86 } 87 // Clear only resources without keys to prevent accidental recreation of resources. 88 // For example, `viewModelScope` would be recreated leading to unexpected behaviour. 89 closeables.clear() 90 } 91 } 92 93 /** @see [ViewModel.addCloseable] */ addCloseablenull94 fun addCloseable(key: String, closeable: AutoCloseable) { 95 // Although no logic should be done after user calls onCleared(), we will 96 // ensure that if it has already been called, the closeable attempting to 97 // be added will be closed immediately to ensure there will be no leaks. 98 if (isCleared) { 99 closeWithRuntimeException(closeable) 100 return 101 } 102 103 val oldCloseable = synchronized(lock) { keyToCloseables.put(key, closeable) } 104 closeWithRuntimeException(oldCloseable) 105 } 106 107 /** @see [ViewModel.addCloseable] */ addCloseablenull108 fun addCloseable(closeable: AutoCloseable) { 109 // Although no logic should be done after user calls onCleared(), we will 110 // ensure that if it has already been called, the closeable attempting to 111 // be added will be closed immediately to ensure there will be no leaks. 112 if (isCleared) { 113 closeWithRuntimeException(closeable) 114 return 115 } 116 117 synchronized(lock) { closeables += closeable } 118 } 119 120 /** @see [ViewModel.getCloseable] */ getCloseablenull121 fun <T : AutoCloseable> getCloseable(key: String): T? = 122 @Suppress("UNCHECKED_CAST") synchronized(lock) { keyToCloseables[key] as T? } 123 closeWithRuntimeExceptionnull124 private fun closeWithRuntimeException(closeable: AutoCloseable?) { 125 try { 126 closeable?.close() 127 } catch (e: Exception) { 128 throw RuntimeException(e) 129 } 130 } 131 } 132