Merhaba saygıdeğer developerlar. Bu yazımda sizlere daha önceden listesi olan objectlerin kayıtlarının nasıl yapıldığını mümkün mertebe clean architecture yaklaşımı ile göstermeye çalışacağım. Bu ekranda kitap kaydı yaparken;

  1. Runtime permission
  2. Validation
  3. Kamera kullanımı
  4. Resim yükleme
  5. Resimden yazı alma (ML Kit Text Recognation)
  6. Image Cropping

gibi konularada değineceğiz ve günün sonunda fotoğrafını çektiğimiz, bilgilerini girdiğimiz kitabı bir API yardımıyla kaydedeceğiz.

Ortaya çıkan ekran ise aşağıdaki gibidir.

Tüm bu bilgilerden sonra ekranın senaryosuna bakalım.

Ekrandaki tüm alanlar zorunludur. [Validation]. Eklenmek istenen kitabın fotoğrafı çekilmeden önce kamera ve storage izinleri istenecektir. [Permission] Kullanıcı bu izinlere allow vermediği müddetçe kitabın fotoğrafı eklenmeyecek ve o alan validationdan geçemekeyecektir. Fotoğrafı çekilen kitabın imageı croplanabilecek ve böylece imageın kesilen kısmının uploadu yapılacaktır. [Kamera kullanımı, Image Cropping] Kitap hakkında açıklama girilen sahaya ister elle ister kameradan açıklama yazısının fotoğrafını çekip oradan alabilecektir. [ML Kit Text Recognation] Kitap tür ve yayın evi bilgisi ise daha önceden ekranlarını yaptığımız parametre listelerinden gelecektir. Kitap türü yada yayınevi listesi bir kere API’den çekilince tekrar API’ye gidilmeyecek olup Room ile cachelenecektir. Buradaki seçim için de bottomsheet kullanılacak olup generic bir BottomSheetSelection componentinden faydalanılacaktır. Kaydet butonuna basınca ilk olarak kitabın bilgileri servise gönderilecek ve servis bize kitabın ID sini döndüğü için bu ID ile bizde fotoğrafı service ile upload edeceğiz.

Akışımız yukarıdaki şekilde. Dilerseniz artık işin kodlama kısmına geçelim.

Ekranda kullandığım UI componentleri; OutlinedTextField , BottomSheet , Card ve third party bir ImageCrop dan ibarettir. Input componenti tüm uygulamada kullanılabilir olup aşağıdaki gibidir.

Kullanımı ise aşağıdaki gibidir.

Bottomsheet açıp içeriye bir liste paslayarak oradan seçim yaptırdığım (kitap türü ve yayın evi listesi) component ise aşağıdaki gibidir.

Bu componenti aşağıdaki gibi parametre paslayarak kullanabiliyoruz.

Bottomsheet in expand olmuş hali aşağıdaki gibidir.

Filteleme case

Bu listede seçilen itemın indexi onSelect lambdasına paslanır ve böylece listenin string e maplenmemiş halindeki selected item ı alınabilir hale gelir.

Permission

Ekranda kamera kullanımı olduğu için permission request olacağından bahsetmiştim. Composeda genelde runtime permission istemek için accompanist in kütüphanesinden faydalanılır. Tabi o da bir tercih fakat ben activityResult kullanmayı tercih ettim. Hem single hemde multiple permission requestlerinin kullanımı aşağıdaki gibidir. Burada Phlipp Lackner ‘ a teşekkür etmeden geçmeyeceğim. :-)

Burada permission un granted , denied gibi durumlarında ne olacağını ekranda ne olup biteceği kontrolünü tamamen viewmodel de state i update ederek belirliyorum. Örneğin izni permanently denied yaptıysa bir popup gösterip uygulamaya ayarlar kısmından kamera izni vermesi gerektiğini söylüyorum. İşte bu popup ın show/hide olması ise tamamen viewmodel de setleniyor.

fun onCameraPermissionResult(
isGranted: Boolean,
isPermanantlyDenied: Boolean,
cameraOpenType: CameraOpenType
) {
if (isGranted) {
onKitapEklemeEvent(
KitapEklemeEvent.KitapResimEklemeOpenClose(
isOpen = true,
cameraOpenType = cameraOpenType
)
)
} else {
handlePermanantlyCameraPermission(isPermanantlyDenied)
}
}

Ekrandaki tüm actionları bir selaed classta tutuyor ve tüm değerleri bu class yardımı ile setliyorum.

