├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── brol
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── brol_android.iml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ ├── Release.xcconfig
│ └── flutter_export_environment.sh
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ └── contents.xcworkspacedata
└── Runner
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-App-1024x1024@1x.png
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ └── Icon-App-83.5x83.5@2x.png
│ └── LaunchImage.imageset
│ │ ├── Contents.json
│ │ ├── LaunchImage.png
│ │ ├── LaunchImage@2x.png
│ │ ├── LaunchImage@3x.png
│ │ └── README.md
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ └── main.m
├── lib
├── api
│ └── tmdb_api.dart
├── blocs
│ ├── application_bloc.dart
│ ├── bloc_provider.dart
│ ├── favorite_bloc.dart
│ ├── favorite_movie_bloc.dart
│ └── movie_catalog_bloc.dart
├── main.dart
├── models
│ ├── movie_card.dart
│ ├── movie_filters.dart
│ ├── movie_genre.dart
│ ├── movie_genres_list.dart
│ └── movie_page_result.dart
├── pages
│ ├── details.dart
│ ├── favorites.dart
│ ├── filters.dart
│ ├── home.dart
│ ├── list.dart
│ └── list_one_page.dart
└── widgets
│ ├── favorite_button.dart
│ ├── favorite_widget.dart
│ ├── filters_summary.dart
│ ├── movie_card_widget.dart
│ ├── movie_details_container.dart
│ └── movie_details_widget.dart
├── pubspec.lock
└── pubspec.yaml
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 |
4 | .packages
5 | .pub/
6 |
7 | build/
8 |
9 | .flutter-plugins
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Streams - BLoC - Reactive Programming
2 |
3 | Sample application to illustrate the article available on [didierboelens.com](https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/).
4 |
5 | This article is an introduction to the notions of **Streams**, **BLoC Pattern** and **Reactive Programming** in Flutter.
6 |
7 |
--------------------------------------------------------------------------------
/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 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/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 flutter.compileSdkVersion
30 | ndkVersion flutter.ndkVersion
31 |
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_1_8
34 | targetCompatibility JavaVersion.VERSION_1_8
35 | }
36 |
37 | kotlinOptions {
38 | jvmTarget = '1.8'
39 | }
40 |
41 | sourceSets {
42 | main.java.srcDirs += 'src/main/kotlin'
43 | }
44 |
45 | defaultConfig {
46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
47 | applicationId "com.example.brol"
48 | // You can update the following values to match your application needs.
49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
50 | minSdkVersion flutter.minSdkVersion
51 | targetSdkVersion flutter.targetSdkVersion
52 | versionCode flutterVersionCode.toInteger()
53 | versionName flutterVersionName
54 | }
55 |
56 | buildTypes {
57 | release {
58 | // TODO: Add your own signing config for the release build.
59 | // Signing with the debug keys for now, so `flutter run --release` works.
60 | signingConfig signingConfigs.debug
61 | }
62 | }
63 | }
64 |
65 | flutter {
66 | source '../..'
67 | }
68 |
69 | dependencies {
70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
71 | }
72 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/brol/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.brol
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/brol_android.iml:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.7.10'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:7.2.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
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/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
6 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vagrant/
3 | .sconsign.dblite
4 | .svn/
5 |
6 | .DS_Store
7 | *.swp
8 | profile
9 |
10 | DerivedData/
11 | build/
12 | GeneratedPluginRegistrant.h
13 | GeneratedPluginRegistrant.m
14 |
15 | .generated/
16 |
17 | *.pbxuser
18 | *.mode1v3
19 | *.mode2v3
20 | *.perspectivev3
21 |
22 | !default.pbxuser
23 | !default.mode1v3
24 | !default.mode2v3
25 | !default.perspectivev3
26 |
27 | xcuserdata
28 |
29 | *.moved-aside
30 |
31 | *.pyc
32 | *sync/
33 | Icon?
34 | .tags*
35 |
36 | /Flutter/app.flx
37 | /Flutter/app.zip
38 | /Flutter/flutter_assets/
39 | /Flutter/App.framework
40 | /Flutter/Flutter.framework
41 | /Flutter/Generated.xcconfig
42 | /ServiceDefinitions.json
43 |
44 | Pods/
45 | .symlinks/
46 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
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 "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Flutter/flutter_export_environment.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # This is a generated file; do not edit or check into version control.
3 | export "FLUTTER_ROOT=d:\flutter"
4 | export "FLUTTER_APPLICATION_PATH=D:\Trash\Streams-Block-Reactive-Programming-in-Flutter"
5 | export "COCOAPODS_PARALLEL_CODE_SIGN=true"
6 | export "FLUTTER_TARGET=lib\main.dart"
7 | export "FLUTTER_BUILD_DIR=build"
8 | export "FLUTTER_BUILD_NAME=0.0.2"
9 | export "FLUTTER_BUILD_NUMBER=0.0.2"
10 | export "DART_OBFUSCATION=false"
11 | export "TRACK_WIDGET_CREATION=true"
12 | export "TREE_SHAKE_ICONS=false"
13 | export "PACKAGE_CONFIG=.dart_tool/package_config.json"
14 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
11 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };
12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
13 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
14 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
15 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
16 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
17 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
18 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; };
19 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
20 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
21 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
22 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
23 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXCopyFilesBuildPhase section */
27 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
28 | isa = PBXCopyFilesBuildPhase;
29 | buildActionMask = 2147483647;
30 | dstPath = "";
31 | dstSubfolderSpec = 10;
32 | files = (
33 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
34 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
35 | );
36 | name = "Embed Frameworks";
37 | runOnlyForDeploymentPostprocessing = 0;
38 | };
39 | /* End PBXCopyFilesBuildPhase section */
40 |
41 | /* Begin PBXFileReference section */
42 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
43 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
44 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };
45 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
46 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; };
47 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
48 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
49 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
50 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
51 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
52 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; };
53 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
54 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
55 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
56 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
57 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
58 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
59 | /* End PBXFileReference section */
60 |
61 | /* Begin PBXFrameworksBuildPhase section */
62 | 97C146EB1CF9000F007C117D /* Frameworks */ = {
63 | isa = PBXFrameworksBuildPhase;
64 | buildActionMask = 2147483647;
65 | files = (
66 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
67 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
68 | );
69 | runOnlyForDeploymentPostprocessing = 0;
70 | };
71 | /* End PBXFrameworksBuildPhase section */
72 |
73 | /* Begin PBXGroup section */
74 | 9740EEB11CF90186004384FC /* Flutter */ = {
75 | isa = PBXGroup;
76 | children = (
77 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,
78 | 3B80C3931E831B6300D905FE /* App.framework */,
79 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
80 | 9740EEBA1CF902C7004384FC /* Flutter.framework */,
81 | 9740EEB21CF90195004384FC /* Debug.xcconfig */,
82 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
83 | 9740EEB31CF90195004384FC /* Generated.xcconfig */,
84 | );
85 | name = Flutter;
86 | sourceTree = "";
87 | };
88 | 97C146E51CF9000F007C117D = {
89 | isa = PBXGroup;
90 | children = (
91 | 9740EEB11CF90186004384FC /* Flutter */,
92 | 97C146F01CF9000F007C117D /* Runner */,
93 | 97C146EF1CF9000F007C117D /* Products */,
94 | CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
95 | );
96 | sourceTree = "";
97 | };
98 | 97C146EF1CF9000F007C117D /* Products */ = {
99 | isa = PBXGroup;
100 | children = (
101 | 97C146EE1CF9000F007C117D /* Runner.app */,
102 | );
103 | name = Products;
104 | sourceTree = "";
105 | };
106 | 97C146F01CF9000F007C117D /* Runner */ = {
107 | isa = PBXGroup;
108 | children = (
109 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
110 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
111 | 97C146FA1CF9000F007C117D /* Main.storyboard */,
112 | 97C146FD1CF9000F007C117D /* Assets.xcassets */,
113 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
114 | 97C147021CF9000F007C117D /* Info.plist */,
115 | 97C146F11CF9000F007C117D /* Supporting Files */,
116 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
117 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
118 | );
119 | path = Runner;
120 | sourceTree = "";
121 | };
122 | 97C146F11CF9000F007C117D /* Supporting Files */ = {
123 | isa = PBXGroup;
124 | children = (
125 | 97C146F21CF9000F007C117D /* main.m */,
126 | );
127 | name = "Supporting Files";
128 | sourceTree = "";
129 | };
130 | /* End PBXGroup section */
131 |
132 | /* Begin PBXNativeTarget section */
133 | 97C146ED1CF9000F007C117D /* Runner */ = {
134 | isa = PBXNativeTarget;
135 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
136 | buildPhases = (
137 | 9740EEB61CF901F6004384FC /* Run Script */,
138 | 97C146EA1CF9000F007C117D /* Sources */,
139 | 97C146EB1CF9000F007C117D /* Frameworks */,
140 | 97C146EC1CF9000F007C117D /* Resources */,
141 | 9705A1C41CF9048500538489 /* Embed Frameworks */,
142 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
143 | );
144 | buildRules = (
145 | );
146 | dependencies = (
147 | );
148 | name = Runner;
149 | productName = Runner;
150 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
151 | productType = "com.apple.product-type.application";
152 | };
153 | /* End PBXNativeTarget section */
154 |
155 | /* Begin PBXProject section */
156 | 97C146E61CF9000F007C117D /* Project object */ = {
157 | isa = PBXProject;
158 | attributes = {
159 | LastUpgradeCheck = 0910;
160 | ORGANIZATIONNAME = "The Chromium Authors";
161 | TargetAttributes = {
162 | 97C146ED1CF9000F007C117D = {
163 | CreatedOnToolsVersion = 7.3.1;
164 | };
165 | };
166 | };
167 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
168 | compatibilityVersion = "Xcode 3.2";
169 | developmentRegion = English;
170 | hasScannedForEncodings = 0;
171 | knownRegions = (
172 | en,
173 | Base,
174 | );
175 | mainGroup = 97C146E51CF9000F007C117D;
176 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
177 | projectDirPath = "";
178 | projectRoot = "";
179 | targets = (
180 | 97C146ED1CF9000F007C117D /* Runner */,
181 | );
182 | };
183 | /* End PBXProject section */
184 |
185 | /* Begin PBXResourcesBuildPhase section */
186 | 97C146EC1CF9000F007C117D /* Resources */ = {
187 | isa = PBXResourcesBuildPhase;
188 | buildActionMask = 2147483647;
189 | files = (
190 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
191 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */,
192 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
193 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
194 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
195 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,
196 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
197 | );
198 | runOnlyForDeploymentPostprocessing = 0;
199 | };
200 | /* End PBXResourcesBuildPhase section */
201 |
202 | /* Begin PBXShellScriptBuildPhase section */
203 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
204 | isa = PBXShellScriptBuildPhase;
205 | buildActionMask = 2147483647;
206 | files = (
207 | );
208 | inputPaths = (
209 | );
210 | name = "Thin Binary";
211 | outputPaths = (
212 | );
213 | runOnlyForDeploymentPostprocessing = 0;
214 | shellPath = /bin/sh;
215 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
216 | };
217 | 9740EEB61CF901F6004384FC /* Run Script */ = {
218 | isa = PBXShellScriptBuildPhase;
219 | buildActionMask = 2147483647;
220 | files = (
221 | );
222 | inputPaths = (
223 | );
224 | name = "Run Script";
225 | outputPaths = (
226 | );
227 | runOnlyForDeploymentPostprocessing = 0;
228 | shellPath = /bin/sh;
229 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
230 | };
231 | /* End PBXShellScriptBuildPhase section */
232 |
233 | /* Begin PBXSourcesBuildPhase section */
234 | 97C146EA1CF9000F007C117D /* Sources */ = {
235 | isa = PBXSourcesBuildPhase;
236 | buildActionMask = 2147483647;
237 | files = (
238 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
239 | 97C146F31CF9000F007C117D /* main.m in Sources */,
240 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
241 | );
242 | runOnlyForDeploymentPostprocessing = 0;
243 | };
244 | /* End PBXSourcesBuildPhase section */
245 |
246 | /* Begin PBXVariantGroup section */
247 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
248 | isa = PBXVariantGroup;
249 | children = (
250 | 97C146FB1CF9000F007C117D /* Base */,
251 | );
252 | name = Main.storyboard;
253 | sourceTree = "";
254 | };
255 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
256 | isa = PBXVariantGroup;
257 | children = (
258 | 97C147001CF9000F007C117D /* Base */,
259 | );
260 | name = LaunchScreen.storyboard;
261 | sourceTree = "";
262 | };
263 | /* End PBXVariantGroup section */
264 |
265 | /* Begin XCBuildConfiguration section */
266 | 97C147031CF9000F007C117D /* Debug */ = {
267 | isa = XCBuildConfiguration;
268 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
269 | buildSettings = {
270 | ALWAYS_SEARCH_USER_PATHS = NO;
271 | CLANG_ANALYZER_NONNULL = YES;
272 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
273 | CLANG_CXX_LIBRARY = "libc++";
274 | CLANG_ENABLE_MODULES = YES;
275 | CLANG_ENABLE_OBJC_ARC = YES;
276 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
277 | CLANG_WARN_BOOL_CONVERSION = YES;
278 | CLANG_WARN_COMMA = YES;
279 | CLANG_WARN_CONSTANT_CONVERSION = YES;
280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
281 | CLANG_WARN_EMPTY_BODY = YES;
282 | CLANG_WARN_ENUM_CONVERSION = YES;
283 | CLANG_WARN_INFINITE_RECURSION = YES;
284 | CLANG_WARN_INT_CONVERSION = YES;
285 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
286 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
287 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
288 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
289 | CLANG_WARN_STRICT_PROTOTYPES = YES;
290 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
291 | CLANG_WARN_UNREACHABLE_CODE = YES;
292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
293 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
294 | COPY_PHASE_STRIP = NO;
295 | DEBUG_INFORMATION_FORMAT = dwarf;
296 | ENABLE_STRICT_OBJC_MSGSEND = YES;
297 | ENABLE_TESTABILITY = YES;
298 | GCC_C_LANGUAGE_STANDARD = gnu99;
299 | GCC_DYNAMIC_NO_PIC = NO;
300 | GCC_NO_COMMON_BLOCKS = YES;
301 | GCC_OPTIMIZATION_LEVEL = 0;
302 | GCC_PREPROCESSOR_DEFINITIONS = (
303 | "DEBUG=1",
304 | "$(inherited)",
305 | );
306 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
307 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
308 | GCC_WARN_UNDECLARED_SELECTOR = YES;
309 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
310 | GCC_WARN_UNUSED_FUNCTION = YES;
311 | GCC_WARN_UNUSED_VARIABLE = YES;
312 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
313 | MTL_ENABLE_DEBUG_INFO = YES;
314 | ONLY_ACTIVE_ARCH = YES;
315 | SDKROOT = iphoneos;
316 | TARGETED_DEVICE_FAMILY = "1,2";
317 | };
318 | name = Debug;
319 | };
320 | 97C147041CF9000F007C117D /* Release */ = {
321 | isa = XCBuildConfiguration;
322 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
323 | buildSettings = {
324 | ALWAYS_SEARCH_USER_PATHS = NO;
325 | CLANG_ANALYZER_NONNULL = YES;
326 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
327 | CLANG_CXX_LIBRARY = "libc++";
328 | CLANG_ENABLE_MODULES = YES;
329 | CLANG_ENABLE_OBJC_ARC = YES;
330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
331 | CLANG_WARN_BOOL_CONVERSION = YES;
332 | CLANG_WARN_COMMA = YES;
333 | CLANG_WARN_CONSTANT_CONVERSION = YES;
334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
335 | CLANG_WARN_EMPTY_BODY = YES;
336 | CLANG_WARN_ENUM_CONVERSION = YES;
337 | CLANG_WARN_INFINITE_RECURSION = YES;
338 | CLANG_WARN_INT_CONVERSION = YES;
339 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
340 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
341 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
342 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
343 | CLANG_WARN_STRICT_PROTOTYPES = YES;
344 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
345 | CLANG_WARN_UNREACHABLE_CODE = YES;
346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
347 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
348 | COPY_PHASE_STRIP = NO;
349 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
350 | ENABLE_NS_ASSERTIONS = NO;
351 | ENABLE_STRICT_OBJC_MSGSEND = YES;
352 | GCC_C_LANGUAGE_STANDARD = gnu99;
353 | GCC_NO_COMMON_BLOCKS = YES;
354 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
355 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
356 | GCC_WARN_UNDECLARED_SELECTOR = YES;
357 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
358 | GCC_WARN_UNUSED_FUNCTION = YES;
359 | GCC_WARN_UNUSED_VARIABLE = YES;
360 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
361 | MTL_ENABLE_DEBUG_INFO = NO;
362 | SDKROOT = iphoneos;
363 | TARGETED_DEVICE_FAMILY = "1,2";
364 | VALIDATE_PRODUCT = YES;
365 | };
366 | name = Release;
367 | };
368 | 97C147061CF9000F007C117D /* Debug */ = {
369 | isa = XCBuildConfiguration;
370 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
371 | buildSettings = {
372 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
373 | CURRENT_PROJECT_VERSION = 1;
374 | ENABLE_BITCODE = NO;
375 | FRAMEWORK_SEARCH_PATHS = (
376 | "$(inherited)",
377 | "$(PROJECT_DIR)/Flutter",
378 | );
379 | INFOPLIST_FILE = Runner/Info.plist;
380 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
381 | LIBRARY_SEARCH_PATHS = (
382 | "$(inherited)",
383 | "$(PROJECT_DIR)/Flutter",
384 | );
385 | PRODUCT_BUNDLE_IDENTIFIER = com.example.moviesStreams;
386 | PRODUCT_NAME = "$(TARGET_NAME)";
387 | VERSIONING_SYSTEM = "apple-generic";
388 | };
389 | name = Debug;
390 | };
391 | 97C147071CF9000F007C117D /* Release */ = {
392 | isa = XCBuildConfiguration;
393 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
394 | buildSettings = {
395 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
396 | CURRENT_PROJECT_VERSION = 1;
397 | ENABLE_BITCODE = NO;
398 | FRAMEWORK_SEARCH_PATHS = (
399 | "$(inherited)",
400 | "$(PROJECT_DIR)/Flutter",
401 | );
402 | INFOPLIST_FILE = Runner/Info.plist;
403 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
404 | LIBRARY_SEARCH_PATHS = (
405 | "$(inherited)",
406 | "$(PROJECT_DIR)/Flutter",
407 | );
408 | PRODUCT_BUNDLE_IDENTIFIER = com.example.moviesStreams;
409 | PRODUCT_NAME = "$(TARGET_NAME)";
410 | VERSIONING_SYSTEM = "apple-generic";
411 | };
412 | name = Release;
413 | };
414 | /* End XCBuildConfiguration section */
415 |
416 | /* Begin XCConfigurationList section */
417 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
418 | isa = XCConfigurationList;
419 | buildConfigurations = (
420 | 97C147031CF9000F007C117D /* Debug */,
421 | 97C147041CF9000F007C117D /* Release */,
422 | );
423 | defaultConfigurationIsVisible = 0;
424 | defaultConfigurationName = Release;
425 | };
426 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
427 | isa = XCConfigurationList;
428 | buildConfigurations = (
429 | 97C147061CF9000F007C117D /* Debug */,
430 | 97C147071CF9000F007C117D /* Release */,
431 | );
432 | defaultConfigurationIsVisible = 0;
433 | defaultConfigurationName = Release;
434 | };
435 | /* End XCConfigurationList section */
436 | };
437 | rootObject = 97C146E61CF9000F007C117D /* Project object */;
438 | }
439 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
56 |
58 |
64 |
65 |
66 |
67 |
68 |
69 |
75 |
77 |
83 |
84 |
85 |
86 |
88 |
89 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : FlutterAppDelegate
5 |
6 | @end
7 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.m:
--------------------------------------------------------------------------------
1 | #include "AppDelegate.h"
2 | #include "GeneratedPluginRegistrant.h"
3 |
4 | @implementation AppDelegate
5 |
6 | - (BOOL)application:(UIApplication *)application
7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
8 | [GeneratedPluginRegistrant registerWithRegistry:self];
9 | // Override point for customization after application launch.
10 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
11 | }
12 |
13 | @end
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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter/553cf12672d02222ea6adeed298032a5ddda4d3e/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 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | movies_streams
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
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/main.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char* argv[]) {
6 | @autoreleasepool {
7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/lib/api/tmdb_api.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'dart:io';
4 |
5 | import 'package:movies_streams/models/movie_genres_list.dart';
6 | import 'package:movies_streams/models/movie_page_result.dart';
7 |
8 | ///
9 | /// TMDB API
10 | ///
11 | /// To get an API key, it is FREE => go to "https://www.themoviedb.org/"
12 | ///
13 |
14 | class TmdbApi {
15 | static const String TMDB_API_KEY = "PUT YOUR KEY, HERE";
16 | static const String baseUrl = 'api.themoviedb.org';
17 | final String imageBaseUrl = 'http://image.tmdb.org/t/p/w185/';
18 | final _httpClient = HttpClient();
19 |
20 | ///
21 | /// Returns the list of movies/tv-show, based on criteria:
22 | /// [type]: movie or tv (show)
23 | /// [pageIndex]: page
24 | /// [minYear, maxYear]: release dates range
25 | /// [genre]: genre
26 | ///
27 | Future pagedList({
28 | String type = "movie",
29 | int pageIndex = 1,
30 | int minYear = 2016,
31 | int maxYear = 2017,
32 | int genre = 28,
33 | }) async {
34 | var uri = Uri.https(
35 | baseUrl,
36 | '3/discover/$type',
37 | {
38 | 'api_key': TMDB_API_KEY,
39 | 'language': 'en-US',
40 | 'sort_by': 'popularity.desc',
41 | 'include_adult': 'false',
42 | 'include_video': 'false',
43 | 'page': '$pageIndex',
44 | 'release_date.gte': '$minYear',
45 | 'release_date.lte': '$maxYear',
46 | 'with_genres': '$genre',
47 | },
48 | );
49 |
50 | var response = await _getRequest(uri);
51 | MoviePageResult list = MoviePageResult.fromJSON(json.decode(response));
52 |
53 | // Give some additional delay to simulate slow network
54 | await Future.delayed(const Duration(seconds: 1));
55 |
56 | return list;
57 | }
58 |
59 | ///
60 | /// Returns the list of all genres
61 | ///
62 | Future movieGenres({String type = "movie"}) async {
63 | var uri = Uri.https(
64 | baseUrl,
65 | '3/genre/$type/list',
66 | {
67 | 'api_key': TMDB_API_KEY,
68 | 'language': 'en-US',
69 | },
70 | );
71 |
72 | var response = await _getRequest(uri);
73 | MovieGenresList list = MovieGenresList.fromJSON(json.decode(response));
74 |
75 | return list;
76 | }
77 |
78 | ///
79 | /// Routine to invoke the TMDB Web Server to get answers
80 | ///
81 | Future _getRequest(Uri uri) async {
82 | var request = await _httpClient.getUrl(uri);
83 | var response = await request.close();
84 |
85 | return response.transform(utf8.decoder).join();
86 | }
87 | }
88 |
89 | TmdbApi api = TmdbApi();
90 |
--------------------------------------------------------------------------------
/lib/blocs/application_bloc.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:collection';
3 |
4 | import 'package:movies_streams/api/tmdb_api.dart';
5 | import 'package:movies_streams/blocs/bloc_provider.dart';
6 | import 'package:movies_streams/models/movie_genre.dart';
7 | import 'package:movies_streams/models/movie_genres_list.dart';
8 |
9 | class ApplicationBloc implements BlocBase {
10 | ///
11 | /// Synchronous Stream to handle the provision of the movie genres
12 | ///
13 | final StreamController?> _syncController =
14 | StreamController?>.broadcast();
15 | Stream?> get outMovieGenres => _syncController.stream;
16 |
17 | ///
18 | final StreamController?> _cmdController =
19 | StreamController?>.broadcast();
20 | StreamSink get getMovieGenres => _cmdController.sink;
21 |
22 | ApplicationBloc() {
23 | // Read all genres from Internet
24 | api.movieGenres().then((list) {
25 | _genresList = list;
26 | });
27 |
28 | _cmdController.stream.listen((_) {
29 | _syncController.sink
30 | .add(UnmodifiableListView(_genresList?.genres ?? []));
31 | });
32 | }
33 |
34 | void dispose() {
35 | _syncController.close();
36 | _cmdController.close();
37 | }
38 |
39 | MovieGenresList? _genresList;
40 | }
41 |
--------------------------------------------------------------------------------
/lib/blocs/bloc_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | // Generic Interface for all BLoCs
4 | abstract class BlocBase {
5 | void dispose();
6 | }
7 |
8 | // Generic BLoC provider
9 | class BlocProvider extends StatefulWidget {
10 | const BlocProvider({
11 | super.key,
12 | required this.child,
13 | required this.bloc,
14 | });
15 |
16 | final T bloc;
17 | final Widget child;
18 |
19 | @override
20 | State> createState() => _BlocProviderState();
21 |
22 | static T? of(BuildContext context) {
23 | BlocProvider? provider =
24 | context.findAncestorWidgetOfExactType>();
25 | return provider?.bloc;
26 | }
27 | }
28 |
29 | class _BlocProviderState extends State> {
30 | @override
31 | void dispose() {
32 | widget.bloc.dispose();
33 | super.dispose();
34 | }
35 |
36 | @override
37 | Widget build(BuildContext context) {
38 | return widget.child;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/blocs/favorite_bloc.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:collection';
3 |
4 | import 'package:movies_streams/blocs/bloc_provider.dart';
5 | import 'package:movies_streams/models/movie_card.dart';
6 | import 'package:rxdart/rxdart.dart';
7 |
8 | class FavoriteBloc implements BlocBase {
9 | ///
10 | /// Unique list of all favorite movies
11 | ///
12 | final Set _favorites = Set();
13 |
14 | // ########## STREAMS ##############
15 | ///
16 | /// Interface that allows to add a new favorite movie
17 | ///
18 | final BehaviorSubject _favoriteAddController =
19 | BehaviorSubject();
20 | Sink get inAddFavorite => _favoriteAddController.sink;
21 |
22 | ///
23 | /// Interface that allows to remove a movie from the list of favorites
24 | ///
25 | final BehaviorSubject _favoriteRemoveController =
26 | BehaviorSubject();
27 | Sink get inRemoveFavorite => _favoriteRemoveController.sink;
28 |
29 | ///
30 | /// Interface that allows to get the total number of favorites
31 | ///
32 | final BehaviorSubject _favoriteTotalController =
33 | BehaviorSubject.seeded(0);
34 | Sink get _inTotalFavorites => _favoriteTotalController.sink;
35 | Stream get outTotalFavorites => _favoriteTotalController.stream;
36 |
37 | ///
38 | /// Interface that allows to get the list of all favorite movies
39 | ///
40 | final BehaviorSubject> _favoritesController =
41 | BehaviorSubject>.seeded([]);
42 | Sink> get _inFavorites => _favoritesController.sink;
43 | Stream> get outFavorites => _favoritesController.stream;
44 |
45 | ///
46 | /// Constructor
47 | ///
48 | FavoriteBloc() {
49 | _favoriteAddController.listen(_handleAddFavorite);
50 | _favoriteRemoveController.listen(_handleRemoveFavorite);
51 | }
52 |
53 | void dispose() {
54 | _favoriteAddController.close();
55 | _favoriteRemoveController.close();
56 | _favoriteTotalController.close();
57 | _favoritesController.close();
58 | }
59 |
60 | // ############# HANDLING #####################
61 |
62 | void _handleAddFavorite(MovieCard movieCard) {
63 | // Add the movie to the list of favorite ones
64 | _favorites.add(movieCard);
65 |
66 | _notify();
67 | }
68 |
69 | void _handleRemoveFavorite(MovieCard movieCard) {
70 | _favorites.remove(movieCard);
71 |
72 | _notify();
73 | }
74 |
75 | void _notify() {
76 | // Send to whomever is interested...
77 | // The total number of favorites
78 | _inTotalFavorites.add(_favorites.length);
79 |
80 | // The new list of all favorite movies
81 | _inFavorites.add(UnmodifiableListView(_favorites));
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/lib/blocs/favorite_movie_bloc.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:movies_streams/blocs/bloc_provider.dart';
4 | import 'package:movies_streams/models/movie_card.dart';
5 | import 'package:rxdart/rxdart.dart';
6 |
7 | class FavoriteMovieBloc implements BlocBase {
8 | ///
9 | /// A stream only meant to return whether THIS movie is part of the favorites
10 | ///
11 | final BehaviorSubject _isFavoriteController = BehaviorSubject();
12 | Stream get outIsFavorite => _isFavoriteController.stream;
13 |
14 | ///
15 | /// Stream of all the favorites
16 | ///
17 | final StreamController> _favoritesController = StreamController>();
18 | Sink> get inFavorites => _favoritesController.sink;
19 |
20 | ///
21 | /// Constructor
22 | ///
23 | FavoriteMovieBloc(MovieCard movieCard){
24 | //
25 | // We are listening to all favorites
26 | //
27 | _favoritesController.stream
28 | // but, we only consider the one that matches THIS one
29 | .map((list) => list.any((MovieCard item) => item.id == movieCard.id))
30 | // if any, we notify that it is part of the Favorites
31 | .listen((isFavorite) => _isFavoriteController.add(isFavorite));
32 | }
33 |
34 | void dispose(){
35 | _favoritesController.close();
36 | _isFavoriteController.close();
37 | }
38 | }
--------------------------------------------------------------------------------
/lib/blocs/movie_catalog_bloc.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:collection';
3 |
4 | import 'package:movies_streams/api/tmdb_api.dart';
5 | import 'package:movies_streams/blocs/bloc_provider.dart';
6 | import 'package:movies_streams/models/movie_card.dart';
7 | import 'package:movies_streams/models/movie_filters.dart';
8 | import 'package:movies_streams/models/movie_page_result.dart';
9 | import 'package:rxdart/rxdart.dart';
10 |
11 | class MovieCatalogBloc implements BlocBase {
12 | ///
13 | /// Max number of movies per fetched page
14 | ///
15 | final int _moviesPerPage = 20;
16 |
17 | ///
18 | /// Genre
19 | ///
20 | int _genre = 28;
21 |
22 | ///
23 | /// Release date min
24 | ///
25 | int _minReleaseDate = 2000;
26 |
27 | ///
28 | /// Release date max
29 | ///
30 | int _maxReleaseDate = 2005;
31 |
32 | ///
33 | /// Total number of movies in the catalog
34 | ///
35 | int _totalMovies = -1;
36 |
37 | ///
38 | /// List of all the movie pages that have been fetched from Internet.
39 | /// We use a [Map] to store them, so that we can identify the pageIndex
40 | /// more easily.
41 | ///
42 | final _fetchPages = {};
43 |
44 | ///
45 | /// List of the pages, currently being fetched from Internet
46 | ///
47 | final _pagesBeingFetched = Set();
48 |
49 | // ########## STREAMS ##############
50 |
51 | ///
52 | /// We are going to need the list of movies to be displayed
53 | ///
54 | final PublishSubject> _moviesController =
55 | PublishSubject>();
56 | Sink> get _inMoviesList => _moviesController.sink;
57 | Stream> get outMoviesList => _moviesController.stream;
58 |
59 | ///
60 | /// Each time we need to render a MovieCard, we will pass its [index]
61 | /// so that, we will be able to check whether it has already been fetched
62 | /// If not, we will automatically fetch the page
63 | ///
64 | final PublishSubject _indexController = PublishSubject();
65 | Sink get inMovieIndex => _indexController.sink;
66 |
67 | ///
68 | /// Let's put to the limits of the automation...
69 | /// Let's consider listeners interested in knowing if a modification
70 | /// has been applied to the filters and total of movies, fetched so far
71 | ///
72 | final BehaviorSubject _totalMoviesController =
73 | BehaviorSubject.seeded(0);
74 | final BehaviorSubject> _releaseDatesController =
75 | BehaviorSubject>.seeded([2000, 2005]);
76 | final BehaviorSubject _genreController = BehaviorSubject.seeded(28);
77 | Sink get _inTotalMovies => _totalMoviesController.sink;
78 | Stream get outTotalMovies => _totalMoviesController.stream;
79 | Sink> get _inReleaseDates => _releaseDatesController.sink;
80 | Stream> get outReleaseDates => _releaseDatesController.stream;
81 | Sink get _inGenre => _genreController.sink;
82 | Stream get outGenre => _genreController.stream;
83 |
84 | ///
85 | /// We also want to handle changes to the filters
86 | ///
87 | BehaviorSubject _filtersController =
88 | BehaviorSubject.seeded(
89 | MovieFilters(genre: 28, minReleaseDate: 2000, maxReleaseDate: 2005));
90 | Sink get inFilters => _filtersController.sink;
91 | Stream get outFilters => _filtersController.stream;
92 |
93 | ///
94 | /// Constructor
95 | ///
96 | MovieCatalogBloc() {
97 | //
98 | // As said, each time we will have to render a MovieCard, the latter will send us
99 | // the [index] of the movie to render. If the latter has not yet been fetched
100 | // we will need to fetch the page from the Internet.
101 | // Therefore, we need to listen to such request in order to handle the request.
102 | //
103 | _indexController.stream
104 | // take some time before jumping into the request (there might be several ones in a row)
105 | .bufferTime(Duration(microseconds: 500))
106 | // and, do not update where this is no need
107 | .where((batch) => batch.isNotEmpty)
108 | .listen(_handleIndexes);
109 |
110 | //
111 | // When filters are changed, we need to consider the changes
112 | //
113 | outFilters.listen(_handleFilters);
114 | }
115 |
116 | void dispose() {
117 | _moviesController.close();
118 | _indexController.close();
119 | _totalMoviesController.close();
120 | _releaseDatesController.close();
121 | _genreController.close();
122 | _filtersController.close();
123 | }
124 |
125 | // ############# HANDLING #####################
126 |
127 | ///
128 | /// For each of the movie index(es), we need to check if the latter
129 | /// has already been fetched. As the user might scroll rapidly, this
130 | /// might end up with multiple pages (since a page contains max 20 movies)
131 | /// to be fetched from Internet.
132 | ///
133 | void _handleIndexes(List indexes) {
134 | // Iterate all the requested indexes and,
135 | // get the index of the page corresponding to the index
136 | indexes.forEach((int index) {
137 | final int pageIndex = 1 + ((index + 1) ~/ _moviesPerPage);
138 |
139 | // check if the page has already been fetched
140 | if (!_fetchPages.containsKey(pageIndex)) {
141 | // the page has NOT yet been fetched, so we need to
142 | // fetch it from Internet
143 | // (except if we are already currently fetching it)
144 | if (!_pagesBeingFetched.contains(pageIndex)) {
145 | // Remember that we are fetching it
146 | _pagesBeingFetched.add(pageIndex);
147 | // Fetch it
148 | api
149 | .pagedList(
150 | pageIndex: pageIndex,
151 | genre: _genre,
152 | minYear: _minReleaseDate,
153 | maxYear: _maxReleaseDate)
154 | .then((MoviePageResult fetchedPage) =>
155 | _handleFetchedPage(fetchedPage, pageIndex));
156 | }
157 | }
158 | });
159 | }
160 |
161 | ///
162 | /// Once a page has been fetched from Internet, we need to
163 | /// 1) record it
164 | /// 2) notify everyone who might be interested in knowing it
165 | ///
166 | void _handleFetchedPage(MoviePageResult page, int pageIndex) {
167 | // Remember the page
168 | _fetchPages[pageIndex] = page;
169 | // Remove it from the ones being fetched
170 | _pagesBeingFetched.remove(pageIndex);
171 |
172 | // Notify anyone interested in getting access to the content
173 | // of all pages... however, we need to only return the pages
174 | // which respect the sequence (since MovieCard are in sequence)
175 | // therefore, we need to iterate through the pages that are
176 | // actually fetched and stop if there is a gap.
177 | List movies = [];
178 | List pageIndexes = _fetchPages.keys.toList();
179 | pageIndexes.sort((a, b) => a.compareTo(b));
180 |
181 | final int minPageIndex = pageIndexes[0];
182 | final int maxPageIndex = pageIndexes[pageIndexes.length - 1];
183 |
184 | // If the first page being fetched does not correspond to the first one, skip
185 | // and as soon as it will become available, it will be time to notify
186 | if (minPageIndex == 1) {
187 | for (int i = 1; i <= maxPageIndex; i++) {
188 | if (!_fetchPages.containsKey(i)) {
189 | // As soon as there is a hole, stop
190 | break;
191 | }
192 | // Add the list of fetched movies to the list
193 | movies.addAll(_fetchPages[i]!.movies);
194 | }
195 | }
196 |
197 | // Take the opportunity to remember the number of movies
198 | // and notify who might be interested in knowing it
199 | if (_totalMovies == -1) {
200 | _totalMovies = page.totalResults;
201 | _inTotalMovies.add(_totalMovies);
202 | }
203 |
204 | // Only notify when there are movies
205 | if (movies.length > 0) {
206 | _inMoviesList.add(UnmodifiableListView(movies));
207 | }
208 | }
209 |
210 | ///
211 | /// We want to set new filters
212 | ///
213 | void _handleFilters(MovieFilters result) {
214 | // First, let's record the new filter information
215 | _minReleaseDate = result.minReleaseDate;
216 | _maxReleaseDate = result.maxReleaseDate;
217 | _genre = result.genre;
218 |
219 | // Then, we need to reset
220 | _totalMovies = -1;
221 | _fetchPages.clear();
222 | _pagesBeingFetched.clear();
223 |
224 | // Let's notify who needs to know
225 | _inGenre.add(_genre);
226 | _inReleaseDates.add([_minReleaseDate, _maxReleaseDate]);
227 | _inTotalMovies.add(0);
228 |
229 | // we need to tell about a change so that we pick another list of movies
230 | _inMoviesList.add([]);
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:movies_streams/blocs/application_bloc.dart';
5 | import 'package:movies_streams/blocs/bloc_provider.dart';
6 | import 'package:movies_streams/blocs/favorite_bloc.dart';
7 | import 'package:movies_streams/pages/home.dart';
8 |
9 | Future main() async {
10 | // debugPrintRebuildDirtyWidgets = true;
11 | return runApp(
12 | BlocProvider(
13 | bloc: ApplicationBloc(),
14 | child: BlocProvider(
15 | bloc: FavoriteBloc(),
16 | child: MyApp(),
17 | ),
18 | )
19 | );
20 | }
21 |
22 | class MyApp extends StatelessWidget {
23 | // This widget is the root of your application.
24 | @override
25 | Widget build(BuildContext context) {
26 | return MaterialApp(
27 | title: 'Movies',
28 | theme: ThemeData(
29 | primarySwatch: Colors.blue,
30 | ),
31 | home: HomePage(),
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/models/movie_card.dart:
--------------------------------------------------------------------------------
1 |
2 | class MovieCard extends Object {
3 | final int id;
4 | final voteAverage;
5 | final String title;
6 | final String posterPath;
7 | final String overview;
8 |
9 | MovieCard(this.id, this.voteAverage, this.title, this.posterPath, this.overview);
10 |
11 | MovieCard.fromJSON(Map json)
12 | : id = json['id'],
13 | voteAverage = json['vote_average'],
14 | title = json['title'],
15 | posterPath = json['poster_path'],
16 | overview = json['overview'];
17 |
18 | @override
19 | bool operator==(dynamic other) => identical(this, other) || this.id == other.id;
20 |
21 | @override
22 | int get hashCode => id;
23 | }
24 |
--------------------------------------------------------------------------------
/lib/models/movie_filters.dart:
--------------------------------------------------------------------------------
1 | class MovieFilters {
2 | MovieFilters({
3 | required this.minReleaseDate,
4 | required this.maxReleaseDate,
5 | required this.genre,
6 | });
7 |
8 | final int minReleaseDate;
9 | final int maxReleaseDate;
10 | final int genre;
11 | }
12 |
--------------------------------------------------------------------------------
/lib/models/movie_genre.dart:
--------------------------------------------------------------------------------
1 | class MovieGenre {
2 | final String text;
3 | final int genre;
4 |
5 | MovieGenre(this.text, this.genre);
6 |
7 | MovieGenre.fromJSON(Map json)
8 | : genre = json["id"],
9 | text = json["name"];
10 | }
--------------------------------------------------------------------------------
/lib/models/movie_genres_list.dart:
--------------------------------------------------------------------------------
1 | import 'package:movies_streams/models/movie_genre.dart';
2 |
3 | class MovieGenresList {
4 | List genres = [];
5 |
6 | MovieGenresList.fromJSON(Map json)
7 | : genres = (json["genres"] as List)
8 | .map((item) => MovieGenre.fromJSON(item)).toList();
9 |
10 | //
11 | // Return the genre by its id
12 | //
13 | MovieGenre findById(int genre) => genres.firstWhere((g) => g.genre == genre);
14 | }
--------------------------------------------------------------------------------
/lib/models/movie_page_result.dart:
--------------------------------------------------------------------------------
1 | import 'package:movies_streams/models/movie_card.dart';
2 |
3 | class MoviePageResult {
4 | final int pageIndex;
5 | final int totalResults;
6 | final int totalPages;
7 | final List movies;
8 |
9 | MoviePageResult.fromJSON(Map json)
10 | : pageIndex = json['page'],
11 | totalResults = json['total_results'],
12 | totalPages = json['total_pages'],
13 | movies = (json['results'] as List)
14 | .map((json) => MovieCard.fromJSON(json))
15 | .toList();
16 | }
17 |
--------------------------------------------------------------------------------
/lib/pages/details.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:movies_streams/blocs/bloc_provider.dart';
3 | import 'package:movies_streams/blocs/favorite_bloc.dart';
4 | import 'package:movies_streams/models/movie_card.dart';
5 | import 'package:movies_streams/widgets/movie_details_widget.dart';
6 |
7 | class DetailsPage extends StatelessWidget {
8 | DetailsPage({
9 | super.key,
10 | required this.data,
11 | });
12 |
13 | final MovieCard data;
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return Scaffold(
18 | appBar: AppBar(
19 | title: Text(data.title),
20 | ),
21 | body: MovieDetailsWidget(
22 | movieCard: data,
23 | favoritesStream: BlocProvider.of(context)!.outFavorites,
24 | ),
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/pages/favorites.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:movies_streams/blocs/bloc_provider.dart';
3 | import 'package:movies_streams/blocs/favorite_bloc.dart';
4 | import 'package:movies_streams/models/movie_card.dart';
5 | import 'package:movies_streams/widgets/favorite_widget.dart';
6 |
7 | class FavoritesPage extends StatelessWidget {
8 | @override
9 | Widget build(BuildContext context) {
10 | final FavoriteBloc bloc = BlocProvider.of(context)!;
11 | return Scaffold(
12 | appBar: AppBar(
13 | title: Text('Favorites Page'),
14 | ),
15 | body: StreamBuilder(
16 | stream: bloc.outFavorites,
17 | // Display as many FavoriteWidgets
18 | builder:
19 | (BuildContext context, AsyncSnapshot> snapshot) {
20 | if (snapshot.hasData) {
21 | return ListView.builder(
22 | itemCount: snapshot.data!.length,
23 | itemBuilder: (BuildContext context, int index) {
24 | return FavoriteWidget(
25 | data: snapshot.data![index],
26 | );
27 | },
28 | );
29 | }
30 | return Container();
31 | },
32 | ),
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/pages/filters.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:movies_streams/blocs/application_bloc.dart';
5 | import 'package:movies_streams/blocs/bloc_provider.dart';
6 | import 'package:movies_streams/blocs/movie_catalog_bloc.dart';
7 | import 'package:movies_streams/models/movie_filters.dart';
8 | import 'package:movies_streams/models/movie_genre.dart';
9 |
10 | typedef FiltersPageCallback(MovieFilters result);
11 |
12 | class FiltersPage extends StatefulWidget {
13 | FiltersPage({
14 | super.key,
15 | });
16 |
17 | @override
18 | FiltersPageState createState() => FiltersPageState();
19 | }
20 |
21 | class FiltersPageState extends State {
22 | late ApplicationBloc _appBloc;
23 | late MovieCatalogBloc _movieBloc;
24 | late double _minReleaseDate;
25 | late double _maxReleaseDate;
26 | MovieGenre? _movieGenre;
27 | List? _genres;
28 |
29 | bool _isInit = false;
30 |
31 | @override
32 | void didChangeDependencies() {
33 | super.didChangeDependencies();
34 |
35 | // As the context of not yet available at initState() level,
36 | // if not yet initialized, we get the list of all genres
37 | // and retrieve the currently selected one, as well as the
38 | // filter parameters
39 | if (_isInit == false) {
40 | _appBloc = BlocProvider.of(context)!;
41 | _movieBloc = BlocProvider.of(context)!;
42 |
43 | _getFilterParameters();
44 | }
45 | }
46 |
47 | @override
48 | Widget build(BuildContext context) {
49 | return _isInit == false
50 | ? Container()
51 | : Scaffold(
52 | appBar: AppBar(
53 | leading: Container(),
54 | title: Text('Filters'),
55 | actions: [
56 | IconButton(
57 | icon: const Icon(Icons.close),
58 | onPressed: () {
59 | Navigator.of(context).pop();
60 | },
61 | ),
62 | ],
63 | ),
64 | body: Padding(
65 | padding: const EdgeInsets.fromLTRB(10.0, 50.0, 10.0, 10.0),
66 | child: Column(
67 | mainAxisAlignment: MainAxisAlignment.start,
68 | crossAxisAlignment: CrossAxisAlignment.start,
69 | children: [
70 | // Release dates range selector
71 |
72 | Text(
73 | 'Years:',
74 | style: TextStyle(decoration: TextDecoration.underline),
75 | ),
76 | Container(
77 | width: double.infinity,
78 | child: Row(
79 | children: [
80 | Container(
81 | constraints: BoxConstraints(
82 | minWidth: 40.0,
83 | maxWidth: 40.0,
84 | ),
85 | child: Text('${_minReleaseDate.toStringAsFixed(0)}'),
86 | ),
87 | Expanded(
88 | child: RangeSlider(
89 | min: 2000.0,
90 | max: 2017.0,
91 | values:
92 | RangeValues(_minReleaseDate, _maxReleaseDate),
93 | divisions: 18,
94 | onChanged: (RangeValues? values) {
95 | if (mounted) {
96 | setState(() {
97 | _minReleaseDate = values!.start;
98 | _maxReleaseDate = values.end;
99 | });
100 | }
101 | },
102 | ),
103 | ),
104 | Container(
105 | constraints: BoxConstraints(
106 | minWidth: 40.0,
107 | maxWidth: 40.0,
108 | ),
109 | child: Text('${_maxReleaseDate.toStringAsFixed(0)}'),
110 | ),
111 | ],
112 | ),
113 | ),
114 |
115 | Divider(),
116 |
117 | // Genre Selector
118 |
119 | Row(
120 | mainAxisAlignment: MainAxisAlignment.start,
121 | children: [
122 | Text('Genre:'),
123 | SizedBox(width: 24.0),
124 | DropdownButton(
125 | items: _genres?.map((MovieGenre movieGenre) {
126 | return DropdownMenuItem(
127 | value: movieGenre,
128 | child: Text(movieGenre.text),
129 | );
130 | }).toList(),
131 | value: _movieGenre,
132 | onChanged: (MovieGenre? newMovieGenre) {
133 | _movieGenre = newMovieGenre!;
134 | if (mounted) {
135 | setState(() {});
136 | }
137 | },
138 | ),
139 | ],
140 | ),
141 | ],
142 | ),
143 | ),
144 |
145 | // Filters acceptance
146 |
147 | floatingActionButton: FloatingActionButton(
148 | child: const Icon(Icons.check),
149 | onPressed: () {
150 | //
151 | // When the user accepts the changes to the filters,
152 | // we need to send the new filters to the MovieCatalogBloc filters sink.
153 | //
154 | _movieBloc.inFilters.add(MovieFilters(
155 | minReleaseDate: _minReleaseDate.round(),
156 | maxReleaseDate: _maxReleaseDate.round(),
157 | genre: _movieGenre!.genre,
158 | ));
159 |
160 | // close the screen
161 | Navigator.of(context).pop();
162 | },
163 | ),
164 | );
165 | }
166 |
167 | ///
168 | /// Very tricky.
169 | ///
170 | /// As we want to be 100% BLoC compliant, we need to retrieve
171 | /// everything from the BLoCs, using Streams...
172 | ///
173 | /// This is ugly but to be considered as a study case.
174 | ///
175 | void _getFilterParameters() {
176 | StreamSubscription? subscriptionMovieGenres;
177 | StreamSubscription? subscriptionFilters;
178 |
179 | subscriptionMovieGenres =
180 | _appBloc.outMovieGenres.listen((List? data) {
181 | _genres = data ?? [];
182 |
183 | subscriptionFilters =
184 | _movieBloc.outFilters.listen((MovieFilters filters) {
185 | _minReleaseDate = filters.minReleaseDate.toDouble();
186 | _maxReleaseDate = filters.maxReleaseDate.toDouble();
187 | _movieGenre = _genres!.firstWhere((g) => g.genre == filters.genre);
188 |
189 | // Simply to make sure the subscriptions are released
190 | subscriptionMovieGenres?.cancel();
191 | subscriptionFilters?.cancel();
192 |
193 | // Now that we have all parameters, we may build the actual page
194 | if (mounted) {
195 | setState(() {
196 | _isInit = true;
197 | });
198 | }
199 | });
200 | });
201 |
202 | // Send a request to get the list of the movie genres via stream
203 | _appBloc.getMovieGenres.add(null);
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/lib/pages/home.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:movies_streams/blocs/bloc_provider.dart';
3 | import 'package:movies_streams/blocs/movie_catalog_bloc.dart';
4 | import 'package:movies_streams/pages/list.dart';
5 | import 'package:movies_streams/pages/list_one_page.dart';
6 | import 'package:movies_streams/widgets/favorite_button.dart';
7 |
8 | class HomePage extends StatelessWidget {
9 | @override
10 | Widget build(BuildContext context) {
11 | return Scaffold(
12 | appBar: AppBar(title: Text('My Movies')),
13 | body: Center(
14 | child: Column(
15 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
16 | children: [
17 | ElevatedButton(
18 | child: Text('Movies List'),
19 | onPressed: () {
20 | _openPage(context);
21 | },
22 | ),
23 | FavoriteButton(
24 | child: Text('Favorite Movies'),
25 | ),
26 | ElevatedButton(
27 | child: Text('One Page'),
28 | onPressed: () {
29 | _openOnePage(context);
30 | },
31 | ),
32 | ],
33 | ),
34 | ),
35 | );
36 | }
37 |
38 | void _openPage(BuildContext context) {
39 | Navigator.of(context)
40 | .push(MaterialPageRoute(builder: (BuildContext context) {
41 | return BlocProvider(
42 | bloc: MovieCatalogBloc(),
43 | child: ListPage(),
44 | );
45 | }));
46 | }
47 |
48 | void _openOnePage(BuildContext context) {
49 | Navigator.of(context)
50 | .push(MaterialPageRoute(builder: (BuildContext context) {
51 | return BlocProvider(
52 | bloc: MovieCatalogBloc(),
53 | child: ListOnePage(),
54 | );
55 | }));
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/pages/list.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:movies_streams/blocs/bloc_provider.dart';
5 | import 'package:movies_streams/blocs/favorite_bloc.dart';
6 | import 'package:movies_streams/blocs/movie_catalog_bloc.dart';
7 | import 'package:movies_streams/models/movie_card.dart';
8 | import 'package:movies_streams/pages/details.dart';
9 | import 'package:movies_streams/pages/filters.dart';
10 | import 'package:movies_streams/widgets/favorite_button.dart';
11 | import 'package:movies_streams/widgets/filters_summary.dart';
12 | import 'package:movies_streams/widgets/movie_card_widget.dart';
13 |
14 | class ListPage extends StatelessWidget {
15 | final GlobalKey _scaffoldKey = GlobalKey();
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | final MovieCatalogBloc movieBloc =
20 | BlocProvider.of(context)!;
21 | final FavoriteBloc favoriteBloc = BlocProvider.of(context)!;
22 |
23 | return Scaffold(
24 | key: _scaffoldKey,
25 | appBar: AppBar(
26 | title: Text('List Page'),
27 | actions: [
28 | // Icon that gives direct access to the favorites
29 | // Also displays "real-time", the number of favorites
30 | FavoriteButton(child: const Icon(Icons.favorite)),
31 | // Icon to open the filters
32 | IconButton(
33 | icon: const Icon(Icons.more_horiz),
34 | onPressed: () {
35 | _scaffoldKey.currentState?.openEndDrawer();
36 | },
37 | ),
38 | ],
39 | ),
40 | body: Column(
41 | mainAxisAlignment: MainAxisAlignment.start,
42 | children: [
43 | FiltersSummary(),
44 | Expanded(
45 | // Display an infinite GridView with the list of all movies in the catalog,
46 | // that meet the filters
47 | child: StreamBuilder>(
48 | stream: movieBloc.outMoviesList,
49 | builder: (BuildContext context,
50 | AsyncSnapshot> snapshot) {
51 | return GridView.builder(
52 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
53 | crossAxisCount: 2,
54 | childAspectRatio: 1.0,
55 | ),
56 | itemBuilder: (BuildContext context, int index) {
57 | return _buildMovieCard(context, movieBloc, index,
58 | snapshot.data, favoriteBloc.outFavorites);
59 | },
60 | itemCount:
61 | (snapshot.data == null ? 0 : snapshot.data!.length) +
62 | 30,
63 | );
64 | }),
65 | ),
66 | ],
67 | ),
68 | endDrawer: FiltersPage(),
69 | );
70 | }
71 |
72 | Widget _buildMovieCard(
73 | BuildContext context,
74 | MovieCatalogBloc movieBloc,
75 | int index,
76 | List? movieCards,
77 | Stream> favoritesStream) {
78 | // Notify the MovieCatalogBloc that we are rendering the MovieCard[index]
79 | movieBloc.inMovieIndex.add(index);
80 |
81 | // Get the MovieCard data
82 | final MovieCard? movieCard =
83 | (movieCards != null && movieCards.length > index)
84 | ? movieCards[index]
85 | : null;
86 |
87 | if (movieCard == null) {
88 | return Center(
89 | child: CircularProgressIndicator(),
90 | );
91 | }
92 |
93 | return MovieCardWidget(
94 | key: Key('movie_${movieCard.id}'),
95 | movieCard: movieCard,
96 | favoritesStream: favoritesStream,
97 | onPressed: () {
98 | Navigator.of(context)
99 | .push(MaterialPageRoute(builder: (BuildContext context) {
100 | return DetailsPage(
101 | data: movieCard,
102 | );
103 | }));
104 | });
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/lib/pages/list_one_page.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:movies_streams/blocs/bloc_provider.dart';
5 | import 'package:movies_streams/blocs/favorite_bloc.dart';
6 | import 'package:movies_streams/blocs/movie_catalog_bloc.dart';
7 | import 'package:movies_streams/models/movie_card.dart';
8 | import 'package:movies_streams/pages/filters.dart';
9 | import 'package:movies_streams/widgets/favorite_button.dart';
10 | import 'package:movies_streams/widgets/filters_summary.dart';
11 | import 'package:movies_streams/widgets/movie_card_widget.dart';
12 | import 'package:movies_streams/widgets/movie_details_container.dart';
13 |
14 | class ListOnePage extends StatelessWidget {
15 | final GlobalKey _scaffoldKey = GlobalKey();
16 | final GlobalKey _movieDetailsKey =
17 | GlobalKey();
18 |
19 | @override
20 | Widget build(BuildContext context) {
21 | final MovieCatalogBloc movieBloc =
22 | BlocProvider.of(context)!;
23 | final FavoriteBloc favoriteBloc = BlocProvider.of(context)!;
24 |
25 | return Scaffold(
26 | key: _scaffoldKey,
27 | appBar: AppBar(
28 | title: Text('List One Page'),
29 | actions: [
30 | // Icon that gives direct access to the favorites
31 | // It also displays "real-time" the number of favorites
32 | FavoriteButton(
33 | child: const Icon(Icons.favorite),
34 | ),
35 | // Icon to open the filters
36 | IconButton(
37 | icon: const Icon(Icons.more_horiz),
38 | onPressed: () {
39 | _scaffoldKey.currentState?.openEndDrawer();
40 | },
41 | ),
42 | ],
43 | ),
44 | body: Column(
45 | mainAxisAlignment: MainAxisAlignment.start,
46 | children: [
47 | // Displays the filters currently being defined
48 | FiltersSummary(),
49 | Container(
50 | height: 150.0,
51 | // Horizontal list of all movies in the catalog
52 | // based on the filters
53 | child: StreamBuilder>(
54 | stream: movieBloc.outMoviesList,
55 | builder: (BuildContext context,
56 | AsyncSnapshot> snapshot) {
57 | return ListView.builder(
58 | scrollDirection: Axis.horizontal,
59 | itemBuilder: (BuildContext context, int index) {
60 | return _buildMovieCard(movieBloc, index, snapshot.data!,
61 | favoriteBloc.outFavorites);
62 | },
63 | itemCount:
64 | (snapshot.data == null ? 0 : snapshot.data!.length) +
65 | 30,
66 | );
67 | }),
68 | ),
69 | Divider(),
70 | Expanded(
71 | child: Padding(
72 | padding: const EdgeInsets.all(10.0),
73 | // Container to show the details related to a movie,
74 | // selected by the user
75 | child: MovieDetailsContainer(
76 | key: _movieDetailsKey,
77 | ),
78 | ),
79 | ),
80 | ],
81 | ),
82 | endDrawer: FiltersPage(),
83 | );
84 | }
85 |
86 | Widget _buildMovieCard(MovieCatalogBloc movieBloc, int index,
87 | List? movieCards, Stream> favoritesStream) {
88 | // Notify the MovieCatalogBloc that we are rendering the MovieCard[index]
89 | movieBloc.inMovieIndex.add(index);
90 |
91 | // Get the MovieCard data
92 | MovieCard? movieCard = (movieCards != null && movieCards.length > index)
93 | ? movieCards[index]
94 | : null;
95 |
96 | // If the movie card is not yet available, display a progress indicator
97 | if (movieCard == null) {
98 | return Center(
99 | child: CircularProgressIndicator(),
100 | );
101 | }
102 |
103 | // Otherwise, display the movie card
104 | return SizedBox(
105 | width: 150.0,
106 | child: MovieCardWidget(
107 | key: Key('movie_${movieCard.id}'),
108 | movieCard: movieCard,
109 | favoritesStream: favoritesStream,
110 | noHero: true,
111 | onPressed: () {
112 | _movieDetailsKey.currentState?.movieCard = movieCard;
113 | },
114 | ),
115 | );
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/lib/widgets/favorite_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:movies_streams/blocs/bloc_provider.dart';
3 | import 'package:movies_streams/blocs/favorite_bloc.dart';
4 | import 'package:movies_streams/pages/favorites.dart';
5 |
6 | class FavoriteButton extends StatelessWidget {
7 | FavoriteButton({
8 | super.key,
9 | required this.child,
10 | });
11 |
12 | final Widget child;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | final FavoriteBloc bloc = BlocProvider.of(context)!;
17 | return ElevatedButton(
18 | onPressed: () {
19 | Navigator.of(context)
20 | .push(MaterialPageRoute(builder: (BuildContext context) {
21 | return FavoritesPage();
22 | }));
23 | },
24 | child: Stack(
25 | clipBehavior: Clip.none,
26 | children: [
27 | child,
28 | Positioned(
29 | top: -12.0,
30 | right: -6.0,
31 | child: Material(
32 | type: MaterialType.circle,
33 | elevation: 2.0,
34 | color: Colors.red,
35 | child: Padding(
36 | padding: const EdgeInsets.all(5.0),
37 | child: StreamBuilder(
38 | stream: bloc.outTotalFavorites,
39 | initialData: 0,
40 | builder: (BuildContext context, AsyncSnapshot snapshot) {
41 | return Text(
42 | snapshot.data.toString(),
43 | style: TextStyle(
44 | fontSize: 13.0,
45 | color: Colors.white,
46 | fontWeight: FontWeight.bold,
47 | ),
48 | );
49 | },
50 | ),
51 | ),
52 | ),
53 | ),
54 | ],
55 | ),
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/widgets/favorite_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:movies_streams/api/tmdb_api.dart';
3 | import 'package:movies_streams/blocs/bloc_provider.dart';
4 | import 'package:movies_streams/blocs/favorite_bloc.dart';
5 | import 'package:movies_streams/models/movie_card.dart';
6 |
7 | class FavoriteWidget extends StatelessWidget {
8 | FavoriteWidget({
9 | super.key,
10 | required this.data,
11 | });
12 |
13 | final MovieCard data;
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | final FavoriteBloc bloc = BlocProvider.of(context)!;
18 |
19 | return Container(
20 | width: double.infinity,
21 | decoration: BoxDecoration(
22 | border: Border(
23 | bottom: BorderSide(
24 | width: 1.0,
25 | color: Colors.black54,
26 | ),
27 | ),
28 | ),
29 | child: ListTile(
30 | leading: Container(
31 | width: 100.0,
32 | height: 100.0,
33 | child: Image.network(api.imageBaseUrl + data.posterPath,
34 | fit: BoxFit.contain),
35 | ),
36 | title: Text(data.title),
37 | subtitle: Text(data.overview, style: TextStyle(fontSize: 10.0)),
38 | trailing: IconButton(
39 | icon: const Icon(
40 | Icons.close,
41 | color: Colors.red,
42 | ),
43 | onPressed: () {
44 | bloc.inRemoveFavorite.add(data);
45 | },
46 | ),
47 | ),
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/widgets/filters_summary.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:movies_streams/blocs/bloc_provider.dart';
3 | import 'package:movies_streams/blocs/movie_catalog_bloc.dart';
4 |
5 | class FiltersSummary extends StatelessWidget {
6 | FiltersSummary({
7 | super.key,
8 | });
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | // final MovieGenre? genre = ApplicationProvider.of(context).genres.firstWhere((g) => g.genre == data.genre);
13 | final MovieCatalogBloc movieBloc =
14 | BlocProvider.of(context)!;
15 |
16 | return Container(
17 | width: double.infinity,
18 | height: 40.0,
19 | decoration: BoxDecoration(
20 | border: Border.all(
21 | color: Colors.black,
22 | width: 1.0,
23 | ),
24 | ),
25 | child: Row(
26 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
27 | children: [
28 | StreamBuilder(
29 | stream: movieBloc.outGenre,
30 | builder: (BuildContext context, AsyncSnapshot snapshot) {
31 | return Text('Genre: ${snapshot.data}');
32 | },
33 | ),
34 | StreamBuilder>(
35 | stream: movieBloc.outReleaseDates,
36 | builder: (BuildContext context, AsyncSnapshot> snapshot) {
37 | if (snapshot.hasData) {
38 | return Text(
39 | 'Years: [${snapshot.data![0]} - ${snapshot.data![1]}]');
40 | }
41 | return Container();
42 | },
43 | ),
44 | StreamBuilder(
45 | stream: movieBloc.outTotalMovies,
46 | builder: (BuildContext context, AsyncSnapshot snapshot) {
47 | return Text('Total: ${snapshot.data}');
48 | },
49 | ),
50 | ],
51 | ),
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/widgets/movie_card_widget.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:movies_streams/api/tmdb_api.dart';
5 | import 'package:movies_streams/blocs/bloc_provider.dart';
6 | import 'package:movies_streams/blocs/favorite_bloc.dart';
7 | import 'package:movies_streams/blocs/favorite_movie_bloc.dart';
8 | import 'package:movies_streams/models/movie_card.dart';
9 |
10 | class MovieCardWidget extends StatefulWidget {
11 | MovieCardWidget({
12 | super.key,
13 | required this.movieCard,
14 | required this.favoritesStream,
15 | required this.onPressed,
16 | this.noHero = false,
17 | });
18 |
19 | final MovieCard movieCard;
20 | final VoidCallback onPressed;
21 | final Stream> favoritesStream;
22 | final bool noHero;
23 |
24 | @override
25 | MovieCardWidgetState createState() => MovieCardWidgetState();
26 | }
27 |
28 | class MovieCardWidgetState extends State {
29 | late FavoriteMovieBloc _bloc;
30 |
31 | ///
32 | /// In order to determine whether this particular Movie is
33 | /// part of the list of favorites, we need to inject the stream
34 | /// that gives us the list of all favorites to THIS instance
35 | /// of the BLoC
36 | ///
37 | StreamSubscription? _subscription;
38 |
39 | @override
40 | void initState() {
41 | super.initState();
42 | _createBloc();
43 | }
44 |
45 | ///
46 | /// As Widgets can be changed by the framework at any time,
47 | /// we need to make sure that if this happens, we keep on
48 | /// listening to the stream that notifies us about favorites
49 | ///
50 | @override
51 | void didUpdateWidget(MovieCardWidget oldWidget) {
52 | super.didUpdateWidget(oldWidget);
53 | _disposeBloc();
54 | _createBloc();
55 | }
56 |
57 | @override
58 | void dispose() {
59 | _disposeBloc();
60 | super.dispose();
61 | }
62 |
63 | void _createBloc() {
64 | _bloc = FavoriteMovieBloc(widget.movieCard);
65 |
66 | // Simple pipe from the stream that lists all the favorites into
67 | // the BLoC that processes THIS particular movie
68 | _subscription = widget.favoritesStream.listen(_bloc.inFavorites.add);
69 | }
70 |
71 | void _disposeBloc() {
72 | _subscription?.cancel();
73 | _bloc.dispose();
74 | }
75 |
76 | @override
77 | Widget build(BuildContext context) {
78 | final FavoriteBloc bloc = BlocProvider.of(context)!;
79 | List children = [
80 | ClipRect(
81 | clipper: _SquareClipper(),
82 | child: widget.noHero
83 | ? Image.network(api.imageBaseUrl + widget.movieCard.posterPath,
84 | fit: BoxFit.cover)
85 | : Hero(
86 | child: Image.network(
87 | api.imageBaseUrl + widget.movieCard.posterPath,
88 | fit: BoxFit.cover),
89 | tag: 'movie_${widget.movieCard.id}',
90 | ),
91 | ),
92 | Container(
93 | decoration: _buildGradientBackground(),
94 | padding: const EdgeInsets.only(
95 | bottom: 16.0,
96 | left: 16.0,
97 | right: 16.0,
98 | ),
99 | child: _buildTextualInfo(widget.movieCard),
100 | ),
101 | ];
102 |
103 | //
104 | // If the movie is part of the favorites, put an icon to indicate it
105 | // A better way of doing this, would be to create a dedicated widget for this.
106 | // This would minimize the rebuild in case the icon would be toggled.
107 | // In this case, only the button would be rebuilt, not the whole movie card widget.
108 | //
109 | children.add(
110 | StreamBuilder(
111 | stream: _bloc.outIsFavorite,
112 | initialData: false,
113 | builder: (BuildContext context, AsyncSnapshot snapshot) {
114 | if (snapshot.data == true) {
115 | return Positioned(
116 | top: 0.0,
117 | right: 0.0,
118 | child: Container(
119 | decoration: BoxDecoration(
120 | color: Colors.white30,
121 | borderRadius: BorderRadius.circular(50.0),
122 | ),
123 | padding: const EdgeInsets.all(4.0),
124 | child: InkWell(
125 | child: const Icon(
126 | Icons.favorite,
127 | color: Colors.red,
128 | ),
129 | onTap: () {
130 | bloc.inRemoveFavorite.add(widget.movieCard);
131 | },
132 | )),
133 | );
134 | }
135 | return Container();
136 | }),
137 | );
138 |
139 | return InkWell(
140 | onTap: widget.onPressed,
141 | child: Card(
142 | child: Stack(
143 | fit: StackFit.expand,
144 | children: children,
145 | ),
146 | ),
147 | );
148 | }
149 |
150 | BoxDecoration _buildGradientBackground() {
151 | return const BoxDecoration(
152 | gradient: LinearGradient(
153 | begin: Alignment.bottomCenter,
154 | end: Alignment.topCenter,
155 | stops: [0.0, 0.7, 0.7],
156 | colors: [
157 | Colors.black,
158 | Colors.transparent,
159 | Colors.transparent,
160 | ],
161 | ),
162 | );
163 | }
164 |
165 | Widget _buildTextualInfo(MovieCard movieCard) {
166 | return Column(
167 | mainAxisAlignment: MainAxisAlignment.end,
168 | crossAxisAlignment: CrossAxisAlignment.center,
169 | children: [
170 | Text(
171 | movieCard.title,
172 | style: const TextStyle(
173 | fontWeight: FontWeight.w500,
174 | fontSize: 16.0,
175 | color: Colors.white,
176 | ),
177 | ),
178 | const SizedBox(height: 4.0),
179 | Text(
180 | movieCard.voteAverage.toString(),
181 | style: const TextStyle(
182 | fontSize: 12.0,
183 | color: Colors.white70,
184 | ),
185 | ),
186 | ],
187 | );
188 | }
189 | }
190 |
191 | class _SquareClipper extends CustomClipper {
192 | @override
193 | Rect getClip(Size size) {
194 | return Rect.fromLTWH(0.0, 0.0, size.width, size.width);
195 | }
196 |
197 | @override
198 | bool shouldReclip(CustomClipper oldClipper) {
199 | return false;
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/lib/widgets/movie_details_container.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:movies_streams/blocs/bloc_provider.dart';
3 | import 'package:movies_streams/blocs/favorite_bloc.dart';
4 | import 'package:movies_streams/models/movie_card.dart';
5 | import 'package:movies_streams/widgets/movie_details_widget.dart';
6 |
7 | class MovieDetailsContainer extends StatefulWidget {
8 | MovieDetailsContainer({
9 | super.key,
10 | });
11 |
12 | @override
13 | MovieDetailsContainerState createState() => MovieDetailsContainerState();
14 | }
15 |
16 | class MovieDetailsContainerState extends State {
17 | MovieCard? _movieCard;
18 |
19 | set movieCard(MovieCard newMovieCard) {
20 | if (mounted) {
21 | setState(() {
22 | _movieCard = newMovieCard;
23 | });
24 | }
25 | }
26 |
27 | @override
28 | Widget build(BuildContext context) {
29 | return (_movieCard == null)
30 | ? Center(
31 | child: Text('Click on a movie to see the details...'),
32 | )
33 | : MovieDetailsWidget(
34 | movieCard: _movieCard!,
35 | boxFit: BoxFit.contain,
36 | favoritesStream:
37 | BlocProvider.of(context)!.outFavorites,
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/widgets/movie_details_widget.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:movies_streams/api/tmdb_api.dart';
5 | import 'package:movies_streams/blocs/bloc_provider.dart';
6 | import 'package:movies_streams/blocs/favorite_bloc.dart';
7 | import 'package:movies_streams/blocs/favorite_movie_bloc.dart';
8 | import 'package:movies_streams/models/movie_card.dart';
9 |
10 | class MovieDetailsWidget extends StatefulWidget {
11 | MovieDetailsWidget({
12 | super.key,
13 | required this.favoritesStream,
14 | required this.movieCard,
15 | this.boxFit = BoxFit.cover,
16 | });
17 |
18 | final MovieCard movieCard;
19 | final BoxFit boxFit;
20 | final Stream> favoritesStream;
21 |
22 | @override
23 | _MovieDetailsWidgetState createState() => _MovieDetailsWidgetState();
24 | }
25 |
26 | class _MovieDetailsWidgetState extends State {
27 | late FavoriteMovieBloc _bloc;
28 |
29 | ///
30 | /// In order to determine whether this particular Movie is
31 | /// part of the list of favorites, we need to inject the stream
32 | /// that gives us the list of all favorites to THIS instance
33 | /// of the BLoC
34 | ///
35 | StreamSubscription? _subscription;
36 |
37 | @override
38 | void initState() {
39 | super.initState();
40 | _createBloc();
41 | }
42 |
43 | ///
44 | /// As Widgets can be changed by the framework at any time,
45 | /// we need to make sure that if this happens, we keep on
46 | /// listening to the stream that notifies us about favorites
47 | ///
48 | @override
49 | void didUpdateWidget(MovieDetailsWidget oldWidget) {
50 | super.didUpdateWidget(oldWidget);
51 | _disposeBloc();
52 | _createBloc();
53 | }
54 |
55 | @override
56 | void dispose() {
57 | _disposeBloc();
58 | super.dispose();
59 | }
60 |
61 | void _createBloc() {
62 | _bloc = FavoriteMovieBloc(widget.movieCard);
63 |
64 | // Simple pipe from the stream that lists all the favorites into
65 | // the BLoC that processes THIS particular movie
66 | _subscription = widget.favoritesStream.listen(_bloc.inFavorites.add);
67 | }
68 |
69 | void _disposeBloc() {
70 | _subscription?.cancel();
71 | _bloc.dispose();
72 | }
73 |
74 | @override
75 | Widget build(BuildContext context) {
76 | final FavoriteBloc bloc = BlocProvider.of(context)!;
77 |
78 | return SingleChildScrollView(
79 | child: Column(
80 | mainAxisAlignment: MainAxisAlignment.start,
81 | children: [
82 | AspectRatio(
83 | aspectRatio: 1.0,
84 | child: Stack(
85 | fit: StackFit.expand,
86 | children: [
87 | Hero(
88 | child: Image.network(
89 | api.imageBaseUrl + widget.movieCard.posterPath,
90 | fit: widget.boxFit,
91 | ),
92 | tag: 'movie_${widget.movieCard.id}',
93 | ),
94 | StreamBuilder(
95 | stream: _bloc.outIsFavorite,
96 | initialData: false,
97 | builder:
98 | (BuildContext context, AsyncSnapshot snapshot) {
99 | return Positioned(
100 | top: 16.0,
101 | right: 16.0,
102 | child: InkWell(
103 | onTap: () {
104 | if (snapshot.data == true) {
105 | bloc.inRemoveFavorite.add(widget.movieCard);
106 | } else {
107 | bloc.inAddFavorite.add(widget.movieCard);
108 | }
109 | },
110 | child: Container(
111 | decoration: BoxDecoration(
112 | color: Colors.black54,
113 | borderRadius: BorderRadius.circular(50.0),
114 | ),
115 | padding: const EdgeInsets.all(4.0),
116 | child: Icon(
117 | snapshot.data == true
118 | ? Icons.favorite
119 | : Icons.favorite_border,
120 | color: snapshot.data == true
121 | ? Colors.red
122 | : Colors.white,
123 | )),
124 | ),
125 | );
126 | },
127 | ),
128 | ],
129 | ),
130 | ),
131 | SizedBox(height: 6.0),
132 | Text('Vote average: ${widget.movieCard.voteAverage}',
133 | style: TextStyle(
134 | fontSize: 12.0,
135 | )),
136 | SizedBox(height: 4.0),
137 | Divider(),
138 | Container(
139 | padding: const EdgeInsets.fromLTRB(4.0, 4.0, 4.0, 8.0),
140 | child: Text(widget.movieCard.overview),
141 | ),
142 | ],
143 | ),
144 | );
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | characters:
5 | dependency: transitive
6 | description:
7 | name: characters
8 | sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
9 | url: "https://pub.dev"
10 | source: hosted
11 | version: "1.2.1"
12 | collection:
13 | dependency: transitive
14 | description:
15 | name: collection
16 | sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
17 | url: "https://pub.dev"
18 | source: hosted
19 | version: "1.17.0"
20 | cupertino_icons:
21 | dependency: "direct main"
22 | description:
23 | name: cupertino_icons
24 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
25 | url: "https://pub.dev"
26 | source: hosted
27 | version: "1.0.5"
28 | flutter:
29 | dependency: "direct main"
30 | description: flutter
31 | source: sdk
32 | version: "0.0.0"
33 | js:
34 | dependency: transitive
35 | description:
36 | name: js
37 | sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
38 | url: "https://pub.dev"
39 | source: hosted
40 | version: "0.6.5"
41 | material_color_utilities:
42 | dependency: transitive
43 | description:
44 | name: material_color_utilities
45 | sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
46 | url: "https://pub.dev"
47 | source: hosted
48 | version: "0.2.0"
49 | meta:
50 | dependency: transitive
51 | description:
52 | name: meta
53 | sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
54 | url: "https://pub.dev"
55 | source: hosted
56 | version: "1.8.0"
57 | rxdart:
58 | dependency: "direct main"
59 | description:
60 | name: rxdart
61 | sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
62 | url: "https://pub.dev"
63 | source: hosted
64 | version: "0.27.7"
65 | sky_engine:
66 | dependency: transitive
67 | description: flutter
68 | source: sdk
69 | version: "0.0.99"
70 | vector_math:
71 | dependency: transitive
72 | description:
73 | name: vector_math
74 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
75 | url: "https://pub.dev"
76 | source: hosted
77 | version: "2.1.4"
78 | sdks:
79 | dart: ">=2.18.0 <3.0.0"
80 | flutter: ">=3.7.0"
81 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: movies_streams
2 | description: Project aimed at explaining Streams and BLoC.
3 | author: Didier Boelens
4 | version: 0.0.2
5 |
6 | environment:
7 | sdk: ">=2.18.0 <3.0.0"
8 | flutter: ">=3.7.0"
9 |
10 | dependencies:
11 | flutter:
12 | sdk: flutter
13 |
14 | cupertino_icons:
15 | rxdart:
16 |
17 | flutter:
18 | uses-material-design: true
19 |
20 |
--------------------------------------------------------------------------------