<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Angelo Cassano on Medium]]></title>
        <description><![CDATA[Stories by Angelo Cassano on Medium]]></description>
        <link>https://medium.com/@angeloavv?source=rss-a68b19b8c054------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*v9OxPlMdJ1P8939D88aMWg.png</url>
            <title>Stories by Angelo Cassano on Medium</title>
            <link>https://medium.com/@angeloavv?source=rss-a68b19b8c054------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 16 Apr 2026 03:59:29 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@angeloavv/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Command-Query Separation: a design pattern for your Flutter Apps]]></title>
            <link>https://angeloavv.medium.com/command-query-separation-a-design-pattern-for-your-flutter-apps-9ba5e36c1232?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/9ba5e36c1232</guid>
            <category><![CDATA[flutter-app-development]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[design-patterns]]></category>
            <category><![CDATA[asynchronous-programming]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Fri, 07 Mar 2025 14:35:55 GMT</pubDate>
            <atom:updated>2025-03-07T14:35:55.237Z</atom:updated>
            <content:encoded><![CDATA[<p>Event-driven programming and declarative UI frameworks like Flutter have changed the developer experience of people approaching app development.</p><p>When moving from an imperative approach to a more declarative way of doing things, you must reconsider how your app exchanges data between different app layers, especially when transitioning information from the business logic layer to the presentation layer and vice-versa.</p><p>Also, event-driven programming leads to asynchronous programming. This means you are writing methods that take data as input, and return futures or streams of something else as output most of the time.</p><h3>Futures versus Streams</h3><p>Before trying to understand what the Command-Query Separation is and how this pattern could help us write better data-driven Flutter Apps, let’s focus on both the definition of <strong>Futures</strong> and <strong>Streams</strong>.</p><p>According to the official Flutter documentation:</p><blockquote>A Future represents a computation that doesn’t complete immediately. Where a normal function returns the result, an asynchronous function returns a Future, which will eventually contain the result. The future will tell you when the result is ready.</blockquote><blockquote>A stream is a sequence of asynchronous events. It is like an asynchronous Iterable — where, instead of getting the next event when you ask for it, the stream tells you that there is an event when it is ready.</blockquote><p>Let’s focus on this sentence:</p><blockquote>instead of getting the next event when you ask for it, the stream tells you that there is an event when it is ready</blockquote><p>Reading between the lines looks like the future could be represented by a voluntary action while the stream might be something we undergo.</p><p>Let me explain…</p><h3>Memo App</h3><p>Imagine you want to develop a Memo App. What you need are two user interfaces: the first one is where you can show the saved memos sorted by the creation date, and the second one is used to gather information like a title or a description of the memo to be saved.</p><p>The best place to save the memos is in a database. We don’t need to do rocket science, then a SQLite database would do the job. We’re going to create a table that contains an auto-increment primary key, two columns representing the title and the description of the memo, and two more columns to store the creation date and the deletion date of the memo itself.</p><p>In our memo app, we need to perform three different operations:</p><ul><li>gathering data from the database: basically, we need to query our SQLite database to get all the non-deleted memos</li><li>saving a memo to the database: creating a new row entry by providing a title and a description</li><li>removing a memo from the database: removing a row by a memo ID</li></ul><p>In our first interface, all we have to do is call a function that gathers data from the database. But what happens after we create or delete a memo? We have to query our database once again to update our list accordingly.</p><p>Is there a way to decouple the operations that read from the database to the ones who write to the database? Yes, we can, using the Command-Query Separation pattern.</p><h3>Command-Query Separation</h3><p>Command-Query Separation (or CQS) is a design pattern that separates data requests from data modifications.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t2XKZH7pZhHpCXPUOuB0tg.png" /></figure><p>A <strong>Command</strong> represents a request change of stored data through operations like Insert, Update, or Delete. The sole responsibility of a command is to update data, which means no data should be returned.</p><pre>Future&lt;void&gt; save({required String title, required String description}) =&gt;<br>    into(memosTable).insert(<br>      MemosTableCompanion(<br>        title: Value(title),<br>        description: Value(description),<br>      ),<br>      mode: InsertMode.insertOrReplace,<br>    );<br><br>Future&lt;void&gt; remove(int id) async {<br>  final query = update(memosTable);<br><br>  query.where((tbl) =&gt; tbl.id.equals(id));<br><br>  await query.write(MemosTableCompanion(deletedAt: Value(DateTime.now())));<br>}</pre><p>On the other hand, a <strong>Query</strong> is a request to a data source to obtain information about an Entity. Query operations return Data Transfer Objects (DTOs) instead.</p><pre>Stream&lt;List&lt;MemoEntity&gt;&gt; fetch() {<br>  final query = select(memosTable);<br><br>  query.where((tbl) =&gt; tbl.deletedAt.isNull());<br>  query.orderBy([<br>    (t) =&gt; OrderingTerm(expression: t.createdAt, mode: OrderingMode.desc),<br>  ]);<br><br>  return query.watch();<br>}</pre><h4>Advantages of CQS in Flutter</h4><p>Command-Query Separation pattern is extremely helpful in event-driven applications, and Flutter extensively relies on event-driven programming.</p><p>By declaring all our command methods as functions that return nothing (void) and our query methods as streams that return a DTO or a collection of DTOs, we can easily apply the CQS pattern.</p><p>Using the CQS pattern, all we have to do is focus on queries and commands:</p><ul><li>With a stream that constantly listens for changes coming from our data source, we don’t need to take care of refreshing our memos list when a new memo is created or deleted, the <strong>query</strong> is already in charge of updating the UI every time something changes.</li><li>When we create a new memo or delete an existing one, we don’t need to update that specific entry in our result set, the <strong>command</strong>’s sole responsibility is to update the data.</li></ul><h3>Memo App: Practical Approach</h3><p>To better showcase an example of how to use the Command-Query Separation design pattern in our Flutter App, let’s create a new Flutter project using Drift as a reactive persistence library, built on top of SQLite.</p><p>After adding Drift dependencies, we need to define our Memo entity to be saved in our database.</p><pre>import &#39;package:drift/drift.dart&#39;;<br><br>@DataClassName(&#39;MemoEntity&#39;)<br>class MemosTable extends Table {<br>  Column&lt;int&gt; get id =&gt; integer().autoIncrement()();<br><br>  Column&lt;String&gt; get title =&gt; text()();<br><br>  Column&lt;String&gt; get description =&gt; text()();<br><br>  Column&lt;DateTime&gt; get createdAt =&gt;<br>      dateTime().named(&#39;created_at&#39;).withDefault(currentDateAndTime)();<br><br>  Column&lt;DateTime&gt; get deletedAt =&gt; dateTime().named(&#39;deleted_at&#39;).nullable()();<br><br>  @override<br>  String get tableName =&gt; &#39;memos&#39;;<br>}</pre><p>Then a Data Access Object class is needed to perform our command and query operations on our SQLite database. Let’s keep in mind that command methods must always return void, and query methods must return the DTOs, in this case represented by the MemoEntity. Usually, it’s better to separate the commands DAO class from the query DAO class, but for the sake of simplicity, we’ll keep everything in a single class.</p><pre>import &#39;package:drift/drift.dart&#39;;<br>import &#39;package:memo_app/features/database/dao/memo/memo_dao.drift.dart&#39;;<br>import &#39;package:memo_app/features/database/memo_database.dart&#39;;<br>import &#39;package:memo_app/features/database/tables/memo/memos_table.dart&#39;;<br>import &#39;package:memo_app/features/database/tables/memo/memos_table.drift.dart&#39;;<br><br>@DriftAccessor(tables: [MemosTable])<br>class MemoDAO extends DatabaseAccessor&lt;MemoDatabase&gt; with $MemoDAOMixin {<br>  MemoDAO(super.attachedDatabase);<br><br>  Future&lt;void&gt; save({required String title, required String description}) =&gt;<br>      into(memosTable).insert(<br>        MemosTableCompanion(<br>          title: Value(title),<br>          description: Value(description),<br>        ),<br>        mode: InsertMode.insertOrReplace,<br>      );<br><br>  Future&lt;void&gt; remove(int id) async {<br>    final query = update(memosTable);<br><br>    query.where((tbl) =&gt; tbl.id.equals(id));<br><br>    await query.write(MemosTableCompanion(deletedAt: Value(DateTime.now())));<br>  }<br><br>  Stream&lt;List&lt;MemoEntity&gt;&gt; fetch() {<br>    final query = select(memosTable);<br><br>    query.where((tbl) =&gt; tbl.deletedAt.isNull());<br>    query.orderBy([<br>      (t) =&gt; OrderingTerm(expression: t.createdAt, mode: OrderingMode.desc),<br>    ]);<br><br>    return query.watch();<br>  }<br>}</pre><p>Let’s move on by creating our MemoRepository, our single source of truth capable of interacting with our data source. Nothing special here, we can spot the very same methods under a different name and with different types: here’s where the DTOs are translated into business logic models.</p><pre>import &#39;package:memo_app/features/database/dao/memo/memo_dao.dart&#39;;<br>import &#39;package:memo_app/models/memo/memo.dart&#39;;<br>import &#39;package:memo_app/repositories/mappers/memo_mapper.dart&#39;;<br>import &#39;package:memo_app/repositories/repository.dart&#39;;<br><br>/// Abstract class of MemoRepository<br>abstract interface class MemoRepository {<br>  Future&lt;void&gt; create({required String title, required String description});<br><br>  Stream&lt;List&lt;Memo&gt;&gt; fetch();<br><br>  Future&lt;void&gt; remove(int id);<br>}<br><br>/// Implementation of the base interface MemoRepository<br>class MemoRepositoryImpl extends Repository implements MemoRepository {<br>  final MemoDAO memoDAO;<br>  final MemoMapper memoMapper;<br><br>  const MemoRepositoryImpl({<br>    required this.memoDAO,<br>    required this.memoMapper,<br>    required super.logger,<br>  });<br><br>  @override<br>  Future&lt;void&gt; create({required String title, required String description}) =&gt;<br>      safeCode(() async {<br>        try {<br>          logger.info(&#39;[$MemoRepository] Creating memo with title: $title&#39;);<br><br>          await memoDAO.save(title: title, description: description);<br><br>          logger.info(&#39;[$MemoRepository] Memo created!&#39;);<br>        } catch (error, stackTrace) {<br>          logger.error(<br>            &#39;[$MemoRepository] Error creating memo&#39;,<br>            error,<br>            stackTrace,<br>          );<br>          rethrow;<br>        }<br>      });<br><br>  @override<br>  Stream&lt;List&lt;Memo&gt;&gt; fetch() {<br>    logger.info(&#39;[$MemoRepository] Fetching memos from database&#39;);<br><br>    final entities = memoDAO.fetch();<br><br>    return entities.map((entity) {<br>      logger.info(&#39;[$MemoRepository] Mapping entities to models&#39;);<br><br>      return memoMapper.toModels(entity);<br>    }).safeCode();<br>  }<br><br>  @override<br>  Future&lt;void&gt; remove(int id) =&gt; safeCode(() async {<br>    try {<br>      logger.info(&#39;[$MemoRepository] Removing memo with id: $id&#39;);<br><br>      await memoDAO.remove(id);<br><br>      logger.info(&#39;[$MemoRepository] Memo removed!&#39;);<br>    } catch (error, stackTrace) {<br>      logger.error(&#39;[$MemoRepository] Error removing memo&#39;, error, stackTrace);<br>      rethrow;<br>    }<br>  });<br>}</pre><p>And here’s where the magic happens! One layer before the presentation layer, we can spot our state management pattern in charge of translating business logic data into events. In this particular example, I decided to use both BLoC and Cubit, but you can use your favorite state management pattern as well.</p><p>In my humble opinion, I believe BLoC and Cubits are well suited to implement the CQS pattern: BLoC is a Cubit with events, and we can associate events with commands! On the other hand, Cubit is pretty simple and we can use it to subscribe to incoming events using a stream subscription.</p><pre>import &#39;dart:async&#39;;<br><br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:flutter_essentials_kit/flutter_essentials_kit.dart&#39;;<br>import &#39;package:freezed_annotation/freezed_annotation.dart&#39;;<br>import &#39;package:memo_app/errors/generic_error.dart&#39;;<br>import &#39;package:memo_app/repositories/memo_repository.dart&#39;;<br><br>part &#39;memo_bloc.freezed.dart&#39;;<br>part &#39;memo_event.dart&#39;;<br>part &#39;memo_state.dart&#39;;<br><br>/// The MemoBloc<br>class MemoBloc extends Bloc&lt;MemoEvent, MemoState&gt; {<br>  final MemoRepository memoRepository;<br><br>  /// Create a new instance of [MemoBloc].<br>  MemoBloc({required this.memoRepository}) : super(const MemoState.initial()) {<br>    on&lt;CreateMemoEvent&gt;(_onCreate);<br>    on&lt;RemoveMemoEvent&gt;(_onRemove);<br>  }<br><br>  /// Method used to add the [CreateMemoEvent] event<br>  void create({required String title, required String description}) =&gt;<br>      add(MemoEvent.create(title: title, description: description));<br><br>  /// Method used to add the [RemoveMemoEvent] event<br>  void remove(int id) =&gt; add(MemoEvent.remove(id));<br><br>  FutureOr&lt;void&gt; _onCreate(<br>    CreateMemoEvent event,<br>    Emitter&lt;MemoState&gt; emit,<br>  ) async {<br>    emit(const MemoState.creating());<br><br>    try {<br>      await memoRepository.create(<br>        title: event.title,<br>        description: event.description,<br>      );<br><br>      emit(const MemoState.created());<br>    } on LocalizedError catch (error) {<br>      emit(MemoState.errorCreating(error));<br>    } catch (_) {<br>      emit(MemoState.errorCreating(GenericError()));<br>    }<br>  }<br><br>  FutureOr&lt;void&gt; _onRemove(<br>    RemoveMemoEvent event,<br>    Emitter&lt;MemoState&gt; emit,<br>  ) async {<br>    emit(const MemoState.removing());<br><br>    try {<br>      await memoRepository.remove(event.id);<br><br>      emit(const MemoState.removed());<br>    } on LocalizedError catch (error) {<br>      emit(MemoState.errorRemoving(error));<br>    } catch (_) {<br>      emit(MemoState.errorRemoving(GenericError()));<br>    }<br>  }<br>}<br><br>extension MemoBlocExtension on BuildContext {<br>  /// Extension method used to get the [MemoBloc] instance<br>  MemoBloc get memoBloc =&gt; read&lt;MemoBloc&gt;();<br>}</pre><pre>import &#39;dart:async&#39;;<br><br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:flutter_essentials_kit/flutter_essentials_kit.dart&#39;;<br>import &#39;package:freezed_annotation/freezed_annotation.dart&#39;;<br>import &#39;package:memo_app/errors/generic_error.dart&#39;;<br>import &#39;package:memo_app/models/memo/memo.dart&#39;;<br>import &#39;package:memo_app/repositories/memo_repository.dart&#39;;<br><br>part &#39;memo_cubit.freezed.dart&#39;;<br>part &#39;memo_state.dart&#39;;<br><br>/// The MemoCubit<br>class MemoCubit extends Cubit&lt;MemoState&gt; {<br>  final MemoRepository memoRepository;<br><br>  StreamSubscription&lt;List&lt;Memo&gt;&gt;? _subscription;<br><br>  /// Create a new instance of [MemoCubit].<br>  MemoCubit({required this.memoRepository}) : super(const MemoState.fetching());<br><br>  /// Method used to perform the [fetch] action<br>  void fetch() {<br>    _subscription = memoRepository.fetch().listen(<br>      (memos) {<br>        emit(<br>          memos.isNotEmpty ? MemoState.fetched(memos) : const MemoState.empty(),<br>        );<br>      },<br>      onError: (error) {<br>        if (error is LocalizedError) {<br>          emit(MemoState.errorFetching(error));<br>        } else {<br>          emit(MemoState.errorFetching(GenericError()));<br>        }<br>      },<br>    );<br>  }<br><br>  @override<br>  Future&lt;void&gt; close() {<br>    _subscription?.cancel();<br><br>    return super.close();<br>  }<br>}<br><br>extension MemoCubitExtension on BuildContext {<br>  /// Extension method used to get the [MemoCubit] instance<br>  MemoCubit get memoCubit =&gt; read&lt;MemoCubit&gt;();<br>}</pre><p>Now let’s move to the presentation layer to better understand how things (do not) interact with each other, making the CQS pattern concrete.</p><p>As we can see, the only thing MainPage class is doing is subscribing for upcoming state changes from MemoCubit, nothing less, nothing more. Even though there are two buttons in charge of navigating to the creation page, we’re not telling anyone to reload our data once deletion or creation is done.</p><pre>import &#39;package:auto_route/auto_route.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:memo_app/blocs/memo/memo_bloc.dart&#39;;<br>import &#39;package:memo_app/cubits/memo/memo_cubit.dart&#39; as cubit;<br>import &#39;package:memo_app/features/localization/extensions/build_context.dart&#39;;<br>import &#39;package:memo_app/features/routing/app_router.dart&#39;;<br>import &#39;package:memo_app/models/memo/memo.dart&#39;;<br>import &#39;package:memo_app/pages/main/widgets/add_fab.dart&#39;;<br>import &#39;package:memo_app/pages/main/widgets/empty_memo_courtesy.dart&#39;;<br>import &#39;package:memo_app/pages/main/widgets/error_memo_courtesy.dart&#39;;<br>import &#39;package:memo_app/pages/main/widgets/memo_card.dart&#39;;<br>import &#39;package:memo_app/widgets/loading_widget.dart&#39;;<br><br>/// Enter the Main documentation here<br>@RoutePage()<br>class MainPage extends StatelessWidget implements AutoRouteWrapper {<br>  /// The constructor of the page.<br>  const MainPage({super.key});<br><br>  @override<br>  Widget wrappedRoute(BuildContext context) =&gt; MultiBlocProvider(<br>    providers: [<br>      BlocProvider&lt;MemoBloc&gt;(<br>        create: (context) =&gt; MemoBloc(memoRepository: context.read()),<br>      ),<br>      BlocProvider&lt;cubit.MemoCubit&gt;(<br>        create:<br>            (context) =&gt;<br>                cubit.MemoCubit(memoRepository: context.read())..fetch(),<br>      ),<br>    ],<br>    child: this,<br>  );<br><br>  @override<br>  Widget build(BuildContext context) =&gt; Scaffold(<br>    appBar: AppBar(title: Text(context.l10n?.appName ?? &#39;appName&#39;)),<br>    body: BlocBuilder&lt;cubit.MemoCubit, cubit.MemoState&gt;(<br>      builder:<br>          (context, state) =&gt; switch (state) {<br>            cubit.FetchingMemoState() =&gt; const LoadingWidget(),<br>            cubit.FetchedMemoState(:final memos) =&gt; ListView.separated(<br>              padding: const EdgeInsets.symmetric(vertical: 16.0),<br>              physics: const BouncingScrollPhysics(),<br>              separatorBuilder: (_, __ )=&gt; const SizedBox(height: 8.0),<br>              itemBuilder: (context, index) {<br>                final memo = memos[index];<br><br>                return MemoCard(<br>                  memo,<br>                  onDismissed: () =&gt; _removeMemo(context, memo),<br>                );<br>              },<br>              itemCount: memos.length,<br>            ),<br>            cubit.EmptyMemoState() =&gt; EmptyMemoCourtesy(<br>              onTap: () =&gt; context.router.push(const CreateMemoRoute()),<br>            ),<br>            cubit.ErrorFetchingMemoState() =&gt; const ErrorMemoCourtesy(),<br>          },<br>    ),<br>    floatingActionButton: BlocSelector&lt;cubit.MemoCubit, cubit.MemoState, bool&gt;(<br>      selector:<br>          (state) =&gt; switch (state) {<br>            cubit.FetchedMemoState() =&gt; true,<br>            _ =&gt; false,<br>          },<br>      builder:<br>          (context, showButton) =&gt; switch (showButton) {<br>            true =&gt; AddFab(<br>              onPressed: () =&gt; context.router.push(const CreateMemoRoute()),<br>            ),<br>            false =&gt; const SizedBox.shrink(),<br>          },<br>    ),<br>  );<br><br>  void _removeMemo(BuildContext context, Memo memo) {<br>    context.memoBloc.remove(memo.id);<br>  }<br>}</pre><p>Of course, we can see there’s another bloc on this page, but its only purpose is to trigger our deletion command, which will propagate down the layers until MemoDAO’s remove function which will set deletedAt value to <em>DateTime.now()</em>.</p><pre>import &#39;package:auto_route/auto_route.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:memo_app/blocs/memo/memo_bloc.dart&#39;;<br>import &#39;package:memo_app/features/localization/extensions/build_context.dart&#39;;<br>import &#39;package:memo_app/pages/create_memo/widgets/create_button.dart&#39;;<br>import &#39;package:memo_app/pages/create_memo/widgets/reactive_description_field.dart&#39;;<br>import &#39;package:memo_app/pages/create_memo/widgets/reactive_title_field.dart&#39;;<br>import &#39;package:memo_app/widgets/loading_widget.dart&#39;;<br>import &#39;package:reactive_forms/reactive_forms.dart&#39;;<br><br>/// Enter the CreateMemo documentation here<br>@RoutePage()<br>class CreateMemoPage extends StatefulWidget implements AutoRouteWrapper {<br>  static const _kFormTitle = &#39;title&#39;;<br>  static const _kFormDescription = &#39;description&#39;;<br><br>  /// The constructor of the page.<br>  const CreateMemoPage({super.key});<br><br>  @override<br>  Widget wrappedRoute(BuildContext context) =&gt; MultiBlocProvider(<br>    providers: [<br>      BlocProvider&lt;MemoBloc&gt;(<br>        create: (context) =&gt; MemoBloc(memoRepository: context.read()),<br>      ),<br>    ],<br>    child: this,<br>  );<br><br>  @override<br>  State&lt;CreateMemoPage&gt; createState() =&gt; _CreateMemoState();<br>}<br><br>class _CreateMemoState extends State&lt;CreateMemoPage&gt; {<br>  final _form = FormGroup({<br>    CreateMemoPage._kFormTitle: FormControl&lt;String&gt;(<br>      validators: [Validators.required],<br>    ),<br>    CreateMemoPage._kFormDescription: FormControl&lt;String&gt;(<br>      validators: [Validators.required],<br>    ),<br>  });<br><br>  @override<br>  Widget build(BuildContext context) =&gt; BlocListener&lt;MemoBloc, MemoState&gt;(<br>    listener:<br>        (context, state) =&gt; switch (state) {<br>          CreatingMemoState() =&gt; _onCreating(context),<br>          CreatedMemoState() =&gt; _onCreated(context),<br>          ErrorCreatingMemoState() =&gt; _onErrorCreating(context),<br>          _ =&gt; null,<br>        },<br>    child: Scaffold(<br>      appBar: AppBar(<br>        title: Text(context.l10n?.titleCreateMemo ?? &#39;titleCreateMemo&#39;),<br>      ),<br>      body: ReactiveForm(<br>        formGroup: _form,<br>        child: ListView(<br>          padding: const EdgeInsets.symmetric(horizontal: 16.0),<br>          physics: const BouncingScrollPhysics(),<br>          children: [<br>            const ReactiveTitleField(<br>              formControlName: CreateMemoPage._kFormTitle,<br>            ),<br>            const ReactiveDescriptionField(<br>              formControlName: CreateMemoPage._kFormDescription,<br>            ),<br>            BlocBuilder&lt;MemoBloc, MemoState&gt;(<br>              builder:<br>                  (context, state) =&gt; switch (state) {<br>                    CreatingMemoState() =&gt; const LoadingWidget(),<br>                    _ =&gt; CreateButton(onPressed: () =&gt; _createMemo(context)),<br>                  },<br>            ),<br>          ],<br>        ),<br>      ),<br>    ),<br>  );<br><br>  void _createMemo(BuildContext context) {<br>    final title = _form.control(CreateMemoPage._kFormTitle).value;<br>    final description = _form.control(CreateMemoPage._kFormDescription).value;<br><br>    context.memoBloc.create(title: title, description: description);<br>  }<br><br>  void _onCreating(BuildContext context) {<br>    _form.markAsDisabled();<br>  }<br><br>  void _onCreated(BuildContext context) {<br>    _form.markAsEnabled();<br><br>    context.maybePop();<br>  }<br><br>  void _onErrorCreating(BuildContext context) {<br>    _form.markAsEnabled();<br><br>    ScaffoldMessenger.of(context).showSnackBar(<br>      SnackBar(<br>        content: Text(<br>          context.l10n?.snackbarErrorCreatingMemo ??<br>              &#39;snackbarErrorCreatingMemo&#39;,<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p>The same approach applies to the CreateMemoPage, a single cubit injected in the tree, capable of creating a new memo entry in our SQLite database.</p><h4>Third-party project libraries</h4><p>Many different third-party dependencies have been used to support this project like <em>auto_route</em>, <em>dart_mapper</em>, <em>freezed</em>, <em>pine</em>, and <em>reactive_forms</em>. If you want to better understand how this project works, <a href="https://github.com/AngeloAvv/memo_app">here</a> you can find the official repository on GitHub.</p><h3>Conclusion</h3><p>In this article, we delved into how to apply the Command-Query Separation pattern to write better data-driven Flutter Apps. CQS pattern could be really helpful when developing applications that rely on a lot of CRUD operations, especially when using a database like SQLite. Libraries like Drift help us write better CQS-driven applications focusing on what matters: data gathering and modifications.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9ba5e36c1232" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Raise your productivity when writing Flutter apps with Pine bricks and Copilot]]></title>
            <link>https://angeloavv.medium.com/raise-your-productivity-when-writing-flutter-apps-with-pine-bricks-and-copilot-da4b30aa39fc?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/da4b30aa39fc</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[brick]]></category>
            <category><![CDATA[masons]]></category>
            <category><![CDATA[productivity]]></category>
            <category><![CDATA[co-pilot]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Fri, 09 Feb 2024 15:44:23 GMT</pubDate>
            <atom:updated>2024-02-09T15:44:23.806Z</atom:updated>
            <content:encoded><![CDATA[<p>Do you believe me when I say you can create this simple Crypto App in less than an hour with good architecture and a minimum number of unit tests?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4v7SYrbzEKekGoJd6X9_qA.png" /></figure><p>But let’s start from the beginning, what are Pine Bricks and Copilot?</p><h3>Pine Bricks</h3><p><a href="https://github.com/MyLittleSuite/pine_bricks">Pine bricks</a> are bricks based on the Pine scaffolding. If you never heard about Pine Architecture, <a href="https://angeloavv.medium.com/pine-a-lightweight-architecture-helper-for-your-flutter-projects-1ce69ac63f74">here</a> you can find more details about it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5OYVhEtr_NgC-Po1.png" /></figure><p>I prefer defining it as an architecture because there’s a bit of software engineering stuff in it, but is a set of tools that helps you use Bloc for state management and Provider as a Dependency Injector/Service Locator following a predefined hierarchy while defining business logic in your app.</p><h4>Bricks</h4><p>But what are bricks? A brick is a template that helps you generate a boilerplate through <a href="https://pub.dev/packages/mason">Mason</a>, a wonderful tool developed by <a href="https://github.com/felangel">Felix Angelov</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Eq9CZWis6QFFL2rn.png" /></figure><p>Don’t you feel bored writing the same code over and over? If you’re capable of finding some sort of pattern in your files, you can create your brick using mason_cli and share it with the community through <a href="https://brickhub.dev/">brickhub.dev</a></p><h3>Copilot</h3><p>Do we need to introduce it? I’ll just stick with the GitHub definition.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/615/0*50vGXaCsytyAp2d-" /></figure><p><em>GitHub Copilot suggests code completions as developers type and turns natural language prompts into coding suggestions based on the project’s context and style conventions.</em></p><h3>Why these tools?</h3><p>By combining these two tools, for some particular tasks, we can raise our productivity by over 300% and now I’ll explain why.</p><p>If you ever created a well-structured application, most of the time you’re going to write the same pieces over and over. Think about a repository or a service: you create an abstraction for the signature, you extend the abstraction with a concrete class, and then you put the logic inside of it. The same story applies when you want to create a Bloc: a bunch of events and states, a list of event subscriptions in the constructor, and the business logic inside of it.</p><p>Even copy-pasting the same code becomes time-consuming because you’re more prone to mistakes. A former professor of mine used to say that <em>50% of errors come from copying stuff.</em></p><p>To maximize our efficiency in writing boilerplate code we’re going to use bricks. Since I prefer using the Pine Architecture, I created a set of bricks for its scaffolding. With these bricks, you can create services, repositories, blocs, pages, models, DTOs, and so on.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jbqOLcA3Lzpns7ZaKlHHcg.png" /></figure><p>Some of these bricks come with a set of <a href="https://docs.pytest.org/en/stable/explanation/fixtures.html">fixtures</a> to generate objects using fakers and they also generate boilerplate code to write unit tests.</p><p>Ok, now that we understood how to spend less time writing boilerplate code using Pine Bricks, what about GitHub Copilot?</p><p>Copilot is a wonderful tool when it comes to writing repetitive patterns in your code. I don’t like it when he tries to generate code when it doesn’t have enough context, that’s why I’m not using the prompt but the suggestion instead.</p><p>Where can you find repetitive patterns? Most of the time when you write CRUDs or tests. You’ll only need to write the first method of the class or the first test in the file and then Copilot will have enough context to understand what’s gonna change in the next iterations. With Copilot you can reduce your time writing tests by 60%, and the only thing you need to do is double-check that everything has been generated properly as Copilot is not always perfect.</p><p>Enough with the theory, let’s go writing our first application to see how to use Pine Bricks and Copilot to speed up our development.</p><h3>Crypto App</h3><p>Crypto App is one of the most developed applications you can find on GitHub or YouTube. It’s simple: you only need to perform some HTTP requests to an API to retrieve data you’re gonna show on your app.</p><p>Let’s create our Flutter Application with the following dependencies inside the <em>pubspec.yaml</em>. Some of these dependencies are mandatory if you want to use Pine Bricks, but I promise once you start using them, you’ll never come back.</p><pre>dependencies:<br>  auto_route: ^7.8.4<br>  cached_network_image: ^3.3.1<br>  dio: ^5.4.0<br>  dio_smart_retry: ^6.0.0<br>  flutter:<br>    sdk: flutter<br>  flutter_bloc: ^8.1.3<br>  flutter_essentials_kit: ^2.5.0<br>  freezed_annotation: ^2.4.1<br>  intl: ^0.18.1<br>  json_annotation: ^4.8.1<br>  pine: ^1.0.3<br>  provider: ^6.1.1<br>  retrofit: ^4.1.0<br>  talker: ^4.0.1<br>  talker_bloc_logger: ^4.0.1<br>  talker_dio_logger: ^4.0.1<br><br>dev_dependencies:<br>  flutter_test:<br>    sdk: flutter<br>  auto_route_generator: ^7.3.2<br>  bloc_test: ^9.1.5<br>  build_runner: ^2.4.8<br>  data_fixture_dart: ^2.2.0<br>  flutter_lints: ^3.0.1<br>  freezed: ^2.4.7<br>  http_mock_adapter: ^0.6.1<br>  json_serializable: ^6.7.1<br>  mockito: ^5.4.4<br>  retrofit_generator: ^8.1.0</pre><p>After getting these dependencies using <em>flutter pub get</em>, we must install <a href="https://pub.dev/packages/mason_cli#installation">mason_cli</a> and our Pine Bricks. Let’s create a file in the root of the project named <em>mason.yaml</em> with the following content:</p><pre>bricks:<br>  pine: ^0.1.0+2<br>  pine_bloc: ^0.2.0<br>  pine_cubit: ^0.2.0<br>  pine_dto_mapper: ^0.1.0+2<br>  pine_model: ^0.1.0+2<br>  pine_network_jto: ^0.1.0+2<br>  pine_network_request: ^0.1.0+2<br>  pine_network_response: ^0.1.0+2<br>  pine_page: ^0.2.1<br>  pine_repository: ^0.1.0+2<br>  pine_retrofit: ^0.1.0+2<br>  pine_service: ^0.1.0+2</pre><p>We’re not gonna use them all in this project, but I suggest you install all of them when writing more complex applications. Now let’s run <em>mason get</em> in our prompt to retrieve the pine bricks. We can spot the <em>mason-lock.json.</em></p><h4>Setup the Dependency Injection</h4><p>By following the Pine documentation, we need a class to define our global providers, repositories, mappers and blocs to be injected into the tree. Under <em>lib/</em> we create a folder named di, and then a file named <em>dependency_injector.dart.</em></p><pre>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:pine/pine.dart&#39;;<br>import &#39;package:provider/provider.dart&#39;;<br><br>class DependencyInjector extends StatelessWidget {<br>  final Widget child;<br><br>  const DependencyInjector({<br>    super.key,<br>    required this.child,<br>  });<br><br>  @override<br>  Widget build(BuildContext context) =&gt; DependencyInjectorHelper(<br>        providers: [],<br>        mappers: [],<br>        repositories: [],<br>        child: child,<br>      );<br>}</pre><p>Let’s inject these items into the tree by adding the DependencyInjector widget on top of our application. We can create a file named <em>app.dart</em> which contains the following:</p><pre>import &#39;package:crypto_app/di/dependency_injector.dart&#39;;<br>import &#39;package:crypto_app/features/routing/app_router.dart&#39;;<br>import &#39;package:crypto_app/features/theme/extensions/color_extension.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br><br>class App extends StatelessWidget {<br>  final _router = AppRouter();<br><br>  App({super.key});<br><br>  @override<br>  Widget build(BuildContext context) =&gt; DependencyInjector(<br>        child: MaterialApp.router(<br>          title: &#39;Crypto App&#39;,<br>          debugShowCheckedModeBanner: false,<br>          routeInformationParser: _router.defaultRouteParser(),<br>          routerDelegate: _router.delegate(),<br>          theme: ThemeData(<br>            colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),<br>            useMaterial3: true,<br>            extensions: [<br>              ColorExtension(<br>                rising: Colors.green[800],<br>                falling: Colors.red[800],<br>                neutral: Colors.grey,<br>              ),<br>            ],<br>            appBarTheme: const AppBarTheme(<br>              centerTitle: true,<br>              toolbarHeight: 80.0,<br>            ),<br>            dividerTheme: const DividerThemeData(<br>              thickness: 1.0,<br>              indent: 16.0,<br>              endIndent: 16.0,<br>            ),<br>          ),<br>        ),<br>      );<br>}</pre><p>And since we moved the App definition inside the above file, we can now simplify the <em>main.dart</em> as follows:</p><pre>import &#39;package:crypto_app/app.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter/services.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:talker_bloc_logger/talker_bloc_logger.dart&#39;;<br><br>void main() async {<br>  WidgetsFlutterBinding.ensureInitialized();<br>  SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack);<br>  SystemChrome.setPreferredOrientations([<br>    DeviceOrientation.portraitUp,<br>    DeviceOrientation.portraitDown,<br>  ]);<br><br>  Bloc.observer = TalkerBlocObserver();<br><br>  runApp(App());<br>}<br></pre><p><strong>Be careful!</strong> I’m not gonna cover all the aspects of the Crypto App development process but I’ll focus on the <strong>Bricks</strong> and <strong>Copilot</strong> stuff only. I.e: if you don’t know where the class <em>ColorExtension</em> comes from, I encourage you to take a look at the <strong>project repository</strong> on <strong>GitHub</strong>.</p><h4>CoinJTO to carry data from the API</h4><p>In software engineering, to carry data across different layers you use a special model called DTO which stands for <a href="https://en.wikipedia.org/wiki/Data_transfer_object">Data Transfer Object</a>. In our Crypto App we need to get data from an API that answers to our HTTP requests with a JSON payload.</p><p>Most of the time our DTO carries JSON data, that’s why I decided to call it JTO which stands for JSON Transfer Object.</p><p>For our Crypto App we’re gonna use the <a href="https://www.coingecko.com/api">Coingecko APIs</a> to request a list of coins with <strong>EUR</strong> currency sorted descendingly by <strong>market capitalization</strong>:</p><p><a href="https://api.coingecko.com/api/v3/coins/markets?vs_currency=EUR&amp;order=market_cap_desc&amp;per_page=250&amp;page=1&amp;parkline=false">https://api.coingecko.com/api/v3/coins/markets?vs_currency=EUR&amp;order=market_cap_desc&amp;per_page=250&amp;page=1&amp;parkline=false</a></p><pre>{<br>&quot;id&quot;: &quot;bitcoin&quot;,<br>&quot;symbol&quot;: &quot;btc&quot;,<br>&quot;name&quot;: &quot;Bitcoin&quot;,<br>&quot;image&quot;: &quot;https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1696501400&quot;,<br>&quot;current_price&quot;: 43177,<br>&quot;market_cap&quot;: 849325665337,<br>&quot;market_cap_rank&quot;: 1,<br>&quot;fully_diluted_valuation&quot;: 908936212551,<br>&quot;total_volume&quot;: 29370790007,<br>&quot;high_24h&quot;: 43377,<br>&quot;low_24h&quot;: 41403,<br>&quot;price_change_24h&quot;: 1767.14,<br>&quot;price_change_percentage_24h&quot;: 4.26748,<br>&quot;market_cap_change_24h&quot;: 34932151048,<br>&quot;market_cap_change_percentage_24h&quot;: 4.28935,<br>&quot;circulating_supply&quot;: 19622762,<br>&quot;total_supply&quot;: 21000000,<br>&quot;max_supply&quot;: 21000000,<br>&quot;ath&quot;: 59717,<br>&quot;ath_change_percentage&quot;: -27.55324,<br>&quot;ath_date&quot;: &quot;2021-11-10T14:24:11.849Z&quot;,<br>&quot;atl&quot;: 51.3,<br>&quot;atl_change_percentage&quot;: 84235.76945,<br>&quot;atl_date&quot;: &quot;2013-07-05T00:00:00.000Z&quot;,<br>&quot;roi&quot;: null,<br>&quot;last_updated&quot;: &quot;2024-02-09T09:19:18.204Z&quot;<br>},</pre><p>Now that we have a deep understanding of what kind of data the JTO will have, it’s time to generate it, let’s type the following in the command prompt:</p><pre>mason make pine_network_jto --name &quot;Coin&quot;</pre><p>We can spot two different files, the JTO under <em>lib/services/network/jto/coin/coin_jto.dart</em> and the Fixture of the JTO under <em>test/fixtures/jto/coin_jto_fixture_factory.dart</em>. It’s time to define the attributes of our JTO and how its fixture can generate one. The JTO relies on the <strong>freezed</strong> dependency, so keep in mind we’re gonna need it in our project. Don’t forget to run the <strong>build_runner</strong> in watch mode as freezed classes depend on code generation.</p><pre>flutter pub run build_runner watch --delete-conflicting-outputs</pre><p>Under <em>lib/services/network/jto/coin/coin_jto.dart</em> let’s define our JTO as follows:</p><pre>import &#39;package:pine/pine.dart&#39;;<br>import &#39;package:freezed_annotation/freezed_annotation.dart&#39;;<br><br>part &#39;coin_jto.g.dart&#39;;<br><br>part &#39;coin_jto.freezed.dart&#39;;<br><br>@Freezed(toJson: false, copyWith: false)<br>class CoinJTO extends DTO with _$CoinJTO {<br>  const factory CoinJTO({<br>    @JsonKey(name: &#39;id&#39;) required String id,<br>    @JsonKey(name: &#39;symbol&#39;) required String symbol,<br>    @JsonKey(name: &#39;name&#39;) required String name,<br>    @JsonKey(name: &#39;image&#39;) String? image,<br>    @JsonKey(name: &#39;current_price&#39;) required num currentPrice,<br>    @JsonKey(name: &#39;market_cap&#39;) required num marketCap,<br>    @JsonKey(name: &#39;market_cap_rank&#39;) num? marketCapRank,<br>    @JsonKey(name: &#39;fully_diluted_valuation&#39;) num? fullyDilutedValuation,<br>    @JsonKey(name: &#39;total_volume&#39;) num? totalVolume,<br>    @JsonKey(name: &#39;high_24h&#39;) num? high24h,<br>    @JsonKey(name: &#39;low_24h&#39;) num? low24h,<br>    @JsonKey(name: &#39;price_change_24h&#39;) num? priceChange24h,<br>    @JsonKey(name: &#39;price_change_percentage_24h&#39;) required num priceChangePercentage24h,<br>    @JsonKey(name: &#39;market_cap_change_24h&#39;) num? marketCapChange24h,<br>    @JsonKey(name: &#39;market_cap_change_percentage_24h&#39;) num? marketCapChangePercentage24h,<br>    @JsonKey(name: &#39;circulating_supply&#39;) num? circulatingSupply,<br>    @JsonKey(name: &#39;total_supply&#39;) num? totalSupply,<br>    @JsonKey(name: &#39;max_supply&#39;) num? maxSupply,<br>    @JsonKey(name: &#39;ath&#39;) num? ath,<br>    @JsonKey(name: &#39;ath_change_percentage&#39;) num? athChangePercentage,<br>    @JsonKey(name: &#39;ath_date&#39;) String? athDate,<br>    @JsonKey(name: &#39;atl&#39;) num? atl,<br>    @JsonKey(name: &#39;atl_change_percentage&#39;) num? atlChangePercentage,<br>    @JsonKey(name: &#39;atl_date&#39;) String? atlDate,<br>    @JsonKey(name: &#39;last_updated&#39;) String? lastUpdated,<br>}) = _CoinJTO;<br><br>  factory CoinJTO.fromJson(Map&lt;String, dynamic&gt; json) =&gt;<br>    _$CoinJTOFromJson(json);<br>}</pre><p>Under <em>test/fixtures/jto/coin_jto_fixture_factory.dart</em> let’s define the CoinJTO Fixture taking advantage of Copilot. To generate our fixtures we can use the library <strong>data_fixture_dart</strong>.</p><pre>import &#39;package:crypto_app/services/network/jto/coin/coin_jto.dart&#39;;<br>import &#39;package:data_fixture_dart/data_fixture_dart.dart&#39;;<br><br>extension CoinJTOFixture on CoinJTO {<br>  static CoinJTOFixtureFactory factory() =&gt; CoinJTOFixtureFactory();<br>}<br><br>class CoinJTOFixtureFactory extends JsonFixtureFactory&lt;CoinJTO&gt; {<br>  @override<br>  FixtureDefinition&lt;CoinJTO&gt; definition() =&gt; define(<br>        (faker) =&gt; CoinJTO(<br>          id: faker.randomGenerator.string(10),<br>          symbol: faker.currency.code(),<br>          name: faker.currency.name(),<br>          image: faker.internet.httpsUrl(),<br>          currentPrice: faker.randomGenerator.decimal(),<br>          marketCap: faker.randomGenerator.integer(1000),<br>          marketCapRank: faker.randomGenerator.integer(10),<br>          fullyDilutedValuation: faker.randomGenerator.integer(1000),<br>          totalVolume: faker.randomGenerator.integer(1000),<br>          high24h: faker.randomGenerator.decimal(),<br>          low24h: faker.randomGenerator.decimal(),<br>          priceChange24h: faker.randomGenerator.decimal(),<br>          priceChangePercentage24h: faker.randomGenerator.decimal(),<br>          marketCapChange24h: faker.randomGenerator.integer(1000),<br>          marketCapChangePercentage24h: faker.randomGenerator.decimal(),<br>          circulatingSupply: faker.randomGenerator.integer(1000),<br>          totalSupply: faker.randomGenerator.integer(1000),<br>          maxSupply: faker.randomGenerator.integer(1000),<br>          ath: faker.randomGenerator.decimal(),<br>          athChangePercentage: faker.randomGenerator.decimal(),<br>          athDate: faker.date.dateTime().toIso8601String(),<br>          atl: faker.randomGenerator.decimal(),<br>          atlChangePercentage: faker.randomGenerator.decimal(),<br>          atlDate: faker.date.dateTime().toIso8601String(),<br>          lastUpdated: faker.date.dateTime().toIso8601String(),<br>        ),<br>      );<br><br>  @override<br>  JsonFixtureDefinition&lt;CoinJTO&gt; jsonDefinition() =&gt; defineJson(<br>        (object) =&gt; {<br>          &#39;id&#39;: object.id,<br>          &#39;symbol&#39;: object.symbol,<br>          &#39;name&#39;: object.name,<br>          &#39;image&#39;: object.image,<br>          &#39;current_price&#39;: object.currentPrice,<br>          &#39;market_cap&#39;: object.marketCap,<br>          &#39;market_cap_rank&#39;: object.marketCapRank,<br>          &#39;fully_diluted_valuation&#39;: object.fullyDilutedValuation,<br>          &#39;total_volume&#39;: object.totalVolume,<br>          &#39;high_24h&#39;: object.high24h,<br>          &#39;low_24h&#39;: object.low24h,<br>          &#39;price_change_24h&#39;: object.priceChange24h,<br>          &#39;price_change_percentage_24h&#39;: object.priceChangePercentage24h,<br>          &#39;market_cap_change_24h&#39;: object.marketCapChange24h,<br>          &#39;market_cap_change_percentage_24h&#39;:<br>              object.marketCapChangePercentage24h,<br>          &#39;circulating_supply&#39;: object.circulatingSupply,<br>          &#39;total_supply&#39;: object.totalSupply,<br>          &#39;max_supply&#39;: object.maxSupply,<br>          &#39;ath&#39;: object.ath,<br>          &#39;ath_change_percentage&#39;: object.athChangePercentage,<br>          &#39;ath_date&#39;: object.athDate,<br>          &#39;atl&#39;: object.atl,<br>          &#39;atl_change_percentage&#39;: object.atlChangePercentage,<br>          &#39;atl_date&#39;: object.atlDate,<br>          &#39;last_updated&#39;: object.lastUpdated,<br>        },<br>      );<br>}</pre><p>We can place our cursor next to the open curly bracket and Copilot will start generating suggestions. Sometimes it’s less accurate but it’s a good starting point to save some time writing this huge Fixture by hand.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2wWlO3HqGg0hVihOugQ1xA.png" /></figure><p>Now that we have our JTO, it’s time to call the API</p><h4>CoinService with Retrofit</h4><p>Retrofit is one of my favorite Flutter libraries. It’s a Dio wrapper that simplifies the process of defining REST APIs in our projects. I created a brick to generate a Retrofit service, let’s run the following to create our CoinService:</p><pre>mason make pine_retrofit --name &quot;Coin&quot;</pre><p>We can spot two different files, the service under <em>lib/services/network/coin/coin_service.dart</em> and the test of the service under <em>test/services/network/coin/coin_service_test.dart</em>.</p><p>In the <strong>CoinService</strong> class we need to define our endpoints as follows:</p><pre>import &#39;package:crypto_app/services/network/jto/coin/coin_jto.dart&#39;;<br>import &#39;package:dio/dio.dart&#39;;<br>import &#39;package:retrofit/retrofit.dart&#39;;<br><br>part &#39;coin_service.g.dart&#39;;<br><br>/// Abstract class of CoinService<br>@RestApi()<br>abstract class CoinService {<br>  factory CoinService(Dio dio, {String baseUrl}) = _CoinService;<br><br>  @GET(&#39;/coins/markets&#39;)<br>  Future&lt;List&lt;CoinJTO&gt;&gt; coins({<br>    @Query(&#39;vs_currency&#39;) String currency = &#39;EUR&#39;,<br>    @Query(&#39;order&#39;) String order = &#39;market_cap_desc&#39;,<br>    @Query(&#39;per_page&#39;) int items = 250,<br>    @Query(&#39;page&#39;) int page = 1,<br>    @Query(&#39;parkline&#39;) bool parkLine = false,<br>  });<br>}</pre><p>Don’t forget to inject this service in the widget tree through our DependencyInjector widget.</p><p>Now that we have defined our service, we can create a bunch of tests. To mock my endpoints I like to use the http_mock_adapter library. We’re gonna use our fixtures to generate fake data for the library and to also make assertions on the output.</p><p>Here we can also take advantage of Copilot after it gains a bit of context. When you start defining these behaviors multiple times, you will only need to write the first test and then Copilot will suggest the other test cases:</p><pre>import &#39;package:crypto_app/services/network/coin/coin_service.dart&#39;;<br>import &#39;package:crypto_app/services/network/jto/coin/coin_jto.dart&#39;;<br>import &#39;package:data_fixture_dart/misc/fixture_tuple.dart&#39;;<br>import &#39;package:dio/dio.dart&#39;;<br>import &#39;package:flutter_test/flutter_test.dart&#39;;<br>import &#39;package:http_mock_adapter/http_mock_adapter.dart&#39;;<br><br>import &#39;../../../fixtures/jto/coin_jto_fixture_factory.dart&#39;;<br><br>/// Test case for the class CoinService<br>void main() {<br>  late Dio dio;<br>  late DioAdapter dioAdapter;<br><br>  late CoinService service;<br><br>  setUp(() {<br>    dio = Dio(BaseOptions());<br>    dioAdapter = DioAdapter(dio: dio);<br><br>    service = CoinService(dio);<br>  });<br><br>  group(&#39;Testing coins endpoint&#39;, () {<br>    late List&lt;FixtureTuple&lt;CoinJTO&gt;&gt; coins;<br><br>    setUp(() {<br>      coins = CoinJTOFixture.factory().makeManyWithJsonArray(5);<br>    });<br><br>    test(&#39;when /coins/markets answers 200 OK successfully&#39;, () async {<br>      dioAdapter.onGet(<br>        &#39;/coins/markets&#39;,<br>        (server) =&gt; server.reply(<br>          200,<br>          coins.map((coin) =&gt; coin.json).toList(),<br>        ),<br>      );<br><br>      final actual = coins.map((coin) =&gt; coin.object).toList();<br>      expect(await service.coins(), actual);<br>    });<br><br>    test(&#39;when /coins/markets answers 422 Unprocessable Entity&#39;, () async {<br>      dioAdapter.onGet(<br>        &#39;/coins/markets&#39;,<br>        (server) =&gt; server.reply(422, null),<br>      );<br><br>      expect(<br>        () async =&gt; await service.coins(),<br>        throwsA(isA&lt;DioException&gt;()),<br>      );<br>    });<br>  });<br>}</pre><p>Now we can inject the service into the tree.</p><h4>The Coin Model</h4><p>What kind of information should we show in our application? This is the typical question we should ask ourselves to understand what attributes we need to extract from our JTOs to display data. Some of you may already be thinking why should we have all these layers of information when it’s way more simple to have a single model instead?</p><p>Well, the answer to our question comes from software engineering. Our data source may change over time, so it’s way simpler to change the behavior of a single mapper despite changing the whole business logic instead.</p><p>In our particular case, we’re not gonna show all the information we added in our JTO, it’s for the sake of this article to have all of them there. That’s why our Coin model will be slim.</p><p>Alright, it’s time to generate our model, we have a brick for it:</p><pre>mason make pine_model --name &quot;Coin&quot;</pre><p>Since the Coin shares some attributes with the CoinJTO class, it’s possible that Copilot will suggest something if we try to put our cursor next to the Coin constructor. If Copilot acts like a shy person, let’s do it on our own.</p><p>Under <em>lib/models/coin/coin.dart</em>:</p><pre>import &#39;package:freezed_annotation/freezed_annotation.dart&#39;;<br><br>part &#39;coin.freezed.dart&#39;;<br><br>@freezed<br>class Coin with _$Coin {<br>  const Coin._();<br><br>  const factory Coin({<br>    required String id,<br>    required String symbol,<br>    required String name,<br>    String? image,<br>    required num currentPrice,<br>    required num marketCap,<br>    required num priceChangePercentage24h,<br>  }) = _Coin;<br><br>  String get formattedName =&gt; &#39;$name (${symbol.toUpperCase()})&#39;;<br><br>  bool get priceRising =&gt; priceChangePercentage24h &gt; 0;<br><br>  bool get priceFalling =&gt; priceChangePercentage24h &lt; 0;<br>}</pre><p>Don’t forget to define the Fixture of this model, it will be useful when we will mock data in our repository or bloc. Under <em>test/fixtures/models/coin_fixture_factory.dart</em>:</p><pre>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:data_fixture_dart/data_fixture_dart.dart&#39;;<br><br>extension CoinFixture on Coin {<br>  static CoinFixtureFactory factory() =&gt; CoinFixtureFactory();<br>}<br><br>class CoinFixtureFactory extends FixtureFactory&lt;Coin&gt; {<br>  @override<br>  FixtureDefinition&lt;Coin&gt; definition() =&gt; define(<br>        (faker) =&gt; Coin(<br>          id: faker.randomGenerator.string(10),<br>          symbol: faker.currency.code(),<br>          name: faker.currency.name(),<br>          image: faker.internet.httpsUrl(),<br>          currentPrice: faker.randomGenerator.decimal(),<br>          marketCap: faker.randomGenerator.integer(1000),<br>          priceChangePercentage24h: faker.randomGenerator.decimal(),<br>        ),<br>      );<br>}</pre><h4>The CoinMapper</h4><p>Now that we have our model, we‘ll take advantage of the mapper pattern to translate data from a JTO to a model and vice-versa. Of course, we have a brick for that:</p><pre>mason make pine_dto_mapper --name &quot;CoinMapper&quot; --model_name &quot;Coin&quot; --dto_name &quot;Coin&quot; --type &quot;jto&quot;</pre><p>After running our brick, we can spot two files, the mapper and its test. Let’s start defining the mapper behavior under <em>lib/repositories/mappers/coin_mapper.dart</em>. Since this is a monodirectional mapper (we use it only to transform our JTO to a model), we can ignore the <em>toDTO</em> method, but I’ll write it anyway for the sake of this article:</p><pre>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:crypto_app/services/network/jto/coin/coin_jto.dart&#39;;<br>import &#39;package:pine/pine.dart&#39;;<br><br>class CoinMapper extends DTOMapper&lt;CoinJTO, Coin&gt; {<br>  const CoinMapper();<br><br>  @override<br>  Coin fromDTO(CoinJTO dto) =&gt; Coin(<br>        id: dto.id,<br>        symbol: dto.symbol,<br>        name: dto.name,<br>        image: dto.image,<br>        currentPrice: dto.currentPrice,<br>        marketCap: dto.marketCap,<br>        priceChangePercentage24h: dto.priceChangePercentage24h,<br>      );<br><br>  @override<br>  CoinJTO toDTO(Coin model) =&gt; CoinJTO(<br>        id: model.id,<br>        symbol: model.symbol,<br>        name: model.name,<br>        image: model.image,<br>        currentPrice: model.currentPrice,<br>        marketCap: model.marketCap,<br>        priceChangePercentage24h: model.priceChangePercentage24h,<br>      );<br>}</pre><p>We can also take advantage of the Copilot suggestions to speed up our process:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MZEUHVjwQudIgCdeindtuw.png" /></figure><p>The mapper test is already there, we simply need to create a Model and a JTO instance. Since the model has fewer attributes than our JTO, we cannot use the Fixtures to generate it. Under <em>test/repositories/mappers/coin_mapper_test.dart</em>:</p><pre>import &#39;package:data_fixture_dart/data_fixture_dart.dart&#39;;<br>import &#39;package:flutter_test/flutter_test.dart&#39;;<br>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:crypto_app/repositories/mappers/coin_mapper.dart&#39;;<br>import &#39;package:crypto_app/services/network/jto/coin/coin_jto.dart&#39;;<br><br>void main() {<br>  late CoinMapper mapper;<br>  late CoinJTO dto;<br>  late Coin model;<br><br>  setUp(() {<br>    dto = CoinJTO(<br>      id: faker.randomGenerator.string(10),<br>      symbol: faker.currency.code(),<br>      name: faker.currency.name(),<br>      image: faker.internet.httpsUrl(),<br>      currentPrice: faker.randomGenerator.decimal(),<br>      marketCap: faker.randomGenerator.integer(1000),<br>      priceChangePercentage24h: faker.randomGenerator.decimal(),<br>    );<br><br>    model = Coin(<br>      id: dto.id,<br>      symbol: dto.symbol,<br>      name: dto.name,<br>      image: dto.image,<br>      currentPrice: dto.currentPrice,<br>      marketCap: dto.marketCap,<br>      priceChangePercentage24h: dto.priceChangePercentage24h,<br>    );<br>    mapper = const CoinMapper();<br>  });<br><br>  test(&#39;mapping Coin object from CoinJTO&#39;, () {<br>    expect(mapper.fromDTO(dto), equals(model));<br>  });<br><br>  test(&#39;mapping Coin to CoinJTO&#39;, () {<br>    expect(mapper.toDTO(model), equals(dto));<br>  });<br>}</pre><p>Now we can inject the mapper into the tree.</p><h4>Data abstraction: the CoinRepository</h4><p>It’s time to go back to our command prompt and type the following instructions:</p><pre>mason make pine_repository --name &quot;Coin&quot;</pre><p>The brick will generate two files, the repository and its test. The coin_repository.dart file now contains an abstraction called CoinRepository and a concrete implementation that implements the aforementioned class. Our goal now is to define the repository behaviors in the abstraction and implement them in the concrete class.</p><p>By defining our repositories in our project multiple times, Copilot will learn the context and will be able to make suggestions speeding up our development.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/970/1*gsOhtVlUl9IAfky7Z0nc0A.png" /></figure><p>Under <em>lib/repositories/coin_repository.dart</em>:</p><pre>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:crypto_app/services/network/coin/coin_service.dart&#39;;<br>import &#39;package:crypto_app/services/network/jto/coin/coin_jto.dart&#39;;<br>import &#39;package:pine/pine.dart&#39;;<br>import &#39;package:talker/talker.dart&#39;;<br><br>/// Abstract class of CoinRepository<br>abstract class CoinRepository {<br>  Future&lt;List&lt;Coin&gt;&gt; get coins;<br>}<br><br>/// Implementation of the base interface CoinRepository<br>class CoinRepositoryImpl implements CoinRepository {<br>  final CoinService coinService;<br>  final DTOMapper&lt;CoinJTO, Coin&gt; coinMapper;<br>  final Talker logger;<br><br>  const CoinRepositoryImpl({<br>    required this.coinService,<br>    required this.coinMapper,<br>    required this.logger,<br>  });<br><br>  @override<br>  Future&lt;List&lt;Coin&gt;&gt; get coins async {<br>    try {<br>      logger.info(&#39;Fetching coins&#39;);<br>      final jtos = await coinService.coins();<br>      logger.info(&#39;Coins fetched&#39;);<br>      return jtos.map(coinMapper.fromDTO).toList(growable: false);<br>    } catch (error, stackTrace) {<br>      logger.error(&#39;Failed to fetch coins&#39;, error, stackTrace);<br>      rethrow;<br>    }<br>  }<br>}</pre><p>After defining the repository behaviors, it’s time to test them. Head back to test/repositories/coin/coin_repository_test.dart to create our test suites. We’ll take advantage of the <strong>mockito</strong> library to mock our service and our mapper, and we’ll use the Fixture to fake the data. After writing the happy path test case, Copilot should be smart enough to create the other ones on its own, helping us save more time.</p><pre>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:crypto_app/repositories/coin_repository.dart&#39;;<br>import &#39;package:crypto_app/repositories/mappers/coin_mapper.dart&#39;;<br>import &#39;package:crypto_app/services/network/coin/coin_service.dart&#39;;<br>import &#39;package:crypto_app/services/network/jto/coin/coin_jto.dart&#39;;<br>import &#39;package:flutter_test/flutter_test.dart&#39;;<br>import &#39;package:mockito/annotations.dart&#39;;<br>import &#39;package:mockito/mockito.dart&#39;;<br>import &#39;package:talker/talker.dart&#39;;<br><br>import &#39;../../fixtures/models/coin_fixture_factory.dart&#39;;<br>import &#39;coin_repository_test.mocks.dart&#39;;<br><br>/// Test case for the class CoinRepositoryImpl<br>@GenerateMocks([<br>  CoinService,<br>  CoinMapper,<br>], customMocks: [<br>  MockSpec&lt;Talker&gt;(unsupportedMembers: {#configure})<br>])<br>void main() {<br>  late MockCoinService service;<br>  late MockCoinMapper mapper;<br>  late MockTalker logger;<br><br>  late CoinRepository repository;<br><br>  setUp(() {<br>    service = MockCoinService();<br>    mapper = MockCoinMapper();<br>    logger = MockTalker();<br><br>    repository = CoinRepositoryImpl(<br>      coinService: service,<br>      coinMapper: mapper,<br>      logger: logger,<br>    );<br>  });<br><br>  group(&#39;Testing the coins getter&#39;, () {<br>    late List&lt;Coin&gt; coins;<br>    late List&lt;CoinJTO&gt; jtos;<br><br>    setUp(() {<br>      coins = CoinFixture.factory().makeMany(5);<br>      jtos = coins<br>          .map((coin) =&gt; CoinJTO(<br>                id: coin.id,<br>                symbol: coin.symbol,<br>                name: coin.name,<br>                image: coin.image,<br>                currentPrice: coin.currentPrice,<br>                marketCap: coin.marketCap,<br>                priceChangePercentage24h: coin.priceChangePercentage24h,<br>              ))<br>          .toList(growable: false);<br>    });<br><br>    test(&#39;get coins successfully&#39;, () async {<br>      when(service.coins()).thenAnswer((_) async =&gt; jtos);<br>      for (var i = 0; i &lt; coins.length; i++) {<br>        when(mapper.fromDTO(jtos[i])).thenReturn(coins[i]);<br>      }<br><br>      expect(await repository.coins, coins);<br><br>      verify(service.coins()).called(1);<br>      for (var i = 0; i &lt; coins.length; i++) {<br>        verify(mapper.fromDTO(jtos[i])).called(1);<br>      }<br>    });<br><br>    test(&#39;get coins with an unexpected error&#39;, () async {<br>      when(service.coins()).thenThrow(Exception());<br><br>      expect(() async =&gt; await repository.coins, throwsException);<br><br>      verify(service.coins()).called(1);<br>    });<br>  });<br>}</pre><p>Now we can inject the repository into the tree.</p><h4>Logic: The BLoC</h4><p>Here come my two favorite bricks <strong>pine_bloc</strong> and <strong>pine_cubit</strong>. They are fabulous: by defining a comma-separated set of events and states I can generate the whole bloc/cubit boilerplate with a single instruction. Let’s do it for our <strong>CoinBloc</strong> as follows:</p><pre>mason make pine_bloc --name &quot;Coin&quot; --events &quot;fetch&quot; --states &quot;initial,fetching,fetched,none,errorFetching&quot;</pre><p>Under <em>lib/blocs/coin/coin_state.dart</em> we need to fill the attributes for our states, the fetch event doesn’t need anything:</p><pre>part of &#39;coin_bloc.dart&#39;;<br><br>@freezed<br>class CoinState with _$CoinState {<br>  const factory CoinState.initial() = InitialCoinState;<br><br>  const factory CoinState.fetching() = FetchingCoinState;<br><br>  const factory CoinState.fetched(List&lt;Coin&gt; coins) = FetchedCoinState;<br><br>  const factory CoinState.none() = NoneCoinState;<br><br>  const factory CoinState.errorFetching(dynamic error) = ErrorFetchingCoinState;<br>}</pre><p>Under <em>lib/blocs/coin/coin_bloc.dart</em> let’s wrap up:</p><pre>import &#39;dart:async&#39;;<br><br>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:crypto_app/repositories/coin_repository.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:freezed_annotation/freezed_annotation.dart&#39;;<br><br>part &#39;coin_bloc.freezed.dart&#39;;<br><br>part &#39;coin_event.dart&#39;;<br><br>part &#39;coin_state.dart&#39;;<br><br>/// The CoinBloc<br>class CoinBloc extends Bloc&lt;CoinEvent, CoinState&gt; {<br>  final CoinRepository coinRepository;<br><br>  /// Create a new instance of [CoinBloc].<br>  CoinBloc({<br>    required this.coinRepository,<br>  }) : super(const CoinState.initial()) {<br>    on&lt;FetchCoinEvent&gt;(_onFetch);<br>  }<br><br>  /// Method used to add the [FetchCoinEvent] event<br>  void fetch() =&gt; add(const CoinEvent.fetch());<br><br>  FutureOr&lt;void&gt; _onFetch(<br>    FetchCoinEvent event,<br>    Emitter&lt;CoinState&gt; emit,<br>  ) async {<br>    try {<br>      emit(const CoinState.fetching());<br>      final coins = await coinRepository.coins;<br>      emit(<br>        coins.isNotEmpty ? CoinState.fetched(coins) : const CoinState.none(),<br>      );<br>    } catch (error) {<br>      emit(CoinState.errorFetching(error));<br>    }<br>  }<br>}</pre><p>And now it’s time for the test. This brick is magical, after you define the first test case, when you put your cursor on the next line, Copilot gains so much context it can enumerate the missing states to suggest the missing scenarios. Of course, it’s not as accurate as it should be, but with literally zero effort we can fix the errors in a few seconds.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nJsynY57WucsWZNly26X8Q.png" /></figure><p>Under <em>test/blocs/coin/coin_bloc_test.dart</em> let’s complete the file as follows:</p><pre>import &#39;package:bloc_test/bloc_test.dart&#39;;<br>import &#39;package:crypto_app/blocs/coin/coin_bloc.dart&#39;;<br>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:crypto_app/repositories/coin_repository.dart&#39;;<br>import &#39;package:flutter_test/flutter_test.dart&#39;;<br>import &#39;package:mockito/annotations.dart&#39;;<br>import &#39;package:mockito/mockito.dart&#39;;<br><br>import &#39;../../fixtures/models/coin_fixture_factory.dart&#39;;<br>import &#39;coin_bloc_test.mocks.dart&#39;;<br><br>@GenerateMocks([CoinRepository])<br>void main() {<br>  late MockCoinRepository coinRepository;<br>  late CoinBloc bloc;<br><br>  setUp(() {<br>    coinRepository = MockCoinRepository();<br>    bloc = CoinBloc(coinRepository: coinRepository);<br>  });<br><br>  /// Testing the event [FetchCoinEvent]<br>  group(&#39;when the event FetchCoinEvent is added to the BLoC&#39;, () {<br>    late List&lt;Coin&gt; coins;<br>    late dynamic exception;<br><br>    setUp(() {<br>      coins = CoinFixture.factory().makeMany(3);<br>      exception = Exception();<br>    });<br><br>    blocTest&lt;CoinBloc, CoinState&gt;(<br>      &#39;test that CoinBloc emits CoinState.fetched when fetch is called&#39;,<br>      setUp: () {<br>        when(coinRepository.coins).thenAnswer((_) async =&gt; coins);<br>      },<br>      build: () =&gt; bloc,<br>      act: (bloc) {<br>        bloc.fetch();<br>      },<br>      expect: () =&gt; &lt;CoinState&gt;[<br>        const CoinState.fetching(),<br>        CoinState.fetched(coins),<br>      ],<br>      verify: (_) {<br>        verify(coinRepository.coins).called(1);<br>      },<br>    );<br><br>    blocTest&lt;CoinBloc, CoinState&gt;(<br>      &#39;test that CoinBloc emits CoinState.errorFetching when fetch is called and an error occurs&#39;,<br>      setUp: () {<br>        when(coinRepository.coins).thenThrow(exception);<br>      },<br>      build: () =&gt; bloc,<br>      act: (bloc) {<br>        bloc.fetch();<br>      },<br>      expect: () =&gt; &lt;CoinState&gt;[<br>        const CoinState.fetching(),<br>        CoinState.errorFetching(exception),<br>      ],<br>      verify: (_) {<br>        verify(coinRepository.coins).called(1);<br>      },<br>    );<br><br>    blocTest&lt;CoinBloc, CoinState&gt;(<br>      &#39;test that CoinBloc emits CoinState.none when fetch is called&#39;,<br>      setUp: () {<br>        when(coinRepository.coins).thenAnswer((_) async =&gt; []);<br>      },<br>      build: () =&gt; bloc,<br>      act: (bloc) {<br>        bloc.fetch();<br>      },<br>      expect: () =&gt; &lt;CoinState&gt;[<br>        const CoinState.fetching(),<br>        const CoinState.none(),<br>      ],<br>      verify: (_) {<br>        verify(coinRepository.coins).called(1);<br>      },<br>    );<br>  });<br>}</pre><h4>Dependency Injection</h4><p>We’re not gonna inject the Bloc on the tree since this is a scoped Bloc, this is how the DependencyInjector class should look at the end of the project:</p><pre>import &#39;package:crypto_app/misc/constants.dart&#39;;<br>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:crypto_app/repositories/coin_repository.dart&#39;;<br>import &#39;package:crypto_app/repositories/mappers/coin_mapper.dart&#39;;<br>import &#39;package:crypto_app/services/network/coin/coin_service.dart&#39;;<br>import &#39;package:crypto_app/services/network/jto/coin/coin_jto.dart&#39;;<br>import &#39;package:dio/dio.dart&#39;;<br>import &#39;package:dio_smart_retry/dio_smart_retry.dart&#39;;<br>import &#39;package:flutter/foundation.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:pine/pine.dart&#39;;<br>import &#39;package:provider/provider.dart&#39;;<br>import &#39;package:talker/talker.dart&#39;;<br>import &#39;package:talker_dio_logger/talker_dio_logger.dart&#39;;<br><br>class DependencyInjector extends StatelessWidget {<br>  final Widget child;<br><br>  const DependencyInjector({<br>    super.key,<br>    required this.child,<br>  });<br><br>  @override<br>  Widget build(BuildContext context) =&gt; DependencyInjectorHelper(<br>        providers: [<br>          Provider&lt;Dio&gt;(<br>            create: (_) {<br>              final dio = Dio(BaseOptions(<br>                connectTimeout: K.networkTimeout,<br>                sendTimeout: K.networkTimeout,<br>                receiveTimeout: K.networkTimeout,<br>              ));<br><br>              dio.interceptors.addAll([<br>                RetryInterceptor(<br>                  dio: dio,<br>                  retries: 3,<br>                  retryDelays: const [<br>                    Duration(seconds: 1),<br>                    Duration(seconds: 2),<br>                    Duration(seconds: 3),<br>                  ],<br>                ),<br>                if (kDebugMode)<br>                  TalkerDioLogger(<br>                    settings: const TalkerDioLoggerSettings(<br>                      printRequestHeaders: true,<br>                      printResponseHeaders: true,<br>                      printResponseMessage: true,<br>                    ),<br>                  ),<br>              ]);<br><br>              return dio;<br>            },<br>          ),<br>          Provider&lt;Talker&gt;(<br>            create: (_) =&gt; Talker(),<br>          ),<br>          Provider&lt;CoinService&gt;(<br>            create: (context) =&gt; CoinService(<br>              context.read(),<br>              baseUrl: K.baseUrl,<br>            ),<br>          ),<br>        ],<br>        mappers: [<br>          Provider&lt;DTOMapper&lt;CoinJTO, Coin&gt;&gt;(<br>            create: (_) =&gt; const CoinMapper(),<br>          ),<br>        ],<br>        repositories: [<br>          RepositoryProvider&lt;CoinRepository&gt;(<br>            create: (context) =&gt; CoinRepositoryImpl(<br>              coinService: context.read(),<br>              coinMapper: context.read(),<br>              logger: context.read(),<br>            ),<br>          ),<br>        ],<br>        child: child,<br>      );<br>}</pre><h4>The UI: CoinsPage</h4><p>Yes, I also have a brick for that. Of course, I rely on <strong>auto_route</strong> to manage routing in my Flutter apps, so if you want to use this brick you have to use it too. When running the following command I’m setting the state to false since I need a stateless page but the auto_route is set to true as I’m gonna take advantage of a special mixin to inject the <strong>CoinBloc</strong> locally.</p><pre>mason make pine_page --name &quot;Coins&quot; --state false --auto_route true</pre><p>What does the CoinsPage look like? Under <em>lib/pages/coins_page.dart</em> we have the following:</p><pre>import &#39;package:auto_route/auto_route.dart&#39;;<br>import &#39;package:crypto_app/blocs/coin/coin_bloc.dart&#39;;<br>import &#39;package:crypto_app/widgets/coin_tile.dart&#39;;<br>import &#39;package:crypto_app/widgets/loading_widget.dart&#39;;<br>import &#39;package:crypto_app/widgets/no_coins_widget.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br><br>/// Enter the Coins documentation here<br>@RoutePage()<br>class CoinsPage extends StatelessWidget implements AutoRouteWrapper {<br>  /// The constructor of the page.<br>  const CoinsPage({super.key});<br><br>  @override<br>  Widget wrappedRoute(BuildContext context) =&gt; MultiBlocProvider(<br>        providers: [<br>          BlocProvider&lt;CoinBloc&gt;(<br>            create: (context) =&gt; CoinBloc(<br>              coinRepository: context.read(),<br>            )..fetch(),<br>          ),<br>        ],<br>        child: this,<br>      );<br><br>  @override<br>  Widget build(BuildContext context) =&gt; Scaffold(<br>        appBar: AppBar(title: const Text(&#39;Crypto App&#39;)),<br>        body: BlocBuilder&lt;CoinBloc, CoinState&gt;(<br>          builder: (context, state) =&gt; switch (state) {<br>            FetchingCoinState() =&gt; const LoadingWidget(),<br>            FetchedCoinState(:final coins) =&gt; ListView.separated(<br>                physics: const BouncingScrollPhysics(),<br>                itemBuilder: (context, index) {<br>                  final coin = coins[index];<br><br>                  return CoinTile(coin);<br>                },<br>                separatorBuilder: (_, __) =&gt; const Divider(),<br>                itemCount: coins.length,<br>              ),<br>            NoneCoinState() =&gt; NoCoinsWidget(<br>                onPressed: () =&gt; context.read&lt;CoinBloc&gt;().fetch(),<br>              ),<br>            ErrorFetchingCoinState() =&gt; NoCoinsWidget(<br>                onPressed: () =&gt; context.read&lt;CoinBloc&gt;().fetch(),<br>              ),<br>            _ =&gt; const SizedBox.shrink(),<br>          },<br>        ),<br>      );<br>}</pre><p>Can you tell I’m obsessed with tests? No? Here’s the test for the <strong>CoinsPage</strong> class under <em>test/pages/coins/coins_page_test.dart</em></p><pre>import &#39;dart:io&#39;;<br><br>import &#39;package:bloc_test/bloc_test.dart&#39;;<br>import &#39;package:crypto_app/blocs/coin/coin_bloc.dart&#39;;<br>import &#39;package:crypto_app/models/coin/coin.dart&#39;;<br>import &#39;package:crypto_app/pages/coins_page.dart&#39;;<br>import &#39;package:crypto_app/widgets/coin_tile.dart&#39;;<br>import &#39;package:crypto_app/widgets/no_coins_widget.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:flutter_bloc/flutter_bloc.dart&#39;;<br>import &#39;package:flutter_test/flutter_test.dart&#39;;<br>import &#39;package:pine/pine.dart&#39;;<br><br>import &#39;../../fixtures/models/coin_fixture_factory.dart&#39;;<br><br>/// Test case for the page Coins<br>void main() {<br>  late MockCoinBloc coinBloc;<br>  late List&lt;BlocProvider&gt; blocs;<br><br>  late List&lt;Coin&gt; coins;<br><br>  setUpAll(() =&gt; HttpOverrides.global = null);<br><br>  setUp(() {<br>    coinBloc = MockCoinBloc();<br>    blocs = [<br>      BlocProvider&lt;CoinBloc&gt;.value(value: coinBloc),<br>    ];<br><br>    coins = CoinFixture.factory().makeMany(3);<br>  });<br><br>  testWidgets(<br>      &#39;Testing that CoinsPage with a list of coins is rendered properly&#39;,<br>      (tester) async {<br>    whenListen(<br>      coinBloc,<br>      Stream.fromIterable([<br>        const FetchingCoinState(),<br>        FetchedCoinState(coins),<br>      ]),<br>      initialState: const FetchingCoinState(),<br>    );<br><br>    await tester.pumpWidget(<br>      DependencyInjectorHelper(<br>        blocs: blocs,<br>        child: const MaterialApp(<br>          home: CoinsPage(),<br>        ),<br>      ),<br>    );<br><br>    expect(find.text(&#39;Crypto App&#39;), findsOneWidget);<br><br>    expect(find.byType(CircularProgressIndicator), findsOneWidget);<br>    expect(find.byType(ListView), findsNothing);<br>    expect(find.byType(NoCoinsWidget), findsNothing);<br>    expect(find.byType(CoinTile), findsNothing);<br><br>    await tester.pumpAndSettle();<br><br>    expect(find.byType(CircularProgressIndicator), findsNothing);<br>    expect(find.byType(ListView), findsOneWidget);<br>    expect(find.byType(NoCoinsWidget), findsNothing);<br>    expect(find.byType(CoinTile), findsNWidgets(coins.length));<br>  });<br><br>  testWidgets(&#39;Testing that CoinsPage with no coins is rendered properly&#39;,<br>      (tester) async {<br>    whenListen(<br>      coinBloc,<br>      Stream.fromIterable([<br>        const FetchingCoinState(),<br>        const NoneCoinState(),<br>      ]),<br>      initialState: const FetchingCoinState(),<br>    );<br><br>    await tester.pumpWidget(<br>      DependencyInjectorHelper(<br>        blocs: blocs,<br>        child: const MaterialApp(<br>          home: CoinsPage(),<br>        ),<br>      ),<br>    );<br><br>    expect(find.text(&#39;Crypto App&#39;), findsOneWidget);<br><br>    expect(find.byType(CircularProgressIndicator), findsOneWidget);<br>    expect(find.byType(ListView), findsNothing);<br>    expect(find.byType(NoCoinsWidget), findsNothing);<br>    expect(find.byType(CoinTile), findsNothing);<br><br>    await tester.pumpAndSettle();<br><br>    expect(find.byType(CircularProgressIndicator), findsNothing);<br>    expect(find.byType(ListView), findsNothing);<br>    expect(find.byType(NoCoinsWidget), findsOneWidget);<br>    expect(find.byType(CoinTile), findsNothing);<br>  });<br><br>  testWidgets(&#39;Testing that CoinsPage with an error is rendered properly&#39;,<br>      (tester) async {<br>    whenListen(<br>      coinBloc,<br>      Stream.fromIterable([<br>        const FetchingCoinState(),<br>        ErrorFetchingCoinState(Exception()),<br>      ]),<br>      initialState: const FetchingCoinState(),<br>    );<br><br>    await tester.pumpWidget(<br>      DependencyInjectorHelper(<br>        blocs: blocs,<br>        child: const MaterialApp(<br>          home: CoinsPage(),<br>        ),<br>      ),<br>    );<br><br>    expect(find.text(&#39;Crypto App&#39;), findsOneWidget);<br><br>    expect(find.byType(CircularProgressIndicator), findsOneWidget);<br>    expect(find.byType(ListView), findsNothing);<br>    expect(find.byType(NoCoinsWidget), findsNothing);<br>    expect(find.byType(CoinTile), findsNothing);<br><br>    await tester.pumpAndSettle();<br><br>    expect(find.byType(CircularProgressIndicator), findsNothing);<br>    expect(find.byType(ListView), findsNothing);<br>    expect(find.byType(NoCoinsWidget), findsOneWidget);<br>    expect(find.byType(CoinTile), findsNothing);<br>  });<br>}<br><br>class MockCoinBloc extends MockBloc&lt;CoinEvent, CoinState&gt; implements CoinBloc {}</pre><p>Copilot is so helpful in these particular scenarios, it can suggest different use cases and it modulates the assertions accordingly</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/902/1*9NuXGYhKYBBBmn1KVUkzxg.png" /></figure><h3>Final thoughts</h3><p>Looks like we finished our application. It took me more time to write this article than to create this simple project with good architecture and unit testing 😂.</p><p>Are you curious about it? Do you want to take a look at the full source code? Here’s the repository on GitHub: <a href="https://github.com/AngeloAvv/crypto_app">https://github.com/AngeloAvv/crypto_app</a></p><h4>Thank you for reading 👋</h4><p>I hope you enjoyed this article. If you have any questions or suggestions please let me know in the comments down below.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=da4b30aa39fc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The never-ending debate on returning data from a screen in Flutter]]></title>
            <link>https://angeloavv.medium.com/the-never-ending-debate-on-returning-data-from-a-screen-in-flutter-4511d5a086fc?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/4511d5a086fc</guid>
            <category><![CDATA[navigation]]></category>
            <category><![CDATA[design-patterns]]></category>
            <category><![CDATA[dart]]></category>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Sat, 28 Oct 2023 17:56:24 GMT</pubDate>
            <atom:updated>2023-10-28T17:56:24.599Z</atom:updated>
            <content:encoded><![CDATA[<p>Passing data across screens in Flutter is a nightmare when it comes to type-checking. Even though you can use a <a href="https://en.wikipedia.org/wiki/Data_transfer_object">Data Transfer Object</a> to send data to a screen and take advantage of such a form of a contract, especially if you’re using the routing navigation, it’s not that practical when you want to return data from it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UMl-JAvPB5CQIvq6" /><figcaption>Photo by <a href="https://unsplash.com/@usmanyousaf?utm_source=medium&amp;utm_medium=referral">Usman Yousaf</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Passing data to a screen</h3><p>Let’s analyze the following example (a custom snippet from the official Flutter docs) to understand what we need to do to pass data from one screen to another:</p><pre>class FirstScreen extends StatelessWidget {<br>  const FirstScreen({super.key});<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: const Text(&#39;First Screen&#39;),<br>      ),<br>      body: Center(<br>        child: ElevatedButton(<br>          child: const Text(&#39;Open screen&#39;),<br>          onPressed: () {<br>            Navigator.push(<br>                context,<br>                MaterialPageRoute(builder: (context) =&gt; SecondScreen(title: &#39;title&#39;)),<br>              );<br>          },<br>        ),<br>      ),<br>    );<br>  }<br>}<br><br>class SecondScreen extends StatelessWidget {<br>  final String title;<br><br>  const SecondScreen({required this.title, super.key});<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(title),<br>      ),<br>      body: Center(<br>        child: ElevatedButton(<br>          onPressed: () {<br>            // Navigate back to first route when tapped.<br>          },<br>          child: const Text(&#39;Go back!&#39;),<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p>When we tap on the <em>ElevatedButton</em> in the <strong>FirstScreen</strong> class, we’re navigating to another route named <strong>SecondScreen</strong> and we’re passing custom data, in this case, the title of the screen. <strong>SecondScreen</strong> has a strong contract definition because we exactly know what kind of data we want to send. If someone else in the future changes the definition of the <strong>SecondScreen</strong> constructor, a syntactic error will occur.</p><h3>Return data from a screen</h3><p>Let’s keep adding features to our snippet so we can return data back:</p><pre>class FirstScreen extends StatelessWidget {<br>  const FirstScreen({super.key});<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: const Text(&#39;First Screen&#39;),<br>      ),<br>      body: Center(<br>        child: ElevatedButton(<br>          child: const Text(&#39;Open screen&#39;),<br>          onPressed: () async {<br>            final result = await Navigator.push(<br>                context,<br>                MaterialPageRoute(builder: (context) =&gt; SecondScreen(title: &#39;title&#39;)),<br>              );<br><br>            ScaffoldMessenger.of(context)<br>              ..removeCurrentSnackBar()<br>              ..showSnackBar(SnackBar(content: Text(&#39;$result&#39;)));<br>          },<br>        ),<br>      ),<br>    );<br>  }<br>}<br><br>class SecondScreen extends StatelessWidget {<br>  final String title;<br><br>  const SecondScreen({required this.title, super.key});<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(title),<br>      ),<br>      body: Center(<br>        child: ElevatedButton(<br>          onPressed: () {<br>            Navigator.pop(context, &#39;Let\&#39;s go back!&#39;);<br>          },<br>          child: const Text(&#39;Go back!&#39;),<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p>In the aforementioned snipped code I changed the behavior of both screens a little bit: in the <strong>SecondScreen</strong>, I added a <em>Navigator.pop</em> which navigates back to <strong>FirstScreen</strong> and returns data, in this case, a <em>String</em>, which will be shown as a <em>Snackbar</em> in the <strong>FirstScreen</strong>.</p><p>If we inspect the definition of <em>Navigator.push</em>, we can see there’s a custom generic type we can take advantage of to declare the type that will be returned if we decide to wait for the future. If we choose to not declare a type, then our result variable will be dynamic.</p><pre>final result = await Navigator.push(<br>    context,<br>    MaterialPageRoute(builder: (context) =&gt; SecondScreen(title: &#39;title&#39;)),<br>  );<br><br>ScaffoldMessenger.of(context)<br>  ..removeCurrentSnackBar()<br>  ..showSnackBar(SnackBar(content: Text(&#39;$result&#39;)));</pre><p>In this particular case, we’re able to show the SnackBar because the type of <em>result</em> implicitly inherits toString(), but if we want to explicitly use <em>result</em> as a String, we need to declare the generic type when using <em>Navigator.push</em>:</p><pre>final result = await Navigator.push&lt;String&gt;(<br>    context,<br>    MaterialPageRoute(builder: (context) =&gt; SecondScreen(title: &#39;title&#39;)),<br>  );<br><br>ScaffoldMessenger.of(context)<br>  ..removeCurrentSnackBar()<br>  ..showSnackBar(SnackBar(content: Text(result)));</pre><p>But what happens if someone in the future changes the type of the returned data? We have no control over the returned data in terms of a contract or type. In the first scenario, the type will always be <em>dynamic</em>, in the second one it will be <em>String</em>, but if someone returns data that is not a <em>String</em> and we declare the generic type as <em>String</em>, we won’t face any kind of syntactic error, the code will compile the same way as before, but as soon as our user will tap on the <em>ElevatedButton</em> in <strong>SecondScreen</strong>, our app will miserably crash.</p><h3>Take advantage of contracts</h3><p>If we don’t want to make mistakes when it comes to returning data from a screen, we need to define contracts across screens to exchange data. Let’s customize the snippet code to see how to introduce it:</p><pre>typedef SecondScreenCallback = void Function(String result);<br><br>class FirstScreen extends StatelessWidget {<br>  const FirstScreen({super.key});<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: const Text(&#39;First Screen&#39;),<br>      ),<br>      body: Center(<br>        child: ElevatedButton(<br>          child: const Text(&#39;Open screen&#39;),<br>          onPressed: () {<br>            Navigator.push(<br>                context,<br>                MaterialPageRoute(builder: (context) =&gt; SecondScreen(<br>                  title: &#39;title&#39;,<br>                  onResult: (result) {<br>                    ScaffoldMessenger.of(context)<br>                      ..removeCurrentSnackBar()<br>                      ..showSnackBar(SnackBar(content: Text(&#39;$result&#39;)));<br>                  }<br>                )),<br>              );<br>          },<br>        ),<br>      ),<br>    );<br>  }<br>}<br><br>class SecondScreen extends StatelessWidget {<br>  final String title;<br>  final SecondScreenCallback onResult;<br><br>  const SecondScreen({required this.title, required this.onResult, super.key});<br><br>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(title),<br>      ),<br>      body: Center(<br>        child: ElevatedButton(<br>          onPressed: () {<br>            Navigator.pop(context);<br><br>            onResult.call(&#39;Let\&#39;s go back!&#39;);<br>          },<br>          child: const Text(&#39;Go back!&#39;),<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><p><strong>SecondScreenCallback</strong> is our contract: we defined a custom callback, a simple function that returns void with a single parameter, the message that will be shown in our <strong>SnackBar</strong> as soon as <em>SecondScreen</em> is popped.</p><p>Instead of waiting for the future when we push <em>SecondScreen</em>, we declare an anonymous function that will be triggered on the screen where we want to send back data. This function contains our Data Transfer Object, in this case, a simple String that will be used later.</p><h3>Pros and Cons</h3><p>Of course, the two solutions aren’t 100% perfect, they both share pros and cons, but I prefer the second one as it gives me a bit more control over the data I’m returning.</p><p>If you decide to go with the contractless implementation, you don’t need to create a contract and declare custom callbacks that can be easily forgotten when you call <em>Navigator.pop</em>. On the other hand, you have no control over the types of data you’re returning.</p><p>With the contract implementation, of course, you have more control over the data but bear in mind you should invoke the callback every time you want to navigate back using <em>Navigator.pop</em>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4511d5a086fc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to setup a logging system in Flutter with Talker]]></title>
            <link>https://angeloavv.medium.com/how-to-setup-a-logging-system-in-flutter-with-talker-45577688e54e?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/45577688e54e</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[bloc]]></category>
            <category><![CDATA[crash]]></category>
            <category><![CDATA[crashlytics]]></category>
            <category><![CDATA[logging]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Thu, 17 Aug 2023 10:13:14 GMT</pubDate>
            <atom:updated>2023-08-17T11:50:58.476Z</atom:updated>
            <content:encoded><![CDATA[<p>One of the most important things we should do when we release an application, no matter what the platform target is, is to keep track of what’s going on when a user interacts with it. The easiest way to acknowledge this is by using a logging system.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*IEsSDS5RZU2bGp7M" /><figcaption>Photo by <a href="https://unsplash.com/@ilyapavlov?utm_source=medium&amp;utm_medium=referral">Ilya Pavlov</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>In the past few years, I created my custom logging systems by combining multiple different tools to log information in Flutter such as <a href="https://pub.dev/packages/logger">logger</a> and <a href="https://pub.dev/packages/firebase_crashlytics">Firebase Crashlytics</a>. The main drawback of using it was strictly related to the hard coupling between the implementation and Firebase Crashlytics: if my application, for any reason, wasn’t meant to include Firebase, I had to change the implementation of the logging system.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*D2o87ocidCHoUP5b" /><figcaption>Photo by <a href="https://unsplash.com/@atik1616?utm_source=medium&amp;utm_medium=referral">Atik sulianami</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Looking for something different</h3><p>Logger is a fantastic library, but I find it a little bit oldish and tricky when it comes to plugging things. For instance, if I want to intercept network calls and use Logger as a pipe, strings are not printed out in a fancy way and it becomes difficult to understand what’s going on.</p><p>I started looking for new libraries on <a href="https://pub.dev/">pub.dev</a> and I found <a href="https://pub.dev/packages/talker">Talker</a>, which by the way integrates pretty well with my Flutter stack: it supports a custom logger for <a href="https://pub.dev/packages/flutter_bloc">Bloc</a> and <a href="https://pub.dev/packages/dio">Dio</a>, and adding Firebase crashlytics is easy peasy. To be honest, I’m really surprised by how this library is not so well known in the Flutter community.</p><h3>How to integrate it</h3><p>Let’s get to the point: first of all, we need to add Talker as a flutter dependency:</p><pre>flutter pub add talker</pre><p>If you’re following the <a href="https://angeloavv.medium.com/pine-a-lightweight-architecture-helper-for-your-flutter-projects-1ce69ac63f74?source=user_profile---------1----------------------------">Pine architecture</a> as I do, we should inject the Talker instance in the widget tree, let’s open the <em>providers.dart</em> file and let’s inject it:</p><pre>Provider&lt;Talker&gt;(<br>  create: (context) =&gt; Talker(),<br>),</pre><p>Now that we’ve properly injected the Talker instance in the tree, we can use it in our services, repositories, blocs, and so on. For instance, I created this repository to obtain news from an RSS feed:</p><pre>class NewsRepository {<br>  final RSSFeed feed;<br>  final Talker logger;<br><br>  const NewsRepository({<br>    required this.feed,<br>    required this.logger,<br>  });<br><br>  Future&lt;AtomFeed&gt; get news async {<br>    String response;<br>    try {<br>      logger.info(<br>        &#39;[NewsRepository] Getting news from API&#39;,<br>      );<br><br>      response = await feed.rss();<br><br>      logger.good(<br>        &#39;[NewsRepository] Got news from API&#39;,<br>      );<br>    } catch (error, stackTrace) {<br>      logger.error(<br>        &#39;Thrown exception during news request&#39;,<br>        error,<br>        stackTrace,<br>      );<br>      throw RepositoryError(error: error);<br>    }<br><br>    return AtomFeed.parse(response);<br>  }<br>}</pre><p>As you can see, this repository has two dependencies: an RSSFeed instance named feed, which is a service, and a Talker instance known as logger. This class contains a single method that calls the RSS method from the feed service and logs the information of what’s going on.</p><p>Before invoking <em>feed.rss()</em> we can spot</p><pre>logger.info(<br>  &#39;[NewsRepository] Getting news from API&#39;,<br>);</pre><p>this allows us to log information at the info level. We’re basically telling “Okay, I’m about to call the RSS service”.</p><p>If you don’t know what logging levels are, please take a look at <a href="https://sematext.com/blog/logging-levels/">this</a> very well-explained document.</p><p>Everything is embraced into a try/catch block: if everything went well, we can use the <em>.good()</em> method from the Talker library to express success, this will print out a green text surrounded by a frame.</p><pre>logger.good(<br>  &#39;[NewsRepository] Got news from API&#39;,<br>);</pre><p>If something goes south, the code execution will enter the catch block and before handling the exception, we can log the error using of course the <em>.error()</em> method: this will print out a red text surrounded by a frame. We can provide more information when logging the error by passing both the error and the stack trace variables.</p><pre>logger.error(<br>  &#39;Thrown exception during news request&#39;,<br>  error,<br>  stackTrace,<br>);</pre><p>We can also use many different methods to log the behavior of our application. In the following image, we can spot the <em>.debug()</em> and the <em>.warning()</em> outputs which respectively print out a white text and an orange text surrounded by a box.</p><figure><img alt="How Talker behaves with different outputs" src="https://cdn-images-1.medium.com/max/1024/0*P4Sxv6R5NMfE8D7o" /></figure><h3>Send the information to Crashlytics</h3><p>To send the information to Firebase Crashlytics, we need to create and register an observer. We can register a single observer and I hope the maintainers of the library will find a way to allow us to register multiple ones, but as for now, let’s create a new class to log events to crashlytics.</p><pre>import &#39;package:firebase_crashlytics/firebase_crashlytics.dart&#39;;<br>import &#39;package:talker/talker.dart&#39;;<br><br>class CrashlitycsTalkerObserver extends TalkerObserver {<br><br>  final FirebaseCrashlytics crashlytics;<br><br>  const CrashlitycsTalkerObserver({required this.crashlytics,});<br><br>  @override<br>  void onLog(TalkerDataInterface log) {<br>    crashlytics.log(log.generateTextMessage());<br>  }<br><br>  @override<br>  void onError(err) {<br>    crashlytics.recordError(<br>      err.error,<br>      err.stackTrace,<br>      reason: err.message,<br>    );<br>  }<br><br>  @override<br>  void onException(err) {<br>    crashlytics.recordError(<br>      err.exception,<br>      err.stackTrace,<br>      reason: err.message,<br>    );<br>  }<br>}</pre><p>First of all, we should create a class that extends a <em>TalkerObserver</em>. It’s not mandatory to override all the methods in the abstract class, we can decide what kind of error we want to send to crashlytics. In my case, I prefer to send everything to better understand what’s happening during the app’s execution flow.</p><p>The <em>onLog</em> method will be triggered every time we call <em>.warning()</em>, <em>.debug()</em>, <em>.info(),</em> and .good(). In case of an error or an exception, <em>onError</em> and <em>onException</em> will be called instead.</p><p>Now that we have our observer, we need to register it, so let’s go back to the <em>providers.dart</em> file and let’s add it when creating our Talker instance.</p><pre>Provider&lt;Talker&gt;(<br>  create: (context) =&gt; Talker(<br>    observer: CrashlitycsTalkerObserver(<br>      crashlytics: context.read(),<br>    ),<br>  ),<br>),</pre><p>Do not forget to also inject a <em>FirebaseCrashlytics</em> instance in the tree, otherwise, <em>Provider</em> won’t be able to locate the crashlytics service needed by the <em>Observer</em>.</p><p>And that’s it, every time we’ll log something with Talker, everything will be also sent to our Firebase Crashlytics console.</p><h3>Talker third-party integrations</h3><p>If you also want to integrate a Dio logger and a Bloc logger, simply run</p><pre>flutter pub add talker_dio_logger<br>flutter pub add talker_bloc_logger</pre><p>To add a Dio logger to our Dio instance, simply add a <em>TalkerDioLogger</em> instance in the Dio interceptors. If you’re following the Pine architecture, you can do it in the <em>providers.dart</em> file as follows:</p><pre>Provider&lt;Dio&gt;(<br>  create: (_) {<br>    final dio = Dio(BaseOptions());<br>    if (kDebugMode) {<br>      dio.interceptors.add(<br>        TalkerDioLogger(<br>          settings: const TalkerDioLoggerSettings(<br>            printRequestHeaders: true,<br>            printResponseHeaders: true,<br>            printResponseMessage: true,<br>          ),<br>        ),<br>      );<br>    }<br><br>    return dio;<br>  },<br>),</pre><p>In this case, I’m registering the interceptor when we’re running the application in debug mode. For security reasons, I don’t want to log Dio calls when the app is in production.</p><p>To register a Bloc logger, simply add this instruction in your <em>main.dart</em> file before executing <em>runApp()</em>:</p><pre>Bloc.observer = TalkerBlocObserver();</pre><p>And we’re good to go. We finally have control of what’s happening in our application when a user interacts with it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=45577688e54e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to gather desktop disk drives information in Flutter]]></title>
            <link>https://angeloavv.medium.com/how-to-gather-desktop-disk-drives-information-in-flutter-6c17adcb674c?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/6c17adcb674c</guid>
            <category><![CDATA[flutter-plugin]]></category>
            <category><![CDATA[hard-drive]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[flutter-desktop]]></category>
            <category><![CDATA[desktop-app]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Sun, 18 Sep 2022 07:27:55 GMT</pubDate>
            <atom:updated>2022-09-18T07:27:55.844Z</atom:updated>
            <content:encoded><![CDATA[<p>In my humble opinion, <strong>Flutter desktop</strong> has been a wonderful breakthrough for desktop app development, more than Flutter web.</p><p>My experience as a developer started almost fourteen years ago: I wasn’t a good programmer because I was a lazy person, I didn’t want to study algorithms and data structures from books and the only thing I kept doing was copy-pasting stuff or doing things without knowledge of the facts.</p><p>Why am I telling you that? Because of that attitude, I decided to stick with a simple programming language like Visual Basic .NET to start developing my first desktop apps.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1O7ztZfNb7Y6-k_f3BpZPQ.jpeg" /><figcaption>A screenshot of a Visual Studio IDE with a Windows Form app</figcaption></figure><p>In the following years, as many of you know, I abandoned desktop app development in favor of mobile app development. In the meantime, I had the chance to test some other desktop frameworks or technologies like <strong>Electron</strong> or <strong>GTK</strong>, but nothing has surprised me like Flutter desktop: for instance, Electron apps are generally heavy-resources consuming due to the Chromium instances.</p><h3>The disks desktop library</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M2J-LSV8aYMG-ui0vSOiJg.png" /></figure><p>After this boring introduction, let’s stick back to the point of this article. Even though Flutter desktop moved to stable a few months ago, it still lacks some things like interaction with its native operative system.</p><p>In my case, I needed to develop a desktop app able to burn ISO or IMG images to external disk drives. When I started diving into the internet looking for libraries or code to do that I found literally nothing.</p><p>I mean, you can do very high-level things like gathering paths with some well-known libraries like path provider, but I wasn’t able to enumerate the available disk drives in my system or understand whether that drive is a system drive, is removable, and so on.</p><p>And that’s why I decided to develop disks_desktop, a Flutter desktop library able to retrieve installed disk drives information. disks_desktop is available for <strong>Windows</strong>, <strong>macOS</strong> and <strong>Linux</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*cvUAUza2xNA925uSZc9jjQ.png" /><figcaption>The disks_desktop library logo</figcaption></figure><p>With Disks Desktop you can get access to disks’ information like:</p><ul><li>block size</li><li>bus type</li><li>bus version</li><li>description</li><li>device name</li><li>device path</li><li>logical block size</li><li>available mountpoints</li><li>disk size</li><li>partition table type</li><li>is in error</li><li>is a card</li><li>is read only</li><li>is removable</li><li>is scsi</li><li>is system</li><li>is uas</li><li>is usb</li><li>is virtual</li><li>is raw</li></ul><h4>Setup</h4><p>In general, put it under <a href="https://dart.dev/tools/pub/dependencies">dependencies</a>, in your <a href="https://dart.dev/tools/pub/pubspec">pubspec.yaml</a>:</p><pre>dependencies:<br>  disks_desktop: ^1.0.1</pre><p>You can install packages from the command line:</p><pre>flutter pub get</pre><p>or simply add it through the command line:</p><pre>flutter pub add disks_desktop</pre><h4>Usage</h4><p>To get the list of the available drives with their details, simply create an instance of a Disk Repository and invoke the query getter.</p><p>Example:</p><pre><strong>final</strong> repository = DiskRepository();<br><strong>final</strong> disks = <strong>await</strong> repository.query;</pre><p>You can also use it with a FutureBuilder:</p><pre>FutureBuilder&lt;List&lt;Disk&gt;&gt;(<br>  future: DisksRepository().query,<br>  builder: (context, snapshot) =&gt; [...]<br>),</pre><h3>Links and Examples</h3><p>The disks_desktop library is available under <a href="https://pub.dev/packages/disks_desktop">pub.dev</a> and on <a href="https://github.com/AngeloAvv/disks">GitHub</a>. Please check the <a href="https://github.com/AngeloAvv/disks/tree/main/example">example</a> project to better understand how to create a desktop app.</p><h3>Thank you for reading 👋</h3><p>I hope you enjoyed this article. If you have any queries or suggestions please let me know in the comments down below.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6c17adcb674c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Pine: A lightweight architecture helper for your Flutter Projects]]></title>
            <link>https://angeloavv.medium.com/pine-a-lightweight-architecture-helper-for-your-flutter-projects-1ce69ac63f74?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/1ce69ac63f74</guid>
            <category><![CDATA[clean-code]]></category>
            <category><![CDATA[flutter-architecture]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[dependency-injection]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Sun, 04 Sep 2022 17:03:07 GMT</pubDate>
            <atom:updated>2022-09-04T17:03:07.047Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QpO8BoRtEMfk8lfp.png" /><figcaption>The Pine logo</figcaption></figure><p>When we speak about the architecture of a Flutter project, every single component resides in the widget tree. It could be a visible widget or something less tangible that has to do with the business logic like a repository or a service, but at the end of the day, everything is there.</p><p>Since the beginning, <strong>Dependency Injection</strong> has been the common design pattern used in Flutter to inject elements into the widget tree. Flutter’s early adopters will easily remember about <a href="https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html">InheritedWidgets</a>, a particular type of widget mainly used to inject non-tangible elements into the widget tree. <strong>InheritedWidget </strong>is really easy to understand and pretty straightforward when it comes to its use. The main downside is the boilerplate code around its definition.</p><p>The revolution started with <a href="https://pub.dev/packages/provider">Provider</a>, a library that simplified the injection of different types of elements into the widget tree. <strong>Provider</strong> has been widely adopted by the Flutter community and works pretty well in combination with <a href="https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html">ChangeNotifier</a>. Provider aims to remove the boilerplate code from the definition of an InheritedWidget. In fact, in terms of the number of lines of code, injecting an element in the widget tree with Provider takes only a single line:</p><pre>Provider&lt;Service&gt;(create: (context) =&gt; Service());</pre><p>Even the most used state management system decided to rely on Provider to inject its component in the widget tree: we are talking about <strong>BLoC</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*AltSzjVEL55WMyXd.png" /><figcaption>flutter_bloc logo</figcaption></figure><p>The <a href="https://pub.dev/packages/flutter_bloc.">flutter_bloc</a> library created by <a href="https://medium.com/@felangelov">Felix Angelov</a> is using a custom provider to inject the blocs into the widget tree.</p><pre>BlocProvider&lt;Bloc&gt;(create: (context) =&gt; Bloc());</pre><h3>The problem</h3><p>Flutter is a relatively easy framework that gives everyone the ability to create applications. Most people out there can to create a product that works, but I strongly believe that as <strong>developers</strong> we should focus more on <em>how things work</em>.</p><p>Software engineering comes in help: if you ever attended a lesson in computer science at the University, one of the first things that will teach you is about software architectures.</p><p>When it comes to Flutter architecture, I love to apply a customized version of the <a href="https://medium.com/ruangguru/an-introduction-to-flutter-clean-architecture-ae00154001b0">Flutter Clean Code Architecture</a>. Generally speaking, my projects are organized into four different layers as follows:</p><h4>UI Layer</h4><p>The UI layer is composed of screens, pages, and widgets. Here you can create a combination of widgets that reside in Stateful and Stateless widgets. Those widgets typically interact with the second layer which is the state manager, in my case I like to use BLoC.</p><h4>Business Logic Component Layer</h4><p>The business logic component (or BLoC) layer, is the layer that is typically a middleware and aims to translate the user interaction with the UI into a set of instructions for the repository layer.</p><h4>Repository Layer</h4><p>The repository layer is an abstraction layer, a single source/point of truth, that queries the lower level, the service layer, translates this information into app-readable data using a set of mappers to convert <a href="https://en.wikipedia.org/wiki/Data_transfer_object">DTOs</a> into models, and delivers back this data, typically to the upper layers like a BLoC.</p><h4>Service Layer</h4><p>The service layer is the bottom layer in this kind of architecture. Here it’s easy to find different services like a REST Client that queries a REST API to get data from a server, or access to a database as a DAO.</p><h3>The solution</h3><p>During these years I used a combination of <strong>MultiProvider</strong>, <strong>MultiRepositoryProvider</strong>, and <strong>MultiBlocProvider</strong> to inject items into the widget tree. But as soon as your project starts growing and your app becomes more complex, it’s easier to have tons of injections in your app widget.</p><p>To avoid an always-growing app widget, I decided to split these providers into different files referenced by a single widget that I decided to name <strong>DependencyInjector</strong>.</p><pre>class DependencyInjector extends StatelessWidget {<br>  final Widget child;<br><br>  const DependencyInjector({<br>    Key? key,<br>    required this.child,<br>  }) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) =&gt; _Providers(<br>        child: _Repositories(<br>          child: _Blocs(<br>            child: child,<br>          ),<br>        ),<br>      );<br>}</pre><p>Each nested element contained in the DependencyInjector widget is also a private Widget that wraps a MultiProvider, MultiRepositoryProvider, and MultiBlocProvider according to its reference layer.</p><p>This implementation sooner became a blocker when I wanted to start testing a single component coming from a particular layer. That’s why I decided to rewrite the dependency injector widget to simplify the process of injecting components into the widget tree.</p><p>Since all these widgets were continuously copied to newer projects, I decided to create a library for myself and for everyone who wants to simplify the injection process, and whose projects rely on Provider and BLoC as a state manager.</p><h3>Pine</h3><p>To add this library to your Flutter project, simply type this instruction in your command line:</p><pre>flutter pub add pine</pre><p>Once you’ve done this, you can start injecting your elements into the widget tree with <a href="https://github.com/MyLittleSuite/pine"><strong>Pine</strong></a>.</p><h4>The Architecture</h4><p>Elements are injected from top to bottom.</p><ol><li>The first elements added in the widget tree are mappers, particularly useful to convert data coming from data layers to something that should be used in the presentation layer.</li><li>The second elements are providers: here you can inject services that manipulate data or access it like REST clients or DAOs interfaces.</li><li>The third layer is used to inject the repositories that access the data layer using an abstraction layer.</li><li>The last layer is used to inject the logic: Pine relies on BLoC as a state manager, that’s why we’ll inject global scoped BLoCs.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*AvcnXlNGUQZYfImZ.png" /><figcaption>The Pine architecture</figcaption></figure><p>Each element might rely on the top level ones and are generally accessed from the bottom level ones: for instance, a repository may need access to a REST client service to gather data, save it into a database, and return it to a BLoC. To access top-level items, you can use the read and watch functions exposed by Provider.</p><h4>The Interactions</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*s04UcsztmVN39sgQ.png" /><figcaption>The Pine interaction</figcaption></figure><h4>How to use it</h4><p>A pine architecture can be achieved by using the <strong>DependencyInjectorHelper</strong> widget, which helps you to inject different types of elements into the widget tree. If you are working on a simple project, you should use the DependencyInjectorHelper straight into your main app widget.</p><pre>class App extends StatelessWidget {<br>  const App({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) =&gt; DependencyInjectorHelper(<br>    blocs: [<br>      BlocProvider&lt;Bloc&gt;(<br>        create: (context) =&gt; Bloc(<br>          repository: context.read(),<br>        )..action(),<br>      ),<br>    ],<br>    mappers: [<br>      Provider&lt;DTOMapper&lt;DTO, Model&gt;&gt;(<br>        create: (_) =&gt; Mapper(),<br>      ),<br>    ],<br>    providers: [<br>      Provider&lt;Service&gt;(<br>        create: (context) =&gt; Service(),<br>      ),<br>    ],<br>    repositories: [<br>      RepositoryProvider&lt;Repository&gt;(<br>        create: (context) =&gt; RepositoryImpl(<br>          service: context.read(),<br>          mapper: context.read(),<br>        ),<br>      ),<br>    ],<br>    child: MaterialApp(<br>      title: &#39;App&#39;,<br>      theme: ThemeData(<br>        primarySwatch: Colors.blue,<br>        visualDensity: VisualDensity.adaptivePlatformDensity,<br>      ),<br>      home: const HomePage(),<br>    ),<br>  );<br>}</pre><p>As the project grows, it’s better to create a new widget that wraps all of these items in different files. We can name this widget <strong>DependencyInjector</strong>.</p><p><strong>dependency_injector.dart</strong></p><pre>part &#39;blocs.dart&#39;;<br>part &#39;mappers.dart&#39;;<br>part &#39;providers.dart&#39;;<br>part &#39;repositories.dart&#39;;<br><br>class DependencyInjector extends StatelessWidget {<br>  final Widget child;<br><br>  const DependencyInjector({<br>    Key? key,<br>    required this.child,<br>  }) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) =&gt; DependencyInjectorHelper(<br>    blocs: _blocs,<br>    providers: _providers,<br>    mappers: _mappers,<br>    repositories: _repositories,<br>    child: child,<br>  );<br>}</pre><p>In this widget, we need to define all the dependencies that are required in our project. I prefer splitting these elements into different files according to their type. In our example, we will create four different files because we inject blocs, mappers, providers, and repositories.</p><p><strong>blocs.dart</strong></p><pre>part of &#39;dependency_injector.dart&#39;;<br><br>final List&lt;BlocProvider&gt; _blocs = [<br>  BlocProvider&lt;Bloc&gt;(<br>    create: (context) =&gt; Bloc(<br>      repository: context.read(),<br>    )..action(),<br>  ),<br>];</pre><p><strong>mappers.dart</strong></p><pre>part of &#39;dependency_injector.dart&#39;;<br><br>final List&lt;SingleChildWidget&gt; _mappers = [<br>  Provider&lt;DTOMapper&lt;DTO, Model&gt;&gt;(<br>    create: (_) =&gt; Mapper(),<br>  ),<br>];</pre><p><strong>providers.dart</strong></p><pre>part of &#39;dependency_injector.dart&#39;;<br><br>final List&lt;SingleChildWidget&gt; _providers = [<br>  Provider&lt;Service&gt;(<br>    create: (context) =&gt; Service(),<br>  ),<br>];</pre><p><strong>repositories.dart</strong></p><pre>part of &#39;dependency_injector.dart&#39;;<br><br>final List&lt;RepositoryProvider&gt; _repositories = [<br>  RepositoryProvider&lt;Repository&gt;(<br>    create: (context) =&gt; RepositoryImpl(<br>      service: context.read(),<br>      mapper: context.read(),<br>    ),<br>  ),<br>];</pre><p>Once we finished defining the global dependencies to inject into the widget tree, we need to wrap our <strong>MaterialApp</strong>/<strong>CupertinoApp</strong> with the DependencyInjector widget as follows:</p><pre>class App extends StatelessWidget {<br>  const App({Key? key}) : super(key: key);<br><br>  @override<br>  Widget build(BuildContext context) =&gt; DependencyInjector(<br>    child: MaterialApp(<br>      title: &#39;App&#39;,<br>      theme: ThemeData(<br>        primarySwatch: Colors.blue,<br>        visualDensity: VisualDensity.adaptivePlatformDensity,<br>      ),<br>      home: const HomePage(),<br>    ),<br>  );<br>}</pre><h4>Testing</h4><p>With the DependencyInjectorHelper it’s easy to inject dependencies into the widget tree. Simply wrap the widget you need to test with the <strong>DependencyInjectorHelper</strong> class and inject the dependencies you need.</p><p>In the following example, we will test the HomePage widget which relies on a Bloc. Before pumping the MaterialApp containing the HomePage, we will wrap it as follows:</p><pre>await tester.pumpWidget(<br>  DependencyInjectorHelper(<br>    blocs: [<br>      BlocProvider&lt;Bloc&gt;.value(value: bloc),<br>    ],<br>    child: const MaterialApp(<br>      home: HomePage(),<br>    ),<br>  ),<br>);</pre><p>Of course, since we are testing the HomePage, we are injecting a mocked bloc.</p><h4>Links and Examples</h4><p>The Pine library is available under <a href="https://pub.dev/packages/pine">pub.dev</a> and on <a href="https://github.com/MyLittleSuite/pine">GitHub</a>. Please check the <a href="https://github.com/MyLittleSuite/pine/tree/master/example">example</a> project <strong><em>News App</em></strong> to better understand how to model a <strong>Pine</strong> based Flutter Architecture.</p><h3>Thank you for reading 👋</h3><p>I hope you enjoyed this article. If you have any queries or suggestions please let me know in the comments down below.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1ce69ac63f74" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A cheat sheet to master interviews as a Flutter developer]]></title>
            <link>https://angeloavv.medium.com/a-cheat-sheet-to-master-interviews-as-a-flutter-developer-ce722b6b936?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/ce722b6b936</guid>
            <category><![CDATA[interview-questions]]></category>
            <category><![CDATA[career-advice]]></category>
            <category><![CDATA[interview]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Thu, 16 Jun 2022 06:59:03 GMT</pubDate>
            <atom:updated>2022-06-16T06:59:03.999Z</atom:updated>
            <content:encoded><![CDATA[<p>In the past six months, I’ve been on both sides of the recruiting process: for those who better know me, I often attend interviews to win shyness, and anxiety, and to keep myself updated about the development scene.</p><p>I always wanted to live an experience as a developer for a company that wasn’t speaking my mother tongue, so I decided to raise the bar by attending interviews in English.</p><p>Once I got a job offer, my former company needed to replace me so I was involved in the recruiting process of selecting a new Flutter developer. This has been an amazing experience because I had the opportunity to meet a lot of candidates and learn many things about the recruiting world as a technical interviewer.</p><p>Since January 2022, I attended a dozen of interviews with different companies as I was applying as a Senior Mobile Developer or Technical Lead, and I had the chance to better understand what companies look for in terms of knowledge and skills.</p><p>And that’s the purpose of this article: to give you a set of non-mandatory rules you can follow to master your next interviews as a Flutter developer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fQMlhHe0A8oljG1s" /><figcaption>Photo by <a href="https://unsplash.com/@linkedinsalesnavigator?utm_source=medium&amp;utm_medium=referral">LinkedIn Sales Solutions</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h4>Rule number one: Know what you did (and you’re doing)</h4><p>One of the questions I like to ask, and many companies also like to, is to describe your journey in the company you’re working for, or you’ve worked for. The answers may vary according to the subjects involved in the interview, but if you’re speaking to a technical one, he might expect you to describe how your contribution impacted on the product.</p><p>You are allowed to speak about technologies, frameworks, and tools, and I think you must <strong>remember </strong>at least a bunch of them. One gold question could be related to your favorite set of third-party libraries that you typically use inside your Flutter project: you could mention three to five dependencies that you use in your daily routine as a Flutter developer. If you are not able to answer that question the interviewer may assume that you are not as fluent as expected.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2z5krvg_UjiMHClI" /><figcaption>Photo by <a href="https://unsplash.com/@marvelous?utm_source=medium&amp;utm_medium=referral">Marvin Meyer</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><strong>Rule number two: State Management</strong></p><p>Flutter has a very easy learning curve. I think this is one of the main drawbacks of this amazing framework because it allows people without any coding background to start creating apps.</p><p>Unless you’re applying for some sort of internship program, I think it’s better to master <strong>at least</strong> one state management. There are plenty of them out there, but please, <strong>please</strong>, don’t say you use <em>setState</em> only.</p><p>Another common mistake people do is to misunderstand the behaviors of the components involved in a state management pattern. Let me give you an example. One of the questions I like to ask the candidate is to tell me what kinds of state management patterns he uses.</p><blockquote>I’m confident using Provider.</blockquote><p>Let me get this straight to the point. <strong>Provider </strong>is not a state management pattern. It’s a way of realizing dependency injection in the widget tree by wrapping the InheritedWidget behavior. The component involved in the state management is <strong>ChangeNotifier</strong>, not certainly Provider!</p><p>TL;DR, the same argument is also valid for the <strong>GetIt </strong>library.</p><p>I think this confusion also comes from the <a href="https://docs.flutter.dev/development/data-and-backend/state-mgmt/options">official Flutter documentation</a> that provides a list of methods to handle state management, without going deep into the definition.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Lqpl0f3pyFOEQJ_U" /><figcaption>Photo by <a href="https://unsplash.com/@neonbrand?utm_source=medium&amp;utm_medium=referral">Kenny Eliason</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><strong>Rule number three: Asynchronous programming</strong></p><p>Flutter is a framework that relies on streams and futures: it mostly uses a reactive programming approach, so knowing how to handle streams and futures is <strong>mandatory</strong>.</p><p>Keep in mind that the interviewer may ask you how Flutter handles multitasking. All of the stuff in Flutter is done in the main thread: if you plan to draw a widget or perform a request to an API, the framework will take care of it. When you need to execute heavy background processes that may involve a lot of CPU or GPU, it’s better you spawn an <strong>Isolate</strong>. An Isolate is a process that runs separately from the main thread and can communicate with the main one using two ports.</p><p>Speaking about streams, probably you won’t be asked to explain how things work under the hood. Just remember the basic stuff like how to listen to it, how to consume it on the UI, and so on. For instance, you may need to show a CircularProgressIndicator as soon as a particular stream hasn’t emitted new events yet, to do that you can use a <strong>StreamBuilder</strong>. Also, if you need to listen for another stream to invoke other methods in your logic, you can listen to its changes by invoking the <strong><em>.listen()</em></strong> method.</p><p>Another tricky question would be to elaborate on the differences between <strong>single subscriptions</strong> and <strong>broadcast </strong>streams, or between <strong>yield </strong>and <strong>yield*</strong>.</p><p>It’s the same for futures. Most of the time you will invoke <strong>async</strong> methods that return a <strong>Future.</strong> For instance, you could request some data coming from the internet by querying a REST API. Just make sure you know the differences by invoking a future with or without the <strong>await</strong> keyword, or how to interact with them on the UI with a <strong>FutureBuilder</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gp8F_oOvhDYwBy74" /><figcaption>Photo by <a href="https://unsplash.com/@andyjh07?utm_source=medium&amp;utm_medium=referral">Andy Holmes</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h4>Rule number four: Testing</h4><p>If you’re trying to apply for a well-structured company, writing tests is one of the most important things you need to know. In Flutter there are basically three ways of writing tests:</p><ul><li>Unit tests: they give you the ability to test a small part of the code, typically a fragment that doesn’t interact too much with other logical layers of the application.</li><li>UI tests: they give you the ability to test a finite widget or a screen/page. It’s a specialization of the unit test applied to the UI</li><li>Integration tests: they give you the ability to test larger areas of your application that interacts with each other. Those kinds of tests are more complex and take more time to write, run and debug.</li></ul><p>You also need to know how the <strong>mocking </strong>process works. When you write tests, you need to define boundaries the test doesn’t need to trespass. These boundaries allow you to test only some specific parts of the code.</p><p>Imagine you need to test a Repository. Typically a Repository is an abstraction layer that communicates with the lower ones without letting the invoker know which one it is. The lower layers invoked could be a REST API <strong>Service</strong> or a <strong>Data Access Object</strong> that reads and writes data to a database instance. In this particular case, you don’t need to test the whole tour, so you will just mock services and DAOs in order to focus on your repository.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CZlrkQZN0Cmfq6y3" /><figcaption>Photo by <a href="https://unsplash.com/es/@nguyendhn?utm_source=medium&amp;utm_medium=referral">Nguyen Dang Hoang Nhu</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h4><strong>Bonus track: Questions</strong></h4><p>I will leave here you a list of the most common questions I received during my interviewing process and some of them I like to ask candidates:</p><ul><li>What are the differences between Stateless and Stateful Widgets?</li><li>Describe the StatefulWidget lifecycle</li><li>What are LocalKeys and GlobalKeys? How would you use them?</li><li>Can you tell me what is the purpose of the Scaffold widget?</li><li>When would you use the SafeArea widget?</li><li>Do you use any kind of architecture for your projects? Have you ever heard about Clean Code Architecture?</li><li>How do you interact with the native? Do you know how to invoke Java/Kotlin or ObjectiveC/Swift code from Flutter?</li><li>How do you deploy your applications? Have you ever used any kind of automatic tools to run tests or deploy your application like Fastlane?</li><li>How do you manage internationalization in your application?</li><li>How do you navigate across pages? Can you explain what kind of methods you use to navigate across them?</li><li>Modifiers: can you tell me the differences between const, final and late?</li><li>Animations: can you tell me what kinds of animations you can create in Flutter?</li><li>Have you ever created flavors inside your flutter project? Do you use any kind of third-party libraries to do that?</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ce722b6b936" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to safely invoke native code in Flutter with Pigeon]]></title>
            <link>https://angeloavv.medium.com/how-to-safely-invoke-native-code-in-flutter-with-pigeon-1ff9d741dd4d?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/1ff9d741dd4d</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[platform-channel]]></category>
            <category><![CDATA[native-development]]></category>
            <category><![CDATA[method-channel]]></category>
            <category><![CDATA[pigeons]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Sun, 17 Apr 2022 15:10:28 GMT</pubDate>
            <atom:updated>2022-04-17T15:10:28.795Z</atom:updated>
            <content:encoded><![CDATA[<p>Creating Flutter apps that target many platforms is pretty straightforward, but not all code features are available in the Flutter SDK: sometimes you have to write them on your own because each platform has its own way to communicate with its native SDK.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VVUVnZzy4XOq0ULy" /><figcaption>Photo by <a href="https://unsplash.com/@christianw?utm_source=medium&amp;utm_medium=referral">Christian Wiediger</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>When it comes to writing platform-specific code in Flutter, you need to create a channel to let the data pass through: this platform channel is called <strong><em>MethodChannel</em></strong>. According to the official <a href="https://docs.flutter.dev/development/platform-integration/platform-channels">Flutter docs</a>:</p><blockquote>The standard platform channels use a standard message codec that supports efficient binary serialization of simple JSON-like values, such as booleans, numbers, Strings, byte buffers, and Lists and Maps of these (see StandardMessageCodec for details). The serialization and deserialization of these values to and from messages happens automatically when you send and receive values.</blockquote><p>The process of writing the interfaces on both Android and iOS hosts plus invoking the channels in the right way is typically error-prone, that’s why the Flutter community has introduced <a href="https://pub.dev/packages/pigeon"><strong>Pigeon</strong></a>, a code generator tool to make communication between Flutter and the host platform type-safe, easier and faster.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2Got9qlN-LToYtWS" /><figcaption>Photo by <a href="https://unsplash.com/@thewonderalice?utm_source=medium&amp;utm_medium=referral">Alice</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>For the sole purpose of demonstrating how to invoke native code using Pigeon, I created a <a href="https://github.com/AngeloAvv/news_app">Flutter App</a> that shows a list of the latest news by querying the <a href="https://newsapi.org">NewsAPI</a> APIs. Let’s deep dive into the process…</p><h3>Setup the project</h3><p>The first step requires you to add Pigeon as a dev dependency in your flutter project. In order to do this simply type in your terminal the following command:</p><pre>flutter pub add --dev</pre><h3>Defining the interface in Flutter</h3><p>Once we’ve added the pigeon dependency, we need to create the interface that we will use to invoke the native code. Since this is a service in a clean code architecture approach, I created a file named <strong><em>pigeon.dart</em></strong> under the <strong><em>lib/services</em></strong> folder.</p><p>Inside this file we need to create an abstract class annotated with the <strong><em>@HostApi </em></strong>annotation referenced in the <strong><em>package:pigeon/pigeon.dart</em></strong> file. It’s important that you also define your <a href="https://en.wikipedia.org/wiki/Data_transfer_object">DTOs</a> inside this file too.</p><p>Here’s my <strong><em>pigeon.dart</em></strong> file, as you can see we have the <strong><em>ArticleApi</em></strong> host API with a single method that returns a list of articles from the native and the <strong><em>Article</em></strong> DTO that carries all the information of single news.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/684/1*8W2qhMobkH3D8e-_CmEQcg.png" /></figure><h3>Generating the code</h3><p>The next step consists of letting Pigeon do its job by generating the code starting from our <strong><em>pigeon.dart</em></strong> file. Open your terminal and type</p><pre>flutter pub run pigeon \<br>--input lib/services/pigeon.dart \<br>--dart_out lib/services/pigeon.g.dart \<br>--objc_header_out ios/Runner/pigeon.h \<br>--objc_source_out ios/Runner/pigeon.m \<br>--java_out ./android/app/src/main/java/it/angelocassano/news_app/Pigeon.java \<br>--java_package &quot;it.angelocassano.news_app&quot;</pre><p>with this command, we are telling pigeon to generate some dart code in the <strong><em>pigeon.g.dart</em></strong> file next to the <strong><em>pigeon.dart</em></strong> file, and to generate the platform-specific code for both Android and iOS. Just make sure the folders where the files will be generated exist.</p><h3>Final steps on Flutter</h3><p>The NewsRepository class will take care of invoking the <strong><em>articles()</em></strong> method from the generated <strong><em>pigeon.g.dart</em></strong> file as follows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jXYNJNBxgPCLXBHZ3cN7Zw.png" /></figure><h3>Switching to Android</h3><p>To query the NewsAPI APIs, I’ve plugged <a href="https://square.github.io/retrofit/">Retrofit</a> as an Android dependency in my <strong><em>build.gradle</em></strong> file. I also created a new DTO class called <strong><em>EverythingResponse</em></strong> and a service called <strong><em>PigeonArticleApi.</em></strong> This last one will take care of gathering data from the APIs using Retrofit and send back the response to Flutter with the Pigeon autogenerated wrapper.</p><figure><img alt="EverythingResponse" src="https://cdn-images-1.medium.com/max/1024/1*IB0AUpNHM7CkX98Iq1vTaA.png" /><figcaption><strong><em>EverythingResponse</em></strong></figcaption></figure><figure><img alt="PigeonArticleApi" src="https://cdn-images-1.medium.com/max/1024/1*cklI46YSiesLTx-emVeLcA.png" /><figcaption><strong><em>PigeonArticleApi</em></strong></figcaption></figure><p>To let the <strong><em>PigeonArticleApi</em></strong> class be invokable by Flutter, we need to register it after spawning the Flutter Engine. Inside our <strong><em>MainActivity</em></strong> class let’s override the <strong><em>configureFlutterEngine</em></strong> method and invoke the <strong><em>Pigeon.ArticleApi.setup</em></strong> method passing the Flutter Engine’s binary messenger and a new <strong><em>PigeonArticleApi</em></strong> instance as follows.</p><figure><img alt="MainActivity" src="https://cdn-images-1.medium.com/max/1024/1*GDhDckp97EuJbPTP3YzOzw.png" /><figcaption><strong><em>MainActivity</em></strong></figcaption></figure><p>In my case, I also had to create a Retrofit instance required as a dependency by the <strong><em>PigeonArticleApi</em></strong> class to query the NewsAPI APIs.</p><h3>Switching to iOS</h3><p>Before writing our code, remember to add both <strong><em>pigeon.h</em></strong> and <strong><em>pigeon.m</em></strong> inside our XCode project under the <strong><em>Runner</em></strong> group.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fLv3-C09zvtu3Kf-A3TrPA.png" /></figure><p>Since the generated code is not swift code, we also need to import the <strong><em>pigeon.h</em></strong> header file inside the <strong><em>Runner-Bridging-Header.h </em></strong>file in order to let the autogenerated objective-C code be visible by the swift classes that we will create in the next steps.</p><figure><img alt="Runner-Bridging-Header.h" src="https://cdn-images-1.medium.com/max/700/1*DPmCV1WgEcXNhL4Fq7ajFg.png" /><figcaption><strong><em>Runner-Bridging-Header.h</em></strong></figcaption></figure><p>To query the NewsAPI APIs, I’ve plugged <a href="https://github.com/Alamofire/Alamofire">Alamofire</a> as an iOS dependency in my <strong><em>Podfile</em></strong> file. I also created a new DTO class called <strong><em>EverythingResponseDTO</em></strong> and a service called <strong><em>PigeonArticleApi.</em></strong> This last one will take care of gathering data from the APIs using Alamofire and send back the response to Flutter with the Pigeon autogenerated wrapper.</p><figure><img alt="EverythingResponseDTO" src="https://cdn-images-1.medium.com/max/1024/1*YDZtB2ZQho1i7JLT7nY4qQ.png" /><figcaption><strong><em>EverythingResponseDTO</em></strong></figcaption></figure><figure><img alt="PigeonArticleApi" src="https://cdn-images-1.medium.com/max/1024/1*398XQsd8cIkLcOdu6VIYQg.png" /><figcaption><strong><em>PigeonArticleApi</em></strong></figcaption></figure><p>To let the <strong><em>PigeonArticleApi</em></strong> class be invokable by Flutter, we need to register it after spawning the Flutter Engine. Inside our <strong><em>AppDelegate</em></strong> class let’s invoke the <strong><em>ArticleApiSetup</em></strong> method passing the Flutter Engine’s binary messenger and a new <strong><em>PigeonArticleApi</em></strong> instance under the <strong><em>didFinishLaunchingWithOptions</em></strong> method as follows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6Aoao0LX0UvCUuS24LH5xg.png" /></figure><h3>Et voila!</h3><p>Once we’ve done all of this stuff, we are ready to launch our application on both Android and iOS devices to see if everything works.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vKLw45VQcKo5NWh_GO5vmQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-HcTN0xC5fULrbvl-a3pMg.png" /></figure><h3>Thank you for reading 👋</h3><p>I hope you enjoyed this article. If you have any queries or suggestions please let me know in the comments down below.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1ff9d741dd4d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to distribute Flutter Desktop app binaries using GitHub Actions]]></title>
            <link>https://angeloavv.medium.com/how-to-distribute-flutter-desktop-app-binaries-using-github-actions-f8d0f9be4d6b?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/f8d0f9be4d6b</guid>
            <category><![CDATA[desktop-app]]></category>
            <category><![CDATA[flutter-desktop]]></category>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[flutter]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Sat, 19 Feb 2022 16:27:39 GMT</pubDate>
            <atom:updated>2022-02-19T16:44:47.241Z</atom:updated>
            <content:encoded><![CDATA[<p>A few weeks ago Flutter team officially announced <a href="https://medium.com/flutter/whats-new-in-flutter-2-10-5aafb0314b12">Flutter 2.10</a>, moving Flutter Desktop for Windows from beta to stable.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nZ1FBJiee44LjM_h79J0Nw.png" /></figure><p>For some time I had an idea in my mind: I wanted to create a desktop application able to run on the three most known and used operative systems to simplify the process of gathering a macOS recovery image.</p><p>Since I’m fluent with Dart and Flutter, I decided to stick with it and leave Electron and Javascript on my back, moreover Flutter Desktop for Windows available on the stable channel gave me a further push to follow this path.</p><h3>The App: MacRecoveryX</h3><p>It took no more than a few days to develop my new app. MacRecoveryX, that’s the name I gave to it it’s a very simple tool. It allows you to choose the version of macOS you wish to download, define where to place the two recovery files (the dmg image system and its chunklist), and start the process to download these files.</p><figure><img alt="MacRecoveryX — welcome page" src="https://cdn-images-1.medium.com/max/1024/1*xE3ts1Cei5nP-Tq7l9iSLQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HUxvWg_01xqc4qrvB55PMQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x48wyYgcpA5HQDsGlb6xYw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sDA4IElM72yxWmVOhTJrlQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TcYz245oFzf0qSnD6YlKnA.png" /></figure><p>Once I finished developing my app, I needed to find a way to deploy it to the world. According to the official <a href="https://docs.flutter.dev/desktop#distribution">Flutter documentation</a>, you can distribute your Flutter apps bundling an MSIX file for Windows, Snap for Linux, and Apple Store for macOS. I thought it was too overkill for a simple tool.</p><h3>The source code</h3><p>As a fervent supporter of the open-source world, I decided to version the MacRecoveryX source code on <a href="https://github.com/AngeloAvv/MacRecoveryX">GitHub</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j3cpS64KiCB96VOnYQhs8Q.png" /></figure><p>That wasn’t enough. MacRecoveryX needs a Flutter SDK to allow users to generate artifacts and run them: I needed a way to generate artifacts after a push (a tag would be better) for each OS target, compress them into a zip file and then release them in the repository. GitHub Actions was the key to success.</p><h3>GitHub Actions pipeline</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Nbann-pKcomstnls1K0FtA.png" /></figure><p>To trigger a GitHub actions pipeline we need to create a <em>.yml</em> file under the <em>.github/workflows</em> folder at the root of our project.</p><p>Since we need to deploy a Flutter app, we will name it <em>Flutter CI. </em>We also need to build our app for the three main targets: <strong>Windows</strong>, <strong>Linux</strong>, and <strong>macOS. </strong>We will split the build flow into three different jobs. The scripts will run respectively on <strong>Windows</strong>, <strong>Ubuntu, </strong>and <strong>macOS</strong>.</p><pre>name: Flutter CI<br><br>on: push<br><br>jobs:<br>  build-and-release-linux:<br>    runs-on: ubuntu-latest<br><br>  build-and-release-windows:<br>    runs-on: windows-latest<br><br>  build-and-release-macos:<br>    runs-on: macos-latest</pre><h4>The build process</h4><p>Now let’s focus on the build process, we need to:</p><ul><li>Check out our source code from GitHub — with <em>actions/checkout@v2</em></li><li>Choose the right version of Flutter to build our project — <em>with subosito/flutter-action@v1</em></li><li>Install a bunch of dependencies to compile our artifacts (some of them are mentioned <a href="https://docs.flutter.dev/desktop#requirements">here</a>)</li><li>Retrieve the flutter project dependencies using <em>flutter pub get</em></li><li>Generate some intermediate files using build_runner</li><li>Enable the target desktop configuration</li><li>Build the artifact</li><li>Compress the artifact and its files in a zip file — with <em>thedoctor0/zip-release@master</em></li><li>Deploy the zip file as a new release — with <em>softprops/action-gh-release@v1</em></li></ul><pre>    steps:<br>      - uses: actions/checkout@v2<br>      - uses: subosito/flutter-action@v1<br>        with:<br>          channel: &#39;stable&#39;<br>          flutter-version: &#39;2.10.0&#39;<br>      - name: Install project dependencies<br>        run: flutter pub get<br>      - name: Generate intermediates<br>        run: flutter pub run build_runner build --delete-conflicting-outputs<br>      - name: Enable windows build<br>        run: flutter config --enable-windows-desktop<br>      - name: Build artifacts<br>        run: flutter build windows --release<br>      - name: Archive Release<br>        uses: thedoctor0/zip-release@master<br>        with:<br>          type: &#39;zip&#39;<br>          filename: MacRecoveryX-${{github.ref_name}}-windows.zip<br>          directory: build/windows/runner/Release<br>      - name: Windows Release<br>        uses: softprops/action-gh-release@v1<br>        if: startsWith(github.ref, &#39;refs/tags/&#39;)<br>        env:<br>          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}<br>        with:<br>          files: build/windows/runner/Release/MacRecoveryX-${{github.ref_name}}-windows.zip</pre><h4>Enabling Desktop support</h4><p>To enable Windows Desktop support we will execute:</p><pre>flutter config --enable-windows-desktop</pre><p>To enable Linux Desktop support we will execute:</p><pre>flutter config --enable-linux-desktop</pre><p>To enable macOS Desktop support we will execute:</p><pre>flutter config --enable-macos-desktop</pre><h4>Building the app</h4><p>Once enabled desktop support, we need to build our app with the following instructions.</p><p>To build a Windows Desktop app we will execute:</p><pre>flutter build windows --release</pre><p>To build a Linux Desktop app we will execute:</p><pre>flutter build linux --release</pre><p>To build a macOS Desktop app we will execute:</p><pre>flutter build macos --release</pre><h4>Zipping the artifacts</h4><p>Every build will create an executable and a bunch of support files like libraries and assets. Every target will create the artifacts in a different path.</p><p>To create a zip artifact for Windows we will run the following step:</p><pre>- name: Archive Release<br>  uses: thedoctor0/zip-release@master<br>  with:<br>    type: &#39;zip&#39;<br>    filename: MacRecoveryX-${{github.ref_name}}-windows.zip<br>    directory: build/windows/runner/Release</pre><p>To create a zip artifact for Linux we will run the following step:</p><pre>- name: Archive Release<br>  uses: thedoctor0/zip-release@master<br>  with:<br>    type: &#39;zip&#39;<br>    filename: MacRecoveryX-${{github.ref_name}}-linux.zip<br>    directory: build/linux/x64/release/bundle</pre><p>To create a zip artifact for macOS we will run the following step:</p><pre>- name: Archive Release<br>  uses: thedoctor0/zip-release@master<br>  with:<br>    type: &#39;zip&#39;<br>    filename: MacRecoveryX-${{github.ref_name}}-macos.zip<br>    directory: build/macos/Build/Products/Release</pre><h4>Releasing the assets</h4><p>Once done, we need to upload these assets as a set of release artifacts.</p><p>To release the Windows zip file we will run the following step:</p><pre>- name: Windows Release<br>  uses: softprops/action-gh-release@v1<br>  if: startsWith(github.ref, &#39;refs/tags/&#39;)<br>  env:<br>    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}<br>  with:<br>    files: build/windows/runner/Release/MacRecoveryX-${{github.ref_name}}-windows.zip</pre><p>To release the Linux zip file we will run the following step:</p><pre>- name: Linux Release<br>  uses: softprops/action-gh-release@v1<br>  if: startsWith(github.ref, &#39;refs/tags/&#39;)<br>  env:<br>    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}<br>  with:<br>    files: build/linux/x64/release/bundle/MacRecoveryX-${{github.ref_name}}-linux.zip</pre><p>To release the macOS zip file we will run the following step:</p><pre>- name: macOS Release<br>  uses: softprops/action-gh-release@v1<br>  if: startsWith(github.ref, &#39;refs/tags/&#39;)<br>  env:<br>    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}<br>  with:<br>    files: build/macos/Build/Products/Release/MacRecoveryX-${{github.ref_name}}-macos.zip</pre><h4>The completed script</h4><p>In the end, the script will look like this:</p><pre>name: Flutter CI<br><br>on: push<br><br>jobs:<br>  build-and-release-linux:<br>    runs-on: ubuntu-latest<br><br>    steps:<br>      - uses: actions/checkout@v2<br>      - uses: subosito/flutter-action@v1<br>        with:<br>          channel: &#39;stable&#39;<br>          flutter-version: &#39;2.10.0&#39;<br>      - name: Install dependencies<br>        run: sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev<br>      - name: Install project dependencies<br>        run: flutter pub get<br>      - name: Generate intermediates<br>        run: flutter pub run build_runner build --delete-conflicting-outputs<br>      - name: Enable linux build<br>        run: flutter config --enable-linux-desktop<br>      - name: Build artifacts<br>        run: flutter build linux --release<br>      - name: Archive Release<br>        uses: thedoctor0/zip-release@master<br>        with:<br>          type: &#39;zip&#39;<br>          filename: MacRecoveryX-${{github.ref_name}}-linux.zip<br>          directory: build/linux/x64/release/bundle<br>      - name: Linux Release<br>        uses: softprops/action-gh-release@v1<br>        if: startsWith(github.ref, &#39;refs/tags/&#39;)<br>        env:<br>          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}<br>        with:<br>          files: build/linux/x64/release/bundle/MacRecoveryX-${{github.ref_name}}-linux.zip<br><br>  build-and-release-windows:<br>    runs-on: windows-latest<br><br>    steps:<br>      - uses: actions/checkout@v2<br>      - uses: subosito/flutter-action@v1<br>        with:<br>          channel: &#39;stable&#39;<br>          flutter-version: &#39;2.10.0&#39;<br>      - name: Install project dependencies<br>        run: flutter pub get<br>      - name: Generate intermediates<br>        run: flutter pub run build_runner build --delete-conflicting-outputs<br>      - name: Enable windows build<br>        run: flutter config --enable-windows-desktop<br>      - name: Build artifacts<br>        run: flutter build windows --release<br>      - name: Archive Release<br>        uses: thedoctor0/zip-release@master<br>        with:<br>          type: &#39;zip&#39;<br>          filename: MacRecoveryX-${{github.ref_name}}-windows.zip<br>          directory: build/windows/runner/Release<br>      - name: Windows Release<br>        uses: softprops/action-gh-release@v1<br>        if: startsWith(github.ref, &#39;refs/tags/&#39;)<br>        env:<br>          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}<br>        with:<br>          files: build/windows/runner/Release/MacRecoveryX-${{github.ref_name}}-windows.zip<br><br>  build-and-release-macos:<br>    runs-on: macos-latest<br><br>    steps:<br>      - uses: actions/checkout@v2<br>      - uses: subosito/flutter-action@v1<br>        with:<br>          channel: &#39;stable&#39;<br>          flutter-version: &#39;2.10.0&#39;<br>      - name: Install project dependencies<br>        run: flutter pub get<br>      - name: Generate intermediates<br>        run: flutter pub run build_runner build --delete-conflicting-outputs<br>      - name: Enable macOS build<br>        run: flutter config --enable-macos-desktop<br>      - name: Build artifacts<br>        run: flutter build macos --release<br>      - name: Archive Release<br>        uses: thedoctor0/zip-release@master<br>        with:<br>          type: &#39;zip&#39;<br>          filename: MacRecoveryX-${{github.ref_name}}-macos.zip<br>          directory: build/macos/Build/Products/Release<br>      - name: macOS Release<br>        uses: softprops/action-gh-release@v1<br>        if: startsWith(github.ref, &#39;refs/tags/&#39;)<br>        env:<br>          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}<br>        with:<br>          files: build/macos/Build/Products/Release/MacRecoveryX-${{github.ref_name}}-macos.zip</pre><h3>The final result</h3><p>Every time we will push into our repository, a new workflow will be triggered. Furthermore, if we push a tag, the pipeline will also proceed to create a release and upload a custom set of artifacts according to the OS target.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TZO_Se4QNzmyE99wZhmlSw.png" /></figure><p>The release section will look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C5ieIwLc9Hl5k6qKnPa5Lw.png" /></figure><h3>Conclusions</h3><p>It’s often unnecessary to build and deploy your Desktop application using the Windows Store, Snap, or the Apple Store. You can simply prepare a GitHub Actions pipeline to create a set of artifacts, zip the files, and release the zip files in the repository.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f8d0f9be4d6b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I will never do take-home exercises anymore]]></title>
            <link>https://angeloavv.medium.com/i-will-never-do-take-home-exercises-anymore-1a049e1135d?source=rss-a68b19b8c054------2</link>
            <guid isPermaLink="false">https://medium.com/p/1a049e1135d</guid>
            <category><![CDATA[recruiting]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[jobs]]></category>
            <category><![CDATA[take-home-exercises]]></category>
            <category><![CDATA[it]]></category>
            <dc:creator><![CDATA[Angelo Cassano]]></dc:creator>
            <pubDate>Tue, 16 Mar 2021 09:08:36 GMT</pubDate>
            <atom:updated>2021-03-16T09:08:36.474Z</atom:updated>
            <content:encoded><![CDATA[<p>Take-home exercises are becoming a standard hard-skill testing recruiting process, especially for particular roles that rely on remote positions. But do they really work?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*iqCgQuhvp_NxkZsrKi2z0A.png" /></figure><p>Taking part in a recruiting process is stimulating. Am I a masochist? Probably. From my perspective, I think everyone should take part in a recruiting process at least once or twice per year.</p><p>Why? There are three main reasons behind that:</p><ul><li>You get to know how other companies work</li><li>It enhances your relational skills</li><li>Keeps your problem-solving capabilities trained</li></ul><p>In the last year, I applied for different roles and of course, I took part in different recruitment processes. Some of them asked me to complete a “small” take-home exercise. Let’s see how it went.</p><h4>Senior Flutter developer selection process experience</h4><p>I found this company on LinkedIn that was looking for a well-knowledged Flutter developer to strengthen their mobile team. I thought I was qualified for this role so I decided to apply.</p><p>I sent my updated CV and I wrote a cover letter to explain why I was suited for the job, then they contacted me a few hours later: it’s a match! They scheduled me for interview where they asked me about my career path and what were my expectations. At the end of the interview, they asked me to complete a “small” take-home exercise during the weekend.</p><p>At this point you might be wondering why I keep embracing the term <strong>small </strong>with double quotes, so let me explain. They asked me to develop a “small” application composed of two interfaces, that talk to an open-source REST API and present the data inside of it.</p><p>Let me get this straight to the point. The hardest part of this exercise is code and UX refinements. You can set up a working product in less than an hour, but it will suck aesthetically and engineeringly.</p><p>It took me two business days to complete the task: the exercise also required good UX, strong architecture choices, animations, unit, and integration tests.</p><h4>Senior software engineer selection process experience</h4><p>At the beginning of 2021, I decided to bring the challenge to another level. I’m a native Italian speaker, so I decided to take part in an English recruitment process.</p><p>In short, this company was looking for a senior software engineer for a particular programming language that I didn’t know, no matter what your knowledge was (in terms of programming languages of course).</p><p>I was very skeptical about that, but the recruiter who contacted me on LinkedIn reassured me, so I decided to get into it. We scheduled an interview and after some questions about my career path, he introduced me to the hard skill selection process.</p><p>Well, that exercise was not quite “small”. They told me they weren’t expecting me to deliver the task in a few days, so I thought that I would have made it in a few weeks. Long story short: I should have studied this new programming language in less than a week, and then complete the exercise, average working days? 5 to 15.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j6k6hX-OOXpr7ahjqcoCqA.jpeg" /></figure><h4>Senior Backend engineer selection process experience</h4><p>In the last few weeks, I get contacted through StackOverflow for a senior backend engineer position. They asked me if I was interested in their company, so we arranged three different interviews.</p><p>Between two interviews, they asked me to develop a “small” authentication system that manages roles and some other different behaviors. They also asked me to mind about database optimizations and algorithm performance, and to provide with a bunch of papers (documentation, flow charts, entity-relationships diagrams, and so on).</p><p>Deadline? 5 days. Did I complete it? Partially. In the next interview, the examiner told me it would have required at least a month to complete it.</p><h3>Do take-home exercises really work?</h3><p>They do. Indisputably.</p><p>I think a take-home exercise should take no more than two or three hours. You need to understand how the developer solves a problem and how he broadly thinks, you don’t need to take him down emotionally or put it under pressure. I need to warn you about a thing, we have a life too.</p><p>And guess what, I even attended paid take-home exercises: yes, some companies really take seriously their recruiting process.</p><p>In this article, I wanted to talk to you about how to not abuse them, so if you are a recruiter, please don’t do it. You will let your candidates run far away from your company.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1a049e1135d" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>