這篇要來寫寫關於Android開發中使用GraphQL連線請求

在寫程式的過程中,蠻多人應該已經對於串接網路http API這件事...沒用過至少也聽過吧XD

那今天文章~~寫點比較特別的

首先,我想問問各位

請問你聽過安麗嗎

欸不是!!並不是!!ಠ_ಠ

請問你聽過GraphQL嗎?

 

GraphQL是什麼?

 

根據維基百科...

GraphQL是一個開源的,面向API而創造出來的資料查詢操作語言以及相應的執行環境

於2012年仍處於Facebook內部開發階段,直到2015年才公開發布

2018年11月7日,Facebook將GraphQL專案轉移到新成立的GraphQL基金會(隸屬於非營利性的Linux基金會)

 

用白話文講,他是一種API傳輸方式

有別於傳統的Http REST 傳輸協定,他是一種可以自由選擇需要的資料,並能以最低資料量達到最高傳輸效果的傳輸方式

恩?你說你還是看不懂?好吧,我們來看看官網以及稍後的範例就能窺知一二了

 

GraphQL官方網站

英文版->https://graphql.org/

中文版->https://graphql.bootcss.com/

 

那麼來到今天的主題,本篇我將在Android上使用GraphQL傳輸,來獲取Matters馬特市的一些資料

關於馬特市Matters,我在這篇有寫,大家可以去看看

碼農日常-『LikeCoin』讚賞LikeCoin是什麼?我的LikeCoin要如何兌現呢?

 

來看看Gif範例吧

 

以及Github

->https://github.com/thumbb13555/GraphQLDemo/tree/master


 

1. 關於Matters 開源API

 

Matters是一個部落格平台,上面每天都有很多文章產出

但是跟傳統部落格不一樣的是,Matters除了原始碼開放之外,他也有官方的API可以用!

那麼,就來一探究竟吧

 

首先,官方提供的API Github在此

->https://github.com/thematters/developer-resource

 

然後,這裡是GraphQL的Playground

->https://server.matters.news/playground

 

再來,我們來看一下Playground,玩玩看他的API吧!

首先是介面

截圖 2021-07-24 下午5.47.08

左邊的部分是輸入GraphQL指令,右邊則是輸出結果

此外,他還有文檔可以看,非常清楚

截圖 2021-07-24 下午5.48.54

截圖 2021-07-24 下午5.51.34

截圖 2021-07-24 下午5.52.29

左下角部分,還有Query Variables跟Http Headers可以用(這裡要往上拉一下..)

那麼,讓我們來輸入看看今天的API吧,請輸入以下內容

query GetRecommend($cursor:String!) {
  viewer {
    recommendation {
      newest(input: { after:$cursor }) {
        totalCount
        edges {
          node {
            title
            id
          }
        }
      }
    }
  }
}

 

並在下方Query Variables輸入以下

截圖 2021-07-24 下午6.13.48

 

最後,按下中間的截圖 2021-07-24 下午5.57.26

看看結果

截圖 2021-07-24 下午5.58.11

恩~如果你真的跟著我打的話,這些資料是他們Matters團隊用於測試的資料

如果想看到真實資料,請把這裡的網址改為https://server.matters.news/graphql就可以了

截圖 2021-07-24 下午6.01.36

截圖 2021-07-24 下午6.02.22

 

好的,會操作GraphQL的Playground之後,那接下來就要進入正題囉~

 


 

2. 載入GraphQL Plugs以及Apollo第三方資源庫

 

這一段開始會稍微比較複雜一點,每個步驟可都要看清楚啦~

在Android中,由於GraphQL的結構較為特殊,所以我在這邊也是建議使用Apollo這個第三方函式庫去完成

而Apollo函式庫他其實也是基於okHttp再加以修改,所以在做傳輸時會發現跟okHttp的寫法非常接近

我的網誌之前也有okHttp的資料,可以參考一下

->碼農日常-『Android studio』以okHttp第三方庫取得網路資料(POST、GET、WebSocket)

 

2-1 載入Apollo第三方函式庫

那麼,我們開始載入Apollo函式庫

這個Apollo函式庫算是很貼心,他還直接告訴你Java和Kotlin怎麼寫

在這->https://www.apollographql.com/docs/android/essentials/get-started-java/

以及這是Apollo的Github

->https://github.com/apollographql/apollo-android

 

...好,我知道你看官網大概霧煞煞

看我的可能比較快XD

首先,我們開啟好專案後,來到

截圖 2021-07-24 下午11.11.43

點build.gradle

然後加入以下內容

plugins {
    id 'com.android.application'
    id("com.apollographql.apollo").version("2.5.9")
}

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.noahliu.graphqldemo"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    implementation 'com.apollographql.apollo:apollo-runtime:2.5.9'
}

 

這邊補充一下,如果你是用Kotlin撰寫的話

那要加的如下

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id("com.apollographql.apollo").version("2.5.9")
}

