Асинхронне програмування у Flutter
Асинхронне програмування є ключовим аспектом розробки Flutter-застосунків. Dart надає потужні інструменти для роботи з асинхронними операціями: Future, async/await та Stream.
Future та async/await
Основи Future
Future представляє результат асинхронної операції, який буде доступний пізніше.
// Створення Future
Future<String> fetchUserData() {
return Future.delayed(
Duration(seconds: 2),
() => 'Дані користувача',
);
}
// Використання then
void main() {
print('Початок');
fetchUserData().then((data) {
print(data);
}).catchError((error) {
print('Помилка: $error');
}).whenComplete(() {
print('Завершено');
});
print('Кінець');
}
// Виведе: Початок, Кінець, Дані користувача, Завершеноasync/await
async/await робить асинхронний код більш читабельним.
Future<void> loadData() async {
try {
print('Завантаження...');
String data = await fetchUserData();
print('Отримано: $data');
} catch (e) {
print('Помилка: $e');
} finally {
print('Завершено');
}
}
// Кілька послідовних запитів
Future<void> loadMultipleData() async {
try {
final user = await fetchUser();
final posts = await fetchUserPosts(user.id);
final comments = await fetchPostComments(posts.first.id);
print('Користувач: ${user.name}');
print('Постів: ${posts.length}');
print('Коментарів: ${comments.length}');
} catch (e) {
print('Помилка: $e');
}
}Паралельне виконання
// Future.wait - чекає завершення всіх Future
Future<void> loadAllData() async {
try {
final results = await Future.wait([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
final users = results[0] as List<User>;
final posts = results[1] as List<Post>;
final comments = results[2] as List<Comment>;
print('Завантажено: ${users.length} користувачів, ${posts.length} постів');
} catch (e) {
print('Помилка: $e');
}
}
// Future.any - повертає результат першого завершеного Future
Future<String> getFirstAvailableData() async {
return await Future.any([
fetchFromServer1(),
fetchFromServer2(),
fetchFromServer3(),
]);
}Обробка помилок
// Обробка специфічних помилок
Future<void> handleErrors() async {
try {
await fetchData();
} on SocketException {
print('Немає з\'єднання з інтернетом');
} on HttpException {
print('Помилка HTTP');
} on FormatException {
print('Невірний формат даних');
} catch (e) {
print('Невідома помилка: $e');
}
}
// Власний клас помилки
class ApiException implements Exception {
final String message;
final int statusCode;
ApiException(this.message, this.statusCode);
@override
String toString() => 'ApiException: $message (код: $statusCode)';
}
Future<User> fetchUser() async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
if (response.statusCode == 200) {
return User.fromJson(jsonDecode(response.body));
} else {
throw ApiException('Не вдалося отримати користувача', response.statusCode);
}
}FutureBuilder
FutureBuilder — віджет для побудови UI на основі результату Future.
class UserProfileScreen extends StatefulWidget {
@override
_UserProfileScreenState createState() => _UserProfileScreenState();
}
class _UserProfileScreenState extends State<UserProfileScreen> {
late Future<User> _userFuture;
@override
void initState() {
super.initState();
_userFuture = fetchUser();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Профіль')),
body: FutureBuilder<User>(
future: _userFuture,
builder: (context, snapshot) {
// Стан завантаження
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
// Помилка
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('Помилка: ${snapshot.error}'),
ElevatedButton(
onPressed: () {
setState(() {
_userFuture = fetchUser();
});
},
child: Text('Спробувати ще'),
),
],
),
);
}
// Успішний результат
if (snapshot.hasData) {
final user = snapshot.data!;
return ListView(
padding: EdgeInsets.all(16),
children: [
CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(user.avatarUrl),
),
SizedBox(height: 16),
Text(user.name, style: Theme.of(context).textTheme.headlineSmall),
Text(user.email),
],
);
}
return Center(child: Text('Немає даних'));
},
),
);
}
}Streams
Основи Stream
Stream — це послідовність асинхронних подій.
// Створення Stream
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
// Використання Stream
void main() async {
await for (final count in countStream(5)) {
print(count);
}
print('Готово!');
}StreamController
class CounterService {
final _counterController = StreamController<int>.broadcast();
int _count = 0;
Stream<int> get counterStream => _counterController.stream;
int get currentCount => _count;
void increment() {
_count++;
_counterController.add(_count);
}
void decrement() {
_count--;
_counterController.add(_count);
}
void dispose() {
_counterController.close();
}
}
// Використання
final counterService = CounterService();
counterService.counterStream.listen(
(count) => print('Лічильник: $count'),
onError: (error) => print('Помилка: $error'),
onDone: () => print('Потік закрито'),
);
counterService.increment(); // Виведе: Лічильник: 1
counterService.increment(); // Виведе: Лічильник: 2Трансформація Stream
// map - перетворення елементів
Stream<String> get formattedStream {
return counterStream.map((count) => 'Значення: $count');
}
// where - фільтрація
Stream<int> get evenNumbersStream {
return counterStream.where((count) => count % 2 == 0);
}
// transform - складне перетворення
final transformer = StreamTransformer<int, String>.fromHandlers(
handleData: (data, sink) {
if (data > 0) {
sink.add('Позитивне: $data');
} else {
sink.add('Негативне: $data');
}
},
);
Stream<String> get transformedStream {
return counterStream.transform(transformer);
}
// asyncMap - асинхронне перетворення
Stream<UserDetails> get userDetailsStream {
return userIdStream.asyncMap((id) => fetchUserDetails(id));
}
// debounce (з rxdart)
Stream<String> get debouncedSearchStream {
return searchStream
.debounceTime(Duration(milliseconds: 300))
.distinct();
}StreamBuilder
class ChatScreen extends StatelessWidget {
final Stream<List<Message>> messagesStream;
const ChatScreen({required this.messagesStream});
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Message>>(
stream: messagesStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Помилка: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('Немає повідомлень'));
}
final messages = snapshot.data!;
return ListView.builder(
reverse: true,
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
return MessageBubble(message: message);
},
);
},
);
}
}Isolates
Isolate дозволяє виконувати важкі обчислення у окремому потоці.
compute()
// Функція для виконання в окремому isolate
List<int> heavyComputation(int count) {
final result = <int>[];
for (int i = 0; i < count; i++) {
result.add(i * i);
}
return result;
}
// Використання compute
Future<void> runHeavyTask() async {
final result = await compute(heavyComputation, 1000000);
print('Обчислено ${result.length} елементів');
}Власний Isolate
import 'dart:isolate';
class IsolateService {
Isolate? _isolate;
ReceivePort? _receivePort;
SendPort? _sendPort;
Future<void> start() async {
_receivePort = ReceivePort();
_isolate = await Isolate.spawn(
_isolateEntry,
_receivePort!.sendPort,
);
_sendPort = await _receivePort!.first;
}
static void _isolateEntry(SendPort sendPort) {
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen((message) {
// Обробка повідомлень
if (message is String) {
print('Отримано: $message');
}
});
}
void sendMessage(String message) {
_sendPort?.send(message);
}
void dispose() {
_isolate?.kill();
_receivePort?.close();
}
}Таймери та періодичні операції
// Одноразовий таймер
Timer(Duration(seconds: 5), () {
print('Пройшло 5 секунд');
});
// Періодичний таймер
Timer.periodic(Duration(seconds: 1), (timer) {
print('Тік: ${timer.tick}');
if (timer.tick >= 10) {
timer.cancel();
print('Таймер зупинено');
}
});
// Скасування таймера
class TimerExample extends StatefulWidget {
@override
_TimerExampleState createState() => _TimerExampleState();
}
class _TimerExampleState extends State<TimerExample> {
Timer? _timer;
int _seconds = 0;
@override
void initState() {
super.initState();
_startTimer();
}
void _startTimer() {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
_seconds++;
});
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('Секунд: $_seconds');
}
}Практичні приклади
Пошук з debounce
class SearchScreen extends StatefulWidget {
@override
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final _searchController = TextEditingController();
final _searchSubject = StreamController<String>();
List<SearchResult> _results = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_searchSubject.stream
.debounceTime(Duration(milliseconds: 300))
.distinct()
.where((query) => query.length >= 3)
.asyncMap((query) => _performSearch(query))
.listen((results) {
setState(() {
_results = results;
_isLoading = false;
});
});
}
Future<List<SearchResult>> _performSearch(String query) async {
setState(() {
_isLoading = true;
});
// API запит
final response = await http.get(
Uri.parse('https://api.example.com/search?q=$query'),
);
return (jsonDecode(response.body) as List)
.map((item) => SearchResult.fromJson(item))
.toList();
}
@override
void dispose() {
_searchController.dispose();
_searchSubject.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Пошук...',
border: InputBorder.none,
),
onChanged: (query) => _searchSubject.add(query),
),
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _results.length,
itemBuilder: (context, index) {
return ListTile(title: Text(_results[index].title));
},
),
);
}
}Завантаження з retry
Future<T> fetchWithRetry<T>(
Future<T> Function() fetchFunction, {
int maxRetries = 3,
Duration delay = const Duration(seconds: 1),
}) async {
int attempts = 0;
while (true) {
try {
return await fetchFunction();
} catch (e) {
attempts++;
if (attempts >= maxRetries) {
rethrow;
}
print('Спроба $attempts не вдалася. Повтор через ${delay.inSeconds} с...');
await Future.delayed(delay * attempts); // Експоненціальна затримка
}
}
}
// Використання
Future<User> fetchUserWithRetry() async {
return fetchWithRetry(
() => fetchUser(),
maxRetries: 3,
delay: Duration(seconds: 2),
);
}Кешування даних
class CacheService<T> {
final Map<String, CacheEntry<T>> _cache = {};
final Duration _cacheDuration;
CacheService({Duration? cacheDuration})
: _cacheDuration = cacheDuration ?? Duration(minutes: 5);
Future<T> get(
String key,
Future<T> Function() fetchFunction,
) async {
final entry = _cache[key];
if (entry != null && !entry.isExpired) {
return entry.data;
}
final data = await fetchFunction();
_cache[key] = CacheEntry(data, DateTime.now().add(_cacheDuration));
return data;
}
void invalidate(String key) {
_cache.remove(key);
}
void clear() {
_cache.clear();
}
}
class CacheEntry<T> {
final T data;
final DateTime expiresAt;
CacheEntry(this.data, this.expiresAt);
bool get isExpired => DateTime.now().isAfter(expiresAt);
}
// Використання
final userCache = CacheService<User>();
Future<User> getUser(String id) async {
return userCache.get(
'user_$id',
() => fetchUser(id),
);
}Найкращі практики
Завжди обробляйте помилки — використовуйте try/catch або onError.
Скасовуйте підписки у dispose() для запобігання витоків пам'яті.
Використовуйте compute() для важких обчислень.
Не створюйте Future в build() — ініціалізуйте їх у initState().
Використовуйте broadcast streams для кількох слухачів.
Використовуйте debounce для пошуку та введення тексту.
Висновок
Асинхронне програмування у Flutter базується на потужних можливостях Dart: Future, Stream та Isolate. Правильне використання цих інструментів дозволяє створювати швидкі та чуйні застосунки, які ефективно обробляють мережеві запити, введення користувача та фонові операції.