前言

这是“移动应用开发”这门课的大作业。用了Spotify Android SDK(后端,调取音乐数据)和figma(设计前端)。


2023.12.22
说实话写得很累,写前端卡前端,写后端卡后端。所用的工具软件、看的文档又是全英文的,在一个期末八门大作业(其中三门开发岗项目、一门python大作业,两份课设、两篇论文)要在短短几周内(甚至到现在只剩几天了)写完的情况下,对于小白来说,不仅考验耐心,更考验效率。出现bug的时候更加不容易像平常那样不急不躁地debug,翻遍全网博客每次都是会看到不少同样的或类似的问题,但鲜少能很快地找到解决方案。

在写这次大作业的时候才发现,之前一直心心念念的论坛,原来一般在开发者平台都能找到(他们若是有把部分项目或文档放到git仓库,可能git仓库底下也有不少能借鉴的Issues),为方便程序员开发,会给程序员建个专属论坛。以往我搜着搜着基本只能搜到stackflow,但大多情况搜到的都是尚未解决的问题,每次见到类似的报错都会很激动,点进去发现一个回答都没有,心又瞬间沉入谷底。

光是调用Spotify Android SDK做到能打开app点击按钮播放一首设定好的歌曲我就挣扎了两个晚上,第一个晚上我搜了国内的那些音乐app大厂,只有网易云有开发者平台,我喜出望外地填写认证信息,填到最后我傻眼了,因为我那时才发现原来那个开发者平台是给网易云音乐内部的程序员用的。其他的大厂,包括抖音也有个音乐开放平台,但进去都是音乐人合作,或者企业合作。

之后又搜罗资源,终于找到了spotify,但就那么一点点的入门文档,我就研究了两个晚上,现在才大致明白逻辑,并成功实现了后台播放。我觉得主要问题是版本吧,Android Studio真得改版太多了,其他的平台和它关联起来也需要不断地维护文档,但凡有一处没更新,对于我这样的小白来讲就真得很难处理问题。今天才懂如何去翻各种官方文档(原来不仅敲代码需要熟练度,看文档也需要熟练度啊)。

目前spotify建了框架,尝试了playlist、track、album,都成功了。我想,开发简易的music player的话,光是这三种也差不多够用了,若是想开发复杂的,估计得看后期的时间是否充裕。

再讲前端,将figma的文件包导入到Android Studio里,目前尝试过的插件(plugin)有俩,一个是Relay,一个是Dhiwise,但都失败了,接下来会重新再尝试,或者在线生成xml,最不济就只能导出图片然后加透明控件了。

比较并总结一下Dhiwise和在线生成xml的优缺点(虽然目前Dhiwise没试成功):

  • Dhiwise可以生成一整个packages,也就是项目包,里边会有图片、字体等各种丰富地资源,基本是能够完美还原源项目了。但是正是因为有这么多丰富的资源,包括有不同的dpi文件夹(这就意味着能适应各种dpi的手机),因此包很大,引入的依赖也很多,导入的时候很麻烦。且引入的是一整个新的前端包,之前写的后端内容,要重新手动cccv进去(我是小白,我只会手动,或许有什么其他的方法吧)。不过Dhiwise好像除了xml还有一个选项,不知道那个是什么用处,原谅一下我这位小白的无知。

  • 在线生成xml的方法,只能是每一个小模块生成一个xml,并且对应的drawable下的xml也需要自己手动创建、复制粘贴,一个稍微好看点的前端界面就有特别特别多的模块,而且稍微复杂点它们的位置就很难调节了,相当得麻烦。

之后的日子再试试看吧,看看能否调出bug或者最终妥协,用“瞎猫”去“捉老鼠”。

真是一个痛苦、难忘,但又充满着很多新鲜感和激情的期末啊!


git仓库

目前还没release,还在创建状态,网址为https://github.com/ETOLucy/Music_app_spotify

前端开发

从数媒的高中同学那得知有figma这个东西,在国内的镜像版本叫做即使设计。于是就用起了figma。

#TODO

后端开发

Spotify文档讲解

网址

安卓开发网址:https://developer.spotify.com/documentation/android
论坛网址:https://community.spotify.com/t5/Spotify-for-Developers/bd-p/Spotify_Developer

