[Flutter] Tìm hiểu về compute

[Flutter] Tìm hiểu về compute

compute là một hàm trong Flutter được sử dụng để thực thi các tác vụ tính toán nặng trên một isolate riêng biệt (tương tự như một thread riêng) nhằm tránh làm chậm (hay bị giật) giao diện người dùng.

1. Mục đích sử dụng compute

Khi bạn có một tác vụ tiêu tốn nhiều thời gian (ví dụ: phân tích dữ liệu JSON lớn, xử lý dữ liệu phức tạp,…) và có thể làm chậm giao diện người dùng nếu chạy trên luồng chính, compute giúp chuyển quá trình xử lý đó sang isolate riêng. Nhờ đó, UI không bị block và người dùng có thể duyệt ứng dụng một cách mượt mà.

2. Nguyên lý hoạt động

  • Isolate trong Dart: Dart sử dụng isolate để chạy các tác vụ song song. Mỗi isolate có bộ nhớ riêng và chạy độc lập, không chia sẻ dữ liệu trực tiếp với nhau.
  • Cách compute hoạt động: Hàm compute nhận vào một hàm callback và dữ liệu truyền vào. Sau đó, nó:
    • Khởi tạo một isolate mới.
    • Gửi dữ liệu cần xử lý và chạy hàm callback đó trên isolate mới.
    • Nhận kết quả từ isolate và trả về dưới dạng Future.

Ví dụ sử dụng với phân tích JSON:

// Hàm phân tích dữ liệu trên isolate riêng biệt
List<Photo> parsePhotos(String responseBody) {
  final List<dynamic> parsed = jsonDecode(responseBody);
  return parsed.map((json) => Photo.fromJson(json)).toList();
}

// Hàm lấy dữ liệu và sử dụng compute để phân tích JSON
Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response = await client.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
  
  // Chuyển quá trình phân tích JSON sang isolate riêng
  return compute(parsePhotos, response.body);
}
Dart

Trong đoạn code trên, parsePhotos được thực thi trong isolate được tạo bởi compute. Điều này giúp giữ cho thread chính chủ động xử lý giao diện, tránh gây lag khi phân tích một lượng lớn dữ liệu JSON.

3. Lưu ý khi sử dụng compute

  • Pure Function: Hàm callback truyền vào compute phải là hàm thuần (pure function), không phụ thuộc vào biến bên ngoài hay trạng thái bên ngoài. Nó chỉ xử lý dữ liệu đầu vào và trả về kết quả.
  • Dữ liệu truyền qua isolate: Dữ liệu truyền qua lại giữa main isolate và isolate phụ phải được serialize (thường là những dữ liệu có thể serialize như số, chuỗi, danh sách, bản đồ,…). Các đối tượng phức tạp hoặc đối tượng không thể serialize được sẽ gây lỗi.
  • Chi phí tạo isolate: Tạo một isolate có chi phí nhất định, do đó nên sử dụng compute cho các tác vụ thực sự nặng và không lặp đi lặp lại quá nhiều.

4. Khi nào nên sử dụng compute

  • Khi bạn xử lý dữ liệu từ network mà cần phân tích dữ liệu phức tạp hoặc có lượng dữ liệu lớn.
  • Khi bạn muốn xử lý bài toán tính toán nặng mà không ảnh hưởng đến responsive của giao diện người dùng.
  • Khi chuyển đổi dữ liệu từ server sang định dạng phù hợp để hiển thị trong UI.

5. Ví dụ

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

List<Photo> parsePhotos(String responseBody) {
  try {
    final List<dynamic> parsed = jsonDecode(responseBody);
    return parsed
      .where((json) => json is Map<String, dynamic>)
      .map<Photo>((json) => Photo.fromJson(json))
      .toList();
  } catch (e) {
    throw Exception('Error parsing photos: ${e.toString()}');
  }
}

Future<List<Photo>> fetchPhotos() async {
  try {
    final response = await http.get(
      Uri.parse("https://jsonplaceholder.typicode.com/photos")
    ).timeout(const Duration(seconds: 10));

    if (response.statusCode == 200) {
      return compute(parsePhotos, response.body);
    }
    throw Exception('Failed to load photos: ${response.statusCode}');
  } catch(e) {
    throw Exception('Failed parse album: ${e.toString()}');
  }
}

class Photo {
  final int albumId;
  final int id;
  final String title;
  final String url;
  final String thumbnailUrl;

  const Photo({required this.albumId, required this.id, required this.title, required this.url, required this.thumbnailUrl});

