Android Developer Roadmap: Bắt tay vào làm quen với Apollo-Android và GraphQL

Xin chào các bạn đồng nghiệp Android, đặc biệt là những bạn đang từng bước xây dựng sự nghiệp lập trình của mình theo lộ trình mà chúng ta đã và đang cùng nhau khám phá!

Trong series “Android Developer Roadmap“, chúng ta đã đi qua rất nhiều chặng đường quan trọng, từ việc làm quen với ngôn ngữ Kotlin, thiết lập môi trường, OOP, cấu trúc dữ liệu, đến xây dựng giao diện, xử lý vòng đời, và quan trọng nhất trong bối cảnh ngày nay: làm việc với dữ liệu từ Internet.

Bài viết trước, chúng ta đã tìm hiểu về cách sử dụng Retrofit và OkHttp để tương tác với các API theo kiến trúc REST. Đây là một kỹ năng nền tảng và vô cùng phổ biến. Tuy nhiên, thế giới công nghệ luôn vận động, và GraphQL đang nổi lên như một giải pháp thay thế mạnh mẽ, mang lại sự linh hoạt và hiệu quả cao hơn trong việc truy vấn dữ liệu.

Nếu bạn đã quen với việc lấy dữ liệu từ Backend thông qua API, bài viết này sẽ mở ra một cánh cửa mới: làm thế nào để tận dụng sức mạnh của GraphQL ngay trên ứng dụng Android của bạn? Và câu trả lời chính là sử dụng thư viện Apollo-Android.

Hãy cùng nhau bắt đầu hành trình khám phá Apollo-Android và GraphQL nhé!

GraphQL là gì? Vì sao không phải là REST?

Trước khi đi sâu vào Apollo, chúng ta cần hiểu GraphQL là gì và lý do nó ra đời, đặc biệt khi so sánh với REST.

Về cơ bản, GraphQL là một ngôn ngữ truy vấn (query language) cho API và là một runtime để thực hiện các truy vấn đó bằng cách sử dụng một hệ thống kiểu dữ liệu (type system) mà bạn định nghĩa cho dữ liệu của mình.

Điểm khác biệt cốt lõi giữa GraphQL và REST nằm ở cách client (ứng dụng Android của bạn) yêu cầu dữ liệu:

  • REST: Client gửi yêu cầu đến các URL/endpoint cụ thể, mỗi endpoint thường trả về một cấu trúc dữ liệu cố định. Ví dụ: `/users` trả về danh sách người dùng, `/users/{id}` trả về thông tin chi tiết một người dùng, `/users/{id}/posts` trả về bài viết của người dùng đó. Để lấy thông tin người dùng và bài viết của họ, bạn có thể cần gọi 2 hoặc nhiều endpoint khác nhau. Điều này dẫn đến tình trạng Over-fetching (nhận nhiều dữ liệu hơn mức cần thiết) hoặc Under-fetching (cần nhiều request để lấy đủ dữ liệu).
  • GraphQL: Client gửi một yêu cầu duy nhất đến một endpoint duy nhất (thường là `/graphql`) và chỉ rõ chính xác những trường dữ liệu mà mình cần. Server GraphQL sẽ xử lý yêu cầu này và chỉ trả về đúng cấu trúc dữ liệu mà client yêu cầu. Bạn có thể lấy thông tin người dùng, bài viết của họ, và số lượng comment trên mỗi bài viết chỉ trong một yêu cầu. Điều này giúp giảm thiểu việc truyền dữ liệu không cần thiết qua mạng và giảm số lượng round trip giữa client và server.

Hãy xem bảng so sánh đơn giản dưới đây:

Tính năng REST GraphQL
Số lượng Endpoint Nhiều, mỗi loại tài nguyên hoặc hành động có thể có endpoint riêng. Thông thường là một endpoint duy nhất.
Cách Client Yêu cầu Dữ liệu Yêu cầu theo URL cụ thể, nhận cấu trúc dữ liệu cố định do Server quy định. Yêu cầu theo loại tài nguyên và chỉ định chính xác các trường (fields) cần nhận.
Vấn đề Dữ liệu Over-fetching (lấy thừa) và Under-fetching (lấy thiếu/cần nhiều request). Loại bỏ hoặc giảm thiểu Over-fetching/Under-fetching.
Quản lý Phiên bản API Thường phức tạp (ví dụ: /v1, /v2) hoặc cần thay đổi endpoint. Dễ dàng thêm trường mới mà không ảnh hưởng client cũ, việc loại bỏ trường cần được quản lý cẩn thận (deprecation).
Kiểu dữ liệu Không có hệ thống kiểu dữ liệu tích hợp sẵn ở cấp độ API, phụ thuộc vào định nghĩa của Server (ví dụ: OpenAPI/Swagger). Có hệ thống kiểu dữ liệu mạnh mẽ, định nghĩa rõ ràng cấu trúc dữ liệu và các thao tác (Query, Mutation, Subscription).