创建应用(以实现集成该SDK)

  • 先解释下,web端叫做“调取api”,对应的,Android叫做“集成SDK”。
  1. 进入Dashboard,创建应用。参考文档创建应用,这里要解释一下,新手可能会很好奇,明明我用的是SDK为什么跳转到web端去了,这个可以自己在Android的开发文档里仔细找,发现它就是有外链的,它给的外链就是我给的这个网址,我放这是为让读者切过去更方便些。
    这里的client id是它自动生成,重定向URI按照文档说的设置好(这里我也不是太懂我设置的uri是否在工作),安卓开发要填Android Package,ios开发填Bundle ID。安卓开发的Android Package可以从任意一个activity的java代码第一行里找,比如package com.example.music_app_spotify;则Android Package为com.example.music_app_spotify

    对了,这里要说明一下,添加这些参数的时候,在输入框的下方会看到很明显的紫色“add”按钮,上面会有很不明显的“remove”按钮,记得一定要点击“add”按钮后再点击“save”按钮!!!
    指纹的话,按照文档来操作吧。

  2. 现在Android Studio是2023版了(现在都快2024年了),所以就连将库作为模块导入到项目都变了样。
  • 首先文档中导入方式的截图就和我们现在的Android Studio是不适配的,文档中的截图是new一个module,就可以选择“导入 .JAR/AAR 包”选项,但我们的方式,一种是点击左上角File,然后找到“Prject Structuer”,在左边的菜单栏选择“Dependencies”,在这里添加;另一种是在build.gradle.kts里(一般在文件最下边)dependencies{}里边添加implementation(files("D:\\Android\\AndroidStudioProjects\\Music_app_spotify\\app\\libs\\spotify-app-remote-release-0.8.0.aar"))(路径依据你自己的来,记住别放在你定期清理的位置,放这个项目的libs下最好)。
  1. 在文档教程底下的授权那一篇里,文档提供的代码是:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    repositories {
    mavenCentral()
    }
    dependencies {
    implementation 'com.spotify.android:auth:1.2.5' // Maven dependency

    // All other dependencies for your app should also be here:
    implementation 'androidx.browser:browser:1.0.0'
    implementation "androidx.appcompat:appcompat:$appCompatVersion"
    }

    而我的代码是(包含一些这份文档其他地方的要求的一些依赖):
    1
    2
    3
    4
    5
    6
    7
    implementation("com.spotify.android:auth:1.2.6")
    implementation ("androidx.browser:browser:1.6.0")
    implementation("androidx.appcompat:appcompat")
    implementation("com.google.android.material:material:1.8.0")
    implementation(files("D:\\Android\\AndroidStudioProjects\\Music_app_spotify\\app\\libs\\spotify-app-remote-release-0.8.0.aar"))
    implementation ("com.google.code.gson:gson:2.10.1")
    implementation("com.google.android.gms:play-services-auth:20.7.0")
    文档这里的代码我几乎是加一行报错千万行,然后debug很长很长时间……
  • 首先是
    1
    2
    3
    repositories {
    mavenCentral()
    }
    当我把这个添加进去的时候,它显示和自带的maven库冲突,我就把它删掉了,也能跑maven。
    然后其他的一些,在Maven Repository里找到最新版引入的话,会显示Mainfest Merger报错,太老了也不对,就各种调吧,反正目前我这样的版本是可以使用的。

授权登录之Activity的start与finish

关于授权,在LoginActivity里和MainActivity里写这部分代码,问了gpt,但是它给的代码差一点(那一点很致命,我只好自己调出那一点)。
看我在LoginActivity.java里的成品的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);

// Check if the result comes from the correct activity
if (requestCode == REQUEST_CODE) {
AuthorizationResponse response = AuthorizationClient.getResponse(resultCode, intent);

switch (((AuthorizationResponse) response).getType()) {
// Response was successful and contains auth token
case TOKEN:
// Handle successful response, for example, navigate to MainActivity
// Intent Main_intent = new Intent(this, MainActivity2.class);
// String message = "ok";
// Intent intent1 = intent.putExtra("if_login_in", message);
// startActivity(Main_intent);
finish(); // finish the LoginActivity
break;

// Auth flow returned an error
case ERROR:
// Handle error response
break;

// Most likely auth flow was cancelled
default:
// Handle other cases
}
}
}

以及在MainActivity.java里的成品的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Get the Intent that started this activity and extract the string
// Intent intent = getIntent();
// String message = intent.getStringExtra("if_login_in");
// if(message.equals("ok"))return;
// Check if the user is already logged in, based on your app's logic
if (userIsLoggedIn()) {
// User is logged in, proceed with your main app logic

} else {
// User is not logged in, navigate to LoginActivity
startActivity(new Intent(this, LoginActivity.class));
// finish(); // finish the MainActivity
}

