Całościowe podejście do projektowania aplikacji mobilnych

0

Jako że jestem zupełnie początkującym programistą ale z 30+ letnim doświadczeniem w IT, mam pytanie do osób, które już znają Fluttera.

Chciałbym napisać bazodanową aplikacje mobilną we Flutterze, która korzystała by z REST API.
Aplikacja zbudowana była by z kilku ekranów, wymagała by logowania przez REST API (skrypty php po stronie serwera na już działającej stronie), wyświetlała by informacje z bazy, dodawała nowe wpisy i edytowała istniejące... zupełnie standardowe rozwiązanie.

Mam bardzo wyrywkową wiedzę zaczerpniętą z https://docs.flutter.dev i YT na temat korzystania z REST API i brak mi wiedzy ogólnej na temat całościowego podejścia do problemu.

Czy mógłby mi ktoś najpierw pomóc zidentyfikować technologie jakich będę musiał użyć w aplikacji? Jak już będę wiedział czego szukać w szczegółach to jakoś znajdę w google ;)

Może ja zacznę od problemów/pytań jakie mam:

  1. Jak wygląda proces uwierzytelniania w UI/Rest API: wiem jak wywołać skrypt PHP i jak pobrać z niego dane, ale nie rozumiem ogólnego procesu tzn. jakie dane wysłać(login/password)/pobarać(sucess???, token??? itd.) ze API...
  2. Jaki ogarnąć problem kontroli sesji PHP w UI (shared_preferences???)
  3. Jakie są typowe techniki programistyczne w tego typu aplikacjach?

Z góry serdecznie dziękuję za pomoc.

0
  1. Najprościej to wykorzystać token JWT. Na backend wysyłasz login i hasło, autoryzujesz i generujesz token JWT(albo zwracasz błąd). Potem ten token przesyłasz w nagłówku i na backendzie sprawdzasz czy token jest poprawny.
  2. REST API jest bezstanowy, więc nie ma czegoś takiego jak sesja. Nie trzymasz danych sesji usera na backendzie.
0

OK... rozumiem... a czy jest jakaś biblioteka JWT czy kod implementujemy sami?

Tak się zastanawiam... jak długo w jest ważny token JWT, bo patrząc od strony UI, to chciałbym żeby po zalogowaniu UI mogło wykonywać zapytania do API aż do momentu wylogowania (co może trwać np. miesiąc lub więcej). Czy w takiej sytuacji ustawia się np. długi czas ważności tokena czy tworzy się mechanizm jego odświeżenia?

2

@Michalk001 coś pomieszałeś. Nie ma przeciwwskazań, żeby przesyłać cookie / sesję zamiast przesyłać tokeny w nagłówkach. Klient rest to taki sam klient http jak przeglądarka, tyle że mamy jsony zamiast html-a. Często się tak robi, po bo po się kopać z jakimiś tokenami w nagłówkach, gdy klient http sam będzie zarządzać sesją i przesyłać cookie z na serwer. Cookie może być też zapisywane w pamięci urządzenia, automatycznie (tak samo jak to działa w przeglądarce). Tokeny musisz sam zapisywać w shared preferences i potem ładować do nagłówków.

Co do tematu (klient we Flutterze), zalecałbym taki zestaw:

Jak to wszystko poskładać masz np tu: https://medium.com/mindful-engineering/retrofit-the-easiest-way-to-call-rest-apis-is-flutter-fe55d1e7c5c2 albo zajrzyj do dokumentacji Retrofit. Szczerze ci radzę użyć Retrofit, bo ręczne skrobanie tego, zwłaszcza jak masz jakieś bardziej skomplikowane rzeczy jak przesyłanie formularzy, plików, query, itd jest dość upierdliwe. Podobnie z modelami do serializacji.

Zarządzanie sesją przez cookie wygląda tak, że najpierw klient wywołuje jakiegoś POST-a, w którym przesyła swoje poświadczenia (user/password np). Dalej standardowo, serwer tworzy sesję i przesyłą cookie. Tego cookie używasz w kliencie http (cookie manager), wszystko się dzieje automatycznie. Gdy cookie zostanie unieważnione (unieważniona sesja), to autoryzowne requesty powinny zwracać 403, wtedy musisz w kliencie utworzyć nową sesję (wywołać odpowiedni POST i dostać nowe cookie)

0

no pięknie... a ja już wygenerowałem token na backendzie (https://github.com/firebase/php-jwt) i zacząłem robić frontend.... może jeszcze poczekam... ;)
co do cookies, czytałem o cookie_manager'ze i wygląda to fajnie, ale gdzieś na forum widziałem, że ktoś pisał że SESSID mu się bardzo często zmienia :(

0

Jeśli masz token, to musisz go sam wrzucać w shared preferences, a żeby nie trzeba było go doklejać do każdego requestu, wrzuć go w interceptor w kliencie http to będzie automatycznie dodawany. Tyle że musisz sam tym zarządzać, zapisywać do shared preferences itd.

Tu masz pokazane jak to zrobić na adnotacjach albo z interceptorem: https://medium.com/flutter-community/hocus-pocus-painless-headers-customization-of-rest-api-requests-in-flutter-5ee9c1a2d9f8

0
  1. to zależy od tego z jakim serwisem gadasz (nie mamy oczywiście wglądu w to co nazywasz, dość zabawnie, skryptami PHP i co one tam potrafią i wymagają)
  2. to też zależy, każde podejście ma swoje cechy szczególne (można trzymać w SP, ale to od razu komplikuje sprawy, można trzymać w RAM, można nie trzymać nigdzie)
  3. nie ma czegoś takiego

Czy odpowiedziałeś sobie na pytanie czemu w ogóle rozważasz jak się domyślam kolejną aplikację (we Flutterze czy czymkolwiek innym)? Skoro tam jest już jakiś BE to pewnie jest też jakoś FE, czemu nie użyć tego zamiast wytwarzać otwarte drzwi?

0

OK... poniżej załączam trochę moich wypocin z tutoriali z googla :)
Nie mogę sobie poradzić z przejściem ze screena LoginPage do HomePage (po poprawnym zalogowaniu).

Próbowałem: Navigator.of(context).push(MaterialPageRoute(builder: (context) => HomePage()));
ale dostałem błąd: "type 'Future' is not a subtype of type '(() => dynamic)?'"

Czy moglibyście na to zerknąć, proszę i doradzić co może być nie tak?

Dzięki

Poniżej kod LoginPage:

class LoginPage extends StatelessWidget {
  LoginPage({super.key});

  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  void signUserIn() async  {

    try{
      Response response = await post(
          Uri.parse('https://www.somepage.com/login.php'),
          body: {
            'email' : emailController.text,
            'password' : passwordController.text
          }
      );

      if(response.statusCode == 200){
        var data = jsonDecode(response.body.toString());
        if(data['success'] == 1) {
          print(data['message']);
          
          SharedPreferences prefs = await SharedPreferences.getInstance();
          prefs.setString('email', emailController.text);
          prefs.setString('token', data['token']);



          // ---> tutaj chciałbym przejść do HomePage 

          


        } else {
          print(data['status']);
          print(data['message']);
        }
      }else {
        print('Login failed...');
        print(response.statusCode);
      }
    }catch(e){
      print(e.toString());
    }

  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[300],
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const SizedBox(height: 25),

                const Icon(
                  Icons.lock,
                  size: 100,
                ),

                const SizedBox(height: 25),

                Text(
                  'Witamy ponownie!',
                  style: TextStyle(
                    color: Colors.grey[700],
                    fontSize: 16,
                  ),
                ),

                const SizedBox(height: 25),

                MyTextField(
                  controller: emailController,
                  hintText: 'Email',
                  obscureText: false,
                ),

                const SizedBox(height: 10),

                MyTextField(
                  controller: passwordController,
                  hintText: 'Hasło',
                  obscureText: true,
                ),

                const SizedBox(height: 10),

                const Padding(
                  padding: EdgeInsets.symmetric(horizontal: 25.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: [
                      Text(
                        'Nie pamiętasz hasła?',
                        style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                ),

                const SizedBox(height: 20),

                MyButton(
                  onTap: signUserIn,
                ),

                const SizedBox(height: 30),

                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 25.0),
                  child: Row(
                    children: [
                      Expanded(
                        child: Divider(
                          thickness: 0.5,
                          color: Colors.grey[400],
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 25.0),
                        child: Text(
                          'Albo kontynuuj z',
                          style: TextStyle(color: Colors.grey[700]),
                        ),
                      ),
                      Expanded(
                        child: Divider(
                          thickness: 0.5,
                          color: Colors.grey[400],
                        ),
                      ),
                    ],
                  ),
                ),

                const SizedBox(height: 30),

                const Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    SquareTile(imagePath: 'lib/images/google.png'),  
                    SizedBox(width: 10),
                    SquareTile(imagePath: 'lib/images/apple.png')
                  ],
                ),

                const SizedBox(height: 30),

                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(
                      'Nie masz konta?',
                      style: TextStyle(color: Colors.grey[700]),
                    ),
                    const SizedBox(width: 4),
                    const Text(
                      'Zarejestruj się',
                      style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
                    ),
                  ],
                )
              ],
          ),
        ),
      ),
    ),
  );
 }
}

i kod HomePage:

class HomePage extends StatelessWidget {
  HomePage({super.key});

  void signUserOut() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.remove('email');
    prefs.remove('token');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[300],
      appBar: AppBar(
        backgroundColor: Colors.grey[900],
        actions: [
          IconButton(
            onPressed: signUserOut,
            icon: Icon(Icons.logout),
          )
        ],
      ),
      body: Center(
          child: Text(
        "TEST",
        style: TextStyle(fontSize: 20),
      )),
    );
  }
}
0

OK... trochę poczytałem i coś z tego zaczyna wychodzić. Mam jednak jeden problem. Nie potrafię zmapować struktur danych do itemów w DropdownButtonFormField.
Czy mógłby ktoś pomóc mi z tym bo utknąłem...

Struktura danych wygląda tak:

class PostListOfDiets {
  String? nazwa;
  String? opis;
  String? komentarz;
  String? dietetyk;
  int? id;

  PostListOfDiets({this.nazwa, this.opis, this.komentarz, this.dietetyk, this.id});

  PostListOfDiets.fromJson(Map<String, dynamic> json) {
    nazwa = json['nazwa'];
    opis = json['opis'];
    komentarz = json['komentarz'];
    dietetyk = json['dietetyk'];
    id = json['id'];
  }
}

a menu tworzę w ten sposób:

            child: DropdownButtonFormField(
                  hint: const Text("Wybierz jadłospis"),
                  
                  // >>> tu jest część której nie rozumiem
                  items: posts.map<DropdownMenuItem<String>>((String value) {
                  return DropdownMenuItem<String>(
                    value: value,
                    child: Text(value),
                  );
                  }).toList(),
                  //<<<tu jest część której nie rozumiem
                  
                  onChanged: (val) {
                    setState(() {
                      _selectedVal = val as String;
                    });
                  },
                  icon: const Icon(
                    Icons.arrow_drop_down_circle,
                    color: Colors.blue,
                  ),
                  dropdownColor: Colors.grey[200],
                  decoration: const InputDecoration(
                    labelText: "Nazwa jadłospisu",
                    labelStyle: TextStyle(color: Colors.blue, fontSize: 16.0),
                    prefixIcon: Icon(
                      Icons.list_alt,
                      color: Colors.blue,
                    ),
                  ),
                )

Dostaje błąd: Error: The argument type 'DropdownMenuItem<String> Function(String)' can't be assigned to the parameter type 'DropdownMenuItem<String> Function(PostListOfDiets)'.

Czy mogę prosić o pomoc... ?

0

kombinowałem i wykombinowałem coś takiego...

items: posts.map((item) {
                    return DropdownMenuItem(
                      value: item.id.toString(),
                      child: Text(item.nazwa.toString()),
                    );
                  }).toList(),

Wygląda na banalnie proste... ale czy dobrze kombinuje...?

1 użytkowników online, w tym zalogowanych: 0, gości: 1