Đối với ứng dụng di động, việc tối ưu hóa dữ liệu truyền qua mạng là cực kỳ quan trọng, đặc biệt ở những khu vực có kết nối yếu hoặc khi người dùng sử dụng dữ liệu di động. GraphQL mang lại lợi thế đáng kể trong những trường hợp này.

Apollo-Android là gì và tại sao cần sử dụng nó?

GraphQL định nghĩa cách client *hỏi* dữ liệu, nhưng bản thân nó không phải là một thư viện mạng để thực hiện request HTTP. Tương tự như khi bạn dùng Retrofit cho REST, bạn cần một client library để tương tác với API GraphQL.

Apollo-Android là một client GraphQL mạnh mẽ, được thiết kế đặc biệt cho nền tảng Android và JVM. Nó không chỉ giúp bạn gửi các truy vấn GraphQL mà còn làm được nhiều hơn thế:

  1. Code Generation: Đây là tính năng “sao” của Apollo. Dựa vào file schema (định nghĩa cấu trúc dữ liệu của API GraphQL) và các file chứa truy vấn/mutation/subscription của bạn, Apollo sẽ tự động sinh mã Kotlin (hoặc Java) tương ứng.
  2. Type Safety: Nhờ việc sinh mã, bạn có các class/object Kotlin được định nghĩa rõ ràng cho dữ liệu trả về từ API. Điều này giúp bạn làm việc với dữ liệu một cách an toàn, tránh các lỗi sai tên trường (typo) hay sai kiểu dữ liệu trong thời gian chạy (runtime). Compiler sẽ bắt lỗi cho bạn ngay lúc viết mã.
  3. Tích hợp Coroutines & RxJava: Apollo hỗ trợ tốt các công nghệ xử lý bất đồng bộ hiện đại trên Android.
  4. Caching: Apollo cung cấp một hệ thống caching mạnh mẽ (In-memory, Normalized Cache) giúp quản lý dữ liệu cục bộ hiệu quả, giảm tải cho mạng và cải thiện trải nghiệm người dùng.
  5. Hỗ trợ Subscription: Ngoài Query (lấy dữ liệu) và Mutation (thay đổi dữ liệu), Apollo còn hỗ trợ Subscription (nhận cập nhật dữ liệu theo thời gian thực) thông qua WebSockets.

Tóm lại, sử dụng Apollo-Android giúp bạn làm việc với GraphQL một cách có cấu trúc, an toàn và hiệu quả hơn nhiều so với việc tự xây dựng các request HTTP thô và phân tích JSON thủ công.

Thiết lập Apollo-Android trong Dự án Android của Bạn

Để sử dụng Apollo-Android, bạn cần thêm các dependencies và cấu hình plugin trong file build.gradle của dự án.

Thêm Apollo Plugin vào Project-level build.gradle

Mở file build.gradle.kts (Project) hoặc build.gradle (Project) ở thư mục gốc dự án và thêm plugin Apollo vào khối plugins:

plugins {
    // ... các plugin khác
    id("com.apollographql.apollo3").version("4.0.0") // Kiểm tra phiên bản mới nhất trên trang chính thức
}

Hoặc với Groovy:

plugins {
    // ... các plugin khác
    id 'com.apollographql.apollo3' version '4.0.0' // Kiểm tra phiên bản mới nhất trên trang chính thức
}

Sync lại dự án sau khi thêm.

Cấu hình Module-level build.gradle

Mở file build.gradle.kts (Module: app) hoặc build.gradle (Module: app) và áp dụng plugin Apollo, sau đó thêm các dependencies cần thiết:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.apollographql.apollo3") // Áp dụng plugin Apollo
    // ... các plugin khác
}

android {
    // ... cấu hình android khác
}

apollo {
    // Cấu hình Apollo (tùy chọn)
    // Ví dụ: đặt tên package cho mã sinh ra
    packageName.set("com.yourcompany.yourapp.graphql")
    // Hoặc nếu muốn sử dụng các tính năng thử nghiệm (experimental features)
    // experimentalFeatures.set(true)
}

dependencies {
    // Apollo Core (bắt buộc)
    implementation("com.apollographql.apollo3:apollo-runtime:4.0.0") // Kiểm tra phiên bản mới nhất

    // Thêm các adapter serialization nếu cần thiết, ví dụ cho JSON
    // implementation("com.apollographql.apollo3:apollo-adapters:4.0.0")

    // Thư viện Coroutines (nếu bạn dùng Coroutines cho network calls)
    // implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.x.x")

    // ... các dependencies khác
}

Hoặc với Groovy:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'com.apollographql.apollo3' version '4.0.0' // Áp dụng plugin Apollo và chỉ định version
    // ... các plugin khác
}

android {
    // ... cấu hình android khác
}

apollo {
    // Cấu hình Apollo (tùy chọn)
    packageName.set("com.yourcompany.yourapp.graphql")
}

dependencies {
    implementation "com.apollographql.apollo3:apollo-runtime:4.0.0"

    // implementation "com.apollographql.apollo3:apollo-adapters:4.0.0"

    // implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.x.x"

    // ... các dependencies khác
}

Sync lại dự án một lần nữa.

Schema và Các File .graphql

Để Apollo có thể sinh mã, nó cần biết cấu trúc của API GraphQL mà bạn sẽ tương tác. Thông tin này được cung cấp thông qua file Schema.

File schema.graphqls

Bạn cần tải file schema từ server GraphQL của mình. Server thường cung cấp một URL để tải schema (thường ở định dạng SDL – Schema Definition Language). Tạo một file tên là schema.graphqls trong thư mục src/main/graphql/com/yourcompany/yourapp/ (đường dẫn này tương ứng với package name bạn đã cấu hình hoặc package mặc định). Paste nội dung schema vào file này.

Ví dụ nội dung file schema.graphqls:

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

type Query {
  users: [User!]!
  user(id: ID!): User
}

type Mutation {
  createUser(name: String!): User!
}

type User {
  id: ID!
  name: String!
  email: String
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String
}

Các File Operation (.graphql)

Các file này chứa các truy vấn (Query), thay đổi dữ liệu (Mutation) hoặc đăng ký nhận cập nhật (Subscription) mà ứng dụng của bạn sẽ thực hiện. Bạn tạo các file này với đuôi .graphql bên trong cùng thư mục src/main/graphql/com/yourcompany/yourapp/ hoặc các thư mục con bên trong đó.

Ví dụ: Tạo file GetAllUsers.graphql:

query GetAllUsers {
  users {
    id
    name
    email
  }
}

Ví dụ: Tạo file CreateNewUser.graphql:

mutation CreateNewUser($name: String!) {
  createUser(name: $name) {
    id
    name
  }
}

Sau khi đặt các file schema và operation vào đúng vị trí, chạy lệnh Build Project trong Android Studio (Build -> Make Project) hoặc chạy task Gradle generateApolloSources. Apollo sẽ đọc các file này và sinh mã Kotlin tương ứng trong thư mục build/generated/source/apollo/.

Với ví dụ trên, Apollo sẽ sinh ra các class như GetAllUsersQueryCreateNewUserMutation.

Thực hiện Query (Truy vấn Dữ liệu)

Bây giờ, chúng ta đã sẵn sàng để gửi truy vấn GraphQL từ ứng dụng Android.

Đầu tiên, bạn cần tạo một instance của ApolloClient. Thường thì bạn sẽ làm điều này một lần duy nhất và sử dụng lại instance này trong toàn bộ ứng dụng, có thể sử dụng Dependency Injection (nếu bạn đã học về Dependency Injection).

import com.apollographql.apollo3.ApolloClient

// URL của API GraphQL của bạn
val serverUrl = "https://your-graphql-backend.com/graphql"

val apolloClient = ApolloClient.Builder()
    .serverUrl(serverUrl)
    // Thêm các interceptor, ví dụ cho authentication
    // .addHttpInterceptor(AuthorizationInterceptor("Bearer your_token"))
    .build()

Với apolloClient đã có, bạn có thể thực hiện truy vấn GetAllUsers:

import com.apollographql.apollo3.api.Optional
import kotlinx.coroutines.launch
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

// Ví dụ trong một ViewModel sử dụng Coroutines
class UserViewModel(private val apolloClient: ApolloClient) : ViewModel() {