// 找到播放按钮
Button playButton = findViewById(R.id.playButton);

// 设置按钮的点击事件
playButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 在这里处理按钮点击事件,可以添加播放音乐的逻辑
playMusic();
}
});
}

可以观察一下我注释掉的部分。一开始运行的时候,是一直显示“正在加载…”,加载了之后又立刻弹出来又“正在加载…”,我以为是授权的地方哪里出错了,找了好久bug也没找到。
后来发现MainActivity.java里if (userIsLoggedIn())底下没有任何实现,当时也没观察LoginActivity里是有start一个新的activity的(gpt给的代码是start MainActivity,我注释部分是我自己动过,也拿其他页面尝试过没成功),以为登陆进去啥操作也没有了,就开始在那加东西捣鼓,结果印象中是没啥反应(逻辑上应该也成立,因为Login那start一个新的Main页面后,main又授权登录,跳到login那,login再start一个新的main页面,因此死循环卡死了),于是又焦虑半天。
再之后,发现login那有写start一个新的main页面,然后开始修改,发现运行结果有变化,这才意识到授权那边的代码没写错,理论上讲,登录应该成功了,出错应该是出在activity的实现上。
我现在成品代码的逻辑是,授权成功后关闭login,这样就回到了main页面。

button之play music & stop music

首先别忘记在xml页面创建按钮。

先观察下我成品代码中的Onstart函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

@Override
protected void onStart() {
super.onStart();
// ConnectionParams connectionParams =
// new ConnectionParams.Builder(CLIENT_ID)
// .setRedirectUri(REDIRECT_URI)
// .showAuthView(true)
// .build();
//
// SpotifyAppRemote.connect(this, connectionParams,
// new Connector.ConnectionListener() {
//
// public void onConnected(SpotifyAppRemote spotifyAppRemote) {
// mSpotifyAppRemote = spotifyAppRemote;
// Log.d("MyActivity", "Connected! Yay!");
//
// // Now you can start interacting with App Remote
// connected();
//
// }
//
// public void onFailure(Throwable throwable) {
// Log.e("MyActivity", throwable.getMessage(), throwable);
//
// // Something went wrong when attempting to connect! Handle errors here
// }
// });
}

观察一下我注释掉的代码里,有一句connected();,这里的connected函数相当于一个工具,就让它的那么摆着吧,丢到button的onClick里边反而不方便。至于其他部分,会发现它在进行连接,然后调取connected函数,执行mSpotifyAppRemote.getPlayerApi().play("spotify:track:4lIsrJyQOdtBtRAyYiY2VD");这句话的时候,就是播放具体的歌曲了。
所以把onStart()里除super.onStart();(这个是Onstart()函数存在它就必须得存在的东西,拿走不要命啦)全部拖到OnClick()下(我是在把它们封装在了playMusic函数里,再在OnClick里调用playMusic函数),如此便能实现运行app就后台播放音乐了。

如何stop music?

至于如何stop music,就再添加一个按钮,但是不能把onStop()下的SpotifyAppRemote.disconnect(mSpotifyAppRemote);放到按钮的OnClick下,这个是断开连接,而不是关闭,要这么写:

1
2
3
4
5
// 在这里处理按钮点击事件,停止音乐播放
if (mSpotifyAppRemote != null && mSpotifyAppRemote.isConnected()) {
// 如果 SpotifyAppRemote 已连接,则停止音乐播放
mSpotifyAppRemote.getPlayerApi().pause();
}

记住!要加上这个if判断条件,不然的话点击停止按钮就会退出app!

关于 “spotify:track:XXXXXX” 怎么用

这里其实很简单,我目前发现的有三种,一种是track,一种是album,一种是playlist。

注:查阅了下官方文档,一共四种,还有一个artist。
如果是album就”spotify:album:XXXXXX”,以此类推,替换两个冒号中间那个单词就行。
至于每个歌曲的”XXXXXXXXXXXXXXXXXX”怎么知道是多少呢?这个其实跟,问到底是专辑(album)还是歌单(playList)还是具体去搜索某首歌(track)一样,去spotify官网,比如我想找邓紫棋唱的面壁者,我搜到这首歌,它的网址是https://open.spotify.com/track/1tCPlIIWX89HBVRhBzc9O1,那么track就是两个冒号间要填的,1tCPlIIWX89HBVRhBzc9O1便是”XXXXXXXXX”。