app-lifecycle
アプリのライフサイクル全体を考慮し、プロセスが停止しても状態を復元したり、バックグラウンドタスクを管理したりするなど、安定した動作を実現するための様々な仕組みを効果的に活用するSkill。
📜 元の英語説明(参考)
App lifecycle patterns - process death handling, SavedStateHandle, ViewModel restoration, lifecycle-aware components, and background task management.
🇯🇵 日本人クリエイター向け解説
アプリのライフサイクル全体を考慮し、プロセスが停止しても状態を復元したり、バックグラウンドタスクを管理したりするなど、安定した動作を実現するための様々な仕組みを効果的に活用するSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o app-lifecycle.zip https://jpskill.com/download/16402.zip && unzip -o app-lifecycle.zip && rm app-lifecycle.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/16402.zip -OutFile "$d\app-lifecycle.zip"; Expand-Archive "$d\app-lifecycle.zip" -DestinationPath $d -Force; ri "$d\app-lifecycle.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
app-lifecycle.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
app-lifecycleフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
App Lifecycle Patterns
Android
ViewModel 内の SavedStateHandle
SavedStateHandle は、構成の変更とプロセスの強制終了の両方を乗り越えます。
class ProductDetailViewModel(
private val savedStateHandle: SavedStateHandle,
private val productRepository: ProductRepository
) : ViewModel() {
// プロセスの強制終了を乗り越えます
val productId: String = savedStateHandle.get<String>("productId") ?: ""
// SavedStateHandle に基づく StateFlow
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
}
// 一時的な状態 (プロセスの強制終了で失われますが、派生データには問題ありません)
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") }
}
}
}
Compose 内の rememberSaveable
@Composable
fun SearchScreen() {
// 構成の変更とプロセスの強制終了の両方を乗り越えます
var searchQuery by rememberSaveable { mutableStateOf("") }
var isFilterExpanded by rememberSaveable { mutableStateOf(false) }
// 複雑なオブジェクトの場合は、カスタムの Saver を使用します
val selectedFilters = rememberSaveable(
saver = listSaver(
save = { it.toList() },
restore = { it.toMutableStateList() }
)
) { mutableStateListOf<String>() }
// Parcelable オブジェクトの場合
var selectedProduct by rememberSaveable(stateSaver = autoSaver()) {
mutableStateOf<Product?>(null)
}
Column {
TextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text("Search...") }
)
}
}
Lifecycle-Aware Data Collection
@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)
}
}
Compose 以外のコンテキストでは、repeatOnLifecycle を使用します。
class ProductListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
Compose 内の LifecycleEventEffect
@Composable
fun AnalyticsScreen(screenName: String) {
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
analytics.logScreenView(screenName)
}
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
analytics.logScreenExit(screenName)
}
// 開始/停止のペアの場合
LifecycleStartEffect(Unit) {
val connection = serviceConnection.bind()
onStopOrDispose {
connection.unbind()
}
}
}
プロセスの強制終了のテスト
# アプリのプロセスを強制終了します (プロセスの強制終了をシミュレートします)
adb shell am kill com.myapp.android
# 完全なフロー: アプリをバックグラウンドに置き、強制終了し、復元します
adb shell input keyevent KEYCODE_HOME
adb shell am kill com.myapp.android
# これで、最近使ったアプリでアプリをタップして復元をトリガーします
# 積極的なテストのために、開発者向けオプションで [アクティビティを保持しない] を有効にします
adb shell settings put global always_finish_activities 1
バックグラウンドタスクのための WorkManager
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()
}
}
}
// 定期的な同期をスケジュールします
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
)
}
Foreground Service パターン
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
}
}
iOS
State Restoration のための @SceneStorage
struct ContentView: View {
// 自動的に保存および復元されます
(原文がここで切り詰められています) 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
App Lifecycle Patterns
Android
SavedStateHandle in ViewModel
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") }
}
}
}
rememberSaveable in Compose
@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...") }
)
}
}
Lifecycle-Aware Data Collection
@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)
}
}
}
}
}
LifecycleEventEffect in Compose
@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()
}
}
}
Process Death Testing
# 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
WorkManager for Background Tasks
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
)
}
Foreground Service Pattern
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
}
}
iOS
@SceneStorage for State Restoration
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)
}
}
}
ScenePhase in SwiftUI
@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")
}
}
Background Tasks (BGTaskScheduler)
// 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"]
Cross-Platform Patterns
State Preservation Strategy
| 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 |
Memory Pressure Handling
// 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()
}
Background/Foreground Transition Checklist
When entering background:
- Save unsaved user input
- Cancel non-essential network requests
- Persist critical state to disk
- Schedule background tasks if needed
- Release large in-memory resources
When returning to foreground:
- Check if session is still valid (token expiry)
- Refresh stale data (compare timestamps)
- Re-establish WebSocket connections
- Sync any offline changes
- Update UI with fresh data