    fun loadUsers() {
        viewModelScope.launch {
            // Tạo instance của truy vấn đã được sinh mã
            val response = apolloClient.query(GetAllUsersQuery())
                .execute() // Thực thi truy vấn bất đồng bộ

            if (response.data != null) {
                // Xử lý dữ liệu thành công
                val userList = response.data?.users // response.data là kiểu an toàn null
                // Cập nhật UI hoặc xử lý dữ liệu
                userList?.forEach { user ->
                    println("User ID: ${user?.id}, Name: ${user?.name}, Email: ${user?.email}")
                }
            } else if (response.hasErrors()) {
                // Xử lý lỗi từ GraphQL server
                response.errors?.forEach { error ->
                    println("GraphQL Error: ${error.message}")
                }
            } else {
                // Xử lý các loại lỗi khác (network, parsing, v.v.)
                println("Request failed: ${response.exception?.message}")
            }
        }
    }
}

Ở đây, chúng ta sử dụng Coroutines để thực thi truy vấn một cách bất đồng bộ. apolloClient.query(GetAllUsersQuery()).execute() trả về một đối tượng Response. Đối tượng này chứa dữ liệu trả về an toàn kiểu (response.data), danh sách lỗi từ GraphQL server (response.errors), và exception nếu có lỗi ở tầng mạng hoặc client (response.exception).

Việc sử dụng mã sinh ra giúp bạn truy cập dữ liệu một cách trực quan: response.data?.users sẽ trả về một danh sách các đối tượng User an toàn kiểu, với các trường id, name, email mà bạn đã yêu cầu trong file .graphql.

Thực hiện Mutation (Thay đổi Dữ liệu)

Thực hiện Mutation cũng tương tự như Query, nhưng bạn sẽ sử dụng phương thức mutation() thay vì query() và truyền vào các biến (variables) nếu mutation đó yêu cầu.

Với file CreateNewUser.graphql đã định nghĩa ở trên, nó yêu cầu một biến $name kiểu String! (bắt buộc). Khi gọi từ code Android, bạn sẽ truyền giá trị cho biến này:

import com.apollographql.apollo3.api.Optional
import kotlinx.coroutines.launch
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

class UserViewModel(private val apolloClient: ApolloClient) : ViewModel() {

    fun createUser(name: String) {
        viewModelScope.launch {
            // Tạo instance của mutation đã được sinh mã, truyền biến
            val response = apolloClient.mutation(CreateNewUserMutation(name = name))
                .execute() // Thực thi mutation bất đồng bộ

            if (response.data != null) {
                // Xử lý dữ liệu thành công
                val createdUser = response.data?.createUser
                println("User created with ID: ${createdUser?.id} and Name: ${createdUser?.name}")
            } else if (response.hasErrors()) {
                // Xử lý lỗi từ GraphQL server
                response.errors?.forEach { error ->
                    println("GraphQL Error: ${error.message}")
                }
            } else {
                // Xử lý các loại lỗi khác
                println("Request failed: ${response.exception?.message}")
            }
        }
    }
}

Lưu ý cách chúng ta truyền biến name vào constructor của class CreateNewUserMutation đã được Apollo sinh ra. Apollo tự động xử lý việc mapping các biến này vào request GraphQL.

Xử lý Kết quả (Handling Responses)

Việc xử lý kết quả với Apollo trở nên an toàn và dễ dàng nhờ mã sinh ra:

  • Dữ liệu: Dữ liệu thành công được truy cập qua response.data. Kiểu dữ liệu của response.data được định nghĩa chính xác dựa trên cấu trúc bạn yêu cầu trong file .graphql. Nếu một trường được đánh dấu là nullable trong schema hoặc trong query của bạn, nó sẽ là nullable trong mã sinh ra (ví dụ: user?.email).
  • Lỗi GraphQL: Nếu server xử lý request thành công nhưng có lỗi ở tầng GraphQL (ví dụ: thiếu quyền truy cập một trường dữ liệu), danh sách lỗi sẽ có trong response.errors. Đây là các lỗi nghiệp vụ từ server.
  • Exception: Các lỗi ở tầng thấp hơn như lỗi mạng (không kết nối được server), lỗi HTTP (404, 500), lỗi phân tích cú pháp (parsing) sẽ được ném ra dưới dạng exception và bạn có thể bắt nó thông qua response.exception hoặc trong khối catch của Coroutines/callback onError.