  factory Photo.fromJson(Map<String, dynamic> json) {
    try {
      return Photo(
          id: int.parse(json['id'].toString()),
          albumId: int.parse(json['albumId'].toString()),
          title: json['title'] as String,
          url: json['url'] as String,
          thumbnailUrl: json['thumbnailUrl'] as String,
      );
    } catch (e) {
      // TODO
      throw Exception('Failed to parse album: ${e.toString()}');
    }
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
  final int _pageSize = 100; // Số ảnh mỗi trang
  int _currentPage = 0; // Trang hiện tại
  List<Photo> _allPhotos = []; // Danh sách ảnh đã tải
  bool _isLoading = false; // Đang tải
  bool _hasMore = true; // Còn ảnh để tải
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _loadMorePhotos();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels >=
          _scrollController.position.maxScrollExtent - 200 &&
          !_isLoading &&
          _hasMore) {
        _loadMorePhotos();
      }
    });
  }

  Future<void> _loadMorePhotos() async {
    if (_isLoading || !_hasMore) return;
    setState(() => _isLoading = true);

    try {
      final response = await http
          .get(Uri.parse('https://jsonplaceholder.typicode.com/photos'))
          .timeout(const Duration(seconds: 10));

      if (response.statusCode == 200) {
        final List<Photo> newPhotos = await compute(parsePhotos, response.body);
        setState(() {
          final startIndex = _currentPage * _pageSize;
          final endIndex = startIndex + _pageSize;
          if (startIndex >= newPhotos.length) {
            _hasMore = false;
          } else {
            _allPhotos.addAll(
                newPhotos.sublist(startIndex, endIndex.clamp(0, newPhotos.length)));
            _currentPage++;
            _hasMore = endIndex < newPhotos.length;
          }
          _isLoading = false;
        });
      } else {
        throw Exception('Không tải được ảnh');
      }
    } catch (e) {
      setState(() => _isLoading = false);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Lỗi: $e')),
      );
    }
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Thư viện ảnh')),
      body: _allPhotos.isEmpty && _isLoading
          ? const Center(child: CircularProgressIndicator())
          : GridView.builder(
        controller: _scrollController,
        padding: const EdgeInsets.all(8.0),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 8.0,
          mainAxisSpacing: 8.0,
          childAspectRatio: 0.8,
        ),
        itemCount: _allPhotos.length + (_hasMore ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == _allPhotos.length && _hasMore) {
            return const Center(child: CircularProgressIndicator());
          }
          return Card(
            elevation: 2,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Expanded(
                  child: Image.network(
                    _allPhotos[index].thumbnailUrl,
                    fit: BoxFit.cover,
                    loadingBuilder: (context, child, loadingProgress) {
                      if (loadingProgress == null) return child;
                      return const Center(child: CircularProgressIndicator());
                    },
                    errorBuilder: (context, error, stackTrace) {
                      return const Icon(Icons.broken_image, size: 50, color: Colors.grey);
                    },
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Text(
                    _allPhotos[index].title,
                    textAlign: TextAlign.center,
                    maxLines: 5,
                    overflow: TextOverflow.ellipsis,
                    style: const TextStyle(fontSize: 12),
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}
class PhotoGrid extends StatelessWidget {
  final List<Photo> photos;

  const PhotoGrid({required this.photos, super.key});

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
        itemCount: photos.length,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
          crossAxisSpacing: 8.0,
          mainAxisSpacing: 8.0,
          childAspectRatio: 0.8,
        ),
        itemBuilder: (context, index) {
          return Card(
            elevation: 1,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Image.network(
                  photos[index].thumbnailUrl,
                  fit: BoxFit.cover,
                  loadingBuilder: (context, child, loadingProgress) {
                    if (loadingProgress == null) return child;
                    return const Center(
                      child: CircularProgressIndicator(),
                    );
                  },
                  errorBuilder: (context, error, stackTrace) {
                    return Image.network(
                      'https://thumb.ac-illust.com/b1/b170870007dfa419295d949814474ab2_t.jpeg',
                      fit: BoxFit.cover,
                      loadingBuilder: (context, child, loadingProgress) {
                        if (loadingProgress == null) return child;
                        return const Center(
                          child: CircularProgressIndicator(),
                        );
                      },
                    );
                  },
                ),
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Text(
                    photos[index].title,
                    textAlign: TextAlign.start,
                    maxLines: 3,
                    overflow: TextOverflow.ellipsis,
                    style: const TextStyle(fontSize: 12),
                  ),
                )
              ],
            ),
          );
        }
    );
  }
}


void main() {
  runApp(MaterialApp(
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Fetch Photos from internet'),
      ),
      body: const HomeScreen(),
    ),
  ));
}
Dart

6. Kết luận

Như vậy, compute là một công cụ mạnh mẽ giúp bạn tối ưu hóa trải nghiệm người dùng bằng cách chuyển các tác vụ nặng sang chạy riêng, tránh làm gián đoạn luồng chính của ứng dụng Flutter. Nếu bạn có thắc mắc thêm về cách sử dụng hoặc cần ví dụ cụ thể khác, hãy cho tôi biết nhé!

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *