Android Kodunuzu Geleceğe Hazırlayın, Bölüm 1: İşlevsel ve Reaktif Programlama Temelleri

Yayınlanan: 2022-08-31

Temiz kod yazmak zor olabilir: Kitaplıklar, çerçeveler ve API'ler geçicidir ve hızla geçersiz hale gelir. Ancak matematiksel kavramlar ve paradigmalar kalıcıdır; yıllarca akademik araştırma gerektiriyorlar ve hatta bizden daha uzun ömürlü olabilirler.

Bu, Kitaplık Y ile X'i nasıl yapacağınızı gösteren bir öğretici değildir. Bunun yerine, geleceğe dönük ve güvenilir Android mimarisi oluşturabilmeniz ve ödün vermeden değişikliklere ölçeklendirip uyum sağlayabilmeniz için işlevsel ve reaktif programlamanın ardındaki kalıcı ilkelere odaklanıyoruz. yeterlik.

Bu makale temelleri atıyor ve Bölüm 2'de hem işlevsel hem de reaktif programlamayı birleştiren bir işlevsel reaktif programlama (FRP) uygulamasına dalacağız.

Bu makale Android geliştiricileri düşünülerek yazılmıştır, ancak kavramlar genel programlama dillerinde deneyime sahip herhangi bir geliştirici için alakalı ve faydalıdır.

Fonksiyonel Programlama 101

İşlevsel programlama (FP), programınızı, istenen çıktı elde edilene kadar verileri $A$'dan $B$'a, $C$'a vb. dönüştürerek işlevlerin bir bileşimi olarak oluşturduğunuz bir kalıptır. Nesne yönelimli programlamada (OOP), bilgisayara talimatla talimatla ne yapacağını söylersiniz. Fonksiyonel programlama farklıdır: Kontrol akışından vazgeçersiniz ve bunun yerine sonucunuzu üretmek için bir “fonksiyon tarifi” tanımlarsınız.

Soldaki "Giriş: x" metnini içeren yeşil bir dikdörtgende, "İşlev: f" etiketli açık gri bir dikdörtgeni gösteren bir ok bulunur. Açık gri dikdörtgenin içinde, okları sağa dönük üç silindir vardır: Birincisi, "A(x)" etiketli açık mavi, ikincisi "B(x)" etiketli koyu mavi ve üçüncüsü "C" etiketli koyu gri. (x)." Açık gri dikdörtgenin sağ tarafında "Çıktı: f(x)" metnini içeren yeşil bir dikdörtgen vardır. Açık gri dikdörtgenin alt kısmında "Yan etkiler" metnini gösteren bir ok vardır.
Fonksiyonel programlama modeli

FP, matematikten, özellikle de bir fonksiyon soyutlama mantık sistemi olan lambda hesabından kaynaklanır. Döngüler, sınıflar, polimorfizm veya kalıtım gibi OOP kavramları yerine, FP kesinlikle soyutlama ve daha yüksek dereceli fonksiyonlarla, diğer fonksiyonları girdi olarak kabul eden matematiksel fonksiyonlarla ilgilenir.

Özetle, FP'nin iki ana "oyuncusu" vardır: veriler (sorununuz için gerekli model veya bilgi) ve işlevler (veriler arasındaki davranış ve dönüşümlerin temsilleri). Buna karşılık, OOP sınıfları, belirli bir alana özgü veri yapısını ve her sınıf örneğiyle ilişkili değerleri veya durumu, onunla kullanılması amaçlanan davranışlara (yöntemlere) açıkça bağlar.

FP'nin üç temel yönünü daha yakından inceleyeceğiz:

  • FP bildirimseldir.
  • FP, işlev bileşimini kullanır.
  • FP fonksiyonları saftır.

FP dünyasına daha fazla dalmak için iyi bir başlangıç ​​noktası, güçlü bir şekilde yazılmış, tamamen işlevsel bir dil olan Haskell'dir. Büyük İyilik için Size Bir Haskell Öğrenin'i tavsiye ederim! Yararlı bir kaynak olarak etkileşimli öğretici.

FP Bileşen 1: Bildirime Dayalı Programlama

