Android: Tiszta back stack, rendes alkalmazás

Posted by | · · · · · | Webstar Works | Nincs hozzászólás a(z) Android: Tiszta back stack, rendes alkalmazás bejegyzéshez

Android platformon alapértelmezetten az activity-k LIFO stack-et alkotnak. Az előtérben levő activity van a verem tetején, a felhasználó pedig a vissza gombbal tudja fentről lefelé rombolni a vermünket. A vissza gomb megnyomásakor a képernyőn látható activity megsemmisül, az alatta levő activity kerül a helyére. A verem kiürülése esetén a Home képernyőre kerül a felhasználó.

diagram_backstack

http://developer.android.com/guide/components/tasks-and-back-stack.html

Általában ez az alapértelmezett működés megfelelő, azonban előfordulnak speciális esetek amikor magunknak kell kézbe venni az irányítást. A fejlesztők alapvetően a speciális intent flag-ekkel és activity attribútumokkal tudják befolyásolni a back stack felépítését és működését. Azonban fontos megjegyezni, hogy körültekintően változtassuk meg az alapértelmezett működést, gondoljuk át még egyszer, hogy továbbra is egyértelmű marad a felhasználó számára a vissza gomb működése.

A probléma

Adott az igény, hogy bizonyos események bekövetkezésekor, új activity-k létrehozásakor törölnünk kell az alkalmazásunkhoz tartozó back stack-et, tehát azt szeretnénk, hogy a frissen létrejött és előtérbe kerülő activity-nk legyen a verem egyetlen eleme. A mi példánkban azt szeretnénk, hogy kijelentkezés után a bejelentkező képernyő legyen a back stack egyetlen eleme. Ez első sorban azért fontos mert nem szeretnénk, hogy a kijelentkezés után a vissza gombbal elérhetőek legyenek az előző képernyők, másodsorban pedig az is fontos, hogy a már elérhetetlen activity-k ne foglalják feleslegesen a memóriát.

logoutab

Az ábrán látható, hogy két esetre kell felkészülnünk. Az “A” esetben az alkalmazás bejelentkezett felhasználó nélkül indul el, míg a “B” esetben már korábban egy felhasználó bejelentkezett.

Megoldás

A legegyszerűbb megoldásnak a bevezetőben is említett intent flag-ek és activity attribútumok mágikus kombinációjának alkalmazása tűnik. A kívánt működést a FLAG_ACTIVITY_CLEAR_TASK használatával lehet megidézni.

public class LogoutActivity extends Activity {

    private void logout(){
        Intent intent = new Intent(this, LoginActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK
                          | Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(intent);
    }
}

A logout() metódus meghívása után pontosan az történik, mint ami feljebb található ábrán látható. A korábbi activity-k megsemmisülnek, a back stack egyetlen eleme a LoginActivity lesz. Fontos, hogy FLAG_ACTIVITY_CLEAR_TASK-ot a FLAG_ACTIVITY_NEW_TASK-al együtt kombinálva használjuk, mert enélkül előfordulhat, hogy a felhasználó a Home képernyőn találja magát.

Ezzel itt be is fejezhetném ezt a post-ot ha nem lenne egy apró bökkenő. A FLAG_ACTIVITY_CLEAR_TASK API level 11-ben jelent meg, tehát csak Android 3.0-tól felfele tudjuk használni. Jelenleg a készülékek körülbelűl 30%-án fut 3.0-nál alacsonyabb verziójú Android, ezért mindenképp szükségünk van egy kerülőútra.

android_chart

2013. 09. 26-tól 2013. 10. 02-ig

Az ábra a 2013 őszi Android verzió helyzetet mutatja. A 2.2 Froyo és 2.3.3 – 2.3.7 Gingerbread a torta 30.7%-át birtokolják.

Megoldások < Android 3.0

android-gingerbread

FLAG_NO_HISTORY

Egy újabb intent flag amit a hasznunkra fordíthatnánk. Legalábbis a Stack Overflow jónéhány válaszolója szerint. Ha minden új activity-t FLAG_NO_HISTORY flag-gel nyitunk meg, kijelentkezés után valóban nem fogunk tudni visszalépni az előző képernyőre. Az egyetlen probléma, hogy az összes többi képernyőn sem tudunk visszalépni az előző activity-re. A FLAG_NO_HISTORY flag-gel indított activity-k amint a háttérbe kerülnek megsemmisülnek.

flag_no_history

A back stack-en mindig csak egy activity

Számunkra ez a működés nem megfelelő, azonban előfordulhatnak olyan más esetek amikor elfogadható.

FLAG_ACTIVITY_CLEAR_TOP

A post-om utolsó intent flag-je. Bizonyos esetekben tökéletesen megfelel az igényeinknek. Amennyiben az indítandó activity-ből van már egy példány a back stack-en, új activity létrehozása helyett, a korábban létrehozott activity felett levő összes activity megsemmisül, így kerül előtérbe a kívánt activity. Azonban ha az indítandó activity-ből nincs korábbi példány a back stack-en, az alapértelmezett működés szerint, létrejön egy új activity a verem tetején.

b_flag_activity_clear_top

Korábban már bejelentkezett a felhasználó

A kijelentkezés “B” eset előnytelen működésével egy apró trükkel tudunk megbirkózni. Abban az esetben is elindítjuk a LoginActivity-t, ha a felhasználó már egy előző indítás során bejelentkezett, csak átugorjuk mielőtt megjelenne.

public class LoginActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (isLoggedIn()) {
            startActivity(new Intent(this,Activity1.class));
        }
    }
}

