Wczoraj opisałem pusty projekt, który dostajemy w Visual Studio, tworząc projekt F# > Android. Dziś czas na zbudowanie krok po kroku naszej pierwszej aplikacji - prostej listy zadań.
Layout
Zaczniemy od najprzyjemniejszej i najprostszej części, czyli ustalenia jak nasza aplikacja będzie wyglądać. W tym celu klikniemy dwukrotnie w plik Main.axml w naszym projekcie, aby otworzyć designer. Z menu View wybieramy Toolbox, aby mieć dostęp do kontrolek, które będziemy mogli umieścić w naszym layoutcie.
Możecie w tym momencie pobawić się umieszczając różne kontrolki i uruchamiając projekt, żeby zobaczyć jak się zachowują.
Będą nas interesować cztery kontrolki: poznany wcześniej LinearLayout, EditText do wprowadzania tekstu, CheckBox do odznaczania naszych zadań i Button do dodawania nowych zadań.
Więc na naszą pustą formę upuśmy EditText i Button, aby uzyskać taki wygląd:

Następnie przejdziemy do zakładki Source w designerze (lub naciśniemy F7) i dodamy między EditText i Button kontrolkę LinearLayout i odpowiednio je nazwiemy (zmienimy id) aby uzyskać:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/txtTask"/>
<LinearLayout
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/layout" />
<Button
android:text="Dodaj"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btnAdd" />
</LinearLayout>
I to nam na ten moment wystarczy. W kodzie ustalimy akcję po naciśnięciu przycisku dodawania, aby tworzyć obiekty CheckBox w naszym layout.
Ustalanie zachowania
Zazwyczaj kiedy tworzymy aplikacje okienkowe mamy doczynienia z programowaniem reaktywnym - t.j. piszemy zachowanie w odpowiedzi na pewne zdarzenie. Takim zdarzeniem może np. być naciśnięcie przycisku lub rozpoczęcie pisania w odpowiednim oknie.
W naszej aplikacji będziemy chcieli podpiąć się najpierw pod zdarzenie naciśnięcia przycisku “Dodaj”. Ale zanim to zrobimy musimy w pewien sposób otrzymać referencje do obiektów w naszym layoutcie. Posłuży nam do tego metoda FindViewById<T>(id) należąca do Activity.
W naszej MainActivity w przeładowanej metodzie OnCreate, poniżej this.SetContentView(Resource_Layout.Main) dodamy następujący kod:
let layout = this.FindViewById<LinearLayout>(Resource_Id.layout)
let txtTask = this.FindViewById<EditText>(Resource_Id.txtTask)
let btnAdd = this.FindViewById<Button>(Resource_Id.btnAdd)
W ten sposób uzyskujemy referencje do kontrolek, które zadeklarowaliśmy w layoutcie. Zwróćmy uwagę na Resource_Id.... Jest to statyczna klasa automatycznie generowana w pliku Resources/Resource.designer.fs na podstawie naszych layoutów. Odpowiednie id nadaliśmy przez atrybut android:id w definicji layoutu.
Mając referencje do kontrolek możemy dodać im zachowanie. Chcemy aby po naciśnięciu przycisku dodać nowy CheckBox z tekstem z okienka tekstowego do kontrolki layout:
btnAdd.Click.Add(fun args ->
let newCheckBox = new CheckBox(this)
newCheckBox.Text <- txtTask.Text
newCheckBox.CheckedChange.Add(fun args ->
layout.RemoveView(newCheckBox)
)
layout.AddView(newCheckBox, 0)
)
Nasz przycisk btnAdd ma zdarzenie Click, któremu dodajemy zachowanie. Pod zdarzenie można podpiąć wiele zachowań.
Nasze zachowanie to anonimowa funkcja, która tworzy nowy obiekt typu CheckBox, ustala jego tekst na zawartość kontrolki txtTask, następnie dodaje zachowanie do naszego checkboxa - jeśli zmieni się jego stan zaznaczenia to usuniemy go z layoutu, a kiedy mamy go przygotowanego to dodajemy go do layoutu na pierwszą pozycję (stąd 0).
Na dobrą sprawę mógłbym ten post zakończyć, bo mamy działającą aplikację ToDo, ale dodamy jeszcze dwa smaczki.
Używanie plików
Na ten moment po wyjściu z aplikacji nasze taski znikają. Chcielibyśmy więc zapisać je sobie podczas zamykania aplikacji oraz wczytać podczas otwierania. Zanim napiszemy kolejny kawałek kodu, zastanowimy się jak wygląda system plików w systemie Android.
Android jest oparty o Linux i ma znany z Linuxa system plików. Został on jednak dodatkowo zaostrzony, t.j. aplikacja może tworzyć pliki tylko w swoich lokalnych folderach oraz takich, do których użytkownik przyznał im dostęp. Ponieważ niekoniecznie chcemy aby użytkownik miał styczność z danymi aplikacji, mógłby je usunąć niechcący (lub chcący), więc będziemy przechowywali nasz plik w folderze lokalnym.
W tym celu utworzymy w naszej MainActivity pole savePath, któremu przypiszemy
let savePath = System.Environment.GetFolderPath(
System.Environment.SpecialFolder.LocalApplicationData) + "/ToDo/tasks"
Następnie utworzymy prywatną listę dla naszych checkboxów:
let mutable tasks = []
Będziemy do niej dodawali nowe checkboxy, aby podczas zamykania aplikacji spisać je do pliku. Aby ułatwić sobie nieco pracę i zachować regułę DRY, przeniesiemy tworzenie nowych checkboxów do osobnej metody:
member this.AddCheckBox text (layout:LinearLayout) =
let newCheckBox = new CheckBox(this)
newCheckBox.Text <- text
newCheckBox.CheckedChange.Add(fun args ->
layout.RemoveView(newCheckBox)
)
layout.AddView(newCheckBox, 0)
tasks <- newCheckBox :: tasks
Oraz zmodyfikujemy nasze zachowanie dla przycisku
btnAdd.Click.Add(fun args ->
this.AddCheckBox (txtTask.Text) layout
txtTask.Text <- String.Empty
)
Dołożyłem jeszcze czyszczenie txtTask po dodaniu nowego checkboxa.
Ok, mając to, przejdziemy teraz do przeładowania metody OnStop(), która jest wywoływana podczas zamykania naszej aplikacji.
override this.OnStop () =
base.OnStop()
let mutable out = ""
tasks |> List.iter (fun t -> out <- out + t.Text + "\n")
File.WriteAllText(savePath, out)
Najpierw uruchamiamy base.OnStop(), żeby uruchomić wszystkie domyślne procesy zamykania, a następnie tworzymy sobie zmienną out typu string, do której dodajemy tekst, linijka po linijce, z naszych checkboxów. Następnie zapisujemy zawartość out do pliku savePath. Proste, prawda?
Teraz aby wczytać nasze taski, na koniec metody OnCreate dodamy następujący kawałek kodu:
if File.Exists(savePath) then
let tasks = File.ReadAllLines(savePath)
Array.iter (fun t -> this.AddCheckBox t layout) tasks
Sprawdzamy czy nasz plik istnieje i jeśli tak, to go wczytujemy (lokalna stała tasks przykrywa chwilowo tą, którą zadeklarowaliśmy powyżej). Otrzymujemy tablicę stringów (lokalne tasks), po którym się iterujemy wywołując this.AddCheckBox.
Tworzenie tasków Enterem
Chcielibyśmy również móc tworzyć taski za pomocą klawisza Enter po wpisaniu tekstu do kontrolki txtTask. W tym celu dodamy zachowanie do zdarzenia txtTask.KeyPress w metodzie OnCreate.
txtTask.KeyPress.Add(fun (args:View.KeyEventArgs) ->
if args.KeyCode = Keycode.Enter then
if not String.IsNullOrEmpty(txtTask.Text) then
this.AddCheckBox (txtTask.Text) layout
txtTask.Text <- String.Empty
args.Handled <- true
else
args.Handled <- false
)
Sprawdzamy najpierw czy wciśnięty został Enter i jeśli nasze okienko nie było puste to wywołujemy this.AddCheckBox, po czym czyścimy okienko i ustawiamy flagę Handled na true, co oznacza, że kolejne zachwowania w naszym zdarzeniu nie będą już tego klawisza po nas obsługiwać.
To by było na tyle. Zobaczcie czy się kompiluje, jeśli nie to piszcie komentarzach, a ja postaram się wszelkie błędy poprawić.
Poniżej pełna klasa MainActivity
[<Activity (Label = "ToDo", MainLauncher = true, Icon = "@drawable/Icon")>]
type MainActivity () =
inherit Activity ()
let savePath = System.Environment.GetFolderPath(
System.Environment.SpecialFolder.LocalApplicationData) + "/ToDo/tasks"
let mutable tasks = []
member this.AddCheckBox text (layout:LinearLayout) =
let newCheckBox = new CheckBox(this)
newCheckBox.Text <- text
newCheckBox.CheckedChange.Add(fun args ->
layout.RemoveView(newCheckBox)
)
layout.AddView(newCheckBox, 0)
tasks <- newCheckBox :: tasks
override this.OnCreate (bundle) =
base.OnCreate (bundle)
this.SetContentView (Resource_Layout.Main)
let layout = this.FindViewById<LinearLayout>(Resource_Id.layout)
let txtTask = this.FindViewById<EditText>(Resource_Id.txtTask)
let btnAdd = this.FindViewById<Button>(Resource_Id.btnAdd)
btnAdd.Click.Add(fun args ->
this.AddCheckBox (txtTask.Text) layout
txtTask.Text <- String.Empty
)
txtTask.KeyPress.Add(fun (args:View.KeyEventArgs) ->
if args.KeyCode = Keycode.Enter then
if not String.IsNullOrEmpty(txtTask.Text) then
this.AddCheckBox (txtTask.Text) layout
txtTask.Text <- String.Empty
args.Handled <- true
else
args.Handled <- false
)
if File.Exists(savePath) then
let tasks = File.ReadAllLines(savePath)
Array.iter (fun t -> this.AddCheckBox t layout) tasks
override this.OnStop () =
base.OnStop()
let mutable out = ""
tasks |> List.iter (fun t -> out <- out + t.Text + "\n")
File.WriteAllText(savePath, out)
