1658445300
/**
* 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
1657244520
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:
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
.Both of those dependencies are included in sheerun/vim-polyglot so if you're already using that then you should be set.
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.
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
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>
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']
Field | Usage | Required | Default value |
---|---|---|---|
name | The value within the attribute lang or type on the tag as well as the value to include in g:svelte_preprocessors . | Yes | None |
tag | The HTML tag to target e.g. script or style . | Yes | None |
as | The syntax name to use for highlighting. | No | The name attribute. |
Note, that enabling and loading a lot of different syntax definitions can considerably degrade Vim's performance. Consider yourself warned.
eslint
and a few other linters/fixers. PRs welcome if the one you want is missing.#if/:else//if
.let g:syntastic_svelte_checkers = ['javascript/eslint', 'html/htmlhint']
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.
Download Details:
Author: evanleck
Source Code: https://github.com/evanleck/vim-svelte
License:
#svelte #javascript
1656378000
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
, await
et Task
en 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 MonoBehaviour
qui a un Transform
et un float
pour 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;
}
async
, await
, et Task
sont en C#async
?En C#, les méthodes peuvent avoir un mot- async
clé 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 , AsyncOperation
soit le système de tâches C# .
await
et comment l'utiliser ?En C#, vous pouvez attendre la fin d'une opération asynchrone en utilisant le mot- await
clé. Ceci est utilisé à l'intérieur de toute méthode qui a le mot- async
clé 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
.
Task
et comment l'utiliser ?A Task
est une méthode asynchrone qui effectue une seule opération et ne renvoie pas de valeur. Pour a Task
qui 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 .
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");
}
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 MonoBehaviour
que 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 Update
né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 Task
boucle 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 await
pour 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.
La tâche intensive prend environ 4 ms et le jeu tourne à environ 200 FPS.
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.
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.
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 Task
dans Unity est qu'ils s'exécutent tous sur le Main
thread. 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.
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 IEnumerator
et we yield return
au 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 MonoBehaviour
et 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.
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);
}
}
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 Update
mé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.
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 Units
parce 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 .
Pour créer un travail, vous créez un stuct
qui implémente l'une des IJob
interfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJob
est un travail de base. IJobFor
et IJobForParallel
permettent 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 IJobForParallel
sera divisé entre plusieurs threads.
Je vais l'utiliser IJob
pour créer un travail d'opération intensive IJobFor
et IJobForParallel
pour 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 IJob
n'en a pas besoin. Le IJobFor
et IJobParallelFor
ont 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 float3
pour 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 Execute
méthode requise. Le IJobFor
et IJobForParallel
les deux nécessitent un int
pour 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 transform
et 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();
}
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 Schedule
méthode retourne un JobHandle
qui 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 update
mé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 Update
méthode, j'ajoute le case
à la switch
section. 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 MoveEnemy
etMoveEnemyParallelJob
Ensuite, j'ai ajouté les emplois à mon énumération. Puis dans la Update
méthode, j'appelle une nouvelle MoveEnemyJob
méthode, en passant le temps delta. Normalement, vous utiliseriez soit le JobFor
ou 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 moveY
que 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);
}
}
Nous devons attendre que le travail soit terminé. Nous pouvons obtenir le statut à partir du JobHandle
que 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 NativeContainer
que 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 Native
tableau :
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.
Le MoveEnemy
travail
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 transform
positions et moveY
les 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.
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.
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
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 .
Pour plus de détails sur le compilateur de rafale, consultez la documentation Unity .
Un travail intensif avec rafale prend environ 3 ms pour se terminer avec le jeu fonctionnant à environ 150 FPS.
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.
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.
Nous pouvons utiliser Task
pour 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 .
1656375600
当您尝试发布到 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;
}
async
,await
和是什么Task
async
?在 C# 中,方法async
前面可以有一个关键字,表示方法是异步方法。这只是告诉编译器我们希望能够在其中执行代码并允许该方法的调用者在等待该方法完成时继续执行的一种方式。
这方面的一个例子是做饭。您将开始烹饪肉,当肉在烹饪并且您正在等待它完成时,您将开始制作侧面。当食物在烹饪时,你会开始摆桌子。代码中的一个示例是static async Task<Steak> MakeSteak(int number)
.
Unity 还有各种可以异步调用的内置方法;有关方法列表,请参阅Unity 文档。通过 Unity 处理内存管理的方式,它使用协程、AsyncOperation
或C# 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
。
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)
。
我们可以通过多种方式等待 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 指令可供我们使用,即WaitForSeconds
、WaitForEndOfFrame
、WaitUntil
或WaitWhile
。
要启动协程,我们需要 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);
}
}
让我们看看使用协程如何影响我们项目的性能。我只会使用性能密集型方法来做到这一点。
我添加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;
}
密集的协程大约需要 6 毫秒才能完成,游戏以大约 90 FPS 的速度运行。
C# Job System 是 Unity 对易于编写的任务的实现,不会像任务那样产生垃圾,并利用Unity 已经创建的工作线程。这解决了任务的所有缺点。
Unity 将作业比作线程,但他们确实说作业执行一项特定任务。作业也可以在运行前依赖其他作业完成;这解决了我没有正确移动我的任务的问题,Units
因为它依赖于首先完成的另一个任务。
Unity 会自动为我们处理作业依赖项。工作系统还内置了一个安全系统,主要用于防止竞争条件。对作业的一个警告是,它们只能包含blittable 类型或NativeContainer类型的成员变量。这是安全系统的一个缺点。
要使用作业系统,您需要创建作业、安排作业、等待作业完成,然后使用作业返回的数据。需要作业系统才能使用 Unity 的面向数据的技术堆栈 (DOTS)。
有关作业系统的更多详细信息,请参阅Unity 文档。
要创建作业,您需要创建一个stuct
实现其中一个IJob
接口(IJob、IJobFor、IJobParallelFor、Unity.Engine.Jobs.IJobParallelForTransform)的作业。IJob
是一项基本工作。IJobFor
并IJobForParallel
用于对本机容器的每个元素执行相同的操作或进行多次迭代。它们之间的区别在于 IJobFor 在单个线程上运行,其中IJobForParallel
将在多个线程之间拆分。
我将用于IJob
创建一个密集的操作工作,IJobFor
并IJobForParallel
创建一个可以移动多个敌人的工作;这只是为了让我们可以看到对性能的不同影响。这些作业将与我们之前创建的任务和方法相同:
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
方法。和IJobFor
都IJobForParallel
需要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
方法,传递增量时间。通常你会使用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;
}
...
}
}
密集的工作大约需要 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 毫秒。
突发编译器是一种将字节码转换为本机代码的编译器。将此与 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部分来实现。
有关突发编译器的更多详细信息,请参阅Unity 文档。
在游戏以大约 150 FPS 的速度运行时,一个带有突发的密集工作大约需要 3 毫秒才能完成。
显示和移动一千个敌人,爆发的工作大约需要 30 毫秒,帧速率约为 30 FPS。
显示和移动一千个敌人,与爆发并行的工作大约需要 6 毫秒,帧速率约为 80 到 90 FPS。
我们可以使用它Task
来提高 Unity 应用程序的性能,但使用它们有几个缺点。根据我们想要做的事情,最好使用 Unity 中打包的东西。如果我们想等待某些东西完成异步加载,请使用协程;我们可以启动协程,而不是停止程序进程的运行。
我们可以使用 C# 作业系统和突发编译器来获得巨大的性能提升,同时在执行进程密集型任务时不必担心所有线程管理问题。使用内置系统,我们确信它以安全的方式完成,不会导致任何不必要的错误或错误。
在不使用突发编译器的情况下,任务确实比作业运行得更好,但这是由于在幕后为我们安全地设置一切而产生的额外开销。使用突发编译器时,我们的工作执行了我们的任务。当您需要可以获得的所有额外性能时,请使用 C# Job System with burst。
这个项目文件可以在我的 GitHub 上找到。
1656374400
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
, await
và Task
trong 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 MonoBehaviour
có a Transform
và 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;
}
async
, await
và Task
có trong C #async
?Trong C #, các phương thức có thể có một async
từ 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 # .
await
sử 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 await
từ khóa. Điều này được sử dụng bên trong bất kỳ phương thức nào có async
từ 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
.
Task
và bạn sử dụng nó như thế nào?A Task
là 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 Task
trả 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 .
task
ví dụ đơn giảnprivate 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");
}
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 MonoBehaviour
cá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à Update
phươ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 Task
vò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 await
cho 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.
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.
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.
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.
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 Task
trong Unity là tất cả chúng đều chạy trên Main
luồ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.
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ề IEnumerator
và chúng tôi yield return
thay 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 MonoBehaviour
và 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.
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);
}
}
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 Coroutine
enum MethodType
và 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 Update
phươ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.
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 Units
vì 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, bạn tạo một công stuct
việc triển khai một trong các IJob
giao diện ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJob
là một công việc cơ bản. IJobFor
và IJobForParallel
đượ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 IJobForParallel
sẽ đượ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 IJobFor
và IJobForParallel
để 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 IJob
không cần bất kỳ. Và cần một IJobFor
phao IJobParallelFor
cho 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 float3
cho 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 Execute
phương thức được yêu cầu. Cả IJobFor
hai IJobForParallel
đều yêu cầu một int
chỉ 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ù transform
và 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();
}
Đầ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 Schedule
thức trả về một JobHandle
có 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 update
phươ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 Update
phương thức, tôi thêm phần case
vào switch
phầ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;
}
...
}
}
Và MoveEnemy
_MoveEnemyParallelJob
Tiếp theo, tôi đã thêm các công việc vào enum của mình. Sau đó, trong Update
phương thức, tôi gọi một MoveEnemyJob
phương thức mới, vượt qua thời gian delta. Thông thường, bạn sẽ sử JobFor
dụ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í moveY
mà 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);
}
}
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 JobHandle
mà 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 NativeContainer
mà 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ý Native
mả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 MoveEnemy
việ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 transform
vị 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.
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.
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
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 .
Để 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ớ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.
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.
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.
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 .
1656373200
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 Task
em 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 MonoBehaviour
que tem a Transform
e a float
para acompanhar a posição e a velocidade para se mover no eixo y:
using UnityEngine;
public class Enemy
{
public Transform transform;
public float moveY;
}
async
, await
e Task
estão em C#async
?Em C#, os métodos podem ter uma palavra- async
chave 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 .
await
e como você usa?Em C#, você pode aguardar a conclusão de uma operação assíncrona usando a palavra- await
chave. Isso é usado dentro de qualquer método que tenha a async
palavra-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
.
Task
e 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 Task
que 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 .
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");
}
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 MonoBehaviour
que 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 Update
mé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 Task
loop de matriz por meio de todos os objetos de jogo simulados e adiciono a tarefa de alto desempenho à matriz de tarefas. Eu, então, await
para 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.
A tarefa intensiva leva cerca de 4ms para ser concluída com o jogo rodando a cerca de 200 FPS.
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.
Exibir e mover mil inimigos com uma tarefa levou cerca de 50ms com uma taxa de quadros de cerca de 30 FPS.
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 Task
no Unity é que todos eles são executados no Main
encadeamento. 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.
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 IEnumerator
e nós yield return
em vez de await
.
O Unity tem vários tipos diferentes de instruções de rendimento que podemos usar, ou seja, WaitForSeconds
, WaitForEndOfFrame
, WaitUntil
ou WaitWhile
.
Para iniciar as corrotinas, precisamos de um MonoBehaviour
e 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.
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);
}
}
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 Coroutine
ao MethodType
enum 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 Update
mé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.
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 Units
porque 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 .
Para criar um trabalho, você cria um stuct
que implementa uma das IJob
interfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJob
é um trabalho básico. IJobFor
e IJobForParallel
sã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 IJobForParallel
será dividido entre vários threads.
Eu usarei IJob
para criar um trabalho de operação intensiva IJobFor
e IJobForParallel
para 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 IJob
não precisa de nenhum. O IJobFor
e IJobParallelFor
precisa 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 float3
para 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 Execute
método necessário. O IJobFor
e IJobForParallel
ambos exigem um int
para o índice da iteração atual que o trabalho está executando.
A diferença é que ao invés de acessar o inimigo transform
e 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();
}
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 Schedule
método retorna um JobHandle
que 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 update
mé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 Update
método, eu adiciono o case
à switch
seçã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 MoveEnemy
eMoveEnemyParallelJob
Em seguida, adicionei os trabalhos ao meu enum. Então, no Update
método, chamo um novo MoveEnemyJob
método, passando o tempo delta. Normalmente você usaria o JobFor
ou 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 moveY
que 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);
}
}
Temos que esperar que o trabalho seja concluído. Podemos obter o status do JobHandle
que 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 NativeContainer
que 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 Native
matriz:
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.
O MoveEnemy
trabalho
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 transform
posições e moveY
os 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.
Exibir e mover mil inimigos com trabalho paralelo levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.
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
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 .
Para obter mais detalhes sobre o compilador de intermitência, consulte a documentação do Unity .
Um trabalho intensivo com rajada leva cerca de 3ms para ser concluído com o jogo rodando a cerca de 150 FPS.
Exibindo e movendo mil inimigos, o trabalho com burst levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.
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.
Podemos usar Task
para 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 .
1656372720
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;
}
async
, await
, and Task
are in C#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.
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
.
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.
task
exampleprivate 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");
}
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.
The intensive task takes around 4ms to complete with the game running at about 200 FPS.
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.
Displaying and moving a thousand enemies with a task took around 50ms with a frame rate of about 30 FPS.
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.
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.
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);
}
}
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.
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.
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();
}
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);
}
}
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.
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.
Displaying and moving a thousand enemies with job parallel took around 30ms with a frame rate of about 30 FPS.
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
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.
For more details on the burst compiler, see the Unity documentation.
An intensive job with burst takes around 3ms to complete with the game running at about 150 FPS.
Displaying and moving a thousand enemies, the job with burst took around 30ms with a frame rate of about 30 FPS.
Displaying and moving a thousand enemies, the job parallel with burst took around 6ms with a frame rate of about 80 to 90 FPS.
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.
1656372000
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
, await
y Task
en 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 MonoBehaviour
que tiene a Transform
y a float
para 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;
}
async
, await
y Task
son en C#async
?En C#, los métodos pueden tener una async
palabra 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# .
await
y cómo se usa?En C#, puede esperar a que se complete una operación asíncrona usando la await
palabra clave. Esto se usa dentro de cualquier método que tenga la async
palabra 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
.
Task
y cómo se usa?A Task
es un método asíncrono que realiza una única operación y no devuelve ningún valor. Para a Task
que 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 .
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");
}
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 MonoBehaviour
que 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 Update
que 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 Task
bucle 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 await
para 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.
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 Task
Unity es que todos se ejecutan en el Main
subproceso. 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.
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 IEnumerator
y we yield return
en lugar de await
.
Unity tiene varios tipos diferentes de instrucciones de rendimiento que podemos usar, es decir, WaitForSeconds
, WaitForEndOfFrame
, WaitUntil
o WaitWhile
.
Para iniciar las corrutinas, necesitamos MonoBehaviour
y 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.
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);
}
}
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é Coroutine
a la MethodType
enumeració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 Update
mé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 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 Units
porque 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 .
Para crear un trabajo, cree uno stuct
que implemente una de las IJob
interfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJob
es un trabajo básico. IJobFor
y IJobForParallel
se 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 IJobForParallel
se dividirá entre varios subprocesos.
Lo usaré IJob
para crear un trabajo de operación intensiva IJobFor
y IJobForParallel
para 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 IJob
no necesita ninguno. Los IJobFor
y IJobParallelFor
necesitan 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 float3
para 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 Execute
método requerido. Los IJobFor
y IJobForParallel
requieren un int
para 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();
}
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 Schedule
método devuelve un JobHandle
que 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 update
mé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 Update
método, agrego el case
a la switch
secció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 MoveEnemy
yMoveEnemyParallelJob
A continuación, agregué los trabajos a mi enumeración. Luego, en el Update
método, llamo a un nuevo MoveEnemyJob
método, pasando el tiempo delta. Normalmente usaría el JobFor
o 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 moveY
que 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);
}
}
Tenemos que esperar a que se complete el trabajo. Podemos obtener el estado del JobHandle
que 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 NativeContainer
que 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 Native
matriz:
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 MoveEnemy
trabajo
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 transform
posiciones y moveY
los 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.
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 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.
Podemos utilizar Task
para 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 .
1656371400
Web、モバイル、コンソール、さらには一部のローエンドPCに公開しようとする場合、パフォーマンスがすべてです。30 FPS未満で実行されているゲームまたはアプリケーションは、ユーザーにフラストレーションを引き起こす可能性があります。CPUの負荷を減らしてパフォーマンスを向上させるために使用できるもののいくつかを見てみましょう。
この投稿では、、、、およびC#とは何かasync
、await
およびTask
Unityでそれらを使用してプロジェクトのパフォーマンスを向上させる方法について説明します。次に、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がメモリ管理を処理する方法では、コルーチン、、、またはAsyncOperation
C #ジョブシステムのいずれかを使用します。
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
。
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でそれらを使用することにはいくつかの欠点があります。Task
Unityで使用する場合の主な欠点は、すべてがMain
スレッド上で実行されることです。はい、他のスレッドで実行することはできますが、Unityはすでに独自のスレッドとメモリの管理を行っており、CPUコアよりも多くのスレッドを作成することでエラーが発生する可能性があり、リソースの競合が発生します。
タスクを正しく実行してデバッグすることも難しい場合があります。元のコードを書いているとき、私はすべてのタスクを実行することになりましたが、どの敵も画面上を移動しませんでした。Task[]
で作成したものを返す必要がありましたTask
。
タスクは、パフォーマンスに影響を与える多くのガベージを作成します。また、プロファイラーには表示されないため、パフォーマンスに影響を与えているプロファイラーがある場合、追跡するのは困難です。また、タスクや更新機能が他のシーンから実行され続けることがあることに気づきました。
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#ジョブシステムは、作成が簡単で、タスクが行うガベージを生成せず、Unityが既に作成したワーカースレッドを利用するUnityのタスクの実装です。これにより、タスクのすべての欠点が修正されます。
Unityはジョブをスレッドとして比較しますが、ジョブは1つの特定のタスクを実行すると言っています。ジョブは、実行前に完了する他のジョブに依存することもあります。Units
これにより、最初に完了するのは別のタスクに依存していたため、適切に移動しなかったタスクの問題が修正されます。
仕事の依存関係はUnityによって自動的に処理されます。ジョブシステムには、主に競合状態から保護するための安全システムも組み込まれています。ジョブに関する注意点の1つは、 blittableタイプまたはNativeContainerタイプのいずれかのメンバー変数のみを含めることができることです。これは安全システムの欠点です。
ジョブシステムを使用するには、ジョブを作成し、ジョブをスケジュールし、ジョブが完了するのを待ってから、ジョブから返されたデータを使用します。UnityのData-OrientedTechnologyStack(DOTS)を使用するには、ジョブシステムが必要です。
ジョブシステムの詳細については、Unityのドキュメントを参照してください。
ジョブを作成するには、インターフェイスの1つ(IJob、IJobFor、IJobParallelFor、Unity.Engine.Jobs.IJobParallelForTransformstuct
)を実装するを作成します。基本的な仕事です。およびは、ネイティブコンテナの各要素に対して、または複数の反復に対して同じ操作を実行するために使用されます。それらの違いは、IJobForが単一のスレッドで実行され、複数のスレッドに分割されることです。IJobIJobIJobForIJobForParallelIJobForParallel
IJob
集中的な操作ジョブIJobFor
をIJobForParallel
作成し、複数の敵を移動させるジョブを作成するために使用します。これは、パフォーマンスへのさまざまな影響を確認できるようにするためです。これらのジョブは、前に作成したタスクとメソッドと同じになります。
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
メソッドを実装することです。IJobFor
とIJobForParallel
両方は、ジョブが実行している現在の反復のインデックスにを必要とします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ミリ秒かかりました。
バーストコンパイラは、バイトコードからネイティブコードに変換するコンパイラです。これを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でした。
Task
Unityアプリケーションのパフォーマンスを向上させるために使用できますが、それらを使用することにはいくつかの欠点があります。やりたいことに応じて、Unityにパッケージ化されているものを使用することをお勧めします。何かが非同期でロードを完了するのを待ちたい場合は、コルーチンを使用します。コルーチンを開始することはできますが、プログラムの実行を停止することはできません。
バーストコンパイラでC#ジョブシステムを使用すると、プロセスを多用するタスクを実行するときにスレッド管理のすべてを気にすることなく、パフォーマンスを大幅に向上させることができます。内蔵システムを使用して、不要なエラーやバグを引き起こさない安全な方法で実行されることを確信しています。
タスクはバーストコンパイラを使用せずにジョブよりも少し良く実行されましたが、それは私たちのためにすべてを安全にセットアップするための舞台裏での少し余分なオーバーヘッドによるものです。バーストコンパイラを使用する場合、ジョブはタスクを実行しました。取得できるすべての追加のパフォーマンスが必要な場合は、バースト付きのC#ジョブシステムを使用してください。
このためのプロジェクトファイルは私のGitHubにあります。
1654604580
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?
1654326531
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
1648873833
Table of Contents
LazyStream
in Flutter and DartFutureGroup
in DartIterable<bool>
in DartFuture<bool>
in FlutterString
Data in DartStream.startWith
in FlutterAnnotatedRegion
in FlutterMap
Equality in DartIterable
to ListView
in FlutterObject.toString()
in DartIterable
Subscripts in DartuseState
in Flutter HooksIterable
+/- in DartEmptyOnError
in DartStream<T>
Initial Value in FlutterDouble.normalize
in DartIterable.compactMap
in DartuseEffect
in Flutter HooksIsolate
Stream in DartListTile
Shadow in Flutter@useResult
in Dart@mustCallSuper
in DartObject.hash
in DartAsyncSnapshot
to Widget
in FlutterMap
Values in DartListView
in FlutterObject
in DartMap
in DartValueNotifier
in FlutterFuture
Error Test in FlutterFuture
Errors in DartFuture
Error Handling in DartMap<K,V>
in DartStream<List<T>>
in DartChangeNotifier
in FlutterOrientationBuilder
in FlutterCheckboxListTile
in Flutter-
Operator on String
in DartFuture<T>
List<T?>?
in DartList<T>
in DartList<List<T>>
in DartList<T>
in DartList<T>
in DartList<Uri>
in DartStream
and StreamBuilder
in FlutterStreamBuilder
and StreamController
in DartComparable
in Dartrethrow
ing Exceptions in Dartmixin
s and JSON Parsing in Dartmixin
s vs abstract class
es in DartLayoutBuilder
, CustomPaint
and CustomPainter
const
Constructors in Dartasync
-await
Over Raw Future
s in DartList<num>
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
rethrow
ing Exceptions in Dart
mixin
s and JSON Parsing in Dart
mixin
s vs abstract class
es 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
1638531900
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.
1636305780
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
1636175760
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