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)