作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Demir是一名开发人员和项目经理,在广泛的软件开发角色方面拥有超过15年的专业经验.
24
谷歌眼镜是一项未来技术,有望彻底改变我们使用设备与世界互动的方式. 但是从开发者的角度来看,为玻璃开发的特别之处在于? 答案是“没什么。!” In fact, from the perspective of an experienced Android developer谷歌眼镜(谷歌Glass)只是另一款屏幕非常小、功能有限的安卓设备!
任何拥有Android开发知识的人都可以成为这个未来可穿戴技术布道者的“精英”社区的一员,这是谷歌Glass如此出色的部分原因. Sure, you will need to learn a few new things, 比如“沉浸式”和“有源卡”的区别, 但是你会看到, 学习曲线并不陡峭.
本谷歌Glass教程的目的是为开发任何Glass应用程序奠定基础, 通过创建一个包含所有常见步骤的简单应用程序. 我的目标是为您节省一些研究和试错的时间, 让你能够尽快运行你的第一个谷歌眼镜应用程序.
在本教程中,我们将首先介绍如何设置开发环境并将谷歌Glass连接到计算机. 然后,我们将创建一个简单的“Hello World”眼镜应用程序,它将包括自定义语音命令,并与眼镜开始菜单集成. 一旦你得到你的第一个应用程序开发和运行在你的眼镜上,你将学习导航的基础知识在眼镜应用程序, 声控菜单, 以及动态内容创作.
谷歌眼镜仍处于“beta测试”阶段, b谷歌为此发明了“探索者程序”这个词. 不管你怎么称呼它,谷歌眼镜还不能像智能手机那样在商店里买到. Unfortunately, Android开发工具仍然没有模拟器,您可以使用它在没有实际硬件的情况下开发应用程序.
Thus, in order to run and debug your application, 你需要通过浏览器程序得到一个真正的谷歌眼镜. 加入计划参观 the sign up page 并注册访问. 一旦批准,准备好你的信用卡,等待你的杯子送到. 谷歌眼镜的Explorer版本目前售价为1美元,500 USD, 但在这款设备上市之前,价格预计会大幅下降.
由于缺乏任何仿真器, 为了开发本教程中的应用程序(或任何谷歌眼镜应用程序),你必须有实际的谷歌谷歌眼镜硬件。, but if getting one is outside your budget, 不要气馁——无论如何,这都是值得一试的. 在本教程中,你会发现为谷歌眼镜开发与为其他Android平台开发几乎是一样的!
If you still haven’t used Google Glass, but are as excited about it as I am, 看看这两个视频,它们应该给你足够的输入来理解用户界面的基础知识.
还有更多关于设置和导航的有用视频 here,以及有关用户界面的更多细节 here.
This 谷歌谷歌眼镜开发者 tutorial makes the following assumptions:
好了,我们开始吧!
你需要做的第一件事是在谷歌眼镜上启用调试模式. 你需要在每台安卓设备上做这样的事情来开发你的应用,这可能很熟悉. 要启用调试,请滑动到“”Settings” -> “Device info” and then tap to open device menu. Select “Turn on debug,它将被启用.
接下来,您需要准备好开发环境. b谷歌Glass的当前版本要求您使用API版本19, so make sure that it is installed. 此外,您需要安装您的玻璃开发工具包. 使用Android SDK管理器安装这两个包,如果你还没有这样做的话.
So let’s make our first piece of “Glassware”. (Yes, Google has coined another term! “Glassware”是在谷歌Glass上运行的任何应用程序的名称。. 我们将从开发一个老式的“Hello World”开始!” application. 像大多数主要的Android开发环境一样, Android Studio会自动用模板填充新应用来显示这句名言. As a result, getting “Hello World!“启动和运行只是基础应用部署的一个练习.
在Android Studio中,点击“New Project,并填写项目表格. You can use something similar to this:
在选择外形和API时,一定要选择“Glass” and API 19
Select “浸没式活动作为你的启动活动.
你还记得我提到过你需要学习沉浸和Live Card之间的区别? Google’s 用户界面文章 解释了不同类型的玻璃屏幕. 下面是一个简短的总结:
Live cards 被添加到谷歌眼镜的时间轴上,并通过高频更新显示有关某件事的实时信息. 即使当用户与不同的卡交互时,它们也会在后台持续运行. 这允许用户在不断访问不同种类的实时信息的情况下进行多任务处理.
Immersions 完全可定制的屏幕在时间轴体验之外运行吗. 它们允许您设计自己的UI,并按照您认为合适的方式处理用户输入. 这就是我们要用的!
在向导的下一个屏幕中,将默认值保留为“Name” and “Title” and click “Finish”.
After Gradle 照顾你的依赖关系,让你的项目准备好, it’s time to put on your Glass plug it in. Now this is futuristic development!
假设你所有的Android ADB驱动程序都在适当的位置,你的眼镜被你的系统识别, you should get your Glass in your device list.
如果这是您第一次将设备连接到计算机, 你的眼镜会请求批准/建立信任. 只需轻敲你的眼镜,允许连接,你应该准备好了.
Click “Run,并将“默认APK”与“MainActivity”作为启动活动部署在“USB”设备上.
几秒钟后,你应该会在谷歌眼镜屏幕上看到如下内容:
Hurray! Your application is running on Glass! 你所要做的就是在创建应用时填写一些默认值!
因为我们没有特别指定,所以Glass会以“show demo”的名称显示你的应用。. 如果你滑回开始屏幕,然后点击打开应用程序菜单, you will see it listed like this:
Ok, 你让它运转起来, 但这看起来不像是真正的谷歌眼镜应用, 你不希望应用程序以“Show demo”开头.
在本教程中,我们将稍微调整一下以获得真实的感觉.
First, 你可不想让这个丑陋的标题栏“Hello World Immersion”占据你小小的谷歌眼镜屏幕, 你肯定不希望你的屏幕是灰色和黑色字体. 为了解决这个问题,我们只需要在Android上切换主题,让谷歌眼镜操作系统来处理它.
Open res/values/styles.xml
for editing. It should have the following content:
Just change android:Theme.Holo.Light
to android:Theme.DeviceDefault
. 这将自动处理应用程序的布局和颜色,使用Glass的默认主题.
Ok, 在这个谷歌眼镜开发教程中,我们要做的下一件事是设置我们的应用程序,使其具有合适的名称和良好的语音控制启动. 打开你的Android Manifest (AndroidManifest.xml
),并在上面添加以下内容
你想用的原因 DEVELOPMENT
权限是这样你就可以玩自定义语音控制. 谷歌对于谷歌眼镜应用中允许使用的语音命令非常严格, and all new commands must be approved. 由于本教程是用于学习目的,您不会将此应用程序提交给官方Glassware商店, 你不必为此担心. Just turn on DEVELOPMENT
权限,您将有权访问“未列出的语音命令”. For more information about this please read this GDK page.
Open voice_trigger.xml
for editing. 这就是定义启动应用程序的语音命令的地方. 它应该位于 res/xml/
folder. You should get content similar to this:
与其说" Show me a demo "来启动应用,不如直接说应用的名字. Change the contents of the file to:
如果你回到你的清单文件,你可能会注意到你的 android: label = " @string / app_name”
是否已自动更新为也使用资源字符串 @string/app_name
而不是硬编码 Hello Glass
价值和以前一样. 如果此设置未更新,请确保将值设置为 android: label = " @string / app_name”
.
And what is your app name, exactly? If you open res /价值/字符串.xml
, your app_name
应列为:
Hello Glass
这应该为您的第一个Hello Glass应用程序收尾了. 现在让我们看看它是如何工作的!
From your Start screen, you can say “ok glass” 打开语音菜单. 您的应用程序现在在声控命令列表中.
在本教程的指导下,这就是你的谷歌眼镜应用程序开始屏幕现在的样子.
If you say “Hello glass你的应用程序应该启动,你应该得到一个标准化的谷歌眼镜体验:
如果您不想使用您的声音来激活应用程序, 你可以点击你的开始屏幕,你会看到你的应用程序在菜单中可用:
密切关注应用程序的界面以及用户与应用程序的交互是非常重要的. 记住,您的用户并不总是能够使用他们的声音——例如, while watching a lecture or presentation. 另一种情况是,他们可能忙得不可开交,无法使用触觉. 我建议你尽可能提供触摸和语音菜单交互, 允许用户同时使用语音和触摸板浏览应用程序.
现在您已经熟悉了Glass的开发, 你创造了Hello Glass, 现在是时候创建一个真正的应用程序,让谷歌眼镜的新功能发挥作用了. 让我们构建一个应用程序,让您浏览基于开发平台的顶级Toptal开发人员的个人资料.
我们的示例Glass应用程序的结构很简单:
让我们快速回顾一下你已经添加的Android知识:
利用这些知识,让你的新应用程序启动并运行. 你可以从上面更新Hello Glass应用, 或者按照同样的步骤启动一个新的应用程序. 将此应用程序命名为“Top Finder”, and make your voice_trigger.xml
文件看起来像这样.
The network="true"
约束告诉glass在启动该应用程序时检查网络连接, 我们将需要连接到Toptal开发人员列表. 如果没有连接,谷歌眼镜将显示一条警告信息.
让我们让我们的应用程序的主屏幕看起来像这样:
When you see the “ok glass” 消息在你的屏幕上,这意味着应用程序有声控菜单在那个地方. 说出这个短语 “ok glass” 这里激活了这个位置的语音菜单. 这个短语是由Glass预先定义的,你不能更改它.
You can think of “ok glass” 作为“应用程序菜单”, 就像你在智能手机/平板电脑开发中使用的那样, and it has exactly the same role. 就像你“点击”屏幕上的“应用程序菜单图标”(通常是3个点或线)来打开Android应用程序菜单一样, you need to say “ok glass” 打开Glassware应用程序中的声控菜单.
为了使 “ok glass” 您需要请求的菜单 FEATURE_VOICE_COMMANDS
from the API. To do this, add the following line in your onCreate
handler in your MainActivity
:
getWindow().requestFeature (WindowUtils.FEATURE_VOICE_COMMANDS);
包含此特性的每个活动都将使用 “ok glass” 文本位于底部中心.
接下来需要做的是为主屏幕创建一个菜单. In your res/menu
文件夹创建一个新的XML菜单定义,命名为 main.xml
. 为了简单起见,我们只启用三个Toptal开发者平台, but feel free to make the changes as you like.
It should have the following content:
你可能想知道为什么我选择了相当长的菜单标题,而不是简单的Android, JavaScript和iOS. 原因很简单. 谷歌眼镜开发团队的人仍在改进语音识别. 建议在菜单中使用两到三个单词,这样谷歌眼镜更容易识别它们.
我已经提到过 “ok glass” 菜单与标准的Android应用程序菜单没有什么不同. 将菜单附加到活动实际上是相同的. Just override the onCreatePanelMenu
handler in your MainActivity
, and inflate the main menu you just created:
@Override
onCreatePanelMenu(int featureId, Menu菜单){
if (featureId == windowstils . if.FEATURE_VOICE_COMMANDS || featureId ==窗口.FEATURE_OPTIONS_PANEL) {
getMenuInflater ().inflate(R.menu.main, menu);
return true;
}
return super.onCreatePanelMenu(featureId, menu);
}
Now we need to add a menu handler. 在此之前,创建一个名为 findDevelopers
. 稍后我们将回到这一点,开始搜索并显示结果. After that you can override your menu handler.
public void findDevelopers(String platform){
}
@Override
公共布尔onMenuItemSelected(int featureId, MenuItem item) {
if (featureId == windowstils . if.FEATURE_VOICE_COMMANDS || featureId ==窗口.FEATURE_OPTIONS_PANEL) {
switch (item.getItemId()) {
case R.id.find_android:
findDevelopers(“Android”);
break;
case R.id.find_javascript:
findDevelopers(“Java脚本”);
break;
case R.id.find_ios:
findDevelopers (iOS);
break;
}
return true;
}
return super.onMenuItemSelected(featureId, item);
}
现在是时候让我们的谷歌Glass应用程序的主屏幕更漂亮了. Import a Toptal logo into your application as res/drawable/logo.png
. 我用了这张图:
In your MainActivity
class, make the following changes.
确保在类开始时声明以下私有变量:
private CardScrollView mCardScroller;
私有视图mView;
private GestureDetector mGestureDetector;
Change the buildView
method to customize the Card layout:
私有视图buildView() {
Card = new Card(this);
card.setText(R.string.app_name);
card.setImageLayout(卡片.ImageLayout.LEFT);
card.addImage(R.drawable.logo);
return card.getView();
}
And change your onCreate
Handler应该是这样的:
protected void onCreate(Bundle bundle) {
super.onCreate(包);
getWindow().addFlags (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().requestFeature (WindowUtils.FEATURE_VOICE_COMMANDS);
mView = buildView();
mCardScroller = new CardScrollView(this);
mCardScroller.setAdapter(new CardScrollAdapter() {
@Override
int getCount() {
return 1;
}
@Override
public Object getItem(int position) {
return mView;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return mView;
}
@Override
public int getPosition(Object item) {
if (mView.equals(item)) {
return 0;
}
返回AdapterView.INVALID_POSITION;
}
});
//处理TAP事件.
mCardScroller.setOnItemClickListener(新 AdapterView.OnItemClickListener () {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
openOptionsMenu ();
}
});
mGestureDetector = createGestureDetector(this);
setContentView (mCardScroller);
}
就像我之前说的,我们想要包含点击激活菜单 “ok glass”,所以只要启用手势,就像你在安卓应用中那样. Add the following methods in your MainActivity
class:
private GestureDetector(创建GestureDetector) {
GestureDetector = new GestureDetector(context);
//Create a base listener for generic gestures
gestureDetector.setBaseListener( new GestureDetector.BaseListener() {
@Override
public boolean onGesture(姿态手势) {
if (gesture ==手势).TAP) {
openOptionsMenu ();
return true;
} else if (gesture == gesture.TWO_TAP) {
// do something on two finger tap
return true;
} else if (gesture == gesture.SWIPE_RIGHT) {
// do something on right (forward) swipe
return true;
} else if (gesture == gesture.SWIPE_LEFT) {
// do something on left (backwards) swipe
return true;
} else if (gesture == gesture.SWIPE_DOWN){
finish();
}
return false;
}
});
gestureDetector.setFingerListener(new GestureDetector.FingerListener () {
@Override
onFingerCountChanged(int preouscount, int currentCount) {
// do something on finger count changes
}
});
gestureDetector.setScrollListener(new GestureDetector.ScrollListener () {
@Override
公共布尔onScroll(浮动位移,浮动增量,浮动速度){
//在滚动时做点什么
return true;
}
});
返回gestureDetector;
}
@Override
onGenericMotionEvent(MotionEvent事件){
如果(mGestureDetector != null) {
返回mGestureDetector.onMotionEvent(事件);
}
return false;
}
应该是这样! 现在你可以启动你的应用程序,并尝试两种菜单激活方法. If you say “ok glass” you get three menu items displayed on screen, 如果你轻敲玻璃, a scrollable menu will be opened. 要浏览菜单项,您可以前后滑动.
Here’s what the voice menu looks like:
这是手势菜单:
如果您选择一个菜单项,将不会发生任何事情 findDevelopers
方法尚未实现.
我们将继续使用默认的Glass Card布局, 左边有一张图片, text on the right, 还有一些页脚信息. 有关设计卡片的最佳实践的更多信息,请参阅谷歌Glass style guide.
我们的开发者配置文件由简单的属性定义:
我们要确保app中有适当的类结构. 创建一个名为 DeveloperModel.java
in your java/models
folder. 我们希望这门课是 serializable, 因为它将包含一个列表中的配置文件.
DeveloperModel实现Serializable {
私有字符串名称;
公共字符串getName(){
return name;
}
public void setName(String name){
this.name=name;
}
私有字符串平台;
公共字符串getPlatform(){
return platform;
}
public void setPlatform(String platform){
this.平台=平台;
}
私有字符串图像;
公共字符串getImage(){
return image;
}
public void setImage(String image){
this.image=image;
}
}
我们希望我们的卡与我们的开发人员配置文件数据紧密绑定. 因为默认值 CardScrollAdapter
它的数据模型是否有点通用,我们需要扩展它并使其成为我们自己的. Create DeveloperAdapter.java
in your java/adapters
folder:
公共类DeveloperAdapter扩展CardScrollAdapter
private List mCards;
private List mData;
public DeveloperAdapter(List cards){
this.mCards = cards;
}
@Override
int getCount() {
return mCards.size();
}
@Override
gettitem (int i) {
return mCards.get(i);
}
@Override
public View getView(int i, View group, ViewGroup) {
return mCards.get(i).getView();
}
@Override
public int getPosition(Object o) {
return this.mCards.indexOf(o);
}
}
我们不希望我们的搜索结果被添加到应用程序的主屏幕, 所以我们将创建一个新的Activity来进行搜索和显示结果. 创建一个新活动, ResultsActivity
, next to your MainActivity
(probably in java/com.helloglass
).
Make sure that it extends Activity
.
接下来,我们需要为开发人员配置文件卡指定一个菜单. 创建一个新菜单, developer.xml
,内容如下:
To enable passing parameters between ResultsActivity
and MainActivity
add the following lines at the start of ResultsActivity
class:
public static final String SEARCH = "search";
private String mPlatform="Android";
确保将新活动添加到清单文件中:
Setting up the initial screen of ResultsActivity
卡牌的配置和我们之前做的很相似 MainActivity
. 首先检查在开始时是否定义了卡片和卷轴:
private CardScrollView mCardScroller;
private List mCards;
private GestureDetector mGestureDetector;
创建一个临时搜索方法,我们将在后面实现它. 向配置文件列表中添加新卡片就像向数组中添加项目一样简单. 我们将命名这个方法 findDevelopers
同样,但是这个属于 ResultsActivity
:
private void findDevelopers(String platform){
for (int i=1; i<=10; i++){
Card = new Card(this);
card.setText(平台+ " " +整数.toString(i));
card.setTimestamp(平台);
card.setImageLayout(卡片.ImageLayout.LEFT);
card.addImage(R.drawable.ic_person_50);
mCards.add(card);
}
mCardScroller.setSelection(0);
}
现在回到你的 MainActivity
and update findDevelopers
从这里开始 ResultsActivity
and pass in the platform
property:
public void findDevelopers(String platform){
Intent resultsIntent = new Intent(this, ResultsActivity).class);
resultsIntent.putExtra (ResultsActivity.搜索、平台);
startActivity (resultsIntent);
}
将开发人员菜单附加到 ResultsActivity
. 您将能够在任何配置文件卡上打开菜单.
@Override
onCreatePanelMenu(int featureId, Menu菜单){
if (featureId == windowstils . if.FEATURE_VOICE_COMMANDS || featureId ==窗口.FEATURE_OPTIONS_PANEL) {
getMenuInflater ().inflate(R.menu.开发人员,菜单);
return true;
}
return super.onCreatePanelMenu(featureId, menu);
}
就像之前一样,在谷歌眼镜上启用手势来操作触摸板 ResultsActivity
is displayed. 要做到这一点,只需打电话 openOptionsMenu()
in your onGesture(姿态手势)
method:
private GestureDetector(创建GestureDetector) {
// …
@Override
public boolean onGesture(姿态手势) {
if (gesture ==手势).TAP) {
openOptionsMenu ();
return true;
} else if
// …
另外,向开发人员相关操作添加菜单处理程序. We will leave simple Toast messages for now.
@Override
公共布尔onMenuItemSelected(int featureId, MenuItem item) {
if (featureId == windowstils . if.FEATURE_VOICE_COMMANDS || featureId ==窗口.FEATURE_OPTIONS_PANEL) {
switch (item.getItemId()) {
case R.id.developer_fav:
Toast.makeText(getApplicationContext()), "Favorite", Toast.LENGTH_LONG).show();
break;
case R.id.developer_hire:
Toast.makeText(getApplicationContext()), "Message", Toast.LENGTH_LONG).show();
break;
case R.id.go_back:
break;
}
return true;
}
return super.onMenuItemSelected(featureId, item);
}
每个应用程序都应该使用一些漂亮的视觉元素,图标等. 谷歌谷歌眼镜团队提供了大量通用的标准化图标,可供谷歌眼镜开发者在他们的应用程序中免费使用. 你可以找到一整套标准的Glass图标和字体 in their library
现在,你只需要一个图标 ic_person_50.png
, so go ahead and download it to your res\drawable
folder. 我们将使用这个图标,而不是下载开发人员的图片.
现在在我们的谷歌眼镜应用开发教程中剩下的最后一件事是重写我们的 onCreate
handler on ResultsActivity
,我们将检查从哪个开发平台转发 MainActivity
填充我们的列表.
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(包);
getWindow().addFlags (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().requestFeature (WindowUtils.FEATURE_VOICE_COMMANDS);
mCardScroller = new CardScrollView(this);
mCards = new ArrayList();
if(getIntent().hasExtra(搜索)){
mPlatform = getIntent().getStringExtra(搜索);
}
findDevelopers (mPlatform);
mCardScroller.setAdapter(new DeveloperAdapter(mCards));
//处理TAP事件.
mCardScroller.setOnItemClickListener(新
AdapterView.OnItemClickListener () {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
openOptionsMenu ();
}
});
mGestureDetector = createGestureDetector(this);
setContentView (mCardScroller);
}
You can leave onResume
and onPause
方法,与 MainActivity
.
If you start your application now, 您可以检查您的开发人员配置文件是如何基于所选菜单动态创建的 MainActivity
. 同样,您可以使用一个选项来显示您的菜单 “ok glass” 或者通过点击触摸板或使用语音激活. 以下是“第10位Android开发者”的简介:
Tapping brings up the touch menu:
And saying “ok glass” 调出语音菜单:
从列表中向下滑动以返回应用程序的主屏幕.
总结一下, 让我们用Toptal排名前10的开发者的真实信息填充菜单 JavaScript, Android, and iOS.
您需要下载他们的个人资料图片,并通过HTTP提供, 或者直接使用url tzyn.net.
因为构建一个网络爬虫只是为了获取Toptal中顶级开发人员的名字,对于本文来说可能太绕弯子了, I’ve created JSON files for you to use for Android, JavaScript, and iOS.
在你的应用中,你需要做的第一件事就是请求从你的Android操作系统访问互联网. Add the following line to your Manifest file
注意,Glass不允许你通过直接使用HTTP请求来阻塞主线程. 您将需要以异步方式处理JSON下载和单个图像. 您可以使用异步任务, create your own download service or intent, or whatever you prefer in your everyday work.
构建这个功能并不是谷歌Glass所特有的,所以我将跳过代码片段. 如果你勇往直前,让这最后一点功能正常工作, your profile cards should look like this:
我真的希望你在这个谷歌Glass开发教程和构建你的第一个Glassware应用程序中玩得很开心. By now, 在谷歌眼镜上编写应用程序与在其他Android平台上编写应用程序并没有太大的不同,你应该对此感到放心.
At this point, 你已经学会了如何扩展谷歌Glass语音激活的主屏幕, how to create your own 声控菜单, 以及如何将语音控制与触摸手势结合起来. 您还应该了解Glass UI的概念和基本构建块, such as cards, layouts, and elements. 您已经看到了如何动态地创建卡片以及如何在不同的活动之间导航.
要深入了解,请访问b谷歌的开发人员资源 developers.google.com/glass. 当您开始构建更复杂的应用程序时,它将被证明是一个非常有用的资源.
请注意,谷歌眼镜仍处于开发阶段,在进入消费者市场之前,可能会有更多的增强功能. 考虑到这一点,我有一个重要的提示给你:
语音识别还有相当多的工作要做,当你试图开始你的活动或填写一些信息时,你可能会发现自己对你想象中的朋友大喊大叫. 不用担心——每个尝试谷歌Glass开发的人都有同样的感觉,所以你并不孤单.
这项技术将得到改进,玻璃很快就会上市销售. 一旦上市,它肯定会引起巨大的轰动, 所以我希望你们和我一样兴奋,因为我是第一个走在这项激动人心的技术前沿的人!
Shout Out: Screenshots in this article are made using Droid@Screen.
Located in 萨拉热窝,波斯尼亚-黑塞哥维那联邦,波斯尼亚-黑塞哥维那
Member since July 8, 2014
Demir是一名开发人员和项目经理,在广泛的软件开发角色方面拥有超过15年的专业经验.
24
World-class articles, delivered weekly.
World-class articles, delivered weekly.
Join the Toptal® community.