Flutter gRPC Client Entegrasyonu

Salih Can
6 min readApr 1, 2023

--

Merhaba! Daha önceki makalelerde gRPC framework’üne giriş yapmış ve TypeScript kullanarak bir gRPC sunucusu oluşturmayı ele almıştık. Bu makalede ise, Flutter’da gRPC framework’ünü kullanarak bir client uygulaması nasıl geliştirilir, adım adım göstereceğim.

Aynı zamanda, önceki makalelerimizde kullandığımız proto dosyamızı kullanarak, sunucu tarafında yazdığımız servisimizi Flutter gRPC client uygulamamızda nasıl kullanabileceğimizi öğreneceğiz.

Haydi, Flutter gRPC client uygulamamızı oluşturmaya başlayalım!

Öncelikle, aşağıdaki bağlantılardan gRPC hakkında daha detaylı bilgi edinebilir ve TypeScript kullanarak sunucu tarafının nasıl oluşturulacağını görebilirsiniz.

Yine alışılageldiği gibi devam edelim ve makale sonunda elde edeceğimiz yapının görüntüsünü aşağıya bırakıyorum. Ayrıca, projenin son halini GitHub’ta görmek isterseniz, makalenin en sonuna GitHub bağlantısını bıraktım.

Sunucu tarafında aşağıdaki proto modelini kullanmıştık. Client tarafında da aynı modeli kullanarak ilgili buffer dosyalarını oluşturacağız. Bu sayede sunucuda bir method u, client tarafında da çalıştırabileceğiz (RPC).

path:lib/app/data/proto

Yukarıdaki dosyayı ./lib/app/data/proto dizinine ekledikten sonra, Dart sınıflarını üretecek paketin kurulumunu yapalım. Aşağıdaki satırı terminal aracılığıyla çalıştırarak protoc kurulumunu tek satırda yapabilirsiniz.

$ dart pub global activate protoc_plugin

./scriptsdizinine proto_generator.sh isimli bir shell dosyası oluşturalım ve içerisine aşağıdaki satır’ı ekleyelim. (Ayrıca belirtmeliyim ki, aşağıdaki shell dosyasını oluşturmadan sadece protockomutundan itibaren alarak kod üretimini başlatabilirsiniz.)