Bir FP programı hakkında farkedeceğiniz ilk şey, onun zorunlu değil, bildirimsel bir tarzda yazılmış olmasıdır. Kısacası, bildirimsel programlama, bir programa nasıl yapılacağı yerine ne yapılması gerektiğini söyler. Bu soyut tanımı, aşağıdaki sorunu çözmek için zorunlu ve bildirimsel programlamanın somut bir örneğiyle temellendirelim: Bir ad listesi verildiğinde, yalnızca en az üç sesli harf içeren ve sesli harfleri büyük harflerle gösterilen adları içeren bir liste döndürün.

Zorunlu Çözüm

İlk olarak, bu sorunun Kotlin'deki zorunlu çözümünü inceleyelim:

 fun namesImperative(input: List<String>): List<String> { val result = mutableListOf<String>() val vowels = listOf('A', 'E', 'I', 'O', 'U','a', 'e', 'i', 'o', 'u') for (name in input) { // loop 1 var vowelsCount = 0 for (char in name) { // loop 2 if (isVowel(char, vowels)) { vowelsCount++ if (vowelsCount == 3) { val uppercaseName = StringBuilder() for (finalChar in name) { // loop 3 var transformedChar = finalChar // ignore that the first letter might be uppercase if (isVowel(finalChar, vowels)) { transformedChar = finalChar.uppercaseChar() } uppercaseName.append(transformedChar) } result.add(uppercaseName.toString()) break } } } } return result } fun isVowel(char: Char, vowels: List<Char>): Boolean { return vowels.contains(char) } fun main() { println(namesImperative(listOf("Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"))) // [IlIyAn, AnnAbEl, NIcOlE] }

Şimdi, birkaç önemli geliştirme faktörünü göz önünde bulundurarak zorunlu çözümümüzü analiz edeceğiz:

  • En verimli: Bu çözüm optimum bellek kullanımına sahiptir ve Big O analizinde iyi performans gösterir (minimum karşılaştırma sayısına göre). Bu algoritmada, karakterler arasındaki karşılaştırma sayısını analiz etmek mantıklıdır çünkü algoritmamızda baskın olan işlem budur. $n$ isimlerin sayısı ve $k$ isimlerin ortalama uzunluğu olsun.

    • En kötü durum karşılaştırma sayısı: $n(10k)(10k) = 100nk^2$
    • Açıklama: $n$ (döngü 1) * $10k$ (her karakter için, 10 olası sesli harfle karşılaştırırız) * $10k$ (karakterin büyük harf olup olmayacağına karar vermek için isVowel() kontrolünü tekrar uygularız - yine, en kötü durumda, bu 10 sesli harfle karşılaştırılır).
    • Sonuç: Ortalama isim uzunluğu 100 karakterden fazla olmayacağı için algoritmamızın $O(n)$ zamanında çalıştığını söyleyebiliriz.
  • Okunabilirliği zayıf olan karmaşık: Daha sonra ele alacağımız bildirimsel çözümle karşılaştırıldığında, bu çözüm çok daha uzun ve takip etmesi daha zor.
  • Hataya açık: Kod result , vowelsCount ve transformedChar Karakteri değiştirir; bu durum mutasyonları vowelsCount tekrar 0'a sıfırlamayı unutmak gibi ince hatalara yol açabilir. Yürütme akışı da karmaşık hale gelebilir ve üçüncü döngüde break ifadesini eklemeyi unutmak kolaydır.
  • Kötü sürdürülebilirlik: Kodumuz karmaşık ve hataya açık olduğundan, bu kodun davranışını yeniden düzenlemek veya değiştirmek zor olabilir. Örneğin, problem üç sesli ve beş sessiz harfli isimleri seçmek için değiştirilseydi, yeni değişkenler eklememiz ve döngüleri değiştirmemiz gerekecekti, bu da hatalar için birçok fırsat bırakacaktı.

Örnek çözümümüz, kodu daha küçük işlevlere dönüştürerek iyileştirebilmenize rağmen, zorunlu kodun ne kadar karmaşık görünebileceğini gösterir.

Bildirim Çözümü

Artık bildirimsel programlamanın ne olmadığını anladığımıza göre, Kotlin'deki bildirimsel çözümümüzü açıklayalım:

 fun namesDeclarative(input: List<String>): List<String> = input.filter { name -> name.count(::isVowel) >= 3 }.map { name -> name.map { char -> if (isVowel(char)) char.uppercaseChar() else char }.joinToString("") } fun isVowel(char: Char): Boolean = listOf('A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u').contains(char) fun main() { println(namesDeclarative(listOf("Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"))) // [IlIyAn, AnnAbEl, NIcOlE] }

Zorunlu çözümümüzü değerlendirmek için kullandığımız kriterleri kullanarak, bildirim kodunun nasıl işlediğini görelim:

  • Verimli: Emir ve bildirim uygulamalarının her ikisi de doğrusal zamanda çalışır, ancak zorunlu olan biraz daha verimli çünkü burada name.count() kullandım, bu ismin sonuna kadar sesli harfleri saymaya devam edecek (üç sesli harf bulduktan sonra bile) ). Basit bir hasThreeVowels(String): Boolean function yazarak bu sorunu kolayca çözebiliriz. Bu çözüm, zorunlu çözümle aynı algoritmayı kullanır, dolayısıyla aynı karmaşıklık analizi burada da geçerlidir: Algoritmamız $O(n)$ zamanında çalışır.
  • İyi okunabilirlik ile özlü: Zorunlu çözüm, bildirime dayalı çözümümüzün küçük girintili 16 satırlık uzunluğuna kıyasla büyük girintili 44 satırdır. Çizgiler ve sekmeler her şey değildir, ancak iki dosyaya bir bakışta bildirimsel çözümümüzün çok daha okunabilir olduğu açıktır.
  • Daha az hataya açık: Bu örnekte her şey değişmez. Tüm isimlerin bir <Dize List<String> List<String> , üç veya daha fazla sesli harf içeren bir <Dize> Listesine dönüştürürüz ve ardından her String kelimesini büyük sesli harfli bir String kelimesine dönüştürürüz. Genel olarak, mutasyon, iç içe döngüler veya kesintiler olmaması ve kontrol akışından vazgeçmesi, hata için daha az yer ile kodu daha basit hale getirir.
  • İyi sürdürülebilirlik: Okunabilirliği ve sağlamlığı nedeniyle bildirim kodunu kolayca yeniden düzenleyebilirsiniz. Önceki örneğimizde (diyelim ki problem , üç sesli ve beş sessiz harfli isimleri seçmek için değiştirildi), basit bir çözüm, filter koşuluna aşağıdaki ifadeleri eklemek olabilir: val vowels = name.count(::isVowel); vowels >= 3 && name.length - vowels >= 5 val vowels = name.count(::isVowel); vowels >= 3 && name.length - vowels >= 5 .

Ek bir olumlu olarak, bildirimsel çözümümüz tamamen işlevseldir: Bu örnekteki her işlev saftır ve hiçbir yan etkisi yoktur. (Saflık hakkında daha sonra.)

Bonus Bildirim Çözümü

Nasıl okunduğunu göstermek için aynı sorunun Haskell gibi tamamen işlevsel bir dilde bildirimsel uygulamasına bir göz atalım. Haskell'e aşina değilseniz, . Haskell'deki operatör “sonra” olarak okur. Örneğin, solution = map uppercaseVowels . filter hasThreeVowels solution = map uppercaseVowels . filter hasThreeVowels , "üç sesli harf içeren adları filtreledikten sonra ünlüleri büyük harfe eşle" anlamına gelir.

 import Data.Char(toUpper) namesSolution :: [String] -> [String] namesSolution = map uppercaseVowels . filter hasThreeVowels hasThreeVowels :: String -> Bool hasThreeVowels s = count isVowel s >= 3 uppercaseVowels :: String -> String uppercaseVowels = map uppercaseVowel where uppercaseVowel :: Char -> Char uppercaseVowel c | isVowel c = toUpper c | otherwise = c isVowel :: Char -> Bool isVowel c = c `elem` vowels vowels :: [Char] vowels = ['A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u'] count :: (a -> Bool) -> [a] -> Int count _ [] = 0 count pred (x:xs) | pred x = 1 + count pred xs | otherwise = count pred xs main :: IO () main = print $ namesSolution ["Iliyan", "Annabel", "Nicole", "John", "Anthony", "Ben", "Ken"] -- ["IlIyAn","AnnAbEl","NIcOlE"]

Bu çözüm, bazı ek avantajlarla birlikte Kotlin bildirim çözümümüze benzer şekilde çalışır: Haskell'in sözdizimini anlarsanız okunabilir, basit, tamamen işlevsel ve tembeldir.

Önemli Çıkarımlar

Bildirimsel programlama, hem FP hem de Reaktif Programlama için faydalıdır (daha sonraki bir bölümde ele alacağız).

  • İfadelerin tam olarak yürütülme sırası ile "nasıl" başaracağınızdan ziyade "ne" elde etmek istediğinizi açıklar.
  • Bir programın kontrol akışını soyutlar ve bunun yerine probleme dönüşümler açısından odaklanır (yani, $A \rightarrow B \rightarrow C \rightarrow D$).
  • Yeniden düzenlemesi ve değiştirmesi daha kolay olan daha az karmaşık, daha özlü ve daha okunabilir kodu teşvik eder. Android kodunuz bir cümle gibi okunmuyorsa, muhtemelen yanlış bir şey yapıyorsunuzdur.

Android kodunuz bir cümle gibi okunmuyorsa, muhtemelen yanlış bir şey yapıyorsunuzdur.

Cıvıldamak

Yine de, bildirimsel programlamanın bazı dezavantajları vardır. Daha fazla RAM tüketen ve zorunlu bir uygulamadan daha kötü performans gösteren verimsiz kodla sonuçlanmak mümkündür. Sıralama, geri yayılım (makine öğreniminde) ve diğer "mutasyona uğratan algoritmalar", değişmez, bildirimsel programlama stili için uygun değildir.

FP Bileşen #2: İşlev Bileşimi

Fonksiyon kompozisyonu, fonksiyonel programlamanın kalbindeki matematiksel kavramdır. $f$ işlevi girdi olarak $A$'ı kabul eder ve çıktı olarak $B$ üretirse ($f: A \rightarrow B$) ve $g$ işlevi $B$'ı kabul eder ve $C$ ($g: B) üretirse \rightarrow C$), sonra $A$'ı kabul eden ve $C$ ($h: A \rightarrow C$) üreten $h$ adlı üçüncü bir işlev oluşturabilirsiniz. Bu üçüncü işlevi $g$'ın $f$ ile bileşimi olarak tanımlayabiliriz, ayrıca $g \circ f$ veya $g(f())$ olarak da gösterilir:

"A" etiketli mavi bir kutuda bir ok vardır, "f", "B" etiketli mavi bir kutuyu işaret eder ve "g", "C" etiketli mavi bir kutuyu gösterir. "A" kutusunda ayrıca doğrudan "C" kutusunu işaret eden "g of f" paralel bir ok vardır.
f, g ve h fonksiyonları, g'nin f ile bileşimi.

Her zorunlu çözüm, problemi daha küçük problemlere ayırarak, bunları bağımsız olarak çözerek ve daha küçük çözümleri fonksiyon kompozisyonu aracılığıyla nihai çözüme yeniden birleştirerek bildirimsel bir çözüme dönüştürülebilir. Bu kavramı eylem halinde görmek için önceki bölümden isim problemimize bakalım. Zorunlu çözümden daha küçük sorunlarımız:

  1. isVowel :: Char -> Bool : Bir Char verildiğinde, sesli harf olup olmadığını döndürür ( Bool ).
  2. countVowels :: String -> Int : Bir String verildiğinde, içindeki sesli harf sayısını döndürür ( Int ).
  3. hasThreeVowels :: String -> Bool : Bir String verildiğinde, en az üç sesli harf olup olmadığını döndürür ( Bool ).
  4. uppercaseVowels :: String -> String : Bir String verildiğinde, büyük harfli yeni bir String döndür.

İşlev bileşimi aracılığıyla elde edilen bildirimsel çözümümüz, map uppercaseVowels . filter hasThreeVowels map uppercaseVowels . filter hasThreeVowels .

Bir üst diyagramda, sağa işaret eden oklarla birbirine bağlanan üç mavi "[String]" kutusu vardır. İlk ok "filtre has3Vowels" olarak etiketlenir ve ikincisi "harita büyük harfVowels" olarak etiketlenir. Aşağıda, ikinci bir diyagramda solda iki mavi kutu vardır, üstte "Char" ve aşağıda "String", sağda mavi bir kutuya işaret eder, "Bool". "Char"dan "Bool"a giden ok "isVowel" olarak etiketlenir ve "String"den "Bool"a giden ok "has3Vowels" olarak etiketlenir. "Dize" kutusunda ayrıca "büyük harfli Sesler" olarak etiketlenmiş kendisini gösteren bir ok bulunur.
İsim problemimizi kullanan bir fonksiyon kompozisyonu örneği.

Bu örnek, basit bir $A \rightarrow B \rightarrow C$ formülünden biraz daha karmaşıktır, ancak fonksiyon kompozisyonunun arkasındaki prensibi gösterir.

Önemli Çıkarımlar

Fonksiyon kompozisyonu basit ama güçlü bir kavramdır.

  • Problemlerin daha küçük, daha basit adımlara bölündüğü ve tek bir çözümde birleştirildiği karmaşık problemleri çözmek için bir strateji sağlar.
  • Herhangi bir şeyi kırma konusunda endişelenmeden nihai çözümün parçalarını kolayca eklemenize, çıkarmanıza veya değiştirmenize olanak tanıyan yapı taşları sağlar.
  • $f$ çıktısı $g$ girdi türüyle eşleşirse $g(f())$ oluşturabilirsiniz.

İşlevleri oluştururken, yalnızca verileri değil, aynı zamanda diğer işlevlere girdi olarak işlevleri de iletebilirsiniz - daha yüksek dereceli işlevlere bir örnek.

FP Bileşeni #3: Saflık

İşlev bileşimi için ele almamız gereken bir anahtar öğe daha var: Oluşturduğunuz işlevler saf olmalıdır, matematikten türetilen başka bir kavram. Matematikte tüm işlevler, aynı girdiyle çağrıldıklarında her zaman aynı çıktıyı veren hesaplamalardır; saflığın temeli budur.

Matematik fonksiyonlarını kullanan bir sözde kod örneğine bakalım. Bir tamsayı girdisini çift yapmak için iki katına çıkaran ve makeEven(x) + x satırını x = 2 girdisini kullanarak çalıştıran makeEven bir fonksiyonumuz olduğunu varsayalım. Matematikte, bu hesaplama her zaman 2x + x = 3x = 3(2) = 6$ hesaplamasına dönüşür ve saf bir fonksiyondur. Ancak bu, programlamada her zaman doğru değildir—eğer makeEven(x) işlevi, kod sonucumuzu döndürmeden önce onu ikiye katlayarak x mutasyona uğrattıysa, satırımız 2x + (2x) = 4x = 4(2) = 8$ hesaplayacaktır. ve daha da kötüsü, her makeEven çağrısında sonuç değişecekti.

Saf olmayan ancak saflığı daha spesifik olarak tanımlamamıza yardımcı olacak birkaç fonksiyon türünü inceleyelim:

  • Kısmi işlevler: Bunlar, bölme gibi tüm giriş değerleri için tanımlanmayan işlevlerdir. Programlama açısından, bunlar bir istisna oluşturan işlevlerdir: fun divide(a: Int, b: Int): Float , sıfıra bölmenin neden olduğu b = 0 girişi için bir ArithmeticException oluşturur.
  • Toplam fonksiyonlar: Bu fonksiyonlar tüm girdi değerleri için tanımlanır ancak aynı girdi ile çağrıldığında farklı bir çıktı veya yan etkiler üretebilir. Android dünyası toplam işlevlerle doludur: Log.d , LocalDateTime.now ve Locale.getDefault sadece birkaç örnektir.

Bu tanımları göz önünde bulundurarak, saf fonksiyonları hiçbir yan etkisi olmayan toplam fonksiyonlar olarak tanımlayabiliriz. Yalnızca saf işlevler kullanılarak oluşturulan işlev bileşimleri daha güvenilir, öngörülebilir ve test edilebilir kodlar üretir.

İpucu: Bir toplam işlevi saf hale getirmek için, yan etkilerini daha yüksek dereceli bir işlev parametresi olarak ileterek soyutlayabilirsiniz. Bu şekilde, sahte bir üst düzey işlevi geçerek toplam işlevleri kolayca test edebilirsiniz. Bu örnek, Ivy FRP öğreticisinde daha sonra inceleyeceğimiz bir kitaplıktan @SideEffect ek açıklamasını kullanır:

 suspend fun deadlinePassed( deadline: LocalDate, @SideEffect currentDate: suspend () -> LocalDate ): Boolean = deadline.isAfter(currentDate())

Önemli Çıkarımlar

Saflık, işlevsel programlama paradigması için gerekli olan son bileşendir.

  • Kısmi işlevlere dikkat edin; uygulamanızı çökertebilirler.
  • Toplam fonksiyonları oluşturmak deterministik değildir; öngörülemeyen davranışlar üretebilir.
  • Mümkün olduğunda, saf işlevler yazın. Artan kod kararlılığından yararlanacaksınız.

İşlevsel programlamaya genel bakışımız tamamlandıktan sonra, geleceğe yönelik Android kodunun bir sonraki bileşenini inceleyelim: reaktif programlama.

Reaktif Programlama 101

Reaktif programlama, programın değişiklikler hakkında bilgi istemek yerine verilere veya olay değişikliklerine tepki verdiği bildirimsel bir programlama modelidir.

İki ana mavi kutu, "Gözlenebilir" ve "Durum", aralarında iki ana yola sahiptir. İlki "Gözlemler (değişiklikleri dinler)" aracılığıyladır. İkincisi, "Bildirir (en son durum)," mavi kutusuna "UI (arka uçta API)", "Kullanıcı girdisini dönüştürür", "Tetikleyiciler" aracılığıyla maviye giden mavi kutu "Olay"a gider. "İşlev bileşimi" kutusu ve son olarak "Üretir (yeni durum)" aracılığıyla. "Devlet" daha sonra "Girdi olarak davranır" aracılığıyla "Fonksiyon bileşimine" geri bağlanır.
Genel reaktif programlama döngüsü.

Bir reaktif programlama döngüsündeki temel unsurlar olaylar, bildirimsel ardışık düzen, durumlar ve gözlemlenebilirlerdir:

  • Olaylar , güncellemeleri tetikleyen, tipik olarak kullanıcı girişi veya sistem olayları biçimindeki dış dünyadan gelen sinyallerdir. Bir olayın amacı, bir sinyali boru hattı girdisine dönüştürmektir.
  • Bildirimsel ardışık düzen , (Event, State) girdi olarak kabul eden ve bu girdiyi yeni bir State (çıktı) dönüştüren bir işlev bileşimidir: (Event, State) -> f -> g -> … -> n -> State . İşlem hatları, diğer işlem hatlarını engellemeden veya bitmesini beklemeden birden çok olayı işlemek için zaman uyumsuz olarak gerçekleştirmelidir.
  • Durumlar , zaman içinde belirli bir noktada yazılım uygulamasının veri modelinin temsilidir. Etki alanı mantığı, istenen sonraki durumu hesaplamak ve ilgili güncellemeleri yapmak için durumu kullanır.
  • Gözlenebilirler , durum değişikliklerini dinler ve aboneleri bu değişikliklerle ilgili olarak günceller. Android'de, gözlemlenebilirler genellikle Flow , LiveData veya RxJava kullanılarak uygulanır ve buna göre tepki verebilmesi için kullanıcı arayüzüne durum güncellemelerini bildirir.

Reaktif programlamanın birçok tanımı ve uygulaması vardır. Burada, bu kavramları gerçek projelere uygulamaya odaklanan pragmatik bir yaklaşım benimsiyorum.

Noktaları Birleştirme: Fonksiyonel Reaktif Programlama

Fonksiyonel ve reaktif programlama iki güçlü paradigmadır. Bu kavramlar, kitaplıkların ve API'lerin kısa ömürlü ömrünün ötesine geçer ve gelecek yıllarda programlama becerilerinizi geliştirecektir.

Ayrıca, FP ve reaktif programlamanın gücü birleştiğinde katlanarak artıyor. Artık işlevsel ve reaktif programlamanın net tanımlarına sahip olduğumuza göre, parçaları bir araya getirebiliriz. Bu öğreticinin 2. bölümünde, işlevsel reaktif programlama (FRP) paradigmasını tanımlıyor ve bunu örnek bir uygulama uygulaması ve ilgili Android kitaplıkları ile uygulamaya koyuyoruz.

Toptal Engineering Blog, bu makalede sunulan kod örneklerini incelediği için Tarun Goyal'a şükranlarını sunar.