Természetesen ha valóban el szeretnénk titkolni a háttérben bujkáló LoginActivity-t akkor nem árt majd felülírnunk az Activity1 onBackPressed() metódusát.
Öröm bodottá! Vagyis nem. Nem igazán rugalmas ez a megoldás. A mi esetünkben még pont megfelelő, de el lehet képzelni olyan eseteket amikor jóval bonyolultabb és logikátlanabb lenne egy activity-nek a beszúrása, csak azért, hogy nekünk megfelelő módon működjön a FLAG_ACTIVITY_CLEAR_TOP flag.

BroadcastReceiver

A megoldás lényege, hogy készítünk egy broadcastReceivert ami egy logout üzenet megérkezésekor megsemmisíti a regisztráló activity-t.

public class LogoutBroadcastReceiver extends BroadcastReceiver {
    public static final String LOGOUT = "com.app.logout";

    private Activity mActivity;

    public LogoutBroadcastReceiver(Activity activity) {
        if (mActivity == null) {
            throw new IllegalArgumentException(
                          "activity must not be null");
        }
        mActivity = activity;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        mActivity.finish();
    }
}

Ezt a receiver-t minden activity regisztrálja az onCreate() függvényében és természetesen leiratkozik az onDestroy() függvényében.

public class LoginActivity, Activity1, Activity2 extends Activity {

    private LogoutBroadcastReceiver mLogoutBroadcastReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_1);
        mLogoutBroadcastReceiver = new
        LogoutBroadcastReceiver(this);
        LocalBroadcastManager.getInstance(this).registerReceiver(
           mLogoutBroadcastReceiver,
           new IntentFilter(LogoutBroadcastReceiver.LOGOUT));
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        LocalBroadcastManager.getInstance(this).unregisterReceiver(
            mLogoutBroadcastReceiver);
   }
}

Amikor a felhasználó megnyomja a kijelentkezés gombot, útjának engedjük a logout üzenetet és elindítjuk a LoginActivity-t.

public class LogoutActivity extends Activity {

    public void onClickLogout(View v){
        Intent intent = new Intent(this,LoginActivity.class);
        startActivity(intent);
        LocalBroadcastManager.getInstance(this).sendBroadcast(
            new Intent(LogoutBroadcastReceiver.LOGOUT));
        finish();
    }
}

Ezzel a megoldással mindkét esetben elérjük a célunkat és elég rugalmas ahhoz, hogy más esetekben is használni tudjuk. Azonban fontos megemlíteni, hogy az üzenet küldése és elkapása aszinkron módon történik. Nem túl ideális esetben előfordulhat, hogy a felhasználó már kijelentkező képernyőt látja, míg a háttérben nem minden activity semmisült meg. Fontos lehet hogy a LoginActivity-t felkészítsük erre az esetre.

Saját Application osztály

Készítünk egy saját Application osztályt, ami rögzíti a létrejött activity-ket és ha szeretnénk megsemmisíti az aktívakat.

public class MyApplication extends Application{
    private Collection mActivities;

    @Override
    public void onCreate() {
        super.onCreate();
        mActivities = new ArrayList();
    }

    public void onActivityCreated(Activity activity){
        mActivities.add(activity);
    }

    public void onActivityDestroyed(Activity activity){
        mActivities.remove(activity);
    }

    public void finishAllActivities(){
        for(Activity activity : mActivities){
            activity.finish();
        }
        mActivities.clear();
    }
}

Az AndroidManifest.xml-ben jelezzük, hogy az alkalmazásunkban a kibővített application osztályunkat szeretnénk használni. Ezek után az alkalmazásunk futása alatt mindvégig elérhető lesz a saját kibővített application objektumunk.


    ...

Ezután minden activity-be beillesztjük a következő sorokat, amivel regisztráljuk az activity létrejöttét és megsemmisülését az application objektumunkban.

public class LoginActivity, Activity1, Activity2  extends Activity {

    private MyApplication mMyApplication;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_2);
        mMyApplication = (MyApplication) getApplication();
        mMyApplication.onActivityCreated(this);
   }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       mMyApplication.onActivityDestroyed(this);
   }
}

Legvégül kijelentkezéskor, a LoginActivity megnyitása előtt meghívjuk az application osztályunk finishAllActivities() metódusát, ami megsemmisíti az aktív activity-ket. Ezzel teljesítettük a követelményeket, biztonságosan kijelentkeztettük a felhasználót.

A bejegyzésemben megpróbáltam összefoglalni milyen lehetőségek vannak arra, hogy a back stack-et kitakarítsuk, tiszta lappal indítsunk új activity-ket. Jó néhány olyan helyzet előfordulhat amikor szükség lehet rá, Android 3.0 alatt mégis küszködős a dolog. Még egy, az összes Android verzión működő megoldást bedobnék a kalapba amiről nem írtam.

Amikor csak lehet, használjatok intent flag-eket a back stack módosításához. Csak hogy még nehezebb legyen a helyzet 3.0 alatt, az utolsó két példával csak óvatosan. Sajnos az onDestroy()  meghívása nem garantált. Nincs kifejtve pontosan mikor, de kivételes esetekben előfordulhat, hogy az OS az onDestroy() meghívása nélkül semmisíti meg az activity-nket.

Ha bármilyen egyéb ötleted, megoldásod van a témában, ne habozz, oszd meg velünk!


No Comments

Leave a comment