cd .. &&  protoc --dart_out=grpc:lib/app/data/generated/ --proto_path=lib/app/data/proto lib/app/data/proto/*.proto

Yukarıdaki satırlarda yer alan protoc shell’ini çağırarak verdiğimiz parametreler sayesinde, proto dosyalarımıza uygun olan Dart sınıfları üretilecek. Aşağıda kullanılan parametrelerin açıklamasını ekliyorum.

  • protoc: Oluşturduğumuz shell dosyasını çağırır.
  • dart_out: Üretilen Dart sınıflarının nereye üretileceğini belirtiyoruz.
  • proto_path: Proto dosyalarımızın bulunduğu konumu belirtiyoruz. Bu parametreyi vermesek de olur ancak, oluşturulan dosyaları ‘$currentProtoPath/generatedFiles’ şeklinde bir yöntemle generate ettiği için, dosya yapısı oldukça karmaşık hale gelebilir. Bu nedenle bu parametrenin kullanımı önemlidir.
  • En son verdiğimiz path, asıl proto dosyalarının konumunu belirtir. Bu alan require oldugu için herhangi bir ön ek gerekmemekte. Bu kullanımda, proto dosyasının içindeki “.proto” ile biten bütün dosyaların proto dosyaları olduğunu belirttik.

Oluşturduğumuz ‘proto_generator.sh’ shell dosyasını aşağıdaki yöntemle terminalde çalıştırarak, Dart sınıflarını proto dosyamıza göre ürettirelim.

sh proto_generator.sh

Bu aşamaları doğru bir şekilde geçtiğinizde, dosya düzeniniz aşağıdaki fotoğraftaki gibi olmalıdır.

Üretilen bu dosyalara göz gezdirdiğinizde, içeriğinin oldukça karmaşık bir durumda olduğunu göreceksiniz. Ancak, içerik bizim için önemli değil. Nedenlerine bir önceki makalelerde defalarca değindim. Bu makalenin başında bağlantılarını bıraktım, eğer nedenleriyle ilgili aklınızda soru işareti kaldıysa göz atabilirsiniz.

Şimdi sunucu metodlarını çağırabileceğimiz ve sunucu bağlantısını kurabileceğimiz RPC istemcisini oluşturma zamanı geldi.

RPC istemcisi oluşturmak için bir RPC Client sınıfı oluşturdum ve gRPC dart kütüphanesinin ağ işlemlerini ele alan sınıfını genişleterek kendi istemcime göre özelleştirdim.

greeter_service_handler.dart dosyasını lib/app/data/remote dizinine oluşturdum ve içeriğini aşağıdaki gibi doldurdum.

import 'dart:io';

import 'package:grpc/grpc.dart';

import '../generated/greeter.pbgrpc.dart';

class GreeterServiceHandler extends GreeterClient {
GreeterServiceHandler()
: super(
ClientChannel(
_host,
port: 1337,
options: const ChannelOptions(
credentials: ChannelCredentials.insecure(),
),
),
options: CallOptions(
timeout: const Duration(seconds: 5),
),
);

static String get _host {
if (Platform.isAndroid) {
return '10.0.2.2';
} else if (Platform.isIOS) {
return '0.0.0.0';
}

return 'localhost';
}
}

Yukarıda port 1337 olarak belirttim, çünkü bir önceki makalede sunucunun kullanması için portu 1337 olarak belirlemiştik. Ayrıca, host adresini dinamik olarak ele aldım, çünkü Android cihazlarda yerel ana bilgisayar ağı erişimi 10.0.2.2 ile gerçekleştiriliyor.

Aşağıda, projenin dosya düzeninin son halini görebilirsiniz.

Bu şekilde Dart tarafında gRPC istemci mantıksal işlemlerini tamamlamış olduk. Şimdi sıra, oluşturduğumuz GreeterServiceHandler aracılığıyla sunucudaki methodları çağırıp cevapları ekrana yansıtmaya geldi. Bunun için Flutter uygulamasını oluşturacağız.

Örnek uygulamamız tek ekrandan oluşacak. Sunucu tarafını oluştururken, SayHello adlı yönteme gelen isteğin içindeki String değerine 'Hello ' ekleyip cevap olarak dönüyorduk. Biz de uygulama açılır açılmaz 'gRPC' mesajını gönderecek ve cevabı ekrana yazdıracağız.

İlk aşamada, uygulamamızın Material iskeletini kuracak olan ‘MaterialApp’ ile uygulamayı oluşturacağız.

Bu işlemlere geçmeden önce sunucuyu çalıştırmayı unutmayalım!

lib/main.dart dizinine ‘main.dart’ dosyasını oluşturdum ve içeriğine aşağıdaki satırları yazdım.

import 'package:flutter/material.dart';

import 'app/init/grpc_client_flutter_app.dart';

void main() {
runApp(const ClientAppForGRPC());
}

Daha sonra, ClientAppForGRPC sınıfını ‘lib/app/init’ dizinine /grpc_client_flutter_app.dart ismiyle oluşturdum ve içeriğine aşağıdaki satırları yazdım.

import 'package:flutter/material.dart';
import 'package:grpc_client_flutter/app/presetantion/home/home_view.dart';

class ClientAppForGRPC extends StatelessWidget {
const ClientAppForGRPC({super.key});

@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomeView(),
);
}
}

Yukarıda gördüğünüz gibi, uygulamamızın ilk açılış ekranının adını HomeView olarak belirledikten sonra ‘HomeView’ı oluşturalım.

Bu projede mümkün olduğunca basit ilerlemek istediğimden, herhangi bir durum yönetim paketi tercih etmedim. Bunun yerine, ilgili durumu kalıtım yaparak mantıksal kodları UI tarafından ayırdım. (Bu konuda aklınıza takılan bir durum olursa, bu makaleye cevap olarak veya diğer platformlardan bana ulaşarak sorabilirsiniz.)

Home ekranıyla ilgili HomeView ve HomeViewModel sınıflarını lib/app/presentation/home dizinine oluşturdum. Dosya isimleri ise home_view.dart ve home_view_model.dart şeklinde belirledim.

HomeView sınıfının icerigi asagidaki sekilde belirttim.

import 'package:flutter/material.dart';

import 'home_view_model.dart';

class HomeView extends StatefulWidget {
const HomeView({Key? key}) : super(key: key);

@override
State<HomeView> createState() => _HomeView();
}

class _HomeView extends HomeViewModel {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(screenTitle),
),
);
}
}

Gördüğünüz gibi, bu sınıfta anlatılacak pek bir detay yok. Ancak kısaca bahsetmem gerekirse, bu ekran, ortasında metin yazdıran basit bir ekrandır ve metin değerini(screenTitle) HomeViewModel sınıfından alır.

Client tarafında RPC işlemleri %99 oranında HomeViewModel sınıfında ele alınmaktadır. Bu sınıfı aşağıdaki satırlarla oluşturduktan sonra altında açıklamasını yazacağım. HomeViewModel sınıfını, daha önce belirttiğim gibi lib/app/presentation/home dizinine home_view_model.dart adlı dosya ismiyle oluşturdum.

import 'package:flutter/material.dart';

import '../../data/generated/greeter.pbgrpc.dart';
import '../../data/remote/greeter_service_handler.dart';
import 'home_view.dart';

abstract class HomeViewModel extends State<HomeView> {
String screenTitle = 'loading...';

@override
void initState() {
_init();
super.initState();
}

Future<void> _init() async {
final greeterService = GreeterServiceHandler();

final request = HelloRequest()..name = 'gRPC';

final response = await greeterService.sayHello(request);

await Future.delayed(const Duration(seconds: 3));

setState(() {
screenTitle = response.message;
});
}
}

Sınıfın içinde screenTitle adında bir değişken tanımlanır ve ‘loading…’ değeriyle başlatılır. HomeView ekranın başlamasıyla birlikte HomeViewModel state’i Stateful Widget tarafından yaratılır.

Yaratılmasıyla birlikte initState içerisindeki _init methodu çağırılır. Bu method öncelikle GreeterServiceHandler sınıfından bir instance yaratır. Ardından HelloRequest adlı sınıfın içindeki name değerine ‘gRPC’ string değeri atanır.

Daha önce oluşturduğumuz GreeterServiceHandler sınıfının instance’ini kullanarak sayHello isimli methodu çağırıyoruz ve parametre olarak istediği HelloRequest isimli sınıfı veriyoruz. Bu methodu çağırmamızla birlikte basit mantıkla sunucu tarafında SayHello isimli methodu çalıştırmış olduk ve belirli bir süre sonra sunucudan aldığımız cevabı response isimli değişkene atıyoruz.

Cevabı aldıktan sonra screenTitle isimli değişkeni cevapla güncelliyoruz ve ekrandaki sonucu hep birlikte görüyoruz.

Sunucudan aldığımız cevaptan sonra 3 saniyelik bir bekleme koydum. Bunun sebebi, localde çalışmamızın da etkisiyle cevabı almanın milisaniyeler sürebilmesiydi ve loading… yazısını hiç göremeyecek olmamızdı. Bu sayede artık öncelikle loading… görüyoruz, daha sonra Hello gRPC mesajını ekranda görüyoruz.

Sol taraftaki konsol sunucu tarafından, sağ taraftaki konsol ise istemci tarafından.

Bu makalede, Flutter da gRPC kullanarak bir istemci uygulaması oluşturmayı öğrendik. Protobuf dosyaları kullanarak kodlarımızı otomatik olarak oluşturduk ve gRPC servisleri aracılığıyla istemci uygulaması ile sunucu uygulaması arasında iletişim kurduk.

Umarım bu makale, gRPC konusuna ilgi duyanlar için yararlı bir kaynak olmuştur. Olabildiğince açık ve basit bir şekilde anlatmaya çalıştım. Ancak, aklınıza takılan herhangi bir soru olursa makaleye yorum olarak veya LinkedIn gibi ortamlardan benimle iletişim kurmaktan çekinmeyin.

--

--

Salih Can
Salih Can

No responses yet