Bu classı birazdan validation yaparken de kullanacağız. Çünkü ekranın tüm state actionları bu selaed classta tutulmaktadır. Bu event classı için ise viewmodelde tek bir function kullanılıyor olup , bu function UI da tetiklenmekte ve when caseleri ile state update olmaktadır. Tıpkı şöyle;

val onChangeYazarAd = remember<(String) -> Unit> {
{
viewModel.onKitapEklemeEvent(KitapEklemeEvent.YazarAdTextChange(it))
}
}


fun onKitapEklemeEvent(event: KitapEklemeEvent) {
when (event) {
....
is KitapEklemeEvent.YazarAdTextChange -> {
_state.update {
it.copy(
yazarAd = event.yazarAd,
yazarAdError = null
)
}
}
....
}

Validation

Aslında bu serinin ilk yazısında validation için kullandığım metodolojiden çok kısa bahsetmiştim.

Burada temel mantık validationları reusable hale getirmek yani bir nevi usacaseler yazmaktır. Bu usecaseler akış ile ilgili kod blokları içerdiğinden domain layer da tutulmalıdır. Hatta multi module bir proje yazıyorsanız bir domain module u açıp use-caselerinizi orada toplamanız faydalı olacaktır.

Bu feature aşağıdaki gibi bir package yapısına sahiptir.

data-domain-presentation

use_case package cı içerisinde gördüğünüz classların isminde ValidationUsecase içeren tüm classlar formun validationu ile alakalıdır. Fakat burada dikkatinizi çekmek istediğim nokta tüm use-caseleri tek tek viewmodel e inject etmek yerine KitapEklemeFormValidationUseCase classına inject edip resultı oradan almak ve sadece KitapEklemeFormValidationUseCase classını viewmodel e inject etmektir. Aksi takdirde daha büyük ve daha karmaşık akışlarda viewmodelin constructoru kalabalıklaşacak ve çirkin bir görüntüye sebep olacaktır.

Burada validation tüm caseler için ya validationSuccess == true ya da false olup, false durumunda bir adet string resource typeında messageResId setlenmesi olayıdır.

Bahsettiğim tüm validationları inject ettiğim class aşağıdaki gibidir.

Viewmodel e inject ettiğimiz bu validation classını aşağıdaki gibi kullanıyoruz.

private fun validateKitapEklemeForm() {
val state = _state.value
val kitapAdValidationResult = kitapEklemeFormValidationUseCase(
KitapEklemeEvent.KitapAdTextChange(
state.kitapAd
)
)
val yazarAdValidationResult = kitapEklemeFormValidationUseCase(
KitapEklemeEvent.YazarAdTextChange(state.yazarAd)
)
val alinmaTarvalidationResult = kitapEklemeFormValidationUseCase(
KitapEklemeEvent.KitapAlinmaTarTextChange(
state.alinmaTar
)
)
val kitapAciklamaValidationResult = kitapEklemeFormValidationUseCase(
KitapEklemeEvent.KitapAciklamaTextChange(
state.kitapAciklama
)
)
val kitapTurValidationResult = kitapEklemeFormValidationUseCase(
KitapEklemeEvent.OnSelectKitapTur(
state.selectedKitapTur
)
)
val yayinEviValidationResult = kitapEklemeFormValidationUseCase(
KitapEklemeEvent.OnSelectYayinEvi(
state.selectedYayinEvi
)
)
val kitapResimValidationResult = kitapEklemeFormValidationUseCase(
KitapEklemeEvent.OnKitapResimCropped(
state.croppedImageBitMap,
state.croppedImageFile
)
)

val hasError = listOf(
kitapAdValidationResult,
yazarAdValidationResult,
alinmaTarvalidationResult,
kitapAciklamaValidationResult,
kitapTurValidationResult,
yayinEviValidationResult,
kitapResimValidationResult
).any {
!it.successfullValidate
}

val mustScrollToKitapTurYayinevi =
kitapResimValidationResult.successfullValidate
&&
kitapAdValidationResult.successfullValidate
&&
yazarAdValidationResult.successfullValidate
&&
alinmaTarvalidationResult.successfullValidate
&&
(kitapTurValidationResult.successfullValidate.not() || yayinEviValidationResult.successfullValidate.not())

if (hasError) {
_state.update {
it.copy(
kitapAdError = kitapAdValidationResult.messageResId,
yazarAdError = yazarAdValidationResult.messageResId,
alinmaTarError = alinmaTarvalidationResult.messageResId,
kitapAciklamaError = kitapAciklamaValidationResult.messageResId,
kitapTurError = kitapTurValidationResult.messageResId,
yayinEviError = yayinEviValidationResult.messageResId,
kitapResimError = kitapResimValidationResult.messageResId,
mustScrollForValidation = mustScrollToKitapTurYayinevi
)
}
} else {
viewModelScope.launch {
kitapKaydet()
}
}
}

hasError true ise success olmayan validation(lar) mevcut demektir. False ise service e request atılır.

Image upload, Crop, ML Text Recognation ve Camera

Image upload işlemi yukarıda ekranın senaryosundada bahsettiğim gibi kitap kaydı yapıldıktan sonra olan işlemdir. Fotoğrafı çekilen ve local e kaydedilen image file ı aşağıdaki gibi upload edilir.

    @Multipart
@POST("api/kitap/resim/yukle")
suspend fun kitapResimYukle(
@Part file: MultipartBody.Part,
@Part("kitapId") kitapId: RequestBody
): Response<ResponseStatusModel>

override suspend fun kitapResimYukle(
kitapResim: File,
kitapId: String
): Response<ResponseStatusModel> {
val kitapIdParam: RequestBody = kitapId.toRequestBody("text/plain".toMediaTypeOrNull())
val fileParam: RequestBody =
kitapResim.asRequestBody("image/jpeg".toMediaTypeOrNull())
val file: MultipartBody.Part =
MultipartBody.Part.createFormData("file", kitapResim.name, fileParam)
return api.kitapResimYukle(file, kitapIdParam)
}

------------------------------------------------------------------------------

kitapResimYukleUseCase(
selectedFile = _state.value.croppedImageFile
?: throw NullPointerException("Lütfen bir resim yükleyiniz"),
kitapId = response.data?.statusMessage
?: throw NullPointerException("Kitap kaydı yapılamamış..."),
).collectLatest { resimYuklemeResponse ->
if (resimYuklemeResponse is BaseResourceEvent.Success) {
_state.value.croppedImageFile?.let {
it.delete()
}
}
_state.update {
it.copy(
kitapResimYukleResourceEvent = resimYuklemeResponse
)
}
}

Burada kitapResimYukleUseCase de dikkat ettiyseniz upload işlemi başarıyla gerçekleştiyse çekilen fotoğrafı delete ediyorum. Buda tamamen tercihinize bağlı olup ister upload olur olmaz, istersenizde bir workmanager yardımı ile günlük delete işlemini yapabilirsiniz.

Tüm kamera işlemlerinde cameraX api yi kullandım. Compose ile nasıl kullanıldığını aşağıdaki yazı dizisinde abimiz güzel anlatmış. Bende oradan (ç)aldım. :D

Oradaki tüm Context extensionlarını kendi repoma alıp capture edilen Image içinde aşağıdaki extensionları (hem text recognation hemde save işlemi için) yazdım.

CameraX i compose da kullanmak malesef kolay değil. :-(. Henüz compose için componentleride yazılmış değil bundan ötürü AndroidView kullanılmış. Resmi çektikten hemen sonra SmartToolFactory nin aşağıdaki kütüphanesi yardımıyla crop ediyorum ve save ediyorum.

https://github.com/SmartToolFactory/Compose-Cropper

Gelelim Text recognationa;

Bu işlem için öncelikle aşağıdaki dependencye ihtiyaç duyulmaktadır.

implementation "com.google.android.gms:play-services-mlkit-text-recognition:18.0.2"

Temelde yapmak istediğim şey aslında bir resim çekip capture eder etmez içindeki texti okuyup, başarılı ise onSuccess functionunu invoke etmek, hata alındıysa da bunun callback etmek. Bu işlem içinde aşağıdaki gibi bir use-case yazdım.

Burada use-case ImageAnalysis.Analyzer interfaceini implement etmekte. Override edilen analyze functionu ise recognize işlemini yapmaktadır. use-case in invoke una gönderdiğim 2 lambda 1 de object var. ImageProxy tipindeki image parametresi capture edilen resim, onReadText lambdası successde invoke olmasını istediğim callback functionum, onFailureReadText ise error durumunda ki callback.

Uygulamanın reposuna aşağıdan ulaşabilirsiniz.

Yazı biraz uzun oldu farkındayım. Okuduğunuz için teşekkür ederim. . Sürçülisan ettiysem affola. Soru ve görüşleriniz için mesutemrecelenk@gmail.com ‘ a mail atabilirsiniz. Sağlıcakla kalın…

--

--