İçeriğe geç

Flutter Zamanlayıcı

başlangıç

Bu eğitimde, bloc kütüphanesini kullanarak nasıl bir zamanlayıcı uygulaması oluşturacağımızı ele alacağız. Tamamlanan uygulama şöyle görünmelidir:

demo

  • BlocObserver ile durum değişikliklerini gözlemleme.
  • BlocProvider, alt widget'larına bir bloc sağlayan Flutter widget'ı.
  • BlocBuilder, yeni durumlara karşılık widget'ın yeniden oluşturulmasını yöneten Flutter widget'ı.
  • Equatable ile gereksiz yeniden oluşturmaları önleme.
  • Bir Bloc içinde StreamSubscription kullanmayı öğrenme.
  • buildWhen ile gereksiz yeniden oluşturmaları önleme.

Yepyeni bir Flutter projesi oluşturarak başlayacağız:

Terminal window
flutter create flutter_timer

Ardından pubspec.yaml dosyasının içeriğini şununla değiştirebiliriz:

pubspec.yaml
name: flutter_timer
description: A new Flutter project.
version: 1.0.0+1
publish_to: none
environment:
sdk: ^3.12.0
dependencies:
bloc: ^9.0.0
equatable: ^2.1.0-dev.0
flutter:
sdk: flutter
flutter_bloc: ^9.1.0
dev_dependencies:
bloc_lint: ^0.3.0
bloc_test: ^10.0.0
flutter_test:
sdk: flutter
mocktail: ^1.0.0
flutter:
uses-material-design: true

Sonrasında, tüm bağımlılıkları kurmak için flutter pub get komutunu çalıştırın.

├── lib
| ├── timer
│ │ ├── bloc
│ │ │ └── timer_bloc.dart
| | | └── timer_event.dart
| | | └── timer_state.dart
│ │ └── view
│ │ | ├── timer_page.dart
│ │ ├── timer.dart
│ ├── app.dart
│ ├── ticker.dart
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml

Ticker, zamanlayıcı uygulamamız için veri kaynağımız olacak. Abone olup tepki verebileceğimiz bir tik stream'i sunacaktır.

ticker.dart dosyasını oluşturarak başlayalım.

lib/ticker.dart
class Ticker {
const Ticker();
Stream<int> tick({required int ticks}) {
return Stream.periodic(
const Duration(seconds: 1),
(x) => ticks - x - 1,
).take(ticks);
}
}

Ticker sınıfımızın yaptığı tek şey, istediğimiz tik (saniye) sayısını alıp her saniye kalan saniyeleri yayan bir stream döndüren bir tick fonksiyonu sunmaktır.

Sırada, Ticker'ı tüketecek olan TimerBloc'umuzu oluşturmamız gerekiyor.

TimerBloc'umuzun bulunabileceği TimerStates'leri tanımlayarak başlayalım.

TimerBloc durumu aşağıdakilerden biri olabilir:

  • TimerInitial: belirtilen süreden geriye saymaya hazır.
  • TimerRunInProgress: belirtilen süreden aktif olarak geri sayıyor.
  • TimerRunPause: belirli bir kalan sürede duraklatılmış.
  • TimerRunComplete: kalan süre 0 ile tamamlanmış.

Bu durumların her biri, kullanıcı arayüzü ve kullanıcının yapabileceği eylemler üzerinde bir etkiye sahip olacaktır. Örneğin:

  • Durum TimerInitial ise kullanıcı zamanlayıcıyı başlatabilecek.
  • Durum TimerRunInProgress ise kullanıcı zamanlayıcıyı duraklatabilecek ve sıfırlayabilecek, ayrıca kalan süreyi görebilecek.
  • Durum TimerRunPause ise kullanıcı zamanlayıcıyı devam ettirebilecek ve sıfırlayabilecek.
  • Durum TimerRunComplete ise kullanıcı zamanlayıcıyı sıfırlayabilecek.

Tüm bloc dosyalarımızı bir arada tutmak için bloc/timer_state.dart ile bir bloc dizini oluşturalım.

lib/timer/bloc/timer_state.dart
part of 'timer_bloc.dart';
sealed class TimerState extends Equatable {
const TimerState(this.duration);
final int duration;
@override
List<Object> get props => [duration];
}
final class TimerInitial extends TimerState {
const TimerInitial(super.duration);
@override
String toString() => 'TimerInitial { duration: $duration }';
}
final class TimerRunPause extends TimerState {
const TimerRunPause(super.duration);
@override
String toString() => 'TimerRunPause { duration: $duration }';
}
final class TimerRunInProgress extends TimerState {
const TimerRunInProgress(super.duration);
@override
String toString() => 'TimerRunInProgress { duration: $duration }';
}
final class TimerRunComplete extends TimerState {
const TimerRunComplete() : super(0);
}

