一键导入
app-lifecycle
App lifecycle patterns - process death handling, SavedStateHandle, ViewModel restoration, lifecycle-aware components, and background task management.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
App lifecycle patterns - process death handling, SavedStateHandle, ViewModel restoration, lifecycle-aware components, and background task management.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Core Android development patterns for Kotlin, including coroutines, lifecycle management, and functional programming idioms.
Apple Combine framework for reactive programming. Publishers, subscribers, operators, and error handling.
Pattern extraction and skill generation for mobile development sessions. Automatically learns from your coding patterns.
Instinct-based learning system with confidence scoring for mobile patterns. Automatically extracts and evolves patterns.
Core Data for iOS persistence. Data models, fetch requests, background contexts, and SwiftData migration.
Kotlin Coroutines and Flow patterns for structured concurrency, error handling, and async operations.
| name | app-lifecycle |
| description | App lifecycle patterns - process death handling, SavedStateHandle, ViewModel restoration, lifecycle-aware components, and background task management. |
SavedStateHandle survives both configuration changes and process death:
class ProductDetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val productRepository: ProductRepository
) : ViewModel() {
// Survives process death
val productId: String = savedStateHandle.get<String>("productId") ?: ""
// StateFlow backed by SavedStateHandle
val searchQuery = savedStateHandle.getStateFlow("searchQuery", "")
val selectedTab = savedStateHandle.getStateFlow("selectedTab", 0)
fun updateSearchQuery(query: String) {
savedStateHandle["searchQuery"] = query
}
fun selectTab(index: Int) {
savedStateHandle["selectedTab"] = index
}
// Transient state (lost on process death, OK for derived data)
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
loadProduct(productId)
}
private fun loadProduct(id: String) {
viewModelScope.launch {
_uiState.value = UiState.Loading
productRepository.getProduct(id)
.onSuccess { _uiState.value = UiState.Success(it) }
.onFailure { _uiState.value = UiState.Error(it.message ?: "Unknown error") }
}
}
}
@Composable
fun SearchScreen() {
// Survives configuration change AND process death
var searchQuery by rememberSaveable { mutableStateOf("") }
var isFilterExpanded by rememberSaveable { mutableStateOf(false) }
// For complex objects, use a custom Saver
val selectedFilters = rememberSaveable(
saver = listSaver(
save = { it.toList() },
restore = { it.toMutableStateList() }
)
) { mutableStateListOf<String>() }
// For Parcelable objects
var selectedProduct by rememberSaveable(stateSaver = autoSaver()) {
mutableStateOf<Product?>(null)
}
Column {
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text("Search...") }
)
}
}
@Composable
fun ProductListScreen(viewModel: ProductListViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is UiState.Loading -> LoadingIndicator()
is UiState.Success -> ProductList(state.products)
is UiState.Error -> ErrorMessage(state.message)
}
}
For non-Compose contexts, use repeatOnLifecycle:
class ProductListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
@Composable
fun AnalyticsScreen(screenName: String) {
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
analytics.logScreenView(screenName)
}
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
analytics.logScreenExit(screenName)
}
// For start/stop pairs
LifecycleStartEffect(Unit) {
val connection = serviceConnection.bind()
onStopOrDispose {
connection.unbind()
}
}
}
# Kill the app process (simulates process death)
adb shell am kill com.myapp.android
# Full flow: put app in background, kill, then restore
adb shell input keyevent KEYCODE_HOME
adb shell am kill com.myapp.android
# Now tap the app in recents to trigger restoration
# Enable "Don't keep activities" in Developer Options for aggressive testing
adb shell settings put global always_finish_activities 1
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val repository = EntryPoint.get(applicationContext).syncRepository()
repository.syncPendingChanges()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
// Schedule periodic sync
fun schedulePeriodicSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 1, TimeUnit.HOURS,
flexInterval = 15, TimeUnit.MINUTES
)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"periodic_sync",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
}
class UploadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, "upload_channel")
.setContentTitle("Uploading...")
.setSmallIcon(R.drawable.ic_upload)
.setProgress(100, 0, true)
.build()
startForeground(NOTIFICATION_ID, notification)
startUpload()
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
companion object {
private const val NOTIFICATION_ID = 1001
}
}
struct ContentView: View {
// Automatically saved and restored per scene
@SceneStorage("selectedTab") private var selectedTab = 0
@SceneStorage("searchQuery") private var searchQuery = ""
@SceneStorage("scrollPosition") private var scrollPosition: Double = 0
var body: some View {
TabView(selection: $selectedTab) {
HomeTab()
.tag(0)
SearchTab(query: $searchQuery)
.tag(1)
ProfileTab()
.tag(2)
}
}
}
@main
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { oldPhase, newPhase in
switch newPhase {
case .active:
// App is in the foreground and interactive
refreshDataIfNeeded()
case .inactive:
// App is visible but not interactive (e.g., multitasking)
saveInProgressWork()
case .background:
// App has moved to the background
scheduleBackgroundTasks()
savePersistentState()
@unknown default:
break
}
}
}
private func refreshDataIfNeeded() {
let lastRefresh = UserDefaults.standard.object(forKey: "lastRefresh") as? Date ?? .distantPast
if Date().timeIntervalSince(lastRefresh) > 300 { // 5 minutes
Task { await DataManager.shared.refresh() }
}
}
private func savePersistentState() {
UserDefaults.standard.set(Date(), forKey: "lastBackgroundTime")
}
}
// Register in AppDelegate or App init
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.myapp.refresh",
using: nil
) { task in
handleAppRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.myapp.sync",
using: nil
) { task in
handleSync(task: task as! BGProcessingTask)
}
}
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.myapp.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 min
try? BGTaskScheduler.shared.submit(request)
}
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleAppRefresh() // Reschedule for next time
let refreshTask = Task {
do {
try await DataManager.shared.refresh()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
refreshTask.cancel()
}
}
Test background tasks in the debugger:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler]
_simulateLaunchForTaskWithIdentifier:@"com.myapp.refresh"]
| State Type | Android | iOS | Survives Process Death |
|---|---|---|---|
| UI state (scroll, tab) | SavedStateHandle | @SceneStorage | Yes |
| Form input | rememberSaveable | @SceneStorage | Yes |
| ViewModel data | Re-fetch on restore | Re-fetch on restore | No (re-load) |
| User session | EncryptedPrefs | Keychain | Yes |
| Cache | Room/DataStore | CoreData/UserDefaults | Yes |
// Android
class MyApplication : Application() {
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
when (level) {
TRIM_MEMORY_RUNNING_LOW -> ImageCache.trimToSize(50)
TRIM_MEMORY_RUNNING_CRITICAL -> ImageCache.evictAll()
TRIM_MEMORY_UI_HIDDEN -> releaseUIResources()
}
}
}
// iOS
NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: .main
) { _ in
ImageCache.shared.removeAll()
URLCache.shared.removeAllCachedResponses()
}
When entering background:
When returning to foreground: