├── README.md ├── main.dart └── pubspec.yaml /README.md: -------------------------------------------------------------------------------- 1 | # 📊 Concert App Case Study 2 | 3 | ## 🎯 Proje Hedefi 4 | 5 | Concert App, kullanıcılara yaklaşan konserleri kolayca keşfetme ve bilet satın alma imkanı sunmayı amaçlayan bir mobil uygulamadır. Proje, müzikseverlerin konser deneyimlerini iyileştirmeyi ve organizatörlerin etkinliklerini daha geniş bir kitleye ulaştırmayı hedeflemektedir. 6 | 7 | ## 🧑‍💻 Geliştirme Süreci 8 | 9 | 1. **Planlama ve Tasarım**: 10 | - Kullanıcı hikayeleri ve gereksinimlerin belirlenmesi 11 | - Wireframe ve UI/UX tasarımlarının oluşturulması 12 | - Teknoloji stack'inin seçilmesi (Flutter & Dart) 13 | 14 | 2. **Geliştirme**: 15 | - Ana ekran ve konser listesi oluşturulması 16 | - Konser detay sayfasının implementasyonu 17 | - API entegrasyonu ve veri yönetimi 18 | - Karanlık tema ve lokalizasyon desteği eklenmesi 19 | 20 | 3. **Test ve İyileştirme**: 21 | - Birim testleri ve UI testlerinin yazılması 22 | - Performans optimizasyonu 23 | - Kullanıcı geri bildirimleri doğrultusunda iyileştirmeler 24 | 25 | ## 🏋️ Zorluklar ve Çözümler 26 | 27 | 1. **Zorluk**: Farklı formatlarda gelen tarih ve saat bilgilerinin işlenmesi 28 | **Çözüm**: Intl paketi kullanılarak esnek tarih/saat formatlaması implementasyonu 29 | 30 | 2. **Zorluk**: Konum bazlı filtreleme için verimli bir yöntem bulunması 31 | **Çözüm**: Backend'de geospatial indexing kullanılması ve client-side ön filtreleme uygulanması 32 | 33 | 3. **Zorluk**: Uygulama performansının büyük veri setlerinde korunması 34 | **Çözüm**: Lazy loading ve pagination tekniklerinin uygulanması 35 | 36 | ## 📈 Sonuçlar ve Öğrenilen Dersler 37 | 38 | - Flutter'ın cross-platform geliştirme sürecini önemli ölçüde hızlandırdığı gözlemlendi 39 | - API tasarımının önemi ve veri optimizasyonunun mobil uygulama performansına etkisi daha iyi anlaşıldı 40 | 41 | ## 🚀 Gelecek Planları 42 | 43 | - Kullanıcı profilleri ve kişiselleştirilmiş öneriler 44 | - In-app bilet satın alma özelliği 45 | - Sanatçı ve mekan incelemeleri 46 | - Sosyal medya entegrasyonu ve etkinlik paylaşımı 47 | 48 | ## 💡 Öneriler 49 | 50 | 1. API tasarımına baştan daha fazla zaman ayırmak, geliştirme sürecini hızlandırabilir 51 | 2. A/B testleri ile kullanıcı arayüzü iyileştirmeleri yapmak 52 | 3. Offline kullanım için caching mekanizmaları geliştirmek 53 | 54 | Bu case study, Concert App projesinin geliştirilme sürecini, karşılaşılan zorlukları ve elde edilen sonuçları özetlemektedir. Proje ekibi, bu deneyimden edindiği bilgileri gelecekteki projelerde kullanmayı ve uygulamayı sürekli olarak geliştirmeyi hedeflemektedir. 55 | -------------------------------------------------------------------------------- /main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:http/http.dart' as http; 4 | import 'dart:convert'; 5 | import 'package:intl/date_symbol_data_local.dart'; 6 | import 'package:url_launcher/url_launcher.dart'; 7 | import 'package:google_fonts/google_fonts.dart'; 8 | 9 | 10 | 11 | void main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | await initializeDateFormatting('tr_TR', null); 14 | runApp(MyApp()); 15 | } 16 | 17 | class MyApp extends StatelessWidget { 18 | @override 19 | Widget build(BuildContext context) { 20 | return MaterialApp( 21 | theme: ThemeData.dark().copyWith( 22 | scaffoldBackgroundColor: Color(0xFF121212), 23 | ), 24 | debugShowCheckedModeBanner: false, 25 | home: ConcertListScreen(), 26 | ); 27 | } 28 | } 29 | 30 | class ConcertListScreen extends StatefulWidget { 31 | @override 32 | _ConcertListScreenState createState() => _ConcertListScreenState(); 33 | } 34 | 35 | class _ConcertListScreenState extends State { 36 | List> concerts = []; 37 | bool isLoading = true; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | fetchConcerts(); 43 | } 44 | 45 | Future fetchConcerts() async { 46 | final response = await http.get(Uri.parse( 47 | 'https://raw.githubusercontent.com/codermert/image-name-changer/main/konserler.json')); 48 | 49 | if (response.statusCode == 200) { 50 | final List jsonData = json.decode(response.body); 51 | setState(() { 52 | concerts = jsonData.expand((data) { 53 | return (data['concerts'] as List).map((concert) { 54 | DateTime parsedDate; 55 | try { 56 | parsedDate = DateFormat("dd MMMM EEEE, HH:mm", "tr_TR").parse(concert['dateTime']); 57 | } catch (e) { 58 | parsedDate = DateTime.now(); 59 | } 60 | return { 61 | "date": parsedDate, 62 | "location": data['location'], 63 | "venue": concert['venue'], 64 | "time": concert['dateTime'], 65 | "title": data['title'], 66 | "imageUrl": data['imageUrl'], 67 | "link": data['link'], 68 | "price": concert['price'], 69 | }; 70 | }); 71 | }).toList(); 72 | isLoading = false; 73 | }); 74 | } else { 75 | throw Exception('Failed to load concerts'); 76 | } 77 | } 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return Scaffold( 82 | body: SafeArea( 83 | child: isLoading 84 | ? Center(child: CircularProgressIndicator()) 85 | : Padding( 86 | padding: const EdgeInsets.all(16.0), 87 | child: Column( 88 | crossAxisAlignment: CrossAxisAlignment.start, 89 | children: [ 90 | Text( 91 | 'Bursa yakınında', 92 | style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), 93 | ), 94 | SizedBox(height: 8), 95 | Text( 96 | 'Bu konumun yakınında yaklaşan etkinlik yok.', 97 | style: TextStyle(color: Colors.grey), 98 | ), 99 | SizedBox(height: 16), 100 | ElevatedButton( 101 | onPressed: () {}, 102 | child: Text('Konumu değiştir'), 103 | style: ElevatedButton.styleFrom( 104 | backgroundColor: Colors.grey[900], 105 | foregroundColor: Colors.white, 106 | shape: RoundedRectangleBorder( 107 | borderRadius: BorderRadius.circular(20), 108 | ), 109 | ), 110 | ), 111 | SizedBox(height: 24), 112 | Container( 113 | decoration: BoxDecoration( 114 | gradient: LinearGradient( 115 | begin: Alignment.topLeft, 116 | end: Alignment.bottomRight, 117 | colors: [ 118 | Color(0xFF8E2DE2), 119 | Color(0xFF4A00E0), 120 | ], 121 | ), 122 | borderRadius: BorderRadius.circular(8), 123 | ), 124 | child: Padding( 125 | padding: const EdgeInsets.all(16.0), 126 | child: Column( 127 | crossAxisAlignment: CrossAxisAlignment.start, 128 | children: [ 129 | Text( 130 | 'Turnede', 131 | style: TextStyle( 132 | color: Colors.white.withOpacity(0.7), 133 | fontSize: 14, 134 | ), 135 | ), 136 | SizedBox(height: 8), 137 | Text( 138 | 'Madrigal 25 Ağu Paz • 21:00\nMaximum Uniq Açıkhava, İstanbul, TR', 139 | style: TextStyle( 140 | color: Colors.white, 141 | fontSize: 18, 142 | fontWeight: FontWeight.bold, 143 | ), 144 | ), 145 | SizedBox(height: 16), 146 | Container( 147 | width: double.infinity, 148 | child: TextButton( 149 | onPressed: () { 150 | final String? link = 'https://www.biletix.com/etkinlik/35A9J/TURKIYE/tr' as String?; 151 | if (link != null) { 152 | launchUrl(Uri.parse(link)); 153 | } 154 | }, 155 | child: Text('Tüm etkinlikleri gör'), 156 | style: TextButton.styleFrom( 157 | foregroundColor: Colors.white, 158 | backgroundColor: Colors.transparent, 159 | shape: RoundedRectangleBorder( 160 | borderRadius: BorderRadius.circular(20), 161 | side: BorderSide(color: Colors.white), 162 | ), 163 | ), 164 | ), 165 | ), 166 | ], 167 | ), 168 | ), 169 | ), 170 | SizedBox(height: 24), 171 | Text( 172 | 'Diğer konumlar', 173 | style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), 174 | ), 175 | SizedBox(height: 16), 176 | Expanded( 177 | child: ListView.builder( 178 | itemCount: concerts.length, 179 | itemBuilder: (context, index) { 180 | final concert = concerts[index]; 181 | return ListTile( 182 | leading: Column( 183 | mainAxisAlignment: MainAxisAlignment.center, 184 | children: [ 185 | Text( 186 | DateFormat('MMM', 'tr_TR').format(concert['date']).substring(0, 3), 187 | style: TextStyle(fontSize: 12, color: Colors.grey), 188 | ), 189 | Text( 190 | concert['date'].day.toString(), 191 | style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), 192 | ), 193 | ], 194 | ), 195 | title: Text(concert['title']), 196 | subtitle: Text('${concert['time']} • ${concert['venue']}'), 197 | trailing: IconButton( 198 | icon: Icon(Icons.add_circle_outline), 199 | onPressed: () { 200 | Navigator.push( 201 | context, 202 | MaterialPageRoute(builder: (context) => ConcertDetailScreen(concert: concert)), 203 | ); 204 | }, 205 | ), 206 | ); 207 | }, 208 | ), 209 | ), 210 | ], 211 | ), 212 | ), 213 | ), 214 | ); 215 | } 216 | } 217 | class ConcertDetailScreen extends StatelessWidget { 218 | final Map concert; 219 | 220 | const ConcertDetailScreen({Key? key, required this.concert}) : super(key: key); 221 | 222 | @override 223 | Widget build(BuildContext context) { 224 | final DateTime concertDate = concert['date'] as DateTime? ?? DateTime.now(); 225 | final String formattedMonth = DateFormat('MMM', 'tr_TR').format(concertDate).substring(0, 3); 226 | final String formattedDay = concertDate.day.toString(); 227 | 228 | return Scaffold( 229 | body: Stack( 230 | children: [ 231 | Container( 232 | decoration: BoxDecoration( 233 | gradient: LinearGradient( 234 | begin: Alignment.topCenter, 235 | end: Alignment.bottomCenter, 236 | colors: [ 237 | Colors.grey[800]!, 238 | Colors.grey[900]!, 239 | Colors.black, 240 | ], 241 | stops: const [0.0, 0.5, 1.0], 242 | ), 243 | ), 244 | ), 245 | Positioned( 246 | top: 40, 247 | left: 10, 248 | child: IconButton( 249 | icon: const Icon(Icons.arrow_back), 250 | color: Colors.white, 251 | onPressed: () => Navigator.pop(context), 252 | ), 253 | ), 254 | Padding( 255 | padding: const EdgeInsets.all(16.0), 256 | child: Column( 257 | crossAxisAlignment: CrossAxisAlignment.start, 258 | children: [ 259 | const SizedBox(height: 80), 260 | Stack( 261 | children: [ 262 | Container( 263 | width: 120, 264 | height: 120, 265 | decoration: BoxDecoration( 266 | shape: BoxShape.circle, 267 | image: DecorationImage( 268 | fit: BoxFit.cover, 269 | image: NetworkImage(concert['imageUrl'] as String? ?? 270 | "https://b6s54eznn8xq.merlincdn.net/dist/assets/img/logodark.svg"), 271 | ), 272 | ), 273 | ), 274 | Positioned( 275 | bottom: 0, 276 | right: 0, 277 | child: Container( 278 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 279 | decoration: BoxDecoration( 280 | color: Colors.black, 281 | borderRadius: BorderRadius.circular(4), 282 | ), 283 | child: Column( 284 | children: [ 285 | Text( 286 | formattedMonth, 287 | style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), 288 | ), 289 | Text( 290 | formattedDay, 291 | style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), 292 | ), 293 | ], 294 | ), 295 | ), 296 | ), 297 | ], 298 | ), 299 | const SizedBox(height: 16), 300 | Text( 301 | concert['title'] as String? ?? 'Bilinmeyen Sanatçı', 302 | style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white), 303 | ), 304 | const SizedBox(height: 8), 305 | Text( 306 | concert['date'] != null 307 | ? DateFormat('d MMMM ', 'tr_TR').format(concert['date'] as DateTime) 308 | : 'Tarih mevcut değil', 309 | style: const TextStyle(fontSize: 16, color: Colors.white70), 310 | ), 311 | const SizedBox(height: 24), 312 | // Yeni eklenen mekan bilgileri 313 | Padding( 314 | padding: const EdgeInsets.all(1.0), 315 | child: Container( 316 | decoration: BoxDecoration( 317 | color: Colors.grey[850], 318 | borderRadius: BorderRadius.circular(12.0), 319 | ), 320 | child: Row( 321 | children: [ 322 | ClipRRect( 323 | borderRadius: BorderRadius.circular(8.0), 324 | child: Image.network( 325 | 'https://i.scdn.co/image/ab6761610000e5eb06cb6902e0666278278ae1f4', 326 | width: 100.0, 327 | height: 100.0, 328 | fit: BoxFit.cover, 329 | ), 330 | ), 331 | SizedBox(width: 16.0), 332 | Expanded( 333 | child: Column( 334 | crossAxisAlignment: CrossAxisAlignment.start, 335 | mainAxisAlignment: MainAxisAlignment.center, 336 | children: [ 337 | SizedBox(height: 5.0), 338 | Text( 339 | 'Turnede', 340 | style: TextStyle( 341 | fontSize: 12.0, 342 | color: Colors.grey[400], 343 | ), 344 | ), 345 | SizedBox(height: 4.0), 346 | Text( 347 | 'Sibel Can', 348 | style: TextStyle( 349 | fontSize: 20.0, 350 | fontWeight: FontWeight.bold, 351 | color: Colors.white, 352 | ), 353 | ), 354 | SizedBox(height: 16.0), 355 | OutlinedButton( 356 | onPressed: () { 357 | final String? link = 'https://biletinial.com/tr-tr/muzik/an-epic-symphony-sibel-can-husnu-senlendirici' as String?; 358 | if (link != null) { 359 | launchUrl(Uri.parse(link)); 360 | } 361 | }, 362 | style: OutlinedButton.styleFrom( 363 | side: BorderSide(color: Colors.white), 364 | shape: RoundedRectangleBorder( 365 | borderRadius: BorderRadius.circular(20.0), 366 | ), 367 | ), 368 | child: Text( 369 | 'Tüm etkinlikleri gör', 370 | style: TextStyle(color: Colors.white), 371 | ), 372 | ), 373 | SizedBox(height: 5.0), 374 | ], 375 | ), 376 | ), 377 | SizedBox(width: 16.0), 378 | ], 379 | ), 380 | ), 381 | ), 382 | const SizedBox(height: 20), 383 | const Text( 384 | 'Mekan', 385 | style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white), 386 | ), 387 | const SizedBox(height: 16), 388 | Row( 389 | children: [ 390 | const Icon(Icons.access_time, color: Colors.white), 391 | const SizedBox(width: 8), 392 | Text( 393 | 'Konser saati: ${concert['time'] ?? 'Saat bilgisi mevcut değil'}', 394 | style: const TextStyle(fontSize: 16, color: Colors.white), 395 | ), 396 | ], 397 | ), 398 | const SizedBox(height: 8), 399 | Row( 400 | crossAxisAlignment: CrossAxisAlignment.start, 401 | children: [ 402 | const Icon(Icons.location_on, color: Colors.white), 403 | const SizedBox(width: 8), 404 | Expanded( 405 | child: Column( 406 | crossAxisAlignment: CrossAxisAlignment.start, 407 | children: [ 408 | Text( 409 | concert['venue'] as String? ?? 'Konum bilgisi mevcut değil', 410 | style: const TextStyle(fontSize: 16, color: Colors.white), 411 | ), 412 | ], 413 | ), 414 | ), 415 | ], 416 | ), 417 | const SizedBox(height: 24), 418 | Row( 419 | children: [ 420 | ElevatedButton.icon( 421 | icon: const Icon(Icons.add), 422 | label: const Text('İlgilenilen'), 423 | onPressed: () {}, 424 | style: ElevatedButton.styleFrom( 425 | backgroundColor: Colors.grey[800], 426 | foregroundColor: Colors.white, 427 | ), 428 | ), 429 | const SizedBox(width: 8), 430 | IconButton( 431 | icon: const Icon(Icons.share), 432 | onPressed: () {}, 433 | color: Colors.white, 434 | ), 435 | IconButton( 436 | icon: const Icon(Icons.more_vert), 437 | onPressed: () {}, 438 | color: Colors.white, 439 | ), 440 | ], 441 | ), 442 | const SizedBox(height: 55), 443 | Row( 444 | children: [ 445 | const CircleAvatar( 446 | backgroundColor: Colors.white, 447 | child: Text('B', style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold)), 448 | ), 449 | const SizedBox(width: 8), 450 | Expanded( 451 | child: Column( 452 | crossAxisAlignment: CrossAxisAlignment.start, 453 | children: const [ 454 | Text('Satışta', style: TextStyle(color: Colors.white)), 455 | Row( 456 | children: [ 457 | Text('biletinial.com', style: TextStyle(color: Colors.white70)), 458 | SizedBox(width: 8), 459 | Icon(Icons.verified, size: 18, color: Colors.blue), 460 | ], 461 | ), 462 | ], 463 | ), 464 | ), 465 | ElevatedButton.icon( 466 | label: const Text('Bilet bul'), 467 | icon: const Icon(Icons.open_in_new), 468 | onPressed: () { 469 | final String? link = concert['link'] as String?; 470 | if (link != null) { 471 | launchUrl(Uri.parse(link)); 472 | } 473 | }, 474 | style: ElevatedButton.styleFrom( 475 | backgroundColor: const Color(0xFF1ed760), 476 | foregroundColor: Colors.black, 477 | ), 478 | ), 479 | ], 480 | ), 481 | const SizedBox(height: 8), 482 | Text( 483 | concert['price'] as String? ?? 'Fiyat bilgisi mevcut değil', 484 | style: const TextStyle(fontSize: 14, color: Colors.white), 485 | ), 486 | ], 487 | ), 488 | ), 489 | ], 490 | ), 491 | ); 492 | } 493 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: concert 2 | description: "A new Flutter project." 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: '>=3.4.3 <4.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | 35 | # The following adds the Cupertino Icons font to your application. 36 | # Use with the CupertinoIcons class for iOS style icons. 37 | cupertino_icons: ^1.0.6 38 | google_fonts: ^6.2.1 39 | intl: ^0.19.0 40 | http: ^1.2.2 41 | url_launcher: ^6.3.0 42 | 43 | 44 | dev_dependencies: 45 | flutter_test: 46 | sdk: flutter 47 | 48 | # The "flutter_lints" package below contains a set of recommended lints to 49 | # encourage good coding practices. The lint set provided by the package is 50 | # activated in the `analysis_options.yaml` file located at the root of your 51 | # package. See that file for information about deactivating specific lint 52 | # rules and activating additional ones. 53 | flutter_lints: ^3.0.0 54 | 55 | # For information on the generic Dart part of this file, see the 56 | # following page: https://dart.dev/tools/pub/pubspec 57 | 58 | # The following section is specific to Flutter packages. 59 | flutter: 60 | 61 | # The following line ensures that the Material Icons font is 62 | # included with your application, so that you can use the icons in 63 | # the material Icons class. 64 | uses-material-design: true 65 | 66 | # To add assets to your application, add an assets section, like this: 67 | # assets: 68 | # - images/a_dot_burr.jpeg 69 | # - images/a_dot_ham.jpeg 70 | 71 | # An image asset can refer to one or more resolution-specific "variants", see 72 | # https://flutter.dev/assets-and-images/#resolution-aware 73 | 74 | # For details regarding adding assets from package dependencies, see 75 | # https://flutter.dev/assets-and-images/#from-packages 76 | 77 | # To add custom fonts to your application, add a fonts section here, 78 | # in this "flutter" section. Each entry in this list should have a 79 | # "family" key with the font family name, and a "fonts" key with a 80 | # list giving the asset and other descriptors for the font. For 81 | # example: 82 | # fonts: 83 | # - family: Schyler 84 | # fonts: 85 | # - asset: fonts/Schyler-Regular.ttf 86 | # - asset: fonts/Schyler-Italic.ttf 87 | # style: italic 88 | # - family: Trajan Pro 89 | # fonts: 90 | # - asset: fonts/TrajanPro.ttf 91 | # - asset: fonts/TrajanPro_Bold.ttf 92 | # weight: 700 93 | # 94 | # For details regarding fonts from package dependencies, 95 | # see https://flutter.dev/custom-fonts/#from-packages 96 | --------------------------------------------------------------------------------