Tüm TimerStates'lerin, bir duration özelliğine sahip soyut temel sınıf TimerState'i genişlettiğine dikkat edin. Bunun nedeni, TimerBloc'umuzun hangi durumda olduğundan bağımsız olarak, ne kadar süre kaldığını bilmek istememizdir. Ek olarak, TimerState, aynı durum oluştuğunda uygulamamızın yeniden oluşturmaları tetiklememesini sağlayarak kodumuzu optimize etmek için Equatable'ı genişletir.

Sırada, TimerBloc'umuzun işleyeceği TimerEvents'leri tanımlayalım ve uygulayalım.

TimerBloc'umuzun aşağıdaki olayları nasıl işleyeceğini bilmesi gerekecek:

  • TimerStarted: TimerBloc'a zamanlayıcının başlatılması gerektiğini bildirir.
  • TimerPaused: TimerBloc'a zamanlayıcının duraklatılması gerektiğini bildirir.
  • TimerResumed: TimerBloc'a zamanlayıcının devam ettirilmesi gerektiğini bildirir.
  • TimerReset: TimerBloc'a zamanlayıcının orijinal duruma sıfırlanması gerektiğini bildirir.
  • _TimerTicked: TimerBloc'a bir tikin gerçekleştiğini ve durumunu buna göre güncellemesi gerektiğini bildirir.

IntelliJ veya VSCode eklentilerini kullanmadıysanız, bloc/timer_event.dart dosyasını oluşturun ve bu olayları uygulayalım.

lib/timer/bloc/timer_event.dart
part of 'timer_bloc.dart';
sealed class TimerEvent {
const TimerEvent();
}
final class TimerStarted extends TimerEvent {
const TimerStarted({required this.duration});
final int duration;
}
final class TimerPaused extends TimerEvent {
const TimerPaused();
}
final class TimerResumed extends TimerEvent {
const TimerResumed();
}
class TimerReset extends TimerEvent {
const TimerReset();
}
class _TimerTicked extends TimerEvent {
const _TimerTicked({required this.duration});
final int duration;
}

Sırada, TimerBloc'u uygulayalım!

Henüz oluşturmadıysanız, bloc/timer_bloc.dart dosyasını oluşturun ve boş bir TimerBloc oluşturun.

lib/timer/bloc/timer_bloc.dart
import 'package:bloc/bloc.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
// TODO: set initial state
TimerBloc(): super() {
// TODO: implement event handlers
}
}

İlk yapmamız gereken şey, TimerBloc'umuzun başlangıç durumunu tanımlamaktır. Bu durumda, TimerBloc'un 1 dakikalık (60 saniye) önceden ayarlanmış bir süreyle TimerInitial durumunda başlamasını istiyoruz.

lib/timer/bloc/timer_bloc.dart
import 'package:bloc/bloc.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
static const int _duration = 60;
TimerBloc() : super(TimerInitial(_duration)) {
// TODO: implement event handlers
}
}

Sırada, Ticker'a olan bağımlılığımızı tanımlamamız gerekiyor.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
// TODO: implement event handlers
}
}

Ayrıca biraz sonra inceleyeceğimiz Ticker'ımız için bir StreamSubscription da tanımlıyoruz.

Bu noktada yapmamız gereken tek şey olay işleyicilerini uygulamaktır. Daha iyi okunabilirlik için, her olay işleyicisini kendi yardımcı fonksiyonuna ayırmayı tercih ediyorum. TimerStarted olayıyla başlayalım.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
}
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
}

TimerBloc, bir TimerStarted olayı alırsa, başlangıç süresiyle bir TimerRunInProgress durumunu yaymaktadır. Ek olarak, halihazırda açık bir _tickerSubscription varsa, belleği serbest bırakmak için onu iptal etmemiz gerekir. Ayrıca TimerBloc kapatıldığında _tickerSubscription'ı iptal edebilmek için TimerBloc'umuzdaki close metodunu da geçersiz kılmamız gerekir. Son olarak, _ticker.tick stream'ini dinliyor ve her tikte kalan süreyle bir _TimerTicked olayı ekliyoruz.

Sırada, _TimerTicked olay işleyicisini uygulayalım.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<_TimerTicked>(_onTicked);
}
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
void _onTicked(_TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: TimerRunComplete(),
);
}
}

