Use Async-await to Consume A Pullable Callbag Source

/**
 * callbag-to-awaitable
 * --------------------
 *
 * Use the `async`/`await` syntax with pullable Callbags! There are actually no
 * Promises in this package, just async-await syntax on objects with a `then`
 * method.
 *
 * `npm install callbag-to-awaitable`
 *
 * Example: on an async pullable callbag source, apply some operators, then
 * convert to an awaitable and consume it in an `async`/`await` function:
 *
 *     const {pipe, take, map} = require('callbag-basics');
 *     const toAwaitable = require('callbag-to-awaitable');
 *
 *     function pullableAsyncSource(start, sink) {
 *       if (start !== 0) return;
 *       let i = 0;
 *       sink(0, t => {
 *         if (t === 1) {
 *           setTimeout(() => { sink(1, i++) }, 1000);
 *         }
 *       });
 *     }
 *
 *     const source = pipe(
 *       pullableAsyncSource, // 0, 1, 2, 3, 4, 5, 6, 7...
 *       take(5), // 0, 1, 2, 3, 4
 *       map(x => x / 4) // 0, 0.25, 0.5, 0.75, 1
 *     );
 *
 *     async function main() {
 *       const next = toAwaitable(source);
 *       try {
 *         while (true) {
 *           const x = await next;
 *           console.log(x);
 *         }
 *       } catch (end) {
 *         console.log('done');
 *       }
 *     }
 *
 *     main()
 *     // 0
 *     // 0.25
 *     // 0.5
 *     // 0.75
 *     // 1
 *     // done
 *
 * For more inquiries, read or fork the source below:
 */

function toAwaitable(source) {
  let talkback;
  let resolve;
  let reject;
  let reason = false;
  source(0, (t, d) => {
    if (t === 0) talkback = d;
    if (t === 1 && resolve) resolve(d);
    if (t === 2) {
      reason = d || true;
      if (reject) reject(reason);
    }
    resolve = void 0;
    reject = void 0;
  });
  return {
    then: (_resolve, _reject) => {
      if (reason) _reject(reason);
      resolve = _resolve;
      reject = _reject;
      if (talkback) talkback(1);
    },
  };
}

module.exports = toAwaitable;

Author: staltz
Source Code: https://github.com/staltz/callbag-to-awaitable 
License: MIT license

#javascript #async #await 

Use Async-await to Consume A Pullable Callbag Source
Joseph  Murray

Joseph Murray

1657244520

Vim Svelte | Vim Syntax Highlighting & indentation for Svelte 3

vim-svelte

Vim syntax highlighting and indentation for Svelte 3 components.

This is mostly just HTML syntax highlighting with some keywords added and all expressions inside of { and } highlighted as JavaScript.

Highlighting includes:

  • HTML attributes with a colon like on:click or transition:fade highlighted as Keyword.
  • #if, /if, :else, and :else if highlighted as Conditional.
  • #await, /await, :catch, :then, and @html highlighted as Keyword.
  • #each and /each highlighted as Repeat.

Dependencies

  1. pangloss/vim-javascript for JavaScript syntax highlighting.
  2. othree/html5.vim for HTML indent.

Both of those dependencies are included in sheerun/vim-polyglot so if you're already using that then you should be set.

Installation

The simplest way to install vim-svelte is via a package manager like Pathogen, Vundle, NeoBundle, Plug, or minpac.

For example, using minpac:

call minpac#add('othree/html5.vim')
call minpac#add('pangloss/vim-javascript')
call minpac#add('evanleck/vim-svelte')

Or using Plug:

Plug 'othree/html5.vim'
Plug 'pangloss/vim-javascript'
Plug 'evanleck/vim-svelte', {'branch': 'main'}

vim-svelte works just fine with Vim 8's native package loading as well.

Options

To disable indentation within <script> and <style> tags, set one of these variables in your vimrc:

let g:svelte_indent_script = 0
let g:svelte_indent_style = 0

Preprocessed languages

Syntax highlighting for additional languages is supported, assuming you have a corresponding syntax definition installed. For example, newer versions of Vim ship with a TypeScript syntax definition, so you wouldn't need anything additional installed for that to work. Supported languages include:

  • less
  • scss
  • sass
  • stylus
  • typescript

Since Svelte doesn't support these out of the box (see svelte-preprocess for how to set up some common language preprocessors with e.g. Rollup), they're all disabled by default so the first thing you'll need to do is enable your languages via the g:svelte_preprocessors variable:

let g:svelte_preprocessors = ['typescript']

Then, use your language in your Svelte components like this:

<script lang='typescript'>
</script>

<!-- Or... -->
<style type='text/scss'>
</style>

Customizing the list of preprocessed languages

In addition to enabling the built-in preprocessors, you can add your own preprocessors that this plugin will detect using the g:svelte_preprocessor_tags variable. It should be a list of dictionaries with at least a name and a tag attribute. You can optionally include an as attribute which maps to the syntax you'd like to use within the tag.

Here's an example:

let g:svelte_preprocessor_tags = [
  \ { 'name': 'postcss', 'tag': 'style', 'as': 'scss' }
  \ ]
" You still need to enable these preprocessors as well.
let g:svelte_preprocessors = ['postcss']

This would highlight <style type="postcss"> contents as scss, useful if you use something like postcss-nested.

You can also create shorthand names if, for example, writing out lang='typescript' takes too long:

let g:svelte_preprocessor_tags = [
  \ { 'name': 'ts', 'tag': 'script', 'as': 'typescript' }
  \ ]
let g:svelte_preprocessors = ['ts']
FieldUsageRequiredDefault value
nameThe value within the attribute lang or type on the tag as well as the value to include in g:svelte_preprocessors.YesNone
tagThe HTML tag to target e.g. script or style.YesNone
asThe syntax name to use for highlighting.NoThe name attribute.

Note, that enabling and loading a lot of different syntax definitions can considerably degrade Vim's performance. Consider yourself warned.

Integrations

  • ALE: vim-svelte should work out of the box with eslint and a few other linters/fixers. PRs welcome if the one you want is missing.
  • matchit.vim: vim-svelte should work out of the box and allow moving between HTML tags as well as flow control like #if/:else//if.
  • Syntastic: vim-syntastic will work with javascript and html checkers, for example:
let g:syntastic_svelte_checkers = ['javascript/eslint', 'html/htmlhint']

Tests

Indentation tests are provided and any contributions would be much appreciated. They can be run with make test which will clone vader.vim into the current working directory and run the test suite.

Alternatives

  1. burner/vim-svelte
  2. leafOfTree/vim-svelte-plugin

Download Details:
Author: evanleck
Source Code: https://github.com/evanleck/vim-svelte
License:

#svelte #javascript

Vim Svelte | Vim Syntax Highlighting & indentation for Svelte 3
Thierry  Perret

Thierry Perret

1656378000

Qu'est-ce qu'Async, Await et Task en C#

Les performances sont primordiales lorsque vous essayez de publier sur le Web, sur mobile, sur des consoles et même sur certains PC bas de gamme. Un jeu ou une application fonctionnant à moins de 30 FPS peut être source de frustration pour les utilisateurs. Jetons un coup d'œil à certaines des choses que nous pouvons utiliser pour augmenter les performances en réduisant la charge sur le processeur.

Dans cet article, nous expliquerons ce que sont async, awaitet Tasken C # et comment les utiliser dans Unity pour gagner en performance dans votre projet. Ensuite, nous examinerons certains des packages intégrés à Unity : les coroutines, le système de tâches C# et le compilateur en rafale. Nous verrons ce qu'ils sont, comment les utiliser et comment ils augmentent les performances de votre projet.

Pour démarrer ce projet, j'utiliserai Unity 2021.3.4f1. Je n'ai testé ce code sur aucune autre version de Unity ; tous les concepts ici devraient fonctionner sur n'importe quelle version d'Unity après Unity 2019.3. Vos résultats de performances peuvent différer si vous utilisez une version plus ancienne, car Unity a apporté des améliorations significatives avec le modèle de programmation async/wait en 2021. En savoir plus à ce sujet dans le blog Unity d'Unity et .NET, quelle est la prochaine étape , en particulier la section intitulée "Modernizing the Exécution de l'unité.

J'ai créé un nouveau projet Core 2D (URP), mais vous pouvez l'utiliser dans n'importe quel type de projet que vous aimez.

J'ai un sprite que j'ai obtenu de Space Shooter (Redux, plus polices et sons) de Kenney Vleugels .

J'ai créé un préfabriqué ennemi qui contient un Sprite Render et un composant ennemi. Le composant ennemi est un MonoBehaviourqui a un Transformet un floatpour garder une trace de la position et de la vitesse de déplacement sur l'axe y :

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

Qu'est -ce que async, await, et Tasksont en C#

Qu'est-ce que c'est async?

En C#, les méthodes peuvent avoir un mot- asyncclé devant elles, ce qui signifie que les méthodes sont des méthodes asynchrones. C'est juste une façon de dire au compilateur que nous voulons pouvoir exécuter du code à l'intérieur et permettre à l'appelant de cette méthode de continuer l'exécution en attendant que cette méthode se termine.

Un exemple de ceci serait la préparation d'un repas. Vous commencerez à cuire la viande, et pendant que la viande cuit et que vous attendez qu'elle se termine, vous commencerez à faire les côtés. Pendant que les aliments cuisent, vous commencez à mettre la table. Un exemple de ceci dans le code serait static async Task<Steak> MakeSteak(int number).

Unity possède également toutes sortes de méthodes intégrées que vous pouvez appeler de manière asynchrone ; voir les docs Unity pour une liste des méthodes. Avec la façon dont Unity gère la gestion de la mémoire, il utilise soit des coroutines , AsyncOperationsoit le système de tâches C# .

Qu'est-ce que c'est awaitet comment l'utiliser ?

En C#, vous pouvez attendre la fin d'une opération asynchrone en utilisant le mot- awaitclé. Ceci est utilisé à l'intérieur de toute méthode qui a le mot- asyncclé pour attendre qu'une opération continue :

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Consultez les documents Microsoft pour en savoir plus sur await.

Qu'est-ce qu'un Tasket comment l'utiliser ?

A Taskest une méthode asynchrone qui effectue une seule opération et ne renvoie pas de valeur. Pour a Taskqui renvoie une valeur, nous utiliserions Task<TResult>.

Pour utiliser une tâche, nous la créons comme créer n'importe quel nouvel objet en C# : Task t1 = new Task(void Action). Ensuite, nous commençons la tâche t1.wait. Enfin, nous attendons que la tâche se termine avec t1.wait.

Il existe plusieurs façons de créer, démarrer et exécuter des tâches. Task t2 = Task.Run(void Action)va créer et démarrer une tâche. await Task.Run(void Action)créera, démarrera et attendra la fin de la tâche. Nous pouvons utiliser la méthode alternative la plus courante avec Task t3 = Task.Factory.Start(void Action).

Il existe plusieurs façons d'attendre que la tâche soit terminée. int index = Task.WaitAny(Task[])attendra la fin de toute tâche et nous donnera l'index de la tâche terminée dans le tableau. await Task.WaitAll(Task[])attendra que toutes les tâches soient terminées.

Pour plus d'informations sur les tâches, consultez les documents Microsoft .

Un exemple simpletask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Comment la tâche affecte les performances

Comparons maintenant les performances d'une tâche par rapport aux performances d'une méthode.

J'aurai besoin d'une classe statique que je peux utiliser dans toutes mes vérifications de performances. Il aura une méthode et une tâche qui simulent une opération gourmande en performances. La méthode et la tâche effectuent exactement la même opération :

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Maintenant, j'ai besoin d'un MonoBehaviourque je peux utiliser pour tester l'impact des performances sur la tâche et la méthode. Juste pour que je puisse voir un meilleur impact sur les performances, je vais prétendre que je veux exécuter cela sur dix objets de jeu différents. Je garderai également une trace du temps Updatenécessaire à l'exécution de la méthode.

Dans Update, j'obtiens l'heure de début. Si je teste la méthode, je parcours tous les objets de jeu simulés et j'appelle la méthode intensive en performances. Si je teste la tâche, je crée une nouvelle Taskboucle de tableau à travers tous les objets de jeu simulés et j'ajoute la tâche gourmande en performances au tableau de tâches. Je puis awaitpour toutes les tâches à accomplir. En dehors de la vérification du type de méthode, je mets à jour l'heure de la méthode, en la convertissant en ms. Je le connecte également.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

La méthode intensive prend environ 65 ms pour se terminer avec le jeu fonctionnant à environ 12 FPS.

Méthode intensiveMéthode intensiveMéthode intensive

La tâche intensive prend environ 4 ms et le jeu tourne à environ 200 FPS.

Tâche intensive

Essayons ceci avec mille ennemis :

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

L'affichage et le déplacement d'un millier d'ennemis avec la méthode ont pris environ 150 ms avec une fréquence d'images d'environ 7 FPS.

Mille ennemis

Afficher et déplacer un millier d'ennemis avec une tâche a pris environ 50 ms avec une fréquence d'images d'environ 30 FPS.

Affichage des ennemis en mouvement

Pourquoi pas useTasks?

Les tâches sont extrêmement performantes et réduisent la pression sur les performances de votre système. Vous pouvez même les utiliser dans plusieurs threads à l'aide de la bibliothèque parallèle de tâches (TPL).

Il y a cependant quelques inconvénients à les utiliser dans Unity. Le principal inconvénient de l'utilisation Taskdans Unity est qu'ils s'exécutent tous sur le Mainthread. Oui, nous pouvons les faire fonctionner sur d'autres threads, mais Unity fait déjà sa propre gestion des threads et de la mémoire, et vous pouvez créer des erreurs en créant plus de threads que de cœurs de processeur, ce qui entraîne une concurrence pour les ressources.

Les tâches peuvent également être difficiles à exécuter correctement et à déboguer. Lors de l'écriture du code d'origine, je me suis retrouvé avec toutes les tâches en cours d'exécution, mais aucun des ennemis ne s'est déplacé à l'écran. Il a fini par être que j'avais besoin de retourner le Task[]que j'ai créé dans le fichier Task.

Les tâches créent beaucoup de déchets qui affectent les performances. Ils n'apparaissent pas non plus dans le profileur, donc si vous en avez un qui affecte les performances, il est difficile de le retrouver. De plus, j'ai remarqué que parfois mes tâches et mes fonctions de mise à jour continuent de s'exécuter à partir d'autres scènes.

Coroutines d'unité

Selon Unity , "Une coroutine est une fonction qui peut suspendre son exécution (rendement) jusqu'à ce que l' instruction YieldInstruction donnée se termine."

Cela signifie que nous pouvons exécuter du code et attendre la fin d'une tâche avant de continuer. Cela ressemble beaucoup à une méthode asynchrone. Il utilise un type de retour IEnumeratoret we yield returnau lieu de await.

Unity a plusieurs types d' instructions de rendement que nous pouvons utiliser, c'est-à-dire , WaitForSeconds, ou .WaitForEndOfFrameWaitUntilWaitWhile

Pour démarrer les coroutines, nous avons besoin de a MonoBehaviouret utilisons le MonoBehaviour.StartCoroutine.

Pour arrêter une coroutine avant qu'elle ne se termine, nous utilisons MonoBehaviour.StopCoroutine. Lorsque vous arrêtez des coroutines, assurez-vous d'utiliser la même méthode que celle que vous avez utilisée pour la démarrer.

Les cas d'utilisation courants des coroutines dans Unity consistent à attendre que les actifs se chargent et à créer des temporisateurs de temps de recharge.

Exemple : Un chargeur de scène utilisant une coroutine

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Vérifier l'impact d'une coroutine sur les performances

Voyons comment l'utilisation d'une coroutine impacte les performances de notre projet. Je ne vais le faire qu'avec la méthode intensive en performances.

J'ai ajouté le Coroutineà l' MethodTypeénumération et aux variables pour garder une trace de son état:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

J'ai créé la coroutine. Ceci est similaire à la tâche et à la méthode gourmandes en performances que nous avons créées précédemment avec du code ajouté pour mettre à jour l'heure de la méthode :

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

Dans la Updateméthode, j'ai ajouté la vérification de la coroutine. J'ai également modifié l'heure de la méthode, mis à jour le code et ajouté du code pour arrêter la coroutine si elle était en cours d'exécution et nous avons changé le type de méthode :

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

La coroutine intensive prend environ 6 ms pour se terminer avec le jeu fonctionnant à environ 90 FPS.

Coroutine intensive

Le système de tâches C# et le compilateur en rafale

Qu'est-ce que le système de tâches C# ?

Le système de tâches C# est l'implémentation par Unity de tâches faciles à écrire, qui ne génèrent pas les déchets générés par les tâches et utilisent les threads de travail que Unity a déjà créés. Cela corrige tous les inconvénients des tâches.

Unity compare les travaux en tant que threads, mais ils disent qu'un travail effectue une tâche spécifique. Les tâches peuvent également dépendre d'autres tâches à terminer avant de s'exécuter ; cela résout le problème avec la tâche que j'avais qui ne s'est pas correctement déplacée Unitsparce qu'elle dépendait d'une autre tâche à terminer en premier.

Les dépendances de travail sont automatiquement prises en charge pour nous par Unity. Le système d'emploi dispose également d'un système de sécurité intégré principalement pour se protéger contre les conditions de course . Une mise en garde avec les travaux est qu'ils ne peuvent contenir que des variables membres qui sont soit des types blittables, soit des types NativeContainer ; c'est un inconvénient du système de sécurité.

Pour utiliser le système de travail, vous créez le travail, planifiez le travail, attendez que le travail se termine, puis utilisez les données renvoyées par le travail. Le système de tâches est nécessaire pour utiliser la pile technologique orientée données (DOTS) de Unity.

Pour plus de détails sur le système de tâches, consultez la documentation Unity .

Création d'un emploi

Pour créer un travail, vous créez un stuctqui implémente l'une des IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobest un travail de base. IJobForet IJobForParallelpermettent d'effectuer la même opération sur chaque élément d'un conteneur natif ou pour plusieurs itérations. La différence entre eux est que le IJobFor s'exécute sur un seul thread où le IJobForParallelsera divisé entre plusieurs threads.

Je vais l'utiliser IJobpour créer un travail d'opération intensive IJobForet IJobForParallelpour créer un travail qui déplacera plusieurs ennemis; c'est juste pour que nous puissions voir les différents impacts sur les performances. Ces tâches seront identiques aux tâches et méthodes que nous avons créées précédemment :

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Ajoutez les variables membres. Dans mon cas, mon IJobn'en a pas besoin. Le IJobForet IJobParallelForont besoin d'un flottant pour le temps delta actuel car les travaux n'ont pas de concept de cadre ; ils opèrent en dehors de Unity MonoBehaviour. Ils ont également besoin d'un tableau de float3pour la position et d'un tableau pour la vitesse de déplacement sur l'axe y :

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

La dernière étape consiste à mettre en œuvre la Executeméthode requise. Le IJobForet IJobForParallelles deux nécessitent un intpour l'index de l'itération actuelle que le travail est en train d'exécuter.

La différence est qu'au lieu d'accéder à l'ennemi transformet de se déplacer, nous utilisons le tableau qui se trouve dans le travail :

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Planification d'un travail

Tout d'abord, nous devons établir le travail et remplir les données des travaux :

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Ensuite, nous planifions le travail avec JobHandle jobHandle = jobData.Schedule();. La Scheduleméthode retourne un JobHandlequi peut être utilisé plus tard.

Nous ne pouvons pas planifier une tâche à partir d'une tâche. Cependant, nous pouvons créer de nouvelles tâches et remplir leurs données à partir d'une tâche. Une fois qu'une tâche a été planifiée, elle ne peut pas être interrompue.

Le travail à haute performance

J'ai créé une méthode qui crée un nouveau travail et le planifie. Il renvoie le descripteur de travail que je peux utiliser dans ma updateméthode :

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

J'ai ajouté le travail à mon énumération. Ensuite, dans la Updateméthode, j'ajoute le caseà la switchsection. J'ai créé un tableau de JobHandles. Je parcours ensuite tous les objets de jeu simulés, en ajoutant une tâche planifiée pour chacun au tableau :

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

Le MoveEnemyetMoveEnemyParallelJob

Ensuite, j'ai ajouté les emplois à mon énumération. Puis dans la Updateméthode, j'appelle une nouvelle MoveEnemyJobméthode, en passant le temps delta. Normalement, vous utiliseriez soit le JobForou le JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

La première chose que je fais est de définir un tableau pour les positions et un tableau pour les moveYque je transmettrai aux tâches. Je remplis ensuite ces tableaux avec les données des ennemis. Ensuite, je crée le travail et définit les données du travail en fonction du travail que je souhaite utiliser. Après cela, je planifie le travail en fonction du travail que je veux utiliser et du type de planification que je veux faire :

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Récupérer les données d'une tâche

Nous devons attendre que le travail soit terminé. Nous pouvons obtenir le statut à partir du JobHandleque nous avons utilisé lorsque nous avons programmé le travail pour le terminer. Cela attendra que le travail soit terminé avant de poursuivre l'exécution : > handle.Complete();ou JobHandle.CompleteAll(jobHandles). Une fois le travail terminé, le NativeContainerque nous avons utilisé pour configurer le travail aura toutes les données que nous devons utiliser. Une fois que nous en avons récupéré les données, nous devons en disposer correctement.

Le travail à haute performance

C'est assez simple puisque je ne suis pas en train de lire ou d'écrire des données dans le travail. J'attends que tous les travaux programmés soient terminés, puis je supprime le Nativetableau :

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

Le travail intensif prend environ 6 ms et le jeu tourne à environ 90 FPS.

Travail intensif

Le MoveEnemytravail

J'ajoute les vérifications complètes appropriées :

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Après les vérifications du type de méthode, je parcoure tous les ennemis, en définissant leurs transformpositions et moveYles données qui ont été définies dans le travail. Ensuite, je dispose correctement des tableaux natifs :

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

L'affichage et le déplacement d'un millier d'ennemis avec le travail ont pris environ 160 ms avec une fréquence d'images d'environ 7 FPS sans gain de performances.

Aucun gain de performances

L'affichage et le déplacement d'un millier d'ennemis avec un travail parallèle ont pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.

Travail parallèle

Qu'est-ce que le compilateur de rafales dans Unity ?

Le compilateur en rafale est un compilateur qui traduit du bytecode en code natif. L'utiliser avec le système de tâches C # améliore la qualité du code généré, vous donnant une augmentation significative des performances ainsi qu'une réduction de la consommation de la batterie sur les appareils mobiles.

Pour l'utiliser, il vous suffit d'indiquer à Unity que vous souhaitez utiliser la compilation en rafale sur le travail avec l' [BurstCompile]attribut :

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Ensuite, dans Unity, sélectionnez Jobs > Burst > Enable Completion

Activer l'achèvement

Burst est juste à temps (JIT) dans l'éditeur, ce qui signifie qu'il peut être désactivé en mode lecture. Lorsque vous construisez votre projet, c'est Ahead-Of-Time (AOT), ce qui signifie que cela doit être activé avant de construire votre projet. Vous pouvez le faire en modifiant la section Burst AOT Settings dans la fenêtre Project Settings .

Paramètres AOT en rafale

Pour plus de détails sur le compilateur de rafale, consultez la documentation Unity .

Un travail gourmand en performances avec le compilateur burst

Un travail intensif avec rafale prend environ 3 ms pour se terminer avec le jeu fonctionnant à environ 150 FPS.

Travail intensif avec rafale

Affichage et déplacement d'un millier d'ennemis, le travail avec rafale a pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.

Rafale 30 ms

Affichage et déplacement d'un millier d'ennemis, le travail parallèle à la rafale a pris environ 6 ms avec une fréquence d'images d'environ 80 à 90 FPS.

6 millisecondes

Conclusion

Nous pouvons utiliser Taskpour augmenter les performances de nos applications Unity, mais leur utilisation présente plusieurs inconvénients. Il est préférable d'utiliser les éléments fournis dans Unity en fonction de ce que nous voulons faire. Utilisez des coroutines si nous voulons attendre que quelque chose finisse de se charger de manière asynchrone ; nous pouvons démarrer la coroutine et ne pas arrêter l'exécution du processus de notre programme.

Nous pouvons utiliser le système de tâches C # avec le compilateur en rafale pour obtenir un gain de performances considérable sans avoir à nous soucier de toutes les tâches de gestion des threads lors de l'exécution de tâches gourmandes en processus. En utilisant les systèmes intégrés, nous sommes sûrs que cela est fait d'une manière sûre qui ne cause pas d'erreurs ou de bugs indésirables.

Les tâches s'exécutaient un peu mieux que les travaux sans utiliser le compilateur en rafale, mais cela est dû au peu de surcharge supplémentaire dans les coulisses pour tout configurer en toute sécurité pour nous. Lors de l'utilisation du compilateur de rafale, nos travaux ont effectué nos tâches. Lorsque vous avez besoin de toutes les performances supplémentaires que vous pouvez obtenir, utilisez le système de tâches C# avec burst.

Les fichiers de projet pour cela peuvent être trouvés sur mon GitHub .

Source : https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

 #csharp #async #await 

Qu'est-ce qu'Async, Await et Task en C#
曾 俊

曾 俊

1656375600

C# 中的 Async、Await 和 Task 是什么

当您尝试发布到 Web、移动设备、控制台甚至一些低端 PC 时,性能就是一切。以低于 30 FPS 的速度运行的游戏或应用程序可能会让用户感到沮丧。让我们看一下可以通过减少 CPU 负载来提高性能的一些方法。

在这篇文章中,我们将介绍 C# 中的 、 和 是什么async以及await如何Task在 Unity 中使用它们来提高项目的性能。接下来,我们将看一下 Unity 的一些内置包:协程、C# 作业系统和突发编译器。我们将了解它们是什么、如何使用它们以及它们如何提高项目的性能。

为了启动这个项目,我将使用 Unity 2021.3.4f1。我没有在任何其他版本的 Unity 上测试过这段代码;这里的所有概念都应该适用于 Unity 2019.3 之后的任何 Unity 版本。如果使用旧版本,您的性能结果可能会有所不同,因为 Unity 在 2021 年确实对 async/await 编程模型进行了一些重大改进。在 Unity 的博客Unity and .NET, what's next中阅读有关它的更多信息,特别是标有“现代化Unity 运行时。”

我创建了一个新的 2D (URP) Core 项目,但您可以在任何您喜欢的项目中使用它。

我有一个来自Kenney Vleugels的 Space Shooter(Redux,加上字体和声音)的精灵。

我创建了一个包含 Sprite Render 和 Enemy 组件的敌人预制件。Enemy 组件是MonoBehaviour具有 aTransform和 a 的 afloat来跟踪在 y 轴上移动的位置和速度:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

C# 中的async,await​​ 和是什么Task

是什么async

在 C# 中,方法async前面可以有一个关键字,表示方法是异步方法。这只是告诉编译器我们希望能够在其中执行代码并允许该方法的调用者在等待该方法完成时继续执行的一种方式。

这方面的一个例子是做饭。您将开始烹饪肉,当肉在烹饪并且您正在等待它完成时,您将开始制作侧面。当食物在烹饪时,你会开始摆桌子。代码中的一个示例是static async Task<Steak> MakeSteak(int number).

Unity 还有各种可以异步调用的内置方法;有关方法列表,请参阅Unity 文档。通过 Unity 处理内存管理的方式,它使用协程AsyncOperationC# Job System

它是什么await以及如何使用它?

await在 C# 中,您可以使用关键字等待异步操作完成。这在任何具有async关键字等待操作继续的方法中使用:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

有关更多信息,请参阅Microsoft 文档await

什么是 aTask以及如何使用它?

ATask是一种异步方法,它执行单个操作并且不返回值。对于Task返回值的 a,我们将使用Task<TResult>.

要使用任务,我们创建它就像在 C# 中创建任何新对象一样:Task t1 = new Task(void Action)。接下来,我们开始任务t1.wait。最后,我们等待任务完成t1.wait

有多种方法可以创建、启动和运行任务。Task t2 = Task.Run(void Action)将创建并启动一个任务。await Task.Run(void Action)将创建、启动并等待任务完成。我们可以使用最常见的替代方式Task t3 = Task.Factory.Start(void Action)

我们可以通过多种方式等待 Task 完成。int index = Task.WaitAny(Task[])将等待任何任务完成并为我们提供数组中已完成任务的索引。await Task.WaitAll(Task[])将等待所有任务完成。

有关任务的更多信息,请参阅Microsoft 文档

一个简单的task例子

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

任务如何影响绩效

现在让我们比较一个任务的性能和一个方法的性能。

我需要一个可以在所有性能检查中使用的静态类。它将具有模拟性能密集型操作的方法和任务。方法和任务都执行相同的操作:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

现在我需要一个MonoBehaviour可以用来测试对任务和方法的性能影响。为了更好地看到对性能的影响,我将假装我想在十个不同的游戏对象上运行它。我还将跟踪Update方法运行所需的时间。

Update中,我得到了开始时间。如果我正在测试该方法,我会遍历所有模拟的游戏对象并调用性能密集型方法。如果我正在测试任务,我会创建一个Task遍历所有模拟游戏对象的新数组循环,并将性能密集型任务添加到任务数组中。然后我await为所有的任务完成。在方法类型检查之外,我更新方法时间,将其转换为ms. 我也记录下来。

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

强化方法大约需要 65 毫秒才能完成,游戏以大约 12 FPS 的速度运行。

强化方法

密集型任务大约需要 4 毫秒才能完成,游戏以大约 200 FPS 的速度运行。

密集任务

让我们用一千个敌人试试这个:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

使用该方法显示和移动一千个敌人大约需要 150 毫秒,帧速率约为 7 FPS。

千敌

显示和移动一千个敌人的任务大约需要 50 毫秒,帧速率约为 30 FPS。

显示移动的敌人

为什么不useTasks呢?

任务非常高效,可以减少系统性能的压力。您甚至可以使用任务并行库 (TPL) 在多个线程中使用它们。

然而,在 Unity 中使用它们有一些缺点。在 Unity 中使用的主要缺点Task是它们都在Main线程上运行。是的,我们可以让它们在其他线程上运行,但是 Unity 已经做了自己的线程和内存管理,你可以通过创建比 CPU Cores 更多的线程来创建错误,这会导致资源竞争。

任务也很难正确执行和调试。在编写原始代码时,我结束了所有任务都在运行,但没有一个敌人在屏幕上移动。结果是我需要返回Task[]我在Task.

任务会产生大量影响性能的垃圾。它们也不会出现在分析器中,所以如果你有一个影响性能的,很难追踪。另外,我注意到有时我的任务和更新功能会继续从其他场景运行。

统一协程

根据Unity的说法,“协程是一个可以暂停执行(yield)直到给定的YieldInstruction完成的函数。”

这意味着我们可以运行代码并等待任务完成后再继续。这很像一个异步方法。它使用返回类型IEnumerator和 weyield return而不是await.

Unity 有几种不同类型的yield 指令可供我们使用,即WaitForSecondsWaitForEndOfFrameWaitUntilWaitWhile

要启动协程,我们需要 aMonoBehaviour并使用MonoBehaviour.StartCoroutine.

要在协程完成之前停止协程,我们使用MonoBehaviour.StopCoroutine. 停止协程时,请确保使用与启动协程相同的方法。

Unity 中协程的常见用例是等待资产加载并创建冷却计时器。

示例:使用协程的场景加载器

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

检查协程对性能的影响

让我们看看使用协程如何影响我们项目的性能。我只会使用性能密集型方法来做到这一点。

我添加CoroutineMethodType枚举和变量中以跟踪其状态:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

我创建了协程。这类似于我们之前创建的性能密集型任务和方法,添加了代码来更新方法时间:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

Update方法中,我添加了对协程的检查。我还修改了方法时间,更新了代码,并添加了代码来停止协程(如果它正在运行)并且我们更改了方法类型:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

密集的协程大约需要 6 毫秒才能完成,游戏以大约 90 FPS 的速度运行。

强化协程

C# 作业系统和突发编译器

什么是 C# 作业系统?

C# Job System 是 Unity 对易于编写的任务的实现,不会像任务那样产生垃圾,并利用Unity 已经创建的工作线程。这解决了任务的所有缺点。

Unity 将作业比作线程,但他们确实说作业执行一项特定任务。作业也可以在运行前依赖其他作业完成;这解决了我没有正确移动我的任务的问题,Units因为它依赖于首先完成的另一个任务。

Unity 会自动为我们处理作业依赖项。工作系统还内置了一个安全系统,主要用于防止竞争条件。对作业的一个警告是,它们只能包含blittable 类型NativeContainer类型的成员变量。这是安全系统的一个缺点。

要使用作业系统,您需要创建作业、安排作业、等待作业完成,然后使用作业返回的数据。需要作业系统才能使用 Unity 的面向数据的技术堆栈 (DOTS)。

有关作业系统的更多详细信息,请参阅Unity 文档

创建工作

要创建作业,您需要创建一个stuct实现其中一个IJob接口(IJobIJobForIJobParallelForUnity.Engine.Jobs.IJobParallelForTransform)的作业。IJob是一项基本工作。IJobForIJobForParallel用于对本机容器的每个元素执行相同的操作或进行多次迭代。它们之间的区别在于 IJobFor 在单个线程上运行,其中IJobForParallel将在多个线程之间拆分。

我将用于IJob创建一个密集的操作工作,IJobForIJobForParallel创建一个可以移动多个敌人的工作;这只是为了让我们可以看到对性能的不同影响。这些作业将与我们之前创建的任务和方法相同:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

添加成员变量。就我而言,我IJob不需要任何东西。由于作业没有框架的概念,IJobFor并且需要当前增量时间的浮点数;IJobParallelFor他们在 Unity 之外运行MonoBehaviour。他们还需要一个float3用于位置的数组和一个用于 y 轴移动速度的数组:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

最后一步是实现所需的Execute方法。和IJobForIJobForParallel需要int作业正在执行的当前迭代的索引。

transform不同之处在于,我们使用工作中的数组,而不是访问敌人的和移动:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

安排工作

首先,我们需要设置作业并填充作业数据:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

然后我们用JobHandle jobHandle = jobData.Schedule();. 该Schedule方法返回一个JobHandle可以在以后使用的。

我们无法从作业中安排作业。但是,我们可以创建新的工作并从工作中填充他们的数据。一旦安排了作业,就不能中断。

性能密集型工作

我创建了一个创建新作业并安排它的方法。它返回我可以在我的update方法中使用的作业句柄:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

我将这份工作添加到我的枚举中。然后,在Update方法中,我将 添加case到该switch部分。我创建了一个数组JobHandles。然后,我循环遍历所有模拟的游戏对象,为每个对象添加一个预定作业到数组中:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

和_MoveEnemyMoveEnemyParallelJob

接下来,我将作业添加到我的枚举中。然后在Update方法中,我调用一个新MoveEnemyJob方法,传递增量时间。通常你会使用JobForJobParallelFor

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

我要做的第一件事是为职位设置一个数组,并为moveY我将传递给工作的数组设置一个数组。然后我用来自敌人的数据填充这些数组。接下来,我创建作业并根据我要使用的作业设置作业的数据。之后,我根据要使用的作业和要执行的调度类型来安排作业:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

从作业中取回数据

我们必须等待工作完成。我们可以从JobHandle我们安排作业完成时使用的状态获取状态。这将在继续执行之前等待作业完成: >handle.Complete();JobHandle.CompleteAll(jobHandles). 作业完成后,NativeContainer我们用来设置作业的那个将拥有我们需要使用的所有数据。一旦我们从它们那里检索到数据,我们就必须妥善处理它们。

性能密集型工作

这非常简单,因为我没有在作业中读取或写入任何数据。我等待所有计划完成的作业,然后处理Native数组:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

密集的工作大约需要 6 毫秒才能完成,游戏以大约 90 FPS 的速度运行。

密集工作

工作_MoveEnemy

我添加了适当的完整检查:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

在方法类型检查之后,我遍历所有敌人,设置他们的transform位置和moveY作业中设置的数据。接下来,我正确处理原生数组:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

显示和移动一千个有工作的敌人大约需要 160 毫秒,帧速率约为 7 FPS,而没有性能提升。

没有性能提升

以约 30 FPS 的帧速率显示和移动 1000 个并行作业的敌人大约需要 30 毫秒。

并行作业

Unity 中的突发编译器是什么?

突发编译器是一种将字节码转换为本机代码的编译器。将此与 C# 作业系统一起使用可提高生成代码的质量,从而显着提高性能并减少移动设备上的电池消耗。

要使用它,您只需告诉 Unity 您想在具有以下[BurstCompile]属性的作业上使用突发编译:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

然后在 Unity 中,选择Jobs > Burst > Enable Completion

启用完成

Burst在编辑器中是即时 (JIT),这意味着在播放模式下可以关闭。当您构建项目时,它是 Ahead-Of-Time (AOT),这意味着需要在构建项目之前启用它。您可以通过编辑Project Settings Window中的Burst AOT Settings部分来实现。

突发 AOT 设置

有关突发编译器的更多详细信息,请参阅Unity 文档

使用突发编译器的性能密集型工作

在游戏以大约 150 FPS 的速度运行时,一个带有突发的密集工作大约需要 3 毫秒才能完成。

密集的工作与突发

显示和移动一千个敌人,爆发的工作大约需要 30 毫秒,帧速率约为 30 FPS。

突发 30 毫秒

显示和移动一千个敌人,与爆发并行的工作大约需要 6 毫秒,帧速率约为 80 到 90 FPS。

6 毫秒

结论

我们可以使用它Task来提高 Unity 应用程序的性能,但使用它们有几个缺点。根据我们想要做的事情,最好使用 Unity 中打包的东西。如果我们想等待某些东西完成异步加载,请使用协程;我们可以启动协程,而不是停止程序进程的运行。

我们可以使用 C# 作业系统和突发编译器来获得巨大的性能提升,同时在执行进程密集型任务时不必担心所有线程管理问题。使用内置系统,我们确信它以安全的方式完成,不会导致任何不必要的错误或错误。

在不使用突发编译器的情况下,任务确实比作业运行得更好,但这是由于在幕后为我们安全地设置一切而产生的额外开销。使用突发编译器时,我们的工作执行了我们的任务。当您需要可以获得的所有额外性能时,请使用 C# Job System with burst。

这个项目文件可以在我的 GitHub 上找到

来源:https ://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

#csharp #async #await 

C# 中的 Async、Await 和 Task 是什么
Hong  Nhung

Hong Nhung

1656374400

Async, Await Và Task Là Gì Trong C #

Hiệu suất là tất cả mọi thứ khi bạn đang cố gắng xuất bản lên web, thiết bị di động, bảng điều khiển và thậm chí một số PC cấp thấp hơn. Một trò chơi hoặc ứng dụng chạy ở tốc độ dưới 30 FPS có thể gây ra sự thất vọng cho người dùng. Chúng ta hãy xem xét một số thứ chúng ta có thể sử dụng để tăng hiệu suất bằng cách giảm tải cho CPU.

Trong bài đăng này, chúng tôi sẽ đề cập đến những gì async, awaitTasktrong C # là gì và cách sử dụng chúng trong Unity để đạt được hiệu suất trong dự án của bạn. Tiếp theo, chúng ta sẽ xem xét một số gói có sẵn của Unity: coroutines, C # Job System và trình biên dịch bùng nổ. Chúng tôi sẽ xem xét chúng là gì, cách sử dụng chúng và cách chúng tăng hiệu suất trong dự án của bạn.

Để bắt đầu dự án này, tôi sẽ sử dụng Unity 2021.3.4f1. Tôi chưa thử nghiệm mã này trên bất kỳ phiên bản Unity nào khác; tất cả các khái niệm ở đây sẽ hoạt động trên bất kỳ phiên bản Unity nào sau Unity 2019.3. Kết quả hiệu suất của bạn có thể khác nếu sử dụng phiên bản cũ hơn vì Unity đã thực hiện một số cải tiến đáng kể với mô hình lập trình async / await vào năm 2021. Đọc thêm về nó trong blog Unity và .NET của Unity, phần tiếp theo là phần có nhãn “Hiện đại hóa Thời gian chạy thống nhất. ”

Tôi đã tạo một dự án 2D (URP) Core mới, nhưng bạn có thể sử dụng dự án này trong bất kỳ loại dự án nào bạn thích.

Tôi có một sprite mà tôi nhận được từ Space Shooter (Redux, cùng với phông chữ và âm thanh) của Kenney Vleugels .

Tôi đã tạo một nhà lắp ghép của kẻ thù có chứa Sprite Render và một Thành phần của kẻ thù. Thành phần địch là một thành phần MonoBehaviourcó a Transformvà a floatđể theo dõi vị trí và tốc độ di chuyển trên trục y:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

Cái gì async, awaitTaskcó trong C #

Là gì async?

Trong C #, các phương thức có thể có một asynctừ khóa phía trước, nghĩa là các phương thức đó là phương thức không đồng bộ. Đây chỉ là một cách để nói với trình biên dịch rằng chúng ta muốn có thể thực thi mã bên trong và cho phép người gọi phương thức đó tiếp tục thực thi trong khi chờ phương thức này kết thúc.

Một ví dụ về điều này sẽ là nấu một bữa ăn. Bạn sẽ bắt đầu nấu thịt, và trong khi thịt đang nấu và bạn chờ nó hoàn thành, bạn sẽ bắt đầu làm các mặt. Trong khi thức ăn đang nấu, bạn sẽ bắt đầu dọn bàn ăn. Một ví dụ về điều này trong mã sẽ là static async Task<Steak> MakeSteak(int number).

Unity cũng có tất cả các loại phương thức có sẵn mà bạn có thể gọi không đồng bộ; xem tài liệu Unity để biết danh sách các phương pháp. Với cách Unity xử lý việc quản lý bộ nhớ, nó sử dụng coroutines hoặcAsyncOperation Hệ thống công việc C # .

Bạn awaitsử dụng nó như thế nào và nó là gì?

Trong C #, bạn có thể đợi hoạt động không đồng bộ hoàn tất bằng cách sử dụng awaittừ khóa. Điều này được sử dụng bên trong bất kỳ phương thức nào có asynctừ khóa để chờ một thao tác tiếp tục:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Xem các tài liệu của Microsoft để biết thêm về await.

A là gì Taskvà bạn sử dụng nó như thế nào?

A Tasklà một phương thức không đồng bộ thực hiện một thao tác đơn lẻ và không trả về giá trị. Đối với một Tasktrả về một giá trị, chúng tôi sẽ sử dụng Task<TResult>.

Để sử dụng một tác vụ, chúng ta tạo nó giống như tạo bất kỳ đối tượng mới nào trong C # Task t1 = new Task(void Action):. Tiếp theo, chúng ta bắt đầu nhiệm vụ t1.wait. Cuối cùng, chúng tôi đợi nhiệm vụ hoàn thành với t1.wait.

Có một số cách để tạo, bắt đầu và chạy tác vụ. Task t2 = Task.Run(void Action)sẽ tạo và bắt đầu một nhiệm vụ. await Task.Run(void Action)sẽ tạo, bắt đầu và đợi tác vụ hoàn thành. Chúng tôi có thể sử dụng cách thay thế phổ biến nhất với Task t3 = Task.Factory.Start(void Action).

Có một số cách mà chúng ta có thể đợi Tác vụ hoàn thành. int index = Task.WaitAny(Task[])sẽ đợi bất kỳ nhiệm vụ nào hoàn thành và cung cấp cho chúng tôi chỉ số của nhiệm vụ đã hoàn thành trong mảng. await Task.WaitAll(Task[])sẽ đợi tất cả các nhiệm vụ hoàn thành.

Để biết thêm về các tác vụ, hãy xem Tài liệu Microsoft .

Một taskví dụ đơn giản

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Nhiệm vụ ảnh hưởng như thế nào đến hiệu suất

Bây giờ chúng ta hãy so sánh hiệu suất của một tác vụ so với hiệu suất của một phương pháp.

Tôi sẽ cần một lớp tĩnh mà tôi có thể sử dụng trong tất cả các lần kiểm tra hiệu suất của mình. Nó sẽ có một phương thức và một nhiệm vụ mô phỏng một hoạt động đòi hỏi nhiều hiệu suất. Cả phương pháp và tác vụ đều thực hiện cùng một hoạt động chính xác:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Bây giờ tôi cần một MonoBehaviourcái mà tôi có thể sử dụng để kiểm tra tác động của hiệu suất đối với tác vụ và phương pháp. Để tôi có thể thấy tác động tốt hơn đến hiệu suất, tôi sẽ giả vờ rằng tôi muốn chạy điều này trên mười đối tượng trò chơi khác nhau. Tôi cũng sẽ theo dõi khoảng thời gian mà Updatephương thức cần để chạy.

Trong Update, tôi nhận được thời gian bắt đầu. Nếu tôi đang thử nghiệm phương pháp này, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng và gọi phương pháp tăng cường hiệu suất. Nếu tôi đang thử nghiệm tác vụ, tôi sẽ tạo một Taskvòng lặp mảng mới thông qua tất cả các đối tượng trò chơi được mô phỏng và thêm nhiệm vụ chuyên sâu về hiệu suất vào mảng tác vụ. Sau đó tôi awaitcho tất cả các nhiệm vụ để hoàn thành. Bên ngoài kiểm tra loại phương thức, tôi cập nhật thời gian phương thức, chuyển đổi nó thành ms. Tôi cũng ghi lại nó.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

Phương pháp chuyên sâu mất khoảng 65 mili giây để hoàn thành trò chơi chạy ở khoảng 12 FPS.

Phương pháp chuyên sâuPhương pháp chuyên sâuPhương pháp chuyên sâu

Nhiệm vụ chuyên sâu mất khoảng 4ms để hoàn thành với trò chơi chạy ở khoảng 200 FPS.

Nhiệm vụ chuyên sâu

Hãy thử điều này với hàng nghìn kẻ thù:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Hiển thị và di chuyển một nghìn kẻ thù với phương pháp này mất khoảng 150ms với tốc độ khung hình khoảng 7 FPS.

Ngàn kẻ thù

Hiển thị và di chuyển hàng nghìn kẻ thù trong một nhiệm vụ mất khoảng 50ms với tốc độ khung hình khoảng 30 FPS.

Hiển thị kẻ thù di chuyển

Tại sao không useTasks?

Các tác vụ cực kỳ thành thạo và giảm bớt căng thẳng về hiệu suất trên hệ thống của bạn. Bạn thậm chí có thể sử dụng chúng trong nhiều chủ đề bằng cách sử dụng Thư viện song song tác vụ (TPL).

Tuy nhiên, có một số hạn chế khi sử dụng chúng trong Unity. Hạn chế lớn khi sử dụng Tasktrong Unity là tất cả chúng đều chạy trên Mainluồng. Có, chúng tôi có thể làm cho chúng chạy trên các luồng khác, nhưng Unity đã thực hiện quản lý luồng và bộ nhớ của riêng mình, và bạn có thể tạo lỗi bằng cách tạo nhiều luồng hơn Lõi CPU, điều này gây ra sự cạnh tranh về tài nguyên.

Các tác vụ cũng có thể khó thực hiện chính xác và gỡ lỗi. Khi viết mã ban đầu, tôi kết thúc với tất cả các nhiệm vụ đang chạy, nhưng không có kẻ thù nào di chuyển trên màn hình. Kết quả là tôi cần trả lại cái Task[]mà tôi đã tạo trong Task.

Tác vụ tạo ra nhiều rác ảnh hưởng đến hiệu suất. Chúng cũng không hiển thị trong hồ sơ, vì vậy nếu bạn có một cái đang ảnh hưởng đến hiệu suất, thật khó để theo dõi. Ngoài ra, tôi nhận thấy rằng đôi khi các tác vụ và chức năng cập nhật của tôi tiếp tục chạy từ các cảnh khác.

Các quy trình thống nhất

Theo Unity , “Một quy trình đăng ký là một hàm có thể tạm dừng việc thực thi của nó (lợi nhuận) cho đến khi kết thúc YieldInstruction đã cho .”

Điều này có nghĩa là chúng ta có thể chạy mã và đợi một tác vụ hoàn thành trước khi tiếp tục. Điều này giống như một phương pháp không đồng bộ. Nó sử dụng kiểu trả về IEnumeratorvà chúng tôi yield returnthay vì await.

Unity có một số loại hướng dẫn lợi nhuận khác nhau mà chúng tôi có thể sử dụng, chẳng hạn như WaitForSeconds, hoặc .WaitForEndOfFrameWaitUntilWaitWhile

Để bắt đầu coroutines, chúng ta cần một MonoBehaviourvà sử dụng MonoBehaviour.StartCoroutine.

Để dừng một quy trình đăng ký trước khi hoàn tất, chúng tôi sử dụng MonoBehaviour.StopCoroutine. Khi dừng coroutines, hãy đảm bảo rằng bạn sử dụng cùng một phương pháp như bạn đã sử dụng để bắt đầu nó.

Các trường hợp sử dụng phổ biến cho coroutines trong Unity là đợi tải nội dung và tạo bộ định thời gian hồi chiêu.

Ví dụ: Trình tải cảnh sử dụng chương trình điều tra

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Kiểm tra tác động của quy trình đăng ký đối với hiệu suất

Hãy xem việc sử dụng một quy trình điều tra tác động như thế nào đến hiệu suất của dự án của chúng tôi. Tôi sẽ chỉ làm điều này với phương pháp chuyên sâu về hiệu suất.

Tôi đã thêm vào Coroutineenum MethodTypevà các biến để theo dõi trạng thái của nó:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

Tôi đã tạo quy trình đăng quang. Điều này tương tự với tác vụ và phương thức chuyên sâu về hiệu suất mà chúng tôi đã tạo trước đó với mã được bổ sung để cập nhật thời gian của phương thức:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

Trong Updatephương thức này, tôi đã thêm dấu kiểm cho quy trình đăng quang. Tôi cũng đã sửa đổi thời gian phương thức, cập nhật mã và thêm mã để dừng chương trình đăng quang nếu nó đang chạy và chúng tôi đã thay đổi loại phương thức:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

Quá trình đăng quang chuyên sâu mất khoảng 6 mili giây để hoàn thành khi trò chơi chạy ở khoảng 90 FPS.

Chương trình điều trị chuyên sâu

Hệ thống công việc C # và trình biên dịch liên tục

Hệ thống công việc C # là gì?

Hệ thống công việc C # là việc Unity thực hiện các tác vụ dễ viết, không tạo ra rác mà các tác vụ thực hiện và sử dụng các chuỗi công nhân mà Unity đã tạo. Điều này khắc phục tất cả các nhược điểm của nhiệm vụ.

Unity so sánh các công việc như các chủ đề, nhưng họ nói rằng một công việc thực hiện một nhiệm vụ cụ thể. Các công việc cũng có thể phụ thuộc vào các công việc khác để hoàn thành trước khi chạy; điều này khắc phục sự cố với nhiệm vụ mà tôi đã không di chuyển đúng cách của tôi Unitsvì nó phụ thuộc vào một nhiệm vụ khác để hoàn thành trước.

Các phụ thuộc công việc sẽ được Unity tự động giải quyết cho chúng tôi. Hệ thống công việc cũng có một hệ thống an toàn được tích hợp chủ yếu để bảo vệ khỏi các điều kiện của cuộc đua . Một lưu ý với các công việc là chúng chỉ có thể chứa các biến thành viên là kiểu blittable hoặc kiểu NativeContainer ; đây là một nhược điểm của hệ thống an toàn.

Để sử dụng hệ thống công việc, bạn tạo công việc, lên lịch công việc, đợi công việc hoàn thành, sau đó sử dụng dữ liệu trả về của công việc. Hệ thống công việc là cần thiết để sử dụng Ngăn xếp Công nghệ Định hướng Dữ liệu (DOTS) của Unity.

Để biết thêm chi tiết về hệ thống công việc, hãy xem tài liệu Unity .

Tạo một công việc

Để tạo một công việc, bạn tạo một công stuctviệc triển khai một trong các IJobgiao diện ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJoblà một công việc cơ bản. IJobForIJobForParallelđược sử dụng để thực hiện cùng một thao tác trên mỗi phần tử của vùng chứa gốc hoặc cho một số lần lặp. Sự khác biệt giữa chúng là IJobFor chạy trên một luồng duy nhất, nơi IJobForParallelsẽ được chia thành nhiều luồng.

Tôi sẽ sử dụng IJobđể tạo ra một công việc hoạt động chuyên sâu IJobForIJobForParallelđể tạo ra một công việc có thể di chuyển nhiều kẻ thù xung quanh; điều này chỉ để chúng ta có thể thấy các tác động khác nhau đến hiệu suất. Các công việc này sẽ giống với các tác vụ và phương pháp mà chúng tôi đã tạo trước đó:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Thêm các biến thành viên. Trong trường hợp của tôi, tôi IJobkhông cần bất kỳ. Và cần một IJobForphao IJobParallelForcho thời gian đồng bằng hiện tại vì các công việc không có khái niệm về khung; họ hoạt động bên ngoài Unity's MonoBehaviour. Họ cũng cần một mảng float3cho vị trí và một mảng cho tốc độ di chuyển trên trục y:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

Bước cuối cùng là thực hiện Executephương thức được yêu cầu. Cả IJobForhai IJobForParallelđều yêu cầu một intchỉ mục của lần lặp hiện tại mà công việc đang thực thi.

Sự khác biệt là thay vì truy cập kẻ thù transformvà di chuyển, chúng tôi sử dụng mảng có trong công việc:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Lên lịch công việc

Đầu tiên, chúng ta cần cài đặt công việc và điền dữ liệu công việc:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Sau đó, chúng tôi lên lịch công việc với JobHandle jobHandle = jobData.Schedule();. Phương Schedulethức trả về một JobHandlecó thể được sử dụng sau này.

Chúng ta không thể sắp xếp một công việc từ bên trong một công việc. Tuy nhiên, chúng tôi có thể tạo công việc mới và điền dữ liệu của họ từ bên trong công việc. Một khi công việc đã được lên lịch, nó không thể bị gián đoạn.

Công việc đòi hỏi hiệu suất cao

Tôi đã tạo ra một phương pháp tạo một công việc mới và lên lịch cho nó. Nó trả về công việc xử lý mà tôi có thể sử dụng trong updatephương thức của mình:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

Tôi đã thêm công việc vào enum của mình. Sau đó, trong Updatephương thức, tôi thêm phần casevào switchphần. Tôi đã tạo một mảng JobHandles. Sau đó, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng, thêm một công việc đã lên lịch cho từng đối tượng vào mảng:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

MoveEnemy_MoveEnemyParallelJob

Tiếp theo, tôi đã thêm các công việc vào enum của mình. Sau đó, trong Updatephương thức, tôi gọi một MoveEnemyJobphương thức mới, vượt qua thời gian delta. Thông thường, bạn sẽ sử JobFordụng JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

Điều đầu tiên tôi làm là thiết lập một mảng cho các vị trí và một mảng cho vị trí moveYmà tôi sẽ chuyển cho các công việc. Sau đó, tôi điền vào các mảng này với dữ liệu từ kẻ thù. Tiếp theo, tôi tạo công việc và thiết lập dữ liệu của công việc tùy thuộc vào công việc mà tôi muốn sử dụng. Sau đó, tôi lên lịch công việc tùy thuộc vào công việc mà tôi muốn sử dụng và loại lập lịch mà tôi muốn thực hiện:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Lấy lại dữ liệu từ một công việc

Chúng ta phải đợi cho công việc được hoàn thành. Chúng ta có thể lấy trạng thái từ trạng thái JobHandlemà chúng ta đã sử dụng khi lên lịch để hoàn thành công việc. Thao tác này sẽ đợi công việc hoàn tất trước khi tiếp tục thực hiện:> handle.Complete();hoặc JobHandle.CompleteAll(jobHandles). Sau khi công việc hoàn tất, công việc NativeContainermà chúng tôi đã sử dụng để thiết lập công việc sẽ có tất cả dữ liệu mà chúng tôi cần sử dụng. Khi chúng tôi lấy dữ liệu từ chúng, chúng tôi phải xử lý chúng đúng cách.

Công việc đòi hỏi hiệu suất cao

Điều này khá đơn giản vì tôi không đọc hoặc ghi bất kỳ dữ liệu nào cho công việc. Tôi đợi cho tất cả các công việc đã được lên lịch hoàn thành sau đó xử lý Nativemảng:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

Công việc chuyên sâu mất khoảng 6ms để hoàn thành với trò chơi chạy ở khoảng 90 FPS.

Công việc chuyên sâu

Công MoveEnemyviệc

Tôi thêm các séc hoàn chỉnh thích hợp:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Sau khi kiểm tra loại phương pháp, tôi lặp qua tất cả kẻ thù, thiết lập transformvị trí của chúng và moveYđến dữ liệu đã được thiết lập trong công việc. Tiếp theo, tôi xử lý đúng cách các mảng gốc:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Việc hiển thị và di chuyển hàng nghìn kẻ thù với công việc mất khoảng 160ms với tốc độ khung hình khoảng 7 FPS mà không tăng hiệu suất.

Không tăng hiệu suất

Hiển thị và di chuyển hàng nghìn kẻ thù song song với công việc mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.

Công việc song song

Trình biên dịch bùng nổ trong Unity là gì?

Trình biên dịch liên tục là trình biên dịch chuyển từ mã bytecode sang mã gốc. Việc sử dụng tính năng này với Hệ thống công việc C # sẽ cải thiện chất lượng của mã được tạo, giúp bạn tăng hiệu suất đáng kể cũng như giảm mức tiêu thụ pin trên thiết bị di động.

Để sử dụng điều này, bạn chỉ cần nói với Unity rằng bạn muốn sử dụng trình biên dịch liên tục trong công việc với [BurstCompile]thuộc tính:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Sau đó, trong Unity, chọn Jobs > Burst> Enable Completion

Cho phép hoàn thành

Burst là Just-In-Time (JIT) khi ở trong Trình chỉnh sửa, có nghĩa là điều này có thể ngừng hoạt động khi ở Chế độ phát. Khi bạn xây dựng dự án của mình, nó là Ahead-Of-Time (AOT), có nghĩa là điều này cần được kích hoạt trước khi xây dựng dự án của bạn. Bạn có thể làm như vậy bằng cách chỉnh sửa phần Cài đặt Burst AOT trong Cửa sổ Cài đặt Dự án .

Cài đặt Burst AOT

Để biết thêm chi tiết về trình biên dịch cụm, hãy xem tài liệu Unity .

Một công việc chuyên sâu về hiệu suất với trình biên dịch liên tục

Một công việc chuyên sâu với liên tục mất khoảng 3ms để hoàn thành với trò chơi chạy ở khoảng 150 FPS.

Công việc chuyên sâu với Burst

Hiển thị và di chuyển hàng nghìn kẻ thù, công việc với liên tục mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.

Burst 30 mili giây

Hiển thị và di chuyển hàng nghìn kẻ thù, công việc song song với liên tục mất khoảng 6ms với tốc độ khung hình khoảng 80 đến 90 FPS.

6 mili giây

Sự kết luận

Chúng tôi có thể sử dụng Taskđể tăng hiệu suất của các ứng dụng Unity của mình, nhưng có một số hạn chế khi sử dụng chúng. Tốt hơn là sử dụng những thứ được đóng gói trong Unity tùy thuộc vào những gì chúng ta muốn làm. Sử dụng coroutines nếu chúng ta muốn đợi một thứ gì đó kết thúc tải một cách không đồng bộ; chúng tôi có thể bắt đầu quy trình đăng ký và không dừng quá trình chạy chương trình của chúng tôi.

Chúng ta có thể sử dụng Hệ thống công việc C # với trình biên dịch liên tục để đạt được hiệu suất lớn trong khi không phải lo lắng về tất cả nội dung quản lý luồng khi thực hiện các tác vụ đòi hỏi nhiều quy trình. Sử dụng các hệ thống có sẵn, chúng tôi chắc chắn rằng nó được thực hiện một cách an toàn và không gây ra bất kỳ lỗi hoặc lỗi không mong muốn nào.

Các công việc đã chạy tốt hơn một chút so với các công việc không sử dụng trình biên dịch liên tục, nhưng đó là do chi phí bổ sung đằng sau hậu trường ít hơn để thiết lập mọi thứ một cách an toàn cho chúng tôi. Khi sử dụng trình biên dịch bùng nổ, các công việc của chúng tôi đã thực hiện các nhiệm vụ của chúng tôi. Khi bạn cần tất cả hiệu suất bổ sung mà bạn có thể nhận được, hãy sử dụng Hệ thống công việc C # với liên tục.

Các tệp dự án cho việc này có thể được tìm thấy trên GitHub của tôi .

Nguồn: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

#csharp  #async  #await 

Async, Await Và Task Là Gì Trong C #

O que Async, Await e Task estão em C #

Desempenho é tudo quando você está tentando publicar na web, dispositivos móveis, consoles e até mesmo alguns dos PCs de baixo custo. Um jogo ou aplicativo rodando a menos de 30 FPS pode causar frustração para os usuários. Vamos dar uma olhada em algumas das coisas que podemos usar para aumentar o desempenho reduzindo a carga na CPU.

Neste post, abordaremos o que são async, await, e Taskem C# e como usá-los no Unity para obter desempenho em seu projeto. Em seguida, vamos dar uma olhada em alguns dos pacotes embutidos do Unity: corrotinas, o C# Job System e o compilador de intermitência. Veremos o que são, como usá-los e como eles aumentam o desempenho em seu projeto.

Para iniciar este projeto, usarei o Unity 2021.3.4f1. Não testei este código em nenhuma outra versão do Unity; todos os conceitos aqui devem funcionar em qualquer versão do Unity após o Unity 2019.3. Seus resultados de desempenho podem diferir se você usar uma versão mais antiga, pois o Unity fez algumas melhorias significativas com o modelo de programação async/await em 2021. Leia mais sobre isso no blog do Unity Unity and .NET, o que vem a seguir , em particular a seção intitulada “Modernizing the Tempo de execução da unidade.”

Criei um novo projeto 2D (URP) Core, mas você pode usá-lo em qualquer tipo de projeto que desejar.

Eu tenho um sprite que peguei do Space Shooter (Redux, além de fontes e sons) de Kenney Vleugels .

Eu criei um prefab inimigo que contém um Sprite Render e um Enemy Component. O Componente Inimigo é um MonoBehaviourque tem a Transforme a floatpara acompanhar a posição e a velocidade para se mover no eixo y:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

O que async, awaite Taskestão em C#

O que é async?

Em C#, os métodos podem ter uma palavra- asyncchave na frente deles, o que significa que os métodos são métodos assíncronos. Esta é apenas uma maneira de dizer ao compilador que queremos poder executar o código e permitir que o chamador desse método continue a execução enquanto aguarda a conclusão desse método.

Um exemplo disso seria cozinhar uma refeição. Você começará a cozinhar a carne e, enquanto a carne estiver cozinhando e você estiver esperando que ela termine, você começará a fazer os lados. Enquanto a comida está cozinhando, você deve começar a colocar a mesa. Um exemplo disso no código seria static async Task<Steak> MakeSteak(int number).

O Unity também tem todos os tipos de métodos embutidos que você pode chamar de forma assíncrona; consulte os documentos do Unity para obter uma lista de métodos. Com a maneira como o Unity lida com o gerenciamento de memória, ele usa coroutines , AsyncOperation, ou o C# Job System .

O que é awaite como você usa?

Em C#, você pode aguardar a conclusão de uma operação assíncrona usando a palavra- awaitchave. Isso é usado dentro de qualquer método que tenha a asyncpalavra-chave para aguardar a continuação de uma operação:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Consulte os documentos da Microsoft para saber mais sobre await.

O que é um Taske como você o usa?

A Taské um método assíncrono que executa uma única operação e não retorna um valor. Para um Taskque retorna um valor, usaríamos Task<TResult>.

Para usar uma tarefa, nós a criamos como criar qualquer novo objeto em C#: Task t1 = new Task(void Action). Em seguida, iniciamos a tarefa t1.wait. Por fim, esperamos que a tarefa seja concluída com t1.wait.

Há várias maneiras de criar, iniciar e executar tarefas. Task t2 = Task.Run(void Action)irá criar e iniciar uma tarefa. await Task.Run(void Action)irá criar, iniciar e aguardar a conclusão da tarefa. Podemos usar a maneira alternativa mais comum com Task t3 = Task.Factory.Start(void Action).

Existem várias maneiras pelas quais podemos esperar que a tarefa seja concluída. int index = Task.WaitAny(Task[])aguardará a conclusão de qualquer tarefa e nos fornecerá o índice da tarefa concluída na matriz. await Task.WaitAll(Task[])aguardará a conclusão de todas as tarefas.

Para obter mais informações sobre tarefas, consulte os Documentos da Microsoft .

Um exemplo simplestask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Como a tarefa afeta o desempenho

Agora vamos comparar o desempenho de uma tarefa versus o desempenho de um método.

Vou precisar de uma classe estática que possa usar em todas as minhas verificações de desempenho. Ele terá um método e uma tarefa que simula uma operação de alto desempenho. Tanto o método quanto a tarefa executam a mesma operação exata:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Agora eu preciso de um MonoBehaviourque eu possa usar para testar o impacto do desempenho na tarefa e no método. Só para que eu possa ver um melhor impacto no desempenho, vou fingir que quero rodar isso em dez objetos de jogo diferentes. Também acompanharei a quantidade de tempo que o Updatemétodo leva para ser executado.

Em Update, recebo a hora de início. Se estou testando o método, percorro todos os objetos de jogo simulados e chamo o método de alto desempenho. Se estou testando a tarefa, crio um novo Taskloop de matriz por meio de todos os objetos de jogo simulados e adiciono a tarefa de alto desempenho à matriz de tarefas. Eu, então, awaitpara que todas as tarefas sejam concluídas. Fora da verificação do tipo de método, atualizo o tempo do método, convertendo-o para ms. Eu também registro.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

O método intensivo leva cerca de 65ms para ser concluído com o jogo rodando a cerca de 12 FPS.

Método Intensivo

A tarefa intensiva leva cerca de 4ms para ser concluída com o jogo rodando a cerca de 200 FPS.

Tarefa Intensiva

Vamos tentar isso com mil inimigos:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Exibir e mover mil inimigos com o método levou cerca de 150ms com uma taxa de quadros de cerca de 7 FPS.

Mil Inimigos

Exibir e mover mil inimigos com uma tarefa levou cerca de 50ms com uma taxa de quadros de cerca de 30 FPS.

Exibindo Inimigos em Movimento

Por que não useTasks?

As tarefas são extremamente eficientes e reduzem a pressão sobre o desempenho do seu sistema. Você pode até usá-los em vários threads usando a Biblioteca Paralela de Tarefas (TPL).

No entanto, existem algumas desvantagens em usá-los no Unity. A principal desvantagem de usar Taskno Unity é que todos eles são executados no Mainencadeamento. Sim, podemos fazê-los rodar em outros threads, mas o Unity já faz seu próprio gerenciamento de thread e memória, e você pode criar erros criando mais threads do que CPU Cores, o que causa competição por recursos.

As tarefas também podem ser difíceis de executar corretamente e depurar. Ao escrever o código original, acabei com as tarefas todas em execução, mas nenhum dos inimigos se moveu na tela. Acabou sendo que precisei retornar o Task[]que criei no Task.

As tarefas criam muito lixo que afeta o desempenho. Eles também não aparecem no criador de perfil, portanto, se você tiver um que esteja afetando o desempenho, é difícil rastrear. Além disso, notei que às vezes minhas tarefas e funções de atualização continuam sendo executadas em outras cenas.

Corrotinas de unidade

De acordo com Unity , “Uma corrotina é uma função que pode suspender sua execução (yield) até que o YieldInstruction termine”.

O que isso significa é que podemos executar o código e esperar que uma tarefa seja concluída antes de continuar. Isso é muito parecido com um método assíncrono. Ele usa um tipo de retorno IEnumeratore nós yield returnem vez de await.

O Unity tem vários tipos diferentes de instruções de rendimento que podemos usar, ou seja, WaitForSeconds, WaitForEndOfFrame, WaitUntilou WaitWhile.

Para iniciar as corrotinas, precisamos de um MonoBehavioure usamos o MonoBehaviour.StartCoroutine.

Para parar uma corrotina antes que ela seja concluída, usamos MonoBehaviour.StopCoroutine. Ao parar as corrotinas, certifique-se de usar o mesmo método usado para iniciá-las.

Casos de uso comuns para corrotinas no Unity são aguardar o carregamento dos ativos e criar temporizadores de resfriamento.

Exemplo: Um carregador de cena usando uma corrotina

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Verificando o impacto de uma corrotina no desempenho

Vamos ver como o uso de uma corrotina afeta o desempenho do nosso projeto. Eu só vou fazer isso com o método de desempenho intensivo.

Eu adicionei Coroutineao MethodTypeenum e variáveis ​​para acompanhar seu estado:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

Eu criei a corrotina. Isso é semelhante à tarefa e ao método de alto desempenho que criamos anteriormente com código adicionado para atualizar o tempo do método:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

No Updatemétodo, adicionei a verificação da corrotina. Também modifiquei o tempo do método, atualizei o código e adicionei código para parar a corrotina se estivesse em execução e alteramos o tipo do método:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

A corrotina intensiva leva cerca de 6ms para ser concluída com o jogo rodando a cerca de 90 FPS.

Corrotina Intensiva

O sistema de trabalho C# e o compilador de intermitência

O que é o sistema de trabalho C#?

O C# Job System é a implementação do Unity de tarefas que são fáceis de escrever, não geram o lixo que as tarefas fazem e utilizam os threads de trabalho que o Unity já criou. Isso corrige todas as desvantagens das tarefas.

O Unity compara jobs como threads, mas eles dizem que um job faz uma tarefa específica. Os trabalhos também podem depender de outros trabalhos para serem concluídos antes de serem executados; isso corrige o problema com a tarefa que eu tinha que não moveu minha corretamente Unitsporque dependia de outra tarefa para ser concluída primeiro.

As dependências de trabalho são automaticamente cuidadas para nós pelo Unity. O sistema de trabalho também possui um sistema de segurança integrado principalmente para proteção contra condições de corrida . Uma ressalva com os jobs é que eles só podem conter variáveis ​​de membro que sejam tipos blittable ou tipos NativeContainer ; esta é uma desvantagem do sistema de segurança.

Para usar o sistema de trabalho, você cria o trabalho, agende o trabalho, aguarde a conclusão do trabalho e use os dados retornados pelo trabalho. O sistema de trabalho é necessário para usar o Data-Oriented Technology Stack (DOTS) da Unity.

Para obter mais detalhes sobre o sistema de tarefas, consulte a documentação do Unity .

Criando um trabalho

Para criar um trabalho, você cria um stuctque implementa uma das IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobé um trabalho básico. IJobFore IJobForParallelsão usados ​​para executar a mesma operação em cada elemento de um contêiner nativo ou em várias iterações. A diferença entre eles é que o IJobFor é executado em um único thread, onde IJobForParallelserá dividido entre vários threads.

Eu usarei IJobpara criar um trabalho de operação intensiva IJobFore IJobForParallelpara criar um trabalho que moverá vários inimigos ao redor; isso é apenas para que possamos ver os diferentes impactos no desempenho. Esses trabalhos serão idênticos às tarefas e métodos que criamos anteriormente:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Adicione as variáveis ​​de membro. No meu caso, o meu IJobnão precisa de nenhum. O IJobFore IJobParallelForprecisa de um float para o tempo delta atual, pois os trabalhos não têm um conceito de quadro; eles operam fora do Unity MonoBehaviour. Eles também precisam de uma matriz de float3para a posição e uma matriz para a velocidade de movimento no eixo y:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

A última etapa é implementar o Executemétodo necessário. O IJobFore IJobForParallelambos exigem um intpara o índice da iteração atual que o trabalho está executando.

A diferença é que ao invés de acessar o inimigo transforme se mover, usamos o array que está no trabalho:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Agendando um trabalho

Primeiro, precisamos instalar o trabalho e preencher os dados dos trabalhos:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Em seguida, agendamos o trabalho com JobHandle jobHandle = jobData.Schedule();. O Schedulemétodo retorna um JobHandleque pode ser usado posteriormente.

Não podemos agendar um trabalho de dentro de um trabalho. Podemos, no entanto, criar novos trabalhos e preencher seus dados de dentro de um trabalho. Depois que um trabalho é agendado, ele não pode ser interrompido.

O trabalho de alto desempenho

Eu criei um método que cria um novo trabalho e o agenda. Ele retorna o identificador de trabalho que posso usar no meu updatemétodo:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

Eu adicionei o trabalho ao meu enum. Então, no Updatemétodo, eu adiciono o caseà switchseção. Eu criei uma matriz de JobHandles. Em seguida, faço um loop por todos os objetos de jogo simulados, adicionando um trabalho agendado para cada um ao array:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

O MoveEnemyeMoveEnemyParallelJob

Em seguida, adicionei os trabalhos ao meu enum. Então, no Updatemétodo, chamo um novo MoveEnemyJobmétodo, passando o tempo delta. Normalmente você usaria o JobForou o JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

A primeira coisa que faço é definir um array para as posições e um array para o moveYque vou passar para os jobs. Eu então preencho essas matrizes com os dados dos inimigos. Em seguida, crio o trabalho e defino os dados do trabalho dependendo de qual trabalho quero usar. Depois disso, agendo o trabalho dependendo do trabalho que quero usar e do tipo de agendamento que quero fazer:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Obtendo os dados de volta de um trabalho

Temos que esperar que o trabalho seja concluído. Podemos obter o status do JobHandleque usamos quando agendamos o trabalho para concluí-lo. Isso aguardará a conclusão do trabalho antes de continuar a execução: > handle.Complete();ou JobHandle.CompleteAll(jobHandles). Quando o trabalho estiver concluído, o NativeContainerque usamos para configurar o trabalho terá todos os dados que precisamos usar. Uma vez que recuperamos os dados deles, temos que descartá-los adequadamente.

O trabalho de alto desempenho

Isso é bem simples, pois não estou lendo ou gravando nenhum dado no trabalho. Aguardo todos os trabalhos que foram agendados para serem concluídos e descarto a Nativematriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

O trabalho intensivo leva cerca de 6ms para ser concluído com o jogo rodando a cerca de 90 FPS.

Trabalho intensivo

O MoveEnemytrabalho

Eu adiciono as verificações completas apropriadas:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Após a verificação do tipo de método, eu percorro todos os inimigos, definindo suas transformposições e moveYos dados que foram definidos no trabalho. Em seguida, descarto adequadamente os arrays nativos:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Exibir e mover mil inimigos com trabalho levou cerca de 160ms com uma taxa de quadros de cerca de 7 FPS sem ganhos de desempenho.

Sem Ganhos de Desempenho

Exibir e mover mil inimigos com trabalho paralelo levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.

Trabalho paralelo

O que é o compilador burst no Unity?

O compilador de intermitência é um compilador que traduz de bytecode para código nativo. Usar isso com o C# Job System melhora a qualidade do código gerado, proporcionando um aumento significativo no desempenho, além de reduzir o consumo da bateria em dispositivos móveis.

Para usar isso, basta informar ao Unity que deseja usar a compilação de rajada no trabalho com o [BurstCompile]atributo:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Em seguida, no Unity, selecione Jobs > Burst > Enable Completion

Ativar conclusão

Burst é Just-In-Time (JIT) enquanto estiver no Editor, o que significa que isso pode ser desativado enquanto estiver no modo Play. Quando você compila seu projeto, ele é Ahead-Of-Time (AOT), o que significa que isso precisa ser ativado antes de compilar seu projeto. Você pode fazer isso editando a seção Burst AOT Settings na janela Project Settings .

Configurações AOT de rajada

Para obter mais detalhes sobre o compilador de intermitência, consulte a documentação do Unity .

Um trabalho de alto desempenho com o compilador de intermitência

Um trabalho intensivo com rajada leva cerca de 3ms para ser concluído com o jogo rodando a cerca de 150 FPS.

Trabalho intensivo com explosão

Exibindo e movendo mil inimigos, o trabalho com burst levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.

Explosão 30 ms

Exibindo e movendo mil inimigos, o trabalho paralelo com burst levou cerca de 6ms com uma taxa de quadros de cerca de 80 a 90 FPS.

6 ms

Conclusão

Podemos usar Taskpara aumentar o desempenho de nossos aplicativos Unity, mas há várias desvantagens em usá-los. É melhor usar as coisas que vêm empacotadas no Unity dependendo do que queremos fazer. Use corrotinas se quisermos esperar que algo termine de carregar de forma assíncrona; podemos iniciar a corrotina e não interromper a execução do processo do nosso programa.

Podemos usar o C# Job System com o compilador de intermitência para obter um enorme ganho de desempenho sem precisar se preocupar com todas as coisas de gerenciamento de threads ao executar tarefas de processo intenso. Usando os sistemas embutidos, temos certeza de que é feito de maneira segura que não causa erros ou bugs indesejados.

As tarefas foram executadas um pouco melhor do que os trabalhos sem usar o compilador de rajadas, mas isso se deve à pequena sobrecarga extra nos bastidores para configurar tudo com segurança para nós. Ao usar o compilador de intermitência, nossos trabalhos executavam nossas tarefas. Quando você precisar de todo o desempenho extra que pode obter, use o C# Job System com burst.

Os arquivos de projeto para isso podem ser encontrados no meu GitHub .

Fonte: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

 #csharp #async #await 

O que Async, Await e Task estão em C #

What Async, Await, and Task are in C#

Performance is everything when you are trying to publish to the web, mobile, consoles, and even some of the lower-end PCs. A game or application running at less than 30 FPS can cause frustration for the users. Let’s take a look at some of the things we can use to increase the performance by reducing the load on the CPU.

In this post, we will be covering what async, await, and Task in C# are and how to use them in Unity to gain performance in your project. Next, we will take a look at some of Unity’s inbuilt packages: coroutines, the C# Job System, and the burst compiler. We will look at what they are, how to use them, and how they increase the performance in your project.

To start this project off, I will be using Unity 2021.3.4f1. I have not tested this code on any other version of Unity; all concepts here should work on any Unity version after Unity 2019.3. Your performance results may differ if using an older version as Unity did make some significant improvements with the async/await programming model in 2021. Read more about it in Unity’s blog Unity and .NET, what’s next, in particular the section labeled “Modernizing the Unity runtime.”

I created a new 2D (URP) Core project, but you can use this in any type of project that you like.

I have a sprite that I got from Space Shooter (Redux, plus fonts and sounds) by Kenney Vleugels.

I created an enemy prefab that contains a Sprite Render and an Enemy Component. The Enemy Component is a MonoBehaviour that has a Transform and a float to keep track of the position and the speed to move on the y axis:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

What async, await, and Task are in C#

What is async?

In C#, methods can have an async keyword in front of them, meaning that the methods are asynchronous methods. This is just a way of telling the compiler that we want to be able to execute code within and allow the caller of that method to continue execution while waiting for this method to finish.

An example of this would be cooking a meal. You will start cooking the meat, and while the meat is cooking and you are waiting for it to finish, you would start making the sides. While the food is cooking, you would start setting the table. An example of this in code would be static async Task<Steak> MakeSteak(int number).

Unity also has all kinds of inbuilt methods that you can call asynchronously; see the Unity docs for a list of methods. With the way Unity handles memory management, it uses either coroutines, AsyncOperation, or the C# Job System.

What is await and how do you use it?

In C#, you can wait for an asynchronous operation to complete by using the await keyword. This is used inside any method that has the async keyword to wait for an operation to continue:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

See the Microsoft documents for more on await.

What is a Task and how do you use it?

A Task is an asynchronous method that performs a single operation and does not return a value. For a Task that returns a value, we would use Task<TResult>.

To use a task, we create it like creating any new object in C#: Task t1 = new Task(void Action). Next, we start the task t1.wait. Lastly, we wait for the task to complete with t1.wait.

There are several ways to create, start, and run tasks. Task t2 = Task.Run(void Action) will create and start a task. await Task.Run(void Action) will create, start, and wait for the task to complete. We can use the most common alternative way with Task t3 = Task.Factory.Start(void Action).

There are several ways that we can wait for Task to complete. int index = Task.WaitAny(Task[]) will wait for any task to complete and give us the index of the completed task in the array. await Task.WaitAll(Task[]) will wait for all of the tasks to complete.

For more on tasks, see the Microsoft Documents.

A simple taskexample

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

How the task affects performance

Now let’s compare the performance of a task versus the performance of a method.

I will need a static class that I can use in all of my performance checks. It will have a method and a task that simulates a performance-intensive operation. Both the method and the task perform the same exact operation:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Now I need a MonoBehaviour that I can use to test the performance impact on the task and the method. Just so I can see a better impact on the performance, I will pretend that I want to run this on ten different game objects. I will also keep track of the amount of time the Update method takes to run.

In Update, I get the start time. If I am testing the method, I loop through all of the simulated game objects and call the performance-intensive method. If I am testing the task, I create a new Task array loop through all of the simulated game objects and add the performance-intensive task to the task array. I then await for all of the tasks to complete. Outside of the method type check, I update the method time, converting it to ms. I also log it.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

The intensive method takes around 65ms to complete with the game running at about 12 FPS.

Intensive MethodIntensive MethodIntensive Method

The intensive task takes around 4ms to complete with the game running at about 200 FPS.

Intensive Task

Let’s try this with a thousand enemies:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Displaying and moving a thousand enemies with the method took around 150ms with a frame rate of about 7 FPS.

Thousand Enemies

Displaying and moving a thousand enemies with a task took around 50ms with a frame rate of about 30 FPS.

Displaying Moving Enemies

Why not useTasks?

Tasks are extremely perficient and reduce the strain on performance on your system. You can even use them in multiple threads using the Task Parallel Library (TPL).

There are some drawbacks to using them in Unity, however. The major drawback with using Task in Unity is that they all run on the Main thread. Yes, we can make them run on other threads, but Unity already does its own thread and memory management, and you can create errors by creating more threads than CPU Cores, which causes competition for resources.

Tasks can also be difficult to get to perform correctly and debug. When writing the original code, I ended up with the tasks all running, but none of the enemies moved on screen. It ended up being that I needed to return the Task[] that I created in the Task.

Tasks create a lot of garbage that affect the performance. They also do not show up in the profiler, so if you have one that is affecting the performance, it is hard to track down. Also, I have noticed that sometimes my tasks and update functions continue to run from other scenes.

Unity coroutines

According to Unity, “A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes.”

What this means is that we can run code and wait for a task to complete before continuing. This is much like an async method. It uses a return type IEnumerator and we yield return instead of await.

Unity has several different types of yield instructions that we can use, i.e., WaitForSeconds, WaitForEndOfFrame, WaitUntil, or WaitWhile.

To start coroutines, we need a MonoBehaviour and use the MonoBehaviour.StartCoroutine.

To stop a coroutine before it completes, we use MonoBehaviour.StopCoroutine. When stopping coroutines, make sure that you use the same method as you used to start it.

Common use cases for coroutines in Unity are to wait for assets to load and to create cooldown timers.

Example: A scene loader using a coroutine

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Checking a coroutine’s impact on performance

Let’s see how using a coroutine impacts the performance of our project. I am only going to do this with the performance-intensive method.

I added the Coroutine to the MethodType enum and variables to keep track of its state:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

I created the coroutine. This is similar to the performance-intensive task and method that we created earlier with added code to update the method time:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

In the Update method, I added the check for the coroutine. I also modified the method time, updated code, and added code to stop the coroutine if it was running and we changed the method type:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

The intensive coroutine takes around 6ms to complete with the game running at about 90 FPS.

Intensive Coroutine

The C# Job System and burst compiler

What is the C# Job System?

The C# Job System is Unity’s implementation of tasks that are easy to write, do not generate the garbage that tasks do, and utilize the worker threads that Unity has already created. This fixes all of the downsides of tasks.

Unity compares jobs as threads, but they do say that a job does one specific task. Jobs can also depend on other jobs to complete before running; this fixes the issue with the task that I had that did not properly move my Units because it was depending on another task to complete first.

The job dependencies are automatically taken care of for us by Unity. The job system also has a safety system built in mainly to protect against race conditions. One caveat with jobs is that they can only contain member variables that are either blittable types or NativeContainer types; this is a drawback of the safety system.

To use the job system, you create the job, schedule the job, wait for the job to complete, then use the data returned by the job. The job system is needed in order to use Unity’s Data-Oriented Technology Stack (DOTS).

For more details on the job system, see the Unity documentation.

Creating a job

To create a job, you create a stuct that implements one of the IJob interfaces (IJob, IJobFor, IJobParallelFor, Unity.Engine.Jobs.IJobParallelForTransform). IJob is a basic job. IJobFor and IJobForParallel are used to perform the same operation on each element of a native container or for a number of iterations. The difference between them is that the IJobFor runs on a single thread where the IJobForParallel will be split up between multiple threads.

I will use IJob to create an intensive operation job, IJobFor and IJobForParallel to create a job that will move multiple enemies around; this is just so we can see the different impacts on performance. These jobs will be identical to the tasks and methods that we created earlier:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Add the member variables. In my case, my IJob does not need any. The IJobFor and IJobParallelFor need a float for the current delta time as jobs do not have a concept of a frame; they operate outside of Unity’s MonoBehaviour. They also need an array of float3 for the position and an array for the move speed on the y axis:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

The last step is to implement the required Execute method. The IJobFor and IJobForParallel both require an int for the index of the current iteration that the job is executing.

The difference is instead of accessing the enemy’s transform and move, we use the array that are in the job:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Scheduling a job

First, we need to instate the job and populate the jobs data:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Then we schedule the job with JobHandle jobHandle = jobData.Schedule();. The Schedule method returns a JobHandle that can be used later on.

We can not schedule a job from within a job. We can, however, create new jobs and populate their data from within a job. Once a job has been scheduled, it cannot be interrupted.

The performance-intensive job

I created a method that creates a new job and schedules it. It returns the job handle that I can use in my update method:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

I added the job to my enum. Then, in the Update method, I add the case to the switch section. I created an array of JobHandles. I then loop through all of the simulated game objects, adding a scheduled job for each to the array:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

The MoveEnemy and MoveEnemyParallelJob

Next, I added the jobs to my enum. Then in the Update method, I call a new MoveEnemyJob method, passing the delta time. Normally you would use either the JobFor or the JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

The first thing I do is set an array for the positions and an array for the moveY that I will pass on to the jobs. I then fill these arrays with the data from the enemies. Next, I create the job and set the job’s data depending on which job I want to use. After that, I schedule the job depending on the job that I want to use and the type of scheduling I want to do:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Getting the data back from a job

We have to wait for the job to be completed. We can get the status from the JobHandle that we used when we scheduled the job to complete it. This will wait for the job to be complete before continuing execution: >handle.Complete(); or JobHandle.CompleteAll(jobHandles). Once the job is complete, the NativeContainer that we used to set up the job will have all the data that we need to use. Once we retrieve the data from them, we have to properly dispose of them.

The performance-intensive job

This is pretty simple since I am not reading or writing any data to the job. I wait for all of the jobs that were scheduled to be completed then dispose of the Native array:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

The intensive job takes around 6ms to complete with the game running at about 90 FPS.

Intensive Job

The MoveEnemy job

I add the appropriate complete checks:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

After the method type checks, I loop through all of the enemies, setting their transform positions and moveY to the data that was set in the job. Next, I properly dispose of the native arrays:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Displaying and moving a thousand enemies with job took around 160ms with a frame rate of about 7 FPS with no performance gains.

No Performance Gains

Displaying and moving a thousand enemies with job parallel took around 30ms with a frame rate of about 30 FPS.

Job Parallel

What is the burst compiler in Unity?

The burst compiler is a compiler that translates from bytecode to native code. Using this with the C# Job System improves the quality of the code generated, giving you a significant boost in performance as well as reducing the consumption of the battery on mobile devices.

To use this, you just tell Unity that you want to use burst compile on the job with the [BurstCompile] attribute:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Then in Unity, select Jobs > Burst > Enable Completion

Enable Completion

Burst is Just-In-Time (JIT) while in the Editor, meaning that this can be down while in Play Mode. When you build your project it is Ahead-Of-Time (AOT), meaning that this needs to be enabled before building your project. You can do so by editing the Burst AOT Settings section in the Project Settings Window.

Burst AOT Settings

For more details on the burst compiler, see the Unity documentation.

A performance-intensive job with the burst compiler

An intensive job with burst takes around 3ms to complete with the game running at about 150 FPS.

Intensive Job With Burst

Displaying and moving a thousand enemies, the job with burst took around 30ms with a frame rate of about 30 FPS.

Burst 30 ms

Displaying and moving a thousand enemies, the job parallel with burst took around 6ms with a frame rate of about 80 to 90 FPS.

6 ms

Conclusion

We can use Task to increase the performance of our Unity applications, but there are several drawbacks to using them. It is better to use the things that come packaged in Unity depending on what we want to do. Use coroutines if we want to wait for something to finish loading asynchronously; we can start the coroutine and not stop the process of our program from running.

We can use the C# Job System with the burst compiler to get a massive gain in performance while not having to worry about all of the thread management stuff when performing process-intensive tasks. Using the inbuilt systems, we are sure that it is done in a safe manner that does not cause any unwanted errors or bugs.

Tasks did run a little better than the jobs without using the burst compiler, but that is due to the little extra overhead behind the scenes to set everything up safely for us. When using the burst compiler, our jobs performed our tasks. When you need all of the extra performance that you can get, use the C# Job System with burst.

The project files for this can be found on my GitHub.

Source: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

#csharp #async #await 

What Async, Await, and Task are in C#

Qué son Async, Await y Task en C#

El rendimiento lo es todo cuando intenta publicar en la web, dispositivos móviles, consolas e incluso algunas de las PC de gama baja. Un juego o aplicación que se ejecuta a menos de 30 FPS puede causar frustración a los usuarios. Echemos un vistazo a algunas de las cosas que podemos usar para aumentar el rendimiento al reducir la carga en la CPU.

En esta publicación, cubriremos qué son async, awaity Tasken C# y cómo usarlos en Unity para obtener rendimiento en su proyecto. A continuación, veremos algunos de los paquetes integrados de Unity: rutinas, el sistema de trabajo de C# y el compilador de ráfagas. Veremos qué son, cómo usarlos y cómo aumentan el rendimiento en su proyecto.

Para comenzar este proyecto, usaré Unity 2021.3.4f1. No he probado este código en ninguna otra versión de Unity; todos los conceptos aquí deberían funcionar en cualquier versión de Unity posterior a Unity 2019.3. Sus resultados de rendimiento pueden diferir si usa una versión anterior, ya que Unity realizó algunas mejoras significativas con el modelo de programación async/await en 2021. Obtenga más información al respecto en el blog de Unity Unity and .NET, what's next , en particular, la sección denominada "Modernizing the Tiempo de ejecución de la unidad”.

Creé un nuevo proyecto Core 2D (URP), pero puede usarlo en cualquier tipo de proyecto que desee.

Tengo un sprite que obtuve de Space Shooter (Redux, además de fuentes y sonidos) de Kenney Vleugels .

Creé un prefabricado enemigo que contiene un Sprite Render y un Enemy Component. El Enemy Component es un MonoBehaviourque tiene a Transformy a floatpara realizar un seguimiento de la posición y la velocidad para moverse en el eje y:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

Qué async, awaity Taskson en C#

¿Qué es async?

En C#, los métodos pueden tener una asyncpalabra clave delante de ellos, lo que significa que los métodos son métodos asincrónicos. Esta es solo una forma de decirle al compilador que queremos poder ejecutar el código interno y permitir que la persona que llama a ese método continúe con la ejecución mientras espera que finalice este método.

Un ejemplo de esto sería cocinar una comida. Comenzarás a cocinar la carne, y mientras la carne se cocina y esperas a que termine, comenzarás a hacer los lados. Mientras se cocina la comida, empezarías a poner la mesa. Un ejemplo de esto en código sería static async Task<Steak> MakeSteak(int number).

Unity también tiene todo tipo de métodos incorporados a los que puede llamar de forma asíncrona; consulte los documentos de Unity para obtener una lista de métodos. Con la forma en que Unity maneja la administración de la memoria, utiliza corrutinas oAsyncOperation el sistema de trabajo de C# .

¿Qué es awaity cómo se usa?

En C#, puede esperar a que se complete una operación asíncrona usando la awaitpalabra clave. Esto se usa dentro de cualquier método que tenga la asyncpalabra clave para esperar a que continúe una operación:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Consulte los documentos de Microsoft para obtener más información sobre await.

¿Qué es un Tasky cómo se usa?

A Taskes un método asíncrono que realiza una única operación y no devuelve ningún valor. Para a Taskque devuelve un valor, usaríamos Task<TResult>.

Para usar una tarea, la creamos como crear cualquier objeto nuevo en C#: Task t1 = new Task(void Action). A continuación, comenzamos la tarea t1.wait. Por último, esperamos a que la tarea se complete con t1.wait.

Hay varias formas de crear, iniciar y ejecutar tareas. Task t2 = Task.Run(void Action)creará y comenzará una tarea. await Task.Run(void Action)creará, iniciará y esperará a que se complete la tarea. Podemos usar la forma alternativa más común con Task t3 = Task.Factory.Start(void Action).

Hay varias formas en que podemos esperar a que se complete la tarea. int index = Task.WaitAny(Task[])esperará a que se complete cualquier tarea y nos dará el índice de la tarea completada en la matriz. await Task.WaitAll(Task[])esperará a que se completen todas las tareas.

Para obtener más información sobre las tareas, consulte los documentos de Microsoft .

un ejemplo sencillotask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Cómo afecta la tarea al rendimiento

Ahora comparemos el rendimiento de una tarea con el rendimiento de un método.

Necesitaré una clase estática que pueda usar en todas mis comprobaciones de rendimiento. Tendrá un método y una tarea que simule una operación intensiva en rendimiento. Tanto el método como la tarea realizan exactamente la misma operación:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Ahora necesito uno MonoBehaviourque pueda usar para probar el impacto en el rendimiento de la tarea y el método. Solo para poder ver un mejor impacto en el rendimiento, fingiré que quiero ejecutar esto en diez objetos de juego diferentes. También realizaré un seguimiento de la cantidad de tiempo Updateque tarda en ejecutarse el método.

En Update, obtengo la hora de inicio. Si estoy probando el método, recorro todos los objetos del juego simulado y llamo al método intensivo en rendimiento. Si estoy probando la tarea, creo un nuevo Taskbucle de matriz a través de todos los objetos del juego simulado y agrego la tarea de rendimiento intensivo a la matriz de tareas. Entonces awaitpara todas las tareas para completar. Fuera de la verificación del tipo de método, actualizo el tiempo del método, convirtiéndolo a ms. También lo registro.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

El método intensivo tarda alrededor de 65 ms en completarse y el juego se ejecuta a unos 12 FPS.

La tarea intensiva tarda alrededor de 4 ms en completarse y el juego se ejecuta a unos 200 FPS.

Intentemos esto con mil enemigos:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Mostrar y mover mil enemigos con el método tomó alrededor de 150 ms con una velocidad de cuadro de aproximadamente 7 FPS.

Mostrar y mover mil enemigos con una tarea tomó alrededor de 50 ms con una velocidad de cuadro de aproximadamente 30 FPS.

¿Por qué no useTasks?

Las tareas son extremadamente eficientes y reducen la tensión en el rendimiento de su sistema. Incluso puede usarlos en múltiples subprocesos usando la Biblioteca paralela de tareas (TPL).

Sin embargo, existen algunos inconvenientes al usarlos en Unity. El principal inconveniente de usar TaskUnity es que todos se ejecutan en el Mainsubproceso. Sí, podemos hacer que se ejecuten en otros subprocesos, pero Unity ya realiza su propia gestión de subprocesos y memoria, y puede crear errores al crear más subprocesos que núcleos de CPU, lo que genera competencia por los recursos.

Las tareas también pueden ser difíciles de realizar correctamente y depurar. Al escribir el código original, terminé con todas las tareas ejecutándose, pero ninguno de los enemigos se movió en la pantalla. Terminó siendo que necesitaba devolver el Task[]que creé en el archivo Task.

Las tareas crean mucha basura que afecta el rendimiento. Tampoco aparecen en el generador de perfiles, por lo que si tiene uno que afecta el rendimiento, es difícil rastrearlo. Además, he notado que a veces mis tareas y funciones de actualización continúan ejecutándose desde otras escenas.

Corrutinas de Unity

Según Unity , "una corrutina es una función que puede suspender su ejecución (rendimiento) hasta que finalice la instrucción de rendimiento dada " .

Lo que esto significa es que podemos ejecutar código y esperar a que se complete una tarea antes de continuar. Esto es muy parecido a un método asíncrono. Utiliza un tipo de retorno IEnumeratory we yield returnen lugar de await.

Unity tiene varios tipos diferentes de instrucciones de rendimiento que podemos usar, es decir, WaitForSeconds, WaitForEndOfFrame, WaitUntilo WaitWhile.

Para iniciar las corrutinas, necesitamos MonoBehavioury usamos el MonoBehaviour.StartCoroutine.

Para detener una rutina antes de que se complete, usamos MonoBehaviour.StopCoroutine. Al detener las rutinas, asegúrese de utilizar el mismo método que utilizó para iniciarlas.

Los casos de uso comunes para corrutinas en Unity son esperar a que se carguen los activos y crear temporizadores de enfriamiento.

Ejemplo: un cargador de escenas usando una corrutina

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Comprobar el impacto de una rutina en el rendimiento

Veamos cómo el uso de una corrutina afecta el rendimiento de nuestro proyecto. Solo voy a hacer esto con el método intensivo en rendimiento.

Agregué Coroutinea la MethodTypeenumeración y las variables para realizar un seguimiento de su estado:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

Creé la rutina. Esto es similar a la tarea y el método de alto rendimiento que creamos anteriormente con código agregado para actualizar el tiempo del método:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

En el Updatemétodo, agregué el cheque para la rutina. También modifiqué el tiempo del método, actualicé el código y agregué código para detener la rutina si se estaba ejecutando y cambiamos el tipo de método:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

La rutina intensiva tarda alrededor de 6 ms en completarse y el juego se ejecuta a unos 90 FPS.

El sistema de trabajos de C# y el compilador de ráfagas

¿Qué es el sistema de trabajo de C#?

El sistema de trabajo de C# es la implementación de Unity de tareas que son fáciles de escribir, no generan la basura que generan las tareas y utilizan los subprocesos de trabajo que Unity ya ha creado. Esto corrige todas las desventajas de las tareas.

Unity compara los trabajos como subprocesos, pero dicen que un trabajo realiza una tarea específica. Los trabajos también pueden depender de otros trabajos para completarse antes de ejecutarse; esto soluciona el problema con la tarea que tenía que no se movió correctamente Unitsporque dependía de que otra tarea se completara primero.

Unity se ocupa automáticamente de las dependencias laborales. El sistema de trabajo también tiene un sistema de seguridad incorporado principalmente para proteger contra las condiciones de carrera . Una advertencia con los trabajos es que solo pueden contener variables miembro que sean tipos blittables o tipos NativeContainer ; esto es un inconveniente del sistema de seguridad.

Para usar el sistema de trabajos, cree el trabajo, programe el trabajo, espere a que se complete y luego use los datos devueltos por el trabajo. El sistema de trabajo es necesario para usar la pila de tecnología orientada a datos (DOTS) de Unity.

Para obtener más detalles sobre el sistema de trabajo, consulte la documentación de Unity .

Creando un trabajo

Para crear un trabajo, cree uno stuctque implemente una de las IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobes un trabajo básico. IJobFory IJobForParallelse utilizan para realizar la misma operación en cada elemento de un contenedor nativo o para varias iteraciones. La diferencia entre ellos es que IJobFor se ejecuta en un solo subproceso donde IJobForParallelse dividirá entre varios subprocesos.

Lo usaré IJobpara crear un trabajo de operación intensiva IJobFory IJobForParallelpara crear un trabajo que moverá a múltiples enemigos; esto es solo para que podamos ver los diferentes impactos en el rendimiento. Estos trabajos serán idénticos a las tareas y métodos que creamos anteriormente:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Agregue las variables miembro. En mi caso, mi IJobno necesita ninguno. Los IJobFory IJobParallelFornecesitan un valor flotante para el tiempo delta actual, ya que los trabajos no tienen un concepto de marco; operan fuera de Unity MonoBehaviour. También necesitan una matriz float3para la posición y una matriz para la velocidad de movimiento en el eje y:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

El último paso es implementar el Executemétodo requerido. Los IJobFory IJobForParallelrequieren un intpara el índice de la iteración actual que se está ejecutando el trabajo.

La diferencia es que en lugar de acceder a los movimientos del enemigo transform, usamos la matriz que está en el trabajo:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Programar un trabajo

Primero, necesitamos instalar el trabajo y completar los datos del trabajo:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Luego programamos el trabajo con JobHandle jobHandle = jobData.Schedule();. El Schedulemétodo devuelve un JobHandleque se puede utilizar más adelante.

No podemos programar un trabajo desde dentro de un trabajo. Sin embargo, podemos crear nuevos trabajos y completar sus datos desde dentro de un trabajo. Una vez que se ha programado un trabajo, no se puede interrumpir.

El trabajo intensivo en rendimiento

Creé un método que crea un nuevo trabajo y lo programa. Devuelve el identificador de trabajo que puedo usar en mi updatemétodo:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

Agregué el trabajo a mi enumeración. Luego, en el Updatemétodo, agrego el casea la switchsección. Creé una matriz de JobHandles. Luego recorro todos los objetos del juego simulado y agrego un trabajo programado para cada uno a la matriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

el MoveEnemyyMoveEnemyParallelJob

A continuación, agregué los trabajos a mi enumeración. Luego, en el Updatemétodo, llamo a un nuevo MoveEnemyJobmétodo, pasando el tiempo delta. Normalmente usaría el JobForo el JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

Lo primero que hago es establecer una matriz para las posiciones y una matriz para las moveYque pasaré a los trabajos. Luego lleno estas matrices con los datos de los enemigos. A continuación, creo el trabajo y configuro los datos del trabajo según el trabajo que quiero usar. Después de eso, programo el trabajo según el trabajo que quiero usar y el tipo de programación que quiero hacer:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Recuperar los datos de un trabajo

Tenemos que esperar a que se complete el trabajo. Podemos obtener el estado del JobHandleque usamos cuando programamos el trabajo para completarlo. Esto esperará a que se complete el trabajo antes de continuar con la ejecución: > handle.Complete();o JobHandle.CompleteAll(jobHandles). Una vez que se completa el trabajo, el NativeContainerque usamos para configurar el trabajo tendrá todos los datos que necesitamos usar. Una vez que recuperamos los datos de ellos, tenemos que desecharlos adecuadamente.

El trabajo intensivo en rendimiento

Esto es bastante simple ya que no estoy leyendo ni escribiendo ningún dato en el trabajo. Espero a que se completen todos los trabajos que estaban programados y luego me deshago de la Nativematriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

El trabajo intensivo tarda alrededor de 6 ms en completarse y el juego se ejecuta a unos 90 FPS.

el MoveEnemytrabajo

Agrego los cheques completos apropiados:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Después de las comprobaciones del tipo de método, recorro a todos los enemigos, establezco sus transformposiciones y moveYlos datos que se establecieron en el trabajo. A continuación, me deshago adecuadamente de las matrices nativas:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Mostrar y mover mil enemigos con el trabajo tomó alrededor de 160 ms con una velocidad de cuadro de aproximadamente 7 FPS sin ganancias de rendimiento.

Mostrar y mover mil enemigos con el trabajo en paralelo tomó alrededor de 30 ms con una velocidad de cuadro de aproximadamente 30 FPS.

¿Qué es el compilador de ráfagas en Unity?

El compilador de ráfagas es un compilador que traduce de bytecode a código nativo. Usar esto con C# Job System mejora la calidad del código generado, lo que le brinda un aumento significativo en el rendimiento y reduce el consumo de batería en los dispositivos móviles.

Para usar esto, simplemente dígale a Unity que desea usar la compilación en ráfaga en el trabajo con el [BurstCompile]atributo:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Luego, en Unity, seleccione Trabajos > Ráfaga > Habilitar finalización

Burst es Just-In-Time (JIT) mientras está en el Editor, lo que significa que esto puede estar inactivo mientras está en Modo de reproducción. Cuando construyes tu proyecto es Ahead-Of-Time (AOT), lo que significa que esto debe habilitarse antes de construir tu proyecto. Puede hacerlo editando la sección Configuración de AOT de ráfaga en la ventana Configuración del proyecto .

Para obtener más detalles sobre el compilador de ráfagas, consulte la documentación de Unity .

Un trabajo de alto rendimiento con el compilador de ráfagas

Un trabajo intensivo con ráfaga tarda alrededor de 3 ms en completarse y el juego se ejecuta a unos 150 FPS.

Mostrando y moviendo mil enemigos, el trabajo con ráfaga tomó alrededor de 30 ms con una velocidad de cuadro de aproximadamente 30 FPS.

Mostrando y moviendo mil enemigos, el trabajo paralelo con la ráfaga tomó alrededor de 6 ms con una velocidad de cuadro de aproximadamente 80 a 90 FPS.

Conclusión

Podemos utilizar Taskpara aumentar el rendimiento de nuestras aplicaciones de Unity, pero existen varios inconvenientes al usarlos. Es mejor usar las cosas que vienen empaquetadas en Unity dependiendo de lo que queramos hacer. Usar corrutinas si queremos esperar a que algo termine de cargarse de forma asíncrona; podemos iniciar la rutina y no detener la ejecución del proceso de nuestro programa.

Podemos usar el sistema de trabajo de C# con el compilador de ráfagas para obtener una ganancia masiva en el rendimiento sin tener que preocuparnos por todas las cuestiones de administración de subprocesos cuando se realizan tareas de procesos intensivos. Usando los sistemas incorporados, estamos seguros de que se hace de una manera segura que no causa errores o errores no deseados.

Las tareas se ejecutaron un poco mejor que los trabajos sin usar el compilador de ráfagas, pero eso se debe a la pequeña sobrecarga adicional detrás de escena para configurar todo de manera segura para nosotros. Al usar el compilador de ráfagas, nuestros trabajos realizaron nuestras tareas. Cuando necesite todo el rendimiento adicional que puede obtener, use el sistema de trabajo de C# con ráfaga.

Los archivos del proyecto para esto se pueden encontrar en mi GitHub .

Fuente: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/ 

#csharp #async #await 

Qué son Async, Await y Task en C#

C#の非同期、待機、タスクとは?

Web、モバイル、コンソール、さらには一部のローエンドPCに公開しようとする場合、パフォーマンスがすべてです。30 FPS未満で実行されているゲームまたはアプリケーションは、ユーザーにフラストレーションを引き起こす可能性があります。CPUの負荷を減らしてパフォーマンスを向上させるために使用できるもののいくつかを見てみましょう。

この投稿では、、、、およびC#とは何かasyncawaitおよびTaskUnityでそれらを使用してプロジェクトのパフォーマンスを向上させる方法について説明します。次に、Unityに組み込まれているパッケージのいくつかを見ていきます。コルーチン、C#ジョブシステム、バーストコンパイラです。それらが何であるか、それらをどのように使用するか、そしてそれらがどのようにプロジェクトのパフォーマンスを向上させるかを見ていきます。

このプロジェクトを開始するには、Unity2021.3.4f1を使用します。私はこのコードを他のバージョンのUnityでテストしていません。ここでのすべての概念は、Unity2019.3以降のすべてのUnityバージョンで機能するはずです。Unityが2021年にasync/awaitプログラミングモデルで大幅な改善を行ったため、古いバージョンを使用すると、パフォーマンスの結果が異なる場合があります。UnityのブログUnity and .NET、次の記事、特に「Modernizing the Unityランタイム。」

新しい2D(URP)コアプロジェクトを作成しましたが、これは任意のタイプのプロジェクトで使用できます。

KenneyVleugelsのSpaceShooter(Redux、およびフォントとサウンド)から入手したスプライトがあります。

スプライトレンダーと敵コンポーネントを含む敵プレハブを作成しました。敵コンポーネントは、y軸上を移動する位置と速度を追跡するためのaとaMonoBehaviourを持つaです。Transformfloat

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

、、、asyncおよびC#のawait内容Task

asyncですか?

C#では、メソッドasyncの前にキーワードを付けることができます。つまり、メソッドは非同期メソッドです。これは、内部でコードを実行し、そのメソッドの呼び出し元がこのメソッドの終了を待つ間、実行を継続できるようにすることをコンパイラーに通知する方法にすぎません。

この例は、食事の調理です。あなたは肉を調理し始めます、そしてあなたは肉が調理されてそしてそれが終わるのを待っている間に、あなたは側面を作り始めるでしょう。食べ物が調理されている間、あなたはテーブルをセットし始めるでしょう。コードでのこの例はですstatic async Task<Steak> MakeSteak(int number)

Unityには、非同期で呼び出すことができるあらゆる種類の組み込みメソッドもあります。メソッドのリストについては、Unityのドキュメントを参照してください。Unityがメモリ管理を処理する方法では、コルーチン、、、またはAsyncOperationC #ジョブシステムのいずれかを使用します。

それは何でawait、どのように使用しますか?

awaitC#では、キーワードを使用して非同期操作が完了するのを待つことができます。asyncこれは、操作の続行を待機するキーワードを持つメソッド内で使用されます。

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

詳細については、Microsoftのドキュメントを参照してくださいawait

とは何ですかTask、どのように使用しますか?

ATaskは、単一の操作を実行し、値を返さない非同期メソッドです。Task値を返すaには、を使用しますTask<TResult>

タスクを使用するには、C#で新しいオブジェクトを作成するのと同じようにタスクを作成しますTask t1 = new Task(void Action)。次に、タスクを開始しますt1.wait。最後に、タスクがで完了するのを待ちますt1.wait

タスクを作成、開始、および実行するには、いくつかの方法があります。Task t2 = Task.Run(void Action)タスクを作成して開始します。await Task.Run(void Action)タスクを作成、開始、および完了するのを待ちます。で最も一般的な代替方法を使用できますTask t3 = Task.Factory.Start(void Action)

タスクが完了するのを待つ方法はいくつかあります。int index = Task.WaitAny(Task[])タスクが完了するのを待ち、配列内の完了したタスクのインデックスを提供します。await Task.WaitAll(Task[])すべてのタスクが完了するのを待ちます。

タスクの詳細については、Microsoftドキュメントを参照してください。

簡単なtask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

タスクがパフォーマンスに与える影響

次に、タスクのパフォーマンスとメソッドのパフォーマンスを比較してみましょう。

すべてのパフォーマンスチェックで使用できる静的クラスが必要になります。パフォーマンスを重視する操作をシミュレートするメソッドとタスクがあります。メソッドとタスクの両方がまったく同じ操作を実行します。

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

ここMonoBehaviourで、タスクとメソッドに対するパフォーマンスの影響をテストするために使用できるが必要です。パフォーマンスへのより良い影響を確認できるように、これを10個の異なるゲームオブジェクトで実行したいふりをします。Updateまた、メソッドの実行にかかる時間を追跡します。

Update、開始時刻を取得します。メソッドをテストしている場合は、シミュレートされたすべてのゲームオブジェクトをループして、パフォーマンスを重視するメソッドを呼び出します。タスクをテストしている場合は、シミュレートされたすべてのゲームオブジェクトを介して新しいTask配列ループを作成し、パフォーマンスを重視するタスクをタスク配列に追加します。次にawait、すべてのタスクを完了します。メソッドタイプチェックの外で、メソッド時間を更新して、に変換しmsます。私もそれを記録します。

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

集中的な方法では、ゲームが約12FPSで実行されるまでに約65ミリ秒かかります。

集中的なタスクは、約200FPSで実行されているゲームで完了するのに約4msかかります。

千人の敵でこれを試してみましょう:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

この方法で1000人の敵を表示して移動するには、フレームレートが約7FPSで約150ミリ秒かかりました。

タスクで1,000人の敵を表示して移動するには、フレームレートが約30FPSで約50ミリ秒かかりました。

どうしてuseTasks

タスクは非常に効率的であり、システムのパフォーマンスへの負担を軽減します。タスク並列ライブラリ(TPL)を使用して、複数のスレッドでそれらを使用することもできます。

ただし、Unityでそれらを使用することにはいくつかの欠点があります。TaskUnityで使用する場合の主な欠点は、すべてがMainスレッド上で実行されることです。はい、他のスレッドで実行することはできますが、Unityはすでに独自のスレッドとメモリの管理を行っており、CPUコアよりも多くのスレッドを作成することでエラーが発生する可能性があり、リソースの競合が発生します。

タスクを正しく実行してデバッグすることも難しい場合があります。元のコードを書いているとき、私はすべてのタスクを実行することになりましたが、どの敵も画面上を移動しませんでした。Task[]で作成したものを返す必要がありましたTask

タスクは、パフォーマンスに影響を与える多くのガベージを作成します。また、プロファイラーには表示されないため、パフォーマンスに影響を与えているプロファイラーがある場合、追跡するのは困難です。また、タスクや更新機能が他のシーンから実行され続けることがあることに気づきました。

Unityコルーチン

Unityによると、「コルーチンは、指定されたYieldInstructionが終了するまで実行(yield)を一時停止できる関数です。」

これが意味するのは、コードを実行し、タスクが完了するのを待ってから続行できるということです。これは非同期メソッドによく似ています。リターンタイプを使用し、の代わりにIEnumerator私たちを使用します。yield returnawait

Unityには、使用できるいくつかの異なるタイプのyield命令あります。つまり、、、、、またはです。WaitForSecondsWaitForEndOfFrameWaitUntilWaitWhile

コルーチンを開始するには、が必要で、MonoBehaviourを使用しMonoBehaviour.StartCoroutineます。

コルーチンが完了する前に停止するには、を使用しますMonoBehaviour.StopCoroutine。コルーチンを停止するときは、開始時に使用したのと同じ方法を使用するようにしてください。

Unityのコルーチンの一般的な使用例は、アセットが読み込まれるのを待って、クールダウンタイマーを作成することです。

例:コルーチンを使用するシーンローダー

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

コルーチンがパフォーマンスに与える影響を確認する

コルーチンの使用がプロジェクトのパフォーマンスにどのように影響するかを見てみましょう。これは、パフォーマンスを重視する方法でのみ行います。

列挙型と変数にを追加しCoroutineて、MethodTypeその状態を追跡します。

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

コルーチンを作成しました。これは、メソッド時間を更新するためのコードを追加して以前に作成した、パフォーマンスを重視するタスクとメソッドに似ています。

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

このUpdate方法では、コルーチンのチェックを追加しました。また、メソッド時間を変更し、コードを更新し、コルーチンが実行されている場合にコルーチンを停止するコードを追加し、メソッドタイプを変更しました。

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

集中的なコルーチンは、約90FPSで実行されているゲームで完了するのに約6msかかります。

C#ジョブシステムとバーストコンパイラ

C#ジョブシステムとは何ですか?

C#ジョブシステムは、作成が簡単で、タスクが行うガベージを生成せず、Unityが既に作成したワーカースレッドを利用するUnityのタスクの実装です。これにより、タスクのすべての欠点が修正されます。

Unityはジョブをスレッドとして比較しますが、ジョブは1つの特定のタスクを実行すると言っています。ジョブは、実行前に完了する他のジョブに依存することもあります。Unitsこれにより、最初に完了するのは別のタスクに依存していたため、適切に移動しなかったタスクの問題が修正されます。

仕事の依存関係はUnityによって自動的に処理されます。ジョブシステムには、主に競合状態から保護するための安全システムも組み込まれています。ジョブに関する注意点の1つは、 blittableタイプまたはNativeContainerタイプのいずれかのメンバー変数のみを含めることができることです。これは安全システムの欠点です。

ジョブシステムを使用するには、ジョブを作成し、ジョブをスケジュールし、ジョブが完了するのを待ってから、ジョブから返されたデータを使用します。UnityのData-OrientedTechnologyStack(DOTS)を使用するには、ジョブシステムが必要です。

ジョブシステムの詳細については、Unityのドキュメントを参照してください。

ジョブの作成

ジョブを作成するには、インターフェイスの1つ(IJobIJobForIJobParallelForUnity.Engine.Jobs.IJobParallelForTransformstuct )を実装するを作成します。基本的な仕事です。およびは、ネイティブコンテナの各要素に対して、または複数の反復に対して同じ操作を実行するために使用されます。それらの違いは、IJobForが単一のスレッドで実行され、複数のスレッドに分割されることです。IJobIJobIJobForIJobForParallelIJobForParallel

IJob集中的な操作ジョブIJobForIJobForParallel作成し、複数の敵を移動させるジョブを作成するために使用します。これは、パフォーマンスへのさまざまな影響を確認できるようにするためです。これらのジョブは、前に作成したタスクとメソッドと同じになります。

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

メンバー変数を追加します。私の場合、私IJobは何も必要ありません。IJobForジョブにはフレームのIJobParallelFor概念がないため、現在のデルタ時間にはフロートが必要です。Unityの外部で動作しMonoBehaviourます。float3また、位置の配列とy軸の移動速度の配列も必要です。

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

最後のステップは、必要なExecuteメソッドを実装することです。IJobForIJobForParallel両方は、ジョブが実行している現在の反復のインデックスにを必要としますint

違いは、敵にアクセスしてtransform移動する代わりに、ジョブにある配列を使用することです。

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

仕事のスケジュール

まず、ジョブを復元し、ジョブデータを入力する必要があります。

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

次に、でジョブをスケジュールしJobHandle jobHandle = jobData.Schedule();ます。このScheduleメソッドは、JobHandle後で使用できるを返します。

ジョブ内からジョブをスケジュールすることはできません。ただし、新しいジョブを作成し、ジョブ内からそれらのデータを入力することはできます。ジョブがスケジュールされると、中断することはできません。

パフォーマンスを重視する仕事

新しいジョブを作成してスケジュールするメソッドを作成しました。updateメソッドで使用できるジョブハンドルを返します。

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

列挙型にジョブを追加しました。次に、メソッドで、セクションUpdateにを追加します。の配列を作成しました。次に、シミュレートされたすべてのゲームオブジェクトをループし、それぞれのスケジュールされたジョブを配列に追加します。caseswitchJobHandles

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

と_MoveEnemyMoveEnemyParallelJob

次に、列挙型にジョブを追加しました。次に、メソッドで、デルタ時間を渡してUpdate新しいメソッドを呼び出します。MoveEnemyJob通常は、JobForまたはJobParallelFor:のいずれかを使用します。

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

私が最初に行うことは、位置の配列とmoveY、ジョブに渡す配列を設定することです。次に、これらの配列を敵からのデータで埋めます。次に、ジョブを作成し、使用するジョブに応じてジョブのデータを設定します。その後、使用するジョブと実行するスケジューリングのタイプに応じて、ジョブをスケジュールします。

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

ジョブからデータを取り戻す

私たちは仕事が完了するのを待たなければなりません。JobHandleジョブを完了するようにスケジュールしたときに使用したステータスからステータスを取得できます。これにより、ジョブが完了するのを待ってから実行を続行します:>handle.Complete();またはJobHandle.CompleteAll(jobHandles)。ジョブが完了するとNativeContainer、ジョブの設定に使用したデータに、使用する必要のあるすべてのデータが含まれます。それらからデータを取得したら、それらを適切に破棄する必要があります。

パフォーマンスを重視する仕事

私はジョブにデータを読み書きしていないので、これは非常に簡単です。スケジュールされたすべてのジョブが完了するのを待ってから、Nativeアレイを破棄します。

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

集中的なジョブは、約90FPSで実行されているゲームで完了するのに約6ミリ秒かかります。

仕事_MoveEnemy

適切な完全なチェックを追加します。

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

メソッドタイプがチェックされた後、私はすべての敵をループし、それらのtransform位置とmoveYジョブで設定されたデータに設定します。次に、ネイティブ配列を適切に破棄します。

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

仕事で千人の敵を表示して移動するのに約160msかかり、フレームレートは約7 FPSで、パフォーマンスは向上しませんでした。

並行してジョブを使用して1,000人の敵を表示および移動するには、フレームレートが約30FPSで約30ミリ秒かかりました。

Unityのバーストコンパイラとは何ですか?

バーストコンパイラは、バイトコードからネイティブコードに変換するコンパイラです。これをC#ジョブシステムで使用すると、生成されるコードの品質が向上し、パフォーマンスが大幅に向上するだけでなく、モバイルデバイスのバッテリーの消費量が削減されます。

[BurstCompile]これを使用するには、次の属性を持つジョブでバーストコンパイルを使用することをUnityに指示するだけです。

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

次に、Unityで[ジョブ] >[バースト]>[完了を有効にする]を選択します

バーストは、エディター内ではジャストインタイム(JIT)です。つまり、再生モード中はこれを停止できます。プロジェクトをビルドするときは、Ahead-Of-Time(AOT)です。つまり、プロジェクトをビルドする前に、これを有効にする必要があります。これを行うには、 [プロジェクト設定]ウィンドウの[バーストAOT設定]セクションを編集します。

バーストコンパイラの詳細については、Unityのドキュメントを参照してください。

バーストコンパイラを使用したパフォーマンス集約型のジョブ

バーストを伴う集中的なジョブは、約150FPSで実行されているゲームで完了するのに約3ミリ秒かかります。

1000人の敵を表示して移動すると、バーストを伴うジョブは約30fPSのフレームレートで約30msかかりました。

1000人の敵を表示して移動させると、バーストと並行した作業に約6ミリ秒かかり、フレームレートは約80〜90FPSでした。

結論

TaskUnityアプリケーションのパフォーマンスを向上させるために使用できますが、それらを使用することにはいくつかの欠点があります。やりたいことに応じて、Unityにパッケージ化されているものを使用することをお勧めします。何かが非同期でロードを完了するのを待ちたい場合は、コルーチンを使用します。コルーチンを開始することはできますが、プログラムの実行を停止することはできません。

バーストコンパイラでC#ジョブシステムを使用すると、プロセスを多用するタスクを実行するときにスレッド管理のすべてを気にすることなく、パフォーマンスを大幅に向上させることができます。内蔵システムを使用して、不要なエラーやバグを引き起こさない安全な方法で実行されることを確信しています。

タスクはバーストコンパイラを使用せずにジョブよりも少し良く実行されましたが、それは私たちのためにすべてを安全にセットアップするための舞台裏での少し余分なオーバーヘッドによるものです。バーストコンパイラを使用する場合、ジョブはタスクを実行しました。取得できるすべての追加のパフォーマンスが必要な場合は、バースト付きのC#ジョブシステムを使用してください。

このためのプロジェクトファイルは私のGitHubにあります。

ソース:https ://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

   #csharp #async #await 

C#の非同期、待機、タスクとは?
Dixie  Wolff

Dixie Wolff

1654604580

How to Write Asynchronous JavaScript using Async/Await

In this visual lesson, we learn how to write asynchronous JavaScript using Async/Await. We will also look at the callbacks and promises and see the drawbacks of each approach. After watching this video, you will be able to answer the below questions:

- What are callbacks and Callback Hell?
- What are promises and Promise Hell?
- What is Async/Await in JavaScript?
- Difference between callbacks, promises, and async/await?
- What are some of the pitfalls to avoid when using async/await?

#javascript #async #await 

How to Write Asynchronous JavaScript using Async/Await
Code  JS

Code JS

1654326531

JavaScript Fetch API Explained | Callbacks, Promises, Async Await

Learn about Callbacks, Promises, and Async Await as the JavaScript Fetch API is explained in this tutorial. You will also learn about thenables and how async / await replaces them in our JS code. The first 30 minutes covers the concepts. The last 30 minutes gives examples of retrieving data from different APIs with Fetch.

Quick Concepts outline:
Fetch API with Async / Await
(0:00) Intro
(0:29) What is a callback function?
(1:15) What is the problem with callbacks?
(3:00) JavaScript Promises have 3 states
(5:28) A promise may not return a value where you expect it to: You need to wait for a promise to resolve
(6:58) Using thenables with a promise
(20:15) An easy mistake to make with promises
(24:00) Creating an async function
(25:00) Applying await inside the function
(33:45) Example 1: Retrieving user data
(40:00) Example 2: Retrieving dad jokes
(47:00) Example 3: Posting data
(49:40) Example 4: Retrieving data with URL parameters
(54:55) Abstract it all into single responsibility functions

Subscribe: https://www.youtube.com/c/DaveGrayTeachesCode/featured 

#async  #await  #fetch #javascript 

JavaScript Fetch API Explained | Callbacks, Promises, Async Await
Bulah  Pfeffer

Bulah Pfeffer

1648873833

A Collection of Flutter and Dart Tips and Tricks

Table of Contents

  • LazyStream in Flutter and Dart
  • Cancelable APIs in Flutter
  • Asset Data in Flutter
  • API Caching in Flutter
  • FutureGroup in Dart
  • Flatten Iterable<bool> in Dart
  • Caching Temp Files in Flutter
  • Custom Lists in Dart
  • Optional Chaining in Dart
  • MapList in Flutter
  • Future<bool> in Flutter
  • Async Bloc Init in Flutter
  • Firebase Auth Errors in Flutter
  • Debug Strings in Flutter
  • Keyboard Appearance in Flutter
  • Get String Data in Dart
  • Stream.startWith in Flutter
  • Optional Functions in Dart
  • AnnotatedRegion in Flutter
  • Unordered Map Equality in Dart
  • Iterable to ListView in Flutter
  • Password Mask in Flutter
  • Fast Object.toString() in Dart
  • Copying Bloc State in Flutter
  • Iterable Subscripts in Dart
  • useState in Flutter Hooks
  • Folding Iterables in Dart
  • Custom Iterables in Dart
  • Class Clusters in Dart
  • Iterable +/- in Dart
  • Periodic Streams in Dart
  • EmptyOnError in Dart
  • Stream<T> Initial Value in Flutter
  • Double.normalize in Dart
  • Hide Sensitive Information in Flutter
  • Iterable.compactMap in Dart
  • useEffect in Flutter Hooks
  • Merging Streams in Dart
  • Isolate Stream in Dart
  • Network Image Retry in Flutter
  • Reusable APIs in Flutter
  • ListTile Shadow in Flutter
  • Transparent AppBar in Flutter
  • Constructors on Abstract Classes in Dart
  • @useResult in Dart
  • @mustCallSuper in Dart
  • Object.hash in Dart
  • Expanded Equally in Flutter
  • Random Iterable Value in Dart
  • Hardcoded Strings in Flutter
  • Manually Scroll in List View in Flutter
  • AsyncSnapshot to Widget in Flutter
  • Breadcrumbs in Flutter
  • Unique Map Values in Dart
  • Smart Quotes/Dashes in Flutter
  • Haptic Feedback in Flutter
  • Localization Delegates in Flutter
  • Extending Functions in Dart
  • Paginated ListView in Flutter
  • Immutable Classes in Dart
  • Card Widget in Flutter
  • List Equality Ignoring Ordering in Dart
  • Shorten GitHub URLs in Dart
  • Time Picker in Flutter
  • Throttled Print in Flutter
  • Map Equality in Dart
  • Unique Maps in Dart
  • Raw Auto Complete in Flutter
  • Title on Object in Dart
  • Compute in Flutter
  • Filter on Map in Dart
  • Type Alias in Dart
  • ValueNotifier in Flutter
  • Object to Integer in Dart
  • Image Opacity in Flutter
  • Covariant in Dart
  • Custom Errors in Streams in Dart
  • Shake Animation in Flutter
  • Throw Enums in Dart
  • Future Error Test in Flutter
  • Generic URL Retrieval in Dart
  • Custom Error Widget in Flutter
  • Handle Multiple Future Errors in Dart
  • Future Error Handling in Dart
  • String to Toast in Flutter
  • Waiting in Dart
  • Loading Dialog in Flutter
  • Compact Map on Map<K,V> in Dart
  • Query Parameters in Dart
  • Multiple Gradients in Container in Flutter
  • Filter on Stream<List<T>> in Dart
  • Generic Route Arguments in Flutter
  • Generic Dialog in Flutter
  • GitHub API in Flutter
  • ChangeNotifier in Flutter
  • Refresh Indicator in Flutter
  • FlatMap in Dart
  • OrientationBuilder in Flutter
  • Linear Gradient in Flutter
  • Bloc Text Editing Controller in Flutter
  • Blurred TabBar in Flutter
  • Play YouTube in Flutter
  • ListView Background in Flutter
  • Integer to Binary in Dart
  • Split String by Length in Dart
  • Image Tint in Flutter
  • SlideTransition in Flutter
  • Expansion Panels and Lists in Flutter
  • Complete CRUD App in Flutter
  • SQLite Storage in Flutter
  • Circular Progress with Percentage in Flutter
  • Opening URLs in Flutter
  • Commodore 64 Screen in Flutter
  • Animated Lists in Flutter
  • CheckboxListTile in Flutter
  • - Operator on String in Dart
  • Dart Progress for Future<T>
  • Move Widget Shadows with Animation
  • Gallery with Blurred Backgrounds in Flutter
  • Custom Path Clippers in Flutter
  • Frost Effect on Images in Flutter
  • Custom Clippers in Flutter
  • Check if Website is Up or Down in Dart
  • Section Titles on ListView in Flutter
  • Circular Progress in Flutter
  • Displaying Scroll Wheels in Flutter
  • Post Messages to Slack with Dart
  • Unwrap List<T?>? in Dart
  • Avoiding UI Jitters When Switching Widgets in Flutter
  • Detect Redirects in Dart
  • Proportional Constraints in Flutter
  • Displaying Cupertino Action Sheets in Flutter
  • Rotating List<T> in Dart
  • Displaying SnackBars in Flutter
  • Custom Tab Bar Using ToggleButtons in Flutter
  • Hashable Mixins in Dart
  • Flutter Tips and Tricks in Terminal
  • Searching List<List<T>> in Dart
  • Cloning Objects in Dart
  • Color Filters in Flutter
  • Flattening Lists in Dart
  • Managing Duplicates in List<T> in Dart
  • FlatMap and CompactMap in Dart
  • Equality of List<T> in Dart
  • Constants in Dart
  • Displaying Scrollable Bottom Sheets in Flutter
  • YouTube Ad Remover in Dart
  • Fade Between Widgets in Flutter
  • Sort Descriptors in Dart
  • User Sortable Columns and Tables in Flutter
  • Content-Length of List<Uri> in Dart
  • Recursive Dot Notation on Maps in Dart
  • Allow User Selection of Text in Flutter
  • Placing Constraints on Widgets in Flutter
  • Animating Position Changes in Flutter
  • Transitioning Between Widgets in Flutter
  • Doubly Linked Lists in Dart
  • Reordering Items Inside List Views in Flutter
  • Custom Stream Transformers in Dart
  • Expanding Stream Elements in Dart
  • Consume Streams for a Duration in Dart
  • Shortening URLs in Dart
  • LimitedBox Widget as ListView Items in Flutter
  • Generically Convert Anything to Int in Dart
  • Validating URL Certificates in Dart
  • Displaying Popup Menus in Flutter
  • Implementing Drag and Drop in Flutter
  • Dismissing List Items in Flutter
  • Animating Widgets with Ease in Flutter
  • Displaying Tool Tips in Flutter
  • Displaying Assorted Widgets Inside TableView in Flutter
  • Page Indicator with Page View in Flutter
  • Animating and Moving a Floating Action Button in Flutter
  • Fading Network Image Widget in Flutter
  • Transparent Alert Dialogs in Flutter
  • Network Image Size in Dart
  • Animated Icons in Flutter
  • Custom Scroll Views in Flutter
  • Parallax App Bar in Flutter
  • JSON HTTP Requests in Dart
  • URL Timeouts in Dart
  • Detecting URL File Types in Dart
  • Paginated Lists in Dart
  • Requesting DELETE on APIs in Dart
  • Animated Containers in Flutter
  • Hiding Widgets in Flutter
  • Simple Opacity Animation in Flutter
  • Vignette Widget in Flutter
  • Drop Down Button Configuration and Usage in Flutter
  • Expandable List Items in Flutter
  • Infinite Scrolling in Flutter
  • Infinite Arrays in Dart
  • Custom Color Picker Component in Flutter
  • Displaying and Reacting to Switches in Flutter
  • Displaying Bottom Bars in Flutter
  • Displaying Buttons on AppBar in Flutter
  • Displaying Bottom Sheets in Flutter
  • Converting Enums to Radio Buttons in Flutter
  • Check Existence of Websites in Flutter
  • Images inside AlertDialog in Flutter
  • Returning Values from AlertDialog in Flutter
  • Simple Grid View in Flutter
  • Rendering Bullet Points in Flutter
  • Retrying Futures in Flutter
  • Containers as ClipOvals in Flutter
  • Rich Texts in Flutter
  • Wrapping Widgets in Flutter
  • Sweep Gradients in Flutter
  • Stream and StreamBuilder in Flutter
  • Blur Effect in Flutter
  • Convert Enums to Strings in Dart
  • Replacing Text in TextField in Flutter
  • Aspect Ratio in Flutter
  • Zoom and Pan in Flutter
  • Resizing Images in Flutter to Fit Screen Height
  • Validating URLs in Flutter
  • FrameBuilder for Network Images in Flutter
  • Adding Shadow to Icons in Flutter
  • Calculating Median of Lists in Dart
  • Generic Functions with Reduce in Dart
  • Passing Back Data From a Screen to the Previous One in Flutter
  • Flinging an Animation in Flutter
  • Fade Animations in Flutter
  • Throttling User Input in Flutter
  • Censoring TextFields in Flutter
  • Customizing TextButton in Flutter
  • Multiline TextFields in Flutter
  • Filtering TextField Input in Flutter
  • Focusing Manually on TextFields in Flutter
  • Data Streams Over HTTP/HTTPs in Dart
  • Catching Nonexistent Accessors or Methods in Dart
  • Using Expando in Dart
  • Implementing Custom Maps in Dart
  • Dynamically Calling Functions in Dart
  • Factory Constructors in Dart
  • Calculating the Sum of List Items in Dart
  • Removing Duplicate Strings in Lists in Dart (Case-Insensitive)
  • Implementing Range in Dart
  • Converting Lists to Maps in Dart
  • Implementing Hashable in Dart
  • Random Name Generator in Dart
  • Capturing Stack Traces in Dart Exceptions
  • Removing Duplicates from Lists in Dart
  • Optional Spread Operator in Dart
  • Calling Optional Functions in Dart
  • Odd-Even Sort in Dart
  • Implementing Zip and Tuples in Dart
  • Swapping Values in Lists with XOR in Dart
  • Waiting for Multiple Futures in Dart
  • Using Queues as Stacks in Dart
  • Custom Iterators in Dart
  • Iterables as Ranges + Transform in Dart
  • Errors vs Exceptions in Dart
  • Custom Annotations in Dart
  • Classes as Enums in Dart
  • Spread Operator in Collection Literals in Dart
  • StreamBuilder and StreamController in Dart
  • Almost Equal in Dart
  • Enum Associated Values in Dart
  • Implementing Comparable in Dart
  • Implementing Custom Integer Types in Dart
  • Custom Subscripts in Dart
  • Dart List Enumeration with Index
  • Applying Mixins to Other Mixins in Dart
  • Parameter Types in Dart
  • Custom Exceptions in Dart
  • rethrowing Exceptions in Dart
  • mixins and JSON Parsing in Dart
  • mixins vs abstract classes in Dart
  • Drawing Shapes in Flutter with LayoutBuilder, CustomPaint and CustomPainter
  • Generic Type Aliases in Dart
  • Callable Classes in Dart
  • Synchronous Generators in Dart
  • Implicit Interfaces in Dart
  • const Constructors in Dart
  • async-await Over Raw Futures in Dart
  • Initializer List and Default Values as Convenience Intializers in Dart
  • Extract Elements of Certain Type from Lists in Dart
  • Type Promotion in Dart
  • Extract Minimum and Maximum Values in List<num> in Dart
  • Functions as First Class Citizens in Dart

 

LazyStream in Flutter and Dart



import 'dart:developer' as devtools show log;
import 'dart:typed_data' show Uint8List;
import 'package:flutter/services.dart' show NetworkAssetBundle, rootBundle;
import 'package:async/async.dart' show LazyStream;

extension LocalFileData on String {
  Future<Uint8List> localFileData() => rootBundle.load(this).then(
        (byteData) => byteData.buffer.asUint8List(),
      );
}

extension Log on Object {
  void log() => devtools.log(toString());
}

void testIt() async {
  final stream = LazyStream(
    () async {
      final allData = await calculateAllData();
      return getImagesData(allData);
    },
  );

  await for (final data in stream) {
    'Got data, length = ${data.length}'.log();
  }
}

Stream<Uint8List> getImagesData(
  List<Future<Uint8List>> allData,
) async* {
  for (final data in allData) {
    yield await data;
  }
}

Future<List<Future<Uint8List>>> calculateAllData() async {
  final futures = Iterable.generate(
    3,
    (i) => 'images/image_list${i + 1}.txt'
        .localFileData()
        .then((data) => String.fromCharCodes(data)),
  );
  final result = Future.wait(futures);
  final lineSplitter = const LineSplitter();
  List<Future<Uint8List>> allData = [];
  for (final string in await result) {
    final urls = lineSplitter.convert(string);
    for (final url in urls) {
      allData.add(
        NetworkAssetBundle(Uri.parse(url))
            .load(url)
            .then((byteData) => byteData.buffer.asUint8List()),
      );
    }
  }
  return allData;
}

Cancelable APIs in Flutter

import 'dart:developer' as devtools show log;
import 'dart:typed_data' show Uint8List;
import 'package:flutter/services.dart' show NetworkAssetBundle, rootBundle;
import 'package:async/async.dart' show CancelableOperation;

extension Log on Object {
  void log() => devtools.log(toString());
}

extension LocalFileData on String {
  Future<Uint8List> localFileData() => rootBundle.load(this).then(
        (byteData) => byteData.buffer.asUint8List(),
      );
}

CancelableOperation<Uint8List> getImageOperation(String url) =>
    CancelableOperation.fromFuture(
      NetworkAssetBundle(Uri.parse(url))
          .load(url)
          .then((byteData) => byteData.buffer.asUint8List()),
      onCancel: () => 'images/template.png'.localFileData(),
    );

void testIt() async {
  final operation = getImageOperation('http://127.0.0.1:5500/images/1.png');
  final cancelledValue = await operation.cancel();
  final result = await operation.valueOrCancellation(cancelledValue);
  result?.log();
}

Asset Data in Flutter


import 'dart:typed_data' show Uint8List;
import 'package:flutter/services.dart' show rootBundle;
import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

extension LocalFileData on String {
  Future<Uint8List> localFileData() => rootBundle.load(this).then(
        (byteData) => byteData.buffer.asUint8List(),
      );
}

void testIt() async {
  (await 'images/template.png'.localFileData()).log();
}

API Caching in Flutter


import 'dart:typed_data' show Uint8List;
import 'package:flutter/services.dart' show NetworkAssetBundle;
import 'dart:developer' as devtools show log;
import 'package:async/async.dart' show AsyncMemoizer;

extension Log on Object {
  void log() => devtools.log(toString());
}

@immutable
class GetImageApi {
  final String url;
  final _fetch = AsyncMemoizer<Uint8List>();

  GetImageApi({required this.url});
  Future<Uint8List> fetch() => _fetch.runOnce(
        () => NetworkAssetBundle(Uri.parse(url))
            .load(url)
            .then((byteData) => byteData.buffer.asUint8List()),
      );
}

void testIt() async {
  final api = GetImageApi(url: 'http://127.0.0.1:5500/images/1.png');
  (await api.fetch()).log(); // fetched
  (await api.fetch()).log(); // cached
}

FutureGroup in Dart



mixin FutureConvertible<T> {
  Future<T> toFuture();
}

@immutable
class LoginApi with FutureConvertible<bool> {
  @override
  Future<bool> toFuture() => Future.delayed(
        const Duration(seconds: 1),
        () => true,
      );
}

@immutable
class SignUpApi with FutureConvertible<bool> {
  @override
  Future<bool> toFuture() => Future.delayed(
        const Duration(seconds: 1),
        () => true,
      );
}

extension Flatten on Iterable<bool> {
  bool flatten() => fold(
        true,
        (lhs, rhs) => lhs && rhs,
      );
}

extension Log on Object {
  void log() => devtools.log(toString());
}

Future<bool> startup({
  required bool shouldLogin,
  required bool shouldSignUp,
}) {
  final group = FutureGroup<bool>();
  if (shouldLogin) {
    group.add(LoginApi().toFuture());
  }
  if (shouldSignUp) {
    group.add(SignUpApi().toFuture());
  }
  group.close();
  return group.future.then((bools) => bools.flatten());
}

void testIt() async {
  final success = await startup(
    shouldLogin: true,
    shouldSignUp: false,
  );
  success.log();
}

Flatten Iterable<bool> in Dart


extension Flatten on Iterable<bool> {
  bool flatten() => fold(
        true,
        (lhs, rhs) => lhs && rhs,
      );
}

void testIt() {
  assert([true, false, true].flatten() == false);
  assert([true, true, true].flatten() == true);
  assert([false, false, false].flatten() == false);
  assert([true].flatten() == true);
  assert([false].flatten() == false);
}

Caching Temp Files in Flutter



@immutable
class NetworkImageAsset {
  final String localPath;
  final String url;
  NetworkImageAsset({required int index})
      : localPath = Directory.systemTemp.path + '/$index.png',
        url = 'http://127.0.0.1:5500/images/$index}.png';

  Future<bool> downloadAndSave() => NetworkAssetBundle(Uri.parse(url))
      .load(url)
      .then((byteData) => byteData.buffer.asUint8List())
      .then((data) => File(localPath).writeAsBytes(data).then((_) => true))
      .catchError((_) => false);
}

void testIt() async {
  await Future.forEach(
    Iterable.generate(
      3,
      (i) => NetworkImageAsset(index: i + 1),
    ),
    (NetworkImageAsset asset) => asset.downloadAndSave(),
  );
}

Custom Lists in Dart



import 'dart:developer' as devtools show log;
import 'dart:collection' show ListBase;

class LowercaseList extends ListBase<String> {
  final List<String> _list = [];

  @override
  int get length => _list.length;
  @override
  set length(int newLength) => _list.length = newLength;

  @override
  String operator [](int index) => _list[index].toUpperCase();

  @override
  void operator []=(int index, value) => _list[index] = value;

  @override
  void addAll(Iterable<String> iterable) => _list.addAll(iterable);

  @override
  void add(String element) => _list.add(element);
}

extension Log on Object {
  void log() => devtools.log(toString());
}

void testIt() {
  final myList = LowercaseList();
  myList.addAll(['foo', 'bar', 'baz']);
  myList[0].log(); // FOO
  myList[1].log(); // BAR
  for (final item in myList) {
    item.log(); // FOO, BAR, BAZ
  }
}

Optional Chaining in Dart



@immutable
class Address {
  final String? firstLine;
  final String? secondLine;
  const Address(this.firstLine, this.secondLine);
}

@immutable
class Person {
  final Person? father;
  final Address? address;
  const Person(this.father, this.address);
}

extension GetFathersFirstAddressLine on Person {
  String? get firstAddressLineOfFather => father?.address?.firstLine;
}

MapList in Flutter



extension MapToList<T> on Iterable<T> {
  List<E> mapList<E>(E Function(T) toElement) => 
    map(toElement).toList();
}

Future<bool> in Flutter



Future<bool> uploadImage({
  required File file,
  required String userId,
}) =>
    FirebaseStorage.instance
        .ref(userId)
        .child(const Uuid().v4())
        .putFile(file)
        .then((_) => true)
        .catchError((_) => false);

Async Bloc Init in Flutter



class App extends StatelessWidget {
  const App({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider<AppBloc>(
      create: (context) => AppBloc()..add(const AppEventInitialize()),
      child: MaterialApp(
        title: 'Photo Library',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        debugShowCheckedModeBanner: false,
        home: BlocConsumer<AppBloc, AppState>(
          listener: (context, state) {
            // handle loading
            if (state.isLoading) {
              LoadingScreen().show(
                context: context,
                text: 'Loading...',
              );
            } else {
              LoadingScreen().hide();
            }
            
            ... rest of your code goes here

Firebase Auth Errors in Flutter



const authErrorMapping = {
  'user-not-found': AuthErrorUserNotFound(),
  'project-not-found': AuthErrorProjectNotFound(),
};

@immutable
abstract class AuthError {
  factory AuthError.from(FirebaseAuthException exception) =>
      authErrorMapping[exception.code.toLowerCase().trim()] ??
      const AuthErrorUnknown();
}

@immutable
class AuthErrorUnknown implements AuthError {
  const AuthErrorUnknown();
}

@immutable
class AuthErrorUserNotFound implements AuthError {
  const AuthErrorUserNotFound();
}

@immutable
class AuthErrorProjectNotFound implements AuthError {
  const AuthErrorProjectNotFound();
}

Debug Strings in Flutter


extension IfDebugging on String {
  String? get ifDebugging => kDebugMode ? this : null;
}

class LoginView extends HookWidget {
  const LoginView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final emailController = useTextEditingController(
      text: 'foo@bar.com'.ifDebugging,
    );
    final passwordController = useTextEditingController(
      text: 'foobarbaz'.ifDebugging,
    );
    
    // rest of your code would be here ...

Keyboard Appearance in Flutter


class LoginView extends HookWidget {
  const LoginView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Log in'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: const [
            TextField(
              keyboardType: TextInputType.emailAddress,
              keyboardAppearance: Brightness.dark,
            ),
            TextField(
              obscureText: true,
              obscuringCharacter: '◉',
            ),
          ],
        ),
      ),
    );
  }
}

Get String Data in Dart


extension ToList on String {
  Uint8List toUint8List() => Uint8List.fromList(codeUnits);
}

final text1Data = 'Foo'.toUint8List();
final text2Data = 'Bar'.toUint8List();

Stream.startWith in Flutter


import 'package:async/async.dart' show StreamGroup;
import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

extension StartWith<T> on Stream<T> {
  Stream<T> startWith(T value) => StreamGroup.merge([
        this,
        Stream<T>.value(value),
      ]);
}

void testIt() {
  Stream.periodic(const Duration(seconds: 1), (i) => i + 1)
      .startWith(0)
      .take(4)
      .forEach((element) {
    element.log();
  }); // 0, 1, 2, 3
}

Optional Functions in Dart



typedef AppBlocRandomUrlPicker = String Function(Iterable<String> allUrls);

extension RandomElement<T> on Iterable<T> {
  T getRandomElement() => elementAt(
        math.Random().nextInt(length),
      );
}

class AppBloc extends Bloc<AppEvent, AppState> {
  String _pickRandomUrl(Iterable<String> allUrls) => allUrls.getRandomElement();

  AppBloc({
    required Iterable<String> urls,
    AppBlocRandomUrlPicker? urlPicker,
  }) : super(const AppState.empty()) {
    on<LoadNextUrlEvent>(
      (event, emit) {
        emit(
          const AppState(
            isLoading: true,
            data: null,
          ),
        );
        // pick a random URL to load
        final url = (urlPicker ?? _pickRandomUrl)(urls);
        HttpClient().getUrl(Uri.parse(url)); // continue here...
      },
    );
  }
}

AnnotatedRegion in Flutter



class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnnotatedRegion<SystemUiOverlayStyle>(
        value: SystemUiOverlayStyle.dark,
        child: Column(
          mainAxisSize: MainAxisSize.max,
          children: [
            Expanded(child: Container(color: Colors.blue)),
            Expanded(child: Container(color: Colors.yellow)),
          ],
        ),
      ),
    );
  }
}

Unordered Map Equality in Dart



import 'package:collection/collection.dart';
import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

extension UnorderedEquality<K, V> on Map<K, V> {
  bool isEqualTo(Map<K, V> other) =>
      const DeepCollectionEquality.unordered().equals(this, other);
}

void testIt() {
  final dict1 = {
    'name': 'foo',
    'age': 20,
    'values': ['foo', 'bar'],
  };
  final dict2 = {
    'age': 20,
    'name': 'foo',
    'values': ['bar', 'foo'],
  };
  dict1.isEqualTo(dict2).log(); // true
}

Iterable to ListView in Flutter



extension ToListView<T> on Iterable<T> {
  Widget toListView() => IterableListView(
        iterable: this,
      );
}

class IterableListView<T> extends StatelessWidget {
  final Iterable<T> iterable;
  const IterableListView({
    Key? key,
    required this.iterable,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: iterable.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(
            iterable.elementAt(index).toString(),
          ),
        );
      },
    );
  }
}

@immutable
class Person {
  final String name;
  final int age;
  const Person({required this.name, required this.age});
  @override
  String toString() => '$name, $age years old';
}

const persons = [
  Person(name: 'Foo', age: 20),
  Person(name: 'Bar', age: 30),
  Person(name: 'Baz', age: 40),
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
      ),
      body: persons.toListView(),
    );
  }
}

Password Mask in Flutter



class PasswordTextField extends StatelessWidget {
  const PasswordTextField({
    Key? key,
    required this.passwordController,
  }) : super(key: key);

  final TextEditingController passwordController;

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: passwordController,
      obscureText: true,
      obscuringCharacter: '◉',
      decoration: const InputDecoration(
        hintText: 'Enter your password here...',
      ),
    );
  }
}

Fast Object.toString() in Dart



@immutable
class AppState {
  final bool isLoading;
  final Object? loginError;
  final String? loginHandle;
  final Iterable<String>? fetchedNotes;

  @override
  String toString() => {
        'isLoading': isLoading,
        'loginError': loginError,
        'loginHandle': loginHandle,
        'fetchedNotes': fetchedNotes
      }.toString();

  const AppState({
    required this.isLoading,
    required this.loginError,
    required this.loginHandle,
    required this.fetchedNotes,
  });
}

Copying Bloc State in Flutter



@immutable
class AppState {
  final bool isLoading;
  final LoginHandle? loginHandle;
  final Iterable<Note>? fetchedNotes;

  const AppState.empty()
      : isLoading = false,
        loginHandle = null,
        fetchedNotes = null;

  const AppState({
    required this.isLoading,
    required this.loginHandle,
    required this.fetchedNotes,
  });

  AppState copiedWith({
    bool? isLoading,
    LoginHandle? loginHandle,
    Iterable<Note>? fetchedNotes,
  }) =>
      AppState(
        isLoading: isLoading ?? this.isLoading,
        loginHandle: loginHandle ?? this.loginHandle,
        fetchedNotes: fetchedNotes ?? this.fetchedNotes,
      );
}

Iterable Subscripts in Dart

// Free Flutter Course 💙 https://linktr.ee/vandadnp
// Want to support my work 🤝? https://buymeacoffee.com/vandad

import 'dart:developer' as devtools show log;

extension Log on Object? {
  void log() => devtools.log(toString());
}

extension Subscript<T> on Iterable<T> {
  T? operator [](int index) => length > index ? elementAt(index) : null;
}

void testIt() {
  Iterable.generate(10, (i) => i + 1)[0].log(); // 1
  Iterable.generate(1, (i) => i)[2].log(); // null
  Iterable.generate(10, (i) => i + 1)[9].log(); // 10
  Iterable.generate(0, (i) => i)[0].log(); // null
}

useState in Flutter Hooks



import 'package:flutter_hooks/flutter_hooks.dart';
import 'dart:math' show min;

@immutable
class VirtualTab {
  final Icon icon;
  final String text;

  const VirtualTab({
    required this.icon,
    required this.text,
  });
}

const tabs = [
  VirtualTab(
    icon: Icon(Icons.picture_as_pdf),
    text: 'All PDF files',
  ),
  VirtualTab(
    icon: Icon(Icons.ac_unit_outlined),
    text: 'Data page',
  ),
  VirtualTab(
    icon: Icon(Icons.person),
    text: 'Profile page',
  ),
];

class HomePage extends HookWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final tabCount = useState(1);
    return DefaultTabController(
      length: tabCount.value,
      initialIndex: tabCount.value - 1,
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            tabs: tabs
                .take(tabCount.value)
                .map((tab) => Tab(icon: tab.icon))
                .toList(),
          ),
        ),
        body: CustomTabBarView(tabCount: tabCount),
      ),
    );
  }
}

class CustomTabBarView extends StatelessWidget {
  const CustomTabBarView({
    Key? key,
    required this.tabCount,
  }) : super(key: key);

  final ValueNotifier<int> tabCount;

  @override
  Widget build(BuildContext context) {
    return TabBarView(
      children: tabs
          .take(tabCount.value)
          .map(
            (tab) => Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                children: [
                  Text(tab.text),
                  TextButton(
                    onPressed: () {
                      final newLength = min(
                        tabs.length,
                        tabCount.value + 1,
                      );
                      tabCount.value = newLength;
                    },
                    child: const Text('Create next tab'),
                  )
                ],
              ),
            ),
          )
          .toList(),
    );
  }
}

 

Folding Iterables in Dart



import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

void testIt() {
  final values = ['foo', 'bar', 'baz', '1.0'];

  values.fold<int>(0, (pe, e) => pe + e.length); // 12

  values.fold<String>('', (pe, e) => '$pe$e'); // foobarbaz1.0

  values.fold<Map<String, int>>(
    {},
    (pe, e) => pe..addAll(<String, int>{e: e.length}),
  ).log(); // {foo: 3, bar: 3, baz: 3, 1.0: 3}

  values.fold<double>(
    0.0,
    (pe, e) => pe + (double.tryParse(e) ?? 0.0),
  ); // 1.0
}

Custom Iterables in Dart



class Address with IterableMixin {
  final String line1;
  final String line2;
  final String postCode;

  Address({
    required this.line1,
    required this.line2,
    required this.postCode,
  });

  @override
  Iterator<String> get iterator => [line1, line2, postCode].iterator;
}

void testIt() {
  final address = Address(
    line1: 'Foo bar avenue, #10',
    line2: 'Baz street',
    postCode: '123456',
  );
  for (final line in address) {
    devtools.log(line);
  }
}

Class Clusters in Dart



enum AnimalType { dog, cat }

@immutable
abstract class Animal {
  const Animal();
  factory Animal.fromType(AnimalType type) {
    switch (type) {
      case AnimalType.dog:
        return const Dog();
      case AnimalType.cat:
        return const Cat();
    }
  }
  void makeNoise();
}

@immutable
class Dog extends Animal {
  const Dog();
  @override
  void makeNoise() => 'Woof'.log();
}

@immutable
class Cat extends Animal {
  const Cat();
  @override
  void makeNoise() => 'Meow'.log();
}

void testIt() {
  final cat = Animal.fromType(AnimalType.cat);
  cat.makeNoise();
  final dog = Animal.fromType(AnimalType.dog);
  dog.makeNoise();
}

Iterable +/- in Dart



extension AddRemoveItems<T> on Iterable<T> {
  Iterable<T> operator +(T other) => followedBy([other]);
  Iterable<T> operator -(T other) => where((element) => element != other);
}

void testIt() {
  final values = ['foo', 'bar']
    .map((e) => e.toUpperCase()) + 'BAZ';
  values.log(); // (FOO, BAR, BAZ)
  (values - 'BAZ').log(); // (FOO, BAR)
}

Periodic Streams in Dart



import 'dart:io';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

@immutable
class Person {
  final String name;
  final int age;

  const Person({
    required this.name,
    required this.age,
  });

  Person.fromJson(Map<String, dynamic> json)
      : name = json["name"] as String,
        age = json["age"] as int;

  @override
  String toString() => 'Person ($name, $age years old)';
}

mixin ListOfThingsAPI<T> {
  Future<Iterable<T>> get(String url) => HttpClient()
      .getUrl(Uri.parse(url))
      .then((req) => req.close())
      .then((resp) => resp.transform(utf8.decoder).join())
      .then((str) => json.decode(str) as List<dynamic>)
      .then((list) => list.cast());
}

class GetPeople with ListOfThingsAPI<Map<String, dynamic>> {
  Future<Iterable<Person>> getPeople(url) => get(url).then(
        (jsons) => jsons.map(
          (json) => Person.fromJson(json),
        ),
      );
}

Stream<dynamic> every(Duration duration) => Stream.periodic(duration);

extension IntToDuration on int {
  Duration get seconds => Duration(seconds: this);
}

void testIt() async {
  await for (final people in every(3.seconds).asyncExpand(
    (_) => GetPeople()
        .getPeople('http://127.0.0.1:5500/apis/people1.json')
        .asStream(),
  )) {
    people.log();
  }
}

/* people1.json
[
    {
        "name": "Foo 1",
        "age": 20
    },
    {
        "name": "Bar 1",
        "age": 30
    }
]
*/

EmptyOnError in Dart



import 'dart:io';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

@immutable
class Person {
  final String name;
  final int age;

  const Person({
    required this.name,
    required this.age,
  });

  Person.fromJson(Map<String, dynamic> json)
      : name = json["name"] as String,
        age = json["age"] as int;

  @override
  String toString() => 'Person ($name, $age years old)';
}

const people1Url = 'http://127.0.0.1:5500/apis/people11.json';
const people2Url = 'http://127.0.0.1:5500/apis/people2.json';

extension EmptyOnError<E> on Future<List<Iterable<E>>> {
  Future<List<Iterable<E>>> emptyOnError() => catchError(
        (_, __) => List<Iterable<E>>.empty(),
      );
}

Future<Iterable<Person>> parseJson(String url) => HttpClient()
    .getUrl(Uri.parse(url))
    .then((req) => req.close())
    .then((resp) => resp.transform(utf8.decoder).join())
    .then((str) => json.decode(str) as List<dynamic>)
    .then((json) => json.map((e) => Person.fromJson(e)));

void testIt() async {
  final persons = await Future.wait([
    parseJson(people1Url),
    parseJson(people2Url),
  ]).emptyOnError();
  persons.log();
}

Stream<T> Initial Value in Flutter



import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: const HomePage(),
    ),
  );
}

const url = 'https://bit.ly/3x7J5Qt';

class HomePage extends HookWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    late final StreamController<double> controller;
    controller = useStreamController<double>(onListen: () {
      controller.sink.add(0.0);
    });

    return Scaffold(
      appBar: AppBar(
        title: const Text('Home page'),
      ),
      body: StreamBuilder<double>(
          stream: controller.stream,
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return const CircularProgressIndicator();
            } else {
              final rotation = snapshot.data ?? 0.0;
              return GestureDetector(
                onTap: () {
                  controller.sink.add(rotation + 10.0);
                },
                child: RotationTransition(
                  turns: AlwaysStoppedAnimation(rotation / 360.0),
                  child: Center(
                    child: Image.network(url),
                  ),
                ),
              );
            }
          }),
    );
  }
}

Double.normalize in Dart



import 'dart:developer' as devtools show log;

extension Normalize on double {
  double normalized(
    double selfRangeMin,
    double selfRangeMax, [
    double normalizedRangeMin = 0.0,
    double normalizedRangeMax = 1.0,
  ]) =>
      (normalizedRangeMax - normalizedRangeMin) *
          ((this - selfRangeMin) / (selfRangeMax - selfRangeMin)) +
      normalizedRangeMin;
}

extension Log on Object {
  void log() => devtools.log(toString());
}

void testIt() async {
  2.0.normalized(0, 2.0).log(); // 1.0
  4.0.normalized(0, 8.0).log(); // 0.5
  5.0.normalized(4.0, 6.0, 10.0, 20.0).log(); // 15
}

Hide Sensitive Information in Flutter

 



import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: const HomePage(),
    ),
  );
}

class HomePage extends HookWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final state = useAppLifecycleState();
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Opacity(
          opacity: state == AppLifecycleState.resumed ? 1.0 : 0.0,
          child: Container(
            decoration: BoxDecoration(
              boxShadow: [
                BoxShadow(
                  blurRadius: 10,
                  color: Colors.black.withAlpha(100),
                  spreadRadius: 10,
                ),
              ],
            ),
            child: Image.asset('assets/card.png'),
          ),
        ),
      ),
    );
  }
}

Iterable.compactMap in Dart



import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

extension CompactMap<T> on Iterable<T?> {
  Iterable<T> compactMap<E>([
    E? Function(T?)? transform,
  ]) =>
      map(transform ?? (e) => e).where((e) => e != null).cast();
}

const list = ['Hello', null, 'World'];

void testIt() {
  list.log(); // [Hello, null, World]
  list.compactMap().log(); // [Hello, World]
  list.compactMap((e) => e?.toUpperCase()).log(); // [HELLO, WORLD]
}

useEffect in Flutter Hooks



import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: const HomePage(),
    ),
  );
}

class HomePage extends HookWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final controller = useTextEditingController();
    final text = useState('');
    useEffect(
      () {
        void listener() {
          text.value = controller.text;
        }

        controller.addListener(listener);
        return () => controller.removeListener(listener);
      },
      [controller],
    );
    return Scaffold(
      body: Column(
        children: [
          TextField(
            controller: controller,
          ),
          Text('You typed ${text.value}')
        ],
      ),
    );
  }
}

Merging Streams in Dart



import 'package:async/async.dart' show StreamGroup;
import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

void testIt() async {
  final streams = Iterable.generate(
    3,
    (i) => Stream.periodic(
      const Duration(seconds: 1),
      (_) => 'Stream $i: ${DateTime.now().toIso8601String()}',
    ).take(i + 1),
  );
  await for (final now in StreamGroup.merge(streams)) {
    now.log();
  }
}

Isolate Stream in Dart



Stream<String> getMessages() {
  final rp = ReceivePort();
  return Isolate.spawn(_getMessages, rp.sendPort)
      .asStream()
      .asyncExpand((_) => rp)
      .takeWhile((element) => element is String)
      .cast();
}

void _getMessages(SendPort sp) async {
  await for (final now in Stream.periodic(
    const Duration(seconds: 1),
    (_) => DateTime.now().toIso8601String(),
  ).take(10)) {
    sp.send(now);
  }
  Isolate.exit(sp);
}

void testIt() async {
  await for (final msg in getMessages()) {
    msg.log();
  }
}

Network Image Retry in Flutter



@immutable
class RetryStrategy {
  final bool shouldRetry;
  final Duration waitBeforeRetry;
  const RetryStrategy({
    required this.shouldRetry,
    required this.waitBeforeRetry,
  });
}

typedef Retrier = RetryStrategy Function(String url, Object error);

class NetworkImageWithRetry extends StatelessWidget {
  final Widget loadingWidget;
  final Widget errorWidget;
  final String url;
  final Retrier retrier;

  final _controller = StreamController<Uint8List>.broadcast();

  NetworkImageWithRetry({
    Key? key,
    required this.url,
    required this.retrier,
    required this.loadingWidget,
    required this.errorWidget,
  }) : super(key: key);

  void getData() async {
    while (true == true) {
      try {
        final networkAsset = NetworkAssetBundle(Uri.parse(url));
        final loaded = await networkAsset.load(url);
        final bytes = loaded.buffer.asUint8List();
        _controller.sink.add(bytes);
        break;
      } catch (e) {
        final strategy = retrier(url, e);
        if (strategy.shouldRetry) {
          await Future.delayed(strategy.waitBeforeRetry);
        } else {
          _controller.sink.addError(e);
          break;
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    getData();
    return StreamBuilder(
      stream: _controller.stream,
      builder: (context, AsyncSnapshot<Uint8List> snapshot) {
        if (snapshot.hasError) {
          return errorWidget;
        } else {
          final data = snapshot.data;
          if (snapshot.hasData && data != null) {
            return Image.memory(data);
          } else {
            return loadingWidget;
          }
        }
      },
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Image Retry'),
      ),
      body: NetworkImageWithRetry(
        url: 'https://bit.ly/3qYOtDm',
        errorWidget: const Text('Got an error!'),
        loadingWidget: const Text('Loading...'),
        retrier: (url, error) {
          return RetryStrategy(
            shouldRetry: error is! FlutterError,
            waitBeforeRetry: const Duration(seconds: 1),
          );
        },
      ),
    );
  }
}

Reusable APIs in Flutter

import 'dart:io';
import 'package:flutter/material.dart';
import 'dart:developer' as devtools show log;
import 'dart:convert' show utf8;
import 'package:meta/meta.dart' show useResult;

extension Log on Object {
  void log() => devtools.log(toString());
}

extension GetOnUri on Object {
  Future<HttpClientResponse> getUrl(
    String url,
  ) =>
      HttpClient()
          .getUrl(
            Uri.parse(
              url,
            ),
          )
          .then((req) => req.close());
}

mixin CanMakeGetCall {
  String get url;
  @useResult
  Future<String> getString() => getUrl(url).then(
        (response) => response
            .transform(
              utf8.decoder,
            )
            .join(),
      );
}

@immutable
class GetPeople with CanMakeGetCall {
  const GetPeople();
  @override
  String get url => 'http://127.0.0.1:5500/apis/people.json';
}

void testIt() async {
  final people = await const GetPeople().getString();
  devtools.log(people);
}

ListTile Shadow in Flutter



enum Currency { dollars }

extension Title on Currency {
  String get title {
    switch (this) {
      case Currency.dollars:
        return '\$';
    }
  }
}

@immutable
class Item {
  final IconData icon;
  final String name;
  final double price;
  final Currency currency;

  const Item({
    required this.icon,
    required this.name,
    required this.price,
    required this.currency,
  });

  String get description => '$price${currency.title}';
}

const items = [
  Item(
    icon: Icons.camera_alt,
    name: 'Camera',
    price: 300,
    currency: Currency.dollars,
  ),
  Item(
    icon: Icons.house,
    name: 'House',
    price: 1000000,
    currency: Currency.dollars,
  ),
  Item(
    icon: Icons.watch,
    name: 'Smart Watch',
    price: 200,
    currency: Currency.dollars,
  ),
];

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (_, index) {
          return ItemTile(
            item: items[index],
          );
        },
      ),
    );
  }
}

class ItemTile extends StatelessWidget {
  final Item item;
  const ItemTile({Key? key, required this.item}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Stack(
        children: [
          const TileBackground(),
          CustomTile(item: item),
        ],
      ),
    );
  }
}

class CustomTile extends StatelessWidget {
  final Item item;
  const CustomTile({
    Key? key,
    required this.item,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 7.0),
      child: Container(
        decoration: customDecoration(),
        child: ListTile(
          leading: Icon(
            item.icon,
            color: Colors.white,
          ),
          title: Text(item.name),
          subtitle: Text(item.description),
        ),
      ),
    );
  }
}

BoxDecoration customDecoration() {
  return BoxDecoration(
    color: const Color.fromARGB(255, 0x7d, 0xcf, 0xff),
    borderRadius: BorderRadius.circular(10.0),
    border: Border.all(
      color: Colors.black,
      width: 2.0,
    ),
  );
}

class TileBackground extends StatelessWidget {
  const TileBackground({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Positioned.fill(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 6.0),
        child: Container(
          decoration: BoxDecoration(
            color: const Color.fromARGB(255, 202, 255, 127),
            borderRadius: BorderRadius.circular(10.0),
            border: Border.all(
              color: Colors.black,
              width: 2.0,
            ),
          ),
        ),
      ),
    );
  }
}

Transparent AppBar in Flutter



const images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        elevation: 0.0,
        backgroundColor: Colors.blueAccent.withAlpha(200),
        title: const Text('Transparent App Bar in Flutter'),
      ),
      body: const ImagesScrollView(),
    );
  }
}

class ImagesScrollView extends StatelessWidget {
  const ImagesScrollView({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.only(top: 80.0),
      child: Padding(
        padding: const EdgeInsets.only(
          top: 40.0,
          left: 10.0,
          right: 10.0,
        ),
        child: Column(
          children: images
              .map((url) => ElevatedNetworkImage(url: url))
              .expand(
                (img) => [
                  img,
                  const SizedBox(height: 30.0),
                ],
              )
              .toList(),
        ),
      ),
    );
  }
}

class ElevatedNetworkImage extends StatelessWidget {
  final String url;
  const ElevatedNetworkImage({Key? key, required this.url}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return PhysicalShape(
      color: Colors.white,
      clipper: Clipper(),
      elevation: 20.0,
      clipBehavior: Clip.none,
      shadowColor: Colors.white.withAlpha(200),
      child: CutEdges(
        child: Image.network(url),
      ),
    );
  }
}

class Clipper extends CustomClipper<Path> {
  static const variance = 0.2;
  static const reverse = 1.0 - variance;

  @override
  Path getClip(Size size) {
    final path = Path();

    path.moveTo(0.0, size.height * Clipper.variance);
    path.lineTo(size.width * Clipper.variance, 0.0);
    path.lineTo(size.width, 0.0);
    path.lineTo(size.width, size.height * Clipper.reverse);
    path.lineTo(size.width * Clipper.reverse, size.height);
    path.lineTo(0.0, size.height);
    path.lineTo(0.0, size.height * Clipper.variance);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

class CutEdges extends StatelessWidget {
  final Widget child;

  const CutEdges({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: Clipper(),
      child: child,
    );
  }
}

Constructors on Abstract Classes in Dart



import 'dart:developer' as devtools show log;

extension Log on Object {
  void log() => devtools.log(toString());
}

enum Type { dog, cat }

abstract class CanRun {
  final Type type;
  const CanRun({required this.type});
}

class Cat extends CanRun {
  const Cat() : super(type: Type.cat);
}

class Dog extends CanRun {
  const Dog() : super(type: Type.dog);
}

@useResult in Dart



import 'package:meta/meta.dart' show useResult;

class Person {
  final String firstName;
  final String lastName;

  const Person({
    required this.firstName,
    required this.lastName,
  });

  @useResult
  String fullName() => '$firstName $lastName';
}

void printFullName() {
  const Person(
    firstName: 'Foo',
    lastName: 'Bar',
  ).fullName();
}

@mustCallSuper in Dart



class Animal {
  @mustCallSuper
  void run() {}
}

class Dog extends Animal {
  @override
  void run() {}
}

Object.hash in Dart



class BreadCrumb {
  final bool isActive;
  final String name;
  BreadCrumb({
    required this.isActive,
    required this.name,
  });
  BreadCrumb activated() => BreadCrumb(
        isActive: true,
        name: name,
      );
  @override
  bool operator ==(covariant BreadCrumb other) =>
      isActive == other.isActive && name == other.name;

  @override
  int get hashCode => Object.hash(isActive, name);
}

Expanded Equally in Flutter



import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: const HomePage(),
    ),
  );
}

extension ExpandEqually on Iterable<Widget> {
  Iterable<Widget> expandedEqually() => map(
        (w) => Expanded(
          flex: 1,
          child: w,
        ),
      );
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home Page'),
      ),
      body: Column(
        children: [
          Row(
            mainAxisSize: MainAxisSize.max,
            children: [
              Container(
                height: 200,
                color: Colors.yellow,
              ),
              Container(
                height: 200,
                color: Colors.blue,
              ),
            ].expandedEqually().toList(),
          )
        ],
      ),
    );
  }
}

Random Iterable Value in Dart



import 'dart:math' as math show Random;

extension RandomElement<T> on Iterable<T> {
  T getRandomElement() => elementAt(
        math.Random().nextInt(length),
      );
}

final colors = [Colors.blue, Colors.red, Colors.brown];

class HomePage extends StatelessWidget {
  final color = ValueNotifier<MaterialColor>(
    colors.getRandomElement(),
  );
  HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('List.Random in Flutter'),
      ),
      body: ColorPickerButton(color: color),
    );
  }
}

class ColorPickerButton extends StatelessWidget {
  final ValueNotifier<MaterialColor> color;

  const ColorPickerButton({
    Key? key,
    required this.color,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<Color>(
      valueListenable: color,
      builder: (context, value, child) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: CenteredTight(
            child: TextButton(
              style: TextButton.styleFrom(backgroundColor: value),
              onPressed: () {
                color.value = colors.getRandomElement();
              },
              child: const Text(
                'Change color',
                style: TextStyle(
                  fontSize: 30,
                  color: Colors.white,
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

Hardcoded Strings in Flutter



extension Hardcoded on String {
  String get hardcoded => '$this 🧨';
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'My hardcoded string'.hardcoded,
        ),
      ),
      body: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('String in body'.hardcoded),
        ],
      ),
    );
  }
}

Manually Scroll in List View in Flutter

// Free Flutter Course 💙 https://linktr.ee/vandadnp

import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

class HomePage extends StatelessWidget {
  final _controller = ItemScrollController();
  HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testing'),
      ),
      body: ScrollablePositionedList.builder(
        itemScrollController: _controller,
        itemCount: allImages.length + 1,
        itemBuilder: (context, index) {
          if (index == 0) {
            return IndexSelector(
              count: allImages.length,
              onSelected: (index) {
                _controller.scrollTo(
                  index: index + 1,
                  duration: const Duration(milliseconds: 370),
                );
              },
            );
          } else {
            return ImageWithTitle(index: index);
          }
        },
      ),
    );
  }
}

class ImageWithTitle extends StatelessWidget {
  final int index;
  const ImageWithTitle({
    Key? key,
    required this.index,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          'Image $index',
          style: const TextStyle(fontSize: 30.0),
        ),
        Image.network(allImages.elementAt(index - 1)),
      ],
    );
  }
}

typedef OnIndexSelected = void Function(int index);

class IndexSelector extends StatelessWidget {
  final int count;
  final OnIndexSelected onSelected;
  final String prefix;
  const IndexSelector({
    Key? key,
    required this.count,
    required this.onSelected,
    this.prefix = 'Image',
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Row(
        children: Iterable.generate(
          count,
          (index) => TextButton(
            onPressed: () {
              onSelected(index);
            },
            child: Text('$prefix ${index + 1}'),
          ),
        ).toList(),
      ),
    );
  }
}

const imageUrls = [
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

AsyncSnapshot to Widget in Flutter



import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    ),
  );
}

final future = Future<String>.delayed(
  const Duration(seconds: 3),
  () => 'Hello world',
);

typedef ResolveToWidget<T> = Widget Function(
  ConnectionState connectionState,
  AsyncSnapshot<T> snapshot,
);

extension Materialize on AsyncSnapshot {
  Widget materialize(ResolveToWidget f) => f(
        connectionState,
        this,
      );
}

class HomePage extends HookWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Hooks'),
      ),
      body: useFuture(future).materialize((connectionState, snapshot) {
        switch (connectionState) {
          case ConnectionState.done:
            return Text(snapshot.data ?? '');
          default:
            return const CircularProgressIndicator();
        }
      }),
    );
  }
}

Breadcrumbs in Flutter



@immutable
class BreadCrumbPath {
  final String title;
  final bool isActive;
  const BreadCrumbPath({
    required this.title,
    required this.isActive,
  });
  BreadCrumbPath activated() {
    return BreadCrumbPath(
      title: title,
      isActive: true,
    );
  }

  @override
  String toString() => title;
}

class BreatCrumbPathView extends StatelessWidget {
  final BreadCrumbPath path;

  const BreatCrumbPathView({
    Key? key,
    required this.path,
  }) : super(key: key);
  @override
  Widget build(BuildContext context) {
    final title = path.isActive ? '${path.title} →' : path.title;
    return Padding(
      padding: const EdgeInsets.all(2.0),
      child: Text(
        title,
        style: TextStyle(
          height: 1.0,
          fontSize: 20.0,
          color: path.isActive ? Colors.blueAccent : Colors.black,
        ),
      ),
    );
  }
}

typedef OnBreadCrumbPathTapped = void Function(BreadCrumbPath path);

class BreadCrumbView extends StatelessWidget {
  final OnBreadCrumbPathTapped onTapped;
  final Stream<List<BreadCrumbPath>> paths;
  const BreadCrumbView({
    Key? key,
    required this.paths,
    required this.onTapped,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<BreadCrumbPath>>(
      stream: paths,
      builder: (context, snapshot) {
        final List<Widget> views;
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
          case ConnectionState.active:
            final paths = snapshot.data ?? [];
            final views = paths
                .map(
                  (path) => GestureDetector(
                    onTap: () => onTapped(path),
                    child: BreatCrumbPathView(path: path),
                  ),
                )
                .toList();
            return Wrap(
              spacing: 4.0,
              children: views,
              alignment: WrapAlignment.start,
              crossAxisAlignment: WrapCrossAlignment.center,
            );
          default:
            return Wrap();
        }
      },
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<BreadCrumbPath> _paths = [];
  late final TextEditingController _textController;
  late final StreamController<List<BreadCrumbPath>> _pathsController;

  @override
  void initState() {
    _pathsController = StreamController<List<BreadCrumbPath>>.broadcast(
      onListen: () {
        _pathsController.add(_paths);
      },
    );
    _textController = TextEditingController();
    super.initState();
  }

  @override
  void dispose() {
    _textController.dispose();
    _pathsController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Breadcrumb in Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            BreadCrumbView(
              paths: _pathsController.stream,
              onTapped: (path) async {
                await showBreadCrumbPathTappedDialog(
                  context,
                  path,
                );
              },
            ),
            TextField(
              controller: _textController,
              textAlign: TextAlign.center,
              decoration: const InputDecoration(
                hintText: 'Enter a new path here',
              ),
            ),
            TextButton(
              onPressed: () {
                _paths = [
                  ..._paths.map((p) => p.activated()),
                  BreadCrumbPath(
                    title: _textController.text,
                    isActive: false,
                  ),
                ];
                _pathsController.add(_paths);
                _textController.clear();
              },
              child: const Center(
                child: Text('Add new path'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Future<void> showBreadCrumbPathTappedDialog(
  BuildContext context,
  BreadCrumbPath path,
) {
  return showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        content: Text('You tapped on $path'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: const Text('OK'),
          ),
        ],
      );
    },
  );
}

Unique Map Values in Dart



import 'dart:developer' as devtools show log;

extension ContainsDuplicateValues on Map {
  bool get containsDuplicateValues => 
    {...values}.length != values.length;
}

extension Log on Object {
  void log() => devtools.log(toString());
}

const people1 = {
  1: 'Foo',
  2: 'Bar',
};
const people2 = {
  1: 'Foo',
  2: 'Foo',
};

void testIt() {
  people1.containsDuplicateValues.log(); // false
  people2.containsDuplicateValues.log(); // true
}

Smart Quotes/Dashes in Flutter



class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Smart Quotes/Dashes in Flutter'),
      ),
      body: const Padding(
        padding: EdgeInsets.all(16.0),
        child: TextField(
          smartQuotesType: SmartQuotesType.disabled,
          smartDashesType: SmartDashesType.disabled,
          maxLines: null,
        ),
      ),
    );
  }
}

Haptic Feedback in Flutter



class CenteredTight extends StatelessWidget {
  final Widget child;
  const CenteredTight({
    Key? key,
    required this.child,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [child],
    );
  }
}

class FullscreenImage extends StatefulWidget {
  final String imageUrl;
  const FullscreenImage({Key? key, required this.imageUrl}) : super(key: key);

  @override
  State<FullscreenImage> createState() => _FullscreenImageState();
}

class _FullscreenImageState extends State<FullscreenImage> {
  var shouldDisplayAppbar = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: shouldDisplayAppbar ? AppBar(title: const Text('Image')) : null,
      body: GestureDetector(
        onTap: () {
          setState(() => shouldDisplayAppbar = !shouldDisplayAppbar);
        },
        child: Image.network(
          widget.imageUrl,
          alignment: Alignment.center,
          width: double.infinity,
          height: double.infinity,
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Haptic Feedback in Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: CenteredTight(
          child: FractionallySizedBox(
            heightFactor: 0.7,
            child: GestureDetector(
              onLongPress: () async {
                await HapticFeedback.lightImpact();
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) {
                      return const FullscreenImage(
                        imageUrl: imageUrl,
                      );
                    },
                  ),
                );
              },
              child: Image.network(imageUrl),
            ),
          ),
        ),
      ),
    );
  }
}

Localization Delegates in Flutter



import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    MaterialApp(
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BlocProvider<AuthBloc>(
        create: (context) => AuthBloc(FirebaseAuthProvider()),
        child: const HomePage(),
      ),
      routes: {
        createOrUpdateNoteRoute: (context) => const CreateUpdateNoteView(),
      },
    ),
  );
}

Extending Functions in Dart



import 'dart:developer' as devtools show log;

extension ToTextButton on VoidCallback {
  TextButton toTextButton(String title) {
    return TextButton(
      onPressed: this,
      child: Text(title),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Extensions in Flutter'),
      ),
      body: () {
        devtools.log('I am pressed');
      }.toTextButton('Press me'),
    );
  }
}

Paginated ListView in Flutter



@immutable
class Season {
  final String name;
  final String imageUrl;
  const Season({required this.name, required this.imageUrl});
  const Season.spring()
      : name = 'Spring',
        imageUrl = 'https://cnn.it/3xu58Ap';
  const Season.summer()
      : name = 'Summer',
        imageUrl = 'https://bit.ly/2VcCSow';
  const Season.autumn()
      : name = 'Autumn',
        imageUrl = 'https://bit.ly/3A3zStC';
  const Season.winter()
      : name = 'Winter',
        imageUrl = 'https://bit.ly/2TNY7wi';
}

const allSeasons = [
  Season.spring(),
  Season.summer(),
  Season.autumn(),
  Season.winter()
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.of(context).size.width;
    final height = width / (16.0 / 9.0);
    return Scaffold(
      appBar: AppBar(
        title: const Text('PageScrollPhysics in Flutter'),
      ),
      body: SizedBox(
        width: width,
        height: height,
        child: ListView(
          shrinkWrap: true,
          scrollDirection: Axis.horizontal,
          physics: const PageScrollPhysics(),
          clipBehavior: Clip.antiAlias,
          children: allSeasons.map((season) {
            return SizedBox(
              width: width,
              height: height,
              child: Image.network(
                season.imageUrl,
                height: height,
                fit: BoxFit.cover,
              ),
            );
          }).toList(),
        ),
      ),
    );
  }
}

Immutable Classes in Dart



import 'package:flutter/foundation.dart' show immutable;

@immutable
abstract class Animal {
  final String name;
  const Animal(this.name);
}

class Cat extends Animal {
  const Cat() : super('Cindy Clawford');
}

class Dog extends Animal {
  int age;
  Dog()
      : age = 0,
        super('Bark Twain');
}

Card Widget in Flutter



class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Card in Flutter'),
      ),
      body: Image.network(
        'https://bit.ly/36fNNj9',
        frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
          return Card(
            child: child,
            clipBehavior: Clip.antiAlias,
          );
        },
        loadingBuilder: (context, child, loadingProgress) {
          final totalBytes = loadingProgress?.expectedTotalBytes;
          final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
          if (totalBytes != null && bytesLoaded != null) {
            return Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: const [CircularProgressIndicator()],
            );
          } else {
            return child;
          }
        },
      ),
    );
  }
}

List Equality Ignoring Ordering in Dart



@immutable
class Person {
  final String name;
  const Person(this.name);

  @override
  bool operator ==(covariant Person other) => other.name == name;

  @override
  int get hashCode => name.hashCode;

  @override
  String toString() => name;
}

const people1 = [Person('Foo'), Person('Bar'), Person('Baz')];
const people2 = [Person('Foo'), Person('Bar'), Person('Baz')];
const people3 = [Person('Bar'), Person('Bar'), Person('Baz')];
const people4 = [Person('Bar'), Person('Baz')];

extension IsEqualToIgnoringOrdering<T> on List<T> {
  bool isEqualToIgnoringOrdering(List<T> other) =>
      length == other.length &&
      {...this}.intersection({...other}).length == length;
}

void testIt() {
  assert(people1.isEqualToIgnoringOrdering(people2));
  assert(!people1.isEqualToIgnoringOrdering(people3));
  assert(!people2.isEqualToIgnoringOrdering(people3));
  assert(!people3.isEqualToIgnoringOrdering(people4));
}

Shorten GitHub URLs in Dart

// Want to support my work 🤝? https://buymeacoffee.com/vandad

import 'dart:developer' as devtools show log;
import 'dart:convert' show utf8;

Future<Uri> shortenGitHubUrl(String longUrl) =>
    HttpClient().postUrl(Uri.parse('https://git.io/')).then((req) {
      req.add(utf8.encode('url=$longUrl'));
      return req.close();
    }).then(
      (resp) async {
        try {
          final location = resp.headers[HttpHeaders.locationHeader]?.first;
          if (location != null) {
            return Uri.parse(location);
          } else {
            throw 'No location was specified';
          }
        } catch (e) {
          return Uri.parse(longUrl);
        }
      },
    );

void testIt() async {
  final uri = await shortenGitHubUrl(
      'https://github.com/vandadnp/flutter-tips-and-tricks');
  devtools.log(uri.toString());
  // logs https://git.io/JS5Fm
}

Time Picker in Flutter



class HomePage extends StatelessWidget {
  final timeOfDay = ValueNotifier<TimeOfDay?>(null);
  HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: timeOfDay,
      builder: (context, value, child) {
        final title = timeOfDay.value?.toString() ?? 'Time Picker in Flutter';
        return Scaffold(
          appBar: AppBar(title: Text(title)),
          body: Center(
            child: TextButton(
              onPressed: () async {
                timeOfDay.value = await showTimePicker(
                  context: context,
                  initialTime: TimeOfDay.now(),
                  initialEntryMode: TimePickerEntryMode.input,
                );
              },
              child: const Text('Please Pick a time'),
            ),
          ),
        );
      },
    );
  }
}

Throttled Print in Flutter



Stream<String> getStream() => Stream.periodic(
      const Duration(milliseconds: 100),
      (e) => DateTime.now().toString(),
    );

void testIt() async {
  await for (final now in getStream()) {
    debugPrintThrottled(now);
  }
}

Map Equality in Dart



typedef Name = String;
typedef Age = int;

const Map<Name, Age> people1 = {
  'foo': 20,
  'bar': 30,
  'baz': 40,
};

const Map<Name, Age> people2 = {
  'baz': 40,
  'foo': 20,
  'bar': 30,
};

void testIt() {
  assert(mapEquals(people1, people2));
}

Unique Maps in Dart



import 'dart:developer' as devtools show log;

typedef Name = String;
typedef Age = int;

const Map<Name, Age> people = {
  'foo': 20,
  'bar': 30,
  'baz': 20,
};

extension Unique<K, V> on Map<K, V> {
  Map<K, V> unique() {
    Map<K, V> result = {};
    for (final value in {...values}) {
      final firstKey = keys.firstWhereOrNull((key) => this[key] == value);
      if (firstKey != null) {
        result[firstKey] = value;
      }
    }
    return result;
  }
}

void testIt() {
  final uniques = people.unique();
  devtools.log(uniques.toString());
  // prints: {foo: 20, bar: 30}
}

Raw Auto Complete in Flutter



const emailProviders = [
  'gmail.com',
  'hotmail.com',
  'yahoo.com',
];

const icons = [
  'https://bit.ly/3HsvvvB',
  'https://bit.ly/3n6GW4L',
  'https://bit.ly/3zf2RLy',
];

class EmailTextField extends StatefulWidget {
  const EmailTextField({Key? key}) : super(key: key);

  @override
  State<EmailTextField> createState() => _EmailTextFieldState();
}

class _EmailTextFieldState extends State<EmailTextField> {
  late final TextEditingController _controller;
  late final FocusNode _focus;

  @override
  Widget build(BuildContext context) {
    return RawAutocomplete<String>(
      textEditingController: _controller,
      focusNode: _focus,
      fieldViewBuilder: (_, controller, focusNode, onSubmitted) {
        return TextFormField(
          controller: controller,
          focusNode: focusNode,
          onFieldSubmitted: (value) {
            onSubmitted();
          },
        );
      },
      optionsBuilder: (textEditingValue) {
        final lastChar = textEditingValue.text.characters.last;
        if (lastChar == '@') {
          return emailProviders;
        } else {
          return [];
        }
      },
      optionsViewBuilder: (context, onSelected, options) {
        return OptionsList(
          onSelected: onSelected,
          options: options,
          controller: _controller,
        );
      },
    );
  }

  @override
  void initState() {
    _controller = TextEditingController();
    _focus = FocusNode();
    super.initState();
  }

  @override
  void dispose() {
    _focus.dispose();
    _controller.dispose();
    super.dispose();
  }
}

class OptionsList extends StatelessWidget {
  final Iterable<String> options;
  final AutocompleteOnSelected<String> onSelected;
  final TextEditingController controller;
  const OptionsList({
    Key? key,
    required this.onSelected,
    required this.options,
    required this.controller,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.topLeft,
      child: Material(
        child: SizedBox(
          height: 150,
          child: ListView.builder(
            padding: const EdgeInsets.all(0.0),
            itemCount: options.length,
            itemBuilder: (context, index) {
              final option = options.elementAt(index);
              return GestureDetector(
                onTap: () => onSelected(controller.text + option),
                child: ListTile(
                  horizontalTitleGap: 2.0,
                  leading: Image.network(
                    icons[index],
                    width: 24,
                    height: 24,
                  ),
                  title: Text(option),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Raw Auto Complete in Flutter'),
      ),
      body: const Padding(
        padding: EdgeInsets.all(16.0),
        child: EmailTextField(),
      ),
    );
  }
}

Title on Object in Dart



import 'dart:developer' as devtools show log;

extension CapTitle on Object {
  String get capitalizedTitle {
    String str;
    if (this is Enum) {
      str = (this as Enum).name;
    } else {
      str = toString();
    }
    return str[0].toUpperCase() + str.substring(1);
  }
}

enum EmailProviders { gmail, yahoo, hotmail }

void testIt() {
  EmailProviders.values.map((p) => p.capitalizedTitle).forEach(devtools.log);
  // prints these:
  // Gmail
  // Yahoo
  // Hotmail
}

Compute in Flutter



import 'dart:developer' as devtools show log;
import 'dart:convert' show utf8, json;

@immutable
class Person {
  final String name;
  final int age;
  const Person(this.name, this.age);
  Person.fromJson(Map<String, dynamic> json)
      : name = json["name"] as String,
        age = json["age"] as int;
}

Future<Iterable<Person>> downloadAndParsePersons(Uri uri) => HttpClient()
    .getUrl(uri)
    .then((req) => req.close())
    .then((response) => response.transform(utf8.decoder).join())
    .then((jsonString) => json.decode(jsonString) as List<dynamic>)
    .then((json) => json.map((map) => Person.fromJson(map)));

void testIt() async {
  final persons = await compute(
    downloadAndParsePersons,
    Uri.parse('https://bit.ly/3Jjcw8R'),
  );
  devtools.log(persons.toString());
}

Filter on Map in Dart



import 'dart:developer' as devtools show log;

typedef Name = String;
typedef Age = int;

extension Filter<K, V> on Map<K, V> {
  Iterable<MapEntry<K, V>> filter(
    bool Function(MapEntry<K, V> entry) f,
  ) sync* {
    for (final entry in entries) {
      if (f(entry)) {
        yield entry;
      }
    }
  }
}

const Map<Name, Age> people = {
  'foo': 20,
  'bar': 31,
  'baz': 25,
  'qux': 32,
};

void testIt() async {
  final peopleOver30 = people.filter((e) => e.value > 30);
  devtools.log(peopleOver30.toString());
  // ☝🏻 prints (MapEntry(bar: 31), MapEntry(qux: 32))
}

Type Alias in Dart



const Map<String, int> people1 = {
  'foo': 20,
  'bar': 30,
  'baz': 25,
};

typedef Age = int;

const Map<String, Age> people2 = {
  'foo': 20,
  'bar': 30,
  'baz': 25,
};

ValueNotifier in Flutter



class DynamicToolTipTextField extends StatelessWidget {
  final TextInputType? keyboardType;
  final ValueNotifier<String?> hint;
  final TextEditingController controller;
  const DynamicToolTipTextField({
    Key? key,
    required this.hint,
    required this.controller,
    this.keyboardType,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: hint,
      builder: (context, value, child) {
        return TextField(
          keyboardType: keyboardType,
          controller: controller,
          decoration: InputDecoration(
            hintText: value as String?,
          ),
        );
      },
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

@immutable
abstract class HasText {
  String get text;
}

enum Hint { pleaseEnterYourEmail, youForgotToEnterYourEmail }

extension GetText on Hint {
  String get text {
    switch (this) {
      case Hint.pleaseEnterYourEmail:
        return 'Please enter your email';
      case Hint.youForgotToEnterYourEmail:
        return 'You forgot to enter your email';
    }
  }
}

class _HomePageState extends State<HomePage> {
  late final ValueNotifier<String?> _hint;
  late final TextEditingController _controller;

  @override
  void initState() {
    _hint = ValueNotifier<String?>(Hint.pleaseEnterYourEmail.text);
    _controller = TextEditingController();
    super.initState();
  }

  @override
  void dispose() {
    _hint.dispose();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ValueNotifier in Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            DynamicToolTipTextField(
              hint: _hint,
              controller: _controller,
              keyboardType: TextInputType.emailAddress,
            ),
            TextButton(
              onPressed: () async {
                final email = _controller.text;
                if (email.trim().isEmpty) {
                  _hint.value = Hint.youForgotToEnterYourEmail.text;
                  await Future.delayed(const Duration(seconds: 2));
                  _hint.value = Hint.pleaseEnterYourEmail.text;
                }
              },
              child: const Text('Log in'),
            )
          ],
        ),
      ),
    );
  }
}

Object to Integer in Dart



enum ToIntStrategy { round, floor, ceil }

typedef ToIntOnErrorHandler = int Function(Object e);

extension ToInt on Object {
  int toInteger({
    ToIntStrategy strategy = ToIntStrategy.round,
    ToIntOnErrorHandler? onError,
  }) {
    try {
      final doubleValue = double.parse(toString());
      switch (strategy) {
        case ToIntStrategy.round:
          return doubleValue.round();
        case ToIntStrategy.floor:
          return doubleValue.floor();
        case ToIntStrategy.ceil:
          return doubleValue.ceil();
      }
    } catch (e) {
      if (onError != null) {
        return onError(e);
      } else {
        return -1;
      }
    }
  }
}

void testIt() {
  assert('xyz'.toInteger(onError: (_) => 100) == 100);
  assert(1.5.toInteger() == 2);
  assert(1.6.toInteger() == 2);
  assert('1.2'.toInteger(strategy: ToIntStrategy.floor) == 1);
  assert('1.2'.toInteger(strategy: ToIntStrategy.ceil) == 2);
  assert('1.5'.toInteger(strategy: ToIntStrategy.round) == 2);
}

Image Opacity in Flutter



class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _opacity;

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
    _opacity = Tween(begin: 0.0, end: 1.0).animate(_controller);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Image.network(
            'https://bit.ly/3ywI8l6',
            opacity: _opacity,
          ),
          Slider(
            value: _controller.value,
            onChanged: (value) {
              setState(() => _controller.value = value);
            },
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Covariant in Dart

// Want to support my work 🤝? https://buymeacoffee.com/vandad

class Person {
  final String name;
  const Person(this.name);
  @override
  bool operator ==(Object other) {
    if (other is! Person) throw ArgumentError('Was expecting a person');
    return other.name == name;
  }

  @override
  int get hashCode => name.hashCode;
}

class Person {
  final String name;
  const Person(this.name);
  @override
  bool operator ==(covariant Person other) => other.name == name;

  @override
  int get hashCode => name.hashCode;
}

Custom Errors in Streams in Dart



class Either<V, E extends Exception> {
  final V? value;
  final E? error;

  const Either({this.value, this.error}) : assert((value ?? error) != null);

  bool get isError => error != null;
  bool get isValue => value != null;

  @override
  String toString() {
    if (value != null) {
      return "Value: $value";
    } else if (error != null) {
      return "Error: $error";
    } else {
      return 'Unknown state';
    }
  }
}

class DateTimeException implements Exception {
  final String reason;
  const DateTimeException({required this.reason});
}

Stream<Either<DateTime, DateTimeException>> getDateTime() async* {
  var index = 0;
  while (true) {
    if (index % 2 == 0) {
      yield Either(value: DateTime.now());
    } else {
      yield const Either(
        error: DateTimeException(reason: 'Something is wrong!'),
      );
    }
    index += 1;
  }
}

void testIt() async {
  await for (final value in getDateTime()) {
    dev.log(value.toString());
  }
}

Shake Animation in Flutter



class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

const animationWidth = 10.0;

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late final TextEditingController _textController;
  late final AnimationController _animationController;
  late final Animation<double> _offsetAnim;
  final defaultHintText = 'Please enter your email here 😊';
  var _hintText = '';

  @override
  void initState() {
    _hintText = defaultHintText;
    _textController = TextEditingController();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 370),
      vsync: this,
    );
    _offsetAnim = Tween(
      begin: 0.0,
      end: animationWidth,
    ).chain(CurveTween(curve: Curves.elasticIn)).animate(
          _animationController,
        )..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            _animationController.reverse();
          }
        },
      );
    super.initState();
  }

  @override
  void dispose() {
    _textController.dispose();
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Shake Animation in Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            AnimatedBuilder(
              animation: _offsetAnim,
              builder: (context, child) {
                return Container(
                  margin: const EdgeInsets.symmetric(
                    horizontal: animationWidth,
                  ),
                  padding: EdgeInsets.only(
                    left: _offsetAnim.value + animationWidth,
                    right: animationWidth - _offsetAnim.value,
                  ),
                  child: TextField(
                    controller: _textController,
                    keyboardType: TextInputType.emailAddress,
                    decoration: InputDecoration(
                      hintText: _hintText,
                    ),
                  ),
                );
              },
            ),
            TextButton(
                onPressed: () async {
                  if (_textController.text.isEmpty) {
                    setState(() {
                      _hintText = 'You forgot to enter your email 🥲';
                      _animationController.forward(from: 0.0);
                    });
                    await Future.delayed(const Duration(seconds: 3));
                    setState(() {
                      _hintText = defaultHintText;
                    });
                  }
                },
                child: const Text('Login'))
          ],
        ),
      ),
    );
  }
}

Throw Enums in Dart



import 'dart:developer' as dev show log;

enum Exceptions { invalidUserName, invalidPassword }

void thisMethodThrows() {
  throw Exceptions.invalidPassword;
}

void testIt() {
  try {
    thisMethodThrows();
  } on Exceptions catch (e) {
    switch (e) {
      case (Exceptions.invalidUserName):
        dev.log("Invalid user name");
        break;
      case (Exceptions.invalidPassword):
        dev.log("Invalid password");
        break;
    }
  }
}

Future Error Test in Flutter



import 'dart:developer' as dev show log;

@immutable
abstract class UserException implements Exception {}

class InvalidUserNameException extends UserException {}

class InvalidUserAgeException extends UserException {}

@immutable
class User {
  final String name;
  final int age;
  User({required this.name, required this.age}) {
    if (!name.contains(RegExp(r'^[a-z ]+$'))) {
      throw InvalidUserNameException();
    } else if (age < 0 || age > 130) {
      throw InvalidUserAgeException();
    }
  }
  const User.anonymous()
      : name = 'Anonymous User',
        age = 0;
}

Future<User> getAsyncUser() => Future.delayed(
      const Duration(seconds: 1),
      () => User(name: 'Foo', age: 20),
    );

void testIt() async {
  final user = await getAsyncUser()
      .catchError(
        handleInvalidUsernameException,
        test: (e) => e is InvalidUserNameException,
      )
      .catchError(
        handleInvalidAgeException,
        test: (e) => e is InvalidUserAgeException,
      );
  dev.log(user.toString());
}

User handleInvalidUsernameException(Object? e) {
  dev.log(e.toString());
  return const User.anonymous();
}

User handleInvalidAgeException(Object? e) {
  dev.log(e.toString());
  return const User.anonymous();
}

Generic URL Retrieval in Dart



import 'dart:developer' as dev show log;

typedef StatusCodeResultBuilder<T> = Future<T> Function(
  int statusCode,
  HttpClientResponse response,
);

extension Get on Uri {
  Future<T?> getBody<T>({
    StatusCodeResultBuilder<T>? statusBuilder,
    T Function(Object error)? onNetworkError,
  }) async {
    try {
      final apiCall = await HttpClient().getUrl(this);
      final response = await apiCall.close();
      final builder = statusBuilder;
      if (builder == null) {
        final data = await response.transform(convert.utf8.decoder).join();
        if (data is T) {
          return data as T?;
        } else {
          return null;
        }
      } else {
        final result = await builder(response.statusCode, response);
        return result;
      }
    } catch (e) {
      if (onNetworkError != null) {
        return onNetworkError(e);
      } else {
        return null;
      }
    }
  }
}

extension ToUri on String {
  Uri toUri() => Uri.parse(this);
}

const url = 'https://bit.ly/3EKWcLa';

void testIt() async {
  final json = await url.toUri().getBody<String>(
    statusBuilder: (statusCode, response) async {
      if (statusCode == 200) {
        return await response.transform(convert.utf8.decoder).join();
      } else {
        return "{'error': 'Unexpected status code $statusCode'}";
      }
    },
    onNetworkError: (error) {
      return "{'error': 'Got network error'}";
    },
  );
  if (json != null) {
    dev.log(json);
  }
}

Custom Error Widget in Flutter



class MyErrorWidget extends StatelessWidget {
  final String text;
  const MyErrorWidget({Key? key, required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SizedBox(
        width: MediaQuery.of(context).size.width,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              Image.network('https://bit.ly/3gHlTCU'),
              Text(
                text,
                textAlign: TextAlign.center,
                style: const TextStyle(
                  color: Colors.red,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void main() {
  ErrorWidget.builder = (FlutterErrorDetails details) {
    bool isInDebugMode = false;
    assert(() {
      isInDebugMode = true;
      return true;
    }());
    final message = details.exception.toString();
    if (isInDebugMode) {
      return MyErrorWidget(text: message);
    } else {
      return Text(
        message,
        textAlign: TextAlign.center,
      );
    }
  };

  runApp(
    const MaterialApp(
      home: HomePage(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Error Widget in Flutter'),
      ),
      body: Builder(
        builder: (context) {
          throw Exception(
              'Here is an exception that is caught by our custom Error Widget in Flutter');
        },
      ),
    );
  }
}

Handle Multiple Future Errors in Dart



import 'dart:developer' as dev show log;

Future<Iterable<T>> waitOn<T>(
  Iterable<Future<T>> futures,
  Function onError,
) async {
  List<T> result = [];
  for (final future in futures) {
    final value = await future.catchError(onError);
    result.add(value);
  }
  return result;
}

void testIt() async {
  final f1 = Future.error('First Error');
  final f2 = Future.delayed(const Duration(seconds: 2), () => 10);
  final f3 = Future.error('Second error');
  final f4 = Future.delayed(const Duration(seconds: 2), () => 'Hello world');
  final result = await waitOn([f1, f2, f3, f4], (error) => -1);
  dev.log(result.toString()); // [-1, 10, -1, Hello world]
}

Future Error Handling in Dart

import 'dart:developer' as dev show log;

extension OnError<T> on Future<T> {
  Future<T> onErrorJustReturn(T value) => catchError((_) => value);
}

Future<bool> isUserRegistered({required String email}) => HttpClient()
    .postUrl(Uri.parse('https://website'))
    .then((req) {
      req.headers.add('email', email);
      return req.close();
    })
    .then((resp) => resp.statusCode == 200)
    .onErrorJustReturn(false);

void testIt() async {
  final isFooRegistered = await isUserRegistered(email: 'foo@flutter.com');
  dev.log(isFooRegistered.toString());
}

String to Toast in Flutter



extension Toast on String {
  Future<void> showAsToast(BuildContext context,
      {required Duration duration}) async {
    final scaffold = ScaffoldMessenger.of(context);
    final controller = scaffold.showSnackBar(
      SnackBar(
        content: Text(this),
        backgroundColor: const Color(0xFF24283b),
        behavior: SnackBarBehavior.floating,
        elevation: 2.0,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
        ),
      ),
    );
    await Future.delayed(duration);
    controller.close();
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: TextButton(
          onPressed: () => 'Hello, World!'.showAsToast(
            context,
            duration: const Duration(seconds: 2),
          ),
          child: const Text('Show the snackbar'),
        ),
      ),
    );
  }
}

Waiting in Dart



Future<void> wait(Duration d) async {
  await Future.delayed(d);
}

extension Wait on int {
  Future<void> get seconds => wait(Duration(seconds: this));
  Future<void> get minutes => wait(Duration(minutes: this));
}

void testIt() async {
  await 2.seconds;
  'After 2 seconds'.log();
  await 3.minutes;
  'After 3 minutes'.log();
}

extension Log on Object {
  void log() {
    dev.log(toString());
  }
}

Loading Dialog in Flutter



typedef CloseDialog = void Function();

CloseDialog showLoadingScreen({
  required BuildContext context,
  required String text,
}) {
  final dialog = AlertDialog(
    content: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        const CircularProgressIndicator(),
        const SizedBox(height: 10),
        Text(text),
      ],
    ),
  );
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (_) => dialog,
  );

  return () => Navigator.of(context).pop();
}

void testIt(BuildContext context) async {
  final closeDialog = showLoadingScreen(
    context: context,
    text: 'Loading data...',
  );
  await Future.delayed(const Duration(seconds: 2));
  closeDialog();
}

Compact Map on Map<K,V> in Dart



const foo = 'foo';
const bar = 'bar';
const baz = 'baz';

const namesAndAges = {
  foo: 20,
  bar: 25,
  baz: 18,
};

const acceptedNames = [
  foo,
  bar,
];

void testIt() {
  final acceptedAges = namesAndAges.compactMap(
    (e) => acceptedNames.contains(e.key) ? e.value : null,
  );
  acceptedAges.log(); // [20, 25]
}

extension CompactMap<T, E> on Map<T, E> {
  Iterable<V> compactMap<V>(V? Function(MapEntry<T, E>) f) sync* {
    for (final entry in entries) {
      final extracted = f(entry);
      if (extracted != null) {
        yield extracted;
      }
    }
  }
}

Query Parameters in Dart



import 'dart:developer' as devtools show log;

const host = 'freecurrencyapi.net';
const path = '/api/v2/latest';
const apiKey = 'YOUR_API_KEY';
const baseCurrency = 'sek';
const params = {
  'apiKey': apiKey,
  'base_currency': 'sek',
};

void insteadOfThis() {
  const url = 'https://$host$path?apiKey=$apiKey&base_currency=$baseCurrency';
  url.log();
}

void doThis() {
  final url = Uri.https(host, path, params);
  url.log();
}

extension Log on Object {
  void log() {
    devtools.log(toString());
  }
}

Multiple Gradients in Container in Flutter



typedef GradientContainersBuilder = Map<LinearGradient, Widget?> Function();

class GradientContainers extends StatelessWidget {
  final GradientContainersBuilder builder;

  const GradientContainers({
    Key? key,
    required this.builder,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: builder().entries.map((mapEntry) {
        final gradient = mapEntry.key;
        final widget = mapEntry.value;
        return GradientContainer(
          gradient: gradient,
          child: widget,
        );
      }).toList(),
    );
  }
}

class GradientContainer extends StatelessWidget {
  final LinearGradient gradient;
  final Widget? child;

  const GradientContainer({Key? key, required this.gradient, this.child})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Positioned.fill(
      child: Container(
        decoration: BoxDecoration(
          gradient: gradient,
        ),
        child: child,
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GradientContainers(
        builder: () => {
          topLeftToBottomRightGradient: null,
          rightToLeftGradient: null,
          leftToRightGradinet: null,
          bottomRightGradient: Image.network('https://bit.ly/3otHHog'),
        },
      ),
    );
  }
}

const transparent = Color(0x00FFFFFF);

const topLeftToBottomRightGradient = LinearGradient(
  begin: Alignment.topLeft,
  end: Alignment.bottomRight,
  colors: [
    Color(0xff2ac3de),
    transparent,
  ],
);

const bottomRightGradient = LinearGradient(
  begin: Alignment.bottomRight,
  end: Alignment.topLeft,
  colors: [
    Color(0xffbb9af7),
    transparent,
  ],
);

const rightToLeftGradient = LinearGradient(
  begin: Alignment.centerRight,
  end: Alignment.centerLeft,
  colors: [
    Color(0xff9ece6a),
    transparent,
  ],
);

const leftToRightGradinet = LinearGradient(
  begin: Alignment.centerLeft,
  end: Alignment.centerRight,
  colors: [
    Color(0xff7dcfff),
    transparent,
  ],
);

void main() {
  runApp(
    const MaterialApp(
      home: HomePage(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

Filter on Stream<List<T>> in Dart



import 'dart:developer' as devtools show log;

extension Filter<T> on Stream<List<T>> {
  Stream<List<T>> filter(bool Function(T) where) =>
      map((items) => items.where(where).toList());
}

final Stream<List<int>> allNumbers = Stream.periodic(
  const Duration(seconds: 1),
  (value) => [for (var i = 0; i < value; i++) i],
);

bool isEven(num value) => value % 2 == 0;
bool isOdd(num value) => value % 2 != 0;

extension EvenOdd<E extends num> on Stream<List<E>> {
  Stream<List<E>> get evenNumbers => filter(isEven);
  Stream<List<E>> get oddNumbers => filter(isOdd);
}

void readEvenNumbers() async {
  await for (final evenNumber in allNumbers.evenNumbers) {
    devtools.log('All even numbers: $evenNumber');
  }
}

void readOddNumbers() async {
  await for (final oddNumber in allNumbers.oddNumbers) {
    devtools.log('All odd numbers: $oddNumber');
  }
}

Generic Route Arguments in Flutter

extension GetArgument on BuildContext {
  T? getArgument<T>() {
    final modalRoute = ModalRoute.of(this);
    if (modalRoute != null) {
      final args = modalRoute.settings.arguments;
      if (args != null && args is T) {
        return args as T;
      }
    }
    return null;
  }
}

Generic Dialog in Flutter

typedef DialogOptionBuilder<T> = Map<String, T> Function();

Future<T?> showGenericDialog<T>({
  required BuildContext context,
  required String title,
  required String content,
  required DialogOptionBuilder optionsBuilder,
}) {
  final options = optionsBuilder();
  return showDialog<T>(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text(title),
        content: Text(content),
        actions: options.keys.map(
          (optionTitle) {
            final T value = options[optionTitle];
            return TextButton(
              onPressed: () {
                Navigator.of(context).pop(value);
              },
              child: Text(optionTitle),
            );
          },
        ).toList(),
      );
    },
  );
}

Future<bool> showLogOutDialog(BuildContext context) {
  return showGenericDialog<bool>(
    context: context,
    title: 'Log out',
    content: 'Are you sure you want to log out?',
    optionsBuilder: () => {
      'Cancel': false,
      'Log out': true,
    },
  ).then(
    (value) => value ?? false,
  );
}

GitHub API in Flutter



import 'dart:io' show HttpHeaders, HttpClient;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:convert' show utf8, json;

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

Future<Iterable<GithubUser>> getGithubFollowers(String accessToken) =>
    HttpClient()
        .getUrl(Uri.parse('https://api.github.com/user/followers'))
        .then((req) {
          req.headers
            ..set(HttpHeaders.authorizationHeader, 'Bearer $accessToken')
            ..set(HttpHeaders.contentTypeHeader, 'application/json');
          return req.close();
        })
        .then((resp) => resp.transform(utf8.decoder).join())
        .then((jsonStr) => json.decode(jsonStr) as List<dynamic>)
        .then(
          (jsonArray) => jsonArray.compactMap((element) {
            if (element is Map<String, dynamic>) {
              return element;
            } else {
              return null;
            }
          }),
        )
        .then(
          (listOfMaps) => listOfMaps.map(
            (map) => GithubUser.fromJson(map),
          ),
        );

class GithubUser {
  final String username;
  final String avatarUrl;

  GithubUser.fromJson(Map<String, dynamic> json)
      : username = json['login'] as String,
        avatarUrl = json['avatar_url'] as String;
}

extension CompactMap<T> on List<T> {
  List<E> compactMap<E>(E? Function(T element) f) {
    Iterable<E> imp(E? Function(T element) f) sync* {
      for (final value in this) {
        final mapped = f(value);
        if (mapped != null) {
          yield mapped;
        }
      }
    }

    return imp(f).toList();
  }
}

const token = 'PUT_YOUR_TOKEN_HERE';

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('GitHub API in Flutter'),
      ),
      body: FutureBuilder(
        future: getGithubFollowers(token),
        builder: (context, snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.done:
              final users = (snapshot.data as Iterable<GithubUser>).toList();
              return ListView.builder(
                itemCount: users.length,
                itemBuilder: (context, index) {
                  final user = users[index];
                  return ListTile(
                    title: Text(user.username),
                    leading: CircularAvatar(url: user.avatarUrl),
                  );
                },
              );
            default:
              return const CircularProgressIndicator();
          }
        },
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: HomePage(),
      debugShowCheckedModeBanner: false,
    ),
  );
}

ChangeNotifier in Flutter



import 'package:provider/provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

const allImages = [
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class ImageData {
  final Uint8List imageData;
  const ImageData(this.imageData);
}

class Images extends ChangeNotifier {
  final List<ImageData> _items = [];

  var _isLoading = false;
  bool get isLoading => _isLoading;

  UnmodifiableListView<ImageData> get items => UnmodifiableListView(_items);

  void loadNextImage() async {
    if (_items.length < allImages.length) {
      // time to load more
      _isLoading = true;
      notifyListeners();
      final imageUrl = allImages[_items.length];
      final networkAsset = NetworkAssetBundle(Uri.parse(imageUrl));
      final loaded = await networkAsset.load(imageUrl);
      final bytes = loaded.buffer.asUint8List();
      final imageData = ImageData(bytes);
      _items.insert(0, imageData);
      _isLoading = false;
      notifyListeners();
    } else {
      if (isLoading) {
        _isLoading = false;
        notifyListeners();
      }
    }
  }
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ChangeNotifier in Flutter'),
        actions: [
          Consumer<Images>(
            builder: (context, value, child) {
              return IconButton(
                onPressed: () {
                  value.loadNextImage();
                },
                icon: const Icon(Icons.add_box_outlined),
              );
            },
          )
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Consumer<Images>(
          builder: (context, value, child) {
            final images = value.items;
            final isLoading = value.isLoading;
            return ListView.builder(
              itemBuilder: (context, index) {
                if (index == 0 && isLoading) {
                  return Center(
                    child: Column(
                      children: const [
                        CircularProgressIndicator(),
                        SizedBox(height: 16.0),
                      ],
                    ),
                  );
                } else {
                  final imageIndex = isLoading ? index - 1 : index;
                  final imageData = images[imageIndex].imageData;
                  return Column(
                    children: [
                      RoundedImageWithShadow(imageData: imageData),
                      const SizedBox(height: 16.0),
                    ],
                  );
                }
              },
              itemCount: isLoading ? images.length + 1 : images.length,
            );
          },
        ),
      ),
    );
  }
}

class RoundedImageWithShadow extends StatelessWidget {
  final Uint8List imageData;
  const RoundedImageWithShadow({Key? key, required this.imageData})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            blurRadius: 2,
            color: Colors.black.withAlpha(40),
            spreadRadius: 2,
          ),
        ],
      ),
      child: Image.memory(
        imageData,
        fit: BoxFit.cover,
      ),
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: ChangeNotifierProvider(
        create: (_) => Images(),
        child: const HomePage(),
      ),
      debugShowCheckedModeBanner: false,
    ),
  );
}

Refresh Indicator in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

const allImages = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _images = [allImages.first];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Refresh Indicator in Flutter'),
      ),
      body: RefreshIndicator(
        onRefresh: () async {
          final nextIndex = _images.length + 1;
          if (nextIndex < allImages.length) {
            setState(() {
              _images.insert(0, allImages[nextIndex]);
            });
          }
        },
        child: ListView.builder(
          physics: const AlwaysScrollableScrollPhysics(),
          padding: const EdgeInsets.all(16),
          itemCount: _images.length,
          itemBuilder: (context, index) {
            final imageUrl = _images[index];
            return Column(
              children: [
                RoundedImageWithShadow(url: imageUrl),
                const SizedBox(height: 16),
              ],
            );
          },
        ),
      ),
    );
  }
}

class RoundedImageWithShadow extends StatelessWidget {
  final String url;
  const RoundedImageWithShadow({Key? key, required this.url}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            blurRadius: 2,
            color: Colors.black.withAlpha(40),
            spreadRadius: 2,
          ),
        ],
      ),
      child: Image.network(url),
    );
  }
}

FlatMap in Dart



extension FlatMap<T> on T? {
  E? flatMap<E>(E? Function(T) f) => this != null ? f(this!) : null;
}

AuthUser? get insteadOfThis {
  final user = FirebaseAuth.instance.currentUser;
  if (user != null) {
    return AuthUser.fromFirebase(user);
  } else {
    return null;
  }
}

AuthUser? get doThis =>
    FirebaseAuth.instance.currentUser.flatMap((u) => AuthUser.fromFirebase(u));

OrientationBuilder in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class RoundedImageWithShadow extends StatelessWidget {
  final String url;
  const RoundedImageWithShadow({Key? key, required this.url}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            blurRadius: 2,
            color: Colors.black.withAlpha(40),
            spreadRadius: 2,
          ),
        ],
      ),
      child: Image.network(url),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: OrientationBuilder(
          builder: (context, orientation) {
            final int count;
            switch (orientation) {
              case Orientation.portrait:
                count = 2;
                break;
              case Orientation.landscape:
                count = 4;
                break;
            }
            return GridView.count(
              padding: const EdgeInsets.all(8.0),
              crossAxisCount: count,
              mainAxisSpacing: 8.0,
              crossAxisSpacing: 8.0,
              children: images
                  .map((url) => RoundedImageWithShadow(url: url))
                  .toList(),
            );
          },
        ),
      ),
    );
  }
}

final images = [
  'https://bit.ly/3qJ2FCf',
  'https://bit.ly/3Hs9JsV',
  'https://bit.ly/3cfT6Cv',
  'https://bit.ly/30wGnIE',
  'https://bit.ly/3kJYsum',
  'https://bit.ly/3oDoMaJ',
  'https://bit.ly/3FndXQM',
  'https://bit.ly/3ci4i1f',
];

Linear Gradient in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Linear Gradient in Flutter'),
      ),
      body: const ImageWithShadow(url: 'https://bit.ly/3otHHog'),
    );
  }
}

class ImageWithShadow extends StatelessWidget {
  final String url;

  const ImageWithShadow({
    Key? key,
    required this.url,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Stack(
        children: [
          Positioned.fill(
            child: Container(
              decoration: BoxDecoration(
                boxShadow: [
                  BoxShadow(
                    blurRadius: 10.0,
                    color: Colors.black.withOpacity(0.5),
                    offset: const Offset(0.0, 3.0),
                  )
                ],
                borderRadius: const BorderRadius.all(Radius.circular(20)),
                gradient: const LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  colors: [
                    Color.fromARGB(255, 176, 229, 251),
                    Color.fromARGB(255, 235, 202, 250)
                  ],
                ),
              ),
            ),
          ),
          Image.network(url),
        ],
      ),
    );
  }
}

Bloc Text Editing Controller in Flutter

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

abstract class Event {
  const Event();
}

class SearchEvent extends Event {
  final String searchString;
  const SearchEvent(this.searchString);
}

class ClearSearch extends Event {}

class SearchBloc extends Bloc<Event, List<String>> {
  static const names = ['foo', 'bar', 'baz'];

  SearchBloc() : super(names) {
    on<Event>((event, emit) {
      if (event is SearchEvent) {
        emit(names
            .where((element) => element.contains(event.searchString))
            .toList());
      } else if (event is ClearSearch) {
        emit(names);
      }
    });
  }
}

class BlocTextEditingController extends TextEditingController {
  SearchBloc? bloc;
  BlocTextEditingController() {
    addListener(() {
      if (text.isEmpty) {
        bloc?.add(ClearSearch());
      } else {
        bloc?.add(SearchEvent(text));
      }
    });
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

const largeStyle = TextStyle(fontSize: 30);

class _HomePageState extends State<HomePage> {
  late final BlocTextEditingController _controller;

  @override
  void initState() {
    _controller = BlocTextEditingController();
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _controller.bloc = BlocProvider.of<SearchBloc>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Bloc Search in Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: BlocBuilder<SearchBloc, List<String>>(
          builder: (context, state) {
            return ListView.builder(
              itemBuilder: (context, index) {
                if (index == 0) {
                  // search field
                  return TextField(
                    decoration: InputDecoration(
                      hintText: 'Enter search term here...',
                      hintStyle: largeStyle,
                    ),
                    style: largeStyle,
                    controller: _controller,
                  );
                } else {
                  final name = state[index - 1];
                  return ListTile(
                    title: Text(
                      name,
                      style: largeStyle,
                    ),
                  );
                }
              },
              itemCount: state.length + 1, // +1 for search
            );
          },
        ),
      ),
    );
  }
}

Blurred TabBar in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

const images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class CustomTabBar extends StatelessWidget {
  final List<IconButton> buttons;

  const CustomTabBar({Key? key, required this.buttons}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: ClipRect(
        child: Container(
          height: 80,
          color: Colors.white.withOpacity(0.4),
          child: BackdropFilter(
            filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),
            child: Container(
              width: double.infinity,
              decoration: BoxDecoration(
                color: Colors.white.withOpacity(0.5),
              ),
              child: Padding(
                padding: const EdgeInsets.only(bottom: 15),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: buttons,
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

const summerIcon = Icon(
  Icons.surfing,
  size: 40.0,
  color: Colors.teal,
);

const autumnIcon = Icon(
  Icons.nature_outlined,
  size: 40.0,
  color: Colors.black45,
);

const winterIcon = Icon(
  Icons.snowboarding,
  size: 40.0,
  color: Colors.black45,
);

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Blurred Tab Bar'),
      ),
      body: Stack(
        children: [
          ListView.builder(
            itemCount: images.length,
            itemBuilder: (context, index) {
              final url = images[index];
              return Image.network(url);
            },
          ),
          CustomTabBar(
            buttons: [
              IconButton(
                icon: summerIcon,
                onPressed: () {
                  // implement me
                },
              ),
              IconButton(
                icon: autumnIcon,
                onPressed: () {
                  // implement me
                },
              ),
              IconButton(
                icon: winterIcon,
                onPressed: () {
                  // implement me
                },
              )
            ],
          )
        ],
      ),
    );
  }
}

Play YouTube in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

const videoIds = [
  'BHACKCNDMW8',
  '26h9hBZFl7w',
  'glENND73k4Q',
  'd0tU18Ybcvk',
];

class VideoView extends StatelessWidget {
  final String videoId;
  final _key = UniqueKey();

  VideoView({required this.videoId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Watch a Video'),
      ),
      body: Center(
        child: Container(
          height: 232.0,
          child: WebView(
            key: _key,
            initialUrl: 'https://www.youtube.com/embed/$videoId',
            javascriptMode: JavascriptMode.unrestricted,
          ),
        ),
      ),
    );
  }
}

class YouTubeVideoThumbnail extends StatelessWidget {
  final String videoId;
  final String thumbnailUrl;

  const YouTubeVideoThumbnail({Key? key, required this.videoId})
      : thumbnailUrl = 'https://img.youtube.com/vi/$videoId/maxresdefault.jpg',
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (_) => VideoView(videoId: videoId),
          ),
        );
      },
      child: Container(
        height: 256.0,
        clipBehavior: Clip.antiAlias,
        decoration: BoxDecoration(
          boxShadow: [
            BoxShadow(
              blurRadius: 10.0,
              color: Colors.black.withAlpha(50),
              spreadRadius: 10.0,
            ),
          ],
          borderRadius: BorderRadius.circular(20),
          image: DecorationImage(
            fit: BoxFit.fitHeight,
            image: NetworkImage(thumbnailUrl),
          ),
        ),
        child: Center(
          child: Icon(
            Icons.play_arrow,
            color: Colors.white,
            size: 100.0,
          ),
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('YouTube Videos in Flutter')),
      body: ListView.builder(
        itemCount: videoIds.length,
        itemBuilder: (context, index) {
          final videoId = videoIds[index];
          return Padding(
            padding: const EdgeInsets.all(8.0),
            child: YouTubeVideoThumbnail(videoId: videoId),
          );
        },
      ),
    );
  }
}

ListView Background in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class ListItem {
  const ListItem();
  factory ListItem.emptyTile() => EmptyTile();
  factory ListItem.tile(
    String title,
    String subTitle,
  ) =>
      Tile(
        title,
        subTitle,
      );
}

class Tile extends ListItem {
  final String title;
  final String subTitle;
  const Tile(this.title, this.subTitle) : super();
}

class EmptyTile extends ListItem {}

final items = [
  for (var i = 1; i <= 6; i++) ListItem.tile('Title $i', 'Sub title $i'),
  ListItem.emptyTile(),
  for (var i = 7; i <= 12; i++) ListItem.tile('Title $i', 'Sub title $i'),
];

class Background extends StatelessWidget {
  final Widget child;
  const Background({Key? key, required this.child}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        image: DecorationImage(
          fit: BoxFit.fitHeight,
          image: NetworkImage('https://bit.ly/3jXSDto'),
        ),
      ),
      child: child,
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Background(
        child: ListView.builder(
          padding: EdgeInsets.zero,
          itemCount: items.length,
          itemBuilder: (context, index) {
            final item = items[index];
            if (item is Tile) {
              return Container(
                color: Colors.grey[200],
                child: ListTile(
                  title: Text(item.title),
                  subtitle: Text(item.subTitle),
                ),
              );
            } else if (item is EmptyTile) {
              return SizedBox(
                height: 450,
              );
            } else {
              throw 'unexpcted item';
            }
          },
        ),
      ),
    );
  }
}

Integer to Binary in Dart

extension ToBinary on int {
  String toBinary(
    int len, {
    int separateAtLength = 4,
    String separator = ',',
  }) =>
      toRadixString(2)
          .padLeft(len, '0')
          .splitByLength(separateAtLength)
          .join(separator);
}

void testIt() {
  assert(1.toBinary(8) == '0000,0001');
  assert(2.toBinary(4) == '0010');
  assert(3.toBinary(16) == '0000,0000,0000,0011');
  assert(255.toBinary(8, separateAtLength: 8) == '11111111');
  assert(255.toBinary(8, separateAtLength: 4) == '1111,1111');
}

extension SplitByLength on String {
  Iterable<String> splitByLength(int len, {String filler = '0'}) sync* {
    final missingFromLength =
        length % len == 0 ? 0 : len - (characters.length % len);
    final expectedLength = length + missingFromLength;
    final src = padLeft(expectedLength, filler);
    final chars = src.characters;
    for (var i = 0; i < chars.length; i += len) {
      yield chars.getRange(i, i + len).toString();
    }
  }
}

Split String by Length in Dart



void testIt() {
  assert('dartlang'
      .splitByLength(5, filler: '💙')
      .isEqualTo(['💙💙dar', 'tlang']));

  assert('0100010'.splitByLength(4).isEqualTo(['0010', '0010']));
  assert('foobar'.splitByLength(3).isEqualTo(['foo', 'bar']));
  assert('flutter'.splitByLength(4, filler: 'X').isEqualTo(['Xflu', 'tter']));
  assert('dart'.splitByLength(5, filler: '').isEqualTo(['dart']));
}

extension SplitByLength on String {
  Iterable<String> splitByLength(int len, {String filler = '0'}) sync* {
    final missingFromLength =
        length % len == 0 ? 0 : len - (characters.length % len);
    final expectedLength = length + missingFromLength;
    final src = padLeft(expectedLength, filler);
    final chars = src.characters;
    for (var i = 0; i < chars.length; i += len) {
      yield chars.getRange(i, i + len).toString();
    }
  }
}

Image Tint in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

enum OverlayColor { brown, orange, yellow, green, blue }

extension Color on OverlayColor {
  MaterialColor get color {
    switch (this) {
      case OverlayColor.blue:
        return Colors.blue;
      case OverlayColor.brown:
        return Colors.brown;
      case OverlayColor.green:
        return Colors.green;
      case OverlayColor.orange:
        return Colors.orange;
      case OverlayColor.yellow:
        return Colors.yellow;
    }
  }
}

extension Title on OverlayColor {
  String get title => toString().split('.').last;
}

extension ToTextButtonWithValue on OverlayColor {
  TextButtonWithValue<OverlayColor> toTextButtonWithValue(
      OnTextButtonWithValuePressed onPressed) {
    return TextButtonWithValue(
      value: this,
      onPressed: onPressed,
      child: Text(title),
    );
  }
}

typedef OnTextButtonWithValuePressed<T> = void Function(T value);

class TextButtonWithValue<T> extends StatelessWidget {
  final T value;
  final OnTextButtonWithValuePressed onPressed;
  final Widget child;
  const TextButtonWithValue({
    Key? key,
    required this.value,
    required this.onPressed,
    required this.child,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () {
        onPressed(value);
      },
      child: child,
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  OverlayColor? _overlayColor;

  ColorFilter? getcolorFilter() {
    final overlayColor = _overlayColor;
    if (overlayColor == null) {
      return null;
    }
    return ColorFilter.mode(
      overlayColor.color,
      BlendMode.colorBurn,
    );
  }

  Iterable<Widget> overlayColorButtons() {
    return OverlayColor.values.map((overlayColor) {
      return Expanded(
        flex: 1,
        child: Container(
          child: overlayColor.toTextButtonWithValue(
            (value) {
              setState(() {
                _overlayColor = value;
              });
            },
          ),
        ),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Tinting Images in Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Container(
              height: 250.0,
              clipBehavior: Clip.antiAlias,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(20),
                image: DecorationImage(
                  colorFilter: getcolorFilter(),
                  fit: BoxFit.fitHeight,
                  image: NetworkImage('https://bit.ly/3jOueGG'),
                ),
              ),
            ),
            SizedBox(height: 16.0),
            Row(
              children: overlayColorButtons().toList(),
            )
          ],
        ),
      ),
    );
  }
}

SlideTransition in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late final _controller = AnimationController(
    duration: Duration(milliseconds: 500),
    vsync: this,
  );

  late final _animation = Tween<Offset>(
    begin: Offset(0.0, 0.0),
    end: Offset(-0.83, 0.0),
  ).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInQuint,
    ),
  );

  var _isExpanded = false;

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _controller.forward();
    return Scaffold(
      body: SizedBox.expand(
        child: Stack(
          fit: StackFit.passthrough,
          children: [
            Image.network(
              'https://bit.ly/3BWYDbz',
              fit: BoxFit.fitHeight,
            ),
            Positioned(
              top: 200.0,
              child: SlideTransition(
                position: _animation,
                child: GestureDetector(
                  onTap: () {
                    _isExpanded = !_isExpanded;
                    if (_isExpanded) {
                      _controller.reverse();
                    } else {
                      _controller.forward();
                    }
                  },
                  child: Box(),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class Box extends StatelessWidget {
  const Box({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.blue[200]?.withAlpha(200),
        border: Border.all(
          color: Colors.blue,
          style: BorderStyle.solid,
          width: 1.0,
        ),
        borderRadius: BorderRadius.only(
          topRight: Radius.circular(10),
          bottomRight: Radius.circular(10),
        ),
      ),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          children: [
            Text(
              'By: Jesper Anhede',
              style: TextStyle(
                fontSize: 18.0,
              ),
            ),
            SizedBox(width: 10.0),
            Icon(
              Icons.info,
              color: Colors.pink[400],
            ),
          ],
        ),
      ),
    );
  }
}

Expansion Panels and Lists in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class Event {
  final String title;
  final String details;
  final String imageUrl;
  bool isExpanded = false;

  Event({
    required this.title,
    required this.details,
    required this.imageUrl,
  });

  @override
  bool operator ==(covariant Event other) => title == other.title;
}

const diwaliDetails =
    '''Diwali, or Dipawali, is India's biggest and most important holiday of the year. The festival gets its name from the row (avali) of clay lamps (deepa) that Indians light outside their homes to symbolize the inner light that protects from spiritual darkness. This festival is as important to Hindus as the Christmas holiday is to Christians.''';

const halloweenDetails =
    '''Halloween or Hallowe'en, less commonly known as Allhalloween, All Hallows' Eve, or All Saints' Eve, is a celebration observed in many countries on 31 October, the eve of the Western Christian feast of All Hallows' Day.''';

final events = [
  Event(
    title: 'Diwali',
    details: diwaliDetails,
    imageUrl: 'https://bit.ly/3mGg8YW',
  ),
  Event(
    title: 'Halloween',
    details: halloweenDetails,
    imageUrl: 'https://bit.ly/3wb1w7j',
  ),
];

extension ToPanel on Event {
  ExpansionPanel toPanel() {
    return ExpansionPanel(
      headerBuilder: (context, isExpanded) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            title,
            style: TextStyle(fontSize: 30.0),
          ),
        );
      },
      isExpanded: isExpanded,
      body: Container(
        height: 250,
        width: double.infinity,
        decoration: BoxDecoration(
          image: DecorationImage(
            fit: BoxFit.fitWidth,
            colorFilter: ColorFilter.mode(
              Colors.black.withOpacity(0.5),
              BlendMode.luminosity,
            ),
            image: NetworkImage(imageUrl),
          ),
        ),
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              details,
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 20, color: Colors.white, shadows: [
                Shadow(
                  blurRadius: 1.0,
                  offset: Offset.zero,
                  color: Colors.black,
                )
              ]),
            ),
          ),
        ),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Expansion Panels in Flutter'),
      ),
      body: SingleChildScrollView(
        child: ExpansionPanelList(
          children: events.map((e) => e.toPanel()).toList(),
          expansionCallback: (panelIndex, isExpanded) {
            setState(() {
              events[panelIndex].isExpanded = !isExpanded;
            });
          },
        ),
      ),
    );
  }
}

Complete CRUD App in Flutter

//Want to support my work 🤝? https://buymeacoffee.com/vandad

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart'
    show getApplicationDocumentsDirectory;

class Person implements Comparable {
  final int id;
  final String firstName;
  final String lastName;
  const Person(this.id, this.firstName, this.lastName);

  String get fullName => '$firstName $lastName';

  Person.fromData(Map<String, Object?> data)
      : id = data['ID'] as int,
        firstName = data['FIRST_NAME'] as String,
        lastName = data['LAST_NAME'] as String;

  @override
  int compareTo(covariant Person other) => other.id.compareTo(id);

  @override
  bool operator ==(covariant Person other) => id == other.id;

  @override
  String toString() =>
      'Person, ID = $id, firstName = $firstName, lastName = $lastName';
}

class PersonDB {
  final _controller = StreamController<List<Person>>.broadcast();
  List<Person> _persons = [];
  Database? _db;
  final String dbName;
  PersonDB({required this.dbName});

  Future<bool> close() async {
    final db = _db;
    if (db == null) {
      return false;
    }
    await db.close();
    return true;
  }

  Future<bool> open() async {
    if (_db != null) {
      return true;
    }
    final directory = await getApplicationDocumentsDirectory();
    final path = '${directory.path}/$dbName';
    try {
      final db = await openDatabase(path);
      _db = db;

      // create the table if it doesn't exist

      final create = '''CREATE TABLE IF NOT EXISTS PEOPLE (
          ID INTEGER PRIMARY KEY AUTOINCREMENT,
          FIRST_NAME STRING NOT NULL,
          LAST_NAME STRING NOT NULL
        )''';

      await db.execute(create);

      // if everything went fine, we then read all the objects
      // and populate the stream

      _persons = await _fetchPeople();
      _controller.add(_persons);
      return true;
    } catch (e) {
      print('error = $e');
      return false;
    }
  }

  Future<List<Person>> _fetchPeople() async {
    final db = _db;
    if (db == null) {
      return [];
    }

    try {
      // read the existing data if any
      final readResult = await db.query(
        'PEOPLE',
        distinct: true,
        columns: ['ID', 'FIRST_NAME', 'LAST_NAME'],
        orderBy: 'ID',
      );

      final people = readResult.map((row) => Person.fromData(row)).toList();
      return people;
    } catch (e) {
      print('error = $e');
      return [];
    }
  }

  Future<bool> delete(Person person) async {
    final db = _db;
    if (db == null) {
      return false;
    }
    try {
      final deletedCount = await db.delete(
        'PEOPLE',
        where: 'ID = ?',
        whereArgs: [person.id],
      );

      // delete it locally as well

      if (deletedCount == 1) {
        _persons.remove(person);
        _controller.add(_persons);
        return true;
      } else {
        return false;
      }
    } catch (e) {
      print('Error inserting $e');
      return false;
    }
  }

  Future<bool> create(String firstName, String lastName) async {
    final db = _db;
    if (db == null) {
      return false;
    }
    try {
      final id = await db.insert(
        'PEOPLE',
        {
          'FIRST_NAME': firstName,
          'LAST_NAME': lastName,
        },
      );

      final person = Person(id, firstName, lastName);
      _persons.add(person);
      _controller.add(_persons);

      return true;
    } catch (e) {
      print('Error inserting $e');
      return false;
    }
  }

  // uses the person's id to update its first name and last name
  Future<bool> update(Person person) async {
    final db = _db;
    if (db == null) {
      return false;
    }
    try {
      final updatedCount = await db.update(
        'PEOPLE',
        {
          'FIRST_NAME': person.firstName,
          'LAST_NAME': person.lastName,
        },
        where: 'ID = ?',
        whereArgs: [person.id],
      );

      if (updatedCount == 1) {
        _persons.removeWhere((p) => p.id == person.id);
        _persons.add(person);
        _controller.add(_persons);
        return true;
      } else {
        return false;
      }
    } catch (e) {
      print('Error inserting $e');
      return false;
    }
  }

  Stream<List<Person>> all() =>
      _controller.stream.map((event) => event..sort());
}

typedef OnCompose = void Function(String firstName, String lastName);

class ComposeWidget extends StatefulWidget {
  final OnCompose onCompose;

  const ComposeWidget({Key? key, required this.onCompose}) : super(key: key);

  @override
  State<ComposeWidget> createState() => _ComposeWidgetState();
}

class _ComposeWidgetState extends State<ComposeWidget> {
  final firstNameController = TextEditingController();
  final lastNameController = TextEditingController();

  @override
  void dispose() {
    firstNameController.dispose();
    lastNameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(children: [
        TextField(
          style: TextStyle(fontSize: 24),
          decoration: InputDecoration(
            hintText: 'Enter first name',
          ),
          controller: firstNameController,
        ),
        TextField(
          style: TextStyle(fontSize: 24),
          decoration: InputDecoration(
            hintText: 'Enter last name',
          ),
          controller: lastNameController,
        ),
        TextButton(
          onPressed: () {
            final firstName = firstNameController.text;
            final lastName = lastNameController.text;
            widget.onCompose(firstName, lastName);
          },
          child: Text(
            'Add to list',
            style: TextStyle(fontSize: 24),
          ),
        ),
      ]),
    );
  }
}

class HomePage extends StatefulWidget {
  HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late final PersonDB _crudStorage;

  @override
  void initState() {
    _crudStorage = PersonDB(dbName: 'db.sqlite');
    _crudStorage.open();
    super.initState();
  }

  @override
  void dispose() {
    _crudStorage.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SQLite in Flutter'),
      ),
      body: StreamBuilder(
        stream: _crudStorage.all(),
        builder: (context, snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.active:
            case ConnectionState.waiting:
              if (snapshot.data == null) {
                return Center(child: CircularProgressIndicator());
              }
              final people = snapshot.data as List<Person>;
              return Column(
                children: [
                  ComposeWidget(
                    onCompose: (firstName, lastName) async {
                      await _crudStorage.create(firstName, lastName);
                    },
                  ),
                  Expanded(
                    child: ListView.builder(
                      itemCount: people.length,
                      itemBuilder: (context, index) {
                        final person = people[index];
                        return ListTile(
                          onTap: () async {
                            final update =
                                await showUpdateDialog(context, person);
                            if (update == null) {
                              return;
                            }
                            await _crudStorage.update(update);
                          },
                          title: Text(
                            person.fullName,
                            style: TextStyle(fontSize: 24),
                          ),
                          subtitle: Text(
                            'ID: ${person.id}',
                            style: TextStyle(fontSize: 18),
                          ),
                          trailing: TextButton(
                            onPressed: () async {
                              final shouldDelete =
                                  await showDeleteDialog(context);
                              if (shouldDelete) {
                                await _crudStorage.delete(person);
                              }
                            },
                            child: Icon(
                              Icons.disabled_by_default_rounded,
                              color: Colors.red,
                            ),
                          ),
                        );
                      },
                    ),
                  ),
                ],
              );
            default:
              return Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }
}

final firstNameController = TextEditingController();
final lastNameController = TextEditingController();

Future<Person?> showUpdateDialog(BuildContext context, Person person) {
  firstNameController.text = person.firstName;
  lastNameController.text = person.lastName;
  return showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('Enter your udpated values here:'),
            TextField(controller: firstNameController),
            TextField(controller: lastNameController),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop(null);
            },
            child: Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              final newPerson = Person(
                person.id,
                firstNameController.text,
                lastNameController.text,
              );
              Navigator.of(context).pop(newPerson);
            },
            child: Text('Save'),
          ),
        ],
      );
    },
  ).then((value) {
    if (value is Person) {
      return value;
    } else {
      return null;
    }
  });
}

Future<bool> showDeleteDialog(BuildContext context) {
  return showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        content: Text('Are you sure you want to delete this item?'),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop(false);
            },
            child: Text('No'),
          ),
          TextButton(
            onPressed: () {
              Navigator.of(context).pop(true);
            },
            child: Text('Delete'),
          )
        ],
      );
    },
  ).then(
    (value) {
      if (value is bool) {
        return value;
      } else {
        return false;
      }
    },
  );
}

SQLite Storage in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart'
    show getApplicationDocumentsDirectory;

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class Person implements Comparable {
  final int id;
  final String firstName;
  final String lastName;
  const Person(this.id, this.firstName, this.lastName);

  String get fullName => '$firstName $lastName';

  Person.fromData(Map<String, Object?> data)
      : id = data['ID'] as int,
        firstName = data['FIRST_NAME'] as String,
        lastName = data['LAST_NAME'] as String;

  @override
  int compareTo(covariant Person other) => other.id.compareTo(id);
}

typedef OnCompose = void Function(String firstName, String lastName);

class ComposeWidget extends StatefulWidget {
  final OnCompose onCompose;

  const ComposeWidget({Key? key, required this.onCompose}) : super(key: key);

  @override
  State<ComposeWidget> createState() => _ComposeWidgetState();
}

class _ComposeWidgetState extends State<ComposeWidget> {
  final firstNameController = TextEditingController();
  final lastNameController = TextEditingController();

  @override
  void dispose() {
    firstNameController.dispose();
    lastNameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(children: [
        TextField(
          style: TextStyle(fontSize: 24),
          decoration: InputDecoration(
            hintText: 'Enter first name',
          ),
          controller: firstNameController,
        ),
        TextField(
          style: TextStyle(fontSize: 24),
          decoration: InputDecoration(
            hintText: 'Enter last name',
          ),
          controller: lastNameController,
        ),
        TextButton(
          onPressed: () {
            final firstName = firstNameController.text;
            final lastName = lastNameController.text;
            widget.onCompose(firstName, lastName);
          },
          child: Text(
            'Add to list',
            style: TextStyle(fontSize: 24),
          ),
        ),
      ]),
    );
  }
}

class _HomePageState extends State<HomePage> {
  late final Database db;
  bool hasSetUpAlready = false;

  Future<bool> setupDatabase() async {
    if (hasSetUpAlready == false) {
      final directory = await getApplicationDocumentsDirectory();
      final path = '${directory.path}/db.sqlite';
      try {
        db = await openDatabase(path);

        // create the table if it doesn't exist

        final create = '''CREATE TABLE IF NOT EXISTS PEOPLE (
          ID INTEGER PRIMARY KEY AUTOINCREMENT,
          FIRST_NAME STRING NOT NULL,
          LAST_NAME STRING NOT NULL
        )''';

        await db.execute(create);
        hasSetUpAlready = true;
        return true;
      } catch (e) {
        print('error = $e');
        hasSetUpAlready = false;
        return false;
      }
    } else {
      return true;
    }
  }

  Future<List<Person>> fetchPersons() async {
    if (!await setupDatabase()) {
      return [];
    }

    try {
      // read the existing data if any
      final readResult = await db.query(
        'PEOPLE',
        distinct: true,
        columns: ['ID', 'FIRST_NAME', 'LAST_NAME'],
        orderBy: 'ID',
      );

      final people = readResult.map((row) => Person.fromData(row)).toList()
        ..sort();
      return people;
    } catch (e) {
      print('error = $e');
      return [];
    }
  }

  Future<bool> addPerson(String firstName, String lastName) async {
    try {
      await db.insert(
        'PEOPLE',
        {
          'FIRST_NAME': firstName,
          'LAST_NAME': lastName,
        },
      );
      return true;
    } catch (e) {
      print('Error inserting $e');
      return false;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SQLite in Flutter'),
      ),
      body: FutureBuilder(
        future: fetchPersons(),
        builder: (context, snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.done:
              final people = snapshot.data as List<Person>;
              return Column(
                children: [
                  ComposeWidget(
                    onCompose: (firstName, lastName) async {
                      await addPerson(firstName, lastName);
                      setState(() {});
                    },
                  ),
                  Expanded(
                    child: ListView.builder(
                      itemCount: people.length,
                      itemBuilder: (context, index) {
                        final person = people[index];
                        return ListTile(
                          title: Text(
                            person.fullName,
                            style: TextStyle(fontSize: 24),
                          ),
                          subtitle: Text(
                            'ID: ${person.id}',
                            style: TextStyle(fontSize: 18),
                          ),
                        );
                      },
                    ),
                  ),
                ],
              );
            default:
              return CircularProgressIndicator();
          }
        },
      ),
    );
  }
}

Circular Progress with Percentage in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class SizedCircularProgressIndicator extends StatelessWidget {
  final double progress;
  final double width;
  final double height;
  final TextStyle? textStyle;
  const SizedCircularProgressIndicator({
    Key? key,
    this.textStyle,
    required this.progress,
    required this.width,
    required this.height,
  }) : super(key: key);

  TextStyle get style => textStyle ?? TextStyle(fontSize: 30.0);
  int get _progress => (progress * 100.0).toInt();

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        Text('$_progress%', style: style),
        SizedBox(
          width: width,
          height: height,
          child: CircularProgressIndicator(
            backgroundColor: Colors.white70,
            value: progress,
            color: Colors.blueAccent,
            strokeWidth: 3.0,
          ),
        ),
      ],
    );
  }
}

const images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: images.length,
        itemBuilder: (context, index) {
          return WithBlurredBackground(imageUrl: images[index]);
        },
      ),
    );
  }
}

class WithBlurredBackground extends StatelessWidget {
  final String imageUrl;

  const WithBlurredBackground({Key? key, required this.imageUrl})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox.expand(
      child: Stack(
        alignment: Alignment.center,
        fit: StackFit.passthrough,
        children: [
          SizedBox.expand(
            child: ClipRect(
              child: ImageFiltered(
                imageFilter: ImageFilter.blur(
                  sigmaX: 10.0,
                  sigmaY: 10.0,
                ),
                child: Image.network(
                  imageUrl,
                  fit: BoxFit.fitHeight,
                ),
              ),
            ),
          ),
          NetworkImageWithProgress(url: imageUrl),
        ],
      ),
    );
  }
}

class NetworkImageWithProgress extends StatelessWidget {
  final String url;

  const NetworkImageWithProgress({Key? key, required this.url})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Image.network(
      url,
      loadingBuilder: (context, child, loadingProgress) {
        final totalBytes = loadingProgress?.expectedTotalBytes;
        final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
        if (totalBytes != null && bytesLoaded != null) {
          return SizedCircularProgressIndicator(
            height: 100,
            width: 100,
            progress: bytesLoaded / totalBytes,
          );
        } else {
          return child;
        }
      },
      errorBuilder: (context, error, stackTrace) {
        return Text('Error!');
      },
    );
  }
}

Opening URLs in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

class Person {
  final String profileUrl;
  final String name;
  final String email;
  final String phoneNumber;
  final String websiteUrl;

  const Person({
    Key? key,
    required this.profileUrl,
    required this.name,
    required this.email,
    required this.phoneNumber,
    required this.websiteUrl,
  });

  Person.vandad()
      : profileUrl = 'https://bit.ly/3jwusS0',
        name = 'Vandad Nahavandipoor',
        email = 'vandad.np@gmail.com',
        phoneNumber = '071234567',
        websiteUrl = 'https://pixolity.se';
}

void open(String url) async {
  if (await canLaunch(url)) {
    await launch(url);
  }
}

class PersonNameView extends StatelessWidget {
  final Person person;
  const PersonNameView(this.person, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      person.name,
      style: TextStyle(
        fontSize: 20,
        fontWeight: FontWeight.bold,
        color: Colors.black,
      ),
    );
  }
}

class PersonEmailView extends StatelessWidget {
  final Person person;
  const PersonEmailView(this.person, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () {
        open('mailto:${person.email}');
      },
      child: Text(
        '💌 Email me',
        style: TextStyle(fontSize: 20.0),
      ),
    );
  }
}

class PersonPhoneView extends StatelessWidget {
  final Person person;
  const PersonPhoneView(this.person, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () {
        open('tel://${person.phoneNumber}');
      },
      child: Text(
        '🤙🏻 Call me',
        style: TextStyle(fontSize: 20.0),
      ),
    );
  }
}

class PersonWebsiteView extends StatelessWidget {
  final Person person;
  const PersonWebsiteView(this.person, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () {
        open(person.websiteUrl);
      },
      child: Text(
        '🌍 Visit my website',
        style: TextStyle(fontSize: 20.0),
      ),
    );
  }
}

const bigText = TextStyle(fontSize: 20.0);

class PersonView extends StatelessWidget {
  final Person person;
  const PersonView({Key? key, required this.person}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
          color: Colors.blue[50],
          borderRadius: BorderRadius.circular(16.0),
          border: Border.all(color: Colors.white, width: 3.0),
          boxShadow: [
            BoxShadow(
              blurRadius: 30.0,
              color: Colors.black.withAlpha(50),
              spreadRadius: 20.0,
            ),
          ]),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircleAvatar(
              radius: 100.0,
              backgroundImage: NetworkImage(person.profileUrl),
            ),
            SizedBox(height: 10),
            PersonNameView(person),
            PersonEmailView(person),
            PersonPhoneView(person),
            PersonWebsiteView(person)
          ],
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SingleChildScrollView(
          child: PersonView(
            person: Person.vandad(),
          ),
        ),
      ),
    );
  }
}

Commodore 64 Screen in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

const textColor = Color.fromRGBO(156, 156, 247, 1);

class BoxPainter extends CustomPainter {
  final bool isBlinking;
  BoxPainter({required this.isBlinking});

  @override
  void paint(Canvas canvas, Size size) {
    if (isBlinking) {
      final rect = Rect.fromLTWH(
        size.width / 20.0,
        size.height / 2.8,
        size.width / 24.0,
        size.height / 13.0,
      );
      final paint = Paint()..color = textColor;
      canvas.drawRect(rect, paint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

class ReadyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final textStyle = TextStyle(
      color: textColor,
      fontSize: size.width / 45.0,
      fontFamily: 'C64',
    );
    final span = TextSpan(
      text: 'READY',
      style: textStyle,
    );
    final painter = TextPainter(
      text: span,
      textDirection: TextDirection.ltr,
    );

    painter.layout(
      minWidth: 0,
      maxWidth: size.width,
    );
    final offset = Offset(
      painter.width / 2.0,
      painter.height * 8.0,
    );
    painter.paint(canvas, offset);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

class SubTitlePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final textStyle = TextStyle(
      color: textColor,
      fontSize: size.width / 45.0,
      fontFamily: 'C64',
    );
    final span = TextSpan(
      text: '64K RAM SYSTEM 38911 BASIC BYTES FREE',
      style: textStyle,
    );
    final painter = TextPainter(
      text: span,
      textDirection: TextDirection.ltr,
    );

    painter.layout(
      minWidth: 0,
      maxWidth: size.width,
    );
    final offset = Offset(
      size.width - painter.width - (size.width / 11),
      painter.height * 6.0,
    );
    painter.paint(canvas, offset);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

class TitlePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final textStyle = TextStyle(
      color: textColor,
      fontSize: size.width / 40.0,
      fontFamily: 'C64',
    );
    final span = TextSpan(
      text: '**** COMMODORE 64 BASIC V2 ****',
      style: textStyle,
    );
    final painter = TextPainter(
      text: span,
      textDirection: TextDirection.ltr,
    );

    painter.layout(
      minWidth: 0,
      maxWidth: size.width,
    );
    final offset = Offset(
      size.width - painter.width - (size.width / 9),
      size.height / 6,
    );
    painter.paint(canvas, offset);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

class BackgroundPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final border = size.width / 20.0;

    final color = Color.fromRGBO(
      58,
      57,
      213,
      1,
    );

    final paint = Paint()..color = color;

    final rect = Rect.fromLTWH(
      border,
      border,
      size.width - (border * 2.0),
      size.height - (border * 2.0),
    );

    canvas.drawRect(rect, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

class BorderPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = textColor;
    final rect = Rect.fromLTWH(
      0.0,
      0.0,
      size.width,
      size.height,
    );

    canvas.drawRect(rect, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

class Commodore64Painter extends CustomPainter {
  final bool isBlinking;

  late final List<CustomPainter> painters;

  Commodore64Painter({required this.isBlinking}) {
    painters = [
      BorderPainter(),
      BackgroundPainter(),
      TitlePainter(),
      SubTitlePainter(),
      ReadyPainter(),
      BoxPainter(isBlinking: isBlinking)
    ];
  }

  @override
  void paint(Canvas canvas, Size size) {
    painters.forEach(
      (p) => p.paint(
        canvas,
        size,
      ),
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => painters
      .map((p) => p.shouldRepaint(oldDelegate))
      .reduce((p1, p2) => p1 || p2);
}

class Commodore64 extends StatefulWidget {
  const Commodore64({Key? key}) : super(key: key);

  @override
  State<Commodore64> createState() => _Commodore64State();
}

class _Commodore64State extends State<Commodore64> {
  bool _isBlinking = false;

  final timer = Stream.periodic(Duration(seconds: 1), (value) => value);

  void _triggerRedraw() async {
    await for (final _ in timer) {
      setState(() {
        _isBlinking = !_isBlinking;
      });
    }
  }

  @override
  void initState() {
    super.initState();
    _triggerRedraw();
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      return Container(
        width: constraints.maxWidth,
        height: constraints.maxWidth / (16.0 / 9.0),
        child: CustomPaint(
          painter: Commodore64Painter(isBlinking: _isBlinking),
        ),
      );
    });
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Commodore64(),
      ),
    );
  }
}

Animated Lists in Flutter



import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class NetworkImage extends StatelessWidget {
  final String url;
  const NetworkImage({
    Key? key,
    required this.url,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Image.network(
      url,
      loadingBuilder: (context, child, loadingProgress) {
        final totalBytes = loadingProgress?.expectedTotalBytes;
        final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
        if (totalBytes != null && bytesLoaded != null) {
          return LinearProgressIndicator(
            value: bytesLoaded / totalBytes,
          );
        } else {
          return child;
        }
      },
      errorBuilder: (context, error, stackTrace) {
        return Text('Error!');
      },
    );
  }
}

class NetworkImageCard extends StatelessWidget {
  final String url;
  const NetworkImageCard({
    Key? key,
    required this.url,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        clipBehavior: Clip.antiAlias,
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(20.0),
        ),
        child: NetworkImage(
          url: url,
        ),
      ),
    );
  }
}

const imageUrls = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

Stream<String> images() => Stream.periodic(
      Duration(seconds: 3),
      (index) => index % imageUrls.length,
    ).map((index) => imageUrls[index]);

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final GlobalKey<AnimatedListState> _key = GlobalKey();

  List<String> imageUrls = [];

  void populateList() async {
    await for (final url in images()) {
      imageUrls.insert(0, url);
      _key.currentState?.insertItem(
        0,
        duration: Duration(milliseconds: 400),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    populateList();
    return Scaffold(
      appBar: AppBar(
        title: Text('Animated Lists in Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: AnimatedList(
          key: _key,
          initialItemCount: imageUrls.length,
          itemBuilder: (context, index, animation) {
            final imageUrl = imageUrls[index];
            return SizeTransition(
              sizeFactor: animation.drive(
                CurveTween(curve: Curves.elasticInOut),
              ),
              child: Column(
                children: [
                  NetworkImageCard(url: imageUrl),
                  SizedBox(height: 10.0),
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}

CheckboxListTile in Flutter

import 'package:flutter/material.dart';



enum Item { umbrella, coat, boots }

extension Info on Item {
  String get title {
    switch (this) {
      case Item.umbrella:
        return 'Umbrella';
      case Item.boots:
        return 'Boots';
      case Item.coat:
        return 'Jacket';
    }
  }

  String get icon {
    switch (this) {
      case Item.umbrella:
        return '☂️';
      case Item.boots:
        return '🥾';
      case Item.coat:
        return '🧥';
    }
  }
}

typedef OnChecked = void Function(bool);

class Tile extends StatelessWidget {
  final Item item;
  final bool isChecked;
  final OnChecked onChecked;
  const Tile({
    Key? key,
    required this.item,
    required this.isChecked,
    required this.onChecked,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints(minHeight: 100.0),
      child: Center(
        child: CheckboxListTile(
          shape: ContinuousRectangleBorder(),
          value: isChecked,
          secondary: Text(
            item.icon,
            style: TextStyle(fontSize: 30.0),
          ),
          title: Text(
            item.title,
            style: TextStyle(fontSize: 40.0),
          ),
          onChanged: (value) {
            onChecked(value ?? false);
          },
        ),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final Set<Item> _enabledItems = {};

  Widget get listView {
    return ListView.builder(
      shrinkWrap: true,
      itemBuilder: (context, index) {
        final item = Item.values[index];
        final isChecked = _enabledItems.contains(item);
        return Tile(
          isChecked: isChecked,
          item: item,
          onChecked: (isChecked) {
            setState(() {
              if (isChecked) {
                _enabledItems.add(item);
              } else {
                _enabledItems.remove(item);
              }
            });
          },
        );
      },
      itemCount: Item.values.length,
    );
  }

  Widget get title {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Text(
        "Remember to pick all items! It's going to be rainy today!",
        textAlign: TextAlign.center,
        style: TextStyle(fontSize: 40),
      ),
    );
  }

  Widget get readyButton {
    final onPressed = () async {
      // program this
      await showDialog(
        context: context,
        builder: (context) {
          return AlertDialog(
            content: Text('You seem to be ready for a rainy day! ✅'),
            actions: [
              TextButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: Text('OK'),
              )
            ],
          );
        },
      );
    };

    final isEnabled = _enabledItems.containsAll(Item.values);

    return FractionallySizedBox(
      widthFactor: 0.8,
      child: ElevatedButton(
        onPressed: isEnabled ? onPressed : null,
        child: Text('Ready!'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Checkbox List Tile in Flutter'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            title,
            listView,
            SizedBox(height: 50.0),
            readyButton,
          ],
        ),
      ),
    );
  }
}

- Operator on String in Dart



extension Minus on String {
  String operator -(String rhs) => replaceAll(rhs, '');
}

void testIt() {
  assert('foo bar' - 'foo ' == 'bar');
  assert('foo bar foo' - 'foo' == ' bar ');
  assert('bar' - 'foo' == 'bar');
  assert('BAR' - 'bar' == 'BAR');
  assert('foo' - 'FOO' == 'foo');
  assert('foobarbaz' - 'bar' == 'foobaz');
}

Dart Progress for Future<T>



import 'dart:io' show stdout;
import 'dart:async' show Future, Stream;

const loadingSequence = ['⢿', '⣻', '⣽', '⣾', '⣷', '⣯', '⣟', '⡿'];
const escape = '\x1B[38;5;';
const color = '${escape}1m';
const textColor = '${escape}6m';

String progress({required int value, required String text}) {
  final progress = '$color${loadingSequence[value % loadingSequence.length]}';
  final coloredText = '$textColor$text';
  return '$progress\t$coloredText';
}

Future<T> performWithProgress<T>({
  required Future<T> task,
  required String progressText,
}) {
  final stream = Stream<String>.periodic(
    Duration(milliseconds: 100),
    (value) => progress(
      value: value,
      text: progressText,
    ),
  );

  final subscription = stream.listen(
    (event) {
      stdout.write('\r $event');
    },
  );

  task.whenComplete(() {
    stdout.write('\r ✅\t$progressText');
    stdout.write('\n');
    subscription.cancel();
  });

  return task;
}

final task1 = Future.delayed(Duration(seconds: 1), () => 'Result 1');
final task2 = Future.delayed(Duration(seconds: 2), () => 'Result 2');
final task3 = Future.delayed(Duration(seconds: 3), () => 'Result 3');

void main(List<String> args) async {
  var result = await performWithProgress(
    task: task1,
    progressText: 'Loading task 1',
  );
  print('\tTask 1 result: $result');
  result = await performWithProgress(
    task: task2,
    progressText: 'Loading task 2',
  );
  print('\tTask 2 result: $result');
  result = await performWithProgress(
    task: task3,
    progressText: 'Loading task 3',
  );
  print('\tTask 3 result: $result');
}

Move Widget Shadows with Animation



import 'package:flutter/material.dart';

class ImageTransition extends AnimatedWidget {
  final String imageUrl;

  Animation<double> get shadowXOffset => listenable as Animation<double>;

  const ImageTransition(this.imageUrl, {shadowXOffset})
      : super(listenable: shadowXOffset);

  @override
  Widget build(BuildContext context) {
    return Container(
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20.0),
        boxShadow: [
          BoxShadow(
            blurRadius: 10,
            offset: Offset(shadowXOffset.value, 20.0),
            color: Colors.black.withAlpha(100),
            spreadRadius: -10,
          )
        ],
      ),
      child: Image.network(imageUrl),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class CustomCurve extends CurveTween {
  CustomCurve() : super(curve: Curves.easeInOutSine);
  @override
  double transform(double t) {
    return (super.transform(t) - 0.5) * 25.0;
  }
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _animation;

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );
    _animation = CustomCurve().animate(_controller);
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _controller.repeat(reverse: true);
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Center(
          child: ImageTransition(
            'https://bit.ly/3x7J5Qt',
            shadowXOffset: _animation,
          ),
        ),
      ),
    );
  }
}

Gallery with Blurred Backgrounds in Flutter


import 'package:flutter/material.dart';

const images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: images.length,
        itemBuilder: (context, index) {
          return WithBlurredBackground(imageUrl: images[index]);
        },
      ),
    );
  }
}

class WithBlurredBackground extends StatelessWidget {
  final String imageUrl;

  const WithBlurredBackground({Key? key, required this.imageUrl})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox.expand(
      child: Stack(
        alignment: Alignment.center,
        fit: StackFit.passthrough,
        children: [
          SizedBox.expand(
            child: ClipRect(
              child: ImageFiltered(
                imageFilter: ImageFilter.blur(
                  sigmaX: 10.0,
                  sigmaY: 10.0,
                ),
                child: Image.network(
                  imageUrl,
                  fit: BoxFit.fitHeight,
                ),
              ),
            ),
          ),
          Image.network(imageUrl),
        ],
      ),
    );
  }
}

Custom Path Clippers in Flutter



import 'package:flutter/material.dart';

const images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.only(
            top: 40.0,
            left: 10.0,
            right: 10.0,
          ),
          child: Column(
            children: images
                .map((url) => ElevatedNetworkImage(url: url))
                .expand(
                  (img) => [
                    img,
                    SizedBox(height: 30.0),
                  ],
                )
                .toList(),
          ),
        ),
      ),
    );
  }
}

class ElevatedNetworkImage extends StatelessWidget {
  final String url;
  const ElevatedNetworkImage({Key? key, required this.url}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return PhysicalShape(
      color: Colors.white,
      clipper: Clipper(),
      elevation: 20.0,
      clipBehavior: Clip.none,
      shadowColor: Colors.white.withAlpha(200),
      child: CutEdges(
        child: Image.network(url),
      ),
    );
  }
}

class Clipper extends CustomClipper<Path> {
  static const variance = 0.2;
  static const reverse = 1.0 - variance;

  @override
  Path getClip(Size size) {
    final path = Path();

    path.moveTo(0.0, size.height * Clipper.variance);
    path.lineTo(size.width * Clipper.variance, 0.0);
    path.lineTo(size.width, 0.0);
    path.lineTo(size.width, size.height * Clipper.reverse);
    path.lineTo(size.width * Clipper.reverse, size.height);
    path.lineTo(0.0, size.height);
    path.lineTo(0.0, size.height * Clipper.variance);
    path.close;
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

class CutEdges extends StatelessWidget {
  final Widget child;

  const CutEdges({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: Clipper(),
      child: child,
    );
  }
}

Frost Effect on Images in Flutter



import 'package:flutter/material.dart';

const images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

final loremIpsum =
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.fromLTRB(
            8.0,
            0.0,
            8.0,
            0.0,
          ),
          child: SingleChildScrollView(
            child: Column(
              children: images
                  .map(
                    (url) => GlossyNetworkImageWithProgress(
                      url: url,
                      title: 'Image title',
                      description: loremIpsum,
                    ),
                  )
                  .expand(
                    (image) => [
                      image,
                      SizedBox(height: 16.0),
                    ],
                  )
                  .toList(),
            ),
          ),
        ),
      ),
    );
  }
}

class GlossyNetworkImageWithProgress extends StatefulWidget {
  final String url;
  final String title;
  final String description;

  const GlossyNetworkImageWithProgress(
      {Key? key,
      required this.url,
      required this.title,
      required this.description})
      : super(key: key);

  @override
  _GlossyNetworkImageWithProgressState createState() =>
      _GlossyNetworkImageWithProgressState();
}

class _GlossyNetworkImageWithProgressState
    extends State<GlossyNetworkImageWithProgress>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );

    _animation = Tween(
      begin: 0.0,
      end: 1.0,
    ).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final networkImage = Image.network(
      widget.url,
      fit: BoxFit.fitHeight,
      frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
        _controller.reset();
        _controller.forward();
        return FadeTransition(
          opacity: _animation,
          child: CustomBox(
            child: child,
          ),
        );
      },
      loadingBuilder: (context, child, loadingProgress) {
        final totalBytes = loadingProgress?.expectedTotalBytes;
        final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
        if (totalBytes != null && bytesLoaded != null) {
          return CustomBox(
            child: CircularProgressIndicator(
              backgroundColor: Colors.white70,
              value: bytesLoaded / totalBytes,
              color: Colors.blue[900],
              strokeWidth: 5.0,
            ),
          );
        } else {
          return child;
        }
      },
      errorBuilder: (context, error, stackTrace) {
        return Text('Error!');
      },
    );

    return BottomGloss(
      networkImage: networkImage,
      title: widget.title,
      description: widget.description,
    );
  }
}

class BottomGloss extends StatelessWidget {
  final String title;
  final String description;

  const BottomGloss(
      {Key? key,
      required this.networkImage,
      required this.title,
      required this.description})
      : super(key: key);

  final Image networkImage;

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.all(Radius.circular(8.0)),
      child: Stack(
        fit: StackFit.passthrough,
        children: [
          networkImage,
          Container(
            height: 300.0,
            alignment: Alignment.bottomCenter,
            child: ClipRect(
              child: FractionallySizedBox(
                heightFactor: 0.5,
                child: BackdropFilter(
                  filter: ImageFilter.blur(
                    sigmaX: 10.0,
                    sigmaY: 10.0,
                  ),
                  child: BottomContents(
                    title: title,
                    description: description,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class BottomContents extends StatelessWidget {
  final String title;
  final String description;

  const BottomContents({
    Key? key,
    required this.title,
    required this.description,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white.withOpacity(0.4),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              TitleText(text: title),
              SizedBox(height: 8.0),
              SubTitleText(text: description),
            ],
          ),
        ),
      ),
    );
  }
}

class SubTitleText extends StatelessWidget {
  final String text;
  const SubTitleText({Key? key, required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: TextStyle(
        color: Colors.black,
        fontSize: 20.0,
      ),
    );
  }
}

class TitleText extends StatelessWidget {
  final String text;
  const TitleText({Key? key, required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: TextStyle(
        color: Colors.white,
        fontSize: 30.0,
      ),
    );
  }
}

class CustomBox extends StatelessWidget {
  final Widget child;

  const CustomBox({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 300.0,
      width: MediaQuery.of(context).size.width,
      child: child is ProgressIndicator ? Center(child: child) : child,
    );
  }
}

Custom Clippers in Flutter

import 'package:flutter/material.dart';
import 'dart:math' show min;

const gridImages = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/3jRSRCu',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GridView.count(
        padding: const EdgeInsets.fromLTRB(8.0, 48.0, 8.0, 48.0),
        crossAxisCount: 2,
        mainAxisSpacing: 8.0,
        crossAxisSpacing: 8.0,
        children: gridImages
            .map((url) => NetworkImageWithProgress(url: url))
            .toList(),
      ),
    );
  }
}

class CircularClipper extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) {
    final center = Offset(
      size.width / 2.0,
      size.height / 2.0,
    );
    final minWidthorHeight = min(size.width, size.height);
    return Rect.fromCenter(
      center: center,
      width: minWidthorHeight,
      height: minWidthorHeight,
    );
  }

  @override
  bool shouldReclip(covariant CustomClipper<Rect> oldClipper) => false;
}

class Circular extends StatelessWidget {
  final Widget child;
  const Circular({Key? key, required this.child}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ClipOval(
      clipper: CircularClipper(),
      child: child,
    );
  }
}

class CustomBox extends StatelessWidget {
  final Widget child;

  const CustomBox({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 220.0,
      width: MediaQuery.of(context).size.width,
      child: child is ProgressIndicator
          ? Center(child: child)
          : Circular(child: child),
    );
  }
}

class NetworkImageWithProgress extends StatefulWidget {
  final String url;

  const NetworkImageWithProgress({Key? key, required this.url})
      : super(key: key);

  @override
  _NetworkImageWithProgressState createState() =>
      _NetworkImageWithProgressState();
}

class _NetworkImageWithProgressState extends State<NetworkImageWithProgress>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );

    _animation = Tween(
      begin: 0.0,
      end: 1.0,
    ).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Image.network(
      widget.url,
      fit: BoxFit.fitHeight,
      frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
        _controller.reset();
        _controller.forward();
        return FadeTransition(
          opacity: _animation,
          child: CustomBox(
            child: child,
          ),
        );
      },
      loadingBuilder: (context, child, loadingProgress) {
        final totalBytes = loadingProgress?.expectedTotalBytes;
        final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
        if (totalBytes != null && bytesLoaded != null) {
          return CustomBox(
            child: CircularProgressIndicator(
              backgroundColor: Colors.white70,
              value: bytesLoaded / totalBytes,
              color: Colors.blue[900],
              strokeWidth: 5.0,
            ),
          );
        } else {
          return child;
        }
      },
      errorBuilder: (context, error, stackTrace) {
        return Text('Error!');
      },
    );
  }
}

Check if Website is Up or Down in Dart



class UpStatus {
  final bool isUp;
  final DateTime timestamp;
  const UpStatus(this.isUp, this.timestamp);
}

class Pling {
  final String url;
  final Duration interval;
  const Pling({
    required this.url,
    required this.interval,
  });
  Stream<UpStatus> checkIfUp() =>
      Stream.periodic(interval, (_) => DateTime.now()).asyncExpand(
        (now) => HttpClient()
            .headUrl(Uri.parse(url))
            .then((req) => req..followRedirects = false)
            .then((req) => req.close())
            .then((resp) => resp.statusCode)
            .then((statusCode) => statusCode == 200)
            .onError((error, stackTrace) => false)
            .then((isUp) => UpStatus(isUp, now))
            .asStream(),
      );
}

const oneSecond = Duration(seconds: 1);
const url = 'https://dart.dev';

extension IsOrIsNot on bool {
  String get isOrIsNot => this ? 'is' : 'is not';
}

void testIt() async {
  final pling = Pling(
    url: url,
    interval: oneSecond,
  );
  await for (final upStatus in pling.checkIfUp()) {
    final timestamp = upStatus.timestamp;
    final isUpStr = upStatus.isUp.isOrIsNot;
    print('$url $isUpStr up at $timestamp');
  }
}

Section Titles on ListView in Flutter

import 'package:flutter/material.dart';

final List<Section> allSections = [
  Section(
    'Spring',
    [
      'https://cnn.it/3xu58Ap',
      'https://bit.ly/3ueqqC1',
    ],
  ),
  Section(
    'Summer',
    [
      'https://bit.ly/3ojNhLc',
      'https://bit.ly/2VcCSow',
    ],
  ),
  Section(
    'Autumn',
    [
      'https://bit.ly/3ib1TJk',
      'https://bit.ly/2XSpjvq',
    ],
  ),
  Section(
    'Winter',
    [
      'https://bit.ly/3iaQNE7',
      'https://bit.ly/3AY8YE4',
    ],
  ),
];

class Section {
  final String title;
  final List<String> urls;
  const Section(this.title, this.urls);
}

extension ToWidgets on Section {
  Iterable<Widget> toNetworkImageCards() {
    return [
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: Text(
          title,
          style: TextStyle(
            fontSize: 40,
          ),
        ),
      ),
      ...urls.expand(
        (url) => [
          NetworkImageCard(url: url),
          SizedBox(height: 10),
        ],
      ),
    ];
  }
}

class NetworkImageCard extends StatelessWidget {
  final String url;
  const NetworkImageCard({
    Key? key,
    required this.url,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        child: NetworkImageWithProgress(
          url: url,
        ),
      ),
    );
  }
}

class NetworkImageWithProgress extends StatefulWidget {
  final String url;

  const NetworkImageWithProgress({Key? key, required this.url})
      : super(key: key);

  @override
  _NetworkImageWithProgressState createState() =>
      _NetworkImageWithProgressState();
}

class _NetworkImageWithProgressState extends State<NetworkImageWithProgress>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );

    _animation = Tween(
      begin: 0.0,
      end: 1.0,
    ).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Image.network(
      widget.url,
      fit: BoxFit.fitWidth,
      frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
        _controller.reset();
        _controller.forward();
        return FadeTransition(
          opacity: _animation,
          child: CustomBox(
            child: child,
          ),
        );
      },
      loadingBuilder: (context, child, loadingProgress) {
        final totalBytes = loadingProgress?.expectedTotalBytes;
        final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
        if (totalBytes != null && bytesLoaded != null) {
          return CustomBox(
            child: CircularProgressIndicator(
              backgroundColor: Colors.white70,
              value: bytesLoaded / totalBytes,
              color: Colors.blue[900],
              strokeWidth: 5.0,
            ),
          );
        } else {
          return child;
        }
      },
      errorBuilder: (context, error, stackTrace) {
        return Text('Error!');
      },
    );
  }
}

class CustomBox extends StatelessWidget {
  final Widget child;

  const CustomBox({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 220.0,
      width: MediaQuery.of(context).size.width,
      child: child is ProgressIndicator ? Center(child: child) : child,
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemBuilder: (context, index) {
          final section = allSections[index];
          return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: section.toNetworkImageCards().toList(),
          );
        },
        itemCount: allSections.length,
      ),
    );
  }
}

Circular Progress in Flutter



import 'package:flutter/material.dart';

class CustomBox extends StatelessWidget {
  final Widget child;

  const CustomBox({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 220.0,
      width: MediaQuery.of(context).size.width,
      child: Center(child: child),
    );
  }
}

class NetworkImageWithProgress extends StatefulWidget {
  final String url;

  const NetworkImageWithProgress({Key? key, required this.url})
      : super(key: key);

  @override
  _NetworkImageWithProgressState createState() =>
      _NetworkImageWithProgressState();
}

class _NetworkImageWithProgressState extends State<NetworkImageWithProgress>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );

    _animation = Tween(
      begin: 0.0,
      end: 1.0,
    ).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Image.network(
      widget.url,
      fit: BoxFit.fitWidth,
      frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
        _controller.reset();
        _controller.forward();
        return FadeTransition(
          opacity: _animation,
          child: CustomBox(
            child: child,
          ),
        );
      },
      loadingBuilder: (context, child, loadingProgress) {
        final totalBytes = loadingProgress?.expectedTotalBytes;
        final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
        if (totalBytes != null && bytesLoaded != null) {
          return CustomBox(
            child: CircularProgressIndicator(
              backgroundColor: Colors.white70,
              value: bytesLoaded / totalBytes,
              color: Colors.blue[900],
              strokeWidth: 5.0,
            ),
          );
        } else {
          return child;
        }
      },
      errorBuilder: (context, error, stackTrace) {
        return Text('Error!');
      },
    );
  }
}

final images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3ywzOla',
].map((url) => NetworkImageWithProgress(url: url)).expand(
      (element) => [
        element,
        SizedBox(
          height: 10.0,
        )
      ],
    );

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Column(
          children: images.toList(),
        ),
      ),
    );
  }
}

Displaying Scroll Wheels in Flutter



import 'package:flutter/material.dart';

class FadingNetworkImage extends StatefulWidget {
  final String url;

  const FadingNetworkImage({Key? key, required this.url}) : super(key: key);

  @override
  _FadingNetworkImageState createState() => _FadingNetworkImageState();
}

class _FadingNetworkImageState extends State<FadingNetworkImage>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 1));

    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Image.network(
      widget.url,
      frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
        _controller.reset();
        _controller.forward();
        return FadeTransition(
          opacity: _animation,
          child: child,
        );
      },
      loadingBuilder: (context, child, loadingProgress) {
        final totalBytes = loadingProgress?.expectedTotalBytes;
        final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
        if (totalBytes != null && bytesLoaded != null) {
          return LinearProgressIndicator(
            value: bytesLoaded / totalBytes,
          );
        } else {
          return child;
        }
      },
      errorBuilder: (context, error, stackTrace) {
        return Text('Error!');
      },
    );
  }
}

final images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3ywzOla',
].map((i) => NetworkImageCard(url: i));

class NetworkImageCard extends StatelessWidget {
  final String url;
  const NetworkImageCard({
    Key? key,
    required this.url,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        clipBehavior: Clip.antiAlias,
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(20.0),
          boxShadow: [
            BoxShadow(
              blurRadius: 5,
              offset: Offset(0, 0),
              color: Colors.black.withAlpha(40),
              spreadRadius: 5,
            )
          ],
        ),
        child: FadingNetworkImage(
          url: url,
        ),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListWheelScrollView(
        itemExtent: 164.0,
        squeeze: 0.9,
        perspective: 0.003,
        children: images.toList(),
      ),
    );
  }
}

Post Messages to Slack with Dart



import 'dart:convert' show utf8;
import 'dart:convert' show json;

class SlackMessage {
  final String? inChannel;
  final String? userName;
  final String message;
  final String? iconEmoji;

  const SlackMessage({
    required this.inChannel,
    required this.userName,
    required this.message,
    required this.iconEmoji,
  });

  Future<bool> send(String webhookUrl) async {
    final payload = {
      'text': message,
      if (inChannel != null) 'channel': inChannel!,
      if (userName != null) 'username': userName!,
      if (iconEmoji != null) 'icon_emoji': iconEmoji!
    };

    final request = await HttpClient().postUrl(Uri.parse(webhookUrl));
    final payloadData = utf8.encode(json.encode(payload));
    request.add(payloadData);
    final response = await request.close();
    return response.statusCode == 200;
  }
}

const webhookUrl = 'put your webhook url here';

void testIt() async {
  final message = SlackMessage(
    inChannel: 'dart',
    userName: 'Flutter',
    message: 'Hello from Dart in Terminal',
    iconEmoji: 'blue_heart:',
  );
  if (await message.send(webhookUrl)) {
    print('Successfully sent the message');
  } else {
    print('Could not send the message');
  }
}

Unwrap List<T?>? in Dart



extension Unwrap<T> on List<T?>? {
  List<T> unwrap() => (this ?? []).whereType<T>().toList();
}

void testOptionalListOfOptionals() {
  final List<int?>? optionalListOfOptionals = [1, 2, null, 3, null];
  final unwrapped = optionalListOfOptionals.unwrap(); // List<int>
  print(unwrapped); // prints [1, 2, 3]
}

void testListOfOptionals() {
  final listOfOptionals = [20, 30, null, 40]; // List<int?>
  final unwrapped = listOfOptionals.unwrap(); // List<int>
  print(unwrapped); // prints [20, 30, 40]
}

void testNormalList() {
  final list = [50, 60, 70]; // List<int>
  final unwrapped = list.unwrap(); // List<int>
  print(unwrapped); // prints [50, 60, 70]
}

Avoiding UI Jitters When Switching Widgets in Flutter



const imageUrls = [
  'https://cnn.it/3xu58Ap', // spring
  'https://bit.ly/2VcCSow', // summer
  'https://bit.ly/3A3zStC', // autumn
  'https://bit.ly/2TNY7wi' // winter
];

extension ToNetworkImage<T extends String> on List<T> {
  List<Widget> toNetworkImages() => map((s) => Image.network(s)).toList();
}

class HomePage extends StatefulWidget {
  @override
  State createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  var _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Indexed Stack')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IndexedStack(
              index: _currentIndex,
              children: imageUrls.toNetworkImages(),
            ),
            TextButton(
              onPressed: () {
                setState(
                  () {
                    _currentIndex++;
                    if (_currentIndex >= imageUrls.length) {
                      _currentIndex = 0;
                    }
                  },
                );
              },
              child: Text('Go to next season'),
            )
          ],
        ),
      ),
    );
  }
}

Detect Redirects in Dart



Future<bool> doesRedirect(String url) => HttpClient()
    .headUrl(Uri.parse(url))
    .then((req) => req..followRedirects = false)
    .then((req) => req.close())
    .then((resp) => resp.statusCode)
    .then((statusCode) => [301, 302, 303, 307, 308].contains(statusCode));

void testIt() async {
  final test1 = await doesRedirect('https://cnn.it/3xu58Ap');
  assert(test1 == true);

  final test2 = await doesRedirect('https://dart.dev');
  assert(test2 == false);

  final test3 = await doesRedirect('https://bit.ly/2VcCSow');
  assert(test3 == true);
}

Proportional Constraints in Flutter



class ProportionalWidthNetworkImage extends StatelessWidget {
  final String url;
  final double widthProportion;

  const ProportionalWidthNetworkImage(
      {Key? key, required this.url, required this.widthProportion})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Image.network(
          url,
          loadingBuilder: (context, child, loadingProgress) {
            final widget =
                loadingProgress == null ? child : LinearProgressIndicator();
            return Container(
              width: constraints.maxWidth * widthProportion,
              child: widget,
            );
          },
        );
      },
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ProportionalWidthNetworkImage(
          url: 'https://cnn.it/3xu58Ap',
          widthProportion: 0.8,
        ),
      ),
    );
  }
}

Displaying Cupertino Action Sheets in Flutter



import 'package:flutter/cupertino.dart';

enum Season { spring, summer, autumn, winter }

extension Title on Season {
  String get title => describeEnum(this).capitalized;
}

extension Caps on String {
  String get capitalized => this[0].toUpperCase() + substring(1);
}

extension ToWidget on Season {
  Widget toWidget() {
    switch (this) {
      case Season.spring:
        return Image.network('https://cnn.it/3xu58Ap');
      case Season.summer:
        return Image.network('https://bit.ly/2VcCSow');
      case Season.autumn:
        return Image.network('https://bit.ly/3A3zStC');
      case Season.winter:
        return Image.network('https://bit.ly/2TNY7wi');
    }
  }
}

class HomePage extends StatefulWidget {
  HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

Future<Season> _chooseSeason(
  BuildContext context,
  Season currentSeason,
) async {
  CupertinoActionSheet actionSheet(BuildContext context) {
    return CupertinoActionSheet(
      title: Text('Choose your favorite season:'),
      actions: Season.values
          .map(
            (season) => CupertinoActionSheetAction(
              onPressed: () {
                Navigator.of(context).pop(season);
              },
              child: Text(season.title),
            ),
          )
          .toList(),
      cancelButton: CupertinoActionSheetAction(
        onPressed: () {
          Navigator.of(context).pop(currentSeason);
        },
        child: Text('Cancel'),
      ),
    );
  }

  return await showCupertinoModalPopup(
    context: context,
    builder: (context) => actionSheet(context),
  );
}

class _HomePageState extends State<HomePage> {
  var _season = Season.spring;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_season.title),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          _season.toWidget(),
          TextButton(
            onPressed: () async {
              _season = await _chooseSeason(
                context,
                _season,
              );
              setState(() {});
            },
            child: Text('Choose a season'),
          ),
        ],
      ),
    );
  }
}

Rotating List<T> in Dart



extension Rotate<T> on List<T> {
  int _rotationTimes(int places) {
    if (isEmpty) {
      return 0;
    }
    if (places == 0) {
      throw ArgumentError('places should be more than 0');
    }
    return places % length;
  }

  List<T> rotatedRight(int places) {
    final times = _rotationTimes(places);
    if (times == 0) {
      return this;
    } else {
      final cutOff = length - times;
      return sublist(cutOff)..addAll(sublist(0, cutOff));
    }
  }

  List<T> rotatedLeft(int places) {
    final times = _rotationTimes(places);
    if (times == 0) {
      return this;
    } else {
      return sublist(times)..addAll(sublist(0, times));
    }
  }
}

extension Equality<T extends Comparable> on List<T> {
  bool isEqualTo(List<T> other) {
    if (other.length != length) {
      return false;
    }
    for (var i = 0; i < length; i++) {
      if (other[i] != this[i]) {
        return false;
      }
    }
    return true;
  }
}

const arr = [1, 2, 3];

void testIt() {
  assert(arr.rotatedRight(1).isEqualTo([3, 1, 2]));
  assert(arr.rotatedRight(2).isEqualTo([2, 3, 1]));
  assert(arr.rotatedRight(3).isEqualTo([1, 2, 3]));
  assert(arr.rotatedRight(4).isEqualTo([3, 1, 2]));
  assert(arr.rotatedLeft(1).isEqualTo([2, 3, 1]));
  assert(arr.rotatedLeft(2).isEqualTo([3, 1, 2]));
  assert(arr.rotatedLeft(3).isEqualTo([1, 2, 3]));
  assert(arr.rotatedLeft(4).isEqualTo([2, 3, 1]));
}

Displaying SnackBars in Flutter



class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hello world'),
      ),
      body: Center(
        child: TextButton(
          onPressed: () {
            final now = DateFormat('kk:mm:ss').format(DateTime.now());
            ScaffoldMessenger.of(context).removeCurrentSnackBar();
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                behavior: SnackBarBehavior.floating,
                elevation: 5.0,
                backgroundColor:
                    Colors.blue[600]!.withOpacity(0.8).withAlpha(200),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(10),
                  side: BorderSide(
                    color: Colors.black.withOpacity(0.4),
                    width: 3.0,
                  ),
                ),
                content: Text('Some text $now'),
              ),
            );
          },
          child: Text('Show toast'),
        ),
      ),
    );
  }
}

Custom Tab Bar Using ToggleButtons in Flutter



class TabBarButton extends StatelessWidget {
  final IconData icon;
  final double size;

  const TabBarButton({Key? key, required this.icon, this.size = 60.0})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Icon(
        icon,
        size: size,
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Toggle Buttons'),
      ),
      body: Column(
        children: [
          CustomTabBar(),
        ],
      ),
    );
  }
}

class CustomTabBar extends StatefulWidget {
  const CustomTabBar({Key? key}) : super(key: key);

  @override
  _CustomTabBarState createState() => _CustomTabBarState();
}

class _CustomTabBarState extends State<CustomTabBar> {
  var _selection = [false, false, false];

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Align(
        alignment: FractionalOffset.bottomCenter,
        child: SafeArea(
          child: ToggleButtons(
            isSelected: _selection,
            onPressed: (index) {
              setState(() {
                _selection = List.generate(
                  _selection.length,
                  (i) => index == i ? true : false,
                );
              });
            },
            selectedColor: Colors.white,
            fillColor: Colors.blue,
            borderRadius: BorderRadius.circular(10.0),
            borderWidth: 4.0,
            borderColor: Colors.blue[400],
            selectedBorderColor: Colors.blue,
            highlightColor: Colors.blue.withOpacity(0.2),
            children: [
              TabBarButton(icon: Icons.settings),
              TabBarButton(icon: Icons.add),
              TabBarButton(icon: Icons.settings_remote)
            ],
          ),
        ),
      ),
    );
  }
}

Hashable Mixins in Dart



enum PetType { cat, dog }

mixin Pet {
  String get name;
  int get age;
  PetType get type;
  @override
  String toString() => 'Pet ($type), name = $name, age = $age';
  @override
  int get hashCode => hashValues(name, age, type);
  @override
  bool operator ==(covariant Pet o) => o.hashCode == hashCode;
}

class Cat with Pet {
  @override
  final String name;
  @override
  final int age;
  @override
  final PetType type;
  const Cat({required this.name, required this.age}) : type = PetType.cat;
}

void testIt() {
  final cats = <Cat>{
    Cat(name: 'Kitty 1', age: 2),
    Cat(name: 'Kitty 2', age: 3),
    Cat(name: 'Kitty 1', age: 2),
  };
  cats.forEach(print);
  /* 👆🏻 prints the following:
  Pet (PetType.cat), name = Kitty 1, age = 2
  Pet (PetType.cat), name = Kitty 2, age = 3
  */
}

Flutter Tips and Tricks in Terminal



import 'dart:convert' show utf8;
import 'dart:io' show HttpClient, exit, Process, stderr;
import 'dart:math' show Random;

const rawBlobRoot =
    'https://raw.githubusercontent.com/vandadnp/flutter-tips-and-tricks/main/';

void main(List<String> args) async {
  final url = Uri.https('bit.ly', '/2V1GKsC');
  try {
    final client = HttpClient();
    final images = await client
        .getUrl(url)
        .then((req) => req.close())
        .then((resp) => resp.transform(utf8.decoder).join())
        .then((body) => body.split('\n').map((e) => e.trim()))
        .then((iter) => iter.toList())
        .then((list) => list..retainWhere((s) => s.endsWith('.jpg)')))
        .then((imageList) => imageList.map((e) => e.replaceAll('![](', '')))
        .then((imageList) => imageList.map((e) => e.replaceAll(')', '')))
        .then((iter) => iter.toList());

    final found = images[Random().nextInt(images.length)];
    final result = '$rawBlobRoot$found';
    await Process.run('open', [result]);
    exit(0);
  } catch (e) {
    stderr.writeln('Could not proceed due to $e');
    exit(1);
  }
}

Searching List<List<T>> in Dart



const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];
const arr = [arr1, arr2, arr3];

extension FlattenFind<T extends Comparable> on Iterable<Iterable<T>> {
  bool containsElement(T value) {
    for (final arr in this) {
      if (arr.contains(value)) {
        return true;
      }
    }
    return false;
  }
}

void testIt() {
  assert(arr.containsElement(2));
  assert(arr.containsElement(8));
  assert(!arr.containsElement(10));
  assert(!arr.containsElement(10));
}

Cloning Objects in Dart



class Person {
  final Map<String, Object> _values;
  static const FIRST_NAME_KEY = 'FIRST_NAME';
  static const LAST_NAME_KEY = 'LAST_NAME';

  Person.from(Map<String, Object> props) : _values = props;

  Person({
    required String firstName,
    required String lastName,
    Map<String, Object>? props,
  }) : _values = {
          FIRST_NAME_KEY: firstName,
          LAST_NAME_KEY: lastName,
        };

  @override
  bool operator ==(covariant Person other) =>
      other.firstName == firstName && other.lastName == lastName;

  @override
  String toString() => _values.toString();
}

extension Properties on Person {
  String get firstName => _values[Person.FIRST_NAME_KEY].toString();
  set firstName(String newValue) => _values[Person.FIRST_NAME_KEY] = newValue;

  String get lastName => _values[Person.LAST_NAME_KEY].toString();
  set lastName(String newValue) => _values[Person.LAST_NAME_KEY] = newValue;
}

extension Clone on Person {
  Person clone([Map<String, Object>? additionalProps]) =>
      Person.from(Map.from(_values)..addAll(additionalProps ?? {}));
}

extension Subscripts on Person {
  Object? operator [](String key) => _values[key];
  operator []=(String key, Object value) => _values[key] = value;
}

void testIt() {
  final foo = Person(
    firstName: 'Foo Firstname',
    lastName: 'Foo Lastname',
  );
  print(foo); // {FIRST_NAME: Foo Firstname, LAST_NAME: Foo Lastname}
  final copyOfFoo = foo.clone();
  print(copyOfFoo); // {FIRST_NAME: Foo Firstname, LAST_NAME: Foo Lastname}
  final bar = foo.clone({'age': 30});
  print(bar); // {FIRST_NAME: Foo Firstname, LAST_NAME: Foo Lastname, age: 30}
  assert(foo == copyOfFoo);
  assert(foo == bar);
  assert(foo['age'] == null);
  assert(copyOfFoo['age'] == null);
  assert(bar['age'] == 30);
}

Color Filters in Flutter



class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  var sliderValue = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Color Filters in Flutter!'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ColorFiltered(
            colorFilter: ColorFilter.mode(
              Colors.orange.withOpacity(sliderValue),
              BlendMode.colorBurn,
            ),
            child: Image.network('https://tinyurl.com/4vtvh35h'),
          ),
          Slider(
            value: sliderValue,
            onChanged: (value) {
              setState(() {
                sliderValue = value;
              });
            },
          )
        ],
      ),
    );
  }
}

Flattening Lists in Dart



class Person {
  final String name;
  const Person(this.name);
  @override
  String toString() => 'Person: $name';
}

class House {
  final List<Person>? tennants;
  final List<Person> builders;
  const House({
    required this.tennants,
    required this.builders,
  });
}

const houses = [
  House(tennants: null, builders: [
    Person('Builder 1'),
  ]),
  House(tennants: [
    Person('Tennant 1'),
    Person('Tennant 2'),
  ], builders: [
    Person('Builder 3')
  ]),
];

extension OptionalFlattend<T> on Iterable<List<T>?> {
  Iterable<T> flattened() => expand((e) => e ?? []);
}

void testOptionalFlatten() {
  final allTennants = houses.map((h) => h.tennants).flattened();
  print(allTennants); // Person: Tennant 1, Person: Tennant 2
}

extension Flattend<T> on Iterable<List<T>> {
  Iterable<T> flattened() => expand((e) => e);
}

void testNonOptionalFlatten() {
  final allBuilders = houses.map((h) => h.builders).flattened();
  print(allBuilders); // Person: Builder 1, Person: Builder 2
}

void testIt() {
  testOptionalFlatten();
  testNonOptionalFlatten();
}

Managing Duplicates in List<T> in Dart



extension Duplicates<T> on List<T> {
  void addAllByAvoidingDuplicates(Iterable<T> values) =>
      replaceRange(0, length, {
        ...([...this] + [...values])
      });

  int get numberOfDuplicates => length - {...this}.length;

  bool get containsDuplicates => numberOfDuplicates > 0;

  List<T> get uniques => [
        ...{...this}
      ];

  void removeDuplicates() => replaceRange(
        0,
        length,
        uniques,
      );

  List<T> get duplicates => [
        for (var i = 0; i < length; i++)
          [...this].skip(i + 1).contains(this[i]) ? this[i] : null
      ].whereType<T>().toList();
}

void testIt() {
  final values = [3, 2, 10, 30, 40, 30, 100, 10];

  assert(values.numberOfDuplicates == 2);
  assert(values.containsDuplicates == true);

  assert(values.uniques.length == values.length - 2);
  print(values.uniques); // [3, 2, 10, 30, 40, 100]

  values.removeDuplicates();
  print(values); // [3, 2, 10, 30, 40, 100]

  assert(values.numberOfDuplicates == 0);
  assert(!values.containsDuplicates);
  assert(values.duplicates.isEmpty);

  values.addAllByAvoidingDuplicates([3, 2, 10, 200]);
  print(values); // [3, 2, 10, 30, 40, 100, 200]
  assert(values.containsDuplicates == false);
}

FlatMap and CompactMap in Dart



extension CompactMap<T> on List<T> {
  List<E> compactMap<E>(E? Function(T element) f) {
    Iterable<E> imp(E? Function(T element) f) sync* {
      for (final value in this) {
        final mapped = f(value);
        if (mapped != null) {
          yield mapped;
        }
      }
    }

    return imp(f).toList();
  }
}

extension FlatMap<T> on T? {
  E? flatMap<E>(E? Function(T value) f) {
    if (this != null) {
      return f(this!);
    } else {
      return null;
    }
  }
}

void testIt() {
  final foo = [1, 2, null, 3, null, 4];
  final bar = foo.compactMap((element) => element.flatMap((e) => e * 2));
  print(bar); // prints 2, 4, 6, 8
}

Equality of List<T> in Dart



extension Equality<T extends Comparable> on List<T> {
  bool isEqualTo(List<T> other) {
    if (other.length != length) {
      return false;
    }
    for (var i = 0; i < length; i++) {
      if (other[i] != this[i]) {
        return false;
      }
    }
    return true;
  }
}

int ascendingComparator<T extends Comparable>(T lhs, T rhs) =>
    lhs.compareTo(rhs);
int descendingComparator<T extends Comparable>(T lhs, T rhs) =>
    rhs.compareTo(lhs);

extension Sorted<T extends Comparable> on List<T> {
  List<T> sorted({bool descending = false}) => descending
      ? ([...this]..sort(descendingComparator))
      : ([...this]..sort(ascendingComparator));
}

void testIt() {
  assert([1, 2, 3].isEqualTo([1, 2, 3]));
  assert(![1, 2, 3].isEqualTo([1, 2, 2]));
  assert([3, 1, 2].sorted().isEqualTo([1, 2, 3]));
  assert(![3, 1, 2].sorted().isEqualTo([3, 1, 2]));
  assert(['Foo', 'Bar', 'Baz'].isEqualTo(['Foo', 'Bar', 'Baz']));
  assert(!['Foo', 'Bar', 'Baz'].isEqualTo(['foo', 'Bar', 'Baz']));
}

Constants in Dart




class Person {
  final String name;
  final int age;
  const Person({required this.name, required this.age});
}

const foo = Person(name: 'Foo', age: 20);
const foo2 = Person(name: 'Foo', age: 20);
const bar = Person(name: 'Bar', age: 20);

void assert_eq(Object lhs, Object rhs) {
  assert(lhs == rhs);
}

void assert_ne(Object lhs, Object rhs) {
  assert(lhs != rhs);
}

void testIt() {
  assert_eq(foo, foo2);
  assert_ne(foo, bar);
  assert_ne(foo2, bar);
}

Displaying Scrollable Bottom Sheets in Flutter



class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Scrollable Sheet')),
      body: DraggableScrollableSheet(
        initialChildSize: 0.2,
        minChildSize: 0.2,
        maxChildSize: 0.8,
        builder: (context, scrollController) {
          return Container(
            decoration: decoration(),
            clipBehavior: Clip.antiAlias,
            child: SingleChildScrollView(
              controller: scrollController,
              child: column(),
            ),
          );
        },
      ),
    );
  }
}

const urls = [
  'https://tinyurl.com/4vtvh35h',
  'https://tinyurl.com/pujhs55w',
  'https://tinyurl.com/u5k7zueh',
];

List<Widget> imageWithLoremIpsum(String uri) => [
      Image.network(uri),
      SizedBox(height: 10),
      loremIpsum(),
      SizedBox(height: 10),
    ];

Column column() => Column(
      children: imageWithLoremIpsum(urls[0]) +
          imageWithLoremIpsum(urls[1]) +
          imageWithLoremIpsum(urls[2]),
    );

Text loremIpsum() => Text(
      'Lorem ipsum ' * 10,
      textAlign: TextAlign.center,
    );

BoxDecoration decoration() => BoxDecoration(
      border: Border.all(color: Colors.white),
      borderRadius: BorderRadius.only(
        topLeft: Radius.circular(10),
        topRight: Radius.circular(10),
      ),
      color: Colors.white70,
    );

YouTube Ad Remover in Dart



import 'dart:io' show stdout, stderr, exitCode;
import 'package:collection/collection.dart' show IterableExtension;

// example argument: https://www.youtube.com/watch?v=mtETXtSP0pA
void main(List<String> args) async {
  if (args.isEmpty) {
    stdout.writeln('usage: dart youtube.dart "https://..."');
    return;
  }

  final link =
      args.firstWhereOrNull((element) => Uri.tryParse(element) != null);

  if (link == null) {
    stderr.writeln('No YouTube url found');
    exitCode = 1;
    return;
  }

  try {
    final uri = Uri.parse(link);
    if (uri.scheme.toLowerCase() != 'https' ||
        uri.host.toLowerCase() != 'www.youtube.com' ||
        uri.queryParameters['v'] == null) {
      throw FormatException();
    } else {
      final videoId = uri.queryParameters['v'];
      final embedUri = Uri.parse('${uri.scheme}://${uri.host}/embed/$videoId');
      stdout.writeln(embedUri);
      exitCode = 0;
    }
  } on FormatException catch (e) {
    stderr.writeln('Invalid Uri, try again! err = $e');
    exitCode = 1;
    return;
  }
}

Fade Between Widgets in Flutter



const urls = [
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
];

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  var isShowingFirstImage = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedCrossFade in Flutter'),
      ),
      body: Center(
        child: AnimatedCrossFade(
          layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) {
            return GestureDetector(
              onTap: () {
                setState(() {
                  isShowingFirstImage = !isShowingFirstImage;
                });
              },
              child: AnimatedCrossFade.defaultLayoutBuilder(
                  topChild, topChildKey, bottomChild, bottomChildKey),
            );
          },
          firstChild: Image.network(urls[0]),
          secondChild: Image.network(urls[1]),
          crossFadeState: isShowingFirstImage
              ? CrossFadeState.showFirst
              : CrossFadeState.showSecond,
          duration: Duration(milliseconds: 400),
        ),
      ),
    );
  }
}

Sort Descriptors in Dart



int ascendingComparator<T extends Comparable>(T lhs, T rhs) =>
    lhs.compareTo(rhs);
int descendingComparator<T extends Comparable>(T lhs, T rhs) =>
    rhs.compareTo(lhs);

extension Sorted<T extends Comparable> on List<T> {
  List<T> sorted({bool descending = false}) => descending
      ? (this..sort(descendingComparator))
      : (this..sort(ascendingComparator));
}

class Person implements Comparable {
  final int age;
  final String name;
  const Person({required this.age, required this.name});
  @override
  int compareTo(covariant Person other) => age.compareTo(other.age);
  @override
  String toString() => 'Person, name = $name ($age)';
}

void testIt() {
  final people = [
    Person(age: 39, name: 'Father Foo'),
    Person(age: 40, name: 'Mother Bar'),
    Person(age: 13, name: 'Son Baz'),
  ];

  print('ascending sort');
  people.sorted().forEach(print);
  // prints Son Baz (13), Father Foo (39), Mother Bar (40)
  print('descending sort');
  people.sorted(descending: true).forEach(print);
  // prints Mother Bar (40), Father Foo (39), Son Baz (13)
}

User Sortable Columns and Tables in Flutter



class Language {
  final String name;
  final Image image;
  const Language(this.name, this.image);
  Language.dart()
      : name = 'Dart',
        image = Image.network('https://bit.ly/3yH1Ivj');
  Language.rust()
      : name = 'Rust',
        image = Image.network('https://bit.ly/3lPTqhb');
  Language.python()
      : name = 'Python',
        image = Image.network('https://bit.ly/3iCFCEP');

  Language.java()
      : name = 'Java',
        image = Image.network('https://bit.ly/3CCapJH');
  static List<Language> all = [
    Language.dart(),
    Language.rust(),
    Language.python(),
    Language.java(),
  ];
}

extension Sort on List<Language> {
  void sortByName(bool ascending) => sort((lhs, rhs) =>
      ascending ? lhs.name.compareTo(rhs.name) : rhs.name.compareTo(lhs.name));
}

List<DataRow> rows(List<Language> langs) => langs
    .map(
      (l) => DataRow(
        cells: [
          DataCell(
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: l.image,
            ),
          ),
          DataCell(Text(l.name)),
        ],
      ),
    )
    .toList();

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<Language> _langs = Language.all..sortByName(true);
  int sortedColumnIndex = 1;
  var isSortedAscending = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('WhatsApp')),
      body: DataTable(
        sortAscending: isSortedAscending,
        sortColumnIndex: sortedColumnIndex,
        columns: [
          DataColumn(label: Text('Image')),
          DataColumn(
            label: Text('Name'),
            onSort: (columnIndex, ascending) {
              setState(() {
                sortedColumnIndex = columnIndex;
                isSortedAscending = ascending;
                _langs.sortByName(ascending);
              });
            },
          ),
        ],
        rows: rows(_langs),
      ),
    );
  }
}

Content-Length of List<Uri> in Dart

Recursive Dot Notation on Maps in Dart




final person = {
  'firstName': 'Foo',
  'lastName': 'Bar',
  'age': 30,
  'address': {
    'street': {
      'name': 'Baz street',
      'numberOfHouses': 20,
    },
    'houseNumber': '#20',
    'city': 'Stockholm',
    'country': 'Sweden'
  },
};

extension KeyPath on Map {
  Object? valueFor({required String keyPath}) {
    final keysSplit = keyPath.split('.');
    final thisKey = keysSplit.removeAt(0);
    final thisValue = this[thisKey];
    if (keysSplit.isEmpty) {
      return thisValue;
    } else if (thisValue is Map) {
      return thisValue.valueFor(keyPath: keysSplit.join('.'));
    }
  }
}

void testIt() {
  assert(person.valueFor(keyPath: 'firstName') == 'Foo');
  assert(person.valueFor(keyPath: 'age') == 30);
  assert(person.valueFor(keyPath: 'address.street.name') == 'Baz street');
  assert(person.valueFor(keyPath: 'address.houseNumber') == '#20');
}

Allow User Selection of Text in Flutter



const text = 'Flutter is an open-source UI software development'
    ' kit created by Google. It is used to develop cross platform applications'
    ' for Android, iOS, Linux, Mac, Windows, Google Fuchsia, '
    'and the web from a single codebase.';

const imageUrl = 'https://bit.ly/3gT5Qk2';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Selectable Text in Flutter'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Image.network(imageUrl),
          SizedBox(height: 10.0),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: SelectableText(
              text,
              textAlign: TextAlign.center,
              showCursor: true,
              cursorColor: Colors.blue,
              toolbarOptions: ToolbarOptions(
                copy: true,
                selectAll: true,
              ),
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.w300,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Placing Constraints on Widgets in Flutter



const dashes = [
  'https://bit.ly/3gHlTCU',
  'https://bit.ly/3wOLO1c',
  'https://bit.ly/3cXWD9j',
  'https://bit.ly/3gT5Qk2',
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ConstrainedBox in Flutter'),
      ),
      body: InteractiveViewer(
        minScale: 1.0,
        maxScale: 2.0,
        child: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Table(
            defaultVerticalAlignment: TableCellVerticalAlignment.middle,
            children: dashes
                .map(
                  (dash) => TableRow(
                    children: [
                      ConstrainedBox(
                        constraints: BoxConstraints(
                          minHeight: 300,
                        ),
                        child: Image.network(dash),
                      ),
                    ],
                  ),
                )
                .toList(),
          ),
        ),
      ),
    );
  }
}

Animating Position Changes in Flutter



class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  var isMovedUp = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('AnimatedPositioned in Flutter')),
      body: Center(
        child: GestureDetector(
          onTap: () => setState(() => isMovedUp = !isMovedUp),
          child: Stack(
            clipBehavior: Clip.none,
            alignment: Alignment.center,
            children: [
              Image.network('https://bit.ly/2VcCSow'),
              Text(
                'Summer 😎',
                style: TextStyle(
                  fontSize: 30,
                  color: Colors.black,
                ),
              ),
              AnimatedPositioned(
                duration: Duration(seconds: 1),
                bottom: isMovedUp ? 140 : 10.0,
                curve: Curves.elasticInOut,
                child: CircleAvatar(
                  radius: 100,
                  backgroundImage: NetworkImage('https://bit.ly/3cXWD9j'),
                  backgroundColor: Colors.orange[300],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Transitioning Between Widgets in Flutter



enum Season { spring, summer, autumn, winter }

extension Caps on String {
  String get capitalized => this[0].toUpperCase() + substring(1);
}

extension Title on Season {
  String get title => describeEnum(this).capitalized;
}

class TitledImage {
  final String title;
  final Uri uri;
  final ValueKey key;
  const TitledImage(this.title, this.uri, this.key);

  TitledImage.spring()
      : title = Season.spring.title,
        uri = Uri.https('cnn.it', '/3xu58Ap'),
        key = ValueKey(1);

  TitledImage.summer()
      : title = Season.summer.title,
        uri = Uri.https('bit.ly', '/2VcCSow'),
        key = ValueKey(2);

  TitledImage.autumn()
      : title = Season.autumn.title,
        uri = Uri.https('bit.ly', '/3A3zStC'),
        key = ValueKey(3);

  TitledImage.winter()
      : title = Season.winter.title,
        uri = Uri.https('bit.ly', '/2TNY7wi'),
        key = ValueKey(4);
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  var _img = TitledImage.summer();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(_img.title)),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          AnimatedSwitcher(
            switchInCurve: Curves.easeIn,
            switchOutCurve: Curves.easeOut,
            duration: Duration(milliseconds: 300),
            transitionBuilder: (child, animation) {
              return FadeTransition(opacity: animation, child: child);
            },
            child: Image.network(
              _img.uri.toString(),
              key: _img.key,
            ),
          ),
          getButtons(),
        ],
      ),
    );
  }

  Widget getButtons() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        TextButton(
          onPressed: () => setState(() => _img = TitledImage.spring()),
          child: Text(Season.spring.title),
        ),
        TextButton(
          onPressed: () => setState(() => _img = TitledImage.summer()),
          child: Text(Season.summer.title),
        ),
        TextButton(
          onPressed: () => setState(() => _img = TitledImage.autumn()),
          child: Text(Season.autumn.title),
        ),
        TextButton(
          onPressed: () => setState(() => _img = TitledImage.winter()),
          child: Text(Season.winter.title),
        ),
      ],
    );
  }
}

Doubly Linked Lists in Dart



class Person extends LinkedListEntry<Person> {
  final String name;
  final int age;
  Person({
    required this.name,
    required this.age,
  });

  @override
  String toString() => 'Person name = $name, age = $age';
}

void testIt() {
  final persons = LinkedList<Person>();
  final dad = Person(name: 'Father Foo', age: 47);
  final mom = Person(name: 'Mother Bar', age: 47);
  final daughter = Person(name: 'Daughter Baz', age: 22);
  persons.addAll([dad, mom, daughter]);

  print(persons.first.previous); // null
  print(persons.first); // Person name = Father Foo, age = 47
  print(persons.first.next); // Person name = Mother Bar, age = 47
  print(persons.last.previous); // Person name = Mother Bar, age = 47
  print(persons.first.next?.next); // Person name = Daughter Baz, age = 22
  print(persons.last.next); // null
}

Reordering Items Inside List Views in Flutter



class Item {
  final Color color;
  final String text;
  final UniqueKey uniqueKey;
  Item(this.color, this.text) : uniqueKey = UniqueKey();
}

extension ToListItem on Item {
  Widget toListItem() => LimitedBox(
        key: uniqueKey,
        maxHeight: 200,
        child: Container(
          color: color,
          child: Padding(
            padding: const EdgeInsets.all(20),
            child: Text(
              text,
              style: TextStyle(
                color: Colors.white,
                fontSize: 100,
              ),
            ),
          ),
        ),
      );
}

class _HomePageState extends State<HomePage> {
  var items = [
    Item(Colors.deepPurple, 'Foo'),
    Item(Colors.blueGrey, 'Bar'),
    Item(Colors.lightGreen, 'Baz')
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Reordered List View in Flutter'),
      ),
      body: ReorderableListView(
        onReorder: (oldIndex, newIndex) {
          setState(() {
            final item = items.removeAt(oldIndex);
            items.insert(newIndex, item);
          });
        },
        children: items.map((i) => i.toListItem()).toList(),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

Custom Stream Transformers in Dart



in this example we have created our own string transformer that
can trim a Stream<String> by trimming whitespace from both
beginning and end of the string
*/

import 'dart:convert' show utf8;

class StringTrimmer extends StreamTransformerBase<String, String> {
  const StringTrimmer();
  @override
  Stream<String> bind(Stream<String> stream) =>
      Stream.fromFuture(stream.join(' ')).map((str) => str.trim());
}

final string =
    '''   A long line of text with spaces in the beginning and the end, 
    divided into three lines just for the purpose of this demonstration    ''';

void testIt() async {
  final bytes = utf8.encode(string);
  final result = await Stream.value(bytes)
      .transform(utf8.decoder)
      .transform(LineSplitter())
      .transform(StringTrimmer())
      .join();
  print(result);
}

Expanding Stream Elements in Dart

/*


in this example we expand every element inside our Stream<int> to
a stream that in turn contains n+1 elements where n is the index generated
by our main stream, that's to say, 0, 1, 2, 3, 4, etc

*/
Stream<int> nestedEvents(int count) {
  return Stream.periodic(
    Duration(seconds: 1),
    (e) => e,
  ).take(count).asyncExpand(
        (i) => Stream.fromIterable(
          Iterable.generate(i + 1),
        ),
      );
}

void testIt() async {
  /* 
  prints the followings in this order
  0, 1
  0, 1, 2
  0, 1, 2, 3
  0, 1, 2, 3, 4
  */
  await for (final value in nestedEvents(5)) {
    print('Value is $value');
  }
}

Consume Streams for a Duration in Dart

extension TakeFor<T> on Stream<T> {
  Stream<T> takeFor(Duration duration) {
    final upTo = DateTime.now().add(duration);
    return takeWhile((_) {
      final now = DateTime.now();
      return now.isBefore(upTo) | now.isAtSameMomentAs(upTo);
    });
  }
}

Stream<DateTime> source() => Stream.periodic(
      Duration(milliseconds: 500),
      (_) => DateTime.now(),
    );

void testIt() async {
  await for (final dateTime in source().takeFor(
    Duration(seconds: 4),
  )) {
    print('date time is $dateTime');
  }
}

Shortening URLs in Dart

import 'dart:convert' show json;

Future<Uri> shortenUri(Uri uri, String bitlyToken) async {
  final client = HttpClient();

  final endpoint = Uri.https('api-ssl.bitly.com', '/v4/shorten');

  final response = await client.postUrl(endpoint).then(
    (req) {

      req.headers
        ..set(HttpHeaders.contentTypeHeader, 'application/json')
        ..set(HttpHeaders.authorizationHeader, 'Bearer $bitlyToken');

      final body = {
        'long_url': uri.toString(),
        'domain': 'bit.ly',
      };
      final bodyBytes = utf8.encode(json.encode(body));
      req.add(bodyBytes);

      return req.close();
    },
  );

  final responseBody = await response.transform(utf8.decoder).join();
  final responseJson = json.decode(responseBody) as Map<String, dynamic>;
  return Uri.parse(responseJson['link']);
}

void testIt() async {
  print(await shortenUri(
    Uri.parse('https://pixolity.se'),
    'XXX',
  ));
}

LimitedBox Widget as ListView Items in Flutter

const images = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/3jRSRCu',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

extension ToListItemImage on String {
  Widget toListItemImage() {
    return LimitedBox(
      maxHeight: 150.0,
      child: Image.network(
        this,
        fit: BoxFit.fitWidth,
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Limited Box in Flutter')),
      body: ListView(
        children: images.map((str) => str.toListItemImage()).toList(),
      ),
    );
  }
}

Generically Convert Anything to Int in Dart

extension ToInt on Object {
  int toInt() {
    final list = [
      if (this is Iterable<Object>)
        ...(List.of(this as Iterable<Object>))
      else if (this is int)
        [this as int]
      else
        (double.tryParse(toString()) ?? 0.0).round()
    ];
    return list
        .map((e) => (double.tryParse(e.toString()) ?? 0.0).round())
        .reduce((lhs, rhs) => lhs + rhs);
  }
}

void testIt() {
  assert(1.toInt() == 1);
  assert((2.2).toInt() == 2);
  assert((2.0).toInt() == 2);
  assert('3'.toInt() == 3);
  assert(['4', '5'].toInt() == 9);
  assert([4, 5].toInt() == 9);
  assert(['2.4', '3.5'].toInt() == 6);
  assert(['2', '3.5'].toInt() == 6);
  assert({'2', 3, '4.2'}.toInt() == 9);
  assert(['2', 3, '4.2', 5.3].toInt() == 14);
}

Validating URL Certificates in Dart

import 'dart:io' show HttpClient;

Future<bool> isSecuredWithValidCert(String uriString) async {
  final uri = Uri.parse(uriString);
  final client = HttpClient();
  try {
    await client.headUrl(uri).then((r) => r.close());
    return true;
  } on HandshakeException {
    return false;
  }
}

void testIt() async {
  await isSecuredWithValidCert('https://expired.badssl.com');
  await isSecuredWithValidCert('https://wrong.host.badssl.com');
  await isSecuredWithValidCert('https://self-signed.badssl.com');
  await isSecuredWithValidCert('https://untrusted-root.badssl.com');
  await isSecuredWithValidCert('https://revoked.badssl.com');
}

Displaying Popup Menus in Flutter

enum ImageAction { copy }

PopupMenuItem<ImageAction> copyButton({VoidCallback? onPressed}) =>
    PopupMenuItem<ImageAction>(
      value: ImageAction.copy,
      child: TextButton.icon(
        icon: Icon(Icons.copy),
        label: Text('Copy'),
        onPressed: onPressed,
      ),
    );

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter'),
      ),
      body: Center(
        child: PopupMenuButton<ImageAction>(
          elevation: 10,
          offset: Offset(0, 50),
          itemBuilder: (_) => [
            copyButton(
              onPressed: () {
                print('Copy the image...');
              },
            ),
          ],
          child: Image.network('https://bit.ly/3ywI8l6'),
        ),
      ),
    );
  }
}

Implementing Drag and Drop in Flutter

class HomePage extends StatefulWidget {
  HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String? _imageUrl;

  bool shouldAccept(String? value) => Uri.tryParse(value ?? '') != null;

  Widget dragTargetBuilder(
    BuildContext context,
    List<String?> incoming,
    dynamic rejected,
  ) {
    final emptyContainer = Container(
      color: Colors.grey[200],
      height: 200,
      child: Center(
        child: Text('Drag an image here'),
      ),
    );

    if (incoming.isNotEmpty) {
      _imageUrl = incoming.first;
    }

    if (_imageUrl == null) {
      return emptyContainer;
    }

    try {
      final uri = Uri.parse(_imageUrl ?? '');
      return Container(
        color: Colors.grey[200],
        height: 200,
        child: Center(
          child: Image.network(uri.toString()),
        ),
      );
    } on FormatException {
      return emptyContainer;
    }
  }

  static final firstImageUrl = 'https://bit.ly/3xnoJTm';
  static final secondImageUrl = 'https://bit.ly/3hIuC78';
  final firstImage = Image.network(firstImageUrl);
  final secondImage = Image.network(secondImageUrl);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Tooltips in Flutter')),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            DragTarget<String>(
              onWillAccept: shouldAccept,
              builder: dragTargetBuilder,
            ),
            SizedBox(height: 10.0),
            DraggableImage(
              imageWidget: firstImage,
              imageUrl: firstImageUrl,
            ),
            SizedBox(height: 10.0),
            DraggableImage(
              imageWidget: secondImage,
              imageUrl: secondImageUrl,
            ),
          ],
        ),
      ),
    );
  }
}

class DraggableImage extends StatelessWidget {
  const DraggableImage({
    Key? key,
    required this.imageWidget,
    required this.imageUrl,
  }) : super(key: key);

  final Image imageWidget;
  final String imageUrl;

  @override
  Widget build(BuildContext context) {
    return Draggable<String>(
      data: imageUrl,
      feedback: Container(
        width: MediaQuery.of(context).size.width,
        decoration: BoxDecoration(
          boxShadow: [
            BoxShadow(
              blurRadius: 30,
              color: Colors.black,
              spreadRadius: 10,
            ),
          ],
        ),
        child: imageWidget,
      ),
      child: imageWidget,
    );
  }
}

Dismissing List Items in Flutter

const gridImages = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3dLJNeD',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/3jRSRCu',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          CustomAppBar(),
          CustomGridView(),
          CustomListView(
            imageUrls: gridImages,
          ),
        ],
      ),
    );
  }
}

class _CustomListViewState extends State<CustomListView> {
  @override
  Widget build(BuildContext context) {
    return SliverPadding(
      padding: EdgeInsets.all(8.0),
      sliver: SliverList(
        delegate: SliverChildBuilderDelegate(
          (context, index) {
            final url = widget.imageUrls[index];
            return Dismissible(
              key: ValueKey(url),
              onDismissed: (_) {
                widget.imageUrls.remove(url);
              },
              background: Container(
                color: Colors.red,
                child: FittedBox(
                  alignment: Alignment.centerRight,
                  fit: BoxFit.fitHeight,
                  child: Icon(Icons.delete, color: Colors.white),
                ),
              ),
              child: Padding(
                padding: const EdgeInsets.only(bottom: 8.0),
                child: Image.network(url),
              ),
            );
          },
          childCount: widget.imageUrls.length,
        ),
      ),
    );
  }
}

class CustomListView extends StatefulWidget {
  final List<String> imageUrls;

  const CustomListView({
    Key? key,
    required this.imageUrls,
  }) : super(key: key);

  @override
  _CustomListViewState createState() => _CustomListViewState();
}

class CustomGridView extends StatelessWidget {
  const CustomGridView({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SliverPadding(
      padding: EdgeInsets.all(8.0),
      sliver: SliverGrid(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
          childAspectRatio: 1.0,
        ),
        delegate: SliverChildBuilderDelegate(
          (context, index) {
            return Container(
              width: 100,
              height: 100,
              child: Image.network(gridImages[index]),
            );
          },
          childCount: gridImages.length,
        ),
      ),
    );
  }
}

class CustomAppBar extends StatelessWidget {
  const CustomAppBar({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SliverAppBar(
      backgroundColor: Colors.orange[300],
      forceElevated: true,
      pinned: false,
      snap: false,
      floating: true,
      expandedHeight: 172,
      flexibleSpace: FlexibleSpaceBar(
        title: Text(
          'Flutter',
          style: TextStyle(
            fontSize: 30,
            color: Colors.white,
            decoration: TextDecoration.underline,
          ),
        ),
        collapseMode: CollapseMode.parallax,
        background: Image.network('https://bit.ly/3x7J5Qt'),
      ),
    );
  }
}

Animating Widgets with Ease in Flutter

class Ball extends StatefulWidget {
  const Ball({Key? key}) : super(key: key);

  @override
  _BallState createState() => _BallState();
}

class _BallState extends State<Ball> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 4),
      reverseDuration: Duration(seconds: 4),
    );
    _animation = Tween(begin: 0.0, end: 2 * pi).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _controller.repeat();
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, image) {
        return Transform.rotate(
          angle: _animation.value,
          child: image,
        );
      },
      child: Image.network('https://bit.ly/3xspdrp'),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animated Builder in Flutter'),
      ),
      body: Center(
        child: Ball(),
      ),
    );
  }
}

Displaying Tool Tips in Flutter

const imagesAndInfo = [
  ['https://bit.ly/3xnoJTm', 'Stockholm, Sweden'],
  ['https://bit.ly/3hIuC78', 'Dalarna, Sweden'],
  ['https://bit.ly/3wi9mdG', 'Brighton, UK'],
  ['https://bit.ly/3dSSMuy', 'Hove, UK'],
  ['https://bit.ly/3xoWCmV', 'Kerala, India'],
  ['https://bit.ly/3hGmjZC', 'Salvador da Bahia, Brazil']
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Tooltips in Flutter')),
      body: ListView.builder(
        itemCount: imagesAndInfo.length,
        itemBuilder: (_, index) {
          return Padding(
            padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
            child: Tooltip(
              decoration: BoxDecoration(
                color: Colors.black,
                boxShadow: [
                  BoxShadow(
                    color: Colors.white.withAlpha(180),
                    offset: Offset.zero,
                    spreadRadius: 30.0,
                    blurRadius: 30.0,
                  ),
                ],
                borderRadius: BorderRadius.all(Radius.circular(8.0)),
              ),
              textStyle: TextStyle(fontSize: 20, color: Colors.white),
              message: imagesAndInfo[index][1],
              child: Image.network(
                imagesAndInfo[index][0],
              ),
            ),
          );
        },
      ),
    );
  }
}

Displaying Assorted Widgets Inside TableView in Flutter

const natureUrls = [
  'https://bit.ly/3dAtFwy',
  'https://bit.ly/36cHehe',
  'https://bit.ly/365uqt1',
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3jBvJYU',
  'https://bit.ly/3yhbHHi'
];

extension ToImage on String {
  Widget toPaddedNetworkImage() => Padding(
        padding: const EdgeInsets.all(8.0),
        child: Image.network(this),
      );
}

extension ToImages on List<String> {
  List<Widget> toPaddedNetworkImages() =>
      map((str) => str.toPaddedNetworkImage()).toList();
}

extension ToTableRow on List<Widget> {
  TableRow toTableRow() => TableRow(children: this);
}

class ListPaginator<T> extends Iterable {
  final List<List<T>> list;
  ListPaginator({required List<T> input, required int itemsPerPage})
      : list = [
          for (var i = 0; i < input.length; i += itemsPerPage)
            input.getRange(i, min(input.length, i + itemsPerPage)).toList(),
        ];

  @override
  Iterator get iterator => list.iterator;
}

class HomePage extends StatelessWidget {
  final provider = ListPaginator<String>(
    input: natureUrls,
    itemsPerPage: 3,
  );
  HomePage({Key? key}) : super(key: key);

  Iterable<TableRow> getRows() sync* {
    for (final List<String> urlBatch in provider) {
      final networkImages = urlBatch.toPaddedNetworkImages();
      yield TableRow(children: networkImages);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TableView in Flutter'),
      ),
      body: SingleChildScrollView(
        child: Table(
          defaultVerticalAlignment: TableCellVerticalAlignment.bottom,
          children: getRows().toList(),
        ),
      ),
    );
  }
}

Page Indicator with Page View in Flutter

const dashes = [
  'https://bit.ly/3gHlTCU',
  'https://bit.ly/3wOLO1c',
  'https://bit.ly/3cXWD9j',
  'https://bit.ly/3gT5Qk2',
];

class PageText extends StatelessWidget {
  final int current;
  final int total;

  const PageText({
    Key? key,
    required this.current,
    required this.total,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(
      'Page ${current + 1} of $total',
      style: TextStyle(fontSize: 30.0, shadows: [
        Shadow(
          offset: Offset(0.0, 1.0),
          blurRadius: 20.0,
          color: Colors.black.withAlpha(140),
        )
      ]),
    );
  }
}

class _HomePageState extends State<HomePage> {
  var _index = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Page Indicator')),
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: PageView.builder(
                onPageChanged: (pageIndex) {
                  setState(() => _index = pageIndex);
                },
                scrollDirection: Axis.horizontal,
                itemCount: dashes.length,
                itemBuilder: (context, index) {
                  return Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Image.network(dashes[index]),
                      Text('Dash #${index + 1}'),
                    ],
                  );
                },
              ),
            ),
            PageText(current: _index, total: dashes.length)
          ],
        ),
      ),
    );
  }
}

Animating and Moving a Floating Action Button in Flutter

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

const List<FloatingActionButtonLocation> locations = [
  FloatingActionButtonLocation.centerDocked,
  FloatingActionButtonLocation.startDocked,
  FloatingActionButtonLocation.startFloat,
  FloatingActionButtonLocation.centerFloat,
  FloatingActionButtonLocation.endFloat,
  FloatingActionButtonLocation.endDocked
];

extension GoAround<T> on List<T> {
  T elementByGoingAround(int index) {
    final finalIndex = index >= length ? index.remainder(length) : index;
    return this[finalIndex];
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  var _locationIndex = 0;

  FloatingActionButtonLocation get location =>
      locations.elementByGoingAround(_locationIndex);

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text('Floating Action Button'),
      ),
      floatingActionButtonLocation: location,
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() => _locationIndex += 1);
        },
        child: Icon(Icons.add),
      ),
      bottomNavigationBar: BottomNavigationBar(
        backgroundColor: Colors.yellow[600],
        selectedItemColor: Colors.black,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.bedtime),
            label: 'Item 1',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.access_alarms),
            label: 'Item 2',
          )
        ],
        currentIndex: 0,
      ),
    );
  }
}

Fading Network Image Widget in Flutter

class FadingNetworkImage extends StatefulWidget {
  final String url;

  const FadingNetworkImage({Key? key, required this.url}) : super(key: key);

  @override
  _FadingNetworkImageState createState() => _FadingNetworkImageState();
}

class _FadingNetworkImageState extends State<FadingNetworkImage>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 1));

    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Image.network(
      widget.url,
      frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
        _controller.reset();
        _controller.forward();
        return FadeTransition(opacity: _animation, child: child);
      },
      loadingBuilder: (context, child, loadingProgress) {
        final totalBytes = loadingProgress?.expectedTotalBytes;
        final bytesLoaded = loadingProgress?.cumulativeBytesLoaded;
        if (totalBytes != null && bytesLoaded != null) {
          return LinearProgressIndicator(
            value: bytesLoaded / totalBytes,
          );
        } else {
          return child;
        }
      },
      errorBuilder: (context, error, stackTrace) {
        return Text('Error!');
      },
    );
  }
}

const dashes = [
  'https://bit.ly/3gHlTCU',
  'https://bit.ly/3wOLO1c',
  'https://bit.ly/3cXWD9j',
  'https://bit.ly/3gT5Qk2',
];

extension GoAround<T> on List<T> {
  T elementByGoingAround(int index) {
    final finalIndex = index >= length ? index.remainder(length) : index;
    return this[finalIndex];
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Faded Image'),
      ),
      body: Center(
          child: Column(
        children: [
          FadingNetworkImage(
            url: dashes.elementByGoingAround(_index),
          ),
          TextButton(
            onPressed: () {
              setState(() => _index += 1);
            },
            child: Text('Load next Dash'),
          ),
        ],
      )),
    );
  }
}

Transparent Alert Dialogs in Flutter

TextStyle get whiteTextStyle => TextStyle(color: Colors.white);

Future<void> showTextDialog({
  required BuildContext context,
  required String text,
}) {
  return showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.all(
            Radius.circular(10),
          ),
          side: BorderSide(
            color: Colors.white,
            style: BorderStyle.solid,
            width: 2,
          ),
        ),
        backgroundColor: Colors.black.withAlpha(150),
        titleTextStyle: whiteTextStyle,
        contentTextStyle: whiteTextStyle,
        content: Text(text),
        actions: [
          TextButton(
            style: TextButton.styleFrom(primary: Colors.white),
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: Text('OK'),
          )
        ],
      );
    },
  );
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Rounded Corder Dialog',
        ),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Image.network('https://bit.ly/3ywI8l6'),
          TextButton(
            onPressed: () async {
              await showTextDialog(
                context: context,
                text: 'Hello world',
              );
            },
            child: Text('Show dialog'),
          ),
        ],
      ),
    );
  }
}

Network Image Size in Dart

import 'dart:ui' as ui;

Future<Size> getImageSize(String uri) {
  final image = Image.network('https://bit.ly/3dAtFwy');
  final comp = Completer<ui.Image>();
  image.image
      .resolve(
        ImageConfiguration.empty,
      )
      .addListener(
        ImageStreamListener(
          (ImageInfo info, _) => comp.complete(info.image),
        ),
      );
  return comp.future.then(
    (image) => Size(
      image.width.toDouble(),
      image.height.toDouble(),
    ),
  );
}

void testIt() async {
  final imageSize = await getImageSize('https://bit.ly/3dAtFwy');
  print(imageSize);
  assert(imageSize.width == 2048.0);
  assert(imageSize.height == 1365.0);
  print(imageSize.aspectRatio);
}

Animated Icons in Flutter

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late final Animation<double> _animation;
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );
    _animation = Tween(
      begin: 0.0,
      end: 1.0,
    ).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _controller.repeat(reverse: true);
    return Scaffold(
      appBar: AppBar(
        title: Text('Animated Icons in Fluter'),
      ),
      body: Center(
        child: AnimatedIcon(
          color: Colors.green[300],
          size: MediaQuery.of(context).size.width,
          icon: AnimatedIcons.search_ellipsis,
          progress: _animation,
        ),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

Custom Scroll Views in Flutter

const gridImages = [
  'https://bit.ly/3x7J5Qt',
  'https://bit.ly/3dLJNeD',
  'https://bit.ly/3ywI8l6',
  'https://bit.ly/3jRSRCu',
  'https://bit.ly/36fNNj9',
  'https://bit.ly/3jOueGG',
  'https://bit.ly/3qYOtDm',
  'https://bit.ly/3wt11Ec',
  'https://bit.ly/3yvFg7X',
  'https://bit.ly/3ywzOla',
  'https://bit.ly/3wnASpW',
  'https://bit.ly/3jXSDto',
];

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          CustomAppBar(),
          CustomGridView(),
          CustomListView(),
        ],
      ),
    );
  }
}

class CustomListView extends StatelessWidget {
  const CustomListView({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SliverPadding(
      padding: EdgeInsets.all(8.0),
      sliver: SliverList(
        delegate: SliverChildBuilderDelegate(
          (context, index) {
            return Padding(
              padding: const EdgeInsets.only(bottom: 8.0),
              child: Image.network(gridImages[index]),
            );
          },
          childCount: gridImages.length,
        ),
      ),
    );
  }
}

class CustomGridView extends StatelessWidget {
  const CustomGridView({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SliverPadding(
      padding: EdgeInsets.all(8.0),
      sliver: SliverGrid(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
          childAspectRatio: 1.0,
        ),
        delegate: SliverChildBuilderDelegate(
          (context, index) {
            return Container(
              width: 100,
              height: 100,
              child: Image.network(gridImages[index]),
            );
          },
          childCount: gridImages.length,
        ),
      ),
    );
  }
}

class CustomAppBar extends StatelessWidget {
  const CustomAppBar({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SliverAppBar(
      backgroundColor: Colors.orange[300],
      forceElevated: true,
      pinned: false,
      snap: false,
      floating: true,
      expandedHeight: 172,
      flexibleSpace: FlexibleSpaceBar(
        title: Text(
          'Flutter',
          style: TextStyle(
            fontSize: 30,
            color: Colors.white,
            decoration: TextDecoration.underline,
          ),
        ),
        collapseMode: CollapseMode.parallax,
        background: Image.network('https://bit.ly/3x7J5Qt'),
      ),
    );
  }
}

Parallax App Bar in Flutter

JSON HTTP Requests in Dart

URL Timeouts in Dart

Detecting URL File Types in Dart

Paginated Lists in Dart

Requesting DELETE on APIs in Dart

Animated Containers in Flutter

Hiding Widgets in Flutter

Simple Opacity Animation in Flutter

Vignette Widget in Flutter

Drop Down Button Configuration and Usage in Flutter

Expandable List Items in Flutter

Infinite Scrolling in Flutter

Infinite Arrays in Dart

Custom Color Picker Component in Flutter

Displaying and Reacting to Switches in Flutter

Displaying Bottom Bars in Flutter

Displaying Buttons on AppBar in Flutter

Displaying Bottom Sheets in Flutter

Converting Enums to Radio Buttons in Flutter

Check Existence of Websites in Flutter

Images inside AlertDialog in Flutter

Returning Values from AlertDialog in Flutter

Simple Grid View in Flutter

Rendering Bullet Points in Flutter

Retrying Futures in Flutter

Containers as ClipOvals in Flutter

Rich Texts in Flutter

Wrapping Widgets in Flutter

Sweep Gradients in Flutter

Stream and StreamBuilder in Flutter

Blur Effect in Flutter

Convert Enums to Strings in Dart

Replacing Text in TextField in Flutter

Aspect Ratio in Flutter

Zoom and Pan in Flutter

Resizing Images in Flutter to Fit Screen Height

Validating URLs in Flutter

FrameBuilder for Network Images in Flutter

Adding Shadow to Icons in Flutter

Calculating Median of Lists in Dart

Generic Functions with Reduce in Dart

Passing Back Data From a Screen to the Previous One in Flutter

Flinging an Animation in Flutter

Fade Animations in Flutter

Throttling User Input in Flutter

Censoring TextFields in Flutter

Customizing TextButton in Flutter

Multiline TextFields in Flutter

Filtering TextField Input in Flutter

Focusing Manually on TextFields in Flutter

Data Streams Over HTTP/HTTPs in Dart

Catching Nonexistent Accessors or Methods in Dart

Using Expando in Dart

Implementing Custom Maps in Dart

Dynamically Calling Functions in Dart

Factory Constructors in Dart

Calculating the Sum of List Items in Dart

Removing Duplicate Strings in Lists in Dart (Case-Insensitive)

Implementing Range in Dart

Converting Lists to Maps in Dart

Implementing Hashable in Dart

Random Name Generator in Dart

Capturing Stack Traces in Dart Exceptions

Removing Duplicates from Lists in Dart

Optional Spread Operator in Dart

Calling Optional Functions in Dart

Odd-Even Sort in Dart

Implementing Zip and Tuples in Dart

Swapping Values in Lists with XOR in Dart

Waiting for Multiple Futures in Dart

Using Queues as Stacks in Dart

Custom Iterators in Dart

Iterables as Ranges and Transform in Dart

Errors vs Exceptions in Dart

Custom Annotations in Dart

Classes as Enums in Dart

Spread Operator in Collection Literals in Dart

StreamBuilder and StreamController in Dart

Almost Equal in Dart

Enum Associated Values in Dart

Implementing Comparable in Dart

Implementing Custom Integer Types in Dart

Custom Subscripts in Dart

Dart List Enumeration with Index

Applying Mixins to Other Mixins in Dart

Parameter Types in Dart

Custom Exceptions in Dart

rethrowing Exceptions in Dart

mixins and JSON Parsing in Dart

mixins vs abstract classes in Dart

Drawing Shapes in Flutter with LayoutBuilder, CustomPaint and CustomPainter

Generic Type Aliases in Dart

Callable Classes in Dart

Synchronous Generators in Dart

Implicit Interfaces in Dart

Did you know that in #Dart, every #class implicitly exports an #interface that can be #implemented (as opposed to #extended) by other classes? This is called "implicit interface".

Do you know how "const" constructors work in #Dart?

Did you know that in #Dart, it is actually preferred to use #async and #await over using raw #Futures?

In #Dart, you can use a combination of #Initializer #List plus default values for your class #member #fields to create elegant and handy convenience initializers

Did you know that in #Dart, you can extract elements of a certain type from your Lists using the #whereType #generic #function instead of calculating the #equality yourselves?

Do you know about #Type #Promotion in Dart?

"address" is an optional field of the "Person" class. If you look at the "doThis()" function you see that I'm saving the value of address in a local variable and then comparing it with null and then returning if it's null. The Dart compiler is intelligent enough to understand that after the if-statement, "address" is NOT null anymore since you've already compared it with null and returned from the function.

If you look at the "insteadOfThis" function, the first one, the Dart compiler cannot make the same assumption if you don't first store the value of address in a local variable. In that first function the Dart compiler, even after the if-statement, needs you to refer to address as an optional, using "address?" syntax.

The mechanism the Dart compiler uses in the "doThis()" function is called Type Promotion.

4 lines of #Dart code that include the #spread operator, #cascade #operator, #generics, #extensions, #private prefix and #getters

Functions as First Class Citizens in Dart

Download Details: 
Author: vandadnp
Source Code: https://github.com/vandadnp/flutter-tips-and-tricks

#flutter #dart #programming #developer 

A Collection of Flutter and Dart Tips and Tricks
Greg  Jackson

Greg Jackson

1638531900

Find out: Rundown of Async/Await

Hey there, my name is Pawan Bisht and I'm a software consultant at Knoldus for the last 1 and a half years focusing on Rust Programming.

This time in Rustlab 2020, I'll be presenting a session on the Asynchronous concept of Rust Programming by primarily focusing on the internals of Asynchronous concept with the practical implementation and we'll also see how Rust futures works internally and what makes a program to run in an asynchronous fashion.

And yes, in this session we'll also talk about some real-life scenarios as well in terms of Asynchronous programming to make this session more interesting. See you there!!!

"As you all know Asynchronous programming lets us run multiple tasks at the same time on a single OS thread.
To be precise, this talk will all about the internal functionality of Async/await like what operations are triggered internally so that we can run our programs asynchronously."

 Async/await (Asynchronous Programming)

As we that async–await syntax hits stable Rust, as part of the 1.39.0 release. So in this talk, we'll try to leverage this interesting feature as much as we can. As the session will be more interactive in terms of asynchronous programming. Like how we write asynchronous code (I know it's very common but to get our hand wet more on this term we'll start with basic first) then we'll understand what operations performed by the Rust compiler that actually makes our code asynchronous like what's the role of waker, poll, etc and how their practical implementation.

This talk will majorly be focused on the internal execution of Rust Futures with async/await.

#async #await #rust 

Find out: Rundown of Async/Await

Async Await in Javascript

In  Javascript Interview questions and answers tutorial, we will learn Async Await in Javascript

Explained in detail about
* Async
* Await
* Fetching country data using Async & Await
* How Async & Await better than promise
* Javascript Interview questions
 

Timeline:
------------------------------------------------------------------------------------
00:00 Introduction
00:28 Async and Async interview question
04:00 Await & interview question on it
08:30 Fetching country data using Async & Await

 #javascript   #await

Async Await in Javascript
Ollie  Dietrich

Ollie Dietrich

1636175760

How Async/Await Works in Javascript

Async/await is a new way to write asynchronous code.  Async/await is actually just syntax sugar built on top of promises. It makes asynchronous code look and behaves a little more like synchronous code.

 

Thumb Rules for async-await:
1. `async` functions return a promise.
2. `await` pauses the code execution within the `async` function.
3. There can be multiple `await` statements within a single `async` function.
4. `await` only pauses the code execution within the `async` function. It makes sure that the next line is executed when the promise resolves.

 

⌚ Timestamps:
00:00 Introduction
00:58 Async
02:27 Await

 #javascript  #async  #await 

How Async/Await Works in Javascript