Bu sayfada, CameraX'in mimarisi (yapısı, API ile çalışma, yaşam döngüleriyle çalışma ve kullanım alanlarını birleştirme) ele alınmaktadır.
CameraX yapısı
CameraX'i, kullanım alanı adı verilen bir soyutlama aracılığıyla cihazın kamerasıyla arayüz oluşturmak için kullanabilirsiniz. Aşağıdaki kullanım alanları mevcuttur:
- Önizleme: Önizleme göstermek için bir yüzey kabul eder (ör.
PreviewView
). - Resim analizi: Makine öğrenimi gibi analizler için CPU'nun erişebileceği arabellekler sağlar.
- Görüntü yakalama: Fotoğraf çekip kaydeder.
- Video kaydı:
VideoCapture
ile video ve ses kaydetme
Kullanım alanları birleştirilebilir ve aynı anda etkin olabilir. Örneğin, bir uygulama, kullanıcının önizleme kullanım alanını kullanarak kameranın gördüğü resmi görüntülemesine, fotoğraftaki kişilerin gülümsediğini belirleyen bir görüntü analizi kullanım alanına ve gülümsedikleri anda fotoğraf çekmek için bir görüntü yakalama kullanım alanına sahip olabilir.
API modeli
Kitaplıkla çalışmak için aşağıdakileri belirtirsiniz:
- Yapılandırma seçenekleriyle birlikte istenen kullanım alanı.
- Dinleyiciler ekleyerek çıkış verileriyle ne yapacağınızı belirleyin.
- Kullanım alanını Android Architecture Lifecycles'a bağlayarak kameraların ne zaman etkinleştirileceği ve verilerin ne zaman oluşturulacağı gibi amaçlanan akış.
CameraX uygulaması yazmanın 2 yolu vardır: a
CameraController
(CameraX'i kullanmanın en basit yolunu istiyorsanız idealdir) veya a
CameraProvider
(daha fazla esnekliğe ihtiyacınız varsa idealdir).
CameraController
CameraController
, CameraX'in temel işlevlerinin çoğunu tek bir sınıfta sağlar. Çok az kurulum kodu gerektirir ve kamera başlatma, kullanım alanı yönetimi, hedef döndürme, dokunarak odaklama, iki parmakla yakınlaştırma gibi işlemleri otomatik olarak gerçekleştirir. CameraController
öğesini genişleten somut sınıf LifecycleCameraController
'dir.
Kotlin
val previewView: PreviewView = viewBinding.previewView var cameraController = LifecycleCameraController(baseContext) cameraController.bindToLifecycle(this) cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA previewView.controller = cameraController
Java
PreviewView previewView = viewBinding.previewView; LifecycleCameraController cameraController = new LifecycleCameraController(baseContext); cameraController.bindToLifecycle(this); cameraController.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA); previewView.setController(cameraController);
CameraController
için varsayılan UseCase
'ler Preview
, ImageCapture
ve ImageAnalysis
'dir. ImageCapture
veya ImageAnalysis
simgesini kapatmak ya da VideoCapture
simgesini açmak için setEnabledUseCases()
yöntemini kullanın.
CameraController
ile ilgili daha fazla kullanım alanı için QR kodu tarayıcı örneğine veya CameraController
ile ilgili temel bilgiler videosuna göz atın.
CameraProvider
CameraProvider
, kullanımı kolay olmaya devam eder ancak kurulumun daha büyük bir bölümü uygulama geliştirici tarafından yapıldığından yapılandırmayı özelleştirmek için daha fazla fırsat vardır. Örneğin, ImageAnalysis
içinde çıkış görüntüsü döndürmeyi etkinleştirebilir veya çıkış görüntüsü biçimini ayarlayabilirsiniz. Kamera önizlemesi için özel bir Surface
de kullanabilirsiniz. Bu, daha fazla esneklik sağlar. CameraController ile ise PreviewView
kullanmanız gerekir. Mevcut Surface
kodunuz, uygulamanızın diğer bölümlerine zaten giriş yapılmışsa faydalı olabilir.
Kullanım alanlarını set()
yöntemlerini kullanarak yapılandırır ve build()
yöntemiyle sonlandırırsınız. Her kullanım alanı nesnesi, kullanım alanına özel bir API grubu sağlar. Örneğin, görüntü yakalama kullanım alanında takePicture()
yöntem çağrısı sağlanır.
Bir uygulama, onResume()
ve onPause()
içinde belirli başlatma ve durdurma yöntemi çağrıları yerleştirmek yerine, cameraProvider.bindToLifecycle()
kullanarak kamerayı ilişkilendireceği bir yaşam döngüsü belirtir.
Bu yaşam döngüsü, kamera yakalama oturumunun ne zaman yapılandırılacağını CameraX'e bildirir ve kamera durumunun, yaşam döngüsü geçişlerine uygun şekilde değişmesini sağlar.
Her kullanım alanıyla ilgili uygulama adımları için Önizleme uygulama, Resimleri analiz etme, Resim yakalama ve Video yakalama başlıklı makaleleri inceleyin.
Önizleme kullanım alanı, görüntüleme için bir Surface
ile etkileşime girer. Uygulamalar, aşağıdaki kodu kullanarak yapılandırma seçenekleriyle kullanım alanı oluşturur:
Kotlin
val preview = Preview.Builder().build() val viewFinder: PreviewView = findViewById(R.id.previewView) // The use case is bound to an Android Lifecycle with the following code val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview) // PreviewView creates a surface provider and is the recommended provider preview.setSurfaceProvider(viewFinder.getSurfaceProvider())
Java
Preview preview = new Preview.Builder().build(); PreviewView viewFinder = findViewById(R.id.view_finder); // The use case is bound to an Android Lifecycle with the following code Camera camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview); // PreviewView creates a surface provider, using a Surface from a different // kind of view will require you to implement your own surface provider. preview.previewSurfaceProvider = viewFinder.getSurfaceProvider();
Daha fazla örnek kod için resmi CameraX örnek uygulamasına bakın.
CameraX Yaşam Döngüleri
CameraX, kameranın ne zaman açılacağını, ne zaman yakalama oturumu oluşturulacağını ve ne zaman durdurulup kapatılacağını belirlemek için bir yaşam döngüsünü gözlemler. Kullanım alanı API'leri, ilerlemeyi izlemek için yöntem çağrıları ve geri çağırmalar sağlar.
Kullanım alanlarını birleştirme bölümünde açıklandığı gibi, bazı kullanım alanı kombinasyonlarını tek bir yaşam döngüsüne bağlayabilirsiniz. Uygulamanızın birleştirilemeyen kullanım alanlarını desteklemesi gerektiğinde aşağıdakilerden birini yapabilirsiniz:
- Uyumlu kullanım alanlarını birden fazla parçada gruplandırın ve parçalar arasında geçiş yapın.
- Özel bir yaşam döngüsü bileşeni oluşturma ve bunu kullanarak kamera yaşam döngüsünü manuel olarak kontrol etme
Görünüm ve kamera kullanım alanlarınızın yaşam döngüsü sahiplerini ayırırsanız (örneğin, özel bir yaşam döngüsü veya retain
fragment kullanıyorsanız) ProcessCameraProvider.unbindAll()
kullanarak veya her kullanım alanını ayrı ayrı kaldırarak tüm kullanım alanlarının CameraX'ten ayrıldığından emin olmanız gerekir. Alternatif olarak, kullanım alanlarını bir yaşam döngüsüne bağladığınızda CameraX'in yakalama oturumunu açıp kapatmasını ve kullanım alanlarının bağlantısını kaldırmasını sağlayabilirsiniz.
Tüm kamera işlevleriniz, AppCompatActivity
veya AppCompat
parçası gibi tek bir yaşam döngüsü farkında bileşenin yaşam döngüsüne karşılık geliyorsa tüm istenen kullanım alanlarını bağlarken bu bileşenin yaşam döngüsünü kullanmak, yaşam döngüsü farkında bileşen etkin olduğunda kamera işlevinin hazır olmasını ve aksi takdirde herhangi bir kaynak tüketmeden güvenli bir şekilde kaldırılmasını sağlar.
Özel LifecycleOwner'lar
Gelişmiş durumlarda, uygulamanızın CameraX oturumunun yaşam döngüsünü standart bir Android LifecycleOwner
'ye bağlamak yerine açıkça kontrol etmesini sağlamak için özel bir LifecycleOwner
oluşturabilirsiniz.
Aşağıdaki kod örneğinde, basit bir özel LifecycleOwner'ın nasıl oluşturulacağı gösterilmektedir:
Kotlin
class CustomLifecycle : LifecycleOwner { private val lifecycleRegistry: LifecycleRegistry init { lifecycleRegistry = LifecycleRegistry(this); lifecycleRegistry.markState(Lifecycle.State.CREATED) } ... fun doOnResume() { lifecycleRegistry.markState(State.RESUMED) } ... override fun getLifecycle(): Lifecycle { return lifecycleRegistry } }
Java
public class CustomLifecycle implements LifecycleOwner { private LifecycleRegistry lifecycleRegistry; public CustomLifecycle() { lifecycleRegistry = new LifecycleRegistry(this); lifecycleRegistry.markState(Lifecycle.State.CREATED); } ... public void doOnResume() { lifecycleRegistry.markState(State.RESUMED); } ... public Lifecycle getLifecycle() { return lifecycleRegistry; } }
Bu LifecycleOwner
sayesinde uygulamanız, kodunda istediği noktalara durum geçişleri yerleştirebilir. Bu işlevi uygulamanızda kullanma hakkında daha fazla bilgi için Özel bir LifecycleOwner uygulama başlıklı makaleyi inceleyin.
Eşzamanlı kullanım alanları
Kullanım alanları eşzamanlı olarak çalıştırılabilir. Kullanım alanları bir yaşam döngüsüne sırayla bağlanabilir ancak tüm kullanım alanlarını CameraProcessProvider.bindToLifecycle()
ile tek bir çağrıya bağlamak daha iyidir. Yapılandırma değişiklikleriyle ilgili en iyi uygulamalar hakkında daha fazla bilgi için Yapılandırma değişikliklerini yönetme başlıklı makaleyi inceleyin.
Aşağıdaki kod örneğinde, uygulama aynı anda oluşturulup çalıştırılacak iki kullanım alanını belirtir. Ayrıca, her iki kullanım alanı için de kullanılacak yaşam döngüsünü belirtir. Böylece her ikisi de yaşam döngüsüne göre başlatılır ve durdurulur.
Kotlin
private lateinit var imageCapture: ImageCapture override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener(Runnable { // Camera provider is now guaranteed to be available val cameraProvider = cameraProviderFuture.get() // Set up the preview use case to display camera preview. val preview = Preview.Builder().build() // Set up the capture use case to allow users to take photos. imageCapture = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build() // Choose the camera by requiring a lens facing val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_FRONT) .build() // Attach use cases to the camera with the same lifecycle owner val camera = cameraProvider.bindToLifecycle( this as LifecycleOwner, cameraSelector, preview, imageCapture) // Connect the preview use case to the previewView preview.setSurfaceProvider( previewView.getSurfaceProvider()) }, ContextCompat.getMainExecutor(this)) }
Java
private ImageCapture imageCapture; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); PreviewView previewView = findViewById(R.id.previewView); ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture.addListener(() -> { try { // Camera provider is now guaranteed to be available ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); // Set up the view finder use case to display camera preview Preview preview = new Preview.Builder().build(); // Set up the capture use case to allow users to take photos imageCapture = new ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build(); // Choose the camera by requiring a lens facing CameraSelector cameraSelector = new CameraSelector.Builder() .requireLensFacing(lensFacing) .build(); // Attach use cases to the camera with the same lifecycle owner Camera camera = cameraProvider.bindToLifecycle( ((LifecycleOwner) this), cameraSelector, preview, imageCapture); // Connect the preview use case to the previewView preview.setSurfaceProvider( previewView.getSurfaceProvider()); } catch (InterruptedException | ExecutionException e) { // Currently no exceptions thrown. cameraProviderFuture.get() // shouldn't block since the listener is being called, so no need to // handle InterruptedException. } }, ContextCompat.getMainExecutor(this)); }
CameraX, Preview
, VideoCapture
, ImageAnalysis
ve ImageCapture
öğelerinin her birinin aynı anda bir örneğinin kullanılmasını sağlar. Ayrıca,
- Her kullanım alanı kendi başına çalışabilir. Örneğin, bir uygulama önizleme kullanmadan video kaydedebilir.
- Uzantılar etkinleştirildiğinde yalnızca
ImageCapture
vePreview
kombinasyonunun çalışacağı garanti edilir. OEM uygulamasına bağlı olarak,ImageAnalysis
eklemek de mümkün olmayabilir. Uzantılar,ImageAnalysis
kullanım alanı için etkinleştirilemez.VideoCapture
Ayrıntılar için Uzantı referans dokümanını inceleyin. - Kamera özelliğine bağlı olarak bazı kameralar daha düşük çözünürlük modlarında birleşimi destekleyebilir ancak aynı birleşimi daha yüksek çözünürlüklerde desteklemeyebilir.
- Kamera donanım düzeyi
FULL
veya daha düşük olan cihazlardaPreview
,VideoCapture
veImageCapture
ya daImageAnalysis
birleştirildiğinde CameraX,Preview
veVideoCapture
için kameranınPRIV
akışını kopyalamak zorunda kalabilir. Akış paylaşımı olarak adlandırılan bu çoğaltma, söz konusu özelliklerin eş zamanlı olarak kullanılmasını sağlar ancak işlem taleplerinin artmasına neden olur. Bu nedenle, biraz daha yüksek gecikme ve daha kısa pil ömrü yaşayabilirsiniz.
Desteklenen donanım düzeyi
Camera2CameraInfo
adresinden alınabilir. Örneğin, aşağıdaki kod, varsayılan arka kameranın LEVEL_3
cihaz olup olmadığını kontrol eder:
Kotlin
@androidx.annotation.OptIn(ExperimentalCamera2Interop::class) fun isBackCameraLevel3Device(cameraProvider: ProcessCameraProvider) : Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return CameraSelector.DEFAULT_BACK_CAMERA .filter(cameraProvider.availableCameraInfos) .firstOrNull() ?.let { Camera2CameraInfo.from(it) } ?.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3 } return false }
Java
@androidx.annotation.OptIn(markerClass = ExperimentalCamera2Interop.class) Boolean isBackCameraLevel3Device(ProcessCameraProvider cameraProvider) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { List\<CameraInfo\> filteredCameraInfos = CameraSelector.DEFAULT_BACK_CAMERA .filter(cameraProvider.getAvailableCameraInfos()); if (!filteredCameraInfos.isEmpty()) { return Objects.equals( Camera2CameraInfo.from(filteredCameraInfos.get(0)).getCameraCharacteristic( CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL), CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3); } } return false; }
İzinler
Uygulamanızın CAMERA
iznine ihtiyacı olacak. Resimleri dosyalara kaydetmek için Android 10 veya sonraki sürümlerin yüklü olduğu cihazlar hariç WRITE_EXTERNAL_STORAGE
izni de gerekir.
Uygulamanız için izinleri yapılandırma hakkında daha fazla bilgi edinmek isterseniz Uygulama İzinleri İste başlıklı makaleyi inceleyin.
Şartlar
CameraX'in minimum sürüm gereksinimleri şunlardır:
- Android API düzeyi 21
- Android Architecture Components 1.1.1
Yaşam döngüsüne duyarlı etkinlikler için FragmentActivity
veya AppCompatActivity
kullanın.
Bağımlılıkları bildirme
CameraX'e bağımlılık eklemek için projenize Google Maven deposunu eklemeniz gerekir.
Projenizin settings.gradle
dosyasını açın ve google()
deposunu aşağıda gösterildiği gibi ekleyin:
Groovy
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } }
Kotlin
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } }
Android bloğunun sonuna aşağıdakileri ekleyin:
Groovy
android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } // For Kotlin projects kotlinOptions { jvmTarget = "1.8" } }
Kotlin
android { compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } // For Kotlin projects kotlinOptions { jvmTarget = "1.8" } }
Bir uygulamanın her modülünün build.gradle
dosyasına aşağıdakileri ekleyin:
Groovy
dependencies { // CameraX core library using the camera2 implementation def camerax_version = "1.5.0-beta01" // The following line is optional, as the core library is included indirectly by camera-camera2 implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" // If you want to additionally use the CameraX Lifecycle library implementation "androidx.camera:camera-lifecycle:${camerax_version}" // If you want to additionally use the CameraX VideoCapture library implementation "androidx.camera:camera-video:${camerax_version}" // If you want to additionally use the CameraX View class implementation "androidx.camera:camera-view:${camerax_version}" // If you want to additionally add CameraX ML Kit Vision Integration implementation "androidx.camera:camera-mlkit-vision:${camerax_version}" // If you want to additionally use the CameraX Extensions library implementation "androidx.camera:camera-extensions:${camerax_version}" }
Kotlin
dependencies { // CameraX core library using the camera2 implementation val camerax_version = "1.5.0-beta01" // The following line is optional, as the core library is included indirectly by camera-camera2 implementation("androidx.camera:camera-core:${camerax_version}") implementation("androidx.camera:camera-camera2:${camerax_version}") // If you want to additionally use the CameraX Lifecycle library implementation("androidx.camera:camera-lifecycle:${camerax_version}") // If you want to additionally use the CameraX VideoCapture library implementation("androidx.camera:camera-video:${camerax_version}") // If you want to additionally use the CameraX View class implementation("androidx.camera:camera-view:${camerax_version}") // If you want to additionally add CameraX ML Kit Vision Integration implementation("androidx.camera:camera-mlkit-vision:${camerax_version}") // If you want to additionally use the CameraX Extensions library implementation("androidx.camera:camera-extensions:${camerax_version}") }
Uygulamanızı bu koşullara uygun şekilde yapılandırma hakkında daha fazla bilgi için Bağımlılıkları bildirme başlıklı makaleyi inceleyin.
CameraX'in Camera2 ile birlikte çalışabilirliği
CameraX, Camera2 üzerine kurulmuştur ve Camera2 uygulamasında özellikleri okumanın, hatta yazmanın yollarını sunar. Tüm ayrıntılar için Interop paketi başlıklı makaleyi inceleyin.
CameraX'in Camera2 özelliklerini nasıl yapılandırdığı hakkında daha fazla bilgi edinmek için
Camera2CameraInfo
kullanarak temel CameraCharacteristics
'ı okuyun. Ayrıca, temel Camera2 özelliklerini aşağıdaki iki yoldan birinde yazmayı da seçebilirsiniz:
Otomatik odaklama modu gibi temel
Camera2CameraControl
'da özellikleri ayarlamanıza olanak tanıyanCaptureRequest
'ı kullanın.UseCase
CameraX'iCamera2Interop.Extender
ile genişletin. Bu,Camera2CameraControl
gibi CaptureRequest üzerinde özellikler ayarlamanıza olanak tanır. Ayrıca, akış kullanım alanını ayarlayarak kamerayı kullanım senaryonuza göre optimize etme gibi bazı ek kontroller de sunar. Daha fazla bilgi için Daha iyi performans için Stream kullanım alanlarını kullanma başlıklı makaleyi inceleyin.
Aşağıdaki kod örneğinde, video görüşmesi için optimizasyon yapmak üzere akış kullanım alanları kullanılmaktadır.
Görüntülü görüşme akışı kullanım alanının kullanılabilir olup olmadığını getirmek için Camera2CameraInfo
kullanın. Ardından, temel yayın kullanım alanını ayarlamak için Camera2Interop.Extender
simgesini kullanın.
Kotlin
// Set underlying Camera2 stream use case to optimize for video calls. val videoCallStreamId = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong() // Check available CameraInfos to find the first one that supports // the video call stream use case. val frontCameraInfo = cameraProvider.getAvailableCameraInfos() .first { cameraInfo -> val isVideoCallStreamingSupported = Camera2CameraInfo.from(cameraInfo) .getCameraCharacteristic( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES )?.contains(videoCallStreamId) val isFrontFacing = (cameraInfo.getLensFacing() == CameraSelector.LENS_FACING_FRONT) (isVideoCallStreamingSupported == true) && isFrontFacing } val cameraSelector = frontCameraInfo.cameraSelector // Start with a Preview Builder. val previewBuilder = Preview.Builder() .setTargetAspectRatio(screenAspectRatio) .setTargetRotation(rotation) // Use Camera2Interop.Extender to set the video call stream use case. Camera2Interop.Extender(previewBuilder).setStreamUseCase(videoCallStreamId) // Bind the Preview UseCase and the corresponding CameraSelector. val preview = previewBuilder.build() camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)
Java
// Set underlying Camera2 stream use case to optimize for video calls. Long videoCallStreamId = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong(); // Check available CameraInfos to find the first one that supports // the video call stream use case. List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos(); CameraInfo frontCameraInfo = null; for (cameraInfo in cameraInfos) { Long[] availableStreamUseCases = Camera2CameraInfo.from(cameraInfo) .getCameraCharacteristic( CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES ); boolean isVideoCallStreamingSupported = Arrays.List(availableStreamUseCases) .contains(videoCallStreamId); boolean isFrontFacing = (cameraInfo.getLensFacing() == CameraSelector.LENS_FACING_FRONT); if (isVideoCallStreamingSupported && isFrontFacing) { frontCameraInfo = cameraInfo; } } if (frontCameraInfo == null) { // Handle case where video call streaming is not supported. } CameraSelector cameraSelector = frontCameraInfo.getCameraSelector(); // Start with a Preview Builder. Preview.Builder previewBuilder = Preview.Builder() .setTargetAspectRatio(screenAspectRatio) .setTargetRotation(rotation); // Use Camera2Interop.Extender to set the video call stream use case. Camera2Interop.Extender(previewBuilder).setStreamUseCase(videoCallStreamId); // Bind the Preview UseCase and the corresponding CameraSelector. Preview preview = previewBuilder.build() Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)
Ek kaynaklar
CameraX hakkında daha fazla bilgi edinmek için aşağıdaki ek kaynaklara göz atın.
Codelab
Kod örneği