Her _TimerTicked olayı alındığında, eğer tikin süresi 0'dan büyükse, yeni süreyle güncellenmiş bir TimerRunInProgress durumu yaymaktayız. Aksi takdirde, tikin süresi 0 ise, zamanlayıcımız sona ermiştir ve bir TimerRunComplete durumu yaymamız gerekir.

Şimdi TimerPaused olay işleyicisini uygulayalım.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<TimerPaused>(_onPaused);
on<_TimerTicked>(_onTicked);
}
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
void _onPaused(TimerPaused event, Emitter<TimerState> emit) {
if (state is TimerRunInProgress) {
_tickerSubscription?.pause();
emit(TimerRunPause(state.duration));
}
}
void _onTicked(_TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: TimerRunComplete(),
);
}
}

_onPaused içinde, TimerBloc'umuzun state'i TimerRunInProgress ise, _tickerSubscription'ı duraklatabilir ve mevcut zamanlayıcı süresiyle bir TimerRunPause durumu yayabiliriz.

Sırada, zamanlayıcının duraklatılmasını kaldırabilmek için TimerResumed olay işleyicisini uygulayalım.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<TimerPaused>(_onPaused);
on<TimerResumed>(_onResumed);
on<_TimerTicked>(_onTicked);
}
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
void _onPaused(TimerPaused event, Emitter<TimerState> emit) {
if (state is TimerRunInProgress) {
_tickerSubscription?.pause();
emit(TimerRunPause(state.duration));
}
}
void _onResumed(TimerResumed resume, Emitter<TimerState> emit) {
if (state is TimerRunPause) {
_tickerSubscription?.resume();
emit(TimerRunInProgress(state.duration));
}
}
void _onTicked(_TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: TimerRunComplete(),
);
}
}

TimerResumed olay işleyicisi, TimerPaused olay işleyicisine çok benzer. TimerBloc TimerRunPause state'inde ise ve bir TimerResumed olayı alırsa, _tickerSubscription'ı devam ettirir ve mevcut süreyle bir TimerRunInProgress durumu yayar.

Son olarak, TimerReset olay işleyicisini uygulamamız gerekir.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
TimerBloc({required this._ticker}) : super(const TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<TimerPaused>(_onPaused);
on<TimerResumed>(_onResumed);
on<TimerReset>(_onReset);
on<_TimerTicked>(_onTicked);
}
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
void _onPaused(TimerPaused event, Emitter<TimerState> emit) {
if (state is TimerRunInProgress) {
_tickerSubscription?.pause();
emit(TimerRunPause(state.duration));
}
}
void _onResumed(TimerResumed resume, Emitter<TimerState> emit) {
if (state is TimerRunPause) {
_tickerSubscription?.resume();
emit(TimerRunInProgress(state.duration));
}
}
void _onReset(TimerReset event, Emitter<TimerState> emit) {
_tickerSubscription?.cancel();
emit(const TimerInitial(_duration));
}
void _onTicked(_TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: const TimerRunComplete(),
);
}
}

TimerBloc, bir TimerReset olayı alırsa, ek tikler hakkında bildirim almaması için mevcut _tickerSubscription'ı iptal etmesi ve orijinal süreyle bir TimerInitial durumu yayması gerekir.

TimerBloc ile ilgili her şey bu kadar. Şimdi geriye kalan tek şey Zamanlayıcı Uygulamamız için kullanıcı arayüzünü uygulamaktır.

main.dart'ın içeriğini silip aşağıdakiyle değiştirerek başlayabiliriz.

lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_timer/app.dart';
void main() => runApp(const App());

Sırada, uygulamamızın kök widget'ı olacak app.dart içinde 'App' widget'ımızı oluşturalım.

lib/app.dart
import 'package:flutter/material.dart';
import 'package:flutter_timer/timer/timer.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Timer',
theme: ThemeData(
colorScheme: const ColorScheme.light(
primary: Color.fromRGBO(72, 74, 126, 1),
),
),
home: const TimerPage(),
);
}
}

Sırada, Timer widget'ımızı uygulamamız gerekiyor.

Timer widget'ımız (lib/timer/view/timer_page.dart), kalan süreyi ve kullanıcıların zamanlayıcıyı başlatmasını, duraklatmasını ve sıfırlamasını sağlayan uygun düğmelerle birlikte görüntülemekten sorumlu olacaktır.

