jpskill.com
🛠️ 開発・MCP コミュニティ

app-lifecycle

アプリのライフサイクル全体を考慮し、プロセスが停止しても状態を復元したり、バックグラウンドタスクを管理したりするなど、安定した動作を実現するための様々な仕組みを効果的に活用するSkill。

📜 元の英語説明(参考)

App lifecycle patterns - process death handling, SavedStateHandle, ViewModel restoration, lifecycle-aware components, and background task management.

🇯🇵 日本人クリエイター向け解説

一言でいうと

アプリのライフサイクル全体を考慮し、プロセスが停止しても状態を復元したり、バックグラウンドタスクを管理したりするなど、安定した動作を実現するための様々な仕組みを効果的に活用するSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
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
🪟 Windows (PowerShell)
$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. 1. 下の青いボタンを押して app-lifecycle.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → app-lifecycle フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

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:

  1. Save unsaved user input
  2. Cancel non-essential network requests
  3. Persist critical state to disk
  4. Schedule background tasks if needed
  5. Release large in-memory resources

When returning to foreground:

  1. Check if session is still valid (token expiry)
  2. Refresh stale data (compare timestamps)
  3. Re-establish WebSocket connections
  4. Sync any offline changes
  5. Update UI with fresh data