בסוף הפרק הלוח והתוצאה ישרדו סיבוב, מעבר לרקע ופתיחה מחדש. נבדיל בין שמירת אובייקט בזמן סיבוב לבין שמירת מידע לאחר סיום התהליך.
ViewModel שומר את GameSession כאשר Android יוצר Activity חדש בסיבוב. הוא אינו דיסק ואינו שורד סיום תהליך. SharedPreferences שומרת את רצף העמודות; ממנו בונים מחדש את המשחק. לא שומרים תמונה של המסך או 42 ערכים נפרדים.
השינויים לפי סדר העבודה
התחילו במצב שעבד בסוף הפרק הקודם. קובץ חדש מוצג במלואו; בקובץ קיים מופיעים רק האזורים שמשתנים. בקטעי diff מסירים את שורות -, מוסיפים את שורות +, ומשאירים את שורות ההקשר. הסימנים עצמם אינם חלק מקוד Java או XML. אין למחוק קוד אחר שאינו מוצג.
build.gradle.kts
Gradle Scripts > build.gradle.kts (Module :app)
הוסיפו את תלות ViewModel לצד תלויות התבנית, ובצעו Sync לפני יצירת המחלקה. אין למחוק תלות קיימת.
}
}
dependencies {
+ implementation("androidx.lifecycle:lifecycle-viewmodel:2.10.0")
implementation(libs.activity.ktx)
implementation(libs.appcompat)
implementation(libs.constraintlayout)
implementation(libs.material)
GameSession.java
app > kotlin+java > com.example.connect4 > GameSession.java
צרו את המחלקה בחבילת האפליקציה. אין בה הפניה ל־Activity או ל־View: אלה מוחלפים בסיבוב. הדגל restored מונע מה־Activity החדש לדרוס משחק שכבר נמצא ב־ViewModel.
package com.example.connect4;
import androidx.lifecycle.ViewModel;
import com.example.connect4.core.GameEngine;
/** Retains the game across Activity recreation, without keeping a View or Activity. */
public final class GameSession extends ViewModel {
public GameEngine engine = new GameEngine();
public boolean restored;
}
MainActivity.java
app > kotlin+java > com.example.connect4 > MainActivity.java
מקבלים את אותה Session דרך ViewModelProvider. ביצירה הראשונה בלבד משחזרים היסטוריה. כל תו הופך לעמודה ועובר דרך apply, בדיוק כמו נגיעה. אם המידע מכיל מהלך אסור, עוצרים ושומרים את הקידומת החוקית; לא מכניסים נתונים ישירות למערך.
שומרים אחרי מהלך ואחרי New game, וגם ב־onStop. כך אין תלות ב־onDestroy, שלא מובטח שייקרא לפני סיום תהליך. SharedPreferences.Editor.apply() מעדכנת בזיכרון ושומרת ברקע; זו מתודה אחרת מ־GameEngine.apply() למרות השם הזהה.
package com.example.connect4;
import android.os.Bundle;
+import androidx.lifecycle.ViewModelProvider;
import com.example.connect4.core.GameAction;
import com.example.connect4.core.GameEngine;
import com.example.connect4.core.GameState;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
- private GameEngine engine = new GameEngine();
+ private GameSession session;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
+ session = new ViewModelProvider(this).get(GameSession.class);
+ if (!session.restored) {
+ String moves = getPreferences(MODE_PRIVATE).getString("moves", "");
+ GameEngine restored = new GameEngine();
+ for (int i = 0; i < moves.length(); i++) {
+ if (!restored.apply(new GameAction(moves.charAt(i) - '0'))) break;
+ }
+ session.engine = restored;
+ session.restored = true;
+ }
binding.board.setColumnListener(column -> {
- if (engine.apply(new GameAction(column))) render();
+ if (session.engine.apply(new GameAction(column))) { persist(); render(); }
});
binding.restart.setOnClickListener(v -> {
- engine = new GameEngine();
+ session.engine = new GameEngine();
+ persist();
render();
});
render();
}
/** Presents the model; drawing does not change the game. */
private void render() {
- GameState state = engine.snapshot();
+ GameState state = session.engine.snapshot();
binding.board.show(state);
if (state.isDraw()) binding.status.setText("Draw — board full");
else if (state.isTerminal()) binding.status.setText(state.winner() == GameState.RED ? "Red wins!" : "Yellow wins!");
else binding.status.setText(state.currentPlayer() == GameState.RED ? "Red to play" : "Yellow to play");
}
+
+ /** Saves only accepted column history; the engine reconstructs the rest. */
+ private void persist() {
+ getPreferences(MODE_PRIVATE).edit().putString("moves", session.engine.snapshot().moves()).apply();
+ }
+
+ @Override protected void onStop() {
+ persist();
+ super.onStop();
+ }
}
מריצים ובודקים
- שחקו כמה מהלכים וסובבו: אותו לוח ואותו תור מופיעים.
- עברו למסך הבית וחזרו: אין שינוי.
- סגרו את האפליקציה ופתחו מחדש: הרצף נשמר. בדקו גם משחק שכבר הסתיים.
- לחצו New game, צאו ופתחו: מופיע לוח ריק.
בדיקת המורה מזריקה היסטוריה עם מהלך אחרי ניצחון ומוודאת ששוחזרה רק הקידומת החוקית. בדיקת יצירת Activity חדש אינה לבדה הוכחה להריגת תהליך; במכשיר בודקים גם סגירה ופתיחה מחדש.
בדיקת הבנה
למה ViewModel לבדו אינו מספיק? אילו פרטים אפשר להסיק מחדש מהיסטוריית העמודות ולכן אין צורך לשמור בנפרד?