170 |
171 | En esta capa no se trabaja con entidades, se trabaja con modelos, éstos heredan de una entidad y tienen métodos toJson y fromJson,
172 | se obtiene el beneficio, por si en algún futuro se decide cambiar de json a xml, sin tener demasiados quebraderos de cabeza.
173 |
174 |
182 |
183 | **Test Driven Development:** es un proceso de desarrollo iterativo, donde el desarrollador escribe una prueba antes de escribir el código
184 | suficiente para cumplirla y luego refactoriza si es necesario.
185 |
186 | Las ventajas de este proceso es que el desarrollador se centra m√°s en los requisitos del software, preguntándose el por qué necesita la fracción de código
187 | que está punto de escribir, antes de continuar con la implementación.
188 |
189 | Mediante este proceso el desarrollador puede identificar requisitos mal definidos y mejorar
190 | sus hábitos con el tiempo, lo que conducir√≠a a una mejora en su calidad de código.
191 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | compileSdkVersion 29
30 |
31 | sourceSets {
32 | main.java.srcDirs += 'src/main/kotlin'
33 | }
34 |
35 | lintOptions {
36 | disable 'InvalidPackage'
37 | }
38 |
39 | defaultConfig {
40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
41 | applicationId "com.example.my_movie_list"
42 | minSdkVersion 16
43 | targetSdkVersion 29
44 | versionCode flutterVersionCode.toInteger()
45 | versionName flutterVersionName
46 | }
47 |
48 | buildTypes {
49 | release {
50 | // TODO: Add your own signing config for the release build.
51 | // Signing with the debug keys for now, so `flutter run --release` works.
52 | signingConfig signingConfigs.debug
53 | }
54 | }
55 | }
56 |
57 | flutter {
58 | source '../..'
59 | }
60 |
61 | dependencies {
62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
63 | }
64 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
8 |
9 |
10 |
15 |
16 |
23 |
27 |
31 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
47 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/my_movie_list/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.my_movie_list
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.50'
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.5.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | jcenter()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | task clean(type: Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 | android.enableR8=true
5 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
7 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/android/settings_aar.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/assets/cats_img/cat1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/assets/cats_img/cat1.jpeg
--------------------------------------------------------------------------------
/assets/cats_img/cat2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/assets/cats_img/cat2.jpeg
--------------------------------------------------------------------------------
/assets/cats_img/cat3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/assets/cats_img/cat3.jpeg
--------------------------------------------------------------------------------
/assets/cats_img/cat4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/assets/cats_img/cat4.jpeg
--------------------------------------------------------------------------------
/assets/img/loading-gif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/assets/img/loading-gif.gif
--------------------------------------------------------------------------------
/assets/img/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/assets/img/loading.gif
--------------------------------------------------------------------------------
/assets/img/no-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/assets/img/no-image.jpg
--------------------------------------------------------------------------------
/coverage/lcov.info:
--------------------------------------------------------------------------------
1 | SF:lib/core/network/network_info.dart
2 | DA:9,1
3 | DA:11,1
4 | DA:12,2
5 | LF:3
6 | LH:3
7 | end_of_record
8 | SF:lib/data/datasources/genres/genres_local_data_source.dart
9 | DA:19,1
10 | DA:21,1
11 | DA:23,2
12 | DA:25,2
13 | DA:30,1
14 | DA:31,2
15 | DA:33,3
16 | DA:35,1
17 | LF:8
18 | LH:8
19 | end_of_record
20 | SF:lib/data/models/genre_model.dart
21 | DA:3,3
22 | DA:4,0
23 | DA:6,3
24 | DA:7,6
25 | DA:8,3
26 | DA:9,3
27 | DA:14,1
28 | DA:15,1
29 | DA:16,1
30 | DA:17,0
31 | DA:22,0
32 | DA:23,0
33 | DA:24,0
34 | DA:25,0
35 | DA:30,0
36 | DA:31,0
37 | DA:34,4
38 | DA:37,4
39 | DA:42,6
40 | DA:43,3
41 | DA:44,3
42 | DA:47,0
43 | DA:48,0
44 | DA:49,0
45 | LF:24
46 | LH:13
47 | end_of_record
48 | SF:lib/domain/entities/genre.dart
49 | DA:5,5
50 | LF:1
51 | LH:1
52 | end_of_record
53 | SF:lib/core/api/movies_api.dart
54 | DA:10,2
55 | DA:11,1
56 | DA:13,1
57 | DA:14,2
58 | DA:16,2
59 | DA:19,1
60 | DA:21,2
61 | DA:22,2
62 | DA:24,0
63 | DA:25,0
64 | DA:28,0
65 | DA:32,0
66 | DA:35,0
67 | DA:39,0
68 | DA:42,1
69 | DA:43,1
70 | DA:44,2
71 | DA:47,0
72 | LF:18
73 | LH:11
74 | end_of_record
75 | SF:lib/data/datasources/genres/genres_remote_data_source.dart
76 | DA:17,1
77 | DA:20,1
78 | DA:21,4
79 | DA:22,2
80 | DA:23,4
81 | DA:25,1
82 | LF:6
83 | LH:6
84 | end_of_record
85 | SF:lib/data/datasources/movies/movies_local_data_source.dart
86 | DA:17,1
87 | DA:19,1
88 | DA:21,2
89 | DA:23,2
90 | DA:27,1
91 | DA:29,2
92 | DA:31,3
93 | DA:33,1
94 | LF:8
95 | LH:8
96 | end_of_record
97 | SF:lib/data/models/movie_model.dart
98 | DA:8,3
99 | DA:9,0
100 | DA:11,3
101 | DA:12,6
102 | DA:13,3
103 | DA:14,3
104 | DA:19,1
105 | DA:20,1
106 | DA:21,1
107 | DA:22,0
108 | DA:48,4
109 | DA:68,4
110 | DA:90,6
111 | DA:91,3
112 | DA:92,3
113 | DA:93,3
114 | DA:94,12
115 | DA:95,1
116 | DA:96,3
117 | DA:97,3
118 | DA:98,3
119 | DA:99,3
120 | DA:100,6
121 | DA:101,3
122 | DA:102,6
123 | DA:103,3
124 | DA:104,3
125 | DA:105,6
126 | DA:106,3
127 | DA:107,3
128 | DA:108,3
129 | DA:109,3
130 | DA:110,3
131 | DA:111,2
132 | DA:112,3
133 | DA:113,3
134 | DA:114,3
135 | DA:115,2
136 | DA:116,3
137 | DA:117,3
138 | DA:120,0
139 | DA:121,0
140 | DA:122,0
141 | DA:123,0
142 | DA:124,0
143 | DA:125,0
144 | DA:126,0
145 | DA:127,0
146 | DA:128,0
147 | DA:129,0
148 | DA:131,0
149 | DA:132,0
150 | DA:133,0
151 | DA:134,0
152 | DA:135,0
153 | DA:136,0
154 | DA:137,0
155 | DA:139,0
156 | DA:141,0
157 | LF:59
158 | LH:38
159 | end_of_record
160 | SF:lib/domain/entities/movie.dart
161 | DA:27,6
162 | LF:1
163 | LH:1
164 | end_of_record
165 | SF:lib/data/models/production_company_model.dart
166 | DA:2,1
167 | DA:14,1
168 | DA:15,1
169 | DA:16,1
170 | DA:17,1
171 | DA:18,1
172 | DA:19,1
173 | DA:22,0
174 | DA:23,0
175 | DA:24,0
176 | DA:25,0
177 | DA:26,0
178 | LF:12
179 | LH:7
180 | end_of_record
181 | SF:lib/data/models/production_country_model.dart
182 | DA:2,1
183 | DA:10,1
184 | DA:11,1
185 | DA:12,1
186 | DA:13,1
187 | DA:16,0
188 | DA:17,0
189 | DA:18,0
190 | LF:8
191 | LH:5
192 | end_of_record
193 | SF:lib/data/datasources/movies/movies_remote_data_source.dart
194 | DA:31,1
195 | DA:34,1
196 | DA:39,3
197 | DA:40,1
198 | DA:42,2
199 | DA:43,4
200 | DA:45,1
201 | DA:49,1
202 | DA:50,4
203 | DA:51,2
204 | DA:52,4
205 | DA:54,1
206 | DA:58,1
207 | DA:59,3
208 | DA:60,1
209 | DA:62,2
210 | DA:63,3
211 | DA:65,1
212 | DA:69,0
213 | DA:70,0
214 | DA:71,0
215 | DA:73,0
216 | DA:74,0
217 | DA:76,0
218 | LF:24
219 | LH:18
220 | end_of_record
221 | SF:lib/data/models/actor_model.dart
222 | DA:3,0
223 | DA:4,0
224 | DA:6,0
225 | DA:7,0
226 | DA:8,0
227 | DA:9,0
228 | DA:15,0
229 | DA:28,0
230 | DA:51,0
231 | DA:52,0
232 | DA:53,0
233 | DA:54,0
234 | DA:55,0
235 | DA:56,0
236 | DA:57,0
237 | DA:58,0
238 | DA:59,0
239 | DA:60,0
240 | DA:61,0
241 | DA:62,0
242 | DA:63,0
243 | DA:66,0
244 | DA:67,0
245 | DA:68,0
246 | DA:69,0
247 | DA:70,0
248 | DA:71,0
249 | DA:72,0
250 | DA:73,0
251 | DA:74,0
252 | DA:75,0
253 | DA:76,0
254 | DA:77,0
255 | DA:78,0
256 | LF:34
257 | LH:0
258 | end_of_record
259 | SF:lib/domain/entities/actor.dart
260 | DA:10,0
261 | DA:20,0
262 | DA:21,0
263 | DA:24,0
264 | LF:4
265 | LH:0
266 | end_of_record
267 | SF:lib/core/errors/failure.dart
268 | DA:4,2
269 | DA:5,2
270 | DA:20,3
271 | DA:21,3
272 | DA:22,3
273 | DA:24,3
274 | LF:6
275 | LH:6
276 | end_of_record
277 | SF:lib/data/repositories/genres_repository_impl.dart
278 | DA:19,1
279 | DA:26,1
280 | DA:27,3
281 | DA:28,0
282 | DA:29,3
283 | DA:31,3
284 | DA:32,2
285 | DA:33,1
286 | DA:34,1
287 | DA:35,1
288 | DA:36,2
289 | DA:40,3
290 | DA:41,1
291 | DA:42,1
292 | DA:43,1
293 | DA:44,2
294 | LF:16
295 | LH:15
296 | end_of_record
297 | SF:lib/data/repositories/movies_repository_impl.dart
298 | DA:20,1
299 | DA:30,1
300 | DA:35,1
301 | DA:36,0
302 | DA:37,1
303 | DA:38,0
304 | DA:39,3
305 | DA:41,3
306 | DA:46,4
307 | DA:47,1
308 | DA:48,1
309 | DA:49,1
310 | DA:50,2
311 | DA:54,3
312 | DA:55,2
313 | DA:57,1
314 | DA:58,1
315 | DA:59,1
316 | DA:60,2
317 | DA:66,1
318 | DA:70,3
319 | DA:72,3
320 | DA:76,1
321 | DA:77,1
322 | DA:78,2
323 | DA:81,2
324 | DA:85,1
325 | DA:89,3
326 | DA:91,3
327 | DA:95,1
328 | DA:96,1
329 | DA:97,2
330 | DA:100,2
331 | DA:104,0
332 | DA:108,0
333 | DA:110,0
334 | DA:114,0
335 | DA:115,0
336 | DA:116,0
337 | DA:119,0
338 | DA:122,1
339 | DA:123,1
340 | DA:124,0
341 | DA:126,2
342 | DA:130,1
343 | DA:131,1
344 | DA:132,0
345 | DA:133,0
346 | DA:135,1
347 | DA:136,2
348 | LF:50
349 | LH:38
350 | end_of_record
351 | SF:lib/domain/usecases/get_movie_detail.dart
352 | DA:11,1
353 | DA:14,1
354 | DA:15,5
355 | DA:23,2
356 | DA:25,1
357 | DA:26,3
358 | LF:6
359 | LH:6
360 | end_of_record
361 | SF:lib/core/usecases/usecase.dart
362 | DA:11,0
363 | DA:12,0
364 | LF:2
365 | LH:0
366 | end_of_record
367 | SF:lib/domain/usecases/get_movies.dart
368 | DA:14,1
369 | DA:17,1
370 | DA:18,3
371 | DA:19,1
372 | DA:20,1
373 | DA:21,1
374 | DA:31,2
375 | DA:37,1
376 | DA:38,4
377 | LF:9
378 | LH:9
379 | end_of_record
380 | SF:lib/domain/usecases/get_genres.dart
381 | DA:12,1
382 | DA:15,1
383 | DA:16,4
384 | DA:23,2
385 | DA:25,1
386 | DA:26,2
387 | LF:6
388 | LH:6
389 | end_of_record
390 | SF:lib/domain/usecases/search_movies.dart
391 | DA:13,1
392 | DA:16,1
393 | DA:17,5
394 | DA:26,2
395 | DA:28,1
396 | DA:29,3
397 | LF:6
398 | LH:6
399 | end_of_record
400 | SF:lib/presentation/genres/business_logic/genres_state.dart
401 | DA:4,1
402 | DA:5,1
403 | DA:14,1
404 | DA:16,1
405 | DA:17,2
406 | DA:22,1
407 | DA:24,1
408 | DA:25,2
409 | LF:8
410 | LH:8
411 | end_of_record
412 | SF:lib/presentation/genres/business_logic/genres_cubit.dart
413 | DA:15,1
414 | DA:17,0
415 | DA:18,2
416 | DA:20,1
417 | DA:23,2
418 | DA:24,4
419 | DA:25,1
420 | DA:26,1
421 | DA:27,3
422 | DA:28,2
423 | DA:33,1
424 | DA:34,1
425 | DA:35,1
426 | LF:13
427 | LH:12
428 | end_of_record
429 | SF:lib/presentation/movies/business_logic/movies_bloc/movies_state.dart
430 | DA:4,1
431 | DA:5,1
432 | DA:14,1
433 | DA:16,1
434 | DA:17,2
435 | DA:22,1
436 | DA:24,1
437 | DA:25,2
438 | LF:8
439 | LH:8
440 | end_of_record
441 | SF:lib/presentation/movies/business_logic/movies_bloc/movies_event.dart
442 | DA:4,0
443 | DA:5,0
444 | DA:13,1
445 | DA:15,0
446 | DA:16,0
447 | LF:5
448 | LH:1
449 | end_of_record
450 | SF:lib/presentation/movies/business_logic/movies_bloc/movies_bloc.dart
451 | DA:15,1
452 | DA:17,0
453 | DA:18,2
454 | DA:21,1
455 | DA:22,3
456 | DA:25,1
457 | DA:26,2
458 | DA:27,3
459 | DA:28,1
460 | DA:29,1
461 | DA:30,1
462 | DA:31,1
463 | DA:33,2
464 | DA:34,3
465 | DA:35,2
466 | LF:15
467 | LH:14
468 | end_of_record
469 | SF:lib/presentation/movies/business_logic/movie_details_cubit/movie_details_state.dart
470 | DA:4,1
471 | DA:5,1
472 | DA:12,1
473 | DA:14,1
474 | DA:15,2
475 | DA:20,1
476 | DA:22,1
477 | DA:23,2
478 | LF:8
479 | LH:8
480 | end_of_record
481 | SF:lib/presentation/movies/business_logic/movie_details_cubit/movie_details_cubit.dart
482 | DA:14,1
483 | DA:15,0
484 | DA:16,2
485 | DA:18,1
486 | DA:19,2
487 | DA:20,3
488 | DA:21,1
489 | DA:23,1
490 | DA:24,2
491 | DA:25,2
492 | DA:27,3
493 | LF:11
494 | LH:10
495 | end_of_record
496 | SF:lib/presentation/movies/business_logic/movies_search_cubit/movies_search_state.dart
497 | DA:4,1
498 | DA:5,1
499 | DA:14,1
500 | DA:16,1
501 | DA:17,2
502 | DA:22,1
503 | DA:24,1
504 | DA:25,2
505 | LF:8
506 | LH:8
507 | end_of_record
508 | SF:lib/presentation/movies/business_logic/movies_search_cubit/movies_search_cubit.dart
509 | DA:17,1
510 | DA:19,0
511 | DA:20,2
512 | DA:22,1
513 | DA:23,2
514 | DA:26,1
515 | DA:30,2
516 | DA:31,0
517 | DA:33,2
518 | DA:36,1
519 | DA:37,1
520 | DA:38,1
521 | DA:39,3
522 | DA:40,1
523 | DA:43,1
524 | DA:44,2
525 | DA:45,2
526 | DA:47,3
527 | LF:18
528 | LH:16
529 | end_of_record
530 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | end
36 |
37 | post_install do |installer|
38 | installer.pods_project.targets.each do |target|
39 | flutter_additional_ios_build_settings(target)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Flutter (1.0.0)
3 | - FMDB (2.7.5):
4 | - FMDB/standard (= 2.7.5)
5 | - FMDB/standard (2.7.5)
6 | - path_provider (0.0.1):
7 | - Flutter
8 | - shared_preferences (0.0.1):
9 | - Flutter
10 | - sqflite (0.0.2):
11 | - Flutter
12 | - FMDB (>= 2.7.5)
13 |
14 | DEPENDENCIES:
15 | - Flutter (from `Flutter`)
16 | - path_provider (from `.symlinks/plugins/path_provider/ios`)
17 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
18 | - sqflite (from `.symlinks/plugins/sqflite/ios`)
19 |
20 | SPEC REPOS:
21 | trunk:
22 | - FMDB
23 |
24 | EXTERNAL SOURCES:
25 | Flutter:
26 | :path: Flutter
27 | path_provider:
28 | :path: ".symlinks/plugins/path_provider/ios"
29 | shared_preferences:
30 | :path: ".symlinks/plugins/shared_preferences/ios"
31 | sqflite:
32 | :path: ".symlinks/plugins/sqflite/ios"
33 |
34 | SPEC CHECKSUMS:
35 | Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
36 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
37 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
38 | shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
39 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
40 |
41 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
42 |
43 | COCOAPODS: 1.10.1
44 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | Movies
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(FLUTTER_BUILD_NAME)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UIViewControllerBasedStatusBarAppearance
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/l10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: lib/l10n
2 | template-arb-file: app_en.arb
3 | output-localization-file: app_localizations.dart
4 |
--------------------------------------------------------------------------------
/lib/core/api/movies_api.dart:
--------------------------------------------------------------------------------
1 | class MoviesApi {
2 | static String _apikey = '4936b271d28ceb320ef9e012cf1363d7';
3 | static String _url = 'api.themoviedb.org';
4 |
5 | // languages
6 | static const String es = 'es-ES';
7 | static const String en = 'en-US';
8 |
9 | // Uris
10 | static Uri getGenres(String language) => Uri.https(
11 | _url, '3/genre/movie/list', {'api_key': _apikey, 'language': language});
12 |
13 | static Uri getMovies(String endpoint, String language, int genreId) =>
14 | Uri.https(_url, endpoint, _getParameters(language, genreId));
15 |
16 | static Uri searchMovies(String language, String query) => Uri.https(
17 | _url,
18 | '3/search/movie',
19 | {'api_key': _apikey, 'language': language, 'query': query});
20 |
21 | static Uri getMovieDetail(String language, int movieId) => Uri.https(
22 | _url, '3/movie/$movieId', {'api_key': _apikey, 'language': language});
23 |
24 | static Uri getMovieCast(String language, int movieId) => Uri.https(_url,
25 | '3/movie/$movieId/credits', {'api_key': _apikey, 'language': language});
26 |
27 | // Img / Posters
28 | static String getMoviePoster(String posterPath) {
29 | if (posterPath == null)
30 | return 'https://cdn11.bigcommerce.com/s-auu4kfi2d9/stencil/59512910-bb6d-0136-46ec-71c445b85d45/e/933395a0-cb1b-0135-a812-525400970412/icons/icon-no-image.svg';
31 | else
32 | return 'https://image.tmdb.org/t/p/w500/$posterPath';
33 | }
34 |
35 | static String getMovieBackgroundImg(String backdropPath) {
36 | if (backdropPath == null)
37 | return 'https://cdn11.bigcommerce.com/s-auu4kfi2d9/stencil/59512910-bb6d-0136-46ec-71c445b85d45/e/933395a0-cb1b-0135-a812-525400970412/icons/icon-no-image.svg';
38 | else
39 | return 'https://image.tmdb.org/t/p/w500/$backdropPath';
40 | }
41 |
42 | static Map
_getParameters(String language, int genreId) {
43 | var parameters = {'api_key': _apikey, 'language': language};
44 | if (genreId == null || genreId == -1)
45 | return parameters;
46 | else {
47 | parameters['with_genres'] = '$genreId';
48 | return parameters;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/core/api/movies_endpoint.dart:
--------------------------------------------------------------------------------
1 | class MoviesEndpoint {
2 | static const String withGenre = '3/discover/movie';
3 | }
4 |
--------------------------------------------------------------------------------
/lib/core/errors/exception.dart:
--------------------------------------------------------------------------------
1 | class ServerException implements Exception {}
2 |
3 | class CacheException implements Exception {}
4 |
--------------------------------------------------------------------------------
/lib/core/errors/failure.dart:
--------------------------------------------------------------------------------
1 | import 'package:equatable/equatable.dart';
2 |
3 | abstract class Failure extends Equatable {
4 | @override
5 | List get props => [];
6 | }
7 |
8 | class ServerFailure extends Failure {}
9 |
10 | class CacheFailure extends Failure {}
11 |
12 | class InternetFailure extends Failure {}
13 |
14 | class FailureMessage {
15 | static String server = 'Error de servidor';
16 | static String unexpected = 'Error inesperado D:';
17 | static String internet = 'Parece que no tienes internet :(';
18 | }
19 |
20 | String mapFailureToMessage(Failure failure) {
21 | switch (failure.runtimeType) {
22 | case InternetFailure:
23 | return FailureMessage.internet;
24 | case ServerFailure:
25 | return FailureMessage.server;
26 | default:
27 | return FailureMessage.unexpected;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/core/network/network_info.dart:
--------------------------------------------------------------------------------
1 | import 'package:data_connection_checker/data_connection_checker.dart';
2 |
3 | abstract class NetworkInfo {
4 | Future get isConnected;
5 | }
6 |
7 | class NetworkInfoImpl implements NetworkInfo {
8 | final DataConnectionChecker connectionChecker;
9 | NetworkInfoImpl(this.connectionChecker);
10 |
11 | @override
12 | Future get isConnected => connectionChecker.hasConnection;
13 | }
14 |
--------------------------------------------------------------------------------
/lib/core/routes.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 |
4 | import '../injection_container.dart';
5 | import '../presentation/custom_drawer/business_logic/drawer_nav_cubit/drawer_nav_cubit.dart';
6 | import '../presentation/movies/business_logic/appbar_search_mode_cubit.dart';
7 | import '../presentation/movies/business_logic/movie_cast_cubit/movie_cast_cubit.dart';
8 | import '../presentation/movies/business_logic/movie_details_cubit/movie_details_cubit.dart';
9 | import '../presentation/movies/movie_profile_view/movie_profile_view.dart';
10 | import '../presentation/index.dart';
11 |
12 | class AppRouter {
13 | Route onGenerateRoute(RouteSettings settings) {
14 | switch (settings.name) {
15 | case '/':
16 | return MaterialPageRoute(
17 | builder: (context) => MultiBlocProvider(
18 | providers: [
19 | BlocProvider(create: (context) => sl()),
20 | BlocProvider(create: (context) => sl()),
21 | ],
22 | child: Index(),
23 | ),
24 | );
25 | case '/movie_profile':
26 | return MaterialPageRoute(
27 | settings: settings,
28 | builder: (context) => MultiBlocProvider(
29 | providers: [
30 | BlocProvider(create: (context) => sl()),
31 | BlocProvider(create: (context) => sl()),
32 | ],
33 | child: MovieProfileView(),
34 | ),
35 | );
36 | default:
37 | return null;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/core/usecases/usecase.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:equatable/equatable.dart';
3 |
4 | import '../errors/failure.dart';
5 |
6 | abstract class UseCase {
7 | Future> call(Params params);
8 | }
9 |
10 | class NoParams extends Equatable {
11 | @override
12 | List get props => [];
13 | }
14 |
--------------------------------------------------------------------------------
/lib/data/datasources/genres/genres_local_data_source.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:shared_preferences/shared_preferences.dart';
5 |
6 | import '../../../core/errors/exception.dart';
7 | import '../../../domain/entities/genre.dart';
8 |
9 | abstract class GenresLocalDataSource {
10 | Future> getLastGenres();
11 | Future cacheGenres(List moviesToCache);
12 | }
13 |
14 | const CACHED_GENRES = 'CACHED_GENRES';
15 |
16 | class GenresLocalDataSourceImpl implements GenresLocalDataSource {
17 | final SharedPreferences sharedPreferences;
18 |
19 | GenresLocalDataSourceImpl({@required this.sharedPreferences});
20 |
21 | @override
22 | Future cacheGenres(List genresToCache) {
23 | return sharedPreferences.setString(
24 | CACHED_GENRES,
25 | json.encode(genreModelListToJsonList(genresToCache)),
26 | );
27 | }
28 |
29 | @override
30 | Future> getLastGenres() async {
31 | final jsonString = sharedPreferences.getString(CACHED_GENRES);
32 | if (jsonString != null) {
33 | return Future.value(genreModelListFromJsonList(json.decode(jsonString)));
34 | } else {
35 | throw CacheException();
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/data/datasources/genres/genres_remote_data_source.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:http/http.dart' as http;
5 |
6 | import '../../../core/api/movies_api.dart';
7 | import '../../../core/errors/exception.dart';
8 | import '../../../domain/entities/genre.dart';
9 |
10 | abstract class GenresRemoteDataSource {
11 | Future> getGenres(String language);
12 | }
13 |
14 | class GenresRemoteDataSourceImpl implements GenresRemoteDataSource {
15 | final http.Client client;
16 |
17 | GenresRemoteDataSourceImpl({@required this.client});
18 |
19 | @override
20 | Future> getGenres(String language) async {
21 | final response = await client.get(MoviesApi.getGenres(language));
22 | if (response.statusCode == 200)
23 | return genreModelListFromJsonList(json.decode(response.body)['genres']);
24 | else
25 | throw ServerException();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/data/datasources/movies/movies_local_data_source.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:shared_preferences/shared_preferences.dart';
5 |
6 | import '../../../core/errors/exception.dart';
7 | import '../../../domain/entities/movie.dart';
8 |
9 | abstract class MoviesLocalDataSource {
10 | Future> getLastMovies(String endpoint);
11 | Future cacheMovies(String endpoint, List moviesToCache);
12 | }
13 |
14 | class MoviesLocalDataSourceImpl implements MoviesLocalDataSource {
15 | final SharedPreferences sharedPreferences;
16 |
17 | MoviesLocalDataSourceImpl({@required this.sharedPreferences});
18 |
19 | @override
20 | Future cacheMovies(String endpoint, List moviesToCache) {
21 | return sharedPreferences.setString(
22 | endpoint,
23 | json.encode(movieModelListToJsonList(moviesToCache)),
24 | );
25 | }
26 |
27 | @override
28 | Future> getLastMovies(String endpoint) {
29 | final jsonString = sharedPreferences.getString(endpoint);
30 | if (jsonString != null) {
31 | return Future.value(movieModelListFromJsonList(json.decode(jsonString)));
32 | } else {
33 | throw CacheException();
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/data/datasources/movies/movies_remote_data_source.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:http/http.dart' as http;
5 |
6 | import '../../../core/api/movies_api.dart';
7 | import '../../../core/errors/exception.dart';
8 | import '../../../domain/entities/actor.dart';
9 | import '../../../domain/entities/movie.dart';
10 |
11 | abstract class MoviesRemoteDataSource {
12 | Future> getMovies(
13 | String endpoint,
14 | String language,
15 | int genreId,
16 | );
17 |
18 | Future> searchMovies(
19 | String language,
20 | String query,
21 | );
22 |
23 | Future getMovieDetail(String language, int movieId);
24 |
25 | Future> getMovieCast(String language, int movieId);
26 | }
27 |
28 | class MoviesRemoteDataSourceImpl implements MoviesRemoteDataSource {
29 | final http.Client client;
30 |
31 | MoviesRemoteDataSourceImpl({@required this.client});
32 |
33 | @override
34 | Future> getMovies(
35 | String endpoint,
36 | String language,
37 | int genreId,
38 | ) async {
39 | final response = await client.get(
40 | MoviesApi.getMovies(endpoint, language, genreId),
41 | );
42 | if (response.statusCode == 200)
43 | return movieModelListFromJsonList(json.decode(response.body)['results']);
44 | else {
45 | print(response.statusCode);
46 | print(response.body);
47 | throw ServerException();
48 | }
49 | }
50 |
51 | @override
52 | Future> searchMovies(String language, String query) async {
53 | final response = await client.get(MoviesApi.searchMovies(language, query));
54 | if (response.statusCode == 200) {
55 | return movieModelListFromJsonList(json.decode(response.body)['results']);
56 | } else
57 | throw ServerException();
58 | }
59 |
60 | @override
61 | Future getMovieDetail(String language, int movieId) async {
62 | final response = await client.get(
63 | MoviesApi.getMovieDetail(language, movieId),
64 | );
65 | if (response.statusCode == 200) {
66 | return Movie.fromJson(json.decode(response.body));
67 | } else
68 | throw ServerException();
69 | }
70 |
71 | @override
72 | Future> getMovieCast(String language, int movieId) async {
73 | final response = await client.get(
74 | MoviesApi.getMovieCast(language, movieId),
75 | );
76 | if (response.statusCode == 200) {
77 | return actorModelListFromJsonList(json.decode(response.body)['cast']);
78 | } else
79 | throw ServerException();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/data/repositories/genres_repository_impl.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import '../../core/errors/exception.dart';
5 | import '../../core/errors/failure.dart';
6 | import '../../core/network/network_info.dart';
7 | import '../../domain/entities/genre.dart';
8 | import '../../domain/repositories/genres_repository.dart';
9 | import '../datasources/genres/genres_local_data_source.dart';
10 | import '../datasources/genres/genres_remote_data_source.dart';
11 |
12 | class GenresRepositoryImpl extends GenresRepository {
13 | final GenresRemoteDataSource remoteDataSource;
14 | final GenresLocalDataSource localDataSource;
15 | final NetworkInfo networkInfo;
16 |
17 | List genres = [];
18 |
19 | GenresRepositoryImpl({
20 | @required this.remoteDataSource,
21 | @required this.localDataSource,
22 | @required this.networkInfo,
23 | });
24 |
25 | @override
26 | Future>> getGenres(String language) async {
27 | if (genres != null && genres.isNotEmpty) {
28 | return Right(genres);
29 | } else if (await networkInfo.isConnected) {
30 | try {
31 | final remoteGenres = await remoteDataSource.getGenres(language);
32 | localDataSource.cacheGenres(remoteGenres);
33 | genres = remoteGenres;
34 | return Right(remoteGenres);
35 | } on ServerException {
36 | return Left(ServerFailure());
37 | }
38 | } else {
39 | try {
40 | final localGenres = await localDataSource.getLastGenres();
41 | genres = localGenres;
42 | return Right(localGenres);
43 | } on CacheException {
44 | return Left(CacheFailure());
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/data/repositories/movies_repository_impl.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import '../../core/api/movies_endpoint.dart';
5 | import '../../core/errors/exception.dart';
6 | import '../../core/errors/failure.dart';
7 | import '../../core/network/network_info.dart';
8 | import '../../domain/entities/actor.dart';
9 | import '../../domain/entities/movie.dart';
10 | import '../../domain/repositories/movies_repository.dart';
11 | import '../datasources/movies/movies_local_data_source.dart';
12 | import '../datasources/movies/movies_remote_data_source.dart';
13 |
14 | class MoviesRepositoryImpl implements MoviesRepository {
15 | final MoviesRemoteDataSource remoteDataSource;
16 | final MoviesLocalDataSource localDataSource;
17 | final NetworkInfo networkInfo;
18 |
19 | MoviesRepositoryImpl({
20 | @required this.remoteDataSource,
21 | @required this.localDataSource,
22 | @required this.networkInfo,
23 | });
24 |
25 | Map> moviesByEnpoint = {};
26 | Map> moviesByCategory = {};
27 |
28 | @override
29 | Future>> getMovies(
30 | String endpoint,
31 | String language,
32 | int genreId,
33 | ) async {
34 | if (_isInMoviesByCategory(endpoint, genreId))
35 | return Right(moviesByCategory[genreId]);
36 | else if (_isInMoviesByEndpoint(endpoint))
37 | return Right(moviesByEnpoint[endpoint]);
38 | else if (await networkInfo.isConnected) {
39 | try {
40 | final remoteMovies = await remoteDataSource.getMovies(
41 | endpoint,
42 | language,
43 | genreId,
44 | );
45 | localDataSource.cacheMovies(endpoint + '$genreId', remoteMovies);
46 | _saveData(endpoint, genreId, remoteMovies);
47 | return Right(remoteMovies);
48 | } on ServerException {
49 | return Left(ServerFailure());
50 | }
51 | } else {
52 | try {
53 | final localMovies = await localDataSource.getLastMovies(
54 | endpoint + '$genreId',
55 | );
56 | _saveData(endpoint, genreId, localMovies);
57 | return Right(localMovies);
58 | } on CacheException {
59 | return Left(CacheFailure());
60 | }
61 | }
62 | }
63 |
64 | @override
65 | Future>> searchMovies(
66 | String language,
67 | String query,
68 | ) async {
69 | if (await networkInfo.isConnected)
70 | try {
71 | final remoteMovies = await remoteDataSource.searchMovies(
72 | language,
73 | query,
74 | );
75 | return Right(remoteMovies);
76 | } on ServerException {
77 | return Left(ServerFailure());
78 | }
79 | else
80 | return Left(InternetFailure());
81 | }
82 |
83 | @override
84 | Future> getMovieDetail(
85 | String language,
86 | int movieId,
87 | ) async {
88 | if (await networkInfo.isConnected)
89 | try {
90 | final remoteMovieDetail = await remoteDataSource.getMovieDetail(
91 | language,
92 | movieId,
93 | );
94 | return Right(remoteMovieDetail);
95 | } on ServerException {
96 | return Left(ServerFailure());
97 | }
98 | else
99 | return Left(InternetFailure());
100 | }
101 |
102 | @override
103 | Future>> getMovieCast(
104 | String language,
105 | int movieId,
106 | ) async {
107 | if (await networkInfo.isConnected)
108 | try {
109 | final remoteMovieDetail = await remoteDataSource.getMovieCast(
110 | language,
111 | movieId,
112 | );
113 | return Right(remoteMovieDetail);
114 | } on ServerException {
115 | return Left(ServerFailure());
116 | }
117 | else
118 | return Left(InternetFailure());
119 | }
120 |
121 | void _saveData(String endpoint, int genreId, List movies) {
122 | if (endpoint == MoviesEndpoint.withGenre) {
123 | moviesByCategory[genreId] = movies;
124 | } else {
125 | moviesByEnpoint[endpoint] = movies;
126 | }
127 | }
128 |
129 | bool _isInMoviesByCategory(String endpoint, int genreId) =>
130 | endpoint == MoviesEndpoint.withGenre &&
131 | moviesByCategory[genreId] != null &&
132 | moviesByCategory[genreId].isNotEmpty;
133 |
134 | bool _isInMoviesByEndpoint(String endpoint) =>
135 | moviesByEnpoint[endpoint] != null && moviesByEnpoint[endpoint].isNotEmpty;
136 | }
137 |
--------------------------------------------------------------------------------
/lib/domain/entities/actor.dart:
--------------------------------------------------------------------------------
1 | List actorModelListFromJsonList(List jsonList) {
2 | if (jsonList == null) return [];
3 |
4 | List cast = [];
5 | jsonList.forEach((item) {
6 | final actor = Actor.fromJson(item);
7 | cast.add(actor);
8 | });
9 | return cast;
10 | }
11 |
12 | class Actor {
13 | Actor({
14 | this.adult,
15 | this.gender,
16 | this.id,
17 | this.knownForDepartment,
18 | this.name,
19 | this.originalName,
20 | this.popularity,
21 | this.profilePath,
22 | this.castId,
23 | this.character,
24 | this.creditId,
25 | this.order,
26 | });
27 |
28 | bool adult;
29 | int gender;
30 | int id;
31 | String knownForDepartment;
32 | String name;
33 | String originalName;
34 | double popularity;
35 | String profilePath;
36 | int castId;
37 | String character;
38 | String creditId;
39 | int order;
40 |
41 | factory Actor.fromJson(Map json) => Actor(
42 | adult: json["adult"],
43 | gender: json["gender"],
44 | id: json["id"],
45 | knownForDepartment: json["known_for_department"],
46 | name: json["name"],
47 | originalName: json["original_name"],
48 | popularity: json["popularity"].toDouble(),
49 | profilePath: json["profile_path"],
50 | castId: json["cast_id"],
51 | character: json["character"],
52 | creditId: json["credit_id"],
53 | order: json["order"],
54 | );
55 |
56 | Map toJson() => {
57 | "adult": adult,
58 | "gender": gender,
59 | "id": id,
60 | "known_for_department": knownForDepartment,
61 | "name": name,
62 | "original_name": originalName,
63 | "popularity": popularity,
64 | "profile_path": profilePath,
65 | "cast_id": castId,
66 | "character": character,
67 | "credit_id": creditId,
68 | "order": order,
69 | };
70 |
71 | getFoto() {
72 | if (profilePath == null) {
73 | return 'http://forum.spaceengine.org/styles/se/theme/images/no_avatar.jpg';
74 | } else {
75 | return 'https://image.tmdb.org/t/p/w500/$profilePath';
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/lib/domain/entities/genre.dart:
--------------------------------------------------------------------------------
1 | List genreModelListFromJsonList(List jsonList) {
2 | if (jsonList == null) return [];
3 |
4 | List genres = [];
5 | jsonList.forEach((item) {
6 | final genre = Genre.fromJson(item);
7 | genres.add(genre);
8 | });
9 | return genres;
10 | }
11 |
12 | List> genreModelListToJsonList(List genres) {
13 | List> genresJson = [];
14 | genres.forEach((genre) {
15 | genresJson.add(genre.toJson());
16 | });
17 | return genresJson;
18 | }
19 |
20 | List> genreListToJsonList(List genres) {
21 | List> genresJson = [];
22 | genres.forEach((genre) {
23 | genresJson.add(genreToJson(genre));
24 | });
25 | return genresJson;
26 | }
27 |
28 | Map genreToJson(Genre genre) =>
29 | {"id": genre.id, "name": genre.name};
30 |
31 | class Genre {
32 | Genre({
33 | this.id,
34 | this.name,
35 | });
36 |
37 | int id;
38 | String name;
39 |
40 | factory Genre.fromJson(Map json) => Genre(
41 | id: json["id"],
42 | name: json["name"],
43 | );
44 |
45 | Map toJson() => {
46 | "id": id,
47 | "name": name,
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/lib/domain/entities/movie.dart:
--------------------------------------------------------------------------------
1 | import 'production_company.dart';
2 | import 'production_country.dart';
3 |
4 | List movieModelListFromJsonList(List jsonList) {
5 | if (jsonList == null) return [];
6 |
7 | List movies = [];
8 | jsonList.forEach((item) {
9 | final movie = Movie.fromJson(item);
10 | movies.add(movie);
11 | });
12 | return movies;
13 | }
14 |
15 | List> movieModelListToJsonList(List movies) {
16 | List> moviesJson = [];
17 | movies.forEach((movie) {
18 | moviesJson.add(movie.toJson());
19 | });
20 | return moviesJson;
21 | }
22 |
23 | class Movie {
24 | int id;
25 | String title;
26 | String homepage;
27 | bool adult;
28 | String backdropPath;
29 | List genreIds;
30 | String originalLanguage;
31 | String originalTitle;
32 | String overview;
33 | double popularity;
34 | String posterPath;
35 | DateTime releaseDate;
36 | bool video;
37 | double voteAverage;
38 | int voteCount;
39 | int budget;
40 | int revenue;
41 | List productionCompanies;
42 | List productionCountries;
43 |
44 | Movie({
45 | this.id,
46 | this.title,
47 | this.homepage,
48 | this.adult,
49 | this.backdropPath,
50 | this.genreIds,
51 | this.originalLanguage,
52 | this.originalTitle,
53 | this.overview,
54 | this.popularity,
55 | this.posterPath,
56 | this.releaseDate,
57 | this.video,
58 | this.voteAverage,
59 | this.voteCount,
60 | this.budget,
61 | this.revenue,
62 | this.productionCompanies,
63 | this.productionCountries,
64 | });
65 |
66 | factory Movie.fromJson(Map json) => Movie(
67 | adult: json["adult"],
68 | backdropPath: json["backdrop_path"],
69 | genreIds: json["genre_ids"] != null
70 | ? List.from(json["genre_ids"].map((x) => x))
71 | : [],
72 | id: json["id"],
73 | originalLanguage: json["original_language"],
74 | originalTitle: json["original_title"],
75 | overview: json["overview"],
76 | popularity: json["popularity"].toDouble(),
77 | posterPath: json["poster_path"],
78 | releaseDate: json["release_date"] != null && json["release_date"] != ""
79 | ? DateTime.parse(json["release_date"])
80 | : null,
81 | title: json["title"],
82 | video: json["video"],
83 | voteAverage: json["vote_average"].toDouble(),
84 | voteCount: json["vote_count"],
85 | budget: json["budget"],
86 | revenue: json["revenue"],
87 | homepage: json["homepage"],
88 | productionCompanies: json["production_companies"] != null
89 | ? List.from(json["production_companies"]
90 | .map((x) => ProductionCompany.fromJson(x)))
91 | : [],
92 | productionCountries: json["production_countries"] != null
93 | ? List.from(json["production_countries"]
94 | .map((x) => ProductionCountry.fromJson(x)))
95 | : [],
96 | );
97 |
98 | Map toJson() => {
99 | "adult": adult,
100 | "backdrop_path": backdropPath,
101 | "genre_ids": List.from(genreIds.map((x) => x)),
102 | "id": id,
103 | "original_language": originalLanguage,
104 | "original_title": originalTitle,
105 | "overview": overview,
106 | "popularity": popularity,
107 | "poster_path": posterPath,
108 | "release_date": releaseDate == null
109 | ? null
110 | : "${releaseDate.year.toString().padLeft(4, '0')}-${releaseDate.month.toString().padLeft(2, '0')}-${releaseDate.day.toString().padLeft(2, '0')}",
111 | "title": title,
112 | "video": video,
113 | "vote_average": voteAverage,
114 | "vote_count": voteCount,
115 | "budget": budget,
116 | "homepage": homepage,
117 | "production_companies":
118 | List.from(productionCompanies.map((x) => x.toJson())),
119 | "production_countries":
120 | List.from(productionCountries.map((x) => x.toJson())),
121 | };
122 | }
123 |
--------------------------------------------------------------------------------
/lib/domain/entities/production_company.dart:
--------------------------------------------------------------------------------
1 | class ProductionCompany {
2 | ProductionCompany({
3 | this.id,
4 | this.logoPath,
5 | this.name,
6 | this.originCountry,
7 | });
8 |
9 | int id;
10 | String logoPath;
11 | String name;
12 | String originCountry;
13 |
14 | factory ProductionCompany.fromJson(Map json) =>
15 | ProductionCompany(
16 | id: json["id"],
17 | logoPath: json["logo_path"],
18 | name: json["name"],
19 | originCountry: json["origin_country"],
20 | );
21 |
22 | Map toJson() => {
23 | "id": id,
24 | "logo_path": logoPath,
25 | "name": name,
26 | "origin_country": originCountry,
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/lib/domain/entities/production_country.dart:
--------------------------------------------------------------------------------
1 | class ProductionCountry {
2 | ProductionCountry({
3 | this.iso31661,
4 | this.name,
5 | });
6 |
7 | String iso31661;
8 | String name;
9 |
10 | factory ProductionCountry.fromJson(Map json) =>
11 | ProductionCountry(
12 | iso31661: json["iso_3166_1"],
13 | name: json["name"],
14 | );
15 |
16 | Map toJson() => {
17 | "iso_3166_1": iso31661,
18 | "name": name,
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/lib/domain/repositories/genres_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 |
3 | import '../../core/errors/failure.dart';
4 | import '../entities/genre.dart';
5 |
6 | abstract class GenresRepository {
7 | Future>> getGenres(String language);
8 | }
9 |
--------------------------------------------------------------------------------
/lib/domain/repositories/movies_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 |
3 | import '../../core/errors/failure.dart';
4 | import '../entities/actor.dart';
5 | import '../entities/movie.dart';
6 |
7 | abstract class MoviesRepository {
8 | Future>> getMovies(
9 | String endpoint,
10 | String language,
11 | int genreId,
12 | );
13 |
14 | Future>> searchMovies(
15 | String language,
16 | String query,
17 | );
18 |
19 | Future> getMovieDetail(
20 | String language,
21 | int movieId,
22 | );
23 |
24 | Future>> getMovieCast(
25 | String language,
26 | int movieId,
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/lib/domain/usecases/get_genres.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:equatable/equatable.dart';
3 |
4 | import '../../core/errors/failure.dart';
5 | import '../../core/usecases/usecase.dart';
6 | import '../entities/genre.dart';
7 | import '../repositories/genres_repository.dart';
8 |
9 | class GetGenres extends UseCase, GenresParams> {
10 | final GenresRepository repository;
11 |
12 | GetGenres(this.repository);
13 |
14 | @override
15 | Future>> call(params) async {
16 | return await repository.getGenres(params.language);
17 | }
18 | }
19 |
20 | class GenresParams extends Equatable {
21 | final String language;
22 |
23 | GenresParams({this.language});
24 |
25 | @override
26 | List get props => [language];
27 | }
28 |
--------------------------------------------------------------------------------
/lib/domain/usecases/get_movie_cast.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:equatable/equatable.dart';
3 |
4 | import '../../core/errors/failure.dart';
5 | import '../../core/usecases/usecase.dart';
6 | import '../entities/actor.dart';
7 | import '../repositories/movies_repository.dart';
8 |
9 | class GetMovieCast extends UseCase, CastParams> {
10 | final MoviesRepository repository;
11 |
12 | GetMovieCast(this.repository);
13 |
14 | @override
15 | Future>> call(params) async {
16 | return await repository.getMovieCast(params.language, params.movieId);
17 | }
18 | }
19 |
20 | class CastParams extends Equatable {
21 | final String language;
22 | final int movieId;
23 |
24 | CastParams({this.language, this.movieId});
25 |
26 | @override
27 | List get props => [language, movieId];
28 | }
29 |
--------------------------------------------------------------------------------
/lib/domain/usecases/get_movie_detail.dart:
--------------------------------------------------------------------------------
1 | import 'package:equatable/equatable.dart';
2 | import 'package:my_movie_list/core/errors/failure.dart';
3 | import 'package:dartz/dartz.dart';
4 | import 'package:my_movie_list/core/usecases/usecase.dart';
5 | import 'package:my_movie_list/domain/entities/movie.dart';
6 | import 'package:my_movie_list/domain/repositories/movies_repository.dart';
7 |
8 | class GetMovieDetail extends UseCase {
9 | final MoviesRepository repository;
10 |
11 | GetMovieDetail(this.repository);
12 |
13 | @override
14 | Future> call(params) async {
15 | return await repository.getMovieDetail(params.language, params.movieId);
16 | }
17 | }
18 |
19 | class MovieDetailParams extends Equatable {
20 | final String language;
21 | final int movieId;
22 |
23 | MovieDetailParams({this.language, this.movieId});
24 |
25 | @override
26 | List get props => [language, movieId];
27 | }
28 |
--------------------------------------------------------------------------------
/lib/domain/usecases/get_movies.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:equatable/equatable.dart';
3 |
4 | import '../../core/errors/failure.dart';
5 | import '../../core/api/movies_api.dart';
6 | import '../../core/api/movies_endpoint.dart';
7 | import '../../core/usecases/usecase.dart';
8 | import '../entities/movie.dart';
9 | import '../repositories/movies_repository.dart';
10 |
11 | class GetMovies extends UseCase, Params> {
12 | final MoviesRepository repository;
13 |
14 | GetMovies(this.repository);
15 |
16 | @override
17 | Future>> call(Params params) async {
18 | return await repository.getMovies(
19 | params.endpoint,
20 | params.language,
21 | params.genreId,
22 | );
23 | }
24 | }
25 |
26 | class Params extends Equatable {
27 | final String language;
28 | final String endpoint;
29 | final int genreId;
30 |
31 | Params({
32 | this.endpoint = MoviesEndpoint.withGenre,
33 | this.language = MoviesApi.es,
34 | this.genreId,
35 | });
36 |
37 | @override
38 | List get props => [this.endpoint, this.language, this.genreId];
39 | }
40 |
--------------------------------------------------------------------------------
/lib/domain/usecases/search_movies.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:equatable/equatable.dart';
3 |
4 | import '../../core/errors/failure.dart';
5 | import '../../core/api/movies_api.dart';
6 | import '../../core/usecases/usecase.dart';
7 | import '../entities/movie.dart';
8 | import '../repositories/movies_repository.dart';
9 |
10 | class SearchMovies extends UseCase, SearchMoviesParams> {
11 | final MoviesRepository repository;
12 |
13 | SearchMovies(this.repository);
14 |
15 | @override
16 | Future>> call(SearchMoviesParams params) async {
17 | var res = await repository.searchMovies(params.language, params.query);
18 | return res;
19 | }
20 | }
21 |
22 | class SearchMoviesParams extends Equatable {
23 | final String language;
24 | final String query;
25 |
26 | SearchMoviesParams({this.language = MoviesApi.es, this.query});
27 |
28 | @override
29 | List get props => [this.language, this.query];
30 | }
31 |
--------------------------------------------------------------------------------
/lib/injection_container.dart:
--------------------------------------------------------------------------------
1 | import 'package:get_it/get_it.dart';
2 | import 'package:http/http.dart' as http;
3 | import 'package:shared_preferences/shared_preferences.dart';
4 | import 'package:data_connection_checker/data_connection_checker.dart';
5 |
6 | import 'core/network/network_info.dart';
7 | import 'data/datasources/genres/genres_local_data_source.dart';
8 | import 'data/datasources/genres/genres_remote_data_source.dart';
9 | import 'data/datasources/movies/movies_local_data_source.dart';
10 | import 'data/datasources/movies/movies_remote_data_source.dart';
11 | import 'data/repositories/genres_repository_impl.dart';
12 | import 'data/repositories/movies_repository_impl.dart';
13 | import 'domain/repositories/genres_repository.dart';
14 | import 'domain/repositories/movies_repository.dart';
15 | import 'domain/usecases/get_genres.dart';
16 | import 'domain/usecases/get_movie_cast.dart';
17 | import 'domain/usecases/get_movie_detail.dart';
18 | import 'domain/usecases/get_movies.dart';
19 | import 'domain/usecases/search_movies.dart';
20 | import 'presentation/custom_drawer/business_logic/drawer_nav_cubit/drawer_nav_cubit.dart';
21 | import 'presentation/genres/business_logic/genres_cubit.dart';
22 | import 'presentation/movies/business_logic/appbar_search_mode_cubit.dart';
23 | import 'presentation/movies/business_logic/movie_cast_cubit/movie_cast_cubit.dart';
24 | import 'presentation/movies/business_logic/movies_bloc/movies_bloc.dart';
25 | import 'presentation/movies/business_logic/movies_search_cubit/movies_search_cubit.dart';
26 | import 'presentation/movies/business_logic/movie_details_cubit/movie_details_cubit.dart';
27 |
28 | final sl = GetIt.instance;
29 |
30 | Future init() async {
31 | //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
32 | //! MOVIES
33 | //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
34 | // Business Logic
35 | sl.registerFactory(() => DrawerNavCubit());
36 | sl.registerFactory(() => MoviesBloc(getMovies: sl()));
37 | sl.registerFactory(() => AppbarSearhModeCubit());
38 | sl.registerFactory(() => MoviesSearchCubit(searchMovies: sl()));
39 | sl.registerFactory(() => MovieDetailsCubit(getMovieDetail: sl()));
40 | sl.registerFactory(() => MovieCastCubit(getMovieCast: sl()));
41 |
42 | // UseCases
43 | sl.registerLazySingleton(() => GetMovies(sl()));
44 | sl.registerLazySingleton(() => SearchMovies(sl()));
45 | sl.registerLazySingleton(() => GetMovieDetail(sl()));
46 | sl.registerLazySingleton(() => GetMovieCast(sl()));
47 |
48 | // Repository
49 | sl.registerLazySingleton(
50 | () => MoviesRepositoryImpl(
51 | networkInfo: sl(),
52 | localDataSource: sl(),
53 | remoteDataSource: sl(),
54 | ),
55 | );
56 |
57 | // DataSources
58 | sl.registerLazySingleton(
59 | () => MoviesRemoteDataSourceImpl(client: sl()),
60 | );
61 | sl.registerLazySingleton(
62 | () => MoviesLocalDataSourceImpl(sharedPreferences: sl()),
63 | );
64 |
65 | //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
66 | //! Genres
67 | //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
68 | // Business Logic
69 | sl.registerFactory(() => GenresCubit(getGenres: sl()));
70 |
71 | // UseCases
72 | sl.registerLazySingleton(() => GetGenres(sl()));
73 |
74 | // Repository
75 | sl.registerLazySingleton(
76 | () => GenresRepositoryImpl(
77 | remoteDataSource: sl(),
78 | localDataSource: sl(),
79 | networkInfo: sl(),
80 | ),
81 | );
82 |
83 | // DataSources
84 | sl.registerLazySingleton(
85 | () => GenresLocalDataSourceImpl(sharedPreferences: sl()),
86 | );
87 |
88 | sl.registerLazySingleton(
89 | () => GenresRemoteDataSourceImpl(client: sl()),
90 | );
91 |
92 | //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
93 | //! CORE
94 | //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
95 | sl.registerLazySingleton(() => NetworkInfoImpl(sl()));
96 |
97 | //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
98 | //! EXTERNAL
99 | //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
100 | // shared preferences
101 | final sharedPreferences = await SharedPreferences.getInstance();
102 | sl.registerLazySingleton(() => sharedPreferences);
103 |
104 | // http
105 | sl.registerLazySingleton(() => http.Client());
106 |
107 | // connection checker
108 | sl.registerLazySingleton(() => DataConnectionChecker());
109 | }
110 |
--------------------------------------------------------------------------------
/lib/l10n/I10n.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../core/api/movies_api.dart';
4 |
5 | class L10n {
6 | static const en = 'en';
7 | static const es = 'es';
8 |
9 | static final all = [
10 | const Locale(en),
11 | const Locale(es),
12 | ];
13 |
14 | static final getLang = {
15 | en: MoviesApi.en,
16 | es: MoviesApi.es,
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/lib/l10n/app_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "loading_genres": "Loading Movie Genres...",
3 | "loading_movies": "Loading Movies...",
4 | "drawer_categories": "Categories",
5 | "no_info" : "No Information",
6 | "search_movies_searching" : "Looking for movies...",
7 | "search_movies_not_found" : "No matching movies found :(",
8 | "search_movies_error" : "It seems that there has been an error, try again!",
9 | "movie_profile_original_title": "Original Title",
10 | "movie_profile_assessment": "Score",
11 | "movie_profile_genres" : "Genres",
12 | "movie_profile_overview": "Overview",
13 | "movie_profile_release_date": "Release Date",
14 | "movie_profile_budget" : "Budget",
15 | "movie_profile_revenue" : "Revenue",
16 | "movie_profile_production_compenies" : "Production Companies",
17 | "movie_profile_production_countries" : "Production Countries",
18 | "movie_profile_cast": "Cast"
19 |
20 | }
--------------------------------------------------------------------------------
/lib/l10n/app_es.arb:
--------------------------------------------------------------------------------
1 | {
2 | "loading_genres": "Cargando Géneros De Películas...",
3 | "loading_movies": "Cargando Películas...",
4 | "drawer_categories": "Categorías",
5 | "no_info" : "Sin Información",
6 | "search_movies_searching" : "Buscando Películas...",
7 | "search_movies_not_found" : "No se han encontrado películas que coincidan :(",
8 | "search_movies_error" : "Parece que ha habido un error, intente otra vez!",
9 | "movie_profile_original_title": "Original Title",
10 | "movie_profile_assessment": "Valoración",
11 | "movie_profile_genres" : "Géneros",
12 | "movie_profile_overview": "Sinopsis",
13 | "movie_profile_release_date": "Fecha De Lanzamiento",
14 | "movie_profile_budget" : "Presupuesto",
15 | "movie_profile_revenue" : "Ingresos",
16 | "movie_profile_production_compenies" : "Compañias De Producción",
17 | "movie_profile_production_countries" : "Países De Producción",
18 | "movie_profile_cast": "Reparto"
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
4 | import 'package:flutter_localizations/flutter_localizations.dart';
5 |
6 | import 'core/routes.dart';
7 | import 'injection_container.dart' as di;
8 | import 'l10n/I10n.dart';
9 | import 'presentation/genres/business_logic/genres_cubit.dart';
10 | import 'presentation/movies/business_logic/movies_bloc/movies_bloc.dart';
11 | import 'presentation/movies/business_logic/movies_search_cubit/movies_search_cubit.dart';
12 |
13 | void main() async {
14 | WidgetsFlutterBinding.ensureInitialized();
15 | await di.init();
16 | runApp(MyApp());
17 | }
18 |
19 | class MyApp extends StatelessWidget {
20 | final _router = AppRouter();
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return MultiBlocProvider(
25 | providers: [
26 | BlocProvider(create: (context) => di.sl()),
27 | BlocProvider(create: (context) => di.sl()),
28 | BlocProvider(create: (context) => di.sl()),
29 | ],
30 | child: MaterialApp(
31 | title: 'Movies',
32 | debugShowCheckedModeBanner: false,
33 | localizationsDelegates: [
34 | AppLocalizations.delegate,
35 | GlobalMaterialLocalizations.delegate,
36 | GlobalWidgetsLocalizations.delegate,
37 | GlobalCupertinoLocalizations.delegate,
38 | ],
39 | supportedLocales: L10n.all,
40 | theme: ThemeData(
41 | primaryColor: Colors.black,
42 | accentColor: Colors.black,
43 | ),
44 | onGenerateRoute: _router.onGenerateRoute,
45 | initialRoute: '/',
46 | ),
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/presentation/custom_drawer/business_logic/drawer_nav_cubit/drawer_nav_cubit.dart:
--------------------------------------------------------------------------------
1 | import 'dart:developer' as logger;
2 | import 'package:equatable/equatable.dart';
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 | import 'package:my_movie_list/domain/entities/genre.dart';
5 |
6 | part 'drawer_nav_state.dart';
7 |
8 | class DrawerNavCubit extends Cubit {
9 | DrawerNavCubit() : super(DrawerNavInitial());
10 |
11 | @override
12 | void onChange(Change change) {
13 | logger.log('3.1.2: genre => $change');
14 | super.onChange(change);
15 | }
16 |
17 | @override
18 | void onError(Object error, StackTrace stackTrace) {
19 | logger.log('3.1.3: $error, $stackTrace');
20 | super.onError(error, stackTrace);
21 | }
22 |
23 | Future getWithGenre(Genre genre) async {
24 | logger.log('3.1.1: genre => ${genre.name}');
25 | emit(DrawerNavGenre(genre));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/presentation/custom_drawer/business_logic/drawer_nav_cubit/drawer_nav_state.dart:
--------------------------------------------------------------------------------
1 | part of 'drawer_nav_cubit.dart';
2 |
3 | abstract class DrawerNavState extends Equatable {
4 | @override
5 | List get props => [];
6 | }
7 |
8 | class DrawerNavInitial extends DrawerNavState {}
9 |
10 | class DrawerNavGenre extends DrawerNavState {
11 | final Genre genre;
12 | DrawerNavGenre(this.genre);
13 |
14 | @override
15 | List get props => [genre];
16 | }
17 |
--------------------------------------------------------------------------------
/lib/presentation/custom_drawer/custom_drawer.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'drawer_categories.dart';
4 |
5 | class CustomDrawer extends StatelessWidget {
6 | @override
7 | Widget build(BuildContext context) {
8 | return SafeArea(
9 | child: Container(
10 | width: MediaQuery.of(context).size.width * 0.8,
11 | decoration: BoxDecoration(
12 | color: Colors.grey[300],
13 | borderRadius: BorderRadius.only(
14 | topRight: Radius.circular(20),
15 | bottomRight: Radius.circular(20),
16 | ),
17 | ),
18 | child: Container(
19 | padding: EdgeInsets.symmetric(horizontal: 20, vertical: 60),
20 | child: Column(
21 | crossAxisAlignment: CrossAxisAlignment.start,
22 | children: [
23 | Row(
24 | children: [
25 | _drawerTitle('Categorías'),
26 | Expanded(child: Container()),
27 | ],
28 | ),
29 | SizedBox(height: 30),
30 | Expanded(child: DrawerCategories()),
31 | ],
32 | ),
33 | ),
34 | ),
35 | );
36 | }
37 |
38 | Widget _drawerTitle(String text) {
39 | return Text(
40 | text,
41 | style: TextStyle(
42 | fontSize: 20,
43 | fontWeight: FontWeight.bold,
44 | ),
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/presentation/custom_drawer/drawer_categories.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 |
4 | import '../genres/business_logic/genres_cubit.dart';
5 | import 'business_logic/drawer_nav_cubit/drawer_nav_cubit.dart';
6 | import 'drawer_category_button.dart';
7 |
8 | class DrawerCategories extends StatelessWidget {
9 | @override
10 | Widget build(BuildContext context) {
11 | return BlocBuilder(
12 | builder: (context, state) {
13 | if (state is GenresLoadSuccess)
14 | return ListView.builder(
15 | itemCount: state.genres.length,
16 | shrinkWrap: true,
17 | itemBuilder: (context, index) {
18 | return Container(
19 | margin: EdgeInsets.only(right: 30),
20 | child: DrawerCategoryButton(
21 | title: state.genres[index].name,
22 | function: () {
23 | Navigator.of(context).pop();
24 | BlocProvider.of(context)
25 | .getWithGenre(state.genres[index]);
26 | },
27 | ),
28 | );
29 | },
30 | );
31 | else if (state is GenresLoadFailure)
32 | return Container(
33 | padding: EdgeInsets.only(top: 20, left: 20),
34 | child: Text(state.message),
35 | );
36 | else
37 | return Container(
38 | padding: EdgeInsets.only(top: 20, left: 20),
39 | child: Column(
40 | children: [
41 | Row(
42 | children: [
43 | CircularProgressIndicator(),
44 | Expanded(child: Container()),
45 | ],
46 | ),
47 | Expanded(child: Container()),
48 | ],
49 | ),
50 | );
51 | },
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/presentation/custom_drawer/drawer_category_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class DrawerCategoryButton extends StatelessWidget {
4 | final String title;
5 | final void Function() function;
6 | const DrawerCategoryButton({
7 | @required this.title,
8 | @required this.function,
9 | });
10 | @override
11 | Widget build(BuildContext context) {
12 | return Material(
13 | color: Colors.transparent,
14 | child: InkWell(
15 | onTap: function,
16 | child: Container(
17 | padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
18 | child: Row(
19 | children: [
20 | SizedBox(width: 15),
21 | Text(
22 | title,
23 | style: TextStyle(
24 | fontWeight: FontWeight.w600,
25 | fontSize: 15,
26 | ),
27 | ),
28 | Expanded(child: Container()),
29 | Icon(Icons.arrow_right),
30 | ],
31 | ),
32 | ),
33 | ),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/presentation/genres/business_logic/genres_cubit.dart:
--------------------------------------------------------------------------------
1 | import 'package:equatable/equatable.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 |
5 | import '../../../core/errors/failure.dart';
6 | import '../../../core/api/movies_api.dart';
7 | import '../../../domain/entities/genre.dart';
8 | import '../../../domain/usecases/get_genres.dart';
9 |
10 | part 'genres_state.dart';
11 |
12 | class GenresCubit extends Cubit {
13 | final GetGenres getGenres;
14 |
15 | GenresCubit({
16 | @required this.getGenres,
17 | }) : assert(getGenres != null),
18 | super(GenresInitial());
19 |
20 | Future genresGet({
21 | String language = MoviesApi.es,
22 | }) async {
23 | emit(GenresLoadInProgress());
24 | final failureOrGenres = await getGenres(GenresParams(language: language));
25 | emit(
26 | failureOrGenres.fold(
27 | (failure) => GenresLoadFailure(message: _mapFailureToMessage(failure)),
28 | (genres) => GenresLoadSuccess(genres: genres),
29 | ),
30 | );
31 | }
32 |
33 | String _mapFailureToMessage(Failure failure) {
34 | switch (failure.runtimeType) {
35 | case ServerFailure:
36 | return FailureMessage.server;
37 | default:
38 | return FailureMessage.unexpected;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/presentation/genres/business_logic/genres_state.dart:
--------------------------------------------------------------------------------
1 | part of 'genres_cubit.dart';
2 |
3 | abstract class GenresState extends Equatable {
4 | @override
5 | List get props => [];
6 | }
7 |
8 | class GenresInitial extends GenresState {}
9 |
10 | class GenresLoadInProgress extends GenresState {}
11 |
12 | class GenresLoadSuccess extends GenresState {
13 | final List genres;
14 | GenresLoadSuccess({@required this.genres});
15 |
16 | @override
17 | List get props => [this.genres];
18 | }
19 |
20 | class GenresLoadFailure extends GenresState {
21 | final String message;
22 | GenresLoadFailure({@required this.message});
23 |
24 | @override
25 | List get props => [this.message];
26 | }
27 |
--------------------------------------------------------------------------------
/lib/presentation/genres/genres_view/genres_loading_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 |
4 | class GenresLoadingView extends StatelessWidget {
5 | @override
6 | Widget build(BuildContext context) {
7 | String loadingGenres = AppLocalizations.of(context).loading_genres;
8 |
9 | return Scaffold(
10 | backgroundColor: Colors.black,
11 | body: Center(
12 | child: Column(
13 | mainAxisAlignment: MainAxisAlignment.center,
14 | children: [
15 | Expanded(flex: 30, child: Container()),
16 | Expanded(
17 | flex: 15,
18 | child: Container(
19 | child: Text(
20 | loadingGenres,
21 | maxLines: 2,
22 | textAlign: TextAlign.center,
23 | softWrap: true,
24 | style: TextStyle(
25 | color: Colors.white,
26 | fontSize: 20,
27 | fontWeight: FontWeight.bold,
28 | ),
29 | ),
30 | ),
31 | ),
32 | Expanded(flex: 1, child: Container()),
33 | Container(
34 | width: 60,
35 | height: 60,
36 | child: CircularProgressIndicator(
37 | strokeWidth: 10,
38 | valueColor: new AlwaysStoppedAnimation(Colors.orange),
39 | ),
40 | ),
41 | Expanded(flex: 15, child: Container()),
42 | ],
43 | ),
44 | ),
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/presentation/global_widgets/dialogs/on_will_pop_dialog.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class OnWillPopDialog extends StatelessWidget {
4 | @override
5 | Widget build(BuildContext context) {
6 | return AlertDialog(
7 | title: Text(
8 | '¿Estás seguro que deseas salir?',
9 | textAlign: TextAlign.center,
10 | ),
11 | actionsOverflowButtonSpacing: 10,
12 | content: Row(
13 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
14 | children: [
15 | ElevatedButton(
16 | style: ButtonStyle(
17 | backgroundColor: MaterialStateProperty.all(
18 | Colors.grey[900],
19 | ),
20 | ),
21 | child: Container(
22 | padding: EdgeInsets.symmetric(horizontal: 15),
23 | child: Text('Salir'),
24 | ),
25 | onPressed: () => Navigator.of(context).pop(true)),
26 | ElevatedButton(
27 | style: ButtonStyle(
28 | backgroundColor: MaterialStateProperty.all(
29 | Colors.grey[900],
30 | ),
31 | ),
32 | child: Text('Cancelar'),
33 | onPressed: () => Navigator.of(context).pop(false)),
34 | ],
35 | ),
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/presentation/index.dart:
--------------------------------------------------------------------------------
1 | import 'dart:developer' as logger;
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_bloc/flutter_bloc.dart';
5 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
6 |
7 | import '../core/api/movies_endpoint.dart';
8 | import '../domain/entities/genre.dart';
9 | import '../l10n/I10n.dart';
10 | import 'custom_drawer/business_logic/drawer_nav_cubit/drawer_nav_cubit.dart';
11 | import 'genres/business_logic/genres_cubit.dart';
12 | import 'genres/genres_view/genres_loading_view.dart';
13 | import 'global_widgets/dialogs/on_will_pop_dialog.dart';
14 | import 'movies/business_logic/movies_bloc/movies_bloc.dart';
15 | import 'movies/movies_view/movies_view.dart';
16 |
17 | class Index extends StatelessWidget {
18 | @override
19 | Widget build(BuildContext context) {
20 | logger.log('1');
21 | return WillPopScope(
22 | onWillPop: () async => showDialog(
23 | context: context,
24 | builder: (context) => OnWillPopDialog(),
25 | ),
26 | child: BlocBuilder(
27 | builder: (context, genresState) {
28 | if (genresState is GenresInitial) {
29 | _initGenres(context);
30 | return GenresLoadingView();
31 | } else if (genresState is GenresLoadInProgress) {
32 | return GenresLoadingView();
33 | } else if (genresState is GenresLoadFailure) {
34 | return _genresLoadFailure(context, genresState.message);
35 | } else if (genresState is GenresLoadSuccess) {
36 | return _genresLoadSuccess(context, genresState.genres);
37 | // return _moviesWithGenre(context, genresState.genres[0]);
38 | } else {
39 | return null;
40 | }
41 | },
42 | ),
43 | );
44 | }
45 |
46 | void _initGenres(BuildContext context) {
47 | logger.log('2');
48 | String language = L10n.getLang[AppLocalizations.of(context).localeName];
49 | BlocProvider.of(context).genresGet(language: language);
50 | }
51 |
52 | Widget _genresLoadSuccess(BuildContext context, List genres) {
53 | logger.log('3');
54 | return BlocBuilder(
55 | builder: (context, state) {
56 | logger.log('3.0.1: state => $state');
57 | if (state is DrawerNavInitial) {
58 | logger.log('3.1: genre => ${genres[0].name}');
59 | BlocProvider.of(context).getWithGenre(genres[0]);
60 | return _moviesWithGenre(context, genres[0]);
61 | } else if (state is DrawerNavGenre) {
62 | logger.log('3.2');
63 | return _moviesWithGenre(context, state.genre);
64 | } else {
65 | return null;
66 | }
67 | },
68 | );
69 | }
70 |
71 | Widget _moviesWithGenre(BuildContext context, Genre genre) {
72 | logger.log('4');
73 | String endpoint = MoviesEndpoint.withGenre;
74 | String language = L10n.getLang[AppLocalizations.of(context).localeName];
75 | String title = genre.name;
76 |
77 | BlocProvider.of(context).add(
78 | MoviesGet(endpoint: endpoint, language: language, genre: genre.id),
79 | );
80 | return MoviesView(title: title, endpoint: endpoint, language: language);
81 | }
82 |
83 | Widget _genresLoadFailure(BuildContext context, String message) {
84 | logger.log('5');
85 | showDialog(
86 | context: context,
87 | builder: (context) => AlertDialog(
88 | title: Text('Error'),
89 | content: Text(message),
90 | ),
91 | );
92 | return GenresLoadingView();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/appbar_search_mode_cubit.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_bloc/flutter_bloc.dart';
2 |
3 | class AppbarSearhModeCubit extends Cubit {
4 | AppbarSearhModeCubit() : super(false);
5 |
6 | void appbarModeSearch() {
7 | emit(true);
8 | }
9 |
10 | void appbarModeNormal() {
11 | emit(false);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movie_cast_cubit/movie_cast_cubit.dart:
--------------------------------------------------------------------------------
1 | import 'package:equatable/equatable.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 |
5 | import '../../../../core/errors/failure.dart';
6 | import '../../../../domain/entities/actor.dart';
7 | import '../../../../domain/usecases/get_movie_cast.dart';
8 |
9 | part 'movie_cast_state.dart';
10 |
11 | class MovieCastCubit extends Cubit {
12 | final GetMovieCast getMovieCast;
13 |
14 | MovieCastCubit({@required this.getMovieCast})
15 | : assert(getMovieCast != null),
16 | super(MovieCastLoadInProgress());
17 |
18 | Future movieCastGet({String language, int movieId}) async {
19 | emit(MovieCastLoadInProgress());
20 | final failureOrMovie = await getMovieCast(
21 | CastParams(movieId: movieId, language: language),
22 | );
23 | failureOrMovie.fold(
24 | (failure) => emit(
25 | MovieCastLoadFailure(message: mapFailureToMessage(failure)),
26 | ),
27 | (cast) => emit(MovieCastLoadSuccess(cast: cast)),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movie_cast_cubit/movie_cast_state.dart:
--------------------------------------------------------------------------------
1 | part of 'movie_cast_cubit.dart';
2 |
3 | abstract class MovieCastState extends Equatable {
4 | @override
5 | List get props => [];
6 | }
7 |
8 | class MovieCastLoadInProgress extends MovieCastState {}
9 |
10 | class MovieCastLoadSuccess extends MovieCastState {
11 | final List cast;
12 | MovieCastLoadSuccess({@required this.cast});
13 |
14 | @override
15 | List get props => [cast];
16 | }
17 |
18 | class MovieCastLoadFailure extends MovieCastState {
19 | final String message;
20 | MovieCastLoadFailure({@required this.message});
21 |
22 | @override
23 | List get props => [message];
24 | }
25 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movie_details_cubit/movie_details_cubit.dart:
--------------------------------------------------------------------------------
1 | import 'package:equatable/equatable.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 |
5 | import '../../../../core/errors/failure.dart';
6 | import '../../../../domain/entities/movie.dart';
7 | import '../../../../domain/usecases/get_movie_detail.dart';
8 |
9 | part 'movie_details_state.dart';
10 |
11 | class MovieDetailsCubit extends Cubit {
12 | final GetMovieDetail getMovieDetail;
13 |
14 | MovieDetailsCubit({@required this.getMovieDetail})
15 | : assert(getMovieDetail != null),
16 | super(MovieDetailsLoadInProgress());
17 |
18 | Future movieDetailsGet({String language, int movieId}) async {
19 | emit(MovieDetailsLoadInProgress());
20 | final failureOrMovie = await getMovieDetail(
21 | MovieDetailParams(movieId: movieId, language: language),
22 | );
23 | failureOrMovie.fold(
24 | (failure) => emit(
25 | MovieDetailsLoadFailure(message: mapFailureToMessage(failure)),
26 | ),
27 | (movie) => emit(MovieDetailsLoadSuccess(movie: movie)),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movie_details_cubit/movie_details_state.dart:
--------------------------------------------------------------------------------
1 | part of 'movie_details_cubit.dart';
2 |
3 | abstract class MovieDetailsState extends Equatable {
4 | @override
5 | List get props => [];
6 | }
7 |
8 | class MovieDetailsLoadInProgress extends MovieDetailsState {}
9 |
10 | class MovieDetailsLoadSuccess extends MovieDetailsState {
11 | final Movie movie;
12 | MovieDetailsLoadSuccess({@required this.movie});
13 |
14 | @override
15 | List get props => [movie];
16 | }
17 |
18 | class MovieDetailsLoadFailure extends MovieDetailsState {
19 | final String message;
20 | MovieDetailsLoadFailure({@required this.message});
21 |
22 | @override
23 | List get props => [message];
24 | }
25 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movies_bloc/movies_bloc.dart:
--------------------------------------------------------------------------------
1 | import 'package:equatable/equatable.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 |
5 | import '../../../../core/errors/failure.dart';
6 | import '../../../../domain/entities/movie.dart';
7 | import '../../../../domain/usecases/get_movies.dart';
8 |
9 | part 'movies_event.dart';
10 | part 'movies_state.dart';
11 |
12 | class MoviesBloc extends Bloc {
13 | final GetMovies getMovies;
14 |
15 | MoviesBloc({
16 | @required this.getMovies,
17 | }) : assert(getMovies != null),
18 | super(MoviesInitial());
19 |
20 | @override
21 | Stream mapEventToState(MoviesEvent event) async* {
22 | if (event is MoviesGet) yield* _moviesGet(event);
23 | }
24 |
25 | Stream _moviesGet(MoviesGet event) async* {
26 | yield MoviesLoadInProgress();
27 | final failureOrMovies = await getMovies(
28 | Params(
29 | endpoint: event.endpoint,
30 | language: event.language,
31 | genreId: event.genre,
32 | ),
33 | );
34 | yield failureOrMovies.fold(
35 | (failure) => MoviesLoadFailure(message: mapFailureToMessage(failure)),
36 | (movies) => MoviesLoadSuccess(movies: movies),
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movies_bloc/movies_event.dart:
--------------------------------------------------------------------------------
1 | part of 'movies_bloc.dart';
2 |
3 | abstract class MoviesEvent extends Equatable {
4 | @override
5 | List get props => [];
6 | }
7 |
8 | class MoviesGet extends MoviesEvent {
9 | final String endpoint;
10 | final String language;
11 | final int genre;
12 |
13 | MoviesGet({this.endpoint, this.language, this.genre = -1});
14 |
15 | @override
16 | List get props => [endpoint, language, genre];
17 | }
18 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movies_bloc/movies_state.dart:
--------------------------------------------------------------------------------
1 | part of 'movies_bloc.dart';
2 |
3 | abstract class MoviesState extends Equatable {
4 | @override
5 | List get props => [];
6 | }
7 |
8 | class MoviesInitial extends MoviesState {}
9 |
10 | class MoviesLoadInProgress extends MoviesState {}
11 |
12 | class MoviesLoadSuccess extends MoviesState {
13 | final List movies;
14 | MoviesLoadSuccess({@required this.movies});
15 |
16 | @override
17 | List get props => [movies];
18 | }
19 |
20 | class MoviesLoadFailure extends MoviesState {
21 | final String message;
22 | MoviesLoadFailure({@required this.message});
23 |
24 | @override
25 | List get props => [message];
26 | }
27 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movies_search_cubit/movies_search_cubit.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:equatable/equatable.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_bloc/flutter_bloc.dart';
6 |
7 | import '../../../../core/errors/failure.dart';
8 | import '../../../../core/api/movies_api.dart';
9 | import '../../../../domain/entities/movie.dart';
10 | import '../../../../domain/usecases/search_movies.dart';
11 |
12 | part 'movies_search_state.dart';
13 |
14 | class MoviesSearchCubit extends Cubit {
15 | final SearchMovies searchMovies;
16 |
17 | MoviesSearchCubit({
18 | @required this.searchMovies,
19 | }) : assert(searchMovies != null),
20 | super(MoviesSearchInitial());
21 |
22 | Future moviesSearchRestart() async {
23 | emit(MoviesSearchInitial());
24 | }
25 |
26 | Future moviesSearch({
27 | String language = MoviesApi.es,
28 | String query,
29 | }) async {
30 | if (query.trim().isEmpty || query == null)
31 | emit(MoviesSearchInitial());
32 | else {
33 | emit(MoviesSearchLoadInProgress());
34 |
35 | // to avoid making a query in every on change
36 | Timer(
37 | Duration(milliseconds: 500),
38 | () async {
39 | final failureOrMovies = await searchMovies(
40 | SearchMoviesParams(language: language, query: query),
41 | );
42 |
43 | failureOrMovies.fold(
44 | (failure) => emit(
45 | MoviesSearchLoadFailure(message: mapFailureToMessage(failure)),
46 | ),
47 | (movies) => emit(MoviesSearchLoadSuccess(movies: movies)),
48 | );
49 | },
50 | );
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/presentation/movies/business_logic/movies_search_cubit/movies_search_state.dart:
--------------------------------------------------------------------------------
1 | part of 'movies_search_cubit.dart';
2 |
3 | abstract class MoviesSearchState extends Equatable {
4 | @override
5 | List get props => [];
6 | }
7 |
8 | class MoviesSearchInitial extends MoviesSearchState {}
9 |
10 | class MoviesSearchLoadInProgress extends MoviesSearchState {}
11 |
12 | class MoviesSearchLoadSuccess extends MoviesSearchState {
13 | final List movies;
14 | MoviesSearchLoadSuccess({this.movies});
15 |
16 | @override
17 | List get props => [this.movies];
18 | }
19 |
20 | class MoviesSearchLoadFailure extends MoviesSearchState {
21 | final String message;
22 | MoviesSearchLoadFailure({this.message});
23 |
24 | @override
25 | List get props => [this.message];
26 | }
27 |
--------------------------------------------------------------------------------
/lib/presentation/movies/movie_profile_view/movie_profile_appbar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class MoviesProfileAppbar extends StatelessWidget with PreferredSizeWidget {
4 | @override
5 | final Size preferredSize;
6 |
7 | final GlobalKey drawerKey;
8 | final String title;
9 |
10 | MoviesProfileAppbar({
11 | @required this.title,
12 | this.drawerKey,
13 | }) : preferredSize = Size.fromHeight(70.0),
14 | super();
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return AppBar(
19 | title: Center(
20 | child: Container(
21 | child: Text(
22 | title,
23 | textAlign: TextAlign.center,
24 | softWrap: true,
25 | maxLines: 2,
26 | ),
27 | ),
28 | ),
29 | centerTitle: true,
30 | backgroundColor: Colors.transparent,
31 | shadowColor: Colors.transparent,
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/presentation/movies/movies_view/movies_grid_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:cached_network_image/cached_network_image.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import '../../../core/api/movies_api.dart';
5 | import '../../../domain/entities/movie.dart';
6 | import '../movies_widgets/movie_rating.dart';
7 |
8 | class MoviesGridView extends StatelessWidget {
9 | final List movies;
10 |
11 | const MoviesGridView({this.movies});
12 | @override
13 | Widget build(BuildContext context) {
14 | double padd = 20;
15 | return Column(
16 | children: [
17 | SizedBox(height: 90),
18 | Expanded(
19 | child: GridView.builder(
20 | padding: EdgeInsets.symmetric(horizontal: padd),
21 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
22 | crossAxisCount: 2,
23 | mainAxisSpacing: 20,
24 | crossAxisSpacing: 20,
25 | childAspectRatio: 5 / 8,
26 | ),
27 | itemCount: movies.length,
28 | itemBuilder: (context, index) {
29 | return MovieGridCard(movies[index]);
30 | }),
31 | ),
32 | ],
33 | );
34 | }
35 | }
36 |
37 | class MovieGridCard extends StatelessWidget {
38 | final Movie movie;
39 | MovieGridCard(this.movie);
40 |
41 | @override
42 | Widget build(BuildContext context) {
43 | return GestureDetector(
44 | onTap: () => _goToMovieProfile(context, movie),
45 | child: Stack(
46 | children: [
47 | Hero(
48 | tag: movie.id,
49 | child: Container(
50 | child: _posterImage(movie),
51 | ),
52 | ),
53 | _movieTitle(movie),
54 | Column(
55 | children: [
56 | Expanded(child: Container()),
57 | Container(
58 | height: 170,
59 | width: double.infinity,
60 | padding: EdgeInsets.all(10),
61 | child: Row(
62 | children: [
63 | Expanded(child: Container()),
64 | MovieRating(movie: movie, tiny: true),
65 | ],
66 | ),
67 | ),
68 | ],
69 | ),
70 | ],
71 | ),
72 | );
73 | }
74 |
75 | Widget _posterImage(Movie movie) {
76 | return CachedNetworkImage(
77 | imageUrl: MoviesApi.getMoviePoster(movie.posterPath),
78 | imageBuilder: (context, imageProvider) => Container(
79 | decoration: BoxDecoration(
80 | borderRadius: BorderRadius.circular(10),
81 | image: DecorationImage(image: imageProvider, fit: BoxFit.cover),
82 | ),
83 | ),
84 | progressIndicatorBuilder: (context, url, downloadProgress) => Center(
85 | child: CircularProgressIndicator(value: downloadProgress.progress)),
86 | errorWidget: (context, url, error) => Icon(Icons.error),
87 | );
88 | }
89 |
90 | Widget _movieTitle(Movie movie) {
91 | return Column(
92 | children: [
93 | Expanded(child: Container()),
94 | Container(
95 | height: 85,
96 | width: double.infinity,
97 | child: Stack(
98 | children: [
99 | Opacity(
100 | opacity: 0.7,
101 | child: Container(
102 | decoration: BoxDecoration(
103 | borderRadius: BorderRadius.only(
104 | bottomLeft: Radius.circular(10),
105 | bottomRight: Radius.circular(10),
106 | ),
107 | color: Colors.black,
108 | ),
109 | ),
110 | ),
111 | Container(
112 | padding: EdgeInsets.symmetric(horizontal: 10),
113 | child: Center(
114 | child: Text(
115 | movie.title,
116 | textAlign: TextAlign.center,
117 | style: TextStyle(
118 | fontSize: 15,
119 | color: Colors.white,
120 | fontWeight: FontWeight.bold,
121 | ),
122 | ),
123 | ),
124 | ),
125 | ],
126 | ),
127 | ),
128 | ],
129 | );
130 | }
131 |
132 | void _goToMovieProfile(BuildContext context, movie) {
133 | Navigator.of(context).pushNamed('/movie_profile', arguments: movie);
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/lib/presentation/movies/movies_view/movies_loading_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 |
4 | class MoviesLoadingView extends StatelessWidget {
5 | @override
6 | Widget build(BuildContext context) {
7 | String loadingMovies = AppLocalizations.of(context).loading_movies;
8 |
9 | return Scaffold(
10 | backgroundColor: Colors.black,
11 | body: Center(
12 | child: Column(
13 | mainAxisAlignment: MainAxisAlignment.center,
14 | children: [
15 | Expanded(flex: 30, child: Container()),
16 | Expanded(
17 | flex: 15,
18 | child: Container(
19 | child: Text(
20 | loadingMovies,
21 | maxLines: 2,
22 | textAlign: TextAlign.center,
23 | softWrap: true,
24 | style: TextStyle(
25 | color: Colors.white,
26 | fontSize: 20,
27 | fontWeight: FontWeight.bold,
28 | ),
29 | ),
30 | ),
31 | ),
32 | Expanded(flex: 1, child: Container()),
33 | Container(
34 | width: 60,
35 | height: 60,
36 | child: CircularProgressIndicator(
37 | strokeWidth: 10,
38 | valueColor: new AlwaysStoppedAnimation(Colors.blue),
39 | ),
40 | ),
41 | Expanded(flex: 15, child: Container()),
42 | ],
43 | ),
44 | ),
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/presentation/movies/movies_view/movies_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 |
4 | import '../../custom_drawer/custom_drawer.dart';
5 | import '../business_logic/appbar_search_mode_cubit.dart';
6 | import '../business_logic/movies_bloc/movies_bloc.dart';
7 | import '../business_logic/movies_search_cubit/movies_search_cubit.dart';
8 | import '../movies_widgets/movies_appbar.dart';
9 | import '../search_movies/search_movies_suggestions.dart';
10 | import 'movies_grid_view.dart';
11 | import 'movies_loading_view.dart';
12 |
13 | class MoviesView extends StatelessWidget {
14 | final String title;
15 | final String endpoint;
16 | final String language;
17 |
18 | const MoviesView({this.title, this.endpoint, this.language});
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | final mainAppbar = MoviesAppBar(title: title);
23 | final bodyMargin = 30.0;
24 |
25 | return Scaffold(
26 | resizeToAvoidBottomInset: false,
27 | extendBodyBehindAppBar: true,
28 | backgroundColor: Colors.transparent,
29 | appBar: mainAppbar,
30 | drawer: CustomDrawer(),
31 | body: BlocBuilder(
32 | builder: (context, searchState) {
33 | if (searchState is MoviesSearchInitial)
34 | return _viewMovies(bodyMargin);
35 | else
36 | return BlocBuilder(
37 | builder: (context, inSearchMode) {
38 | if (inSearchMode)
39 | return Stack(
40 | children: [
41 | _viewMovies(bodyMargin),
42 | Column(
43 | children: [
44 | SizedBox(height: mainAppbar.preferredSize.height),
45 | SizedBox(height: bodyMargin),
46 | SizedBox(height: 10),
47 | SearchMoviesSuggestions(),
48 | ],
49 | ),
50 | ],
51 | );
52 | else
53 | return _viewMovies(bodyMargin);
54 | },
55 | );
56 | },
57 | ),
58 | );
59 | }
60 |
61 | Widget _viewMovies(double bodyMargin) {
62 | return Column(
63 | children: [
64 | Container(height: bodyMargin, color: Colors.black),
65 | Expanded(
66 | child: BlocBuilder(
67 | builder: (context, state) {
68 | if (state is MoviesLoadSuccess)
69 | return MoviesGridView(movies: state.movies);
70 | else if (state is MoviesLoadFailure)
71 | return Center(child: Text(state.message));
72 | else if (state is MoviesLoadInProgress)
73 | return MoviesLoadingView();
74 | else if (state is MoviesInitial) {
75 | return MoviesLoadingView();
76 | } else
77 | return null;
78 | },
79 | ),
80 | ),
81 | ],
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/lib/presentation/movies/movies_widgets/movie_poster.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:cached_network_image/cached_network_image.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | import '../../../core/api/movies_api.dart';
7 | import '../../../domain/entities/movie.dart';
8 |
9 | class MoviePoster extends StatelessWidget {
10 | final Movie movie;
11 |
12 | const MoviePoster({this.movie});
13 | @override
14 | Widget build(BuildContext context) {
15 | return CachedNetworkImage(
16 | imageUrl: MoviesApi.getMoviePoster(movie.posterPath),
17 | imageBuilder: (context, imageProvider) => Opacity(
18 | opacity: 0.7,
19 | child: Container(
20 | decoration: BoxDecoration(
21 | image: DecorationImage(image: imageProvider, fit: BoxFit.cover),
22 | ),
23 | child: BackdropFilter(
24 | filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
25 | child: Container(
26 | decoration:
27 | new BoxDecoration(color: Colors.white.withOpacity(0.0)),
28 | ),
29 | ),
30 | ),
31 | ),
32 | progressIndicatorBuilder: (context, url, downloadProgress) => Center(
33 | child: CircularProgressIndicator(value: downloadProgress.progress),
34 | ),
35 | errorWidget: (context, url, error) => Icon(Icons.error),
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/presentation/movies/movies_widgets/movie_rating.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:percent_indicator/percent_indicator.dart';
3 |
4 | import '../../../domain/entities/movie.dart';
5 |
6 | class MovieRating extends StatelessWidget {
7 | final Movie movie;
8 | final bool tiny;
9 |
10 | const MovieRating({this.movie, this.tiny = false});
11 | @override
12 | Widget build(BuildContext context) {
13 | final _percent = movie.voteAverage / 10;
14 | return Center(
15 | child: Container(
16 | padding: EdgeInsets.symmetric(vertical: 30),
17 | child: CircularPercentIndicator(
18 | radius: tiny ? 45 : 65.0,
19 | animation: true,
20 | animationDuration: 800,
21 | lineWidth: tiny ? 5.0 : 7.0,
22 | percent: _percent,
23 | center: Stack(
24 | children: [
25 | Center(
26 | child: Opacity(
27 | opacity: 0.6,
28 | child: Container(
29 | padding: EdgeInsets.all(tiny ? 8.2 : 15),
30 | decoration: BoxDecoration(
31 | color: Colors.black,
32 | borderRadius: BorderRadius.circular(1000),
33 | ),
34 | child: Text('....'),
35 | ),
36 | ),
37 | ),
38 | Center(
39 | child: Text(
40 | '${movie.voteAverage}',
41 | style: TextStyle(
42 | fontWeight: FontWeight.bold,
43 | fontSize: tiny ? 12 : 14.0,
44 | color: Colors.white,
45 | ),
46 | ),
47 | ),
48 | ],
49 | ),
50 | circularStrokeCap: CircularStrokeCap.round,
51 | backgroundColor: Colors.black.withOpacity(0.4),
52 | progressColor: movie.voteAverage >= 7
53 | ? Colors.green
54 | : movie.voteAverage >= 4
55 | ? Colors.yellow
56 | : Colors.red,
57 | ),
58 | ),
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/presentation/movies/movies_widgets/movies_appbar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 |
4 | import '../business_logic/appbar_search_mode_cubit.dart';
5 | import '../search_movies/search_movies_bar.dart';
6 |
7 | class MoviesAppBar extends StatelessWidget with PreferredSizeWidget {
8 | @override
9 | final Size preferredSize;
10 |
11 | final GlobalKey drawerKey;
12 | final String title;
13 |
14 | MoviesAppBar({
15 | @required this.title,
16 | this.drawerKey,
17 | }) : preferredSize = Size.fromHeight(80.0),
18 | super();
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | return BlocBuilder(
23 | builder: (context, state) {
24 | if (state)
25 | return AppBar(
26 | backgroundColor: Colors.transparent,
27 | shadowColor: Colors.transparent,
28 | flexibleSpace: SafeArea(child: Center(child: SearchMoviesBar())),
29 | actions: [
30 | IconButton(
31 | icon: Icon(Icons.search_off),
32 | onPressed: () => BlocProvider.of(context)
33 | .appbarModeNormal(),
34 | ),
35 | ],
36 | );
37 | else
38 | return AppBar(
39 | title: Center(
40 | child: Container(
41 | child: Text(
42 | title,
43 | textAlign: TextAlign.center,
44 | softWrap: true,
45 | maxLines: 2,
46 | ),
47 | ),
48 | ),
49 | centerTitle: true,
50 | backgroundColor: Colors.transparent,
51 | shadowColor: Colors.transparent,
52 | actions: [
53 | IconButton(
54 | icon: Icon(Icons.search),
55 | onPressed: () => BlocProvider.of(context)
56 | .appbarModeSearch(),
57 | ),
58 | ],
59 | );
60 | },
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/presentation/movies/search_movies/search_movies_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 |
4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
5 | import '../business_logic/movies_search_cubit/movies_search_cubit.dart';
6 |
7 | class SearchMoviesBar extends StatelessWidget {
8 | final double radius = 30;
9 | final controller = TextEditingController();
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | String searching = AppLocalizations.of(context).search_movies_searching;
14 |
15 | return Opacity(
16 | opacity: 0.7,
17 | child: Container(
18 | margin: EdgeInsets.symmetric(vertical: 5, horizontal: 50),
19 | decoration: BoxDecoration(
20 | color: Colors.black,
21 | borderRadius: BorderRadius.circular(radius),
22 | ),
23 | child: TextField(
24 | onChanged: (query) => BlocProvider.of(context)
25 | .moviesSearch(query: query),
26 | autofocus: true,
27 | cursorColor: Colors.white,
28 | controller: controller,
29 | style: TextStyle(color: Colors.white),
30 | decoration: InputDecoration(
31 | contentPadding: EdgeInsets.symmetric(horizontal: 30, vertical: 20),
32 | suffixIconConstraints: BoxConstraints(minWidth: 70),
33 | hintStyle: TextStyle(color: Colors.white, fontSize: 15),
34 | hintText: searching,
35 | suffixIcon: _iconClearButton(context),
36 | focusedBorder: _outlineInputBorder(radius, Colors.white),
37 | enabledBorder: _outlineInputBorder(radius, Colors.red),
38 | ),
39 | ),
40 | ),
41 | );
42 | }
43 |
44 | Widget _iconClearButton(BuildContext context) {
45 | return IconButton(
46 | icon: Icon(Icons.clear, color: Colors.white),
47 | onPressed: () {
48 | controller.clear();
49 | BlocProvider.of(context).moviesSearchRestart();
50 | },
51 | );
52 | }
53 |
54 | OutlineInputBorder _outlineInputBorder(double radius, Color borderColor) {
55 | return OutlineInputBorder(
56 | borderSide: BorderSide(color: borderColor),
57 | borderRadius: BorderRadius.circular(radius),
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/presentation/movies/search_movies/search_movies_success.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 |
4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
5 | import '../../../../core/api/movies_api.dart';
6 | import '../../../../domain/entities/movie.dart';
7 | import '../business_logic/appbar_search_mode_cubit.dart';
8 | import '../business_logic/movies_search_cubit/movies_search_cubit.dart';
9 |
10 | class SearchMoviesSuccess extends StatelessWidget {
11 | final List movies;
12 | SearchMoviesSuccess(this.movies);
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | final viewInsets = EdgeInsets.fromWindowPadding(
17 | WidgetsBinding.instance.window.viewInsets,
18 | WidgetsBinding.instance.window.devicePixelRatio,
19 | );
20 |
21 | if (movies.isNotEmpty)
22 | return Container(
23 | height: MediaQuery.of(context).size.height * 0.75 - viewInsets.bottom,
24 | child: ListView.builder(
25 | padding: EdgeInsets.all(0),
26 | itemCount: movies.length,
27 | itemBuilder: (context, i) => _movieTile(context, movies[i]),
28 | ),
29 | );
30 | else
31 | return _noItemsFound(context);
32 | }
33 |
34 | Widget _movieTile(BuildContext context, Movie movie) {
35 | return ListTile(
36 | onTap: () {
37 | BlocProvider.of(context).moviesSearchRestart();
38 | BlocProvider.of(context).appbarModeNormal();
39 | Navigator.of(context).pushNamed('/movie_profile', arguments: movie);
40 | },
41 | leading: _moviePoster(context, movie),
42 | title: Text(movie.title, style: TextStyle(fontWeight: FontWeight.w500)),
43 | subtitle: Text(movie.originalTitle),
44 | );
45 | }
46 |
47 | Widget _moviePoster(BuildContext context, Movie movie) {
48 | return FadeInImage(
49 | image: NetworkImage(MoviesApi.getMoviePoster(movie.posterPath)),
50 | width: 60.0,
51 | fit: BoxFit.contain,
52 | placeholder: AssetImage('assets/img/no-image.jpg'),
53 | placeholderErrorBuilder: (context, object, stacktrace) {
54 | return Container(
55 | child: Image(image: AssetImage('assets/img/no-image.jpg')),
56 | );
57 | },
58 | imageErrorBuilder: (context, object, stacktrace) {
59 | return Container(
60 | child: Image(image: AssetImage('assets/img/no-image.jpg')),
61 | );
62 | },
63 | );
64 | }
65 |
66 | Widget _noItemsFound(BuildContext context) {
67 | String notFound = AppLocalizations.of(context).search_movies_not_found;
68 | return BlocBuilder(
69 | builder: (context, state) {
70 | if (state is MoviesSearchLoadFailure)
71 | return Text(
72 | state.message,
73 | style: TextStyle(fontSize: 15),
74 | );
75 | else
76 | return Text(
77 | notFound,
78 | style: TextStyle(fontSize: 15),
79 | );
80 | });
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/presentation/movies/search_movies/search_movies_suggestions.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 |
4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
5 | import '../business_logic/movies_search_cubit/movies_search_cubit.dart';
6 | import 'search_movies_success.dart';
7 |
8 | class SearchMoviesSuggestions extends StatelessWidget {
9 | @override
10 | Widget build(BuildContext context) {
11 | return Container(
12 | width: MediaQuery.of(context).size.width,
13 | margin: EdgeInsets.symmetric(horizontal: 30),
14 | padding: EdgeInsets.all(20),
15 | decoration: BoxDecoration(
16 | borderRadius: BorderRadius.circular(15),
17 | color: Colors.grey[300],
18 | ),
19 | child: BlocBuilder(
20 | builder: (context, state) {
21 | if (state is MoviesSearchLoadSuccess)
22 | return SearchMoviesSuccess(state.movies);
23 | else if (state is MoviesSearchLoadInProgress)
24 | return _loadingBuilder();
25 | else if (state is MoviesSearchLoadFailure)
26 | return _errorBuilder(context);
27 | else if (state is MoviesSearchInitial)
28 | return Container();
29 | else
30 | return null;
31 | },
32 | ),
33 | );
34 | }
35 |
36 | Widget _loadingBuilder() {
37 | return Row(
38 | children: [
39 | CircularProgressIndicator(
40 | valueColor: new AlwaysStoppedAnimation(Colors.black),
41 | ),
42 | Expanded(child: Container()),
43 | ],
44 | );
45 | }
46 |
47 | Widget _errorBuilder(BuildContext context) {
48 | String error = AppLocalizations.of(context).search_movies_error;
49 |
50 | return Text(
51 | error,
52 | style: TextStyle(fontSize: 15),
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: my_movie_list
2 | description: A new Flutter project.
3 |
4 | publish_to: "none"
5 |
6 | version: 1.0.0+1
7 |
8 | environment:
9 | sdk: ">=2.7.0 <3.0.0"
10 |
11 | dependencies:
12 | flutter:
13 | sdk: flutter
14 |
15 | # Base
16 | cupertino_icons: ^1.0.0
17 | http: ^0.13.1
18 | dartz: 0.10.0-nullsafety.1
19 | get_it: ^6.1.1
20 | equatable: ^2.0.0
21 | flutter_bloc: ^7.0.0
22 | shared_preferences: ^2.0.5
23 | data_connection_checker: ^0.3.4
24 |
25 | # Beauty
26 | cached_network_image: ^2.5.0
27 | percent_indicator: ^3.0.1
28 |
29 | # Internationalization
30 | intl: ^0.17.0
31 | flutter_localizations:
32 | sdk: flutter
33 |
34 | dev_dependencies:
35 | flutter_test:
36 | sdk: flutter
37 | test: ^1.16.0
38 | bloc_test: ^8.0.0
39 | mockito: ^4.1.1
40 |
41 | flutter:
42 | uses-material-design: true
43 | generate: true
44 |
45 | assets:
46 | - assets/img/
47 | - assets/cats_img/
48 |
--------------------------------------------------------------------------------
/readme_sources/architecture.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/readme_sources/architecture.jpeg
--------------------------------------------------------------------------------
/readme_sources/categories.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/readme_sources/categories.gif
--------------------------------------------------------------------------------
/readme_sources/clean_architecture.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/readme_sources/clean_architecture.jpeg
--------------------------------------------------------------------------------
/readme_sources/data_layer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/readme_sources/data_layer.png
--------------------------------------------------------------------------------
/readme_sources/domain_layer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/readme_sources/domain_layer.png
--------------------------------------------------------------------------------
/readme_sources/movie_profile.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/readme_sources/movie_profile.gif
--------------------------------------------------------------------------------
/readme_sources/presentation_layer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/readme_sources/presentation_layer.png
--------------------------------------------------------------------------------
/readme_sources/search.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sr-Te/Flutter-CleanArchitecture-TDD/e2b22625417c30e454be5b82ea7cebacc587ea28/readme_sources/search.gif
--------------------------------------------------------------------------------
/test/core/network/network_info_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:data_connection_checker/data_connection_checker.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/network/network_info.dart';
5 |
6 | class MockDataConnectionChecker extends Mock implements DataConnectionChecker {}
7 |
8 | void main() {
9 | NetworkInfoImpl networkInfo;
10 | MockDataConnectionChecker mockDataConnectionChecker;
11 |
12 | setUp(() {
13 | mockDataConnectionChecker = MockDataConnectionChecker();
14 | networkInfo = NetworkInfoImpl(mockDataConnectionChecker);
15 | });
16 |
17 | group('isConnected', () {
18 | test(
19 | 'should forward the call to DataConnectionChecker.hasConnection',
20 | () {
21 | // arrange
22 | final tHasConnectionFuture = Future.value(true);
23 | when(mockDataConnectionChecker.hasConnection)
24 | .thenAnswer((_) => tHasConnectionFuture);
25 | // act
26 | final result = networkInfo.isConnected;
27 | // assert
28 | verify(mockDataConnectionChecker.hasConnection);
29 | expect(result, tHasConnectionFuture);
30 | },
31 | );
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/test/data/datasources/genres/genres_local_data_source_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:mockito/mockito.dart';
5 | import 'package:matcher/matcher.dart';
6 | import 'package:my_movie_list/core/errors/exception.dart';
7 | import 'package:my_movie_list/data/datasources/genres/genres_local_data_source.dart';
8 | import 'package:my_movie_list/domain/entities/genre.dart';
9 | import 'package:shared_preferences/shared_preferences.dart';
10 |
11 | import '../../../fixtures/fixture_reader.dart';
12 |
13 | class MockSharedPreferences extends Mock implements SharedPreferences {}
14 |
15 | void main() {
16 | GenresLocalDataSourceImpl dataSource;
17 | MockSharedPreferences mockSharedPreferences;
18 |
19 | setUp(() {
20 | mockSharedPreferences = MockSharedPreferences();
21 | dataSource = GenresLocalDataSourceImpl(
22 | sharedPreferences: mockSharedPreferences,
23 | );
24 | });
25 |
26 | group('getLastGenres', () {
27 | test(
28 | 'should return List from SharedPreferences when is at least one movie in the cache',
29 | () async {
30 | // arrange
31 | when(mockSharedPreferences.getString(any))
32 | .thenReturn(fixture('genres_cached.json'));
33 | // act
34 | final result = await dataSource.getLastGenres();
35 | // assert
36 | verify(mockSharedPreferences.getString(CACHED_GENRES));
37 | expect(result, isA>());
38 | },
39 | );
40 |
41 | test(
42 | 'should throw a CacheException when there is not a cached value',
43 | () {
44 | // arrange
45 | when(mockSharedPreferences.getString(any)).thenReturn(null);
46 | // act
47 | final call = dataSource.getLastGenres;
48 | // assert
49 | expect(() => call(), throwsA(TypeMatcher()));
50 | },
51 | );
52 | });
53 |
54 | group('cacheGenres', () {
55 | final List tGenreListModel = [];
56 |
57 | test(
58 | 'should call SharedPreferences to cache the data',
59 | () {
60 | // act
61 | dataSource.cacheGenres(tGenreListModel);
62 | // assert
63 | final expectedJsonString = json.encode(
64 | genreModelListToJsonList(tGenreListModel),
65 | );
66 | verify(mockSharedPreferences.setString(
67 | CACHED_GENRES,
68 | expectedJsonString,
69 | ));
70 | },
71 | );
72 | });
73 | }
74 |
--------------------------------------------------------------------------------
/test/data/datasources/genres/genres_remote_data_source_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:matcher/matcher.dart';
5 | import 'package:mockito/mockito.dart';
6 | import 'package:http/http.dart' as http;
7 | import 'package:my_movie_list/core/errors/exception.dart';
8 | import 'package:my_movie_list/core/api/movies_api.dart';
9 | import 'package:my_movie_list/data/datasources/genres/genres_remote_data_source.dart';
10 | import 'package:my_movie_list/domain/entities/genre.dart';
11 |
12 | import '../../../fixtures/fixture_reader.dart';
13 |
14 | class MockHttpClient extends Mock implements http.Client {}
15 |
16 | void main() {
17 | GenresRemoteDataSourceImpl dataSource;
18 | MockHttpClient mockHttpClient;
19 |
20 | setUp(() {
21 | mockHttpClient = MockHttpClient();
22 | dataSource = GenresRemoteDataSourceImpl(client: mockHttpClient);
23 | });
24 |
25 | void setUpMockHttpClientSuccess200() {
26 | when(mockHttpClient.get(any)).thenAnswer(
27 | (_) async => http.Response(
28 | fixture('genres.json'),
29 | 200,
30 | headers: {
31 | HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8',
32 | },
33 | ),
34 | );
35 | }
36 |
37 | void setUpMockHttpClientFailure404() {
38 | when(mockHttpClient.get(any)).thenAnswer(
39 | (_) async => http.Response('Something went wrong', 404),
40 | );
41 | }
42 |
43 | group('getGenres', () {
44 | final tLanguage = MoviesApi.es;
45 |
46 | test(
47 | 'should preform a GET request on a URL',
48 | () {
49 | // arrange
50 | setUpMockHttpClientSuccess200();
51 | // act
52 | dataSource.getGenres(tLanguage);
53 | // assert
54 | verify(mockHttpClient.get(MoviesApi.getGenres(tLanguage)));
55 | },
56 | );
57 |
58 | test(
59 | 'shoud return a List when the Response code is 200 (success)',
60 | () async {
61 | // arrange
62 | setUpMockHttpClientSuccess200();
63 | // act
64 | final result = await dataSource.getGenres(tLanguage);
65 | // assert
66 | expect(result, isA>());
67 | },
68 | );
69 |
70 | test(
71 | 'should throw a ServerException when response code is 404 or other',
72 | () async {
73 | // arrange
74 | setUpMockHttpClientFailure404();
75 | // act
76 | final call = dataSource.getGenres;
77 | // assert
78 | expect(
79 | () => call(tLanguage),
80 | throwsA(TypeMatcher()),
81 | );
82 | },
83 | );
84 | });
85 | }
86 |
--------------------------------------------------------------------------------
/test/data/datasources/movies/movies_local_data_source_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:mockito/mockito.dart';
5 | import 'package:matcher/matcher.dart';
6 | import 'package:my_movie_list/core/errors/exception.dart';
7 | import 'package:my_movie_list/core/api/movies_endpoint.dart';
8 | import 'package:my_movie_list/data/datasources/movies/movies_local_data_source.dart';
9 | import 'package:my_movie_list/domain/entities/movie.dart';
10 | import 'package:shared_preferences/shared_preferences.dart';
11 |
12 | import '../../../fixtures/fixture_reader.dart';
13 |
14 | class MockSharedPreferences extends Mock implements SharedPreferences {}
15 |
16 | void main() {
17 | MoviesLocalDataSourceImpl dataSource;
18 | MockSharedPreferences mockSharedPreferences;
19 |
20 | setUp(() {
21 | mockSharedPreferences = MockSharedPreferences();
22 | dataSource = MoviesLocalDataSourceImpl(
23 | sharedPreferences: mockSharedPreferences,
24 | );
25 | });
26 |
27 | final tEndpoint = MoviesEndpoint.withGenre;
28 |
29 | group('getLastMovies', () {
30 | test(
31 | 'should return List from SharedPreferences when is at least one movie in the cache',
32 | () async {
33 | // arrange
34 | when(mockSharedPreferences.getString(any))
35 | .thenReturn(fixture('movies_cached.json'));
36 | // act
37 | final result = await dataSource.getLastMovies(tEndpoint);
38 | // assert
39 | verify(mockSharedPreferences.getString(tEndpoint));
40 | expect(result, isA>());
41 | },
42 | );
43 |
44 | test(
45 | 'should throw a CacheException when there is not a cached value',
46 | () async {
47 | // arrange
48 | when(mockSharedPreferences.getString(any)).thenReturn(null);
49 | // act
50 | final call = dataSource.getLastMovies;
51 | // assert
52 | expect(() => call(tEndpoint), throwsA(TypeMatcher()));
53 | },
54 | );
55 | });
56 |
57 | group('cacheMovies', () {
58 | final List tMovieListModel = [];
59 |
60 | test(
61 | 'should call SharedPreferences to cache the data',
62 | () {
63 | // act
64 | dataSource.cacheMovies(tEndpoint, tMovieListModel);
65 | // assert
66 | final expectedJsonString = json.encode(
67 | movieModelListToJsonList(tMovieListModel),
68 | );
69 | verify(mockSharedPreferences.setString(
70 | tEndpoint,
71 | expectedJsonString,
72 | ));
73 | },
74 | );
75 | });
76 | }
77 |
--------------------------------------------------------------------------------
/test/data/datasources/movies/movies_remote_data_source_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:matcher/matcher.dart';
5 | import 'package:mockito/mockito.dart';
6 | import 'package:http/http.dart' as http;
7 | import 'package:my_movie_list/core/errors/exception.dart';
8 | import 'package:my_movie_list/core/api/movies_api.dart';
9 | import 'package:my_movie_list/core/api/movies_endpoint.dart';
10 | import 'package:my_movie_list/data/datasources/movies/movies_remote_data_source.dart';
11 | import 'package:my_movie_list/domain/entities/movie.dart';
12 |
13 | import '../../../fixtures/fixture_reader.dart';
14 |
15 | class MockHttpClient extends Mock implements http.Client {}
16 |
17 | void main() {
18 | MoviesRemoteDataSourceImpl dataSource;
19 | MockHttpClient mockHttpClient;
20 |
21 | setUp(() {
22 | mockHttpClient = MockHttpClient();
23 | dataSource = MoviesRemoteDataSourceImpl(client: mockHttpClient);
24 | });
25 |
26 | void setUpMockHttpClientSuccess200(String jsonFile) {
27 | when(mockHttpClient.get(any)).thenAnswer(
28 | (_) async => http.Response(
29 | fixture(jsonFile),
30 | 200,
31 | headers: {
32 | HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8',
33 | },
34 | ),
35 | );
36 | }
37 |
38 | void setUpMockHttpClientFailure404() {
39 | when(mockHttpClient.get(any)).thenAnswer(
40 | (_) async => http.Response('Something went wrong', 404),
41 | );
42 | }
43 |
44 | group('getMovies', () {
45 | final tLanguage = MoviesApi.es;
46 | final tEndpoint = MoviesEndpoint.withGenre;
47 |
48 | test(
49 | 'should preform a GET request on a URL',
50 | () {
51 | // arrange
52 | setUpMockHttpClientSuccess200('movies_now_playing.json');
53 | // act
54 | dataSource.getMovies(tEndpoint, tLanguage, -1);
55 | // assert
56 | verify(mockHttpClient.get(
57 | MoviesApi.getMovies(tEndpoint, tLanguage, -1),
58 | ));
59 | },
60 | );
61 |
62 | test(
63 | 'should return List when response code is 200 (success)',
64 | () async {
65 | // arrange
66 | setUpMockHttpClientSuccess200('movies_now_playing.json');
67 | // act
68 | final result = await dataSource.getMovies(tEndpoint, tLanguage, -1);
69 | // arrange
70 | expect(result, isA>());
71 | },
72 | );
73 |
74 | test(
75 | 'should throw a ServerException when response code is 404 or other',
76 | () async {
77 | // arrange
78 | setUpMockHttpClientFailure404();
79 | // act
80 | final call = dataSource.getMovies;
81 | // assert
82 | expect(
83 | () => call(tEndpoint, tLanguage, -1),
84 | throwsA(TypeMatcher()),
85 | );
86 | },
87 | );
88 | });
89 |
90 | group('searchMovies', () {
91 | final tLanguage = MoviesApi.es;
92 | final tQuery = 'k';
93 |
94 | test(
95 | 'should return List when response code is 200 (success)',
96 | () async {
97 | // arrange
98 | setUpMockHttpClientSuccess200('movies_now_playing.json');
99 | // act
100 | final result = await dataSource.searchMovies(tLanguage, tQuery);
101 | // assert
102 | expect(result, isA>());
103 | },
104 | );
105 |
106 | test(
107 | 'should throw a ServerException when response code is 404 or other',
108 | () async {
109 | // arrange
110 | setUpMockHttpClientFailure404();
111 | // act
112 | final call = dataSource.searchMovies;
113 | // assert
114 | expect(
115 | () => call(tLanguage, tQuery),
116 | throwsA(TypeMatcher()),
117 | );
118 | },
119 | );
120 | });
121 |
122 | group('getMovieDetail', () {
123 | final tLanguage = MoviesApi.es;
124 | final tMovieId = 399566;
125 |
126 | test(
127 | 'should return a Movie when response code is 200 (success)',
128 | () async {
129 | // arrange
130 | setUpMockHttpClientSuccess200('movie_detail.json');
131 | // act
132 | final result = await dataSource.getMovieDetail(tLanguage, tMovieId);
133 | // assert
134 | expect(result, isA());
135 | },
136 | );
137 |
138 | test(
139 | 'should throw a ServerException when response code is 404 or other',
140 | () async {
141 | // arrange
142 | setUpMockHttpClientFailure404();
143 | // act
144 | final call = dataSource.getMovieDetail;
145 | // assert
146 | expect(
147 | () => call(tLanguage, tMovieId),
148 | throwsA(TypeMatcher()),
149 | );
150 | },
151 | );
152 | });
153 | }
154 |
--------------------------------------------------------------------------------
/test/data/repositories/genres_repository_impl_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/errors/exception.dart';
5 | import 'package:my_movie_list/core/errors/failure.dart';
6 | import 'package:my_movie_list/core/api/movies_api.dart';
7 | import 'package:my_movie_list/core/network/network_info.dart';
8 | import 'package:my_movie_list/data/datasources/genres/genres_local_data_source.dart';
9 | import 'package:my_movie_list/data/datasources/genres/genres_remote_data_source.dart';
10 | import 'package:my_movie_list/data/repositories/genres_repository_impl.dart';
11 | import 'package:my_movie_list/domain/entities/genre.dart';
12 |
13 | class MockRemoteDataSource extends Mock implements GenresRemoteDataSource {}
14 |
15 | class MockLocalDataSource extends Mock implements GenresLocalDataSource {}
16 |
17 | class MockNetworkInfo extends Mock implements NetworkInfo {}
18 |
19 | void main() {
20 | GenresRepositoryImpl repository;
21 | MockRemoteDataSource mockRemoteDataSource;
22 | MockLocalDataSource mockLocalDataSource;
23 | MockNetworkInfo mockNetworkInfo;
24 |
25 | setUp(() {
26 | mockRemoteDataSource = MockRemoteDataSource();
27 | mockLocalDataSource = MockLocalDataSource();
28 | mockNetworkInfo = MockNetworkInfo();
29 | repository = GenresRepositoryImpl(
30 | remoteDataSource: mockRemoteDataSource,
31 | localDataSource: mockLocalDataSource,
32 | networkInfo: mockNetworkInfo,
33 | );
34 | });
35 |
36 | void runTestsOnline(Function body) {
37 | group('device is online', () {
38 | setUp(() {
39 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
40 | });
41 |
42 | body();
43 | });
44 | }
45 |
46 | void runTestsOffline(Function body) {
47 | group('device is offline', () {
48 | setUp(() {
49 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => false);
50 | });
51 |
52 | body();
53 | });
54 | }
55 |
56 | group('getGenres', () {
57 | final tLanguage = MoviesApi.en;
58 | final tGenresList = [Genre(id: 1, name: 'test')];
59 |
60 | test(
61 | 'should check if the device is online',
62 | () {
63 | // arrange
64 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
65 | // act
66 | repository.getGenres(tLanguage);
67 | // assert
68 | verify(mockNetworkInfo.isConnected);
69 | },
70 | );
71 |
72 | runTestsOnline(() {
73 | test(
74 | 'should return remote data when the call to remote data source is successful',
75 | () async {
76 | // arrange
77 | when(mockRemoteDataSource.getGenres(any))
78 | .thenAnswer((_) async => tGenresList);
79 | // act
80 | final result = await repository.getGenres(tLanguage);
81 | // assert
82 | verify(mockRemoteDataSource.getGenres(tLanguage));
83 | expect(result, equals(Right(tGenresList)));
84 | },
85 | );
86 |
87 | test(
88 | 'should cache the data locally when the call to remote data source is successful',
89 | () async {
90 | // arrange
91 | when(mockRemoteDataSource.getGenres(any))
92 | .thenAnswer((_) async => tGenresList);
93 | // act
94 | await repository.getGenres(tLanguage);
95 | // assert
96 | verify(mockRemoteDataSource.getGenres(tLanguage));
97 | verify(mockLocalDataSource.cacheGenres(tGenresList));
98 | },
99 | );
100 |
101 | test(
102 | 'should return server failure when the call to remote data source is unsuccessful',
103 | () async {
104 | // arrange
105 | when(mockRemoteDataSource.getGenres(any))
106 | .thenThrow(ServerException());
107 | // act
108 | final result = await repository.getGenres(tLanguage);
109 | // assert
110 | verify(mockRemoteDataSource.getGenres(tLanguage));
111 | expect(result, equals(Left(ServerFailure())));
112 | },
113 | );
114 | });
115 |
116 | runTestsOffline(() {
117 | test(
118 | 'should return last locally cached data when the cached data is present',
119 | () async {
120 | // arrange
121 | when(mockLocalDataSource.getLastGenres())
122 | .thenAnswer((_) async => tGenresList);
123 | // act
124 | final result = await repository.getGenres(tLanguage);
125 | // assert
126 | verifyZeroInteractions(mockRemoteDataSource);
127 | verify(mockLocalDataSource.getLastGenres());
128 | expect(result, equals(Right(tGenresList)));
129 | },
130 | );
131 |
132 | test(
133 | 'shoul return CacheFailure when there is no cached data present',
134 | () async {
135 | // arrange
136 | when(mockLocalDataSource.getLastGenres()).thenThrow(CacheException());
137 | // act
138 | final result = await repository.getGenres(tLanguage);
139 | // assert
140 | verifyZeroInteractions(mockRemoteDataSource);
141 | verify(mockLocalDataSource.getLastGenres());
142 | expect(result, equals(Left(CacheFailure())));
143 | },
144 | );
145 | });
146 | });
147 | }
148 |
--------------------------------------------------------------------------------
/test/data/repositories/movie_repository_impl_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/api/movies_api.dart';
5 | import 'package:my_movie_list/core/api/movies_endpoint.dart';
6 | import 'package:my_movie_list/core/errors/exception.dart';
7 | import 'package:my_movie_list/core/errors/failure.dart';
8 | import 'package:my_movie_list/core/network/network_info.dart';
9 | import 'package:my_movie_list/data/datasources/movies/movies_local_data_source.dart';
10 | import 'package:my_movie_list/data/datasources/movies/movies_remote_data_source.dart';
11 | import 'package:my_movie_list/data/repositories/movies_repository_impl.dart';
12 | import 'package:my_movie_list/domain/entities/movie.dart';
13 |
14 | class MockRemoteDataSource extends Mock implements MoviesRemoteDataSource {}
15 |
16 | class MockLocalDataSource extends Mock implements MoviesLocalDataSource {}
17 |
18 | class MockNetworkInfo extends Mock implements NetworkInfo {}
19 |
20 | void main() {
21 | MoviesRepositoryImpl repository;
22 | MockRemoteDataSource mockRemoteDataSource;
23 | MockLocalDataSource mockLocalDataSource;
24 | MockNetworkInfo mockNetworkInfo;
25 |
26 | setUp(() {
27 | mockRemoteDataSource = MockRemoteDataSource();
28 | mockLocalDataSource = MockLocalDataSource();
29 | mockNetworkInfo = MockNetworkInfo();
30 |
31 | repository = MoviesRepositoryImpl(
32 | remoteDataSource: mockRemoteDataSource,
33 | localDataSource: mockLocalDataSource,
34 | networkInfo: mockNetworkInfo,
35 | );
36 | });
37 |
38 | void runTestsOnline(Function body) {
39 | group('device is online', () {
40 | setUp(() {
41 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
42 | });
43 |
44 | body();
45 | });
46 | }
47 |
48 | void runTestsOffline(Function body) {
49 | group('device is offline', () {
50 | setUp(() {
51 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => false);
52 | });
53 |
54 | body();
55 | });
56 | }
57 |
58 | group('getMovies', () {
59 | // DATA FOR THE MOCKS AND ASSERTIONS
60 | final tLanguage = MoviesApi.en;
61 | final tEndpoint = MoviesEndpoint.withGenre;
62 | final genreId = -1;
63 | final List tMovieModelList = [];
64 |
65 | test(
66 | 'should check if the device is online',
67 | () {
68 | // arrange
69 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
70 | // act
71 | repository.getMovies(tEndpoint, tLanguage, -1);
72 | // assert
73 | verify(mockNetworkInfo.isConnected);
74 | },
75 | );
76 |
77 | runTestsOnline(() {
78 | setUp(() {
79 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
80 | });
81 |
82 | test(
83 | 'should return remote data when the call to remote data source is successful',
84 | () async {
85 | // arrange
86 | when(
87 | mockRemoteDataSource.getMovies(any, any, any),
88 | ).thenAnswer((_) async => tMovieModelList);
89 | // act
90 | final result =
91 | await repository.getMovies(tEndpoint, tLanguage, genreId);
92 | // assert
93 | verify(mockRemoteDataSource.getMovies(any, any, any));
94 | expect(result, equals(Right(tMovieModelList)));
95 | },
96 | );
97 |
98 | test(
99 | 'should cache the data locally when the call to remote data source is successful',
100 | () async {
101 | // arrange
102 | when(mockRemoteDataSource.getMovies(any, any, any))
103 | .thenAnswer((_) async => tMovieModelList);
104 | // act
105 | await repository.getMovies(tEndpoint, tLanguage, genreId);
106 | // assert
107 | verify(mockRemoteDataSource.getMovies(tEndpoint, tLanguage, genreId));
108 |
109 | verify(
110 | mockLocalDataSource.cacheMovies(
111 | tEndpoint + '$genreId',
112 | tMovieModelList,
113 | ),
114 | );
115 | },
116 | );
117 |
118 | test(
119 | 'should return server failure when the call to remote data source is unsuccessful',
120 | () async {
121 | // arrange
122 | when(mockRemoteDataSource.getMovies(any, any, any))
123 | .thenThrow(ServerException());
124 | // act
125 | final result =
126 | await repository.getMovies(tEndpoint, tLanguage, genreId);
127 | // assert
128 | verify(mockRemoteDataSource.getMovies(tEndpoint, tLanguage, genreId));
129 | expect(result, equals(Left(ServerFailure())));
130 | },
131 | );
132 | });
133 |
134 | runTestsOffline(() {
135 | test(
136 | 'should return last locally cached data when the cached data is present',
137 | () async {
138 | // arrange
139 | when(mockLocalDataSource.getLastMovies(tEndpoint + '$genreId'))
140 | .thenAnswer((_) async => tMovieModelList);
141 | // act
142 | final result =
143 | await repository.getMovies(tEndpoint, tLanguage, genreId);
144 | // assert
145 | verifyZeroInteractions(mockRemoteDataSource);
146 | verify(mockLocalDataSource.getLastMovies(tEndpoint + '$genreId'));
147 | expect(result, equals(Right(tMovieModelList)));
148 | },
149 | );
150 |
151 | test('should return CacheFailure when there is no cached data present',
152 | () async {
153 | // arrange
154 | when(mockLocalDataSource.getLastMovies(tEndpoint + '$genreId'))
155 | .thenThrow(CacheException());
156 | // act
157 | final result =
158 | await repository.getMovies(tEndpoint, tLanguage, genreId);
159 | // assert
160 | verifyZeroInteractions(mockRemoteDataSource);
161 | verify(mockLocalDataSource.getLastMovies(tEndpoint + '$genreId'));
162 | expect(result, equals(Left(CacheFailure())));
163 | });
164 | });
165 | });
166 |
167 | group('searchMovies', () {
168 | final tLanguage = MoviesApi.en;
169 | final tQuery = 'k';
170 | final List tMovieList = [];
171 |
172 | test(
173 | 'should check if the device is online',
174 | () {
175 | // arrange
176 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
177 | // act
178 | repository.searchMovies(tLanguage, tQuery);
179 | // assert
180 | verify(mockNetworkInfo.isConnected);
181 | },
182 | );
183 |
184 | runTestsOnline(() {
185 | test(
186 | 'should return remote data when the call to remote data source is successful',
187 | () async {
188 | // arrange
189 | when(
190 | mockRemoteDataSource.searchMovies(any, any),
191 | ).thenAnswer((_) async => tMovieList);
192 | // act
193 | final result = await repository.searchMovies(tLanguage, tQuery);
194 | // assert
195 | verify(mockRemoteDataSource.searchMovies(any, any));
196 | expect(result, equals(Right(tMovieList)));
197 | },
198 | );
199 |
200 | test(
201 | 'should return server failure when the call to remote data source is unsuccessful',
202 | () async {
203 | // arrange
204 | when(mockRemoteDataSource.searchMovies(any, any))
205 | .thenThrow(ServerException());
206 | // act
207 | final result = await repository.searchMovies(tLanguage, tQuery);
208 | // assert
209 | verify(mockRemoteDataSource.searchMovies(tLanguage, tQuery));
210 | expect(result, equals(Left(ServerFailure())));
211 | },
212 | );
213 | });
214 |
215 | runTestsOffline(() {
216 | test(
217 | 'should return internet failure when there is no internet',
218 | () async {
219 | // act
220 | final result = await repository.searchMovies(tLanguage, tQuery);
221 | // assert
222 | verifyZeroInteractions(mockRemoteDataSource);
223 | expect(result, equals(Left(InternetFailure())));
224 | },
225 | );
226 | });
227 | });
228 |
229 | group('getMovieDetail', () {
230 | final tLanguage = MoviesApi.en;
231 | final tMovieId = 399566;
232 | final tMovieModel = Movie();
233 |
234 | test(
235 | 'should check if the device is online',
236 | () {
237 | // arrange
238 | when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
239 | // act
240 | repository.getMovieDetail(tLanguage, tMovieId);
241 | // assert
242 | verify(mockNetworkInfo.isConnected);
243 | },
244 | );
245 |
246 | runTestsOnline(() {
247 | test(
248 | 'should return remote data when the call to remote data source is successful',
249 | () async {
250 | // arrange
251 | when(
252 | mockRemoteDataSource.getMovieDetail(any, any),
253 | ).thenAnswer((_) async => tMovieModel);
254 | // act
255 | final result = await repository.getMovieDetail(tLanguage, tMovieId);
256 | // assert
257 | verify(mockRemoteDataSource.getMovieDetail(any, any));
258 | expect(result, equals(Right(tMovieModel)));
259 | },
260 | );
261 |
262 | test(
263 | 'should return server failure when the call to remote data source is unsuccessful',
264 | () async {
265 | // arrange
266 | when(mockRemoteDataSource.getMovieDetail(any, any))
267 | .thenThrow(ServerException());
268 | // act
269 | final result = await repository.getMovieDetail(tLanguage, tMovieId);
270 | // assert
271 | verify(mockRemoteDataSource.getMovieDetail(tLanguage, tMovieId));
272 | expect(result, equals(Left(ServerFailure())));
273 | },
274 | );
275 | });
276 |
277 | runTestsOffline(() {
278 | test(
279 | 'should return internet failure when there is no internet',
280 | () async {
281 | // act
282 | final result = await repository.getMovieDetail(tLanguage, tMovieId);
283 | // assert
284 | verifyZeroInteractions(mockRemoteDataSource);
285 | expect(result, equals(Left(InternetFailure())));
286 | },
287 | );
288 | });
289 | });
290 | }
291 |
--------------------------------------------------------------------------------
/test/domain/usecases/get_genres_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/api/movies_api.dart';
5 | import 'package:my_movie_list/domain/entities/genre.dart';
6 | import 'package:my_movie_list/domain/repositories/genres_repository.dart';
7 | import 'package:my_movie_list/domain/usecases/get_genres.dart';
8 |
9 | class MockGenresRepository extends Mock implements GenresRepository {}
10 |
11 | void main() {
12 | GetGenres usecase;
13 | MockGenresRepository mockMovieRepository;
14 |
15 | setUp(() {
16 | mockMovieRepository = MockGenresRepository();
17 | usecase = GetGenres(mockMovieRepository);
18 | });
19 |
20 | final tLanguage = MoviesApi.en;
21 | final tGenreList = [Genre(id: 1, name: 'test')];
22 |
23 | test(
24 | 'shoul get genres list from the repository',
25 | () async {
26 | // arrange
27 | when(mockMovieRepository.getGenres(any))
28 | .thenAnswer((_) async => Right(tGenreList));
29 | // act
30 | final result = await usecase(GenresParams(language: tLanguage));
31 | // assert
32 | expect(result, Right(tGenreList));
33 | verify(mockMovieRepository.getGenres(tLanguage));
34 | verifyNoMoreInteractions(mockMovieRepository);
35 | },
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/test/domain/usecases/get_movie_detail_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/api/movies_api.dart';
5 | import 'package:my_movie_list/domain/entities/movie.dart';
6 | import 'package:my_movie_list/domain/repositories/movies_repository.dart';
7 | import 'package:my_movie_list/domain/usecases/get_movie_detail.dart';
8 |
9 | class MockMoviesRepository extends Mock implements MoviesRepository {}
10 |
11 | void main() {
12 | GetMovieDetail usecase;
13 | MockMoviesRepository mockMoviesRepository;
14 |
15 | setUp(() {
16 | mockMoviesRepository = MockMoviesRepository();
17 | usecase = GetMovieDetail(mockMoviesRepository);
18 | });
19 |
20 | final tLanguage = MoviesApi.en;
21 | final tMovieId = 399566;
22 | final tMovie = Movie();
23 |
24 | test(
25 | 'should get a movie from repository',
26 | () async {
27 | // arrange
28 | when(mockMoviesRepository.getMovieDetail(any, any))
29 | .thenAnswer((_) async => Right(tMovie));
30 | // act
31 | final result = await usecase(
32 | MovieDetailParams(language: tLanguage, movieId: tMovieId),
33 | );
34 | // assert
35 | expect(result, Right(tMovie));
36 | verify(mockMoviesRepository.getMovieDetail(tLanguage, tMovieId));
37 | verifyNoMoreInteractions(mockMoviesRepository);
38 | },
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/test/domain/usecases/get_movies_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/api/movies_api.dart';
5 | import 'package:my_movie_list/core/api/movies_endpoint.dart';
6 | import 'package:my_movie_list/domain/entities/movie.dart';
7 | import 'package:my_movie_list/domain/repositories/movies_repository.dart';
8 | import 'package:my_movie_list/domain/usecases/get_movies.dart';
9 |
10 | class MockMovieRepository extends Mock implements MoviesRepository {}
11 |
12 | void main() {
13 | GetMovies usecase;
14 | MockMovieRepository mockMovieRepository;
15 |
16 | setUp(() {
17 | mockMovieRepository = MockMovieRepository();
18 | usecase = GetMovies(mockMovieRepository);
19 | });
20 |
21 | final tLanguage = MoviesApi.en;
22 | final tEndpoint = MoviesEndpoint.withGenre;
23 | final genreId = -1;
24 | final List tMovieList = [];
25 |
26 | test(
27 | 'shoul get movie list from the repository',
28 | () async {
29 | // arrange
30 | when(mockMovieRepository.getMovies(any, any, any))
31 | .thenAnswer((_) async => Right(tMovieList));
32 | // act
33 | final result = await usecase(
34 | Params(endpoint: tEndpoint, language: tLanguage, genreId: genreId));
35 | // assert
36 | expect(result, Right(tMovieList));
37 | verify(mockMovieRepository.getMovies(tEndpoint, tLanguage, genreId));
38 | verifyNoMoreInteractions(mockMovieRepository);
39 | },
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/test/domain/usecases/search_movies_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/api/movies_api.dart';
5 | import 'package:my_movie_list/domain/entities/movie.dart';
6 | import 'package:my_movie_list/domain/repositories/movies_repository.dart';
7 | import 'package:my_movie_list/domain/usecases/search_movies.dart';
8 |
9 | class MockMovieRepository extends Mock implements MoviesRepository {}
10 |
11 | void main() {
12 | SearchMovies usecase;
13 | MockMovieRepository mockMovieRepository;
14 |
15 | setUp(() {
16 | mockMovieRepository = MockMovieRepository();
17 | usecase = SearchMovies(mockMovieRepository);
18 | });
19 |
20 | final List tMovieList = [];
21 | final tLanguage = MoviesApi.es;
22 | final tQuery = 'k';
23 |
24 | test(
25 | 'shoul get movie list from the repository',
26 | () async {
27 | // arrange
28 | when(mockMovieRepository.searchMovies(any, any))
29 | .thenAnswer((_) async => Right(tMovieList));
30 | // act
31 | final result = await usecase(
32 | SearchMoviesParams(language: tLanguage, query: tQuery),
33 | );
34 | // assert
35 | expect(result, Right(tMovieList));
36 | verify(mockMovieRepository.searchMovies(tLanguage, tQuery));
37 | verifyNoMoreInteractions(mockMovieRepository);
38 | },
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/test/fixtures/fixture_reader.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | String fixture(String name) {
4 | var dir = Directory.current.path;
5 | if (dir.endsWith('/test')) {
6 | dir = dir.replaceAll('/test', '');
7 | }
8 | return File('$dir/test/fixtures/$name').readAsStringSync();
9 | }
10 |
--------------------------------------------------------------------------------
/test/fixtures/genres.json:
--------------------------------------------------------------------------------
1 | {
2 | "genres": [
3 | {
4 | "id": 28,
5 | "name": "Action"
6 | },
7 | {
8 | "id": 12,
9 | "name": "Adventure"
10 | },
11 | {
12 | "id": 16,
13 | "name": "Animation"
14 | },
15 | {
16 | "id": 35,
17 | "name": "Comedy"
18 | },
19 | {
20 | "id": 80,
21 | "name": "Crime"
22 | },
23 | {
24 | "id": 99,
25 | "name": "Documentary"
26 | },
27 | {
28 | "id": 18,
29 | "name": "Drama"
30 | },
31 | {
32 | "id": 10751,
33 | "name": "Family"
34 | },
35 | {
36 | "id": 14,
37 | "name": "Fantasy"
38 | },
39 | {
40 | "id": 36,
41 | "name": "History"
42 | },
43 | {
44 | "id": 27,
45 | "name": "Horror"
46 | },
47 | {
48 | "id": 10402,
49 | "name": "Music"
50 | },
51 | {
52 | "id": 9648,
53 | "name": "Mystery"
54 | },
55 | {
56 | "id": 10749,
57 | "name": "Romance"
58 | },
59 | {
60 | "id": 878,
61 | "name": "Science Fiction"
62 | },
63 | {
64 | "id": 10770,
65 | "name": "TV Movie"
66 | },
67 | {
68 | "id": 53,
69 | "name": "Thriller"
70 | },
71 | {
72 | "id": 10752,
73 | "name": "War"
74 | },
75 | {
76 | "id": 37,
77 | "name": "Western"
78 | }
79 | ]
80 | }
81 |
--------------------------------------------------------------------------------
/test/fixtures/genres_cached.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 28,
4 | "name": "Action"
5 | },
6 | {
7 | "id": 12,
8 | "name": "Adventure"
9 | },
10 | {
11 | "id": 16,
12 | "name": "Animation"
13 | },
14 | {
15 | "id": 35,
16 | "name": "Comedy"
17 | },
18 | {
19 | "id": 80,
20 | "name": "Crime"
21 | },
22 | {
23 | "id": 99,
24 | "name": "Documentary"
25 | },
26 | {
27 | "id": 18,
28 | "name": "Drama"
29 | },
30 | {
31 | "id": 10751,
32 | "name": "Family"
33 | },
34 | {
35 | "id": 14,
36 | "name": "Fantasy"
37 | },
38 | {
39 | "id": 36,
40 | "name": "History"
41 | },
42 | {
43 | "id": 27,
44 | "name": "Horror"
45 | },
46 | {
47 | "id": 10402,
48 | "name": "Music"
49 | },
50 | {
51 | "id": 9648,
52 | "name": "Mystery"
53 | },
54 | {
55 | "id": 10749,
56 | "name": "Romance"
57 | },
58 | {
59 | "id": 878,
60 | "name": "Science Fiction"
61 | },
62 | {
63 | "id": 10770,
64 | "name": "TV Movie"
65 | },
66 | {
67 | "id": 53,
68 | "name": "Thriller"
69 | },
70 | {
71 | "id": 10752,
72 | "name": "War"
73 | },
74 | {
75 | "id": 37,
76 | "name": "Western"
77 | }
78 | ]
79 |
--------------------------------------------------------------------------------
/test/fixtures/movie_detail.json:
--------------------------------------------------------------------------------
1 | {
2 | "adult": false,
3 | "backdrop_path": "/inJjDhCjfhh3RtrJWBmmDqeuSYC.jpg",
4 | "belongs_to_collection": {
5 | "id": 535313,
6 | "name": "Godzilla Collection",
7 | "poster_path": "/inNN466SKHNjbGmpfhfsaPQNleS.jpg",
8 | "backdrop_path": "/oboBn4VYB79uDxnyIri0Nt3U3N2.jpg"
9 | },
10 | "budget": 200000000,
11 | "genres": [
12 | {
13 | "id": 878,
14 | "name": "Science Fiction"
15 | },
16 | {
17 | "id": 28,
18 | "name": "Action"
19 | },
20 | {
21 | "id": 18,
22 | "name": "Drama"
23 | }
24 | ],
25 | "homepage": "https://www.godzillavskong.net/",
26 | "id": 399566,
27 | "imdb_id": "tt5034838",
28 | "original_language": "en",
29 | "original_title": "Godzilla vs. Kong",
30 | "overview": "In atime when monsters walk the Earth, humanity’s fight for its future sets Godzilla and Kong on a collision course that will see the two most powerful forces of nature on the planet collide in a spectacular battle for the ages.",
31 | "popularity": 1564.627,
32 | "poster_path": "/pgqgaUx1cJb5oZQQ5v0tNARCeBp.jpg",
33 | "production_companies": [
34 | {
35 | "id": 174,
36 | "logo_path": "/IuAlhI9eVC9Z8UQWOIDdWRKSEJ.png",
37 | "name": "Warner Bros. Pictures",
38 | "origin_country": "US"
39 | },
40 | {
41 | "id": 923,
42 | "logo_path": "/5UQsZrfbfG2dYJbx8DxfoTr2Bvu.png",
43 | "name": "Legendary Pictures",
44 | "origin_country": "US"
45 | }
46 | ],
47 | "production_countries": [
48 | {
49 | "iso_3166_1": "US",
50 | "name": "United States of America"
51 | }
52 | ],
53 | "release_date": "2021-03-24",
54 | "revenue": 415590000,
55 | "runtime": 113,
56 | "spoken_languages": [
57 | {
58 | "english_name": "English",
59 | "iso_639_1": "en",
60 | "name": "English"
61 | },
62 | {
63 | "english_name": "Turkish",
64 | "iso_639_1": "tr",
65 | "name": "Türkçe"
66 | }
67 | ],
68 | "status": "Released",
69 | "tagline": "One Will Fall",
70 | "title": "Godzilla vs. Kong",
71 | "video": false,
72 | "vote_average": 8.1,
73 | "vote_count": 5559
74 | }
75 |
--------------------------------------------------------------------------------
/test/presentation/custom_drawer/business_logic/drawer_nav_cubit/drawer_nav_cubit_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:my_movie_list/domain/entities/genre.dart';
3 | import 'package:my_movie_list/presentation/custom_drawer/business_logic/drawer_nav_cubit/drawer_nav_cubit.dart';
4 |
5 | void main() {
6 | DrawerNavCubit cubit;
7 |
8 | setUp(() {
9 | cubit = DrawerNavCubit();
10 | });
11 |
12 | final tGenre = Genre(id: 28, name: "Action");
13 |
14 | test(
15 | 'initState should be DrawerNavPopular',
16 | () {
17 | // assert
18 | expect(cubit.state, equals(DrawerNavInitial()));
19 | },
20 | );
21 |
22 | test(
23 | 'should emit DrawerNavWithGenres when requested',
24 | () {
25 | // assert layer
26 | final expected = [DrawerNavGenre(tGenre)];
27 | expectLater(cubit.stream, emitsInOrder(expected));
28 | //act
29 | cubit.getWithGenre(tGenre);
30 | },
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/test/presentation/genres/business_logic/genres_cubit_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:mockito/mockito.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:my_movie_list/core/errors/failure.dart';
5 | import 'package:my_movie_list/core/api/movies_api.dart';
6 | import 'package:my_movie_list/domain/entities/genre.dart';
7 | import 'package:my_movie_list/domain/usecases/get_genres.dart';
8 | import 'package:my_movie_list/presentation/genres/business_logic/genres_cubit.dart';
9 |
10 | class MockGetGenres extends Mock implements GetGenres {}
11 |
12 | void main() {
13 | GenresCubit cubit;
14 | MockGetGenres mockGetGenres;
15 |
16 | setUp(() {
17 | mockGetGenres = MockGetGenres();
18 | cubit = GenresCubit(getGenres: mockGetGenres);
19 | });
20 |
21 | test('initialState should be GenresInitial', () {
22 | // assert
23 | expect(cubit.state, equals(GenresInitial()));
24 | });
25 |
26 | group('genresGet', () {
27 | final List tGenreList = [];
28 | final tLanguage = MoviesApi.en;
29 |
30 | test(
31 | 'should get data from getGenres usecase',
32 | () async {
33 | // arrange
34 | when(mockGetGenres(any)).thenAnswer((_) async => Right(tGenreList));
35 | // act
36 | cubit.genresGet(language: tLanguage);
37 | await untilCalled(mockGetGenres(any));
38 | // assert
39 | verify(mockGetGenres(GenresParams(language: tLanguage)));
40 | },
41 | );
42 |
43 | test(
44 | 'should emit [GenresLoadInProgress, GenresLoadSuccess] when data is gotten successfully',
45 | () async {
46 | // arrange
47 | when(mockGetGenres(any)).thenAnswer((_) async => Right(tGenreList));
48 | // act
49 | final expected = [
50 | GenresLoadInProgress(),
51 | GenresLoadSuccess(genres: tGenreList),
52 | ];
53 | expectLater(cubit.stream, emitsInOrder(expected));
54 | // assert
55 | cubit.genresGet(language: tLanguage);
56 | },
57 | );
58 |
59 | test(
60 | 'should emit [GenresLoadInProgress, GenresLoadFailure] when data is gotten unsuccessfully',
61 | () async {
62 | // arrange
63 | when(mockGetGenres(any)).thenAnswer((_) async => Left(ServerFailure()));
64 | // act
65 | final expected = [
66 | GenresLoadInProgress(),
67 | GenresLoadFailure(message: FailureMessage.server),
68 | ];
69 | expectLater(cubit.stream, emitsInOrder(expected));
70 | // assert
71 | cubit.genresGet(language: tLanguage);
72 | },
73 | );
74 | });
75 | }
76 |
--------------------------------------------------------------------------------
/test/presentation/movies/business_logic/appbar_search_mode_cubit_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 | import 'package:my_movie_list/presentation/movies/business_logic/appbar_search_mode_cubit.dart';
3 |
4 | void main() {
5 | AppbarSearhModeCubit cubit;
6 |
7 | setUp(() {
8 | cubit = AppbarSearhModeCubit();
9 | });
10 |
11 | test(
12 | 'initState should be false',
13 | () {
14 | // assert
15 | expect(cubit.state, equals(false));
16 | },
17 | );
18 |
19 | test(
20 | 'should emit true when appbarModeSearch requested',
21 | () {
22 | // assert layer
23 | final expected = [true];
24 | expectLater(cubit.stream, emitsInOrder(expected));
25 | //act
26 | cubit.appbarModeSearch();
27 | },
28 | );
29 |
30 | test(
31 | 'should emit true when appbarModeNormal requested',
32 | () {
33 | // assert layer
34 | final expected = [false];
35 | expectLater(cubit.stream, emitsInOrder(expected));
36 | //act
37 | cubit.appbarModeNormal();
38 | },
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/test/presentation/movies/business_logic/movie_cast_cubit/movie_cast_cubit_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/api/movies_api.dart';
5 | import 'package:my_movie_list/core/errors/failure.dart';
6 | import 'package:my_movie_list/domain/entities/actor.dart';
7 | import 'package:my_movie_list/domain/usecases/get_movie_cast.dart';
8 | import 'package:my_movie_list/presentation/movies/business_logic/movie_cast_cubit/movie_cast_cubit.dart';
9 |
10 | class MockGetMovieCast extends Mock implements GetMovieCast {}
11 |
12 | void main() {
13 | MovieCastCubit cubit;
14 | MockGetMovieCast mockGetMovieCast;
15 |
16 | setUp(() {
17 | mockGetMovieCast = MockGetMovieCast();
18 | cubit = MovieCastCubit(getMovieCast: mockGetMovieCast);
19 | });
20 |
21 | test(
22 | 'initState should be MovieCastLoadInProgress',
23 | () {
24 | // assert
25 | expect(cubit.state, equals(MovieCastLoadInProgress()));
26 | },
27 | );
28 |
29 | group('getMovieCast', () {
30 | final tCast = [Actor()];
31 | final tLanguage = MoviesApi.en;
32 | final tMovieId = 399566;
33 |
34 | test(
35 | 'should get data from getMovieCast usecase',
36 | () async {
37 | // arrange
38 | when(mockGetMovieCast(any)).thenAnswer((_) async => Right(tCast));
39 | // act
40 | cubit.movieCastGet(language: tLanguage, movieId: tMovieId);
41 | await untilCalled(mockGetMovieCast(any));
42 | // assert
43 | verify(mockGetMovieCast(
44 | CastParams(language: tLanguage, movieId: tMovieId),
45 | ));
46 | },
47 | );
48 |
49 | test(
50 | 'should emit [MovieCastLoadInProgress, MovieCastLoadSuccess] when data is gotten successfully',
51 | () async {
52 | // arrange
53 | when(mockGetMovieCast(any)).thenAnswer((_) async => Right(tCast));
54 | // assert
55 | final expected = [
56 | MovieCastLoadInProgress(),
57 | MovieCastLoadSuccess(cast: tCast),
58 | ];
59 | expectLater(cubit.stream, emitsInOrder(expected));
60 | //act
61 | cubit.movieCastGet(language: tLanguage, movieId: tMovieId);
62 | },
63 | );
64 |
65 | test(
66 | 'should emit [MovieCastLoadInProgress, MovieCastLoadFailure] when getting data fails',
67 | () async {
68 | // arrange
69 | when(mockGetMovieCast(any)).thenAnswer(
70 | (_) async => Left(ServerFailure()),
71 | );
72 | // assert layer
73 | final expected = [
74 | MovieCastLoadInProgress(),
75 | MovieCastLoadFailure(message: FailureMessage.server),
76 | ];
77 | expectLater(cubit.stream, emitsInOrder(expected));
78 | //act
79 | cubit.movieCastGet(language: tLanguage, movieId: tMovieId);
80 | },
81 | );
82 |
83 | test(
84 | 'should emit [MoviesCastLoadInProgress, MoviesCastLoadFailure] when there is no internet',
85 | () async {
86 | // arrange
87 | when(mockGetMovieCast(any)).thenAnswer(
88 | (_) async => Left(InternetFailure()),
89 | );
90 | // assert layer
91 | final expected = [
92 | MovieCastLoadInProgress(),
93 | MovieCastLoadFailure(message: FailureMessage.internet),
94 | ];
95 | expectLater(cubit.stream, emitsInOrder(expected));
96 | //act
97 | cubit.movieCastGet(language: tLanguage, movieId: tMovieId);
98 | },
99 | );
100 | });
101 | }
102 |
--------------------------------------------------------------------------------
/test/presentation/movies/business_logic/movie_details_cubit/movie_details_cubit_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/api/movies_api.dart';
5 | import 'package:my_movie_list/core/errors/failure.dart';
6 | import 'package:my_movie_list/domain/entities/movie.dart';
7 | import 'package:my_movie_list/domain/usecases/get_movie_detail.dart';
8 | import 'package:my_movie_list/presentation/movies/business_logic/movie_details_cubit/movie_details_cubit.dart';
9 |
10 | class MockGetMovieDetails extends Mock implements GetMovieDetail {}
11 |
12 | void main() {
13 | MovieDetailsCubit cubit;
14 | MockGetMovieDetails mockGetMovieDetails;
15 |
16 | setUp(() {
17 | mockGetMovieDetails = MockGetMovieDetails();
18 | cubit = MovieDetailsCubit(getMovieDetail: mockGetMovieDetails);
19 | });
20 |
21 | test(
22 | 'initState should be MoviesSearchInitial ',
23 | () {
24 | // assert
25 | expect(cubit.state, equals(MovieDetailsLoadInProgress()));
26 | },
27 | );
28 |
29 | group('getMovieDetail', () {
30 | final tLanguage = MoviesApi.en;
31 | final tMovieId = 399566;
32 | final tMovie = Movie();
33 |
34 | test(
35 | 'should get data from getMovieDetail usecase',
36 | () async {
37 | // arrange
38 | when(mockGetMovieDetails(any)).thenAnswer((_) async => Right(tMovie));
39 | // act
40 | cubit.movieDetailsGet(language: tLanguage, movieId: tMovieId);
41 | await untilCalled(mockGetMovieDetails(any));
42 | // assert
43 | verify(mockGetMovieDetails(
44 | MovieDetailParams(language: tLanguage, movieId: tMovieId),
45 | ));
46 | },
47 | );
48 |
49 | test(
50 | 'should emit [MovieDetailsLoadInProgress, MovieDetailsLoadSuccess] when data is gotten successfully',
51 | () async {
52 | // arrange
53 | when(mockGetMovieDetails(any)).thenAnswer((_) async => Right(tMovie));
54 | // assert
55 | final expected = [
56 | MovieDetailsLoadInProgress(),
57 | MovieDetailsLoadSuccess(movie: tMovie),
58 | ];
59 | expectLater(cubit.stream, emitsInOrder(expected));
60 | //act
61 | cubit.movieDetailsGet(language: tLanguage, movieId: tMovieId);
62 | },
63 | );
64 |
65 | test(
66 | 'should emit [MovieDetailsLoadInProgress, MovieDetailsLoadFailure] when getting data fails',
67 | () async {
68 | // arrange
69 | when(mockGetMovieDetails(any)).thenAnswer(
70 | (_) async => Left(ServerFailure()),
71 | );
72 | // assert layer
73 | final expected = [
74 | MovieDetailsLoadInProgress(),
75 | MovieDetailsLoadFailure(message: FailureMessage.server),
76 | ];
77 | expectLater(cubit.stream, emitsInOrder(expected));
78 | //act
79 | cubit.movieDetailsGet(language: tLanguage, movieId: tMovieId);
80 | },
81 | );
82 |
83 | test(
84 | 'should emit [MoviesDetailsLoadInProgress, MoviesDetailsLoadFailure] when there is no internet',
85 | () async {
86 | // arrange
87 | when(mockGetMovieDetails(any)).thenAnswer(
88 | (_) async => Left(InternetFailure()),
89 | );
90 | // assert layer
91 | final expected = [
92 | MovieDetailsLoadInProgress(),
93 | MovieDetailsLoadFailure(message: FailureMessage.internet),
94 | ];
95 | expectLater(cubit.stream, emitsInOrder(expected));
96 | //act
97 | cubit.movieDetailsGet(language: tLanguage, movieId: tMovieId);
98 | },
99 | );
100 | });
101 | }
102 |
--------------------------------------------------------------------------------
/test/presentation/movies/business_logic/movies_bloc/movies_bloc_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:mockito/mockito.dart';
3 | import 'package:flutter_test/flutter_test.dart';
4 | import 'package:my_movie_list/core/errors/failure.dart';
5 | import 'package:my_movie_list/core/api/movies_api.dart';
6 | import 'package:my_movie_list/core/api/movies_endpoint.dart';
7 | import 'package:my_movie_list/domain/entities/movie.dart';
8 | import 'package:my_movie_list/domain/usecases/get_movies.dart';
9 | import 'package:my_movie_list/presentation/movies/business_logic/movies_bloc/movies_bloc.dart';
10 |
11 | class MockGetMovies extends Mock implements GetMovies {}
12 |
13 | void main() {
14 | MoviesBloc bloc;
15 | MockGetMovies mockGetMovies;
16 |
17 | setUp(() {
18 | mockGetMovies = MockGetMovies();
19 | bloc = MoviesBloc(getMovies: mockGetMovies);
20 | });
21 |
22 | test('initialState should be MoviesInitial', () {
23 | // assert
24 | expect(bloc.state, equals(MoviesInitial()));
25 | });
26 |
27 | group('MoviesGet', () {
28 | final List tMovieList = [];
29 | final tLanguage = MoviesApi.en;
30 | final genreId = -1;
31 | final tEndpoint = MoviesEndpoint.withGenre;
32 |
33 | test(
34 | 'should get data from getMovies usecase',
35 | () async {
36 | // arrange
37 | when(mockGetMovies(any)).thenAnswer((_) async => Right(tMovieList));
38 | // act
39 | bloc.add(
40 | MoviesGet(endpoint: tEndpoint, language: tLanguage, genre: genreId),
41 | );
42 | await untilCalled(mockGetMovies(any));
43 | // assert
44 | verify(mockGetMovies(Params(
45 | endpoint: tEndpoint, language: tLanguage, genreId: genreId)));
46 | },
47 | );
48 |
49 | test(
50 | 'should emit [MoviesLoadInProgress, MoviesLoadSuccess] when data is gotten successfully',
51 | () async {
52 | // arrange
53 | when(mockGetMovies(any)).thenAnswer((_) async => Right(tMovieList));
54 | // assert
55 | final expected = [
56 | MoviesLoadInProgress(),
57 | MoviesLoadSuccess(movies: tMovieList),
58 | ];
59 | expectLater(bloc.stream, emitsInOrder(expected));
60 | //act
61 | bloc.add(
62 | MoviesGet(endpoint: tEndpoint, language: tLanguage, genre: genreId),
63 | );
64 | },
65 | );
66 |
67 | test(
68 | 'should emit [LoadingMovies, ErrorMovies] when getting data fails',
69 | () async {
70 | // arrange
71 | when(mockGetMovies(any)).thenAnswer((_) async => Left(ServerFailure()));
72 | // assert layer
73 | final expected = [
74 | MoviesLoadInProgress(),
75 | MoviesLoadFailure(message: FailureMessage.server),
76 | ];
77 | expectLater(bloc.stream, emitsInOrder(expected));
78 | //act
79 | bloc.add(
80 | MoviesGet(endpoint: tEndpoint, language: tLanguage, genre: genreId),
81 | );
82 | },
83 | );
84 | });
85 | }
86 |
--------------------------------------------------------------------------------
/test/presentation/movies/business_logic/movies_search_cubit/movies_search_cubit_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:dartz/dartz.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:mockito/mockito.dart';
4 | import 'package:my_movie_list/core/errors/failure.dart';
5 | import 'package:my_movie_list/core/api/movies_api.dart';
6 | import 'package:my_movie_list/domain/entities/movie.dart';
7 | import 'package:my_movie_list/domain/usecases/search_movies.dart';
8 | import 'package:my_movie_list/presentation/movies/business_logic/movies_search_cubit/movies_search_cubit.dart';
9 |
10 | class MockSearchMovies extends Mock implements SearchMovies {}
11 |
12 | void main() {
13 | MoviesSearchCubit cubit;
14 | MockSearchMovies mockSearchMovies;
15 |
16 | setUp(() {
17 | mockSearchMovies = MockSearchMovies();
18 | cubit = MoviesSearchCubit(searchMovies: mockSearchMovies);
19 | });
20 |
21 | test(
22 | 'initState should be MoviesSearchInitial ',
23 | () {
24 | // assert
25 | expect(cubit.state, equals(MoviesSearchInitial()));
26 | },
27 | );
28 |
29 | group('moviesSearchRestart', () {
30 | test(
31 | 'should emit MoviesSearchInitial when it is call',
32 | () async {
33 | // act
34 | cubit.moviesSearchRestart();
35 | // assert
36 | expect(cubit.state, equals(MoviesSearchInitial()));
37 | },
38 | );
39 | });
40 |
41 | group('searchMovies', () {
42 | String tLanguage = MoviesApi.es;
43 | String tQuery = 'k';
44 | final List tMovieList = [];
45 |
46 | test(
47 | 'should get data from searchMovies usecase',
48 | () async {
49 | // arrange
50 | when(mockSearchMovies(any)).thenAnswer((_) async => Right(tMovieList));
51 | // act
52 | cubit.moviesSearch(language: tLanguage, query: tQuery);
53 | await untilCalled(mockSearchMovies(any));
54 | // assert
55 | verify(mockSearchMovies(
56 | SearchMoviesParams(language: tLanguage, query: tQuery),
57 | ));
58 | },
59 | );
60 |
61 | test(
62 | 'should emit [MoviesSearchLoadInProgress, MoviesSearchLoadSuccess] when data is gotten successfully',
63 | () async {
64 | // arrange
65 | when(mockSearchMovies(any)).thenAnswer((_) async => Right(tMovieList));
66 | // assert
67 | final expected = [
68 | MoviesSearchLoadInProgress(),
69 | MoviesSearchLoadSuccess(movies: tMovieList),
70 | ];
71 | expectLater(cubit.stream, emitsInOrder(expected));
72 | //act
73 | cubit.moviesSearch(language: tLanguage, query: tQuery);
74 | },
75 | );
76 |
77 | test(
78 | 'should emit [MoviesSearchLoadInProgress, MoviesSearchLoadFailure] when getting data fails',
79 | () async {
80 | // arrange
81 | when(mockSearchMovies(any))
82 | .thenAnswer((_) async => Left(ServerFailure()));
83 | // assert layer
84 | final expected = [
85 | MoviesSearchLoadInProgress(),
86 | MoviesSearchLoadFailure(message: FailureMessage.server),
87 | ];
88 | expectLater(cubit.stream, emitsInOrder(expected));
89 | //act
90 | cubit.moviesSearch(language: tLanguage, query: tQuery);
91 | },
92 | );
93 |
94 | test(
95 | 'should emit [MoviesSearchLoadInProgress, MoviesSearchLoadFailure] when there is no internet',
96 | () async {
97 | // arrange
98 | when(mockSearchMovies(any))
99 | .thenAnswer((_) async => Left(InternetFailure()));
100 | // assert layer
101 | final expected = [
102 | MoviesSearchLoadInProgress(),
103 | MoviesSearchLoadFailure(message: FailureMessage.internet),
104 | ];
105 | expectLater(cubit.stream, emitsInOrder(expected));
106 | //act
107 | cubit.moviesSearch(language: tLanguage, query: tQuery);
108 | },
109 | );
110 | });
111 | }
112 |
--------------------------------------------------------------------------------