Luôn kiểm tra response.data != null hoặc response.hasErrors() để xác định trạng thái của response. Apollo cũng cung cấp các cơ chế xử lý chi tiết hơn cho từng loại lỗi.

Caching Cơ bản (Giới thiệu)

Apollo cung cấp một tầng caching tích hợp sẵn, cho phép bạn lưu trữ và quản lý dữ liệu nhận được từ GraphQL server ngay trên thiết bị. Apollo có thể tự động đọc dữ liệu từ cache khi có sẵn, giảm nhu cầu gọi mạng lặp lại cho cùng một dữ liệu.

Có nhiều loại cache trong Apollo, phổ biến nhất là Normalized Cache, nơi dữ liệu được lưu trữ dưới dạng các đối tượng độc lập và liên kết với nhau bằng ID. Điều này giúp tránh trùng lặp dữ liệu và đảm bảo tính nhất quán khi dữ liệu thay đổi.

Việc cấu hình cache là một chủ đề nâng cao hơn một chút và vượt ra ngoài phạm vi bài viết “Getting Started” này. Tuy nhiên, bạn nên biết rằng nó tồn tại và là một tính năng quan trọng của Apollo giúp tối ưu hóa ứng dụng của bạn.

Ưu điểm và Nhược điểm khi sử dụng GraphQL & Apollo-Android

Ưu điểm:

  • Hiệu quả truyền tải dữ liệu: Chỉ lấy chính xác những gì cần, giảm thiểu Over-fetching.
  • Giảm số lượng request: Lấy được nhiều loại dữ liệu liên quan trong một request duy nhất.
  • Type Safety mạnh mẽ: Nhờ mã sinh ra, lỗi được phát hiện sớm khi compile.
  • Giảm công sức phân tích JSON thủ công: Apollo tự động map JSON response vào các đối tượng Kotlin an toàn kiểu.
  • Tích hợp caching và subscription: Các tính năng nâng cao hữu ích cho ứng dụng hiện đại.
  • Phù hợp cho Backend phát triển nhanh: Client ít bị ảnh hưởng khi Backend thêm trường mới.

Nhược điểm:

  • Độ phức tạp ban đầu: Cần thời gian để làm quen với các khái niệm GraphQL (schema, query, mutation) và cách cấu hình Apollo.
  • Yêu cầu Backend hỗ trợ: Cần có một server GraphQL backend.
  • Caching phức tạp hơn REST: Việc quản lý cache cho GraphQL có thể phức tạp hơn so với caching response dựa trên URL của REST.
  • Uploading File: Việc upload file với GraphQL theo chuẩn có thể hơi khác so với REST truyền thống. Apollo có hỗ trợ, nhưng cần tìm hiểu thêm.

GraphQL và Apollo-Android là những công cụ tuyệt vời, nhưng không phải là “thuốc tiên” cho mọi trường hợp. Chúng đặc biệt phù hợp với các ứng dụng cần linh hoạt trong việc lấy dữ liệu, tương tác với Backend phức tạp hoặc cần tối ưu hóa việc sử dụng mạng.

Lời kết

Chúc mừng! Bạn đã hoàn thành bài viết này và có những kiến thức nền tảng về GraphQL và cách sử dụng thư viện Apollo-Android để tích hợp nó vào ứng dụng của mình. Đây là một bước tiến quan trọng trong việc làm quen với các công nghệ giao tiếp mạng hiện đại.

Việc chuyển từ REST sang GraphQL (hoặc sử dụng cả hai tùy trường hợp) có thể mang lại nhiều lợi ích về hiệu năng và khả năng phát triển ứng dụng. Apollo-Android giúp quá trình này trở nên mượt mà và an toàn hơn.

Hãy thử áp dụng Apollo-Android vào một dự án nhỏ hoặc tìm kiếm các API GraphQL công cộng để thực hành. Bạn sẽ thấy sức mạnh của code generation và type safety mà nó mang lại.

Hành trình Android Developer Roadmap vẫn còn nhiều điều thú vị phía trước. Sau khi đã nắm vững cách lấy và xử lý dữ liệu từ mạng (cả REST lẫn GraphQL), chúng ta sẽ cần tìm hiểu sâu hơn về cách xử lý các tác vụ chạy ngầm, tối ưu hiệu năng, và chuẩn bị cho việc đưa ứng dụng ra thị trường.

Hẹn gặp lại trong các bài viết tiếp theo của series!

Chỉ mục