niedziela, lipca 11, 2010

Modale dobre - confirm dla Androida :-)

Dla niezorientowanych, w komputerach mamy 2 typy okienek z komunikatami. Okienka modalne i niemodalne.
Modalne to takie, które blokują interfejs użytkownika i często też przepływ programu, niemodalne to takie, które niczego nie blokują.
Zwykle niemodalne są lepsze.
Jest jednak kilka sytuacji gdy okienka modalne w najbardziej klasycznej wersji ułatwiają życie.

W JavaScrip'cie możemy mieć taki fragment:
if (confirm("Czy mam coś zrobić?")) {
// coś robię
}

Który wyświetli modalne okienko z pytaniem "Czy mam coś zrobić", jeżeli użytkownik odpowie tak to zostanie wykonany kod opisany jako "coś robię".

Jeżeli jednak użyjemy okienka niemodalnego to już tak prosto nie jest:
function code4Yes() {
// coś robię
}
myConfirm("Czy mam coś zrobić?",{onYes:code4Yes});


Jeszcze gorzej w takim przypadku:
if (confirm("Czy mam coś zrobić?")) {
// coś robię
} else {
// robię coś innego
}
// robię część wspólną


Bo wtedy rozwiązanie niemodalne się jeszcze bardziej komplikuje:

function restOfOperations() {
// robię część wspólną
}
function code4Yes() {
// coś robię
restOfOperations();
}
function code4No() {
// robię coś innego
restOfOperations();
}
myConfirm("Czy mam coś zrobić?",{onYes:code4Yes,onNo:code4No});


W JavaScript możemy po prostu nie chcieć użyć confirm bo okienko jest brzydkie, ale w takim Androidzie sprawa ma się tak, że po prostu nie wolno nam użyć "klasycznego" modala :-(
Tak się składa, że nie możemy z wątku nie związanego z UI nic z tym UI robić. W ogóle w modelu wątków/pamięci Java'y trudno byłoby zrealizować takie zachowanie jak to z klasycznym confirm.

Spróbowałem sobie coś takiego zakodować przed chwilą i okazało się to być jeszcze bardziej skomplikowane od rozwiązania z okienkiem modalnym, którego używa się normalnie ;-)

final StringBuilder sb = new StringBuilder();
v.post(new Runnable() {
public void run() {
AlertDialog.Builder builder = new AlertDialog.Builder(AddAccount.this);
builder.setMessage("I know this account!\nDo you want me to refresh list of blogs for this account?")
.setCancelable(false)
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
synchronized (sb) {
sb.append("1");
}
}
})
.setNegativeButton("No", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
AddAccount.this.finish();
}
});
AlertDialog alert = builder.create();
alert.show();
}
});
while (1==1) {
Log.i("toster", "waiting....");
synchronized (sb) {
if (sb.length()>0) {
Log.i("toster","done :-)");
break;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException ie) {

}
}

W tym kodzie jest na dodatek błąd, który spowodowałby, że w razie kliknięcia przez użytkownika "No" watek sprawdzający długość StringBuildera nadal by biegał, nawet po zamknięciu aktywności ;-)
Bardziej klasycznie to powinno wyglądać tak:
v.post(new Runnable() {       
public void run() {
AlertDialog.Builder builder = new AlertDialog.Builder(AddAccount.this);
builder.setMessage("I know this account!\nDo you want me to refresh list of blogs for this account?")
.setCancelable(false)
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Log.i("toster","done :-)");
}
})
.setNegativeButton("No", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
AddAccount.this.finish();
}
});
AlertDialog alert = builder.create();
alert.show();
}
});


To poprzednie rozwiązanie z blokowaniem przepływu można by było jednak tak ubrać by wyglądało to tak, że wołalibyśmy tylko confirm, które już wewnętrznie tworzyłoby StringBuildera [albo StringBuffera czy coś innego, StringBuffer pozwoliłby nam wywalić synchronizację ;-)] oraz ukrywał przed oczami kodera to sprawdzanie czy button został kliknięty.
Dla kodera wyglądałoby to jak klasyczny confirm z JavaScript. Czyli wykonanie kodu zostałoby wstrzymane do momentu gdy użytkownik nie kliknąłby na Yes.
Na pewno byłoby to rozwiązanie brzydkie od strony czytelności kodu, ale zdecydowanie ułatwiałoby to życie ;-)

Zakodowałem to i kod wygląda teraz tak:
private boolean confirm(final String msg) {
class Result {
int result;
};
final Result result = new Result();
runOnUiThread(new Runnable() {
public void run() {
AlertDialog.Builder builder = new AlertDialog.Builder(AddAccount.this);
builder.setMessage(msg)
.setCancelable(false)
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
synchronized(result) {
result.result=1;
result.notifyAll();
}
}
})
.setNegativeButton("No", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
synchronized(result) {
result.result=2;
result.notifyAll();
}
}
});
AlertDialog alert = builder.create();
alert.show();
}
});
while (result.result==0) {
synchronized (result) {
try {
result.wait();
} catch (InterruptedException e) {
// OK, nothing important happen
}
}
}
}


Wywołanie takiego kodu mogłoby wyglądać wtedy tak:
if (confirm("I know this account!\nDo you want me to refresh list of blogs for this account?")) {
Log.i("toster","done :-)");
}


Zwróćcie uwagę na przebiegłe wykorzystanie wait i notifyAll ;-)
Mój confirm działa tak, że przy użyciu przekazanego widoku wątek UI proszony jest o wykonanie kodu, w którym tworzony i wyświetlany jest dialog. W tym samym czasie wątek "proszący" sprawdza czy zwrócono jakiś rezultat, a jeżeli nie zwrócono to synchronizuje się na obiekcie rezultatu i mówi JVM "poczekam sobie na monitorze podpiętym do result". JVM przesuwa ten wątek do śpiących/czekających. W tym samym czasie jakiś wątek UI obserwuje czy użytkownik nie kliknął nam na któryś z guziczków, jeżeli klikną to wywoływany jest kod anonimowych listenerów, które działają w taki sposób, że synchronizują się na obiekcie rezultatu, ustawiają odpowiednią wartość na tym rezultacie i wysyłają notifyAll() budząc wszystkie wątki czekające na monitorze podpiętym do result. W tym momencie nasz wątek jest budzony i zaczyna znów biec.

Ktoś mi powie czemu takie rozwiązane jest złe? Czy może jest OK? A jeżeli jest OK to dlaczego nie ma czegoś takiego "w standardzie"? :-)


Podobne postybeta
Modale nie takie dobre dla Androida ;-)
wait() i notify()/notifyAll() - najbardziej nierozumiane metody klasy Object ;-)
Przepływ sterowany danymi - A takie Java'owe coś ;-)
Potfór ;-) czyli generator z yield w Java'ie
Sztuczki tropiciela błędów, part 4