Merge branch 'integration' into networking-101-udp

This commit is contained in:
Corbin Crutchley
2020-03-29 10:54:57 -07:00
279 changed files with 21391 additions and 9531 deletions

View File

@@ -1,4 +1,5 @@
{ {
"parser": "@typescript-eslint/parser",
"extends": "react-app", "extends": "react-app",
"plugins": ["prettier"], "plugins": ["prettier"],
"rules": { "rules": {

42
.github/workflows/testing-deploying.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: testing-deploying
on: push
jobs:
testing-deploying:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Use Node.js 10.x
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: Run npm install, test, and build
run: |
npm ci
npm test
npm run build
env:
CI: true
- name: Deploy to beta.unicorn-utterances.com
if: github.ref == 'refs/heads/integration'
uses: AEnterprise/rsync-deploy@v1.0
env:
DEPLOY_KEY: ${{ secrets.JAILED_PRIVATE_KEY }}
ARGS: "-rlt --delete --delete-delay --delay-updates"
SERVER_PORT: 22
FOLDER: "./public/"
SERVER_IP: unicorn-utterances.com
USERNAME: ${{ secrets.JAILED_USERNAME }}
SERVER_DESTINATION: ${{ secrets.JAILED_BETA_PATH }}
- name: Deploy to unicorn-utterances.com
if: github.ref == 'refs/heads/master'
uses: AEnterprise/rsync-deploy@v1.0
env:
DEPLOY_KEY: ${{ secrets.JAILED_PRIVATE_KEY }}
ARGS: "-rlt --delete --delete-delay --delay-updates"
SERVER_PORT: 22
FOLDER: "./public/"
SERVER_IP: unicorn-utterances.com
USERNAME: ${{ secrets.JAILED_USERNAME }}
SERVER_DESTINATION: ${{ secrets.JAILED_PROD_PATH }}

3
.gitignore vendored
View File

@@ -67,6 +67,3 @@ yarn-error.log
.pnp.js .pnp.js
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
# Gatsby config consts for the server
/config/gatsby-config-consts.js

View File

@@ -1,4 +1,3 @@
{ {
"endOfLine": "lf",
"useTabs": true "useTabs": true
} }

View File

@@ -1,8 +0,0 @@
sudo: false
language: node_js
node_js:
- '10'
cache: npm
script:
- npm test
- npm run build

180
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,180 @@
As a site and community alike, we have a myriad of ways to contribute to the site and the community at large. Below is the Table of Contents for this `CONTRIBUTING.md` file, but if you get stuck or want to ask questions, feel free to [open an issue on GitHub](https://github.com/unicorn-utterances/unicorn-utterances/issues/new) or [reach out to us on our Discord](https://discord.gg/FMcvc6T).
- [Blog Posts](#Blog-Posts)
- [Adding a New Post](#Adding-a-New-Blog-Post)
- [Author Data](#Author-Data-File)
- [Markdown Post](#Markdown-Post)
- [Editing a Blog Post](#Editing-a-Blog-Post)
- [Code Contributions](#Code)
- [Deployment](#Deployment)
# Blog Posts
## Adding a New Blog Post
To credit you for your new post, we'll start by adding you to the list of authors for the site.
### Author Data File
The author data file is located at [`content/data/unicorns.json`](./content/data/unicorns.json) 🦄
> This section assumes you'd like to contribute directly through GitHub. If you'd rather contribute an article without using Git, [reach out to us on our Discord](https://discord.gg/FMcvc6T), and we'll work with you to introduce your (attributed) content.
To add yourself as an author in a PR for a new post, you'd add your information
as a new JSON object in the array.
This information includes:
- A username for your profile (used in your profile URL).
[IE, our founder's username is `crutchcorn`, and [their page can be found here](https://unicorn-utterances.com/unicorns/crutchcorn)]
- Full name
- A separate field for the first name and last name as well
(this is because SEO meta tags force us to use first names and last names)
- If you wish to go by an alias, nickname, or any other appropriate identifier, that is absolutely permitted.
- A short description of yourself
- If you need to have a line break, simply add in `\n` and the code will handle it for you
- We ask that your description is less than 256 characters long
- Your social media metadata. We currently support the following:
- Twitter
- Please leave your username without a `@` preceding it
- GitHub
- Please leave your username, not the full GitHub profile link
- A personal website
- Do leave the full URI of your website, including `http` or `https`
- We [have an issue on supporting arbitrary profile links](https://github.com/unicorn-utterances/unicorn-utterances/issues/23). If you'd like to add other social media options, open an issue or a PR and we'll take a look at it
- You may also leave these fields blank and simply provide an empty object as an alternative. Don't feel forced to leave any social media if you'd rather not
- Your preferred pronouns
- Please keep [our Code of Conduct](https://github.com/unicorn-utterances/unicorn-utterances/blob/integration/CODE_OF_CONDUCT.md) in mind. We do not accept bigotry of _**any kind**_.
- This value must match [one of the `id` fields of our `pronouns.json` file](https://github.com/unicorn-utterances/unicorn-utterances/blob/integration/content/data/pronouns.json)
- If your preferred pronouns are not present, simply add a new value inside of said JSON file
- A profile picture, used on your profile page.
- This profile picture should be a path to a PNG or a JPEG file with the resolution of at least `512x512` that's saved inside of `./content/data`
- Don't want to show your real picture on the site? That's alright! We have a [myriad of custom unicorn emotes that can be used as profile pictures as well](https://github.com/unicorn-utterances/design-assets/tree/master/emotes). They're adorable, go check! 🤩
> While this feels like a lot of metadata, keep in mind that all of this is optional or can provide defaults that don't identify you. Please never feel pressured to provide more information than you're comfortable with!
### Markdown Post
Now that we have your user attribution data, we can move onto the post data itself. We store all blog posts under a format called ["Markdown."](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) This format is a plaintext file with some special syntax for formatting improvements. I recommend using [an app like Typora](http://typora.io/), which allows you to edit markdown files similarly to a Word document. If you're coming from Google Drive, [there's an open-source script](https://github.com/lmmx/gdocs2md-html) that may help you convert your documents to Markdown.
#### Save Location
Once you have your `.md` file, we'll need a place to put it. We place a subdirectory in our [`content/blog` folder](https://github.com/unicorn-utterances/unicorn-utterances/tree/integration/content/blog) for each of the blog posts on the site. The naming of these subdirectories is integral to keep in mind, as they reflect the URL path of the article once finished. For example, the folder [`what-is-ssr-and-ssg`](./content/blog/what-is-ssr-and-ssg) will turn into the URL for the article:
[https://unicorn-utterances.com/posts/what-is-ssr-and-ssg/](https://unicorn-utterances.com/posts/what-is-ssr-and-ssg/)
Once you've created a subfolder with the URI you'd like your article to have, move the `.md` file into the folder with the name `index.md`. If you have linked images or videos, you'll need to save those files in the same folder and change your markdown file to reference them locally:
An example of referencing a video locally is:
```markdown
`video: title: "A comparison of how text spacing is applied on iOS and Android": ./ios_vs_android.mp4`
```
Where you include the title of the video and the video.
##### File Naming
We expect images and videos to be fully lowercase with underscores ([often called `snake_case`](https://en.wikipedia.org/wiki/Snake_case)) for various build reasons. We've often had build issues when files contain uppercase or dashes in the file name.
##### Static File Linking
There may be instances where you want an image or arbitrary file to be linked within your article. Say you've made a PowerPoint presentation that you'd like to link inside of the article. To do this, create a subfolder with the same name as the URI of the blog post user [`static/posts`](./static/posts). You're then able to reference your files inside of the markdown file:
```markdown
[styles.xml](./styles.xml)
```
#### Frontmatter
Now that we've placed the file in the correct location, we need to add metadata about the blog post itself. We do this inside of the `index.md` file itself using what's called a "Frontmatter." An example frontmatter looks something like this:
```markdown
---
{
title: 'Hard grids & baselines: How I achieved 1:1 fidelity on Android',
description: 'Testing the limits of `firstBaselineToTopHeight` and `lastBaselineToBottomHeight` to deliver a perfect result.',
published: '2019-10-07T22:07:09.945Z',
edited: '2020-02-02T22:07:09.945Z',
authors: ['edpratti'],
tags: ['android', 'design', 'figma'],
attached: [],
license: 'cc-by-nc-nd-4'
}
---
```
The following data **must** be present:
- Title for the article
- We ask that your titles are less than 80 characters.
- A description of the article
- We ask that your descriptions are less than 190 characters.
- A published date
- Please follow the format as seen above
- An array of authors
- This array must have every value match [one of the `id`s of the `unicorns.json` file](./content/data/unicorns.json)
- An array of related tags
- Please try to use existing tags if possible. If you don't find any, that's alright
- We ask that you keep it to 4 tags maximum
- A `license` to be associated with the post
- This must match the `id` field for one of the values [in our `license.json` file](./content/data/licenses.json)
- If you're not familiar with what these licenses mean, view the `explainLink` for each of them in the `license.json` file. It'll help you understand what permissions the public has to the post
- For example, can they modify the article and re-release it or not?
## Editing a Blog Post
Our blog posts can all be found under [`/content/blog`](./content/blog). Simply find the article based on the URL path and edit the `index.md` file. We'll have one of our editors review the post changes, and we'll try to reach out to the author for them to review your PR as well.
# Code
While we have a lot of code that is not yet this way, we try our best to build our code for the site in such a way that it's generic enough to be useful for others. For example, some of our UI components have led to the creation of [our sister NPM library `batteries-not-included`](https://github.com/unicorn-utterances/batteries-not-included). We now directly consume said library for our own components. We've also found ourselves requiring a custom markdown processing utility [in the form of `unist-util-flat-filter`](https://github.com/unicorn-utterances/unist-util-flat-filter).
Keep in mind that we request developers reach out [via our Discord](https://discord.gg/FMcvc6T) or [via GitHub issue](https://github.com/unicorn-utterances/unicorn-utterances/issues/new) before extensive development is pursued. If you have a feature you'd like to add to the site, let us know! We'd love to do some brainstorming before coding begins!
## Develop Mode
To start the development server, run `npm run develop`, it will then start the local instance at `http://localhost:8000`. You also can check out the GraphiQL tool at `http://localhost:8000/___graphql`. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql).
## Debugging Plugins
We have a few local plugins for Gatsby. However, I've found that while debugging these plugins, Gatsby's cache can often get in the way. If you run `npm run debug`, it will create a debuggable ([using Chrome](https://unicorn-utterances.com/posts/debugging-nodejs-programs-using-chrome/)) instance of the Gatsby develop mode and clear the cache of Gatsby. This script will ensure that your debugger provides proper insight into what's happening in the build process.
# Deployment
## Git Strategy
We loosely follow [the Gitflow branching strategy](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)
where our development branch is called `integration`, and our mainline branch is called `master`.
This means that any new pull requests should be made against `integration`
unless it is an emergency hotfix (to be approved by the DevOps team).
We also have the `master` branch, which is a live reflection of the code
hosted on the server. Any time `integration` is pulled into `master`, the
site will be deployed. A PR from `integration` to `master` should only be
opened by a Unicorn Utterances team member and must be approved by at
least one DevOps member
## CI/CD
As mentioned before, we follow the GitFlow branching strategy. What we didn't say before is how we utilize those branches to deploy our code. Upon a PR being merged into `integration`, we have [a GitHub Action to deploy](https://github.com/unicorn-utterances/unicorn-utterances/actions?query=workflow%3Atesting-deploying) those changes to our beta link:
[https://beta.unicorn-utterances.com](https://beta.unicorn-utterances.com)
Likewise, we make periodic PRs to the `master` branch. We call these PR's `Integration merge MM/DD/YY`. These PRs **must** be approved by one or more of the members of the DevOps team before being able to be merged. Upon merging, there will be an automated deployment ([using the same GitHub action](https://github.com/unicorn-utterances/unicorn-utterances/actions?query=workflow%3Atesting-deploying)) to our homepage:
[https://unicorn-utterances.com](https://unicorn-utterances.com)

View File

@@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img alt="Unicorn Utterances logo" src="./content/assets/unicorn-utterances-logo-512.png"/> <img alt="Unicorn Utterances logo" width="256" src="./src/assets/unicorn_utterances_logo_512.png"/>
</p> </p>
<h1 align="center"> <h1 align="center">
Unicorn Utterances Website Unicorn Utterances Website
@@ -7,19 +7,15 @@
<div align="center"> <div align="center">
[![Join chat on Discord](https://badgen.net/badge/discord/join%20chat/7289DA?icon=discord)](https://discord.gg/FMcvc6T) [![Join chat on Discord](https://badgen.net/badge/discord/join%20chat/7289DA?icon=discord)](https://discord.gg/FMcvc6T)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) [![Contributor Covenant v1.4 adopted](https://badgen.net/badge/Contributor%20Covenant/v1.4%20adopted/ff69b4)](CODE_OF_CONDUCT.md)
[![Check Status](https://badgen.net/github/checks/unicorn-utterances/unicorn-utterances/?icon=github)](https://github.com/unicorn-utterances/unicorn-utterances/actions)
Master branch status: [![Master Branch Build Status](https://travis-ci.org/unicorn-utterances/unicorn-utterances.svg?branch=master)](https://travis-ci.org/unicorn-utterances/unicorn-utterances)
Integration branch status: [![Integration Branch Build Status](https://travis-ci.org/unicorn-utterances/unicorn-utterances.svg?branch=integration)](https://travis-ci.org/unicorn-utterances/unicorn-utterances)
</div> </div>
This repository acts as the source code location for [the Unicorn Utterances blog](https://unicorn-utterances.com) This repository acts as the source code location for [the Unicorn Utterances blog](https://unicorn-utterances.com).
## Sponsors ## Sponsors
<a href="https://www.thepolyglotdeveloper.com/" target="_blank" rel="noopener noreferrer sponsored"><img alt="The Polyglot Developer" src="https://unicorn-utterances.com/sponsors/the-polyglot-developer.svg" width="300"/></a> <a href="https://www.thepolyglotdeveloper.com/" target="_blank" rel="noopener noreferrer sponsored"><img alt="The Polyglot Developer" src="https://unicorn-utterances.com/sponsors/the-polyglot-developer.svg" width="300"/></a>
## Statement of Ethics ## Statement of Ethics
@@ -35,45 +31,11 @@ through the project. Not every sponsorship contains a financial contribution,
but if one does we will disclose both what those finances but if one does we will disclose both what those finances
are going towards and what will be done in exchange. are going towards and what will be done in exchange.
## Important Repo Files ## Contributing
### Blog Posts We highly encourage and celebrate others contributing to our site and our community! We've written [a comprehensive guide on how to do so here](./CONTRIBUTING.md). This guide includes instructions for how to add a new post to the site, how to edit our code, and what our deployment strategy is.
Should be located under [`content/blog/post-name-here`](./content/blog/). Keep in mind that we request developers reach out [via our Discord](https://discord.gg/FMcvc6T) or [via GitHub issue](https://github.com/unicorn-utterances/unicorn-utterances/issues/new) before extensive development is pursued. If you have a feature you'd like to add to the site, let us know! We'd love to do some brainstorming before coding begins!
You should then have an `index.md` file containing a frontmatter (with JS
header, not YAML) portion and any related files should be in the same folder.
### Author Data File We extend this invitation to those who may be unfamiliar with our processes. Be sure to check out [our CONTRIBUTING.md](./CONTRIBUTING.md) file first, but don't be afraid to join in and ask questions if you're uncertain of anything
The author data file is located at [`src/data/unicorns.json`](./src/data/unicorns.json) 🦄
To add yourself as an author in a PR for a new post, you'd add your information
as a new JSON object in the array, then add a profile picture to the `data`
folder. The `pronouns` field should match an `id` in the `pronouns.json` (if
yours is not listed, please add it as a new value in that file, we've tried to
do our best to include everything we've found!)
> If you do not want to show a profile picture or commit your picture to
the repo, we have a [myriad of emotes that can be used as profile pictures as well](./content/assets/branding/emotes).
They're adorable, go check! 🤩
## 🚀 Develop
To start the develop server, run `npm run develop`, it will then start
the local instance at `http://localhost:8000`. You also have the ability to
checkout the GraphiQL tool at `http://localhost:8000/___graphql`. This is a
tool you can use to experiment with querying your data. Learn more about
using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql).
## Git Strategy
We loosely follow [the Gitflow branching strategy](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)
where our development branch is called `integration` and our mainline branch is called `master`.
This means that any new pull requests should be made against `integration`
unless it is an emergency hotfix (to be approved by the devops team).
We also have the `master` branch which is a live reflection of the code
hosted on the server. Any time `integration` is pulled into `master`, the
site will be deployed. A PR from `integration` to `master` should only be
opened by a Unicorn Utterances team member and must be approved by at
least one devops member

View File

@@ -15,7 +15,9 @@ export const MockPost = {
license: MockLicense license: MockLicense
}, },
fields: { fields: {
slug: "/this-post-name-here" slug: "/this-post-name-here",
inlineCount: 0,
headingsWithId: []
}, },
wordCount: { wordCount: {
words: 10000 words: 10000
@@ -37,7 +39,9 @@ export const MockMultiAuthorPost = {
license: MockLicense license: MockLicense
}, },
fields: { fields: {
slug: "/this-other-post-name-here" slug: "/this-other-post-name-here",
inlineCount: 0,
headingsWithId: []
}, },
wordCount: { wordCount: {
words: 100000 words: 100000

View File

@@ -1,14 +1,17 @@
import { MockRole } from "./mock-role"; import { MockRole } from "./mock-role";
import { UnicornInfo } from "../../src/types";
export const MockUnicorn = { export const MockUnicorn: UnicornInfo = {
name: "Joe", name: "Joe",
firstName: "Joe",
lastName: "Other",
id: "joe", id: "joe",
description: "Exists", description: "Exists",
color: "red", color: "red",
fields: { fields: {
isAuthor: true isAuthor: true
}, },
roles: [MockRole], roles: [MockRole as any],
socials: { socials: {
twitter: "twtrusrname", twitter: "twtrusrname",
github: "ghusrname", github: "ghusrname",

View File

@@ -1,12 +0,0 @@
import React from "react"
jest.mock('gatsby-image', () => {
return (props) => {
return <img
src={props.fixed}
alt={props.alt}
data-testid={props['data-testid']}
className={props.className}
/>;
}
});

View File

@@ -0,0 +1,14 @@
import React from "react";
jest.mock("gatsby-image", () => {
return (props: any) => {
return (
<img
src={props.fixed}
alt={props.alt}
data-testid={props["data-testid"]}
className={props.className}
/>
);
};
});

View File

@@ -1,15 +0,0 @@
import React from 'react'
import {onLinkClick} from 'gatsby-plugin-google-analytics';
afterEach(() => {
onLinkClick.mockReset();
})
jest.mock('gatsby-plugin-google-analytics', () => {
const onLinkClick = jest.fn();
return {
OutboundLink: (props) => <div onClick={onLinkClick}>{props.children}</div>,
onLinkClick
}
});

View File

@@ -0,0 +1,17 @@
import React from "react";
import { onLinkClick } from "gatsby-plugin-google-analytics";
afterEach(() => {
onLinkClick.mockReset();
});
jest.mock("gatsby-plugin-google-analytics", () => {
const onLinkClick = jest.fn();
return {
OutboundLink: (props: any) => (
<div onClick={onLinkClick}>{props.children}</div>
),
onLinkClick
};
});

View File

@@ -1,43 +0,0 @@
const React = require("react")
import {onLinkClick} from 'gatsby';
afterEach(() => {
onLinkClick.mockReset();
})
jest.mock('gatsby', () => {
const react = require('react');
const gatsbyOGl = jest.requireActual('gatsby');
const onLinkClick = jest.fn();
return {
...gatsbyOGl,
Link: react.forwardRef((props, ref) => {
const {
// these props are invalid for an `a` tag
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
replace,
to,
...rest
} = props;
return <a
{...rest}
onClick={onLinkClick}
style={props.style}
className={props.className}
ref={ref}
href={to}
>
{props.children}
</a>
}),
onLinkClick,
graphql: jest.fn(),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn(),
}
})

View File

@@ -0,0 +1,45 @@
import { onLinkClick } from "gatsby";
const React = require("react");
afterEach(() => {
onLinkClick.mockReset();
});
jest.mock("gatsby", () => {
const react = require("react");
const gatsbyOGl = jest.requireActual("gatsby");
const onLinkClick = jest.fn();
return {
...gatsbyOGl,
Link: react.forwardRef((props: any, ref: any) => {
const {
// these props are invalid for an `a` tag
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
replace,
to,
...rest
} = props;
return (
<a
{...rest}
onClick={onLinkClick}
style={props.style}
className={props.className}
ref={ref}
href={to}
>
{props.children}
</a>
);
}),
onLinkClick,
graphql: jest.fn(),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn()
};
});

View File

@@ -1,4 +0,0 @@
import './gatsby';
import './gatsby-image';
import './disqus-react';
import './gatsby-plugin-google-analytics'

View File

@@ -0,0 +1,4 @@
import "./gatsby";
import "./gatsby-image";
import "./disqus-react";
import "./gatsby-plugin-google-analytics";

View File

@@ -1 +0,0 @@
module.exports = () => null;

View File

@@ -0,0 +1 @@
export default () => null;

View File

@@ -1,3 +1,3 @@
module.exports = { module.exports = {
googleAnalytics: '' googleAnalytics: "UA-143062623-1"
} };

230
config/gatsby/gatsbyNode.ts Normal file
View File

@@ -0,0 +1,230 @@
import { GatsbyNode, SourceNodesArgs } from "gatsby";
import { PostInfo, UnicornInfo } from "../../src/types";
const path = require(`path`);
const fs = require("fs");
const { createFilePath } = require(`gatsby-source-filesystem`);
export const onCreateNode: GatsbyNode["onCreateNode"] = ({
node,
actions,
getNode
}) => {
const { createNodeField } = actions;
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({
node,
getNode
});
createNodeField({
name: `slug`,
node,
value
});
}
if (node.internal.type === `UnicornsJson`) {
const value = createFilePath({
node,
getNode
});
createNodeField({
name: `slug`,
node,
value
});
}
};
export const sourceNodes: GatsbyNode["sourceNodes"] = async ({
getNodesByType,
actions: { createNodeField }
}: SourceNodesArgs) => {
const postNodes: PostInfo[] = getNodesByType(`MarkdownRemark`);
const unicornNodes: UnicornInfo[] = getNodesByType(`UnicornsJson`);
unicornNodes.forEach(unicornNode => {
const isAuthor = postNodes
// Ensure it's actually a post
.filter(post => !!post.frontmatter.authors)
.some(post => {
return ((post.frontmatter.authors as unknown) as string[]).includes(
unicornNode.id
);
});
createNodeField({
name: `isAuthor`,
node: unicornNode as any,
value: isAuthor
});
});
};
export const createPages: GatsbyNode["createPages"] = ({
graphql,
actions
}) => {
const { createPage } = actions;
const blogPost = path.resolve(`./src/templates/blog-post.tsx`);
const blogProfile = path.resolve(`./src/templates/blog-profile.tsx`);
const postList = path.resolve(`./src/templates/post-list.tsx`);
return graphql(
`
{
allMarkdownRemark(
sort: { fields: [frontmatter___published], order: DESC }
filter: { fileAbsolutePath: { regex: "/content/blog/" } }
limit: 1000
) {
edges {
node {
fields {
slug
}
frontmatter {
title
authors {
id
}
}
}
}
}
allUnicornsJson(limit: 100) {
edges {
node {
id
}
}
}
}
`
).then(result => {
if (result.errors) {
throw result.errors;
}
// Create blog posts pages.
const posts: {
node: PostInfo & { frontmatter: { attached: { file: string }[] } };
}[] = (result.data as any).allMarkdownRemark.edges;
const unicorns: { node: UnicornInfo }[] = (result.data as any)
.allUnicornsJson.edges;
posts.forEach((post, index, arr) => {
const previous = index === arr.length - 1 ? null : arr[index + 1].node;
const next = index === 0 ? null : arr[index - 1].node;
const postInfo = post.node.frontmatter;
if (postInfo.attached && postInfo.attached.length > 0) {
postInfo.attached.forEach(({ file: fileStr }) => {
const postPath = post.node.fields.slug;
const relFilePath = path.join(
__dirname,
"static",
"posts",
postPath,
fileStr
);
const fileExists = fs.existsSync(path.resolve(relFilePath));
if (!fileExists) {
console.error(
`Could not find file to attach in the static folder: ${postPath}${fileStr}`
);
console.error(
`To fix this problem, attach the file to the static folder's expected path above, or remove it from the post frontmatter definition`
);
process.exit(1);
}
});
}
createPage({
path: `posts${post.node.fields.slug}`,
component: blogPost,
context: {
slug: post.node.fields.slug,
previous,
next
}
});
});
const postsPerPage = 8;
const numberOfPages = Math.ceil(posts.length / postsPerPage);
createPage({
path: `/`,
component: postList,
context: {
limitNumber: postsPerPage,
skipNumber: 0,
pageIndex: 1,
numberOfPages,
relativePath: ""
}
});
for (const i of Array(numberOfPages).keys()) {
if (i === 0) continue;
const pageNum = i + 1;
const skipNumber = postsPerPage * i;
createPage({
path: `page/${pageNum}`,
component: postList,
context: {
limitNumber: postsPerPage,
skipNumber,
pageIndex: pageNum,
numberOfPages,
relativePath: ""
}
});
}
unicorns.forEach(unicorn => {
const uniId = unicorn.node.id;
const uniPosts = posts.filter(({ node: { frontmatter } }) =>
frontmatter.authors.find(uni => uni.id === uniId)
);
const numberOfUniPages = Math.ceil(uniPosts.length / postsPerPage);
createPage({
path: `unicorns/${unicorn.node.id}`,
component: blogProfile,
context: {
slug: uniId,
limitNumber: postsPerPage,
skipNumber: 0,
pageIndex: 1,
numberOfPages: numberOfUniPages,
relativePath: `unicorns/${uniId}`
}
});
for (const i of Array(numberOfUniPages).keys()) {
if (i === 0) continue;
const pageNum = i + 1;
const skipNumber = postsPerPage * i;
createPage({
path: `unicorns/${uniId}/page/${pageNum}`,
component: blogProfile,
context: {
slug: uniId,
limitNumber: postsPerPage,
skipNumber,
pageIndex: pageNum,
numberOfPages: numberOfUniPages,
relativePath: `unicorns/${uniId}`
}
});
}
});
return null;
});
};

View File

@@ -1,5 +0,0 @@
const babelOptions = {
presets: ["babel-preset-gatsby"],
}
module.exports = require("babel-jest").createTransformer(babelOptions)

View File

@@ -1,3 +0,0 @@
global.___loader = {
enqueue: jest.fn(),
}

View File

@@ -1,4 +0,0 @@
import React from "react"
import "@testing-library/jest-dom/extend-expect"
import 'jest-axe/extend-expect';
import '../../__mocks__/modules';

View File

@@ -0,0 +1,26 @@
import React from "react";
import "@testing-library/jest-dom/extend-expect";
import "jest-axe/extend-expect";
import "../../__mocks__/modules";
const originConsoleErr = console.error;
console.error = (message?: any, ...optionalParams: any[]) => {
if (/Not implemented: navigation/.exec(message)) return;
originConsoleErr(message, ...optionalParams);
};
(global as any).IntersectionObserver = class IntersectionObserver {
constructor() {}
observe() {
return null;
}
disconnect() {
return null;
}
unobserve() {
return null;
}
};

View File

@@ -1,403 +0,0 @@
Attribution-NonCommercial-NoDerivatives 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial-NoDerivatives 4.0
International Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-NoDerivatives 4.0 International Public
License ("Public License"). To the extent this Public License may be
interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the
Licensor receives from making the Licensed Material available under
these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
c. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
d. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
e. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
f. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
g. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
h. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
i. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce and reproduce, but not Share, Adapted Material
for NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material, You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
For the avoidance of doubt, You do not have permission under
this Public License to Share Adapted Material.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only and provided You do not Share Adapted Material;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@@ -37,7 +37,7 @@ It's going to be a long article, so please feel free to take breaks, grab a drin
Sound like a fun time? Let's goooo! 🏃🌈 Sound like a fun time? Let's goooo! 🏃🌈
> The contents of this post was also presented in a talk under the same name. You can [find the slides here](./slides.pptx). > The contents of this post was also presented in a talk under the same name. You can [find the slides here](./slides.pptx) or a live recording of that talk given by the post's author [on our YouTube channel](https://www.youtube.com/watch?v=7AilTMFPxqQ).
# Introduction To Templates {#intro} # Introduction To Templates {#intro}
@@ -436,8 +436,6 @@ _The browser takes the items that've been defined in HTML and turns them into a
![A chart showing the document object model layout of the above code. It shows that the 'main' tag is the parent to a 'ul' tag, and so on](./dom_tree.svg "Diagram showing the above code as a graph") ![A chart showing the document object model layout of the above code. It shows that the 'main' tag is the parent to a 'ul' tag, and so on](./dom_tree.svg "Diagram showing the above code as a graph")
This tree tells the browser where to place items and includes some logic when combined with CSS, even. For example, when the following CSS is applied to the `index.html` file: This tree tells the browser where to place items and includes some logic when combined with CSS, even. For example, when the following CSS is applied to the `index.html` file:
```css ```css
@@ -452,6 +450,7 @@ It finds the element with the ID of `b`, then the children of that tag are color
> The `ul` element is marked as green just to showcase that it is the element being marked by the first part of the selector > The `ul` element is marked as green just to showcase that it is the element being marked by the first part of the selector
> If you want to have a better grasp on the DOM and how it relates to the content you see on-screen, [check out our article that outlines what the DOM is and how your code interfaces with it through the browser](/posts/understanding-the-dom/).
## View Hierarchy Tree ## View Hierarchy Tree
@@ -1618,8 +1617,8 @@ Of course, this means that you can send any value as the context. Change the cod
```typescript ```typescript
{ {
$implicit: pigLatinVal, $implicit: pigLatinVal,
original: this.makePiglatin, original: this.makePiglatin,
makePiglatinCasing: 'See? Any value' makePiglatinCasing: 'See? Any value'
} }
``` ```

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,133 @@
---
{
title: "Networking 101: A Basic Overview of Packets and OSI",
description: 'You use networking every day - even to read this text! Join us as we dive into explaining how we send data across a network and what the OSI model is.',
published: '2020-03-11T13:45:00.284Z',
authors: ['reikaze', 'crutchcorn'],
tags: ['networking', 'osi model'],
attached: [],
license: 'cc-by-nc-sa-4'
}
---
Networking is the foundation that all interactions on the internet are built upon. Every chat you send, every website you visit, _every interaction that is digitally shared with another person is built upon the same fundamentals_. These fundamentals layout and explain how computers are able to communicate with one another and how they interlink with one another in order to provide you with the experience you've come to know and love with the internet.
_This article is the beginning of a series that will outline the core components that construct those very same fundamentals_. However, without a holistic view, it may be difficult to follow along with many of the more minute details. As such, we'll be covering what each of the core components at play is and how they fit into the grand scheme of things. The articles to follow will explain and expand upon this base of knowledge.
It's important to note that _"networking" is a broad, catch-all term that infers **any** communication between one-or-more computer devices_. This includes parts in your computer communicating between themselves. For example, do you need your computer keyboard input to be processed by your CPU to display data on-screen? That requires the networking to transfer the data to the CPU and from your CPU to your monitor, so on and so forth. As a result, this article will be a bit broad in order to cover not just cross-device hardware, but inter-device communication fundamentals as well. We'll dive deeper into cross-device communication in future articles in the series
> If you don't understand how CPUs work, that's okay, it's not required knowledge for this post. Just know that they take in binary data, process it, then send data out to other devices to use. They convert binary data into binary instructions for other devices to follow
>
> That said, you need the right binary data to be input into the CPU for it to process, just like our brains need the right input to find the answer of what to do. Because of this, communication with the CPU is integral
# Architecture {#network-architectures}
There are a lot of ways that information can be connected and transferred. We use various types of architecture to connect them.
_Computers speak in `1`s and `0`s, known as binary_. These binary values come in incredibly long strings of combinations of one of the two symbols to _construct all of the data used in communication_.
> [We covered how these two numbers can be combined to turn into other data in another post](/posts/non-decimal-numbers-in-tech/). For a better understanding of how binary represents data, check out that post.
This is true regardless of the architecture used to send data - its all binary under-the-hood somewhere in the process. The architecture used to send data is simply a way of organizing the ones and zeros effectively to enable the types of communication required for a specific use-case.
## Bus Architecture {#bus-architecture}
For example, one of the ways that we can send and receive data is by, well, sending them. _The bus architecture_, often used in low-level hardware such as CPU inter-communication, _simply streams the ones and zeros directly_.
It doesn't wait for a full message to be sent or provide many guidelines for how to send the data, it just tells them to "come on over".
`video: title: "A series of cars driving across 3 lines at various speeds. This is meant to represent the data flow of binary data on a bus architecture": ./bus_animation.mp4`
In this example, the bus icons are similar to binary data - either a one or a zero. They're able to _move as quickly as possible down a "lane" that is allocated for a specific stream of data to come through_. A collection of "lanes" is called a "bus" (which is where the name of the architecture comes from. I was just being silly by representing the binary data as buses in the video above). Your system, right now, is streaming through _**many**_ thousands of these busses to communicate between your CPU and I/O devices (like your keyboard or speakers) and tons of other things. They're _typically divided to send specific data through specific lanes (or busses)_, but outside of that, there's little high-level organization or concepts to think through.
Furthermore, because error-handled bi-directional cancelable subscriptions (like the ones you make to servers to connect to the internet) are difficult using the bus architecture, _we typically don't use it for large-scale multi-device networks like the internet_.
## Packet Architecture {#packet-architecture}
The weaknesses of the bus architecture led to the creation of the packet architecture. The packet architecture requires a bit more of a higher-level understanding of how data is sent and received. To explain this concept, we'll use an analogy that fits really well.
Let's say you want to send a note to your friend that's hours away from you. You don't have the internet so you decide to send a letter. In a typical correspondence, you'd send off a letter, include a return address, and wait for a response back. That said, _there's nothing stopping someone from sending more than a single letter before receiving a response_. This chart is a good example of that:
![An image showcasing the rules of data sending both ways](./image_of_unidirectional_data_being_sent.svg)
Similarly, a packet is _sent from a single sender, received by a single recipient, addressed where to go, and contains a set of information_.
### Metadata {#packet-metadata}
Letters may not give you the same kind of continuous stream of consciousness as in-person communications, but they do provide something in return: structure.
The way you might structure your thoughts when speaking is significantly different from how you might organize your thoughts on paper. For example, in this article, there is a clear beginning, end, and structured headings to each of the items in this article. Such verbose metadata (such as overall length) cannot be communicated via in-person talking. _The way you may structure data in a packet may also differ from how you might communicate data via a bus_.
That said, simply because there's a defined start and an end does not mean that you cannot _send large sequences of data through multiple packets and stitch them together_. Neither is true for the written word. This article does not contain the full set of information the series we hope to share, but rather provides a baseline and structure for how the rest of the information is to be consumed. So too can packets provide addendums to other packets, if you so wish.
#### Headers
Not only does the spoken-word lack the same form of structure that can be provided by the written word, but _you're also able to categorize and assign metadata to a letter_ that you wouldn't be able to do with a conversation. You do this every time you send a letter to someone through the mail: You include their name, address, and return address on the envelope that's used to send the letter. The same is done with packets.
While the "body" of your packet would contain the data you want the other party to receive, the "header" of the packet might contain data **about** said data. Such metadata might include the size of the contained data or the format that data is in.
![A breakdown of a packet showing a combination of a header with metadata and a body with data for the client](./breakdown_of_a_packet.svg)
As a result, you might have a middleware packet handler that reads only the header of the packet in order to decide where to send the packet in question - much like the mail service you use will read the outside of the envelope to see where to send your letter
`video: title: "An example of a small packet being sent to a small file server and a larger packet being sent to the large file server based on the data in the packet header": ./header_routing.mp4`
# [It Takes A Village](https://en.wikipedia.org/wiki/It_takes_a_village) To Send A Letter {#osi-layers}
Understanding what a letter is likely the most important part of the communication aspect if you intend to write letters, but if someone asked you to deliver a letter it helps to have a broader understanding of how the letter gets sent. That's right: _there's a whole structure set in place to send the letters (packets) you want to be sent_. This structure is comprised of many levels, which we'll outline here.
> For each of these levels, there are many intricacies that we won't be touching on. This is for the sake of conciseness. As this article is only the start of the series, you can expect these intricacies to be explained with greater detail as these articles are released
This structure is comprised of seven levels for most networking applications. _These layers interact with one another as a form of stack-the-blocks method_. For example, describing layer 4 encapsulates the behavior of layers 3 and 2. As a result, lower-level applications of networking may have fewer than seven layers. That said, those seven levels are, in order:
- Application
- Presentation
- Session
- Transport
- Network
- Data Link
- Physical
As you communicate with others online, and as computers communicate within themselves using packets, they go down those layers, then back up them to be processed and interpreted
![A diagram of the aforementioned layers with lines communicating data flow down then up that list](./osi_layer.svg)
This breakdown of layers is referred to as the [OSI model](https://en.wikipedia.org/wiki/OSI_model). This conceptual model allows us to think abstractly about the different components that make up our network communications. While we won't do a deep-dive into each layer here, we'll try to at least make them fit into our mailroom analogy.
Let's start from the bottom and make our way up. Remember that each of these layers builds on top of each other, allowing you to make more complex but efficient processes to send data on each step.
## Physical {#osi-layer-1-physical}
The physical layer is similar to the trucks, roads, and workers that are driving to send the data. Sure, you could send a letter just by handing letters one-by-one from driver to driver, but without some organization that's usually dispatched to higher levels, things can go wrong (as they often do [in a game of telephone](https://en.wikipedia.org/wiki/Chinese_whispers)).
In the technical world, _this layer refers to the binary bits themselves_ ([which compose to makeup letters and the rest of structure to your data](/posts/non-decimal-numbers-in-tech/)) _and the physical wiring_ constructed to transfer those bits. As it is with the mail world, this layer alone _can_ be used alone, but often needs delegation from higher layers to be more effective.
## Data Link {#osi-layer-2-data-link}
Data link would be like UPS or FedEx offices: sending information between post office to post office. These offices don't have mail sorters yet (that's a layer up) but they do provide a means for drivers to arrive to exchange mail at a designated area. As a result, instead of having to meet the drivers in the road to receive my mail, I can simply go to a designated office to receive my mail.
Likewise, _the data link layer is the layer that transfers binary data between different locations_. This becomes especially helpful when _dealing with local networks that only exchange data between a single physical location_, where you might not need the added complexity large-level packet sorting might come into play.
## Network {#osi-layer-3-network}
The network layer is similar to the mail sorters. Between being transferred from place to place, there may be instances where the mail is needed to be sorted and organized. This is _done with packets in the network layer to handle routing_ and other related activities between clients
## Transport {#osi-layer-4-transport}
The transport layer delivers it from the post office to my apartment building. This means that not only does the package gets delivered from post-office building to post-office building, but it gets to-and-from its destination as intended.
## Session {#osi-layer-5-session}
With newer packages delivered through services like UPS, you may want a tracking number for your package. This is similar to the session layer. With this layer, it includes a back-and-forth that can give you insight into the progress of the delivery or even include information like return-to-sender.
## Presentation {#osi-layer-6-presentation}
But when a package gets received by you, it doesn't stop there, does it? You want to bring the package inside your home. For most packages, this is relatively trivial - you simply take it inside. However, for some specialized instances, this may require hiring movers to get a couch in your house. In this same way, HTTP and other protocols don't typically differentiate between the presentation layer and the application layer, but some networks do. When they do, they use the presentation layer to outline how the data is formed for sending and receiving
## Application {#osi-layer-7-application}
You've just been delivered the fancy new blender you ordered for smoothies. After unwrapping the package, you plug it in and give it a whirl, making the most delicious lunch-time smoothie you've ever had. Congrats, you've just exemplified the application layer. In this layer, it encapsulates the layer your user (developer or end-user alike) will use, the application that communicates back-and-forth and the reason you wanted to send data in the first place.
# Conclusion
We've done an initial overview of what layers you utilize when accessing a network. Although we've only done an initial glance at those layers, the next few steps will outline what those layers comprise of, and how data can transfer across a network. These steps will come in the order of various articles in the series in the future. To make sure you don't miss those next articles, be sure to subscribe to our newsletter down below!
You can also [join us in our community Discord](https://discord.gg/FMcvc6T) and chat with us there!

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

View File

@@ -0,0 +1,309 @@
---
{
title: "Debugging NodeJS Applications Using Chrome",
description: 'Learn how to interactively debug your NodeJS applications using a GUI-based debugger built into Chrome.',
published: '2020-01-21T05:12:03.284Z',
authors: ['crutchcorn'],
tags: ['nodejs', 'debugging', 'chrome'],
attached: [],
license: 'cc-by-nc-sa-4'
}
---
Debugging is one of the most difficult aspects of development. Regardless of skill level, experience, or general knowledge, every developer finds themselves in an instance where they need to drop down and start walking through the process. Many, especially those who are in complex environments or just starting on their developmental path, may utilize `console.log`s to help debug JavaScript applications. However, there is a tool for developers using Node.JS that makes debugging significantly easier in many instances.
The tool I'm referring to is [the Node Debugger utility](https://nodejs.org/api/debugger.html). While this utility is powerful and helpful all on its own, _it can be made even more powerful by utilizing the power of the Chrome debugger_ to attach to a Node debuggable process in order to _provide you a GUI for a debugging mode_ in your Node.JS applications.
Let's look at how we can do so and how to use the Chrome debugger for such purposes.
# Example Application {#example-code}
Let's assume we're building an [Express server](https://expressjs.com/) in NodeJS. We want to `GET` an external endpoint and process that data, but we're having issues with the output data. Since it's not clear where the issue resides, we decide to turn to the debugger.
Let's use the following code as our example Express app:
```javascript
// app.js
const express = require("express");
const app = express();
const request = require("request");
app.get('/', (req, res) => {
request("http://www.mocky.io/v2/5e1a9abe3100004e004f316b", (error, response, body) => {
const responseList = JSON.parse(body);
const partialList = responseList.slice(0, 20);
const employeeAges = partialList.map(employee => {
return employee.employeeAge;
});
console.log(employeeAges);
});
});
app.listen(3000);
```
You'll notice that we're using the dummy endpoint http://www.mocky.io/v2/5e1a9abe3100004e004f316b. This endpoint returns an array of values with a shape much like this:
```json
[
{
"id": "1",
"employee_name": "Adam",
"employee_salary": "12322",
"employee_age": "23",
"profile_image": ""
}
]
```
Once you run the `app.js` file in Node, however, you'll see the `console.log`s of:
```javascript
[ undefined ]
```
Instead of the ages of the employees as we might expect. We'll need to dive deeper to figure out what's going on, let's move forward with setting up and using the debugger.
> You may have already spotted the error in this small code sample, but I'd still suggest you read on. Having the skillsets to run a debugger can help immeasurably when dealing with large-scale codebases with many moving parts or even when dealing with an unfamiliar or poorly documented API.
# Starting the Debugger {#starting-the-debugger}
Whereas a typical Express application might have `package.json` file that looks something like this:
```json
{
"name": "example-express-debug-code",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^4.17.1",
"request": "^2.88.0"
},
"scripts": {
"start": "node ./app.js"
}
}
```
We'll be adding one more `scripts` item for debug mode:
```json
{
"name": "example-express-debug-code",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^4.17.1",
"request": "^2.88.0"
},
"scripts": {
"start": "node ./app.js",
"debug": "node --inspect ./app.js"
}
}
```
Once you add in this flag, you can simply run `npm run debug` to start the debuggable session.
> A quick sidenote:
> Some folks like to use [the `nodemon` tool](https://nodemon.io/) in order to get their application to reload upon making changes to their source file.
> That doesn't mean you can't join in the debugger fun! Just replace `node` with `nodemon` for the following `package.json`:
>
> ```json
> {
> "name": "example-express-debug-code",
> "version": "1.0.0",
> "main": "app.js",
> "dependencies": {
> "express": "^4.17.1",
> "request": "^2.88.0"
> },
> "devDependencies": {
> "nodemon": "^2.0.2"
> },
> "scripts": {
> "start": "nodemon ./app.js",
> "debug": "nodemon --inspect ./app.js"
> }
> }
> ```
Once you start your debuggable session, you should be left with a message similar to the following:
```
Debugger listening on ws://127.0.0.1:9229/ffffffff-ffff-ffff-ffff-ffffffffffff
For help, see: https://nodejs.org/en/docs/inspector
```
At this point, _it will hang and not process the code or run it_. That's okay though, as we'll be running the inspector to get the code to run again in the next step.
# The Debugger {#the-debugger}
In order to access the debugger, you'll need to open up Chrome and go to the URL `chrome://inspect`. You should see a list of selectable debug devices, including the node instance you just started.
![A list of inspectable devices from Chrome](./chrome_inspect.png)
Then you'll want to select `inspect` on the node instance.
Doing so will bring up a screen of your entrypoint file with the source code in a window with line numbers.
![The aforementioned code screen](./initial_debugger.png)
These line numbers are important for a simple reason: They allow you to add breakpoints. In order to explain breakpoints, allow me to make an analogy about debug mode to race-car driving.
Think about running your code like driving an experimental race-car. This car has the ability to drive around the track, you can watch it run using binoculars, but that doesn't give you great insight as to whether there's anything wrong with the car. If you want to take a closer inspection of a race-car, you need to have it pull out to the pit-stop in order to examine the technical aspects of the car before sending it off to drive again.
It's similar to a debug mode of your program. You can evaluate data using `console.log`, but _to gain greater insight, you may want to pause your application_, inspect the small details in the code during a specific state, and to do so you must pause your code. This is where breakpoints come into play: they allow you to place "pause" points into your code so that when you reach the part of code that a breakpoint is present on, your code will pause and you'll be given much better insight to what your code is doing.
To set a breakpoint from within the debugging screen, you'll want to select a code line number off to the left side of the screen. This will create a blue arrow on the line number.
> If you accidentally select a line you didn't mean to, that's okay. Pressing this line again will disable the breakpoint you just created
![The blue arrow being added to line 7 of the app.js file](./breakpoint_add.png)
A race-car needs to drive around the track until the point where the pit-stop is in order to be inspected; _your code needs to run through a breakpoint in order to pause and give you the expected debugging experience_. This means that, with only the above breakpoint enabled, the code will not enter into debug mode until you access `localhost:3000` in your browser to run the `app.get('/')` route.
> Some applications may be a bit [quick-on-the-draw](https://en.wiktionary.org/wiki/quick_on_the_draw) in regards to finding an acceptable place to put a breakpoint. If you're having difficulties beating your code running, feel free to replace the `--inspect` flag with `--inspect-brk` which will automatically add in a breakpoint to the first line of code in your running file.
>
> This way, you should have the margins to add in a breakpoint where you'd like one beforehand.
# Using The Debugging Tools {#using-debug-tools}
Once your code runs through a breakpoint, this window should immediately raise to focus (even if it's in the background).
![A breakpoint paused on line 7 at the JSON parsing line](./breakpoint_paused.png)
> If you don't see the `Console` tab at the bottom of the screen, as is shown here, you can bring it up by pressing the `Esc` key. This will allow you to interact with your code in various ways that are outlined below.
Once you do so, you're in full control of your code and its state. You can:
- _Inspect the values of variables_ (either by highlighting the variable you're interested in, looking under the "scope" tab on the right sidebar, or using the `Console` tab to run inspection commands à la [`console.table`](https://developer.mozilla.org/en-US/docs/Web/API/Console/table) or [`console.log`](https://developer.mozilla.org/en-US/docs/Web/API/Console/log)):
![A screenshot of all three of the mentioned methods to inspect a variable's value](./inspect_variable_value.png)
- _Change the value of a variable:_
![A screenshot of using the Console tab in order to change the value of a variable as you would any other JavaScript variable](./change_variable_value.png)
- _Run arbitrary JavaScript commands_, similar to how a code playground might allow you to:
![A screenshot of indexing the body using "body.slice(0, 100)"](./arbitrary_js.png)
## Running Through Lines {#running-through-lines}
But that's not all! Once you make changes (or inspect the values), you're also able to control the flow of your application. For example, you may have noticed the following buttons in the debug window:
![A red circle highlighting the "play" and "skip" buttons in the debugger](./breakpoint_paused_buttons.png)
Both of these buttons allow you to control where your debugger moves next. _The button to the left_ is more of a "play/pause" button. Pressing this _will unpause your code and keep running it_ (with your variable changes intact) _until it hits the next breakpoint_. If this happens to be two lines down, then it will run the line in-between without pausing and then pause once it reached that next breakpoint.
So, if we want to see what happens after the `body` JSON variable is parsed into a variable, we could press the "next" button to the right to get to that line of code and pause once again.
![A screenshot of the JSON being parsed into a few variables with some console logs to prove it did really parse and run the line above it](./next_line.png)
Knowing this, let's move through the next few lines manually by pressing each item. The ran values of the variables as they're assigned should show up to the right of the code itself in a little yellow box; This should help you understand what each line of code is running and returning without `console.log`ging or otherwise manually.
![A screenshot showing ran lines until line 12 of the "console.log". It shows that "employeeAges" is "[undefined]"](./next_few_lines.png)
But oh no! You can see, `employeeAges` on line `9` is the one that results in the unintended `[undefined]`. It seems to be occurring during the `map` phase, so let's add in a breakpoint to line `10` and reload the `localhost:3000` page (to re-run the function in question).
Once you hit the first breakpoint on line `7`, you can press "play" once again to keep running until it hits the breakpoint on line `10`.
![Two breakpoints on line 7 and 10, currently paused on line 10](./press_run_twice.png)
This will allow us to see the value of `employee` to see what's going wrong in our application to cause an `undefined` value.
![A show of the "employee" object that has a property "employee_age"](./inspect_employee.png)
Oh! As we can see, the name of the field we're trying to query is `employee_age`, not the mistakenly typo'd `employeeAge` property name we're currently using in our code. Let's stop our server, make the required changes, and then restart the application.
We will have to run through the breakpoints we've set by pressing the "play" button twice once our code is paused, but once we get through them we should see something like the following:
![Showing that the console log works out the way expected once the map is changed](./working_ran_debugger_code.png)
There we go! We're able to get the expected "23"! That said, it was annoying to have to press "play" twice. Maybe there's something else we can do in similar scenarios like this?
## Disabling Breakpoints {#disabling-breakpoints}
As mentioned previously in an aside, you can disable breakpoints as simply as pressing the created breakpoint once again (pressing the line number will cause the blue arrow to disappear). However, you're also able to temporarily disable all breakpoints if you want to allow code to run normally for a time. To do this, you'll want to look in the same toolbar as the "play" and "skip" button. Pressing this button will toggle breakpoints from enabling or not. If breakpoints are disabled, you'll see that the blue color in the arrows next to the line number will become a lighter shade.
![Showcasing the lighter shade with a red arrow over the mentioned button](./disabled_breakpoints.png)
Whereas code used to pause when reaching breakpoints, it will now ignore your custom set breakpoints and keep running as normal.
## Step Into {#debugger-step-into}
In many instances (such as the `map` we use in the following code), you may find yourself wanting to step _into_ a callback function (or an otherwise present function) rather than step over it. For example, [when pressing the "next" button in the previous section](#running-through-lines), it skipped over the `map` instead of running the line in it (line 10). This is because the arrow function that's created and passed to `map` is considered its own level of code. To dive deeper into the layers of code and therefore **into** that line of code, instead of the "next line" button to advance, you'll need to press the "step into" button.
![A showcase of a breakpoint on line 9 currently paused and a circle around the step into button](./step_inside.png)
Let's say you're on line `9` and want to move into the `map` function. You can press the "step into" to move into line `10`.
![The after effects of pressing the "step into" button after the screenshot above](./step_inside_part-2.png)
Once inside the `map` function, there's even a button _to get you outside of that function and back to the parent caller's next line_. This might if you're inside of a lengthy `map` function, have debugged the line you wanted to inspect, and want to move past the `map` to the next line (the `console.log`). Doing so is as simple as "stepping in" a function, you simply press the "step outside" button to move to the next line
![The "step outside" button being highlighted with a circle with the line 12 console log being paused](./step_outside.png)
> While the example uses a callback in `map`, both of these "step into" and "step out of" also work on functions that are called. For example, assume the code was written as the following:
>
> ```javascript
> const getEmployeeAges = partialList => {
> const ageArray = [];
> for (employee of partialList) {
> ageArray.push(employee.employee_age);
> }
> return ageArray;
> };
>
> app.get('/', (req, res) => {
> request('http://www.mocky.io/v2/5e1a9abe3100004e004f316b', (error, response, body) => {
> const responseList = JSON.parse(body);
> const partialList = responseList.slice(0, 20);
> const employeeAges = getEmployeeAges(partialList);
> console.log(employeeAges);
> });
> });
> ```
>
> You would still be able to "step into" `getEmployeeAges` and, once inside, "step outside" again in the same manor of the `map` function, as shown prior.
# Saving Files {#editing-files-in-chrome}
One more feature I'd like to touch on with the debugger before closing things out is the ability to edit the source files directly from the debugger. Using this feature, it can make the Chrome debugger a form of lite IDE, which may improve your workflow. So, let's revert our code to [the place it was at before we applied the fix we needed](#example-code) and go from there.
![The screenshot of the debugger with the original code](./initial_debugger.png)
Once this window is open, you're able to tab into or use your cursor to select within the text container that holds your code. Once inside, it should work just like a `textarea`, which _allows you to change code as you might expect from any other code editor_. Changing line `10` to `return employee.employee_age` instead of `return employee.employeeAge` will show an asterisk (`*`) to let you know your changes have not yet been applied. _Running your code in this state will not reflect the changes made to the code content on the screen_, which may have unintended effects.
![Showing the asterisk once changes made but not saved](./edited_but_not_saved.png)
In order to make your changes persist, you'll need to press `Ctrl + S` or `Command + S` in order to save the file (much like a Word document). Doing so will bring up a yellow triangle instead of an asterisk indicating _your changes are not saved to the source file but your changes will now take effect_. Re-running the `localhost:3000` route will now correct the behavior you want, but if you open `app.js` in a program like Visual Studio Code, it will show the older broken code.
![A screenshot showing the yellow triangle once this occurs](./temporarily_saved.png)
Not only does VS Code not recognize your changes, but once you close your debugging window, you won't know what you'd changed in order to get your code to work. While this may help in short debugging sessions, this won't do for a longer session of code changes. To do that, you'll want your changes to save to the local file system.
## Persisting Changes {#chrome-as-ide-persist-changes}
In order to save the changes from inside the Chrome to the file system, you need to permit Chrome access to read and write your files. To do this, you'll want to press the "Add folder to workspace" button off to the left of the code screen.
![A red rectangle around "Add folder to workspace" button](./add_folder_to_workspace.png)
Selecting the folder your `app.js` is present in will bring up the dialog to give Chrome permission to view the files and folders within. You'll need to press "Allow" in order to save your saves to your file system.
![A screenshot showing the "Allow"/"Deny" dialog](./allow_fs_usage.png)
Once done, you should now see a list of the files and folders in the parent folder. It should automatically have highlights over the `app.js` file and remove the yellow triangle in favor of another asterisk.
![A screenshot as described in the previous paragraph](./fs_permitted_not_saved.png)
As I'm sure you've guessed, the asterisk indicates that you'll need to save the file again. Once done (using the key combo), the asterisk should disappear.
It's not just JavaScript files you're able to edit, though! You can click or use your keyboard to navigate the file tree of the parent folder. Doing so will allow you to edit and save changes to _any_ file in the filesystem. This would include the `package.json` file in the folder.
![A screenshot of the package json file being edited](./package_json.png)
# Conclusion
While we've covered a lot of functionality present within the Chrome debugger, there's still more to cover about it! If you'd like to read more about it, you may want to take a look at [the extensive blog series by the Chrome team](https://developers.google.com/web/tools/chrome-devtools/javascript) that offers a much deeper dive into all of the debugging tools present within Chrome. Luckily, the skills that you gain while debugging Node.JS applications carries over to debugging front-end JavaScript, so hopefully this article has helped introduce you to the myriad of tools that Chrome has to offer.
Leave a comment down below if you have a question or comment, or feel free to join [our Discord](https://discord.gg/FMcvc6T) to have a direct line to us about the article (or just general tech questions).

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -3,6 +3,7 @@
title: 'Hard grids & baselines: How I achieved 1:1 fidelity on Android', title: 'Hard grids & baselines: How I achieved 1:1 fidelity on Android',
description: 'Testing the limits of `firstBaselineToTopHeight` and `lastBaselineToBottomHeight` to deliver a perfect result.', description: 'Testing the limits of `firstBaselineToTopHeight` and `lastBaselineToBottomHeight` to deliver a perfect result.',
published: '2019-10-07T22:07:09.945Z', published: '2019-10-07T22:07:09.945Z',
edited: '2020-02-02T22:07:09.945Z',
authors: ['edpratti'], authors: ['edpratti'],
tags: ['android', 'design', 'figma'], tags: ['android', 'design', 'figma'],
attached: [], attached: [],
@@ -43,33 +44,35 @@ Android has two main `TextView`s; one of them is `AppCompatTextView`, which has
With Android 9.0 Pie, Google introduced 3 new attributes for `TextView`s: `firstBaselineToTopHeight`, `lastBaselineToBottomHeight` and `lineHeight`. These control everything youd need to build a UI with. With Android 9.0 Pie, Google introduced 3 new attributes for `TextView`s: `firstBaselineToTopHeight`, `lastBaselineToBottomHeight` and `lineHeight`. These control everything youd need to build a UI with.
Shortly after, Google removed those API restrictions by backporting those features to [`AppCompatTextView`](https://developer.android.com/reference/androidx/appcompat/widget/AppCompatTextView) and subsequently, [`MaterialTextView`](https://developer.android.com/reference/com/google/android/material/textview/MaterialTextView?hl=en). This means these attributes can now be used across all supported versions of Android!
However, if you seek fidelity, youll find that `lineHeight` on Android differs from other platforms and most design tools. However, if you seek fidelity, youll find that `lineHeight` on Android differs from other platforms and most design tools.
## How is it any different? ## How is it any different?
Let us take a look at some examples; one with a single line, then two lines, then three lines with line height set to `24pt/sp`. Let us take a look at some examples; one with a single line, then two lines, then three lines with line height set to `24pt/sp`.
![A side-by-side comparison of the differences in line-height between the Figma design tool (which reflect the web and Sketch as well) and Android. Shows how a single line string is "24pt" on the web while it's rounded to "19sp" on Android, it shows how a string that splits two lines is "48pt" on Figma while "43sp" on Android and finally how a three-line string is "72pt" on Figma while "67sp" on Android](./images/Line_Height_Difference.png "A comparison between Figma and Android line-heights") ![A side-by-side comparison of the differences in line-height between the Figma design tool (which reflect the web and Sketch as well) and Android. Shows how a single line string is "24pt" on the web while it's rounded to "19sp" on Android, it shows how a string that splits two lines is "48pt" on Figma while "43sp" on Android and finally how a three-line string is "72pt" on Figma while "67sp" on Android](line_height_difference.png "A comparison between Figma and Android line-heights")
As you can probably tell, Android `TextViews` are always smaller than the ones given to a developer from a design tool and those implemented on the web. In reality, Androids `lineHeight` is not line-height at all! **Its just a smart version of line-spacing.** As you can probably tell, Android `TextViews` are always smaller than the ones given to a developer from a design tool and those implemented on the web. In reality, Androids `lineHeight` is not line-height at all! **Its just a smart version of line-spacing.**
![A side-by-side comparison of line-spacing on Figma and Android. Figma provides equal spacing above and belong to text string to align them with space around while Android is a space between with no spacing on the top for the first item or spacing on the bottom for the last](./images/Under_The_Hood_01.png "A comparison between Figma and Android line-spacing") ![A side-by-side comparison of line-spacing on Figma and Android. Figma provides equal spacing above and belong to text string to align them with space around while Android is a space between with no spacing on the top for the first item or spacing on the bottom for the last](under_the_hood_01.png "A comparison between Figma and Android line-spacing")
![A further comparison of the above image's demo of spacing around on Figma and spacing between on Android](./images/Under_The_Hood_02.png "Another comparison between Figma and Android line-spacing") ![A further comparison of the above image's demo of spacing around on Figma and spacing between on Android](./under_the_hood_02.png "Another comparison between Figma and Android line-spacing")
Now you might ask yourself, “*How can I calculate the height of each `TextView`, then?*” Now you might ask yourself, “*How can I calculate the height of each `TextView`, then?*”
When you use a `TextView`, it has one parameter turned on by default: **`includeFontPadding`**. `includeFontPadding` increases the height of a `TextView` to give room to ascenders and descenders that might not fit within the regular bounds. When you use a `TextView`, it has one parameter turned on by default: **`includeFontPadding`**. `includeFontPadding` increases the height of a `TextView` to give room to ascenders and descenders that might not fit within the regular bounds.
![A comparison between having "includeFontPadding" on and off. When it's off the height is "19sp" and when it's on it is "21.33sp". It shows the formula "includeFontPadding = TextSize * 1.33"](./images/includeFontPadding.png "A comparison of having the 'includeFontPadding' property enabled") ![A comparison between having "includeFontPadding" on and off. When it's off the height is "19sp" and when it's on it is "21.33sp". It shows the formula "includeFontPadding = TextSize * 1.33"](includefontpadding.png "A comparison of having the 'includeFontPadding' property enabled")
Now that we know how Androids typography works, lets look at an example. Now that we know how Androids typography works, lets look at an example.
Heres a simple mockup, detailing the spacing between a title and a subtitle. It is built at `1x`, with Figma, meaning line height defines the final height of a text box — not the text size. (This is how most design tools work) Heres a simple mockup, detailing the spacing between a title and a subtitle. It is built at `1x`, with Figma, meaning line height defines the final height of a text box — not the text size. (This is how most design tools work)
![A spec file of a phone dailing application](./images/Specs.png) ![A spec file of a phone dailing application](./specs.png)
![A mockup with spec lines enabled of a call log app](./images/Implementation.png ) ![A mockup with spec lines enabled of a call log app](./implementation.png )
*Of course, because its Android, the line height has no effect on the height of the `TextView`, and the layout is therefore `8dp` too short of the mockups.* *Of course, because its Android, the line height has no effect on the height of the `TextView`, and the layout is therefore `8dp` too short of the mockups.*
@@ -79,7 +82,7 @@ But even if it did have an effect, the problems wouldnt stop there; the issue
Designers, like myself, like to see perfect alignment. We like consistent values and visual rhythm. Designers, like myself, like to see perfect alignment. We like consistent values and visual rhythm.
![A showcase of the differences in line spacing between a mockup and an implementation in Android](./images/Designers_Want_Designers_Get.png) ![A showcase of the differences in line spacing between a mockup and an implementation in Android](./designers_want_designers_get.png)
Unfortunately, translating values from a design tool wasnt possible. You had the option to either pixel nudge (pictured above, right), or forget about alignment altogether, thus leading to an incorrect implementation that would, yet again, be shorter than the mockups. Unfortunately, translating values from a design tool wasnt possible. You had the option to either pixel nudge (pictured above, right), or forget about alignment altogether, thus leading to an incorrect implementation that would, yet again, be shorter than the mockups.
@@ -87,13 +90,13 @@ Unfortunately, translating values from a design tool wasnt possible. You had
_`firstBaselineToTopHeight`_ and _`lastBaselineToBottomHeight`_ are powerful tools for Android design. They do as the name suggests: If _`firstBaselineToTopHeight`_ is set to `56sp`, then thatll become the distance between the first baseline and the top of a `TextView`. _`firstBaselineToTopHeight`_ and _`lastBaselineToBottomHeight`_ are powerful tools for Android design. They do as the name suggests: If _`firstBaselineToTopHeight`_ is set to `56sp`, then thatll become the distance between the first baseline and the top of a `TextView`.
![A subtitle block showing "56sp" height despite the text visually being much shorter](./images/56sp.png) ![A subtitle block showing "56sp" height despite the text visually being much shorter](56sp.png)
This means that designers, alongside developers, can force the bounds of a `TextView` to match the design specs and open the door to perfect implementations of their mockups. This means that designers, alongside developers, can force the bounds of a `TextView` to match the design specs and open the door to perfect implementations of their mockups.
This is something Ive personally tested in an app I designed. [**Memoire**, a note-taking app](http://tiny.cc/getmemoire) for Android, is a 1:1 recreation of its mockups — for every single screen. This was made possible due to these APIs — *and because [**@sasikanth**](https://twitter.com/its\_sasikanth) is not confrontational* — since text is what almost always makes baseline alignment and hard grids impossible to implement in production. This is something Ive personally tested in an app I designed. [**Memoire**, a note-taking app](http://tiny.cc/getmemoire) for Android, is a 1:1 recreation of its mockups — for every single screen. This was made possible due to these APIs — *and because [**@sasikanth**](https://twitter.com/its\_sasikanth) is not confrontational* — since text is what almost always makes baseline alignment and hard grids impossible to implement in production.
`video: title: "Near-perfect duplication of guidelines against Memoire's mockups and actual app": ./images/Memoire_Bounds_and_Baselines.mp4` `video: title: "Near-perfect duplication of guidelines against Memoire's mockups and actual app": ./memoire_bounds_and_baselines.mp4`
*Memoires TextViews are all customized using these APIs.* *Memoires TextViews are all customized using these APIs.*
@@ -101,7 +104,7 @@ This is something Ive personally tested in an app I designed. [**Memoire**, a
In reality, the new attributes were actually made to be used when creating layouts: you want to make sure the baseline is a certain distance from another element, and it also helps to align the first and lastBaseline to a `4dp` grid. This mirrors the way iOS layouts are built. In reality, the new attributes were actually made to be used when creating layouts: you want to make sure the baseline is a certain distance from another element, and it also helps to align the first and lastBaseline to a `4dp` grid. This mirrors the way iOS layouts are built.
![A showcase of "firstBaselineToTopHeight" being used to create top-padding from an image and lower text on a card, "lastBaselineToBottomHeight" to create bottom padding against the card edge, and "lineHeight" to set the text spacing](./images/Intended_Use.png "A showcase of the various props to size this card") ![A showcase of "firstBaselineToTopHeight" being used to create top-padding from an image and lower text on a card, "lastBaselineToBottomHeight" to create bottom padding against the card edge, and "lineHeight" to set the text spacing](intended_use.png "A showcase of the various props to size this card")
**However, theres one giant flaw: You cant align a `TextView`s `firstBaseline` to another `TextView`s `lastBaseline`.** So a problem immediately arises due to this limitation: **However, theres one giant flaw: You cant align a `TextView`s `firstBaseline` to another `TextView`s `lastBaseline`.** So a problem immediately arises due to this limitation:
@@ -109,13 +112,13 @@ In reality, the new attributes were actually made to be used when creating layou
As you might imagine, **if we want to keep our text aligned to a baseline grid, we need to ensure that the height of each `TextView` is a multiple of 4 while doing so.** This means we must apply first and lastBaseline attributes to both / all of the stacked TextViews — and that becomes hard to maintain. As you might imagine, **if we want to keep our text aligned to a baseline grid, we need to ensure that the height of each `TextView` is a multiple of 4 while doing so.** This means we must apply first and lastBaseline attributes to both / all of the stacked TextViews — and that becomes hard to maintain.
![A comparison table of Dos and Donts that matches the below table](./images/Dos_Donts.png) ![A comparison table of Dos and Donts that matches the below table](./dos_donts.png)
|✅ Good|🛑 Bad| |✅ Good|🛑 Bad|
|--|--| |--|--|
|Applying `firstBaseline` and `lastBaseline` in styles allows you to know exactly what the distance between baselines is, without having to set them one by one to ensure they properly align to a `4dp` grid. | Without applying `firstBaseline` and `lastBaseline` in styles, you cant detect what the default values are, so you are forced to apply these one by one to every `TextView` to ensure they align to a `4dp` grid. | |Applying `firstBaseline` and `lastBaseline` in styles allows you to know exactly what the distance between baselines is, without having to set them one by one to ensure they properly align to a `4dp` grid. | Without applying `firstBaseline` and `lastBaseline` in styles, you cant detect what the default values are, so you are forced to apply these one by one to every `TextView` to ensure they align to a `4dp` grid. |
`video: title: "A comparison of how text spacing is applied on iOS and Android": ./images/iOS_vs_Android.mp4` `video: title: "A comparison of how text spacing is applied on iOS and Android": ./ios_vs_android.mp4`
The solution is to apply them in your `styles.xml` so that, when themed, the `TextView` is given the right text size, height, font, and baseline properties. The solution is to apply them in your `styles.xml` so that, when themed, the `TextView` is given the right text size, height, font, and baseline properties.
@@ -125,7 +128,7 @@ The solution is to apply them in your `styles.xml` so that, when themed, the `Te
The overrides will take precedence to whatever value you set in your **`styles.xml`**, requiring you to hunt down occurrences until you can find a layout that was broken due to the change. Lets look at an example: The overrides will take precedence to whatever value you set in your **`styles.xml`**, requiring you to hunt down occurrences until you can find a layout that was broken due to the change. Lets look at an example:
`video: title: "Allowing margin changes instead will let the text grow to it's expected sie without having issues with the baseline not being centered": ./images/Dont_Override.mp4` `video: title: "Allowing margin changes instead will let the text grow to it's expected sie without having issues with the baseline not being centered": ./dont_override.mp4`
Implementing margins instead of overriding values also matches the way layouts work within Android Studio and design tools like Sketch and Figma. It also ensures that your layouts can scale well to different font sizes. Implementing margins instead of overriding values also matches the way layouts work within Android Studio and design tools like Sketch and Figma. It also ensures that your layouts can scale well to different font sizes.
@@ -135,7 +138,7 @@ Its actually pretty simple. Lets walk through how to adapt one of Material
**Step 1: Place a text box of the text style youd like to adapt — in this case, Headline 6.** **Step 1: Place a text box of the text style youd like to adapt — in this case, Headline 6.**
![A headline 6 within Figma showing "32pt" height](./images/Figma_TextBox_Size.png "Text box within Figma") ![A headline 6 within Figma showing "32pt" height](./figma_textbox_size.png "Text box within Figma")
*Text box within Figma.* *Text box within Figma.*
@@ -143,7 +146,7 @@ Here we can see that the text box has a height of `32`. This is inherited from t
> Headline 6 = `20` (text size) `* 1.33` (`includeFontPadding`) = `26.667sp` > Headline 6 = `20` (text size) `* 1.33` (`includeFontPadding`) = `26.667sp`
![An image showcasing the headline height mentioned above](./images/Android_TextView_Size.png "TextView on Android") ![An image showcasing the headline height mentioned above](./android_textview_size.png "TextView on Android")
*`TextView` on Android.* *`TextView` on Android.*
@@ -151,27 +154,27 @@ Now resize your Figma text box to `26.6` — *it will round it to `27`, but that
**Step 2: With the resized text box, align its baseline with the nearest `4dp` breakpoint in your grid.** **Step 2: With the resized text box, align its baseline with the nearest `4dp` breakpoint in your grid.**
![Baseline now sits on the "4dp" grid.](./images/Step_01.png) ![Baseline now sits on the "4dp" grid.](./step_01.png)
*Baseline now sits on the `4dp` grid.* *Baseline now sits on the `4dp` grid.*
**Step 3: Measure the distance between the baseline and the top and bottom of the text box.** **Step 3: Measure the distance between the baseline and the top and bottom of the text box.**
![Showcasing the above effect by having 'firstBaselineToTopHeight' set to 20.66 and 'lastBaselineToBottomHeight' to 6.0](./images/Step_02.png) ![Showcasing the above effect by having 'firstBaselineToTopHeight' set to 20.66 and 'lastBaselineToBottomHeight' to 6.0](step_02.png)
*`firstBaselineToTopHeight`: `20.66` | `lastBaselineToBottomHeight`: `6.0`* *`firstBaselineToTopHeight`: `20.66` | `lastBaselineToBottomHeight`: `6.0`*
**Step 4: Now right click the text box and select Frame Selection.** **Step 4: Now right click the text box and select Frame Selection.**
![The right-click dialog hovering over Frame Selection, key binding Ctrl+Alt+G ](./images/Step_03.png "The right-click dialog hovering over Frame Selection") ![The right-click dialog hovering over Frame Selection, key binding Ctrl+Alt+G](./step_03.png "The right-click dialog hovering over Frame Selection")
*When created from an object, a frames dimensions are dependent on the content inside it.* *When created from an object, a frames dimensions are dependent on the content inside it.*
**Step 5: While holding Ctrl / Command, drag the frame handles and resize it so that the top and bottom align with the nearest baselines beyond the minimum values.** **Step 5: While holding Ctrl / Command, drag the frame handles and resize it so that the top and bottom align with the nearest baselines beyond the minimum values.**
![The moving of the baseline by holding the key commands](./images/Step_04.png) ![The moving of the baseline by holding the key commands](./step_04.png)
![Another view of the same adjustment](./images/Step_05.png) ![Another view of the same adjustment](./step_05.png)
**NOTE: Keep in mind we must not resize the text box with it. Holding Ctrl / Command is very, very important.** **NOTE: Keep in mind we must not resize the text box with it. Holding Ctrl / Command is very, very important.**
@@ -181,23 +184,23 @@ The same thing was done to the last baseline and the bottom; we changed it from
**Step 6: Select the text box inside the frame, and set the text to Grow Vertically.** **Step 6: Select the text box inside the frame, and set the text to Grow Vertically.**
![A view of the image aligning tool with the tooltip enabled for "Grow Vertically"](./images/Step_06.png "You can recreate the margin vertical grow functionality by selecting this") ![A view of the image aligning tool with the tooltip enabled for "Grow Vertically"](./step_06.png "You can recreate the margin vertical grow functionality by selecting this")
This will cause the text box to return to its original height of `32sp` — inherited from the line height. This will cause the text box to return to its original height of `32sp` — inherited from the line height.
![A showcase of the text box being "1sp" down from the frame](./images/Step_07.png) ![A showcase of the text box being "1sp" down from the frame](./step_07.png)
*The text box is 1sp down from the frame, but thats normal. We no longer care about the text box height.* *The text box is 1sp down from the frame, but thats normal. We no longer care about the text box height.*
**Step 7: With the text box selected, set its constraints to *Left & Right* and *Top & Bottom*.** **Step 7: With the text box selected, set its constraints to *Left & Right* and *Top & Bottom*.**
![A view of the constraints dialog in Figma on the headline](./images/Step_08.png) ![A view of the constraints dialog in Figma on the headline](./step_08.png)
*Now your text box will resize with your frame. This is essential when using the text components.* *Now your text box will resize with your frame. This is essential when using the text components.*
You would need to find these values for every text style in your app, but if youre taking the Material Design Type Spec as a base for your own, I have already measured and picked the right values for each! _**Resources at the end.**_ You would need to find these values for every text style in your app, but if youre taking the Material Design Type Spec as a base for your own, I have already measured and picked the right values for each! _**Resources at the end.**_
![A showcase of what the headings and text should look like at the end](./images/headline-text-size-showcase.png) ![A showcase of what the headings and text should look like at the end](./headline_text_size_showcase.png)
## How to implement these values (as a developer) ## How to implement these values (as a developer)
@@ -224,7 +227,7 @@ We first set up a `TextAppearance` — which your app probably already has —
Lets use Memoire once again as an example. Lets use Memoire once again as an example.
![An example of the Memoire codebase showing the headline of 4](./images/memoire-headline-4-code.png) ![An example of the Memoire codebase showing the headline of 4](./memoire_headline_4_code.png)
### Each has a different function: ### Each has a different function:
@@ -235,7 +238,7 @@ For example, _**`textAppearanceCaption`**_, _**`textAppearanceBody1`**_, etc.
**`TextStyle`:** Applied to `TextView`s in layouts, to ensure `4dp` alignment. **`TextStyle`:** Applied to `TextView`s in layouts, to ensure `4dp` alignment.
![A display of code styling when "TextStyle" is properly applied. See 'styles.xml' at the bottom of the post for an example](./images/text-style-applied-properly.png "A display of code styling when TextStyle is properly applied") ![A display of code styling when "TextStyle" is properly applied. See 'styles.xml' at the bottom of the post for an example](text_style_applied_properly.png "A display of code styling when TextStyle is properly applied")
*What happens to a `TextView` when a `TextStyle` is properly applied.* *What happens to a `TextView` when a `TextStyle` is properly applied.*
@@ -249,7 +252,7 @@ When setting a style to a `TextView`, keep in mind that `firstBaseline` and `las
Applying a `TextStyle` to a component — instead of a `TextAppearance` — causes serious issues. Applying a `TextStyle` to a component — instead of a `TextAppearance` — causes serious issues.
![A showcase of a "button" component not having the text align to the height of the component](./images/TextStyle_Buttons.png) ![A showcase of a "button" component not having the text align to the height of the component](./textstyle_buttons.png)
*Uh-oh…* *Uh-oh…*
@@ -261,7 +264,7 @@ As far as other issues, I havent been able to find any.
Now that youve scrolled all the way down without reading a single word, heres all the stuff youll need: Now that youve scrolled all the way down without reading a single word, heres all the stuff youll need:
![A preview of the Figma document with code and layout samples](./images/Preview.png) ![A preview of the Figma document with code and layout samples](./preview.png)
*Figma document with code and layout samples.* *Figma document with code and layout samples.*

View File

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 317 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,163 @@
---
{
title: "How to Pick Tech Stacks For New Projects",
description: 'I often get asked: "How do you pick a tech stack for your projects?". This article answers that by outlining what questions you should be asking early on',
published: '2020-03-02T05:12:03.284Z',
authors: ['crutchcorn'],
tags: ['engineering', 'advice'],
attached: [],
license: 'cc-by-nc-sa-4'
}
---
I talk to engineers; I talk to a lot of engineers. I've spoken to engineers from various backgrounds and various skillsets. We all have had to face the same thing at some point: "What tools do you pick for the job?". It's a question that was phrased perfectly by [Lindsay Campbell](https://www.linkedin.com/in/lindsaycampbelldeveloper/) on [the public Unicorn Utterances Discord server our community use to chat](https://discord.gg/FMcvc6T):
_"When you start a new project, how do you go about planning it? How do you know what features you want? How do you even start do you figure out frameworks, libraries you will use for those different features? What do you do to also make sure that all the different technologies you will be using will work together nicely in your application? Thanks!"_
> Side note, Lindsay is an excellent engineer. You should [check out her profile](https://www.linkedin.com/in/lindsaycampbelldeveloper/) and give her a follow
The answer is ironically a lot less about the solution to a given problem as much as it is discovering the root of the problem itself.
For example, let's look at a project I've been debating on spinning up with a few folks:
An online-first Bootcamp system with interactive quizzes, live-streamed content (like video sessions), a large set of hosted video, and other education-related features.
The first thing I do, _before looking at any tech whatsoever is think about it from a business perspective_.
- "Who is this for?"
- "What do they want?"
- "What's most important to have done first?"
- "What are my stretch goals/holistic vision/defining drive?"
- "What is the profit model?" (if that matters to that project)
- "What's my budget?" (_Budget means more than just finances_, if you're talking about a side project, the budget is the time you have to work on the project).
These are all of the questions I layout before even thinking about coding. I first start by white-boarding these things, explaining them to both myself and my partners, and generally doing my due-diligence concerning project planning.
## Wholistic Vision {#whats-your-vision}
My holistic vision would consist of:
- Simple-to-use UI
- Lots of full-filled content, such as video courses or pictures to serve alongside their written content
- A single place to host a course for someone
- An independent creator feeling comfortable enough to host content here without having to make their landing page in a separate service. As such, we'll need to provide a lightweight customization of a page to showcase their own brand/course.
- Focus on groups rather than single courses. Subscribing to a single content group/creator rather than "React course #1" which has no clear distinction from another "React course #1"
While the first point doesn't inform us of much at this early stage (we'll touch on UI tooling selection later), we can glean from the second point that we'll have to maintain some kind of storage layer. This will be something we'll need to keep in mind as we structure our goals.
## Target Audience {#who-are-you-targetting}
In this case, the groups of people I would want to appeal to are:
- Students looking for a place to learn remotely
- Independent teachers looking for a unified platform to publish through
- Bootcamps looking to have an organized, content-focused site to host their courses
This potentially broad appeal might be able to drive a lot of business, but without a focused plan and a solid profit model, the project would fall flat.
## Profit Model {#layout-your-profit-model}
We'd plan to drive revenue by using the following profit model:
- We'd focus on a B2B type solution where you could pay for a pro account that would make promoting your courses and stuff to other students easier.
- No students would pay for accounts but might pay for a subscription to course content
- We'd likely take a cut of the subscription or charge for course features in some way
## Budget {#define-your-budget}
Finally, none of this can be done without resources. These resources should be budgeted upfront, so what have we got? We have:
- Myself, maybe a few other folks local to my area working on this project
- This project would be a second job or side project for all of us
- Additionally, I'd be working on this project on top of working on other UU content and other side projects
Our limited budget tells us that we will have to be hyper-focused when it comes time to planning out our MVP. We'll need to keep our goals well defined, and if we intend to make it profitable, we'll need to _keep those goals closely aligned with our profit model's requirements define_.
Now that we have a more precise goal of what the problem space we're entering is, we can more clearly define our goals (next part)
# Goals {#mvp}
Now that we're onto setting goals, I like to start thinking about "What is the bare minimum we need to show this to someone to spark a conversation." _This is often called the "minimum viable product" or "MVP" for short_.
Looking at what we need to do from the previous section, I can say that we could probably get away with the following to reach that "MVP":
- User account creation. We'll keep only one type of user for now, but we do need to be aware that users will have different permission roles in the future
- Organizations creation/viewing (we can manually assign users to organizations using the database for now, but we'll want to structure data to support many users per organization)
- This org will need courses, so creation/view of those (no need to manage permissions, that'd be a future feature)
- Courses will need content, so a way to upload/view content on courses
While thinking about these features, I want to keep the implementation details to a minimum, just enough to suffice with our resources by ignoring the nuances of certain permission features. However, notice how, despite thinking about the features minimally, *I'm also mentally mapping how the data should be structured and thinking about long-term implications* in such a way that we can add them later without refactoring everything. This balance during architecture can be tough to achieve and becomes more and more natural with experience.
# Requirements {#data-requirements}
Finally, I look at the data requirements and features and start thinking about what code requirements I'll run into to implement those data requirements.
- I need to upload/download files
Speaking from experience, doing this with GraphQL is tricky, so I'll stick with REST for the MVP
- My data isn't likely to change structure very much
As a result, I'd feel comfortable using SQL for something like this.
- I need user authentication
I don't like rolling my own auth solution, so I'll probably use [passport](https://www.npmjs.com/package/passport) since it's been well tested and stable. If I want to enable users to sign in from their Google accounts or something in the future, I should keep that in mind even if I'm not building that functionality right away
- I am going to be focusing on per-user UI (achievements, dashboards, etc.)
As such, my use of something like [Gatsby](https://www.gatsbyjs.org/) for static site generation (SSG) isn't realistically beneficial. We could go with server-side rendering (SSR) with something like [Next.JS](https://nextjs.org/), but due to using a lot of media (video/picture), I'd argue there's not much of a return-on-investment (ROI) by building SSR-first since the content has to be loaded by the DOM regardless.
- I'm not likely to have many forms in my application - primarily focusing on viewing rather than form creation
Sometimes it's important to know what an application is and _isn't_ going to be using. If we were highly focused on forms, I might advocate for [Angular](https://angular.io/) to be used in the front-end (since I have found their form system to be quite robust). However, since I know my team is not as familiar with Angular as other options and we have a limited budget, we likely won't be moving forward with it
- However, we'll be hoping to have a lot of live-streamed user content in the future
Stuff like "live quizzes," live streaming/playback of video, anything that requires tracking of time/etc is all a great use case for event-based programming. One of the most prominent implementations of this in JavaScript is [RxJS](https://github.com/ReactiveX/rxjs).
So there we have it - a non-Angular, REST API, Passport authenticated, SQL DB, non-SSR, RxJS powered application
Now, this doesn't give us the whole idea, but from here we can start doing further research (next part)
# Extra Pieces
From here, things start becoming a lot more subjective and a lot more social.
While I personally prefer Vue, after talking with my team, it became clear that they're much more comfortable with React. Because React has a large ecosystem with a sturdy backing, I'm not against using it since I feel it can sustain our product's growth over time.
Moving onto CSS was more of the same: It was less "what can support this specific use-case" and more "what is familiar and can sustain our growth?".
This example is where things get really tricky because you often are not just picking a framework or library, but often a philosophy of CSS as well. After a long-form discussion with my (front-end focused) team about this, we decided to go with Styled Components and Material UI. These tools were decided on due to their flexibility, general A11Y support (for MUI), themability, and our comfort with the tools. The size and stability also took a role in this discussion.
Smaller decisions of libraries for me often boil down to a formula-of-sorts:
- What's their community support like? (can I ask a question and have an answer within a few days)
- What's the size of their community? (typically judged by questions I can find on StackOverflow, their community site/forum or even npm downloads)
- How stable is the tool?
- When was it last updated?
- Does it handle my edge-cases?
- What's the performance of the tool?
Each tool and usage will weigh these questions differently. If I'm looking for a simple timer component library and come across two options, I may be more likely to pick a small library over a larger one, depending on the context. For example, _if that smaller library has clean, easily readable code, but only has seven stars on GitHub, that's likely better for my project than a more bloated alternative because I know I can maintain it if all else fails._ However, I personally wouldn't likely go with something like IO.js (now defunct alternative to Node.js) for a larger project regardless of how clean the code is because I'd be unable to maintain the much more complex tool if I ever needed to.
# Conclusion
To recap, it's a mixture of:
- Proper planning (focusing on features and experience rather than tech)
- This point should take double priority, as understanding what to start building after picking the tech is important
- Expertise
I knew that SQL would suffice for our data thanks to my experience scaffolding various applications with SQL and NoSQL alike
- Research
The only reason I knew that working with binary data over GQL is due to research I did ahead of time before even writing any product code
- Communication
This one is often overlooked but is **critical** - especially within teams. _Leverage each other's strengths and weaknesses and be open and receptive to suggestions/concerns_
That's by **no** means an easy feat to do, despite reading as if they were. Don't worry if you're not able to execute these skills flawlessly - goodness knows I can't! I'm sure a lot of the decisions I made here, even with the group I spoke to, could have been better guided in different ways. These are the skills that I think I value the most in seniors developer, especially communication. _Communication becomes critical when working with medium/larger teams (or really, groups of any size) since reasonable minds may differ on toolsets that they might see strengths/weaknesses in_.
Have a similar question to the one Lindsey asked? Like conversations like this? Have something to add? [Join us in our Discord server](https://discord.gg/FMcvc6T) to jump into the community and engage in conversations like this! We wouldn't have the quality of our content without our community!

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -0,0 +1,326 @@
---
{
title: "Integrating Native Android Code in Unity",
description: 'Have you ever wanted to run native Java and Kotlin code from your mobile game written in Unity? Well you can! This article outlines how to set that up!',
published: '2020-01-04T05:12:03.284Z',
authors: ['crutchcorn'],
tags: ['unity', 'android', 'c#', 'java', 'kotlin'],
attached: [],
license: 'cc-by-nc-sa-4'
}
---
Working on mobile games with Unity, you may come across some instances where you'll want to run native code. Whether it be to access specific sensors, run code in the background, or other closer-to-hardware mobile-specific actions, knowing how to call native code from within your Unity's C# environment can be a great boon to your developmental efforts.
Luckily for us, Unity has a system of "plugins" that allow us to do just that. Unity contains the ability to map code between C# and Java by using in-house-developed helper classes to cross-talk between the two languages. This article will outline [how to set up a development environment](#set-up-a-development-environment), [how to manage Android dependencies in Unity](#android-dependencies), and finally [how to call Android-specific code from C#](#call-android-from-c-sharp). Without further ado, let's dive in! 🏊‍♂️
> ⚠️ Be aware that this information is based on Unity 2018 versions. While this might be relevant for older versions of Unity, I have not tested much of this methodology of integration with older versions.
# Setting up Development Environment {#set-up-a-development-environment}
[Unity supports using either Java files or Kotlin source files as plugins](https://docs.unity3d.com/Manual/AndroidJavaSourcePlugins.html). This means that you're able to take Android source files (regardless of if they're written in Java or Kotlin) and treat them as callable compiled library code. Unity will then take these files and then include them into its own Gradle build process, allowing you — the developer — to focus on development rather than the build process.
> For anyone who may have experimented with doing so in older versions of Unity in the past will note that this is a massive improvement — it used to be that you'd have to compile to AAR files and include them manually.
That said, the editor you may be using may not be best suited for editing Android code, and it would be great to have a powerful development experience while working with. For this purpose, it would be great to edit code using [the official IDE for Android development: Android Studio](https://developer.android.com/studio/).
Unfortunately, I've had difficulties getting the same Android Studio development environment to sync with the "source file" interoperability that Unity provides. For this reason, I tend to have two folders:
- One of these folders lives at the root of the project (directly under `Unity/ProjectName`) called `AndroidStudioDev` that I open in Android Studio.
- The other folder is one that lives under `Assets` called `AndroidCode`, which contains copied-and-pasted files from `AndroidStudioDev` that are only the related source files I need to call.
![Showcase of the filesystem as described by the previous paragraph](./android_code_fs_layout.png)
Once the copying of the files from the Android Studio environment to `Assets` has finished, you'll need to mark it as being included in the Android build within Unity's inspector window that comes up when you highlight the source file.
![The inspector window showing "Android" selected](./unity_inspector.png)
> If you forget to do this, your class or file may not be found. This is an important step to keep in mind during debugging.
This will naturally incur a question for developers who have tried to maintain a system of duplication of any size:
**How do you manage dependencies between these two folders?**
## Managing Android Dependencies {#android-dependencies}
Luckily for us, managing Android code dependencies in Unity has a thought-out solution from a large company: Google. [Because Google writes a Firebase SDK for Unity](https://firebase.google.com/docs/unity/setup), they needed a solid way to manage native dependencies within Unity.
### Installing the Unity Jar Resolver {#installing-jar-resolver}
> If you've installed the Unity Firebase SDK already, you may skip the step of installing.
[This plugin, called the "Unity Jar Resolver"](https://github.com/googlesamples/unity-jar-resolver/), is hugely useful to us for synchronizing our development environment. You can start by downloading it from [their releases tab on GitHub](https://github.com/googlesamples/unity-jar-resolver/releases).
> If you have a hard time finding the download link, you'll want to press the three dots (or, if you're looking for the alt text: the "Toggle commit message" button). There will typically be a link for downloading the `.unitypackage` file.
In your project, you'll then want to select `Assets > Import Package > Custom Package` in order to import the downloaded plugin.
![A visual of where to find that menu in the MacOS menubar](./import_custom_package.png)
Then, you'll see a dialog screen that'll ask what files you want to import with your Unity Package. Ensure that all of the files are selected, then press "Import".
![A screenshot of the dialog mentioned](./importing_the_plugin.png)
> Your screen may look slightly different from the one above. That's okay — so long as all of the files are selected, pressing "Import" is perfectly fine.
### Using the Jar Resolver {#using-jar-resolver}
Using the Jar resolver is fairly straightforward. Whenever you want to use a dependency in your Android code, you can add them to a file within [the `Assets/AndroidCode` folder](#set-up-a-development-environment) that adds dependencies with the same keys as the ones typically found in a `build.gradle` file for dependencies.
```xml
<!-- DeviceNameDependencies.xml -->
<dependencies>
<androidPackages>
<androidPackage spec="com.jaredrummler:android-device-names:1.1.8">
</androidPackage>
</androidPackages>
</dependencies>
```
The only rule with this file structure is that your file must end with `Dependencies.xml`. You can have as many of these files as you'd like. Let's say you want to separate out dependencies based on features? You can do that, just have separate files that follow that naming pattern!
```xml
<!-- LocationCodeDependencies.xml -->
<!-- Alongside the other file -->
<dependencies>
<androidPackages>
<androidPackage spec="com.google.android.gms:play-services-location:16.0.0">
</androidPackage>
</androidPackages>
</dependencies>
```
After creating the files, in the menubar, go to `Assets > Play Services Resolver > Android Resolver > Resolve`, and it should go fetch the AAR files related to those specific libraries and download them.
![The MacOS menubar showing the above path to resolve libraries](./resolve_dependencies.png)
So long as your file ends with `Dependencies.xml`, it should be picked up by the plugin to resolve the AAR files.
#### Adding Support into Android Studio Environment {#add-android-studio-support}
But that's only half of the equation. When editing code in Android Studio, you won't be able to use the libraries you've downloaded in Unity. This means that you're stuck manually editing both of the locations for dependencies. This is where a simple trick with build files comes into play.
Assuming, like me, you used the built-in "Create Project" method of starting a codebase in Android Studio, you'll have a `build.gradle` file for managing dependencies. However, you'll notice that when you run the `Resolve` on the plugin in Unity, it'll download AAR and JAR files to `Assets/Plugins/Android`. You can tell Android Studio's Gradle to include them by adding the following line to your `dependencies`:
```groovy
dependencies {
implementation fileTree(dir: '../../Assets/Plugins/Android', include: ['*.jar', '*.aar'])
}
```
This will take all of the AAR files and JAR files and treat them as if they were synced by Android Studio's Gradle sync.
For more information on how to manage your app's dependencies from within Unity, you may want to check out [this article created by the Firebase developers](https://medium.com/firebase-developers/how-to-manage-your-native-ios-and-android-dependencies-in-unity-like-firebase-921659843aef), who coincidentally made the plugin for managing Android dependencies in Unity.
# Call Android code from C# {#call-android-from-c-sharp}
It's great that we're able to manage those dependencies, but they don't mean much if you're not able to utilize the code from them!
For example, take the following library: https://github.com/jaredrummler/AndroidDeviceNames
That library allows you to grab metadata about a user's device. This might be useful for analytics or bug reporters you may be developing yourself. Let's see how we're able to integrate this Java library in our C# code when building for the Android platform.
## Introduction {#intro-call-android-from-c-sharp}
You must make your callback extend the type of callback that is used in the library. For example, take the following code sample from the README of the library mentioned above:
```java
DeviceName.with(context).request(new DeviceName.Callback() {
@Override public void onFinished(DeviceName.DeviceInfo info, Exception error) {
String manufacturer = info.manufacturer; // "Samsung"
String name = info.marketName; // "Galaxy S8+"
String model = info.model; // "SM-G955W"
String codename = info.codename; // "dream2qltecan"
String deviceName = info.getName(); // "Galaxy S8+"
}
});
```
While this example may seem straightforward, let's dissct what we're doing step-by-step here. This will allow us to make the migration to C# code much simpler to do mentally.
```java
// Create a new "DeviceName.Callback" instance
DeviceName.Callback handleOnFinished = new DeviceName.Callback() {
// Provide an implementation of the `onFinished` function in the `Callback` class
// Notice that there are two parameters for this method: one for info, the other for errors
@Override public void onFinished(DeviceName.DeviceInfo info, Exception error) {
// ... Assignment logic here
}
};
// Create a `DeviceName.Request` by passing the current context into the `DeviceName.with` method
DeviceName.Request withInstance = DeviceName.with(context);
// Use that request instance to pass the `DeviceName.Callback` instance from above to run the related code
withInstance.request(handleOnFinished);
```
You can see that we have a few steps here:
1) Make a new `Callback` instance
- Provide an implementation of `onFinished` for said instance
2) Call `DeviceName.with` to create a request we can use later
- This means that we have to gain access to the currently running context to gain device access. When calling the code from Unity, it means we have to get access to the `UnityPlayer` context that Unity engine runs on
3) Call that request's `request` method with the `Callback` instance
For each of these steps, we need to have a mapping from the Java code to C# code. Let's walk through these steps one-by-one
## Create `Callback` Instance {#android-c-sharp-callback}
In order to create an instance of a `Callback` in C# code, we first need a C# class that maps to the `Java` interface. To do so, let's start by extending the Android library interface. We can do this by using the `base` constructor of `AndroidJavaProxy` and the name of the Java package path. You're able to use `$` to refer to the interface name from within the Java package.
```csharp
private class DeviceCallback : AndroidJavaProxy
{
// `base` calls the constructor on `AndroidJava` to pass the path of the interface
// `$` refers to interface name
public DeviceCallback() : base("com.jaredrummler.android.device.DeviceName$Callback") {}
}
```
> [This package path can be found in the library's code at the following path](https://github.com/jaredrummler/AndroidDeviceNames/blob/e23b73dbb81be6cb64dfa541a3e93800ee26b185/library/src/main/java/com/jaredrummler/android/device/DeviceName.java#L17). The `DeviceName` is referring to the path of the `.java` file name.
We can then provide an implementation of the `onFinished` method of that `Callback`. Recall how we previously had two params? Well, now the implementation will require we use the `AndroidJavaObject` type for both of those params.
Otherwise — if we type the function with a C# interface or class that matches the Java implementation — the method will not be called when we expect it to. This is due to function overloading expecting to get the `AndroidJavaObject` from the code Unity has developed to call mapped functions and classes.
This [`AndroidJavaObject` type has a myriad of methods that can be called to assist in gathering data from or interfacing with the Java object](https://docs.unity3d.com/ScriptReference/AndroidJavaObject.html). One of such methods is the [`Get` method](https://docs.unity3d.com/ScriptReference/AndroidJavaObject.Get.html). When called on an `AndroidJavaObject` instance in C#, it allows you to grab a value from Java. Likewise, if you intend to call a method from the Java code, you can use [`AndroidJavaObject.Call`](https://docs.unity3d.com/ScriptReference/AndroidJavaObject.Call.html).
```csharp
private class DeviceCallback : AndroidJavaProxy
{
public DeviceCallback() : base("com.jaredrummler.android.device.DeviceName$Callback") {}
// These both MUST be `AndroidJavaObject`s. If not, it won't match the Java method type and therefore won't be called
void onFinished(AndroidJavaObject info, AndroidJavaObject err)
{
// When running `AndroidJavaObject` methods, you need to provide a type for the value to be assigned to
var manufacturer = info.Get<string>("manufacturer"); // "Samsung"
var readableName = info.Get<string>("marketName"); // "Galaxy S8+"
var model = info.Get<string>("model"); // "SM-G955W"
var codename = info.Get<string>("codename"); // "dream2qltecan"
var deviceName = info.Call<string>("getName"); // "Galaxy S8+"
}
}
```
## Get Current Context {#get-unity-context}
Just as all Android applications have some context to their running code, so too does the compiled Unity APK. When compiling down to Android, Unity includes a package called the "UnityPlayer" to run the compiled Unity code. The package path for the player in question is `com.unity3d.player.UnityPlayer`.
While there is not a docs reference page for this Java class, [some of the company's code samples](https://docs.unity3d.com/530/Documentation/Manual/PluginsForAndroid.html) provide us with some useful methods and properties on the class. For example, that page mentions a static property of `currentActivity` that gives us the context we need to pass to `DeviceName.with` later on:
```csharp
var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
var activity = player.GetStatic<AndroidJavaObject>("currentActivity");
```
We can then gain access to the `DeviceName` Java class. If we look at [the related Java code from the previous section](#call-android-from-c-sharp), we can see that we're calling `DeviceName.with` without making a new instance of `DeviceName`:
```java
DeviceName.Request withInstance = DeviceName.with(context);
```
This means that `with` must be a static method on the `DeviceName` class. In order to call static Java methods, we'll use the `AndroidJavaClass.CallStatic` method in C#.
```csharp
var jc = new AndroidJavaClass("com.jaredrummler.android.device.DeviceName");
var withCallback = jc.CallStatic<AndroidJavaObject>("with", activity);
```
Finally, we can add the call to `request` with an instance of the `DeviceCallback` class.
```csharp
var deviceCallback = new DeviceCallback();
withCallback.Call("request", deviceCallback);
```
## Complete Code Example {#android-c-sharp-code-sample}
Line-by-line explanations are great, but often miss the wholistic image of what we're trying to achieve. The following is a more complete code sample that can be used to get device information from an Android device from Unity.
```csharp
public class DeviceInfo {
public string manufacturer; // "Samsung"
public string readableName; // "Galaxy S8+"
public string model; // "SM-G955W"
public string codename; // "dream2qltecan"
public string deviceName; // "Galaxy S8+"
}
class DeviceName : MonoBehaviour {
private class DeviceCallback : AndroidJavaProxy {
// Add in a field for us to gain access to the device info after the callback has ran
public DeviceInfo deviceInfo;
public DeviceCallback() : base("com.jaredrummler.android.device.DeviceName$Callback") {}
void onFinished(AndroidJavaObject info, AndroidJavaObject err) {
deviceInfo.manufacturer = info.Get<string>("manufacturer");
deviceInfo.readableName = info.Get<string>("marketName");
deviceInfo.model = info.Get<string>("model");
deviceInfo.codename = info.Get<string>("codename");
deviceInfo.deviceName = info.Call<string>("getName");
}
}
private void Start() {
var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
var activity = player.GetStatic<AndroidJavaObject>("currentActivity");
var jc = new AndroidJavaClass("com.jaredrummler.android.device.DeviceName");
var withCallback = jc.CallStatic<AndroidJavaObject>("with", activity);
var deviceCallback = new DeviceCallback();
withCallback.Call("request", deviceCallback);
Debug.Log(deviceCallback.deviceInfo.deviceName);
}
}
```
# Calling Source Code from Unity {#call-source-from-unity}
Calling native Android code can be cool, but what if you have existing Android code you want to call from Unity? Well, that's supported as well. Let's take the following Kotlin file:
```kotlin
// Test.kt
package com.company.example
import android.app.Activity
import android.util.Log
class Test() {
fun runDebugLog() {
Log.i("com.company.example", "Removing location updates")
}
}
```
Assuming you [copied it over to the `Assets/AndroidCode` folder and marked it to be included in the Android build](#set-up-a-development-environment), you should be able to use the `package` name and the name of the class in order to run the related code.
```csharp
var testAndroidObj = new AndroidJavaObject("com.company.example.Test");
testAndroidObj.Call("runDebugLog");
```
# AndroidManifest.XML Overwriting {#manifest-file}
Many Android app developers know how important it can be to have the ability to customize their manifest file. By doing so, you're able to assign various metadata to your application that you otherwise would be unable to. Luckily for us, Unity provides the ability to overwrite the default XML file.
By placing a file under `Assets\Plugins\Android\AndroidManifest.xml`, you're able to add new values, change old ones, and much more.
If you want to find what the default manifest file looks like, you'll want to look for the following file: `<UnityInstallationDirecory>\Editor\Data\PlaybackEngines\AndroidPlayer\Apk\AndroidManifest.xml`. This file is a good baseline to copy into your project to then extend upon. The reason I suggest starting with the default XML is that Unity requires its own set of permissions and such. After that, however, you're able to take the manifest and customize it to your heart's content.
> It's worth mentioning that if you use Firebase Unity SDK and wish to provide your own manifest file, you'll need to [customize the default manifest file to support Firebase opperations](https://firebase.google.com/docs/cloud-messaging/unity/client#configuring_an_android_entry_point_activity).
# Firebase Support {#firebase}
Let's say you're one of the users who utilizes the Firebase SDK for Unity. What happens if you want to send data from Android native code or even use background notification listeners in your mobile app?
You're in luck! Thanks to the Unity Firebase plugin using native code in the background, you're able to share your configuration of Firebase between your native and Unity code. So long as you've [configured Firebase for Unity properly](https://firebase.google.com/docs/cloud-messaging/unity/client#add-config-file) and [added the config change to Android Studio](#add-android-studio-support), you should be able to simply call Firebase code from within your source files and have the project configs carry over. This means that you don't have to go through the tedium of setting up and synchronizing the Unity and Android config files to setup Firebase — simply call Firebase code from your source files, and you should be good-to-go! No dependency fiddling required!
# Conclusion {#conclusion}
I hope this article has been helpful to anyone hoping to use Android code in their Unity mobile game; I know how frustrating it can be sometimes to get multiple moving parts to mesh together to work. Rest assured, once it does, it's a satisfying result knowing that you're utilizing the tools that Unity and the Firebase team have so graciously provided to game developers.
If you have any questions or comments, please leave them down below. Thanks for reading!

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Some files were not shown because too many files have changed in this diff Show More