android {
    compileSdkVersion 30

    defaultConfig {
        applicationId "com.example.apollotry2"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    implementation("com.apollographql.apollo:apollo-runtime:2.5.9")
}
apollo {
    generateKotlinModels.set(true)
}

 

一樣,按下Sync Now,編譯過就是ok囉!

至於如果有編譯不過的話...應該是沒有啦QQ

我一開始一直編譯不過,後來我改成目前這樣才過的

實測是沒問題的啦!

 

2-2 插入JS GraphQL Plugins

接下來,由於Android studio IDE本身沒有支援.graphql檔案格式

因此我們要插入Plugs,讓他支援

首先點Android Studio->Preferences

截圖 2021-07-23 下午8.44.32

 

找到Plugins,按下Install

截圖 2021-07-23 下午8.45.19

 

ok,到這邊就完成插件了,稍後我們在創建文件的時候,就能夠正常打開囉!

 


 

3. 下載Schema.json

 

好,我們來到了最重要的一個步驟了

GraphQL他之所以可以在Playground顯示,是因為他背後有一個Schema檔案在撐腰

也因此我們現在就要來下載這個檔案

那在這之前,我們必須要創建專屬於GraphQL的資料夾

準備好了嗎?前面的步驟要先過編譯才能接下來這個步驟喔!

那,開始吧

 

3-1 創建相同路徑資料夾

首先,請先把資料夾顯示模式更換為Project

截圖 2021-07-24 下午11.49.32

 

接下來,找到main資料夾,在底下new->Directory

截圖 2021-07-23 下午8.56.51

恩~總之就是一個創建資料夾的過程

重點是要跟要跟java資料夾底下的路徑一模一樣的名字!

像這樣

截圖 2021-07-24 下午11.55.24

 

ok,接下來要來下載schema了

 

3-2 下載Schema

這裡也是研究很久的部分(嘆

我當時在下載的時候,我看的是官方的介紹

->https://www.apollographql.com/docs/android/tutorial/02-add-the-graphql-schema/

可是...我當時看了半天...看不懂啊!!!!(抱頭

所以後來我找了很多資料,好不容易找到這篇發問

->https://stackoverflow.com/questions/62483120/how-do-i-generate-the-schema-graphql-file-when-using-apollo-server

才成功處理完這個問題

那麼,我們就來下載他吧

 

首先,請到目標資料夾上面,右鍵->Open in Termial

截圖 2021-07-23 下午9.11.21

接下來就會看到這個畫面(此方法Windows通用)

截圖 2021-07-25 上午12.26.17

 

然後輸入這一串

npm install -g apollo
 

按Enter讓他跑

截圖 2021-07-25 上午12.28.15

出現這樣表示載入完成

 

此外,在Mac會有權限不足的問題

missing write access to /usr/local/lib/node_modules mac

這時候開啟終端機

輸入 sudo chown -R $USER /usr/local/lib/node_modules 後,打個密碼即可

截圖 2021-07-24 下午12.50.42

再來下載json,請接著輸入這一串

apollo client:download-schema --endpoint=https://server.matters.news/graphql schema.json
 

同樣Enter給他跑,跑好如下

截圖 2021-07-25 上午12.33.02

OK,最後檢查一下資料夾內是否多了Schema.json檔案

截圖 2021-07-24 下午12.56.14

若有的話,就是成功囉

有跑成功但是沒有顯示檔案,可以重新選許一下資料夾,有時候是他的UI沒有跑出來的緣故

稍微總結一下該輸入的內容

總之終端機的部分就是輸入以下兩串

npm install -g apollo

apollo client:download-schema --endpoint=https://server.matters.news/graphql schema.json

即可成功載入!

 


 

4. 撰寫 .graphql檔案

 

接下來要來撰寫GraphQL檔案,也就是每個REST檔案

這些檔案我的建議是都先在Playground先寫好,測試沒問題後再複製過來是比較好的作法

這次我的目標是要抓出馬特市的總用戶數,因此我在Playground測試是使用以下這串

query GetTotalUsers($cursor:String!){
  oss{
    users(input:{after:$cursor}){
      totalCount
      pageInfo{
        startCursor
        endCursor
        hasPreviousPage
        hasNextPage
      }
      edges{
          node{
          displayName
          info{
            createdAt
          }
        }
      }
    }
  }
}

 

接著,我創立一個graphql檔案,請在資料夾上方點右鍵->new->GraphQL File

截圖 2021-07-25 上午1.42.58

然後把剛剛那一串貼上去(我的檔名GetTotalUsersInMatters.graphql)

截圖 2021-07-25 上午1.45.30

會滿江紅!!

但不用擔心,這是正常現象

新增完後,請按Build->Rebuild Project

截圖 2021-07-25 上午1.47.05

 

這步驟非常、非常重要

如果API有改或有新增的情況下,做完更動後每次都要Rebuild一次,這樣程式能編譯,並找出他的檔案

 


 

5. 建立連線

 

來到最後一步驟了,寫連線程式

到這邊為止,必須確認前面都有做到定位後,這裡才能確實執行

然後...其實這部分很簡單XD而且官方網站其實有給你範例!

在這裡

->https://www.apollographql.com/docs/android/essentials/get-started-java/

那麼我照著範例,做出今天的程式

 

介面

activity_main.xml

截圖 2021-07-25 上午2.01.07

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textview_Respond"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textSize="24sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textview_Respond" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

MainActivity.java

public class MainActivity extends AppCompatActivity {
    public static final String TAG = MainActivity.class.getSimpleName() + "My";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ApolloClient apolloClient = ApolloClient.builder()
                .serverUrl("https://server.matters.news/graphql")
                .okHttpClient(
                        new OkHttpClient.Builder().addInterceptor(new AuthorizationInterceptor(""))
                                .build()
                )
                .build();
        Button btClick = findViewById(R.id.button);
        btClick.setOnClickListener(v -> {
            getMattersUserInfo(apolloClient);
        });
    }
    /***/
    private void getMattersUserInfo(ApolloClient apolloClient){
        apolloClient.query(new GetTotalUsersQuery(""))
                .enqueue(new ApolloCall.Callback<GetTotalUsersQuery.Data>() {
                    @SuppressLint("SetTextI18n")
                    @Override
                    public void onResponse(@NotNull Response<GetTotalUsersQuery.Data> response) {
                        runOnUiThread(()->{
                            TextView textView = findViewById(R.id.textview_Respond);
                            String totalCount = String.valueOf(response.getData().oss.users.totalCount);
                            textView.setText("馬特市總人口為: "+totalCount);
                        });
                    }

                    @Override
                    public void onFailure(@NotNull ApolloException e) {

                    }
                });
    }

    /**Mutate 範例*/
    private  void mutateExample(ApolloClient apolloClient){
        apolloClient.mutate(new ReadArticleMutation("QXJ0aWNsZToxNTkxOTg"))
                .enqueue(new ApolloCall.Callback<ReadArticleMutation.Data>() {
                    @Override
                    public void onResponse(@NotNull Response<ReadArticleMutation.Data> response) {
                        Log.d(TAG, "onResponse: " + response.getData().readArticle.title);
                    }

                    @Override
                    public void onFailure(@NotNull ApolloException e) {

                    }
                });
    }
    /**Query範例*/
    private void queryExample(ApolloClient apolloClient) {
        apolloClient.query(new GetRecommendQuery())
                .enqueue(new ApolloCall.Callback<GetRecommendQuery.Data>() {
                    @Override
                    public void onResponse(@NotNull Response<GetRecommendQuery.Data> response) {
                        Log.d(TAG, "onResponse: " + response.getData().viewer.recommendation.newest.totalCount);
                    }

                    @Override
                    public void onFailure(@NotNull ApolloException e) {

                    }
                });
    }
    /**新增Header*/
    private static class AuthorizationInterceptor implements Interceptor {
        private String token;

        public AuthorizationInterceptor(String token) {
            this.token = token;
        }

        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
            Request request = chain.request().newBuilder()
                    .addHeader("x-access-token", token)
                    .build();
            return chain.proceed(request);
        }
    }
}

 

好,講重點

首先,先宣告ApolloClient型別

ApolloClient apolloClient = ApolloClient.builder()
        .serverUrl("https://server.matters.news/graphql")
        .okHttpClient(
                new OkHttpClient.Builder().addInterceptor(new AuthorizationInterceptor(""))
                        .build()
        )
        .build();

再來是回傳

apolloClient.query(new GetTotalUsersQuery(""))
        .enqueue(new ApolloCall.Callback<GetTotalUsersQuery.Data>() {
            @SuppressLint("SetTextI18n")
            @Override
            public void onResponse(@NotNull Response<GetTotalUsersQuery.Data> response) {
                runOnUiThread(()->{
                    TextView textView = findViewById(R.id.textview_Respond);
                    String totalCount = String.valueOf(response.getData().oss.users.totalCount);
                    textView.setText("馬特市總人口為: "+totalCount);
                });
            }

            @Override
            public void onFailure(@NotNull ApolloException e) {

            }
        });

 

特別注意,這裡的GetTotalUsersQuery不是對應檔案名稱,是對應檔頭名稱(在我的Github程式中,我刻意寫不一樣的名字就是為了證實這點)

截圖 2021-07-25 上午2.09.25

 

OK,做到這邊,不知道你是否也成功接收到資料了呢?

 


 

這篇GraphQL老實說我真的搞很久才搞定..(哎,程度差,努鈍的我QQ)

會寫這個,真的是純粹因為好奇XD

因為這東西算是蠻新的,用的公司目前還不是很多

像Matters這樣子願意開源讓大家去撈資料,真的是只能說非常非常的感謝

當然,同時我自己也是Matters作者,也一直再以自己的方式回饋這個社群..包含今天的文章也是其中一部分

而今天的文對我來說也是得來不易,畢竟是真的在嘗試很多錯誤後得到的,也是希望大家看我的文章後可以一次完成,不要走歪路了QQ

那嘛,文章就到這邊,如果覺得我文章不錯..

TK

arrow
arrow

    碼農日常 發表在 痞客邦 留言(11) 人氣()