Commit 8dd7f102 by Patryk Czarnik

Projekt 4 - dodanie przeliczania walut !

parent dc6720d9
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
......
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
......
...@@ -9,9 +9,20 @@ W tym projektcie: ...@@ -9,9 +9,20 @@ W tym projektcie:
- większość napisów jest umieszczona w `strings.xml` - większość napisów jest umieszczona w `strings.xml`
- w podziale na pliki oparliśmy się o wzór z inicjalnego projektu → dwa fragmenty - w podziale na pliki oparliśmy się o wzór z inicjalnego projektu → dwa fragmenty
Nie traktujcie tego projektu jako wzoru do naśladowania w kontekście wykorzystania fragmentów
– nie robię tu tego w zalecany sposób.
Bardziej zależało mi na pokazaniu pozostałych elementów, a gotowy wzorzec, o który oparliśmy się,
akurat używa techniki fragmentów.
## Waluty ## Waluty
Aplikacja na żądanie pobiera kursy walut w formacie XML z serwera NBP i wyświetla pobrane dane. Aplikacja na żądanie pobiera kursy walut w formacie XML z serwera NBP i wyświetla pobrane dane.
Pobieranie jest wykonywane w tle przez wątek / executor. Pobieranie jest wykonywane w tle przez wątek / executor.
Aby aplikacja miała uprawnienie „internet”, trzeba było dodać stosowny wpis do manifestu. Aby aplikacja miała uprawnienie „internet”, trzeba było dodać stosowny wpis do manifestu.
**Uzupełnione po zajęciach:** Na drugim ekranie (*Second Fragment*) dodałem wybór waluty z rozwijanej listy (`Spinner`)
i możliwość przeliczania kwot poprzez wpisanie kwoty w górne lub dolne pole i zatwierdzenie (na komputerze po prostu Enter).
Aby oba ekrany (oddzielne klasy w Javie) miały dostęp do wspólnych danych, użyłem prymitywnego
lecz skutecznego rozwiązania - klasy typu *Singleton*.
...@@ -16,12 +16,24 @@ import com.example.projekt4.databinding.FragmentFirstBinding; ...@@ -16,12 +16,24 @@ import com.example.projekt4.databinding.FragmentFirstBinding;
import com.example.waluty.ExchangeRatesTable; import com.example.waluty.ExchangeRatesTable;
import com.example.waluty.ObslugaNBP; import com.example.waluty.ObslugaNBP;
import com.example.waluty.Rate; import com.example.waluty.Rate;
import com.example.waluty.SingletonPobranaTabela;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
/* Ważnym aspektem tego przykładu jest pobieranie danych z sieci, które trzeba wykonać w tle,
aby nie blokować głównego wątku obsługującego interfejs użytkownika.
Gdybyśmy pobieranie wykonali bezpośrednio w procedurze obsługi zdarzenia, aplikacja
byłaby nieresponsywna w trakcie tego pobierania.
Android sam wykrywa taką sytuację i przerywa pobieranie wyjątkiem.
Na potrzeby pobierania w tle przygotowujemy dodatkowy wątek w formie tzw. Executora
- jest to rozwiązanie, które można łatwo skalować zwiększając liczbę wątków w puli.
U nas będzie to jednak pojedynczy dodatkowy wątek.
*/
public class FirstFragment extends Fragment { public class FirstFragment extends Fragment {
private ExecutorService watkiPobierajace = Executors.newSingleThreadExecutor(); // albo fixedThreadPool private final ExecutorService watkiPobierajace = Executors.newSingleThreadExecutor();
// private ExecutorService watkiPobierajace = Executors.newFixedThreadPool(4);
private FragmentFirstBinding binding; private FragmentFirstBinding binding;
...@@ -42,20 +54,28 @@ public class FirstFragment extends Fragment { ...@@ -42,20 +54,28 @@ public class FirstFragment extends Fragment {
NavHostFragment.findNavController(FirstFragment.this) NavHostFragment.findNavController(FirstFragment.this)
.navigate(R.id.action_FirstFragment_to_SecondFragment) .navigate(R.id.action_FirstFragment_to_SecondFragment)
); );
// jeśli waluty były już pobierane - wykorzystaj dane zapisane w pamięci
ExchangeRatesTable table = SingletonPobranaTabela.getInstance().getTable();
if(table != null) {
odswiezWidok(table);
}
// do przycisku "Pobierz bieżące" przypisujemy akcję pobierania
binding.buttonPobierzBiezace.setOnClickListener(v -> { binding.buttonPobierzBiezace.setOnClickListener(v -> {
wykonajCalePobieranie(); wykonajCalePobieranie();
}); });
// TODO dla chętnych - dodaj operację pobierania kursów archiwalnych w oparciu o datę
} }
private void wykonajCalePobieranie() { private void wykonajCalePobieranie() {
Log.d("Waluty", "pobieranie rozpoczęte"); Log.d("Waluty", "zlecam pobieranie");
// komunikacji sieciowej nie wolno robić w wątku głównym // zlecam zadanie do wykonania w tle
watkiPobierajace.submit(() -> { watkiPobierajace.submit(() -> {
Log.d("Waluty", "a kuku 1"); Log.d("Waluty", "executor: początek pobierania");
ExchangeRatesTable table = ObslugaNBP.pobierzBiezaceKursy(); ExchangeRatesTable table = ObslugaNBP.pobierzBiezaceKursy();
Log.d("Waluty", "a kuku 2"); SingletonPobranaTabela.getInstance().setTable(table); // aby SecondFragment też mógł korzystać...
Log.d("Waluty", "executor: koniec pobierania");
// jednak zmiany w wyglądzie UI powinny być wykonane przez wątek główny // jednak zmiany w wyglądzie UI powinny być wykonane przez wątek główny
// - zlecam wątkowi głównemu wykonanie "gdy znajdzie na to czas"
this.getActivity().runOnUiThread(() -> { this.getActivity().runOnUiThread(() -> {
odswiezWidok(table); odswiezWidok(table);
}); });
......
...@@ -4,16 +4,29 @@ import android.os.Bundle; ...@@ -4,16 +4,29 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.example.projekt4.databinding.FragmentSecondBinding; import com.example.projekt4.databinding.FragmentSecondBinding;
import com.example.waluty.ExchangeRatesTable;
import com.example.waluty.Rate;
import com.example.waluty.SingletonPobranaTabela;
public class SecondFragment extends Fragment { import java.math.BigDecimal;
public class SecondFragment extends Fragment {
private FragmentSecondBinding binding; private FragmentSecondBinding binding;
private String[] codes = {};
private KtoraKwota ktoraKwotaBylaWpsiana = null;
private ExchangeRatesTable table;
private Rate selectedRate;
private enum KtoraKwota {WALUTA, PLN};
@Override @Override
public View onCreateView( public View onCreateView(
...@@ -29,10 +42,66 @@ public class SecondFragment extends Fragment { ...@@ -29,10 +42,66 @@ public class SecondFragment extends Fragment {
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
table = SingletonPobranaTabela.getInstance().getTable();
codes = table != null ? table.getCodes() : new String[0];
Spinner spinner = binding.spinnerWyborWalut;
ArrayAdapter<String> adapter = new ArrayAdapter<>(this.getContext(), android.R.layout.simple_spinner_item, codes);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
binding.buttonSecond.setOnClickListener(v -> binding.buttonSecond.setOnClickListener(v ->
NavHostFragment.findNavController(SecondFragment.this) NavHostFragment.findNavController(SecondFragment.this)
.navigate(R.id.action_SecondFragment_to_FirstFragment) .navigate(R.id.action_SecondFragment_to_FirstFragment)
); );
binding.spinnerWyborWalut.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
selectedRate = table.find(codes[position]);
odwiezWidok();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
selectedRate = null;
odwiezWidok();
}
});
// zdarzenia po wpisaniu liczby w górnym lub dolnym polu i zatwierdzeniu
// (na komputerze to "naciśnięcie enter")
binding.editTextKwota1.setOnClickListener(v -> {
ktoraKwotaBylaWpsiana = KtoraKwota.WALUTA;
odwiezWidok();
});
binding.editTextKwota2.setOnClickListener(v -> {
ktoraKwotaBylaWpsiana = KtoraKwota.PLN;
odwiezWidok();
});
}
private void odwiezWidok() {
binding.textViewCurrencyName.setText(selectedRate == null ? "" : selectedRate.getCurrency());
binding.labelKwota1.setText(selectedRate == null ? "Kwota" : ("Kwota " + selectedRate.getCode()));
if(selectedRate == null || ktoraKwotaBylaWpsiana == null) {
binding.editTextKwota1.setText("");
binding.editTextKwota2.setText("");
return;
}
switch (ktoraKwotaBylaWpsiana) {
case WALUTA: {
BigDecimal kwota1 = new BigDecimal(binding.editTextKwota1.getText().toString().replace(',', '.'));
BigDecimal kwota2 = selectedRate.przeliczNaZlote(kwota1);
binding.editTextKwota2.setText(String.valueOf(kwota2));
break;
}
case PLN: {
BigDecimal kwota2 = new BigDecimal(binding.editTextKwota2.getText().toString().replace(',', '.'));
BigDecimal kwota1 = selectedRate.przeliczNaWalute(kwota2);
binding.editTextKwota1.setText(String.valueOf(kwota1));
break;
}
}
} }
@Override @Override
......
package com.example.waluty;
/* Ta klasa działa jako singleton, tzn w pamięci jest utrzymywany pojedynczy obiekt tej klasy,
dostępny za pomocą getInstance. Dostęp do niego mają różne klasy aplikacji
- w tym przypadku FirstFragment i SenondFragment.
Rozwiązanie przypomina stosowanie zmiennych globalnych w innych językach programowania,
dlatego przez wiele osób jest uważane za niezbyt porządne.
Ale jego zaletą jest prostota i łatwość dostępu do wspólnych danych przez różne fragmenty aplikacji.
*/
public class SingletonPobranaTabela {
private static SingletonPobranaTabela instance;
private ExchangeRatesTable table;
public synchronized static SingletonPobranaTabela getInstance() {
if(instance == null) {
instance = new SingletonPobranaTabela();
}
return instance;
}
public synchronized ExchangeRatesTable getTable() {
return this.table;
};
public synchronized void setTable(ExchangeRatesTable table) {
this.table = table;
};
}
...@@ -16,20 +16,70 @@ ...@@ -16,20 +16,70 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/previous" android:text="@string/previous"
app:layout_constraintBottom_toTopOf="@id/textview_second"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<Spinner android:id="@+id/spinnerWyborWalut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="64dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_second"
/>
<TextView <TextView
android:id="@+id/textview_second" android:id="@+id/textViewCurrencyName"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:text=""
android:text="@string/lorem_ipsum" android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/spinnerWyborWalut"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBaseline_toBaselineOf="@+id/spinnerWyborWalut"
/>
<TextView
android:id="@+id/labelKwota1"
android:labelFor="@id/editTextKwota1"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginTop="64dp"
android:text="Kwota ___"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spinnerWyborWalut"
/>
<EditText
android:id="@+id/editTextKwota1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autofillHints="0"
android:ems="10"
android:inputType="numberDecimal"
app:layout_constraintStart_toEndOf="@id/labelKwota1"
app:layout_constraintBaseline_toBaselineOf="@+id/labelKwota1" />
<TextView
android:id="@+id/labelKwota2"
android:labelFor="@id/editTextKwota2"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Kwota PLN"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_second" /> app:layout_constraintTop_toBottomOf="@+id/labelKwota1"
/>
<EditText
android:id="@+id/editTextKwota2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autofillHints="0"
android:ems="10"
android:inputType="numberDecimal"
app:layout_constraintStart_toEndOf="@id/labelKwota2"
app:layout_constraintBaseline_toBaselineOf="@+id/labelKwota2" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment