FreeShare

Lập trình app đa nền tảng với flutter

Chủ nhật, 18/02/2024
image
1. Flutter là gì? Flutter là mobile UI framework của Google để tạo ra các giao diện chất lượng cao trên iOS và Android trong khoảng thời gian ngắn. Flutter hoạt động với những code sẵn có được sử dụng bởi các lập trình viên, các tổ chức. Flutter hoàn toàn miễn phí và cũng […]

1. Flutter là gì?

  • Flutter là mobile UI framework của Google để tạo ra các giao diện chất lượng cao trên iOS và Android trong khoảng thời gian ngắn. Flutter hoạt động với những code sẵn có được sử dụng bởi các lập trình viên, các tổ chức.
  • Flutter hoàn toàn miễn phí và cũng là mã nguồn mở.

2. Tại sao lại dùng Flutter?

  • Nếu bạn đang tìm kiếm các phương pháp thay thế để phát triển ứng dụng Android, bạn nên cân nhắc thử Flutter của Google, một framework dựa trên ngôn ngữ lập trình Dart.
  • Các ứng dụng được xây dựng với Flutter hầu như không thể phân biệt với những ứng dụng được xây dựng bằng cách sử dụng Android SDK, cả về giao diện và hiệu suất. Hơn nữa, với những tinh chỉnh nhỏ, chúng có thể chạy trên thiết bị iOS.

3. Tài liệu

bạn có thể tham khảo hướng dẫn tại đây

4 xây dựng ứng dụng đầu tiên

các bước cài đặt bạn có thể làm theo docs , mình không hướng dẫn lại tại đây (hihi)

Tạo dự án với các file mẫu có cấu trúc như sau

File api/api_list.dart cấu hình đường dẫn các đường dẫn api vào đây

class APIS {
  static final String _baseUrl = "https://dummyjson.com";
  static var postList = "$_baseUrl/products";
}

File components/bottomNav.dat , file này để làm cái menu dưới chân app

import 'package:flutter/material.dart';
import 'package:helloworld/screens/profileScreen.dart';
import 'package:helloworld/screens/homeScreen.dart';

class BottomNav extends StatefulWidget {
  const BottomNav({Key? key}) : super(key: key);
  @override
  State createState() => _BottomNav();
}
int _selectedIndex = 0;
class _BottomNav extends State {
  @override
  Widget build(BuildContext context) {
    void _onItemTapped(int index) {
      setState(() {
        _selectedIndex = index;
      });
      switch (_selectedIndex) {
        case 0:
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => HomeScreen()),
          );
          break;
        case 1:
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => ProfileScreen()),
          );
          break;
      }
    }
    return BottomNavigationBar(
      currentIndex: _selectedIndex, // this will be set when a new tab is tapped
      onTap: _onItemTapped,
      selectedItemColor: Colors.amber[800],
      unselectedItemColor: Colors.grey,
      items: [
        BottomNavigationBarItem(
          icon: new Icon(Icons.home),
          label: 'Home',
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.person),
          label: 'Profile',
        )
      ],
    );
  }
}

File components/reusbaleRow.dat file này để định dạng lại các dữ liệu chi tiết

import 'package:flutter/material.dart';

class ReusbaleRow extends StatelessWidget {
  String title , value ;
  ReusbaleRow({Key? key , required this.title , required this.value}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(title),
          Text(value ),
        ],
      ),
    );
  }
}

File models/article.dart để định nghĩa cấu trúc dữ liệu api trả về

class Article {
  final int id;
  final String title;
  final String description;
  final double price;
  final double discountPercentage;
  final double rating;
  final int stock;
  final String brand;
  final String category;
  final String thumbnail;

  Article(
      this.id,
      this.title,
      this.description,
      this.price,
      this.discountPercentage,
      this.rating,
      this.stock,
      this.brand,
      this.category,
      this.thumbnail
  );

  Article.fromJsonMap(Map map):
      id = map["id"],
      title = map["title"],
      description = map["description"],
      price = map["price"].toDouble(),
      discountPercentage = map["discountPercentage"].toDouble(),
      rating = map["rating"].toDouble(),
      stock = map["stock"],
      brand = map["brand"],
      category = map["category"],
      thumbnail = map["thumbnail"];

  Map toJson() {
    final Map data = new Map();
    data['id'] = id;
    data['title'] = title;
    data['description'] = description;
    data['price'] = price;
    data['discountPercentage'] = discountPercentage;
    data['rating'] = rating;
    data['stock'] = stock;
    data['brand'] = brand;
    data['category'] = category;
    data['thumbnail'] = thumbnail;
    return data;
  }
}

File screens/homeScreen.dart là dành cho màn hình trang chủ

import 'package:flutter/material.dart';
import 'package:helloworld/screens/detailPostScreen.dart';
import 'package:helloworld/models/article.dart';
import 'package:helloworld/components/bottomNav.dart';
import 'package:http/http.dart' as http;
import 'package:helloworld/api/api_list.dart';
import 'dart:async';
import 'dart:convert';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State createState() => _HomeScreen();
}

class _HomeScreen extends State {

  List _data = [];

  Future getPosts() async {
    final response = await http.get(Uri.parse(APIS.postList), headers: {"Accept": "application/json"});
    var res = jsonDecode(response.body.toString());
    final products = res['products'];
    if(response.statusCode == 200) {
      setState(() {
        _data = products.map((data) => Article.fromJsonMap(data)).toList();
      });
      return "success";
    }
    return "failed";
  }

  @override
  void initState() {
    super.initState();
    getPosts();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home'),
        automaticallyImplyLeading: false
      ),
      body: ListView.builder(
        itemCount: _data == null ? 0 : _data.length,
        itemBuilder: (context, index) {
          return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
            child: ListTile( //use ListTile to show a Todo
              leading: Image.network(
                  _data[index].thumbnail.toString(),
                  width: 120.0,
                  fit: BoxFit.cover
              ),
              title: Text(_data[index].title),
              subtitle: Text(_data[index].description),
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) => DetailPostScreen(detail: _data[index]),
                      settings: RouteSettings(
                          arguments: _data[index].id
                      )
                  ),
                );
              },
            )
          );
        },
      ),
      bottomNavigationBar: BottomNav(),
    );
  }
}

File screens/detailPostScreen.dart là để dành cho màn chi tiết bài viết

import 'package:flutter/material.dart';
import 'package:helloworld/models/article.dart';
import 'package:helloworld/api/api_list.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';
import 'package:helloworld/components/reusbaleRow.dart';

class DetailPostScreen extends StatefulWidget {
  final Article detail;
  DetailPostScreen({super.key, required this.detail});

  @override
  State createState() => _DetailPostScreen(detail);
}

class _DetailPostScreen extends State {
  // Declare a field that holds the Todo

   Article detail;
  _DetailPostScreen(this.detail);

  Future getDetailPosts() async {
    final response = await http.get(Uri.parse(APIS.postList + '/' + detail.id.toString()), headers: {"Accept": "application/json"});
    var res = jsonDecode(response.body.toString());
    if(response.statusCode == 200) {
      setState(() {
        detail = Article(
            res['id'],
            res['title'],
            res['description'],
            res['price'].toDouble(),
            res['discountPercentage'].toDouble(),
            res['rating'].toDouble(),
            res['stock'],
            res['brand'],
            res['category'],
            res['thumbnail']
        );
      });
      return "success";
    }
    return "failed";
  }


  @override
  void initState() {
    super.initState();
    getDetailPosts();
  }

  @override
  Widget build(BuildContext context) {
    // Use the Todo to create our UI
    return Scaffold(
      appBar: AppBar(
        title: Text("${detail.title}"),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Column(
          children: [
            Image.network(
                detail.thumbnail.toString(),
                fit: BoxFit.cover
            ),
            Padding(
                padding: EdgeInsets.symmetric(horizontal: 0, vertical: 15),
                child: Text(
                    '${detail.description}',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                )
            ),
            ReusbaleRow(title: 'Price', value: detail.price.toString() + " dolar"),
            ReusbaleRow(title: 'DiscountPercentage', value: detail.discountPercentage.toString() + ' %'),
            ReusbaleRow(title: 'Rating', value: detail.rating.toString() + ' star'),
            ReusbaleRow(title: 'Stock', value: detail.stock.toString()),
            ReusbaleRow(title: 'Brand', value: detail.brand.toString()),
            ReusbaleRow(title: 'Category', value: detail.category.toString())
          ],
        ),
      ),
    );
  }
}

File screens/profileScreen.dart để làm chức năng đăng nhập và nếu đăng nhập thì hiển thị thông tin của user

import 'package:flutter/material.dart';
import 'package:helloworld/components/bottomNav.dart';
import 'dart:convert';
import 'package:http/http.dart';
import 'package:localstorage/localstorage.dart';
import 'package:helloworld/components/reusbaleRow.dart';

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({Key ? key}) : super(key: key);
  @override
  State createState() => _ProfileScreen();
}

class _ProfileScreen extends State {

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

  bool isLogin = false;

  void login(String email , password) async {
    try{
      print("email: " + email);
      print("password: "+password);
      Response response = await post(
          Uri.parse('https://reqres.in/api/login'),
          body: {
            'email' : email,
            'password' : password
          }
      );

      if(response.statusCode == 200){
        var data = jsonDecode(response.body.toString());
        print("token: " + data['token']);
        addItemsToLocalStorage(email, data['token']);
        print('Login successfully');
        setState(() {
          isLogin = true;
        });
      }else {
        print('failed');
      }
    }catch(e){
      print(e.toString());
    }
  }

  final LocalStorage storage = new LocalStorage('localstorage_app');
  void addItemsToLocalStorage(email, token) {
    final info = json.encode({'email': email, 'token': token});
    storage.setItem('info', info);
  }

  dynamic getItemsInLocalStorage() {
    return storage.getItem('info');
  }

  void logout(){
    storage.deleteItem('info');
    setState(() {
      isLogin = false;
    });
  }

  @override
  void initState() {
    super.initState();
    final info = getItemsInLocalStorage();
    if(info != null) setState(() {
      isLogin = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Profile'),
        automaticallyImplyLeading: false
      ),
      body: isLogin == false ? Center(
        child: Container(
          padding: const EdgeInsets.all(80.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                'Login',
                style: Theme.of(context).textTheme.headline3,
              ),
              TextFormField(
                controller: emailController,
                decoration: const InputDecoration(
                  hintText: 'Email',
                ),
              ),
              SizedBox(height: 20,),
              TextFormField(
                controller: passwordController,
                decoration: const InputDecoration(
                  hintText: 'Password',
                ),
                obscureText: true,
              ),
              const SizedBox(
                height: 20,
              ),
              ElevatedButton(
                onPressed: () {
                  login(emailController.text.toString(), passwordController.text.toString());
                },
                style: ElevatedButton.styleFrom(
                  primary: Colors.yellow,
                ),
                child: const Text('LOGIN'),
              )
            ],
          ),
        ),
      ) : Center(
        child: Container(
          padding: const EdgeInsets.all(80.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ReusbaleRow(title: 'Email', value: '111'),
              const SizedBox(
                height: 20,
              ),
              ElevatedButton(
                onPressed: () {
                  logout();
                },
                style: ElevatedButton.styleFrom(
                  primary: Colors.yellow,
                ),
                child: const Text('LOGOUT'),
              )
            ],
          ),
        ),
      ),
      bottomNavigationBar: BottomNav(),
    );
  }
}

Và file main.dart sẽ có nội dung như sau

import 'package:flutter/material.dart';
import 'package:helloworld/screens/profileScreen.dart';
import 'package:helloworld/screens/homeScreen.dart';
import 'dart:developer';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {

  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    log('Load success');
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => const HomeScreen(),
        '/login': (context) => const ProfileScreen(),
      }
    );
  }
}

Dùng IDE inteliJ chạy file main.dart lên với máy ảo iphone

Tham khảo code mẫu tại đây