lib/timer/view/timer_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_timer/ticker.dart';
import 'package:flutter_timer/timer/timer.dart';
class TimerPage extends StatelessWidget {
const TimerPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TimerBloc(ticker: Ticker()),
child: const TimerView(),
);
}
}
class TimerView extends StatelessWidget {
const TimerView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter Timer')),
body: Stack(
children: [
const Background(),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: const <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 100.0),
child: Center(child: TimerText()),
),
Actions(),
],
),
],
),
);
}
}
class TimerText extends StatelessWidget {
const TimerText({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final duration = context.select((TimerBloc bloc) => bloc.state.duration);
final minutesStr =
((duration / 60) % 60).floor().toString().padLeft(2, '0');
final secondsStr = (duration % 60).floor().toString().padLeft(2, '0');
return Text(
'$minutesStr:$secondsStr',
style: Theme.of(
context,
).textTheme.displayLarge?.copyWith(fontWeight: FontWeight.w500),
);
}
}

Şimdiye kadar, sadece TimerBloc'umuzun örneğine erişmek için BlocProvider kullanıyoruz.

Sırada, uygun eylemlere (başlat, duraklat ve sıfırla) sahip olacak Actions widget'ımızı uygulayacağız.

Timer bölümündeki içe aktarmalarımızı temizlemek için bir barrel dosyası timer/timer.dart oluşturmamız gerekir.

lib/timer/timer.dart
export 'bloc/timer_bloc.dart';
export 'view/timer_page.dart';
lib/timer/view/timer_page.dart
class Actions extends StatelessWidget {
const Actions({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<TimerBloc, TimerState>(
buildWhen: (prev, state) => prev.runtimeType != state.runtimeType,
builder: (context, state) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
...switch (state) {
TimerInitial() => [
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () => context
.read<TimerBloc>()
.add(TimerStarted(duration: state.duration)),
),
],
TimerRunInProgress() => [
FloatingActionButton(
child: const Icon(Icons.pause),
onPressed: () =>
context.read<TimerBloc>().add(const TimerPaused()),
),
FloatingActionButton(
child: const Icon(Icons.replay),
onPressed: () =>
context.read<TimerBloc>().add(const TimerReset()),
),
],
TimerRunPause() => [
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () =>
context.read<TimerBloc>().add(const TimerResumed()),
),
FloatingActionButton(
child: const Icon(Icons.replay),
onPressed: () =>
context.read<TimerBloc>().add(const TimerReset()),
),
],
TimerRunComplete() => [
FloatingActionButton(
child: const Icon(Icons.replay),
onPressed: () =>
context.read<TimerBloc>().add(const TimerReset()),
),
]
}
],
);
},
);
}
}

Actions widget'ı, her yeni TimerState aldığımızda kullanıcı arayüzünü yeniden oluşturmak için bir BlocBuilder kullanan başka bir StatelessWidget'tan başka bir şey değildir. Actions, TimerBloc örneğine erişmek için context.read<TimerBloc>() kullanır ve TimerBloc'un mevcut durumuna göre farklı FloatingActionButtons döndürür. FloatingActionButtons'ların her biri, TimerBloc'u bilgilendirmek için onPressed geri çağrısında bir olay ekler.

builder fonksiyonunun ne zaman çağrılacağı konusunda ince ayarlı bir kontrol istiyorsanız, BlocBuilder'a opsiyonel bir buildWhen sağlayabilirsiniz. buildWhen, önceki bloc durumunu ve mevcut bloc durumunu alır ve bir boolean döndürür. buildWhen true döndürürse, builder state ile çağrılır ve widget yeniden oluşturulur. buildWhen false döndürürse, builder state ile çağrılmaz ve yeniden oluşturma gerçekleşmez.

Bu durumda, verimsiz olacağı için Actions widget'ının her tikte yeniden oluşturulmasını istemiyoruz. Bunun yerine, yalnızca TimerState'in runtimeType'ı değiştiğinde (TimerInitial => TimerRunInProgress, TimerRunInProgress => TimerRunPause, vb.) Actions'ın yeniden oluşturulmasını istiyoruz.

Sonuç olarak, her yeniden oluşturmada widget'ları rastgele renklendirseydik, şuna benzeyecekti:

BlocBuilder buildWhen demo

Son olarak, arka plan widget'ını aşağıdaki gibi ekleyin:

lib/timer/view/timer_page.dart
class Background extends StatelessWidget {
const Background({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue.shade50,
Colors.blue.shade500,
],
),
),
);
}
}

İşte hepsi bu kadar! Bu noktada, yalnızca yeniden oluşturulması gereken widget'ları verimli bir şekilde yeniden oluşturan oldukça sağlam bir zamanlayıcı uygulamamız var.

Bu örneğin tam kaynak kodu buradan ulaşılabilir.